Spaces:
Paused
Paused
File size: 8,228 Bytes
ec17af3 | 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 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 | "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>
);
}
|