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} + + + + ))} + + + +
+ +
+
+ + +
+
+
+
{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.name} + {user.role} +
+
+ )} + + {/* Admin button */} + {user && user.role === 'ADMIN' && ( +
+ +
+ )} + + {/* Theme and logout buttons */} +
+ + +
+
+
+
{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 ( +
+ + + + + + + + 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} + + + ))} +
+ )} +
+ ); +} \ 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