From b7c717ffb3be8c8fb854d8eedd82820da685a5ba Mon Sep 17 00:00:00 2001 From: Mathis HERRIOT <197931332+0x485254@users.noreply.github.com> Date: Mon, 9 Feb 2026 15:34:26 +0100 Subject: [PATCH] feat(ui): add responsive mobile header and footer components - Implemented `MobileHeader` with support for displaying unread messages. - Created `MobileFooter` with navigation to key sections (home, explore, publish, trends, profile). - Replaced legacy mobile header with new `MobileHeader` and `MobileFooter` in the dashboard layout. - Optimized mobile sidebar rendering for improved responsiveness. --- frontend/src/app/(dashboard)/layout.tsx | 26 ++---- frontend/src/app/(dashboard)/profile/page.tsx | 24 ++++++ frontend/src/components/app-sidebar.tsx | 4 + frontend/src/components/mobile-filters.tsx | 21 ++--- frontend/src/components/mobile-footer.tsx | 80 +++++++++++++++++++ frontend/src/components/mobile-header.tsx | 66 +++++++++++++++ 6 files changed, 192 insertions(+), 29 deletions(-) create mode 100644 frontend/src/components/mobile-footer.tsx create mode 100644 frontend/src/components/mobile-header.tsx diff --git a/frontend/src/app/(dashboard)/layout.tsx b/frontend/src/app/(dashboard)/layout.tsx index 848fae1..7f365f5 100644 --- a/frontend/src/app/(dashboard)/layout.tsx +++ b/frontend/src/app/(dashboard)/layout.tsx @@ -1,15 +1,11 @@ import * as React from "react"; import { AppSidebar } from "@/components/app-sidebar"; import { MobileFilters } from "@/components/mobile-filters"; -import { ModeToggle } from "@/components/mode-toggle"; +import { MobileFooter } from "@/components/mobile-footer"; +import { MobileHeader } from "@/components/mobile-header"; import { SearchSidebar } from "@/components/search-sidebar"; -import { - SidebarInset, - SidebarProvider, - SidebarTrigger, -} from "@/components/ui/sidebar"; +import { SidebarInset, SidebarProvider } from "@/components/ui/sidebar"; import { Toaster } from "@/components/ui/sonner"; -import { UserNavMobile } from "@/components/user-nav-mobile"; export default function DashboardLayout({ children, @@ -22,20 +18,9 @@ export default function DashboardLayout({ - +
-
- -
- - MemeGoat - -
-
- - -
-
+
{children} {modal} @@ -43,6 +28,7 @@ export default function DashboardLayout({ +
diff --git a/frontend/src/app/(dashboard)/profile/page.tsx b/frontend/src/app/(dashboard)/profile/page.tsx index 562e669..54ae86f 100644 --- a/frontend/src/app/(dashboard)/profile/page.tsx +++ b/frontend/src/app/(dashboard)/profile/page.tsx @@ -3,16 +3,19 @@ import { Calendar, Camera, + HelpCircle, LogIn, LogOut, Settings, Share2, + ShieldCheck, } from "lucide-react"; import Link from "next/link"; import { useSearchParams } from "next/navigation"; import * as React from "react"; import { toast } from "sonner"; import { ContentList } from "@/components/content-list"; +import { ModeToggle } from "@/components/mode-toggle"; import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { Button } from "@/components/ui/button"; import { @@ -157,6 +160,19 @@ export default function ProfilePage() {
+ {user.role === "admin" && ( + + )} + + +
diff --git a/frontend/src/components/app-sidebar.tsx b/frontend/src/components/app-sidebar.tsx index 7cb10f1..821f8aa 100644 --- a/frontend/src/components/app-sidebar.tsx +++ b/frontend/src/components/app-sidebar.tsx @@ -54,6 +54,7 @@ import { SidebarRail, SidebarTrigger, } from "@/components/ui/sidebar"; +import { useIsMobile } from "@/hooks/use-mobile"; import { useAuth } from "@/providers/auth-provider"; import { useSocket } from "@/providers/socket-provider"; import { CategoryService } from "@/services/category.service"; @@ -79,6 +80,7 @@ const mainNav = [ ]; export function AppSidebar() { + const isMobile = useIsMobile(); const pathname = usePathname(); const searchParams = useSearchParams(); const { user, logout, isAuthenticated } = useAuth(); @@ -129,6 +131,8 @@ export function AppSidebar() { : "/memegoat-black.svg"; }, [resolvedTheme, mounted]); + if (isMobile) return null; + return ( diff --git a/frontend/src/components/mobile-filters.tsx b/frontend/src/components/mobile-filters.tsx index af8c55b..1f041dd 100644 --- a/frontend/src/components/mobile-filters.tsx +++ b/frontend/src/components/mobile-filters.tsx @@ -1,10 +1,9 @@ "use client"; -import { Filter, Search } from "lucide-react"; +import { Search } from "lucide-react"; import { usePathname, useRouter, useSearchParams } from "next/navigation"; import * as React from "react"; import { Badge } from "@/components/ui/badge"; -import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { ScrollArea } from "@/components/ui/scroll-area"; import { Separator } from "@/components/ui/separator"; @@ -13,7 +12,6 @@ import { SheetContent, SheetHeader, SheetTitle, - SheetTrigger, } from "@/components/ui/sheet"; import { CategoryService } from "@/services/category.service"; import { TagService } from "@/services/tag.service"; @@ -29,6 +27,16 @@ export function MobileFilters() { const [query, setQuery] = React.useState(searchParams.get("query") || ""); const [open, setOpen] = React.useState(false); + React.useEffect(() => { + if (searchParams.get("openSearch") === "true") { + setOpen(true); + // Nettoyer l'URL sans recharger + const params = new URLSearchParams(searchParams.toString()); + params.delete("openSearch"); + router.replace(`${pathname}?${params.toString()}`, { scroll: false }); + } + }, [searchParams, pathname, router]); + React.useEffect(() => { if (open) { CategoryService.getAll().then(setCategories).catch(console.error); @@ -61,13 +69,8 @@ export function MobileFilters() { const currentCategory = searchParams.get("category"); return ( -
+
- - - Recherche & Filtres diff --git a/frontend/src/components/mobile-footer.tsx b/frontend/src/components/mobile-footer.tsx new file mode 100644 index 0000000..7213ca3 --- /dev/null +++ b/frontend/src/components/mobile-footer.tsx @@ -0,0 +1,80 @@ +"use client"; + +import { Home, PlusCircle, Search, TrendingUp, User } from "lucide-react"; +import Link from "next/link"; +import { usePathname } from "next/navigation"; +import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; +import { cn } from "@/lib/utils"; +import { useAuth } from "@/providers/auth-provider"; + +export function MobileFooter() { + const pathname = usePathname(); + const { user, isAuthenticated } = useAuth(); + + const navItems = [ + { + title: "Accueil", + url: "/", + icon: Home, + }, + { + title: "Explorer", + url: "/trends?openSearch=true", + icon: Search, + }, + { + title: "Publier", + url: "/upload", + icon: PlusCircle, + }, + { + title: "Tendances", + url: "/trends", + icon: TrendingUp, + }, + { + title: "Profil", + url: "/profile", + icon: User, + }, + ]; + + return ( +
+ +
+ ); +} diff --git a/frontend/src/components/mobile-header.tsx b/frontend/src/components/mobile-header.tsx new file mode 100644 index 0000000..96abe67 --- /dev/null +++ b/frontend/src/components/mobile-header.tsx @@ -0,0 +1,66 @@ +"use client"; + +import { MessageCircle } from "lucide-react"; +import Link from "next/link"; +import { usePathname } from "next/navigation"; +import * as React from "react"; +import { Button } from "@/components/ui/button"; +import { useAuth } from "@/providers/auth-provider"; +import { useSocket } from "@/providers/socket-provider"; +import { MessageService } from "@/services/message.service"; + +export function MobileHeader() { + const pathname = usePathname(); + const { isAuthenticated } = useAuth(); + const { socket } = useSocket(); + const [unreadMessages, setUnreadMessages] = React.useState(0); + + React.useEffect(() => { + if (isAuthenticated) { + MessageService.getUnreadCount().then(setUnreadMessages).catch(console.error); + } + }, [isAuthenticated]); + + React.useEffect(() => { + if (socket && isAuthenticated) { + const handleNewMessage = () => { + if (pathname !== "/messages") { + setUnreadMessages((prev) => prev + 1); + } + }; + socket.on("new_message", handleNewMessage); + return () => { + socket.off("new_message", handleNewMessage); + }; + } + }, [socket, isAuthenticated, pathname]); + + React.useEffect(() => { + if (pathname === "/messages") { + setUnreadMessages(0); + } + }, [pathname]); + + return ( +
+ + + MemeGoat + + + +
+ +
+
+ ); +}