From e24001bb4a423b64014a6ab9110e364ecdd05f34 Mon Sep 17 00:00:00 2001 From: Aroy-Art <Aroy-Art@pm.me> Date: Thu, 20 Mar 2025 19:15:22 +0100 Subject: [PATCH] Add: Navbar component --- frontend/src/components/Navbar.tsx | 94 ++++++++++++++++++ frontend/src/components/UserDropdown.tsx | 117 +++++++++++++++++++++++ 2 files changed, 211 insertions(+) create mode 100644 frontend/src/components/Navbar.tsx create mode 100644 frontend/src/components/UserDropdown.tsx diff --git a/frontend/src/components/Navbar.tsx b/frontend/src/components/Navbar.tsx new file mode 100644 index 0000000..eefb606 --- /dev/null +++ b/frontend/src/components/Navbar.tsx @@ -0,0 +1,94 @@ +import { useState, useEffect } from "react"; +import { Link } from "react-router-dom"; +import LightModeToggle from "@/components/LightModeToggle"; +import { Separator } from "@/components/ui/separator"; +import { logout } from "@/services/auth"; // Import the logout function + +import ProfileDropdown from "@/components/ProfileDropdown"; +import UserDropdown from "@/components/UserDropdown"; + +const Navbar = () => { + const [isOpen, setIsOpen] = useState(false); + const [isLoggedIn, setIsLoggedIn] = useState(false); + + // Check login status on component mount + useEffect(() => { + const token = localStorage.getItem("access_token"); + setIsLoggedIn(!!token); // Convert token presence to boolean + }, []); + + return ( + <nav className="bg-violet-600 p-4 shadow-md"> + <div className="container mx-auto flex items-center justify-between"> + {/* Logo (Always Centered on Mobile) */} + <Link to="/" className="text-white text-2xl font-bold mx-auto md:mx-0"> + {__SITE_NAME__} + </Link> + + {/* Desktop Navigation */} + <div className="hidden md:flex items-center space-x-6"> + <Link to="/" className="text-white hover:text-gray-300">Home</Link> + <Link to="/browse/" className="text-white hover:text-gray-300">Browse</Link> + <Link to="/gallery" className="text-white hover:text-gray-300">Gallery</Link> + <LightModeToggle /> + {isLoggedIn ? ( + <UserDropdown /> + ) : ( + <Link to="/user/login" className="text-white hover:text-gray-300">Login</Link> + )} + </div> + + {/* Mobile Menu Button */} + <button + onClick={() => setIsOpen(!isOpen)} + className="text-white text-2xl font-bold md:hidden" + > + {isOpen ? '✖' : '☰'} + </button> + </div> + + {/* Mobile Side Panel */} + {isOpen && ( + <div className="fixed top-0 right-0 w-2/3 h-full bg-violet-700 z-40 shadow-lg p-4"> + <div className="flex justify-end"> + <button + onClick={() => setIsOpen(false)} + className="text-white text-2xl font-bold" + > + ✖ + </button> + </div> + <ul className="space-y-4 mt-4"> + <li> + <Link to="/" className="text-white hover:text-gray-300 block" onClick={() => setIsOpen(false)}>Home</Link> + </li> + <li> + <Link to="/browse/" className="text-white hover:text-gray-300 block" onClick={() => setIsOpen(false)}>Browse</Link> + </li> + <li> + <Link to="/gallery" className="text-white hover:text-gray-300 block" onClick={() => setIsOpen(false)}>Gallery</Link> + </li> + <li> + <LightModeToggle /> + </li> + <li> + {isLoggedIn ? ( + <UserDropdown /> + ) : ( + <Link to="/user/login" className="text-white hover:text-gray-300 block" onClick={() => setIsOpen(false)}> + Login + </Link> + )} + </li> + <li> + <Link to="/protected" className="text-white hover:text-gray-300 block" onClick={() => setIsOpen(false)}>Protected</Link> + </li> + </ul> + </div> + )} + </nav> + ); +}; + +export default Navbar; + diff --git a/frontend/src/components/UserDropdown.tsx b/frontend/src/components/UserDropdown.tsx new file mode 100644 index 0000000..d052f2c --- /dev/null +++ b/frontend/src/components/UserDropdown.tsx @@ -0,0 +1,117 @@ +import { useState, useEffect } from "react"; +import { Link } from "react-router-dom"; +import { Switch } from "@/components/ui/switch"; +import { Button } from "@/components/ui/button"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { Skeleton } from "@/components/ui/skeleton"; + +import apiInstance from "@/services/api"; +import { logout } from "@/services/auth"; + +interface UserProfile { + username: string; + email: string; + profile: { + show_mature: boolean; + }; +} + +const UserDropdown = () => { + const [user, setUser] = useState<UserProfile | null>(null); + const [loading, setLoading] = useState(true); + const [nsfwEnabled, setNsfwEnabled] = useState(false); + + useEffect(() => { + const fetchUser = async () => { + try { + const response = await apiInstance.get<UserProfile>("user/profile/"); + setUser(response.data); + setNsfwEnabled(response.data.profile.show_mature); + } catch (error) { + console.error("Failed to fetch user:", error); + } finally { + setLoading(false); + } + }; + fetchUser(); + }, []); + + const handleNsfwToggle = async () => { + if (!user) return; + const newSetting = !nsfwEnabled; + setNsfwEnabled(newSetting); + + try { + await apiInstance.patch("user/profile/", { + profile: { show_mature: newSetting }, + }); + } catch (error) { + console.error("Failed to update NSFW setting:", error); + setNsfwEnabled(!newSetting); + } + }; + + const handleLogout = () => { + logout(); + }; + + return ( + <DropdownMenu> + <DropdownMenuTrigger asChild> + <Link className="relative flex items-center space-x-2"> + {loading ? ( + <Skeleton className="h-8 w-8 rounded-full bg-gray-500 hover:bg-gray-600" /> + ) : ( + <div className="h-8 w-8 bg-gray-500 hover:bg-gray-600 rounded-full flex items-center justify-center shadow-lg"> + {user?.username.charAt(0).toUpperCase()} + </div> + )} + </Link> + </DropdownMenuTrigger> + + <DropdownMenuContent align="end" className="p-4 shadow-lg rounded-lg border"> + {loading ? ( + <div className="flex flex-col space-y-3"> + <Skeleton className="h-4 w-32" /> + <Skeleton className="h-4 w-48" /> + <Skeleton className="h-10 w-full" /> + </div> + ) : ( + <div className="space-y-2"> + {/* User Info */} + <div className="text-sm"> + <p className="font-semibold">{user?.username}</p> + <p className="text-gray-500">{user?.email}</p> + </div> + + <div className="border-t my-2"></div> + + {/* NSFW Toggle (Prevent Dropdown from Closing) */} + <DropdownMenuItem asChild> + <div + className="flex items-center justify-between w-full cursor-pointer" + onClick={(e) => e.stopPropagation()} // Prevents dropdown from closing + > + <span className="text-sm">Show NSFW Content</span> + <Switch checked={nsfwEnabled} onCheckedChange={handleNsfwToggle} /> + </div> + </DropdownMenuItem> + + {/* Logout Button */} + <Button variant="destructive" className="w-full" onClick={handleLogout}> + Logout + </Button> + </div> + )} + </DropdownMenuContent> + </DropdownMenu> + ); +}; + +export default UserDropdown; +