Member page

This commit is contained in:
cdricms
2025-02-05 09:11:17 +01:00
parent 0595f5dba7
commit 09b0c9b142
12 changed files with 372 additions and 95 deletions

View File

@@ -1,12 +1,15 @@
FROM golang:alpine
FROM golang:alpine AS build
WORKDIR /app
COPY . .
RUN go mod download
RUN go mod tidy
RUN go build main.go
RUN go build -o /app .
CMD ["./main"]
FROM scratch AS final
COPY --from=build /app /app
CMD ["/app"]

View File

@@ -5,6 +5,7 @@ services:
build:
context: ./frontend/
dockerfile: Dockerfile
target: final
depends_on:
- latosa-escrima.fr-backend
env_file: .env

View File

@@ -0,0 +1,221 @@
"use client";
import { UserIcon, Building, X } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Role, User } from "@/types/types";
import { useParams } from "next/navigation";
import { useApi } from "@/hooks/use-api";
import { useState } from "react";
import request from "@/lib/request";
export default function UserDetailsPage() {
const { uuid } = useParams<{ uuid: string }>();
const user = useApi<User>(`/users/${uuid}`, {}, true);
const availableRoles = useApi<Role[]>("/roles", {}, true);
availableRoles.data ??= [];
const [selectedRole, setSelectedRole] = useState<Role | null>(null);
// const [selectedOrg, setSelectedOrg] = useState("");
const addRole = async (role: Role) => {
const res = await request(
`/users/${user.data?.userId}/roles/${role.id}/add`,
{ method: "PATCH", requiresAuth: true },
);
if (res.status === "Success") {
setSelectedRole(null);
user.mutate();
}
};
const removeRole = async (role: Role) => {
const res = await request(
`/users/${user.data?.userId}/roles/${role.id}/remove`,
{ method: "PATCH", requiresAuth: true },
);
if (res.status === "Success") user.mutate();
};
const addOrganization = () => {
// if (selectedOrg && !user.organizations.includes(selectedOrg)) {
// setUser((prevUser) => ({
// ...prevUser,
// organizations: [...prevUser.organizations, selectedOrg],
// }));
// setSelectedOrg("");
// }
};
const removeOrganization = (orgToRemove: string) => {
// setUser((prevUser) => ({
// ...prevUser,
// organizations: prevUser.organizations.filter(
// (org) => org !== orgToRemove,
// ),
// }));
};
if (!user.data || !user.success) return <p>Error</p>;
return (
<div className="container mx-auto py-10">
<Card>
<CardContent className="pt-6">
<div className="grid gap-6">
<div className="flex items-center space-x-4">
<UserIcon className="h-12 w-12 text-gray-400" />
<div>
<h2 className="text-xl font-semibold">
{user.data.firstname} {user.data.lastname}
</h2>
<p className="text-sm text-gray-500">
{user.data.email}
</p>
</div>
</div>
<div className="grid gap-6 md:grid-cols-2">
<div>
<h3 className="text-lg font-semibold mb-2">
Rôles
</h3>
<div className="flex flex-wrap gap-2">
{user.data.roles?.map((role) => (
<Badge
key={role.id}
variant="secondary"
className="text-sm py-1 px-2"
>
{role.name}
<button
onClick={() => removeRole(role)}
className="ml-2 text-gray-500 hover:text-gray-700"
>
<X className="h-3 w-3" />
</button>
</Badge>
))}
</div>
<div className="mt-2 flex space-x-2">
<Select
value={
selectedRole
? selectedRole.name
: ""
}
onValueChange={(s) => {
const r = availableRoles.data?.find(
(r) => r.name === s,
);
console.log(r);
if (r) setSelectedRole(r);
}}
>
<SelectTrigger className="w-[180px]">
<SelectValue placeholder="Select an organization" />
</SelectTrigger>
<SelectContent>
{availableRoles.data
.filter(
(org) =>
!user.data?.roles?.includes(
org,
),
)
.map((role) => (
<SelectItem
key={role.id}
value={role.name}
>
{role.name}
</SelectItem>
))}
</SelectContent>
</Select>
<Button
disabled={!user.data || !selectedRole}
onClick={() => addRole(selectedRole!)}
className="flex items-center"
>
<Building className="mr-2 h-4 w-4" />
Ajouter le rôle
</Button>
</div>
</div>
{/*<div>
<h3 className="text-lg font-semibold mb-2">
Organizations
</h3>
<div className="flex flex-wrap gap-2">
{user.data.organizations.map((org) => (
<Badge
key={org}
variant="outline"
className="text-sm py-1 px-2"
>
{org}
<button
onClick={() =>
removeOrganization(org)
}
className="ml-2 text-gray-500 hover:text-gray-700"
>
<X className="h-3 w-3" />
</button>
</Badge>
))}
</div>
<div className="mt-2 flex space-x-2">
<Select
value={selectedOrg}
onValueChange={setSelectedOrg}
>
<SelectTrigger className="w-[180px]">
<SelectValue placeholder="Select an organization" />
</SelectTrigger>
<SelectContent>
{availableOrganizations
.filter(
(org) =>
!user.organizations.includes(
org,
),
)
.map((org) => (
<SelectItem
key={org}
value={org}
>
{org}
</SelectItem>
))}
</SelectContent>
</Select>
<Button
onClick={addOrganization}
className="flex items-center"
>
<Building className="mr-2 h-4 w-4" />
Add Org
</Button>
</div>
</div> */}
</div>
</div>
</CardContent>
</Card>
</div>
);
}

