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;
+