diff --git a/frontend/components/admin-layout.tsx b/frontend/components/admin-layout.tsx
new file mode 100644
index 0000000..f97ccdf
--- /dev/null
+++ b/frontend/components/admin-layout.tsx
@@ -0,0 +1,140 @@
+"use client";
+
+import { ReactNode } from "react";
+import Link from "next/link";
+import { usePathname } from "next/navigation";
+import {
+ LayoutDashboard,
+ Users,
+ Tags,
+ Settings,
+ LogOut,
+ Sun,
+ Moon,
+ Shield,
+ BarChart4
+} from "lucide-react";
+
+import { Button } from "@/components/ui/button";
+import {
+ Sidebar,
+ SidebarContent,
+ SidebarFooter,
+ SidebarHeader,
+ SidebarMenu,
+ SidebarMenuItem,
+ SidebarMenuButton,
+ SidebarProvider,
+ SidebarTrigger,
+} from "@/components/ui/sidebar";
+import { useTheme } from "next-themes";
+
+interface AdminLayoutProps {
+ children: ReactNode;
+}
+
+export function AdminLayout({ children }: AdminLayoutProps) {
+ const pathname = usePathname();
+ const { theme, setTheme } = useTheme();
+
+ const navigation = [
+ {
+ name: "Tableau de bord",
+ href: "/admin",
+ icon: LayoutDashboard,
+ },
+ {
+ name: "Utilisateurs",
+ href: "/admin/users",
+ icon: Users,
+ },
+ {
+ name: "Tags globaux",
+ href: "/admin/tags",
+ icon: Tags,
+ },
+ {
+ name: "Statistiques",
+ href: "/admin/stats",
+ icon: BarChart4,
+ },
+ {
+ name: "Paramètres système",
+ href: "/admin/settings",
+ icon: Settings,
+ },
+ ];
+
+ return (
+
+
+
+
+
+
+ Admin
+
+
+
+
+
+ {navigation.map((item) => (
+
+
+
+
+ {item.name}
+
+
+
+ ))}
+
+
+
+
+
+
+
+ Mode utilisateur
+
+
+
+
+ setTheme(theme === "dark" ? "light" : "dark")}
+ aria-label="Toggle theme"
+ >
+ {theme === "dark" ? (
+
+ ) : (
+
+ )}
+
+
+
+
+
+
+
+
+
+
{children}
+
+
+ );
+}
\ No newline at end of file
diff --git a/frontend/components/auth-loading.tsx b/frontend/components/auth-loading.tsx
new file mode 100644
index 0000000..371ea8e
--- /dev/null
+++ b/frontend/components/auth-loading.tsx
@@ -0,0 +1,24 @@
+"use client";
+
+import { useAuth } from "@/lib/auth-context";
+import { Loader2 } from "lucide-react";
+
+interface AuthLoadingProps {
+ children: React.ReactNode;
+}
+
+export function AuthLoading({ children }: AuthLoadingProps) {
+ const { isLoading } = useAuth();
+
+ if (isLoading) {
+ return (
+
+
+
Chargement...
+
Veuillez patienter pendant que nous vérifions votre authentification.
+
+ );
+ }
+
+ return <>{children}>;
+}
\ No newline at end of file
diff --git a/frontend/components/dashboard-layout.tsx b/frontend/components/dashboard-layout.tsx
new file mode 100644
index 0000000..b2cc18f
--- /dev/null
+++ b/frontend/components/dashboard-layout.tsx
@@ -0,0 +1,168 @@
+"use client";
+
+import { ReactNode } from "react";
+import Link from "next/link";
+import { usePathname } from "next/navigation";
+import {
+ LayoutDashboard,
+ Users,
+ FolderKanban,
+ Tags,
+ Settings,
+ LogOut,
+ Sun,
+ Moon,
+ Shield,
+ User
+} from "lucide-react";
+import { useAuth } from "@/lib/auth-context";
+
+import { Button } from "@/components/ui/button";
+import {
+ Sidebar,
+ SidebarContent,
+ SidebarFooter,
+ SidebarHeader,
+ SidebarMenu,
+ SidebarMenuItem,
+ SidebarMenuButton,
+ SidebarProvider,
+ SidebarTrigger,
+} from "@/components/ui/sidebar";
+import { useTheme } from "next-themes";
+
+interface DashboardLayoutProps {
+ children: ReactNode;
+}
+
+export function DashboardLayout({ children }: DashboardLayoutProps) {
+ const pathname = usePathname();
+ const { theme, setTheme } = useTheme();
+ const { user, logout } = useAuth();
+
+ const navigation = [
+ {
+ name: "Tableau de bord",
+ href: "/dashboard",
+ icon: LayoutDashboard,
+ },
+ {
+ name: "Projets",
+ href: "/projects",
+ icon: FolderKanban,
+ },
+ {
+ name: "Personnes",
+ href: "/persons",
+ icon: Users,
+ },
+ {
+ name: "Tags",
+ href: "/tags",
+ icon: Tags,
+ },
+ {
+ name: "Paramètres",
+ href: "/settings",
+ icon: Settings,
+ },
+ ];
+
+ return (
+
+
+
+
+
+ Groupes
+
+
+
+
+
+ {navigation.map((item) => (
+
+
+
+
+ {item.name}
+
+
+
+ ))}
+
+
+
+ {/* User info */}
+ {user && (
+
+
+ {user.avatar ? (
+
+ ) : (
+
+ )}
+
+
+ {user.name}
+ {user.role}
+
+
+ )}
+
+ {/* Admin button */}
+ {user && user.role === 'ADMIN' && (
+
+
+
+
+ Mode administrateur
+
+
+
+ )}
+
+ {/* Theme and logout buttons */}
+
+ setTheme(theme === "dark" ? "light" : "dark")}
+ aria-label="Toggle theme"
+ className="flex items-center justify-center"
+ >
+ {theme === "dark" ? (
+
+ ) : (
+
+ )}
+
+ logout()}
+ className="flex items-center justify-center"
+ >
+
+
+
+
+
+
{children}
+
+
+ );
+}
diff --git a/frontend/components/tag-selector.tsx b/frontend/components/tag-selector.tsx
new file mode 100644
index 0000000..6ce8a59
--- /dev/null
+++ b/frontend/components/tag-selector.tsx
@@ -0,0 +1,218 @@
+"use client";
+
+import { useState, useEffect } from "react";
+import { Badge } from "@/components/ui/badge";
+import { Button } from "@/components/ui/button";
+import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem } from "@/components/ui/command";
+import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
+import { Check, ChevronsUpDown, X } from "lucide-react";
+import { cn } from "@/lib/utils";
+
+// Map color names to Tailwind classes
+const colorMap: Record = {
+ blue: "bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-300",
+ green: "bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300",
+ purple: "bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-300",
+ pink: "bg-pink-100 text-pink-800 dark:bg-pink-900 dark:text-pink-300",
+ orange: "bg-orange-100 text-orange-800 dark:bg-orange-900 dark:text-orange-300",
+ yellow: "bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-300",
+ amber: "bg-amber-100 text-amber-800 dark:bg-amber-900 dark:text-amber-300",
+ red: "bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-300",
+};
+
+export interface Tag {
+ id: number;
+ name: string;
+ description: string;
+ color: string;
+}
+
+interface TagSelectorProps {
+ selectedTags: Tag[];
+ onChange: (tags: Tag[]) => void;
+ placeholder?: string;
+ disabled?: boolean;
+ className?: string;
+}
+
+export function TagSelector({
+ selectedTags = [],
+ onChange,
+ placeholder = "Sélectionner des tags...",
+ disabled = false,
+ className,
+}: TagSelectorProps) {
+ const [open, setOpen] = useState(false);
+ const [tags, setTags] = useState([]);
+ const [loading, setLoading] = useState(true);
+
+ // Mock data for tags - in a real app, this would be fetched from an API
+ useEffect(() => {
+ const fetchTags = async () => {
+ setLoading(true);
+ try {
+ // Simulate API call
+ await new Promise(resolve => setTimeout(resolve, 500));
+
+ // Mock data
+ const mockTags = [
+ {
+ id: 1,
+ name: "Frontend",
+ description: "Développement frontend",
+ color: "blue",
+ },
+ {
+ id: 2,
+ name: "Backend",
+ description: "Développement backend",
+ color: "green",
+ },
+ {
+ id: 3,
+ name: "Fullstack",
+ description: "Développement fullstack",
+ color: "purple",
+ },
+ {
+ id: 4,
+ name: "UX/UI",
+ description: "Design UX/UI",
+ color: "pink",
+ },
+ {
+ id: 5,
+ name: "DevOps",
+ description: "Infrastructure et déploiement",
+ color: "orange",
+ },
+ {
+ id: 6,
+ name: "Junior",
+ description: "Niveau junior",
+ color: "yellow",
+ },
+ {
+ id: 7,
+ name: "Medior",
+ description: "Niveau intermédiaire",
+ color: "amber",
+ },
+ {
+ id: 8,
+ name: "Senior",
+ description: "Niveau senior",
+ color: "red",
+ },
+ ];
+
+ setTags(mockTags);
+ } catch (error) {
+ console.error("Error fetching tags:", error);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ fetchTags();
+ }, []);
+
+ const handleSelect = (tag: Tag) => {
+ const isSelected = selectedTags.some(t => t.id === tag.id);
+
+ if (isSelected) {
+ onChange(selectedTags.filter(t => t.id !== tag.id));
+ } else {
+ onChange([...selectedTags, tag]);
+ }
+ };
+
+ const handleRemove = (tagId: number) => {
+ onChange(selectedTags.filter(tag => tag.id !== tagId));
+ };
+
+ return (
+
+
+
+
+ {selectedTags.length > 0
+ ? `${selectedTags.length} tag${selectedTags.length > 1 ? "s" : ""} sélectionné${selectedTags.length > 1 ? "s" : ""}`
+ : placeholder}
+
+
+
+
+
+
+ Aucun tag trouvé.
+
+ {loading ? (
+
+
+
+ ) : (
+ tags.map(tag => (
+ handleSelect(tag)}
+ >
+ t.id === tag.id)
+ ? "opacity-100"
+ : "opacity-0"
+ )}
+ />
+
+
+ {tag.name}
+
+
+ {tag.description}
+
+
+
+ ))
+ )}
+
+
+
+
+
+ {selectedTags.length > 0 && (
+
+ {selectedTags.map(tag => (
+
+ {tag.name}
+ handleRemove(tag.id)}
+ disabled={disabled}
+ >
+
+ Supprimer
+
+
+ ))}
+
+ )}
+
+ );
+}
\ No newline at end of file
diff --git a/frontend/components/theme-provider.tsx b/frontend/components/theme-provider.tsx
new file mode 100644
index 0000000..8337198
--- /dev/null
+++ b/frontend/components/theme-provider.tsx
@@ -0,0 +1,9 @@
+"use client";
+
+import * as React from "react";
+import { ThemeProvider as NextThemesProvider } from "next-themes";
+import { type ThemeProviderProps } from "next-themes";
+
+export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
+ return {children} ;
+}
\ No newline at end of file