| "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> |
| ); |
| } |
|
|