Download src/components/Navbar.js from NeoPy/hugging-c-ai: direct link, hf CLI and curl.
- Browser
- Download file 18.1 kB
-
https://huggingface.co/NeoPy/hugging-c-ai/resolve/main/src/components/Navbar.js
- Command line
-
hf download hf://NeoPy/hugging-c-ai/src/components/Navbar.js
-
curl -L -o Navbar.js https://huggingface.co/NeoPy/hugging-c-ai/resolve/main/src/components/Navbar.js
18.1 kB
| "use client"; | |
| import Link from "next/link"; | |
| import { useSession, signOut, signIn } from "next-auth/react"; | |
| import { useState, useEffect } from "react"; | |
| import { usePathname } from "next/navigation"; | |
| import { IoClose, IoMenu } from "react-icons/io5"; | |
| import { FiMoon, FiSun, FiLogOut, FiDollarSign, FiPlus, FiUser, FiKey, FiCheck, FiX, FiTrash2 } from "react-icons/fi"; | |
| import config from "@/lib/config"; | |
| import toast from "react-hot-toast"; | |
| function HuggingFaceIcon({ className = "w-3 h-3" }) { | |
| return ( | |
| <svg className={className} viewBox="0 0 120 120" fill="currentColor" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"> | |
| <path d="M60 0C26.86 0 0 26.86 0 60s26.86 60 60 60 60-26.86 60-60S93.14 0 60 0z" /> | |
| <g fill="#fff"> | |
| <circle cx="42" cy="50" r="6" /> | |
| <circle cx="78" cy="50" r="6" /> | |
| <path d="M30 70c5 12 17 20 30 20s25-8 30-20" stroke="#fff" strokeWidth="6" fill="none" strokeLinecap="round" /> | |
| </g> | |
| </svg> | |
| ); | |
| } | |
| const HF_DUPLICATE_URL = "https://huggingface.co/spaces?duplicate=sonic-coders/Hugging-c-ai"; | |
| export default function Navbar() { | |
| const { data: session, status, update: updateSession } = useSession(); | |
| const pathname = usePathname(); | |
| const [isOpen, setIsOpen] = useState(false); | |
| const [isProfileOpen, setIsProfileOpen] = useState(false); | |
| const [isApiKeyModalOpen, setIsApiKeyModalOpen] = useState(false); | |
| const [apiKeyInput, setApiKeyInput] = useState(""); | |
| const [savingKey, setSavingKey] = useState(false); | |
| const appName = config?.appName || "AI SaaS"; | |
| const logoLetter = appName.trim().charAt(0).toUpperCase(); | |
| const isApiKeyActive = Boolean(session?.user?.customApiKey); | |
| useEffect(() => { | |
| if (session?.user?.customApiKey) { | |
| setApiKeyInput(session.user.customApiKey); | |
| } | |
| }, [session?.user?.customApiKey]); | |
| const appMatch = pathname ? pathname.match(/^\/app\/([^\/]+)/) : null; | |
| const currentAppId = appMatch ? appMatch[1] : null; | |
| const navLinks = currentAppId | |
| ? [ | |
| { name: "Workspace", path: `/app/${currentAppId}` }, | |
| { name: "Gallery", path: `/app/${currentAppId}/gallery` }, | |
| { name: "Pricing", path: `/app/${currentAppId}/pricing` }, | |
| ] | |
| : [ | |
| { name: "Workspace", path: "/" }, | |
| { name: "Gallery", path: "/gallery" }, | |
| { name: "Pricing", path: "/pricing" }, | |
| ]; | |
| const handleSaveApiKey = async (e) => { | |
| e.preventDefault(); | |
| const key = apiKeyInput.trim(); | |
| if (!key) { | |
| toast.error("Please enter a valid API Key"); | |
| return; | |
| } | |
| setSavingKey(true); | |
| try { | |
| if (status === "authenticated") { | |
| const res = await fetch("/api/user/apikey", { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify({ apiKey: key }), | |
| }); | |
| const data = await res.json(); | |
| if (!res.ok) throw new Error(data.error || "Failed to save API key"); | |
| await updateSession({ customApiKey: key }); | |
| toast.success("Custom API Key updated!"); | |
| setIsApiKeyModalOpen(false); | |
| window.location.reload(); | |
| } else { | |
| const res = await signIn("credentials", { | |
| apiKey: key, | |
| redirect: false, | |
| }); | |
| if (res?.error) { | |
| throw new Error(res.error || "Failed to sign in with API key"); | |
| } | |
| toast.success("Signed in with API Key!"); | |
| setIsApiKeyModalOpen(false); | |
| window.location.reload(); | |
| } | |
| } catch (err) { | |
| toast.error(err.message || "Failed to save API Key"); | |
| } finally { | |
| setSavingKey(false); | |
| } | |
| }; | |
| const handleRemoveApiKey = async () => { | |
| setSavingKey(true); | |
| try { | |
| const res = await fetch("/api/user/apikey", { method: "DELETE" }); | |
| const data = await res.json(); | |
| if (!res.ok) throw new Error(data.error || "Failed to remove API key"); | |
| await updateSession({ customApiKey: null }); | |
| setApiKeyInput(""); | |
| toast.success("Custom API Key removed"); | |
| setIsApiKeyModalOpen(false); | |
| window.location.reload(); | |
| } catch (err) { | |
| toast.error(err.message || "Failed to remove API Key"); | |
| } finally { | |
| setSavingKey(false); | |
| } | |
| }; | |
| return ( | |
| <header className="sticky top-0 z-50 w-full glass-panel border-b border-divider/50 shadow-md"> | |
| <div className="mx-auto flex max-w-7xl items-center justify-between px-4 py-4 sm:px-6 lg:px-8"> | |
| {/* Logo and Brand Title */} | |
| <Link href="/" className="flex items-center gap-2 transition-transform hover:scale-[1.02] active:scale-95"> | |
| <div className="flex h-9 w-9 items-center justify-center rounded-lg bg-primary text-white font-extrabold text-lg shadow-md shadow-primary/30"> | |
| {logoLetter} | |
| </div> | |
| <span className="text-lg font-black tracking-tight text-primary-text text-nowrap"> | |
| {appName} | |
| </span> | |
| </Link> | |
| {/* Desktop Navigation Links */} | |
| <nav className="hidden md:flex items-center gap-6"> | |
| {navLinks.map((link) => { | |
| const isActive = pathname === link.path; | |
| return ( | |
| <Link | |
| key={link.name} | |
| href={link.path} | |
| className={`text-[13px] font-semibold transition-all relative py-1 ${ | |
| isActive ? "text-primary" : "text-secondary-text hover:text-primary-text" | |
| }`} | |
| > | |
| {link.name} | |
| {isActive && ( | |
| <div className="absolute -bottom-[20px] left-0 right-0 h-0.5 bg-primary rounded-full" /> | |
| )} | |
| </Link> | |
| ); | |
| })} | |
| </nav> | |
| {/* Desktop Actions Section */} | |
| <div className="hidden md:flex items-center gap-3"> | |
| {/* HF Space Duplicate Button */} | |
| <a | |
| href={HF_DUPLICATE_URL} | |
| target="_blank" | |
| rel="noopener noreferrer" | |
| title="Duplicate this app to your own HuggingFace Space" | |
| className="flex items-center gap-2 rounded-full border border-divider px-4 py-1.5 text-xs font-bold text-secondary-text hover:text-yellow-400 hover:border-yellow-400/40 transition-colors shadow-sm" | |
| > | |
| <HuggingFaceIcon className="w-3.5 h-3.5 text-yellow-400" /> | |
| <span>Duplicate to HF Space</span> | |
| </a> | |
| {/* Add/Manage API Key - Directly visible in Navbar */} | |
| <button | |
| onClick={() => setIsApiKeyModalOpen(true)} | |
| className={`flex items-center gap-1.5 px-3 py-1.5 rounded-full text-xs font-bold border transition-all cursor-pointer ${ | |
| isApiKeyActive | |
| ? "bg-amber-500/10 border-amber-500/30 text-amber-400 hover:bg-amber-500/20" | |
| : "bg-bg-page/50 border-divider text-secondary-text hover:text-white hover:border-primary/40" | |
| }`} | |
| > | |
| <FiKey className={isApiKeyActive ? "text-amber-400" : "text-secondary-text"} /> | |
| <span>{isApiKeyActive ? "Custom API Key" : "Add API Key"}</span> | |
| </button> | |
| {status === "authenticated" ? ( | |
| <div className="flex items-center"> | |
| {/* Credit Balance indicator */} | |
| <div className="flex items-center h-9 border border-divider rounded-l bg-bg-page/30 overflow-hidden pr-2"> | |
| <span className="font-bold text-[13px] px-3 flex items-center text-primary-text gap-1"> | |
| <FiDollarSign className="text-emerald-500 text-xs" /> | |
| {isApiKeyActive ? "∞ (API Key)" : session.user.credits !== undefined ? session.user.credits : 0} | |
| </span> | |
| {!isApiKeyActive && ( | |
| <Link | |
| href="/pricing" | |
| className="flex items-center justify-center w-5 h-5 rounded hover:bg-bg-card text-secondary-text transition-colors" | |
| > | |
| <FiPlus size={14} /> | |
| </Link> | |
| )} | |
| </div> | |
| {/* Profile Menu Toggle */} | |
| <div className="relative"> | |
| <button | |
| onClick={() => setIsProfileOpen(!isProfileOpen)} | |
| onBlur={() => setTimeout(() => setIsProfileOpen(false), 200)} | |
| className="h-9 w-9 flex items-center justify-center border-y border-r border-divider rounded-r bg-bg-page/30 hover:bg-bg-page transition-colors cursor-pointer" | |
| > | |
| {session.user.image ? ( | |
| <img | |
| src={session.user.image} | |
| alt="Profile" | |
| className="h-6 w-6 rounded-full object-cover" | |
| /> | |
| ) : ( | |
| <FiUser className="text-secondary-text" size={16} /> | |
| )} | |
| </button> | |
| {/* Profile Dropdown */} | |
| {isProfileOpen && ( | |
| <div className="absolute right-0 top-11 w-52 rounded border border-divider bg-bg-card p-1 shadow-lg z-[100] animate-scale-up"> | |
| <div className="px-3 py-2 text-xs text-secondary-text border-b border-divider/50 mb-1 truncate"> | |
| {session.user.email} | |
| </div> | |
| <button | |
| onClick={() => setIsApiKeyModalOpen(true)} | |
| className="flex w-full items-center gap-2 rounded px-3 py-2 text-left text-xs font-semibold text-primary-text hover:bg-primary/10 transition-colors" | |
| > | |
| <FiKey size={14} className="text-amber-400" /> | |
| <span>{isApiKeyActive ? "Manage API Key" : "Add API Key"}</span> | |
| </button> | |
| <button | |
| onClick={() => signOut({ callbackUrl: "/login" })} | |
| className="flex w-full items-center gap-2 rounded px-3 py-2 text-left text-xs font-semibold text-red-500 hover:bg-red-500/10 transition-colors" | |
| > | |
| <FiLogOut size={14} /> | |
| <span>Sign Out</span> | |
| </button> | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| ) : ( | |
| <Link | |
| href="/login" | |
| className="bg-primary text-white px-5 py-1.5 rounded-full text-sm font-bold hover:bg-primary-hover transition-all shadow-md shadow-primary/20" | |
| > | |
| Sign In | |
| </Link> | |
| )} | |
| </div> | |
| {/* Mobile Navbar Controls */} | |
| <div className="flex md:hidden items-center gap-2"> | |
| {status === "authenticated" && ( | |
| <div className="flex items-center h-8 border border-divider rounded bg-bg-page/30 px-2.5 text-xs font-bold text-primary-text gap-0.5"> | |
| <FiDollarSign className="text-emerald-500 text-[10px]" /> | |
| {isApiKeyActive ? "∞ Key" : session.user.credits !== undefined ? session.user.credits : 0} | |
| </div> | |
| )} | |
| <button | |
| onClick={() => setIsOpen(!isOpen)} | |
| className="hover:bg-bg-card p-2 rounded cursor-pointer transition-colors text-primary-text border border-divider/50" | |
| aria-label="Toggle Menu" | |
| > | |
| {isOpen ? <IoClose size={20} /> : <IoMenu size={20} />} | |
| </button> | |
| </div> | |
| </div> | |
| {/* Mobile Menu Dropdown */} | |
| {isOpen && ( | |
| <div className="absolute top-full left-0 right-0 z-[200] glass-dropdown border-b border-divider shadow-2xl py-4 px-6 md:hidden animate-fade-in"> | |
| <nav className="flex flex-col gap-3"> | |
| <span className="text-[10px] uppercase font-bold text-secondary-text tracking-widest mb-1">Navigation</span> | |
| {navLinks.map((link) => ( | |
| <Link | |
| key={link.name} | |
| href={link.path} | |
| onClick={() => setIsOpen(false)} | |
| className={`flex items-center py-2.5 rounded text-sm font-semibold transition-all ${ | |
| pathname === link.path ? "bg-primary/10 text-primary px-3 border border-primary/20" : "text-primary-text hover:bg-bg-card" | |
| }`} | |
| > | |
| {link.name} | |
| </Link> | |
| ))} | |
| <button | |
| onClick={() => { | |
| setIsOpen(false); | |
| setIsApiKeyModalOpen(true); | |
| }} | |
| className="flex w-full items-center justify-between rounded border border-amber-500/30 bg-amber-500/10 px-3 py-2.5 text-xs font-bold text-amber-400" | |
| > | |
| <div className="flex items-center gap-2"> | |
| <FiKey /> | |
| <span>{isApiKeyActive ? "Manage Custom API Key" : "Add API Key"}</span> | |
| </div> | |
| </button> | |
| <div className="h-px bg-divider/50 my-2" /> | |
| {/* HF Space Duplicate in Mobile menu */} | |
| <a | |
| href={HF_DUPLICATE_URL} | |
| target="_blank" | |
| rel="noopener noreferrer" | |
| className="flex w-full items-center justify-center gap-2 rounded-full border border-divider py-3 text-xs font-bold text-secondary-text hover:text-yellow-400 hover:border-yellow-400/40 transition-all" | |
| > | |
| <HuggingFaceIcon className="w-3.5 h-3.5 text-yellow-400" /> | |
| <span>Duplicate to HF Space</span> | |
| </a> | |
| {status === "authenticated" ? ( | |
| <button | |
| onClick={() => { | |
| setIsOpen(false); | |
| signOut({ callbackUrl: "/login" }); | |
| }} | |
| className="flex w-full items-center justify-center gap-2 rounded bg-red-500/10 text-red-500 py-3 text-sm font-bold hover:bg-red-500/20 transition-all border border-red-500/20 mt-2" | |
| > | |
| <FiLogOut size={16} /> | |
| <span>Sign Out</span> | |
| </button> | |
| ) : ( | |
| <Link | |
| href="/login" | |
| onClick={() => setIsOpen(false)} | |
| className="flex w-full items-center justify-center rounded bg-primary text-white py-3 text-sm font-bold hover:bg-primary-hover transition-all shadow-md shadow-primary/20 mt-2" | |
| > | |
| Sign In | |
| </Link> | |
| )} | |
| </nav> | |
| </div> | |
| )} | |
| {/* API Key Modal */} | |
| {isApiKeyModalOpen && ( | |
| <div className="fixed inset-0 z-[300] flex items-center justify-center bg-black/70 backdrop-blur-sm p-4"> | |
| <div className="bg-bg-card border border-divider w-full max-w-md rounded-xl p-6 space-y-5 shadow-2xl animate-scale-up"> | |
| <div className="flex items-center justify-between border-b border-divider/60 pb-3"> | |
| <div className="flex items-center gap-2 text-amber-400 font-black text-sm uppercase"> | |
| <FiKey className="text-base" /> | |
| <span>Custom API Key Settings</span> | |
| </div> | |
| <button | |
| onClick={() => setIsApiKeyModalOpen(false)} | |
| className="text-secondary-text hover:text-white transition-colors cursor-pointer" | |
| > | |
| <FiX size={18} /> | |
| </button> | |
| </div> | |
| <p className="text-xs text-secondary-text leading-relaxed"> | |
| Use your own <strong>HF token</strong>, OpenAI key, Anthropic key, | |
| Groq key, or MuAPI key. We auto-detect the provider from the key | |
| prefix. Keys are encrypted at rest. | |
| </p> | |
| <form onSubmit={handleSaveApiKey} className="space-y-4"> | |
| <div className="space-y-1.5"> | |
| <label className="block text-[11px] uppercase font-bold text-secondary-text tracking-wider"> | |
| API Key / HF Token | |
| </label> | |
| <input | |
| type="password" | |
| value={apiKeyInput} | |
| onChange={(e) => setApiKeyInput(e.target.value)} | |
| placeholder="hf_... | sk-... | sk-ant... | gsk_... | mu_..." | |
| className="w-full bg-bg-page border border-divider rounded-lg px-3.5 py-2.5 text-xs text-white placeholder-secondary-text/50 focus:outline-none focus:border-amber-400 transition-colors" | |
| /> | |
| </div> | |
| <div className="flex items-center justify-between gap-3 pt-2"> | |
| {isApiKeyActive && status === "authenticated" && ( | |
| <button | |
| type="button" | |
| onClick={handleRemoveApiKey} | |
| disabled={savingKey} | |
| className="flex items-center gap-1.5 px-3 py-2 rounded-lg bg-red-500/10 hover:bg-red-500/20 text-red-500 text-xs font-bold border border-red-500/20 transition-all cursor-pointer" | |
| > | |
| <FiTrash2 /> | |
| <span>Remove Key</span> | |
| </button> | |
| )} | |
| <div className="flex items-center gap-2 ml-auto"> | |
| <button | |
| type="button" | |
| onClick={() => setIsApiKeyModalOpen(false)} | |
| className="px-4 py-2 rounded-lg bg-bg-page border border-divider text-xs font-semibold text-secondary-text hover:text-white cursor-pointer" | |
| > | |
| Cancel | |
| </button> | |
| <button | |
| type="submit" | |
| disabled={savingKey || !apiKeyInput.trim()} | |
| className="flex items-center gap-1.5 px-4 py-2 rounded-lg bg-amber-500 hover:bg-amber-400 text-neutral-950 text-xs font-bold transition-all disabled:opacity-50 cursor-pointer shadow-md shadow-amber-500/20" | |
| > | |
| <FiCheck /> | |
| <span>{savingKey ? "Processing..." : status === "authenticated" ? "Save Key" : "Sign In with API Key"}</span> | |
| </button> | |
| </div> | |
| </div> | |
| </form> | |
| </div> | |
| </div> | |
| )} | |
| </header> | |
| ); | |
| } | |