View File

@@ -16,7 +16,8 @@ import { PhotoDialog } from "@/components/photo-dialog";
import useFileUpload from "@/hooks/use-file-upload";
import useMedia from "@/hooks/use-media";
import Media from "@/interfaces/Media";
import useApiMutation, { request } from "@/hooks/use-api";
import useApiMutation from "@/hooks/use-api";
import request from "@/lib/request";
export default function PhotoGallery() {
const {

View File

@@ -15,7 +15,8 @@ import {
} from "@/components/ui/dialog";
import { ChevronDown, ChevronRight, Plus, Trash2 } from "lucide-react";
import { toTitleCase } from "@/lib/utils";
import { request, useApi } from "@/hooks/use-api";
import { useApi } from "@/hooks/use-api";
import request from "@/lib/request";
type Action = string;

View File

@@ -3,7 +3,8 @@
import { useState } from "react";
import { ShortcodeTable } from "@/components/shortcodes-table";
import type IShortcode from "@/interfaces/IShortcode";
import { request, useApi } from "@/hooks/use-api";
import { useApi } from "@/hooks/use-api";
import request from "@/lib/request";
import { Loader2 } from "lucide-react";
export default function ShortcodesPage() {

View File

@@ -2,12 +2,25 @@ import { ExternalLink } from "lucide-react";
import { Button } from "@/components/ui/button";
import Link from "next/link";
import { API_URL } from "@/lib/constants";
import Image from "next/image";
const Hero = () => {
const background = `${API_URL}/media/591ab183-c72d-46ff-905c-ec04fed1bb34/file`;
return (
<section className="relative flex h-[calc(100vh-68px)] items-center justify-center overflow-hidden py-32">
<div className="">
<div className="magicpattern absolute inset-x-0 top-0 -z-10 flex h-full w-full items-center justify-center bg-blue-50 opacity-100" />
<Image
src={background}
layout="fill"
objectFit="cover"
priority
alt="Hero image"
unoptimized
className="grayscale"
/>
{/* Gradient and Blur Overlay */}
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-transparent to-transparent bg-opacity-30 backdrop-blur-sm"></div>
<div className="mx-auto flex max-w-5xl flex-col items-center">
<div className="z-10 flex flex-col items-center gap-6 text-center">
<img
@@ -17,9 +30,9 @@ const Hero = () => {
/>
<div>
<h1 className="mb-6 text-pretty text-2xl font-bold text-primary lg:text-5xl">
Trouvez votre équilibre
Trouvez votre équilibre avec
<br />
avec Latosa-Escrima
Latosa-Escrima
</h1>
<p className="text-muted-foreground lg:text-xl">
Une évolution des arts martiaux Philippins

View File

@@ -13,8 +13,8 @@ import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import { ScrollArea } from "@/components/ui/scroll-area";
import MemberDialog, { Member } from "./member-dialog";
import * as z from "zod";
import { request, useApi } from "@/hooks/use-api";
import { useApi } from "@/hooks/use-api";
import request from "@/lib/request";
import {
CircleX,
Loader2,
@@ -22,6 +22,7 @@ import {
UserRoundPen,
UserRoundPlus,
} from "lucide-react";
import Link from "next/link";
export default function MembersTable() {
const {
@@ -107,7 +108,7 @@ export default function MembersTable() {
<TableRow>
{selectMode && (
<TableHead className="w-[50px]">
Selectionner
Sélectionner
</TableHead>
)}
<TableHead>Prénom</TableHead>
@@ -140,9 +141,23 @@ export default function MembersTable() {
</TableCell>
)}
<TableCell>
<Link
href={`/dashboard/members/${member.userId}`}
>
<span className="underline">
{member.firstname}
</span>
</Link>
</TableCell>
<TableCell>
<Link
href={`/dashboard/members/${member.userId}`}
>
<span className="underline">
{member.lastname}
</span>
</Link>
</TableCell>
<TableCell>{member.lastname}</TableCell>
<TableCell>{member.email}</TableCell>
<TableCell>{member.phone}</TableCell>
<TableCell>{member.role}</TableCell>

View File

@@ -1,6 +1,7 @@
"use client";
import { ApiResponse, request } from "@/hooks/use-api";
import { ApiResponse } from "@/types/types";
import request from "@/lib/request";
import "@schedule-x/theme-shadcn/dist/index.css";
import { useNextCalendarApp, ScheduleXCalendar } from "@schedule-x/react";
import { createEventsServicePlugin } from "@schedule-x/events-service";

View File

@@ -1,60 +1,9 @@
"use client";
import { API_URL } from "@/lib/constants";
import { getCookie } from "cookies-next";
import request from "@/lib/request";
import { ApiResponse } from "@/types/types";
import useSWR, { SWRConfiguration } from "swr";
import useSWRMutation, { type SWRMutationConfiguration } from "swr/mutation";
export interface ApiResponse<T> {
status: "Error" | "Success";
message: string;
data?: T;
}
export async function request<T>(
endpoint: string,
options: {
method?: "GET" | "POST" | "PATCH" | "DELETE";
body?: any;
requiresAuth?: boolean;
csrfToken?: boolean;
} = {},
): Promise<ApiResponse<T>> {
const { method = "GET", body, requiresAuth = true } = options;
const headers: Record<string, string> = {
"Content-Type": "application/json",
};
if (options.csrfToken) {
const res: ApiResponse<{ csrf: string }> = await (
await fetch(`${API_URL}/csrf-token`)
).json();
if (res.data) headers["X-CSRF-Token"] = res.data.csrf;
}
if (requiresAuth) {
const authToken = getCookie("auth_token");
if (!authToken) {
throw new Error("User is not authenticated");
}
headers.Authorization = `Bearer ${authToken}`;
}
const response = await fetch(`${API_URL}${endpoint}`, {
method,
headers,
body: body ? JSON.stringify(body) : undefined,
credentials: options.csrfToken ? "include" : "omit",
});
const apiResponse: ApiResponse<T> = await response.json();
if (apiResponse.status === "Error") {
throw new Error(apiResponse.message || "An unexpected error occurred");
}
return apiResponse;
}
async function fetcher<T>(
url: string,
requiresAuth: boolean = true,

56
frontend/lib/request.ts Normal file
View File

@@ -0,0 +1,56 @@
import { API_URL } from "@/lib/constants";
import { ApiResponse } from "@/types/types";
import { getCookie } from "cookies-next";
import { ReadonlyRequestCookies } from "next/dist/server/web/spec-extension/adapters/request-cookies";
export default async function request<T>(
endpoint: string,
options: {
method?: "GET" | "POST" | "PATCH" | "DELETE";
body?: any;
requiresAuth?: boolean;
csrfToken?: boolean;
cookies?: () => Promise<ReadonlyRequestCookies>;
} = {},
): Promise<ApiResponse<T>> {
console.log("Hello everyone");
const { method = "GET", body, requiresAuth = true } = options;
const headers: Record<string, string> = {
"Content-Type": "application/json",
};
if (options.csrfToken) {
const res: ApiResponse<{ csrf: string }> = await (
await fetch(`${API_URL}/csrf-token`)
).json();
if (res.data) headers["X-CSRF-Token"] = res.data.csrf;
}
if (requiresAuth) {
let authToken;
if (!options.cookies) {
authToken = getCookie("auth_token");
} else {
authToken = (await options.cookies()).get("auth_token")?.value;
}
if (!authToken) {
throw new Error("User is not authenticated");
}
headers.Authorization = `Bearer ${authToken}`;
}
const response = await fetch(`${API_URL}${endpoint}`, {
method,
headers,
body: body ? JSON.stringify(body) : undefined,
credentials: options.csrfToken ? "include" : "omit",
});
const apiResponse: ApiResponse<T> = await response.json();
if (apiResponse.status === "Error") {
throw new Error(apiResponse.message || "An unexpected error occurred");
}
return apiResponse;
}

View File

@@ -1,8 +1,16 @@
export interface Permission {
resource: string;
action: string;
}
// Role type as a string literal
export type Role = 'admin' | 'user';
export interface Role {
id: string;
name: string;
permissions: Permission[];
}
// Status type as a string literal
export type Status = 'Active' | 'Inactive';
export type Status = "Active" | "Inactive";
// Event type (you can expand this type as needed based on your schema)
export interface Event {
@@ -28,9 +36,9 @@ export interface Blog {
// User type definition
export interface User {
userID: string; // UUID represented as a string
firstName: string;
lastName: string;
userId: string; // UUID represented as a string
firstname: string;
lastname: string;
email: string;
password?: string; // Optional field, since it's omitted in the JSON
phone: string;
@@ -40,4 +48,11 @@ export interface User {
events?: Event[]; // Many-to-many relation with Event (optional)
articles?: Blog[]; // One-to-many relation with Blog (optional)
roles?: Role[];
}
export interface ApiResponse<T> {
status: "Error" | "Success";
message: string;
data?: T;
}