Spaces:
Running
Running
File size: 5,968 Bytes
ec1a96a 7804693 629fd4f 7804693 ba42f3b 7804693 629fd4f 7804693 629fd4f 7804693 ba42f3b 7804693 ba42f3b ec1a96a 7804693 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 | import { Briefcase, FileCheck2, FileUser, History, LayoutDashboard, LogOut, Menu, Moon, Plus, Search, Settings, Sun, UploadCloud, X } from "lucide-react";
import { useState, type ReactNode } from "react";
import { Link, NavLink, useNavigate } from "react-router-dom";
import { useAuth } from "../lib/auth";
import { useI18n } from "../lib/i18n";
import { Logo, useTheme } from "./ui";
export function LangSwitch() {
const { lang, setLang } = useI18n();
return (
<button onClick={() => setLang(lang === "ar" ? "en" : "ar")} className="rounded-lg px-2.5 py-1.5 text-sm font-medium text-muted hover:bg-sunken hover:text-ink"
aria-label="Change language">
{lang === "ar" ? "English" : "العربية"}
</button>
);
}
export function ThemeSwitch() {
const [dark, toggle] = useTheme();
const { t } = useI18n();
return (
<button onClick={toggle} className="rounded-lg p-2 text-muted hover:bg-sunken hover:text-ink" aria-label={dark ? t("theme_light") : t("theme_dark")}>
{dark ? <Sun size={18} /> : <Moon size={18} />}
</button>
);
}
export function PublicHeader() {
const { profile } = useAuth();
const { t } = useI18n();
return (
<header className="mx-auto flex max-w-6xl items-center justify-between gap-2 px-3 py-5 sm:gap-4 sm:px-6">
<Link to="/" aria-label="EvalBot home"><Logo /></Link>
<nav className="flex items-center gap-1 sm:gap-2">
<Link to="/jobs" className="hidden rounded-xl px-3 py-2 text-sm font-medium text-ink hover:bg-sunken sm:inline-block">{t("browse_jobs")}</Link>
<LangSwitch />
<span className="hidden sm:inline-flex"><ThemeSwitch /></span>
{profile ? (
<Link to="/app" className="rounded-xl bg-nile px-4 py-2 text-sm font-medium text-white dark:text-paper">{t("nav_dashboard")}</Link>
) : (
<>
<Link to="/login" className="whitespace-nowrap rounded-xl border border-line bg-surface px-2.5 py-2 text-sm font-medium text-ink hover:border-nile hover:text-nile sm:px-3.5">{t("sign_in")}</Link>
<Link to="/signup" className="whitespace-nowrap rounded-xl bg-nile px-2.5 py-2 text-sm font-medium text-white hover:bg-nile-deep sm:px-3.5 dark:text-paper">{t("sign_up")}</Link>
</>
)}
</nav>
</header>
);
}
export function AppShell({ children }: { children: ReactNode }) {
const { profile, signOut } = useAuth();
const { t } = useI18n();
const nav = useNavigate();
const [open, setOpen] = useState(false);
const isRecruiter = profile?.role === "recruiter";
const links = isRecruiter
? [
{ to: "/app", icon: LayoutDashboard, label: t("nav_overview"), end: true },
{ to: "/app/jobs", icon: Briefcase, label: t("nav_jobs") },
]
: [
{ to: "/app", icon: LayoutDashboard, label: t("nav_dashboard"), end: true },
{ to: "/app/board", icon: Search, label: t("nav_board") },
{ to: "/app/applications", icon: FileCheck2, label: t("nav_applications") },
{ to: "/app/cv", icon: FileUser, label: t("nav_cv") },
{ to: "/app/new", icon: Plus, label: t("nav_new") },
{ to: "/app/history", icon: History, label: t("nav_history") },
];
const item = "flex items-center gap-3 rounded-xl px-3 py-2.5 text-[15px] font-medium transition-colors";
const sidebar = (
<div className="flex h-full flex-col gap-1 p-4">
<Link to="/" className="mb-6 px-2" onClick={() => setOpen(false)}><Logo /></Link>
{links.map((l) => (
<NavLink key={l.to} to={l.to} end={l.end} onClick={() => setOpen(false)}
className={({ isActive }) => `${item} ${isActive ? "bg-nile-soft text-nile-deep" : "text-muted hover:bg-sunken hover:text-ink"}`}>
<l.icon size={18} /> {l.label}
</NavLink>
))}
<a href="/demo" className={`${item} text-muted hover:bg-sunken hover:text-ink`}><UploadCloud size={18} /> {t("nav_quick")}</a>
<div className="mt-auto flex flex-col gap-1 border-t border-line pt-3">
<NavLink to="/app/settings" onClick={() => setOpen(false)}
className={({ isActive }) => `${item} ${isActive ? "bg-nile-soft text-nile-deep" : "text-muted hover:bg-sunken hover:text-ink"}`}>
<Settings size={18} /> {t("nav_settings")}
</NavLink>
<button className={`${item} text-muted hover:bg-sunken hover:text-ink`} onClick={async () => { await signOut(); nav("/"); }}>
<LogOut size={18} className="rtl:rotate-180" /> {t("sign_out")}
</button>
<div className="flex items-center gap-1 px-1 pt-2"><LangSwitch /><ThemeSwitch /></div>
{profile && <p className="truncate px-3 pt-1 text-xs text-muted">{profile.full_name || profile.email}{profile.company ? `, ${profile.company.name}` : ""}</p>}
</div>
</div>
);
return (
<div className="min-h-screen lg:flex">
<aside className="sticky top-0 hidden h-screen w-64 flex-none border-e border-line bg-surface lg:block">{sidebar}</aside>
<div className="flex items-center justify-between border-b border-line bg-surface px-4 py-3 lg:hidden">
<Link to="/app"><Logo size={24} /></Link>
<button onClick={() => setOpen(true)} className="rounded-lg p-2 text-ink hover:bg-sunken" aria-label="Menu"><Menu size={22} /></button>
</div>
{open && (
<div className="fixed inset-0 z-40 lg:hidden" role="dialog" aria-modal="true">
<div className="absolute inset-0 bg-ink/40" onClick={() => setOpen(false)} />
<div className="absolute inset-y-0 start-0 w-72 bg-surface shadow-xl">
<button onClick={() => setOpen(false)} className="absolute end-3 top-4 rounded-lg p-2 text-muted hover:bg-sunken" aria-label="Close"><X size={20} /></button>
{sidebar}
</div>
</div>
)}
<main className="min-w-0 flex-1 px-4 py-6 sm:px-8 sm:py-8">
<div className="mx-auto max-w-5xl">{children}</div>
</main>
</div>
);
}
|