Seed: fitness template (17 files)
This commit is contained in:
@@ -0,0 +1,133 @@
|
|||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet";
|
||||||
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogTrigger } from "@/components/ui/dialog";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { Menu, Dumbbell } from "lucide-react";
|
||||||
|
import { ThemeToggle } from "@/components/ThemeToggle";
|
||||||
|
import { useAuth } from "@/hooks/use-tygarun";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
|
||||||
|
function AuthDialog({ trigger }: { trigger: React.ReactNode }) {
|
||||||
|
const { user, isSignedIn, signup, signin, signout } = useAuth();
|
||||||
|
const [openAuth, setOpenAuth] = useState(false);
|
||||||
|
const [mode, setMode] = useState<"signin" | "signup">("signup");
|
||||||
|
const [name, setName] = useState("");
|
||||||
|
const [email, setEmail] = useState("");
|
||||||
|
const [password, setPassword] = useState("");
|
||||||
|
const [busy, setBusy] = useState(false);
|
||||||
|
|
||||||
|
if (isSignedIn) {
|
||||||
|
return (
|
||||||
|
<Button variant="ghost" size="sm" className="text-slate-300 hover:text-orange-400 uppercase tracking-wide font-bold" onClick={() => { signout(); toast.success("Signed out"); }}>
|
||||||
|
Sign Out{user?.email ? ` (\${user.email})` : ""}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const submit = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setBusy(true);
|
||||||
|
try {
|
||||||
|
if (mode === "signup") await signup({ name, email, password });
|
||||||
|
else await signin(email, password);
|
||||||
|
toast.success(mode === "signup" ? "Welcome to the crew!" : "Welcome back!");
|
||||||
|
setOpenAuth(false);
|
||||||
|
setName(""); setEmail(""); setPassword("");
|
||||||
|
} catch {
|
||||||
|
toast.error("Authentication failed. Please try again.");
|
||||||
|
} finally {
|
||||||
|
setBusy(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={openAuth} onOpenChange={setOpenAuth}>
|
||||||
|
<DialogTrigger asChild>{trigger}</DialogTrigger>
|
||||||
|
<DialogContent className="bg-slate-950 border-slate-800 text-white">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="uppercase tracking-wide">{mode === "signup" ? "Join the Gym" : "Member Login"}</DialogTitle>
|
||||||
|
<DialogDescription className="text-slate-400">{mode === "signup" ? "Create your member account to book classes and manage your membership." : "Sign in to your member account."}</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<form onSubmit={submit} className="space-y-4">
|
||||||
|
{mode === "signup" && (
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<Label htmlFor="auth-name">Name</Label>
|
||||||
|
<Input id="auth-name" value={name} onChange={(e) => setName(e.target.value)} required className="bg-slate-900 border-slate-700" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<Label htmlFor="auth-email">Email</Label>
|
||||||
|
<Input id="auth-email" type="email" value={email} onChange={(e) => setEmail(e.target.value)} required className="bg-slate-900 border-slate-700" />
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<Label htmlFor="auth-password">Password</Label>
|
||||||
|
<Input id="auth-password" type="password" value={password} onChange={(e) => setPassword(e.target.value)} required className="bg-slate-900 border-slate-700" />
|
||||||
|
</div>
|
||||||
|
<Button type="submit" disabled={busy} className="w-full bg-gradient-to-r from-orange-500 to-red-500 hover:from-orange-600 hover:to-red-600 text-white font-bold uppercase tracking-wide rounded-full">
|
||||||
|
{busy ? "Please wait…" : mode === "signup" ? "Create Account" : "Sign In"}
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
<button type="button" className="text-sm text-slate-400 hover:text-orange-400 mt-2" onClick={() => setMode(mode === "signup" ? "signin" : "signup")}>
|
||||||
|
{mode === "signup" ? "Already a member? Sign in" : "New here? Create an account"}
|
||||||
|
</button>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Header() {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const [scrolled, setScrolled] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fn = () => setScrolled(window.scrollY > 20);
|
||||||
|
window.addEventListener("scroll", fn);
|
||||||
|
return () => window.removeEventListener("scroll", fn);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const links = [
|
||||||
|
{ label: "Classes", to: "/classes" },
|
||||||
|
{ label: "Trainers", to: "/trainers" },
|
||||||
|
{ label: "Pricing", to: "/pricing" },
|
||||||
|
{ label: "Contact", to: "/contact" },
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<header className={`fixed top-0 inset-x-0 z-50 transition-all duration-300 \${scrolled ? "bg-slate-950/95 backdrop-blur-lg border-b border-slate-800 shadow-lg" : "bg-transparent"}`}>
|
||||||
|
<div className="mx-auto max-w-7xl flex items-center justify-between px-6 py-4">
|
||||||
|
<Link to="/" className="flex items-center gap-2 group">
|
||||||
|
<div className="p-1.5 rounded-lg bg-gradient-to-br from-orange-500 to-red-500 shadow-lg shadow-orange-500/25 group-hover:shadow-orange-500/40 transition-shadow">
|
||||||
|
<Dumbbell className="h-5 w-5 text-white" />
|
||||||
|
</div>
|
||||||
|
<span className="text-lg font-extrabold uppercase tracking-wider text-white">Apex Fitness</span>
|
||||||
|
</Link>
|
||||||
|
<nav className="hidden md:flex items-center gap-8">
|
||||||
|
{links.map((l) => (
|
||||||
|
<Link key={l.to} to={l.to} className={`text-sm font-semibold uppercase tracking-wide transition-colors hover:text-orange-400 \${scrolled ? "text-slate-300" : "text-white/90"}`}>{l.label}</Link>
|
||||||
|
))}
|
||||||
|
</nav>
|
||||||
|
<div className="hidden md:flex items-center gap-3">
|
||||||
|
<ThemeToggle />
|
||||||
|
<AuthDialog trigger={<Button size="sm" className="bg-gradient-to-r from-orange-500 to-red-500 hover:from-orange-600 hover:to-red-600 text-white font-bold uppercase tracking-wide rounded-full px-6 shadow-lg shadow-orange-500/25 active:scale-[0.97] transition-all">Join Now</Button>} />
|
||||||
|
</div>
|
||||||
|
<Sheet open={open} onOpenChange={setOpen}>
|
||||||
|
<SheetTrigger asChild className="md:hidden">
|
||||||
|
<Button variant="ghost" size="icon" className="text-white"><Menu className="h-5 w-5" /></Button>
|
||||||
|
</SheetTrigger>
|
||||||
|
<SheetContent side="right" className="w-72 bg-slate-950 border-slate-800">
|
||||||
|
<nav className="mt-8 flex flex-col gap-4">
|
||||||
|
{links.map((l) => (
|
||||||
|
<Link key={l.to} to={l.to} className="text-lg font-semibold uppercase text-slate-200 hover:text-orange-400 transition-colors" onClick={() => setOpen(false)}>{l.label}</Link>
|
||||||
|
))}
|
||||||
|
<AuthDialog trigger={<Button className="mt-4 w-full bg-gradient-to-r from-orange-500 to-red-500 hover:from-orange-600 hover:to-red-600 text-white font-bold uppercase rounded-full active:scale-[0.97] transition-all">Join Now</Button>} />
|
||||||
|
<ThemeToggle />
|
||||||
|
</nav>
|
||||||
|
</SheetContent>
|
||||||
|
</Sheet>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user