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 WORKDIR /app
COPY . . COPY . .
RUN go mod download 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: build:
context: ./frontend/ context: ./frontend/
dockerfile: Dockerfile dockerfile: Dockerfile
target: final
depends_on: depends_on:
- latosa-escrima.fr-backend - latosa-escrima.fr-backend
env_file: .env 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 useFileUpload from "@/hooks/use-file-upload";
import useMedia from "@/hooks/use-media"; import useMedia from "@/hooks/use-media";
import Media from "@/interfaces/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() { export default function PhotoGallery() {
const { const {

View File

@@ -15,7 +15,8 @@ import {
} from "@/components/ui/dialog"; } from "@/components/ui/dialog";
import { ChevronDown, ChevronRight, Plus, Trash2 } from "lucide-react"; import { ChevronDown, ChevronRight, Plus, Trash2 } from "lucide-react";
import { toTitleCase } from "@/lib/utils"; 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; type Action = string;

View File

@@ -3,7 +3,8 @@
import { useState } from "react"; import { useState } from "react";
import { ShortcodeTable } from "@/components/shortcodes-table"; import { ShortcodeTable } from "@/components/shortcodes-table";
import type IShortcode from "@/interfaces/IShortcode"; 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"; import { Loader2 } from "lucide-react";
export default function ShortcodesPage() { export default function ShortcodesPage() {

View File

@@ -2,12 +2,25 @@ import { ExternalLink } from "lucide-react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import Link from "next/link"; import Link from "next/link";
import { API_URL } from "@/lib/constants";
import Image from "next/image";
const Hero = () => { const Hero = () => {
const background = `${API_URL}/media/591ab183-c72d-46ff-905c-ec04fed1bb34/file`;
return ( return (
<section className="relative flex h-[calc(100vh-68px)] items-center justify-center overflow-hidden py-32"> <section className="relative flex h-[calc(100vh-68px)] items-center justify-center overflow-hidden py-32">
<div className=""> <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="mx-auto flex max-w-5xl flex-col items-center">
<div className="z-10 flex flex-col items-center gap-6 text-center"> <div className="z-10 flex flex-col items-center gap-6 text-center">
<img <img
@@ -17,9 +30,9 @@ const Hero = () => {
/> />
<div> <div>
<h1 className="mb-6 text-pretty text-2xl font-bold text-primary lg:text-5xl"> <h1 className="mb-6 text-pretty text-2xl font-bold text-primary lg:text-5xl">
Trouvez votre équilibre Trouvez votre équilibre avec
<br /> <br />
avec Latosa-Escrima Latosa-Escrima
</h1> </h1>
<p className="text-muted-foreground lg:text-xl"> <p className="text-muted-foreground lg:text-xl">
Une évolution des arts martiaux Philippins 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 { Checkbox } from "@/components/ui/checkbox";
import { ScrollArea } from "@/components/ui/scroll-area"; import { ScrollArea } from "@/components/ui/scroll-area";
import MemberDialog, { Member } from "./member-dialog"; import MemberDialog, { Member } from "./member-dialog";
import * as z from "zod"; import { useApi } from "@/hooks/use-api";
import { request, useApi } from "@/hooks/use-api"; import request from "@/lib/request";
import { import {
CircleX, CircleX,
Loader2, Loader2,
@@ -22,6 +22,7 @@ import {
UserRoundPen, UserRoundPen,
UserRoundPlus, UserRoundPlus,
} from "lucide-react"; } from "lucide-react";
import Link from "next/link";
export default function MembersTable() { export default function MembersTable() {
const { const {
@@ -107,7 +108,7 @@ export default function MembersTable() {
<TableRow> <TableRow>
{selectMode && ( {selectMode && (
<TableHead className="w-[50px]"> <TableHead className="w-[50px]">
Selectionner Sélectionner
</TableHead> </TableHead>
)} )}
<TableHead>Prénom</TableHead> <TableHead>Prénom</TableHead>
@@ -140,9 +141,23 @@ export default function MembersTable() {
</TableCell> </TableCell>
)} )}
<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>
<TableCell>{member.lastname}</TableCell>
<TableCell>{member.email}</TableCell> <TableCell>{member.email}</TableCell>
<TableCell>{member.phone}</TableCell> <TableCell>{member.phone}</TableCell>
<TableCell>{member.role}</TableCell> <TableCell>{member.role}</TableCell>

View File

@@ -1,6 +1,7 @@
"use client"; "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 "@schedule-x/theme-shadcn/dist/index.css";
import { useNextCalendarApp, ScheduleXCalendar } from "@schedule-x/react"; import { useNextCalendarApp, ScheduleXCalendar } from "@schedule-x/react";
import { createEventsServicePlugin } from "@schedule-x/events-service"; import { createEventsServicePlugin } from "@schedule-x/events-service";

View File

@@ -1,60 +1,9 @@
"use client"; "use client";
import { API_URL } from "@/lib/constants"; import request from "@/lib/request";
import { getCookie } from "cookies-next"; import { ApiResponse } from "@/types/types";
import useSWR, { SWRConfiguration } from "swr"; import useSWR, { SWRConfiguration } from "swr";
import useSWRMutation, { type SWRMutationConfiguration } from "swr/mutation"; 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>( async function fetcher<T>(
url: string, url: string,
requiresAuth: boolean = true, 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,43 +1,58 @@
export interface Permission {
resource: string;
action: string;
}
// Role type as a string literal // 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 // 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) // Event type (you can expand this type as needed based on your schema)
export interface Event { export interface Event {
eventID: string; eventID: string;
title: string; title: string;
date: string; // Assuming ISO date string date: string; // Assuming ISO date string
} }
// Blog type (you may already have this defined as shown in your previous example) // Blog type (you may already have this defined as shown in your previous example)
export interface Blog { export interface Blog {
blogID: string; blogID: string;
slug: string; slug: string;
content: string; content: string;
label?: string; label?: string;
authorID: string; authorID: string;
published: string; published: string;
summary?: string; summary?: string;
image?: string; image?: string;
href?: string; href?: string;
author: User; // Relation to User author: User; // Relation to User
} }
// User type definition // User type definition
export interface User { export interface User {
userID: string; // UUID represented as a string userId: string; // UUID represented as a string
firstName: string; firstname: string;
lastName: string; lastname: string;
email: string; email: string;
password?: string; // Optional field, since it's omitted in the JSON password?: string; // Optional field, since it's omitted in the JSON
phone: string; phone: string;
role: Role; // 'admin' or 'user' role: Role; // 'admin' or 'user'
createdAt: string; // ISO date string createdAt: string; // ISO date string
updatedAt: string; // ISO date string updatedAt: string; // ISO date string
events?: Event[]; // Many-to-many relation with Event (optional) events?: Event[]; // Many-to-many relation with Event (optional)
articles?: Blog[]; // One-to-many relation with Blog (optional) articles?: Blog[]; // One-to-many relation with Blog (optional)
roles?: Role[];
}
export interface ApiResponse<T> {
status: "Error" | "Success";
message: string;
data?: T;
} }