Spaces:
Paused
Paused
Download apps/web/src/components/app-shell.tsx from nathycodes/presentercoach: direct link, hf CLI and curl.
- Browser
- Download file 8.23 kB
-
https://huggingface.co/spaces/nathycodes/presentercoach/resolve/main/apps/web/src/components/app-shell.tsx
- Command line
-
hf download hf://spaces/nathycodes/presentercoach/apps/web/src/components/app-shell.tsx
-
curl -L -o app-shell.tsx https://huggingface.co/spaces/nathycodes/presentercoach/resolve/main/apps/web/src/components/app-shell.tsx
8.23 kB
| "use client"; | |
| import type { ReactNode } from "react"; | |
| import Link from "next/link"; | |
| import { usePathname, useRouter } from "next/navigation"; | |
| import { Button } from "@/components/ui/button"; | |
| import { cn } from "@/lib/utils"; | |
| import { useAuthStore } from "@/store/useAuthStore"; | |
| type NavItem = { | |
| href: string; | |
| label: string; | |
| shortLabel: string; | |
| icon: (active: boolean) => ReactNode; | |
| }; | |
| const NAV_ITEMS: NavItem[] = [ | |
| { | |
| href: "/", | |
| label: "Command Center", | |
| shortLabel: "Home", | |
| icon: (active) => ( | |
| <svg className={cn("h-5 w-5", active ? "text-ember" : "text-ink/65")} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"> | |
| <path d="M3 10.5L12 3l9 7.5V21a1 1 0 0 1-1 1h-5v-6h-6v6H4a1 1 0 0 1-1-1v-10.5z" strokeLinecap="round" strokeLinejoin="round" /> | |
| </svg> | |
| ), | |
| }, | |
| { | |
| href: "/curriculum", | |
| label: "30-Day Plan", | |
| shortLabel: "30-Day", | |
| icon: (active) => ( | |
| <svg className={cn("h-5 w-5", active ? "text-ember" : "text-ink/65")} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"> | |
| <rect x="3" y="4" width="18" height="17" rx="3" /> | |
| <path d="M8 2v4M16 2v4M3 9h18M8 14l2.5 2.5L16 11" strokeLinecap="round" strokeLinejoin="round" /> | |
| </svg> | |
| ), | |
| }, | |
| { | |
| href: "/record", | |
| label: "Camera Studio", | |
| shortLabel: "Studio", | |
| icon: (active) => ( | |
| <svg className={cn("h-5 w-5", active ? "text-ember" : "text-ink/65")} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"> | |
| <rect x="2" y="6" width="14" height="12" rx="2.5" /> | |
| <path d="M16 10l6-3.5v11L16 14v-4z" strokeLinecap="round" strokeLinejoin="round" /> | |
| <circle cx="9" cy="12" r="2.2" fill="currentColor" /> | |
| </svg> | |
| ), | |
| }, | |
| { | |
| href: "/tech-news", | |
| label: "Tech News Lab", | |
| shortLabel: "News", | |
| icon: (active) => ( | |
| <svg className={cn("h-5 w-5", active ? "text-ember" : "text-ink/65")} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"> | |
| <path d="M4 5h13a2 2 0 0 1 2 2v11a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V5z" /> | |
| <path d="M19 8h1a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2h-1M8 9h7M8 13h7M8 17h4" strokeLinecap="round" /> | |
| </svg> | |
| ), | |
| }, | |
| { | |
| href: "/exercises", | |
| label: "Daily Drills", | |
| shortLabel: "Drills", | |
| icon: (active) => ( | |
| <svg className={cn("h-5 w-5", active ? "text-ember" : "text-ink/65")} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"> | |
| <path d="M13 2L3 14h8l-1 8 10-12h-8l1-8z" strokeLinecap="round" strokeLinejoin="round" /> | |
| </svg> | |
| ), | |
| }, | |
| { | |
| href: "/progress", | |
| label: "Progress & Compare", | |
| shortLabel: "Progress", | |
| icon: (active) => ( | |
| <svg className={cn("h-5 w-5", active ? "text-ember" : "text-ink/65")} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"> | |
| <path d="M4 19h16M7 15V9M12 15V5M17 15v-3" strokeLinecap="round" strokeLinejoin="round" /> | |
| </svg> | |
| ), | |
| }, | |
| ]; | |
| type AppShellProps = { | |
| children: ReactNode; | |
| title?: string; | |
| subtitle?: string; | |
| eyebrow?: string; | |
| actions?: ReactNode; | |
| maxWidthClass?: string; | |
| }; | |
| export function AppShell({ | |
| children, | |
| title, | |
| subtitle, | |
| eyebrow, | |
| actions, | |
| maxWidthClass = "max-w-7xl", | |
| }: AppShellProps) { | |
| const pathname = usePathname(); | |
| const router = useRouter(); | |
| const userEmail = useAuthStore((state) => state.userEmail); | |
| const logout = useAuthStore((state) => state.logout); | |
| const isActive = (href: string) => { | |
| if (href === "/") return pathname === "/"; | |
| return pathname === href || pathname?.startsWith(`${href}/`); | |
| }; | |
| const handleLogout = async () => { | |
| await logout(); | |
| router.replace("/login"); | |
| }; | |
| return ( | |
| <div className="min-h-screen bg-hero-gradient pb-24 lg:pb-12"> | |
| <header className="sticky top-0 z-30 border-b border-ink/10 bg-cream/85 backdrop-blur-md"> | |
| <div className={cn("mx-auto flex items-center justify-between gap-4 px-4 py-3 sm:px-6", maxWidthClass)}> | |
| <Link href="/" className="flex items-center gap-3"> | |
| <span className="flex h-10 w-10 items-center justify-center rounded-2xl bg-ink text-white shadow-sm"> | |
| <svg className="h-5 w-5 text-ember" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.2"> | |
| <rect x="2" y="6" width="13" height="12" rx="2.5" /> | |
| <path d="M15 10.5l6-3.5v10l-6-3.5v-3z" strokeLinecap="round" strokeLinejoin="round" /> | |
| </svg> | |
| </span> | |
| <div className="leading-tight"> | |
| <p className="font-display text-base font-semibold tracking-tight text-ink">Speech-Machine</p> | |
| <p className="text-[11px] font-medium uppercase tracking-[0.18em] text-ink/55"> | |
| Tech Presenter Coach | |
| </p> | |
| </div> | |
| </Link> | |
| <nav aria-label="Main navigation" className="hidden items-center gap-1 lg:flex"> | |
| {NAV_ITEMS.map((item) => { | |
| const active = isActive(item.href); | |
| return ( | |
| <Link | |
| key={item.href} | |
| href={item.href} | |
| className={cn( | |
| "flex items-center gap-2 rounded-full px-3.5 py-2 text-sm font-medium transition", | |
| active | |
| ? "bg-ink text-white shadow-sm" | |
| : "text-ink/75 hover:bg-ink/5 hover:text-ink" | |
| )} | |
| > | |
| {item.label} | |
| </Link> | |
| ); | |
| })} | |
| </nav> | |
| <div className="flex items-center gap-2"> | |
| {pathname !== "/record" && ( | |
| <Link href="/record" className="hidden sm:inline-flex"> | |
| <Button size="sm" className="bg-ember text-white hover:bg-ember/90"> | |
| Open Studio | |
| </Button> | |
| </Link> | |
| )} | |
| {userEmail && ( | |
| <span className="hidden max-w-[160px] truncate rounded-full bg-white/75 px-3 py-1.5 text-xs font-medium text-ink/70 xl:inline-block"> | |
| {userEmail} | |
| </span> | |
| )} | |
| <Button variant="ghost" size="sm" onClick={handleLogout} title="Sign out"> | |
| Sign out | |
| </Button> | |
| </div> | |
| </div> | |
| </header> | |
| <main className={cn("mx-auto px-4 pt-6 sm:px-6 lg:pt-8", maxWidthClass)}> | |
| {(title || actions) && ( | |
| <div className="mb-6 flex flex-col gap-4 sm:flex-row sm:items-end sm:justify-between"> | |
| <div className="space-y-1"> | |
| {eyebrow && ( | |
| <p className="text-xs font-semibold uppercase tracking-[0.26em] text-ember"> | |
| {eyebrow} | |
| </p> | |
| )} | |
| {title && ( | |
| <h1 className="font-display text-2xl font-semibold tracking-tight text-ink sm:text-3xl lg:text-4xl"> | |
| {title} | |
| </h1> | |
| )} | |
| {subtitle && <p className="max-w-3xl text-sm text-ink/70 sm:text-base">{subtitle}</p>} | |
| </div> | |
| {actions && <div className="flex flex-wrap items-center gap-2.5">{actions}</div>} | |
| </div> | |
| )} | |
| {children} | |
| </main> | |
| <nav | |
| aria-label="Mobile navigation" | |
| className="fixed inset-x-0 bottom-0 z-40 border-t border-ink/10 bg-cream/95 px-2 pb-[calc(env(safe-area-inset-bottom)+0.35rem)] pt-1.5 backdrop-blur-md lg:hidden" | |
| > | |
| <div className="mx-auto grid max-w-lg grid-cols-6 gap-1"> | |
| {NAV_ITEMS.map((item) => { | |
| const active = isActive(item.href); | |
| return ( | |
| <Link | |
| key={item.href} | |
| href={item.href} | |
| className={cn( | |
| "flex flex-col items-center justify-center gap-0.5 rounded-2xl py-1.5 text-[11px] font-medium transition", | |
| active ? "bg-ink/8 font-semibold text-ink" : "text-ink/65 hover:text-ink" | |
| )} | |
| > | |
| {item.icon(active)} | |
| <span className="truncate">{item.shortLabel}</span> | |
| </Link> | |
| ); | |
| })} | |
| </div> | |
| </nav> | |
| </div> | |
| ); | |
| } | |