Files
memegoat/frontend/src/components/app-sidebar.tsx
Mathis HERRIOT 38adbb6e77 feat(media): add public URL generation for media files and improve S3 integration
Introduce `getPublicUrl` in `S3Service` for generating public URLs. Replace custom file URL generation logic across services with the new method. Add media controller for file streaming and update related tests. Adjust frontend to display user roles instead of email in the sidebar. Update environment schema to include optional `API_URL`. Fix help page contact email.
2026-01-14 23:13:28 +01:00

302 lines
8.5 KiB
TypeScript

"use client";
import {
ChevronRight,
Clock,
Heart,
HelpCircle,
History,
Home,
LayoutGrid,
LogIn,
LogOut,
PlusCircle,
Settings,
ShieldCheck,
TrendingUp,
User as UserIcon,
} from "lucide-react";
import Link from "next/link";
import { usePathname, useSearchParams } from "next/navigation";
import * as React from "react";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarGroup,
SidebarGroupLabel,
SidebarHeader,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
SidebarMenuSub,
SidebarMenuSubButton,
SidebarMenuSubItem,
} from "@/components/ui/sidebar";
import { useAuth } from "@/providers/auth-provider";
import { CategoryService } from "@/services/category.service";
import type { Category } from "@/types/content";
const mainNav = [
{
title: "Accueil",
url: "/",
icon: Home,
},
{
title: "Tendances",
url: "/trends",
icon: TrendingUp,
},
{
title: "Nouveautés",
url: "/recent",
icon: Clock,
},
];
export function AppSidebar() {
const pathname = usePathname();
const searchParams = useSearchParams();
const { user, logout, isAuthenticated } = useAuth();
const [categories, setCategories] = React.useState<Category[]>([]);
React.useEffect(() => {
CategoryService.getAll().then(setCategories).catch(console.error);
}, []);
return (
<Sidebar collapsible="icon">
<SidebarHeader className="flex items-center justify-center py-4">
<Link href="/" className="flex items-center gap-2 font-bold text-xl">
<div className="bg-primary text-primary-foreground p-1 rounded">🐐</div>
<span className="group-data-[collapsible=icon]:hidden">MemeGoat</span>
</Link>
</SidebarHeader>
<SidebarContent>
<SidebarGroup>
<SidebarMenu>
{mainNav.map((item) => (
<SidebarMenuItem key={item.title}>
<SidebarMenuButton
asChild
isActive={pathname === item.url}
tooltip={item.title}
>
<Link href={item.url}>
<item.icon />
<span>{item.title}</span>
</Link>
</SidebarMenuButton>
</SidebarMenuItem>
))}
</SidebarMenu>
</SidebarGroup>
<SidebarGroup>
<SidebarGroupLabel>Explorer</SidebarGroupLabel>
<SidebarMenu>
<Collapsible asChild className="group/collapsible">
<SidebarMenuItem>
<CollapsibleTrigger asChild>
<SidebarMenuButton tooltip="Catégories">
<LayoutGrid />
<span>Catégories</span>
<ChevronRight className="ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90" />
</SidebarMenuButton>
</CollapsibleTrigger>
<CollapsibleContent>
<SidebarMenuSub>
{categories.map((category) => (
<SidebarMenuSubItem key={category.id}>
<SidebarMenuSubButton
asChild
isActive={pathname === `/category/${category.slug}`}
>
<Link href={`/category/${category.slug}`}>
<span>{category.name}</span>
</Link>
</SidebarMenuSubButton>
</SidebarMenuSubItem>
))}
</SidebarMenuSub>
</CollapsibleContent>
</SidebarMenuItem>
</Collapsible>
</SidebarMenu>
</SidebarGroup>
<SidebarGroup>
<SidebarGroupLabel>Communauté</SidebarGroupLabel>
<SidebarMenu>
<SidebarMenuItem>
<SidebarMenuButton asChild tooltip="Publier">
<Link href="/upload">
<PlusCircle />
<span>Publier</span>
</Link>
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
</SidebarGroup>
<SidebarGroup>
<SidebarGroupLabel>Ma Bibliothèque</SidebarGroupLabel>
<SidebarMenu>
<SidebarMenuItem>
<SidebarMenuButton
asChild
isActive={
pathname === "/profile" && searchParams.get("tab") === "favorites"
}
tooltip="Mes Favoris"
>
<Link href="/profile?tab=favorites">
<Heart />
<span>Mes Favoris</span>
</Link>
</SidebarMenuButton>
</SidebarMenuItem>
<SidebarMenuItem>
<SidebarMenuButton
asChild
isActive={
pathname === "/profile" && searchParams.get("tab") === "memes"
}
tooltip="Mes Mèmes"
>
<Link href="/profile?tab=memes">
<History />
<span>Mes Mèmes</span>
</Link>
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
</SidebarGroup>
{isAuthenticated && user?.role === "admin" && (
<SidebarGroup>
<SidebarGroupLabel>Administration</SidebarGroupLabel>
<SidebarMenu>
<SidebarMenuItem>
<SidebarMenuButton
asChild
isActive={pathname.startsWith("/admin")}
tooltip="Dashboard Admin"
>
<Link href="/admin">
<ShieldCheck />
<span>Admin</span>
</Link>
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
</SidebarGroup>
)}
</SidebarContent>
<SidebarFooter>
<SidebarMenu>
{isAuthenticated && user ? (
<SidebarMenuItem>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<SidebarMenuButton
size="lg"
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
>
<Avatar className="h-8 w-8 rounded-lg">
<AvatarImage src={user.avatarUrl} alt={user.username} />
<AvatarFallback className="rounded-lg">
{user.username.slice(0, 2).toUpperCase()}
</AvatarFallback>
</Avatar>
<div className="grid flex-1 text-left text-sm leading-tight group-data-[collapsible=icon]:hidden">
<span className="truncate font-semibold">
{user.displayName || user.username}
</span>
<span className="truncate text-xs">{user.role}</span>
</div>
<ChevronRight className="ml-auto size-4 group-data-[collapsible=icon]:hidden" />
</SidebarMenuButton>
</DropdownMenuTrigger>
<DropdownMenuContent
className="w-(--radix-dropdown-menu-trigger-width) min-w-56 rounded-lg"
side="right"
align="end"
sideOffset={4}
>
<DropdownMenuLabel className="p-0 font-normal">
<div className="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
<Avatar className="h-8 w-8 rounded-lg">
<AvatarImage src={user.avatarUrl} alt={user.username} />
<AvatarFallback className="rounded-lg">
{user.username.slice(0, 2).toUpperCase()}
</AvatarFallback>
</Avatar>
<div className="grid flex-1 text-left text-sm leading-tight">
<span className="truncate font-semibold">
{user.displayName || user.username}
</span>
<span className="truncate text-xs">{user.role}</span>
</div>
</div>
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuItem asChild>
<Link href="/profile" className="flex items-center gap-2">
<UserIcon className="size-4" />
<span>Profil</span>
</Link>
</DropdownMenuItem>
<DropdownMenuItem asChild>
<Link href="/settings" className="flex items-center gap-2">
<Settings className="size-4" />
<span>Paramètres</span>
</Link>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={() => logout()}>
<LogOut className="size-4 mr-2" />
<span>Déconnexion</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</SidebarMenuItem>
) : (
<SidebarMenuItem>
<SidebarMenuButton asChild tooltip="Se connecter">
<Link href="/login">
<LogIn className="size-4" />
<span>Se connecter</span>
</Link>
</SidebarMenuButton>
</SidebarMenuItem>
)}
<SidebarMenuItem>
<SidebarMenuButton asChild tooltip="Aide">
<Link href="/help">
<HelpCircle />
<span>Aide</span>
</Link>
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
</SidebarFooter>
</Sidebar>
);
}