Member page
This commit is contained in:
221
frontend/app/(auth)/dashboard/members/[uuid]/page.tsx
Normal file
221
frontend/app/(auth)/dashboard/members/[uuid]/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
{member.firstname}
|
||||
<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>
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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
56
frontend/lib/request.ts
Normal 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;
|
||||
}
|
||||
@@ -1,43 +1,58 @@
|
||||
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 {
|
||||
eventID: string;
|
||||
title: string;
|
||||
date: string; // Assuming ISO date string
|
||||
eventID: string;
|
||||
title: string;
|
||||
date: string; // Assuming ISO date string
|
||||
}
|
||||
|
||||
// Blog type (you may already have this defined as shown in your previous example)
|
||||
export interface Blog {
|
||||
blogID: string;
|
||||
slug: string;
|
||||
content: string;
|
||||
label?: string;
|
||||
authorID: string;
|
||||
published: string;
|
||||
summary?: string;
|
||||
image?: string;
|
||||
href?: string;
|
||||
blogID: string;
|
||||
slug: string;
|
||||
content: string;
|
||||
label?: string;
|
||||
authorID: string;
|
||||
published: string;
|
||||
summary?: string;
|
||||
image?: string;
|
||||
href?: string;
|
||||
|
||||
author: User; // Relation to User
|
||||
author: User; // Relation to User
|
||||
}
|
||||
|
||||
// User type definition
|
||||
export interface User {
|
||||
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;
|
||||
role: Role; // 'admin' or 'user'
|
||||
createdAt: string; // ISO date string
|
||||
updatedAt: string; // ISO date 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;
|
||||
role: Role; // 'admin' or 'user'
|
||||
createdAt: string; // ISO date string
|
||||
updatedAt: string; // ISO date string
|
||||
|
||||
events?: Event[]; // Many-to-many relation with Event (optional)
|
||||
articles?: Blog[]; // One-to-many relation with Blog (optional)
|
||||
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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user