profileShare

rasmusjy / tasteprint

Read-only snapshot

No repository description.

main default branch 169 files Expires Sep 13, 2026, 9:06 AM
AuthPage.tsx 4,978 bytes
1 import { useState, type FormEvent } from "react";
2 import { ArrowLeft, ArrowRight, LoaderCircle, Stamp } from "lucide-react";
3 import { Link, Navigate, useLocation, useNavigate, useSearchParams } from "react-router-dom";
4 import { Logo } from "../components/Logo";
5 import { useAuth } from "../lib/auth";
6 import { ApiError } from "../lib/api";
7
8 export function AuthPage({ mode }: { mode: "login" | "register" }) {
9 const { user, loading, login, register } = useAuth();
10 const [params] = useSearchParams();
11 const navigate = useNavigate();
12 const location = useLocation();
13 const [displayName, setDisplayName] = useState("");
14 const [email, setEmail] = useState(params.get("demo") === "true" ? "demo@tasteprint.app" : "");
15 const [password, setPassword] = useState(params.get("demo") === "true" ? "tasteprint" : "");
16 const [submitting, setSubmitting] = useState(false);
17 const [error, setError] = useState("");
18
19 if (!loading && user) {
20 return <Navigate to={params.get("next") || "/app"} replace />;
21 }
22
23 async function submit(event: FormEvent) {
24 event.preventDefault();
25 setSubmitting(true);
26 setError("");
27 try {
28 if (mode === "register") {
29 await register(displayName, email, password);
30 } else {
31 await login(email, password);
32 }
33 navigate(params.get("next") || "/app", { replace: true });
34 } catch (caught) {
35 setError(caught instanceof ApiError ? caught.message : "Sign in could not be completed.");
36 } finally {
37 setSubmitting(false);
38 }
39 }
40
41 async function openDemo() {
42 setEmail("demo@tasteprint.app");
43 setPassword("tasteprint");
44 setSubmitting(true);
45 setError("");
46 try {
47 await login("demo@tasteprint.app", "tasteprint");
48 navigate(params.get("next") || "/app", { replace: true });
49 } catch (caught) {
50 setError(caught instanceof ApiError ? caught.message : "The demo could not be opened.");
51 } finally {
52 setSubmitting(false);
53 }
54 }
55
56 return (
57 <main className="auth-page">
58 <section className="auth-story">
59 <Logo />
60 <div>
61 <span className="eyebrow">Your edible travel record</span>
62 <h1>Every destination leaves a different mark.</h1>
63 <div className="auth-stamps" aria-hidden="true">
64 <span className="auth-stamp auth-stamp-jp">JP<small>44%</small></span>
65 <span className="auth-stamp auth-stamp-pt">PT<small>68%</small></span>
66 <span className="auth-stamp auth-stamp-mx">MX<small>35%</small></span>
67 </div>
68 </div>
69 <p>Built for travellers who remember a city by what was on the table.</p>
70 </section>
71
72 <section className="auth-panel">
73 <Link className="back-link" to="/"><ArrowLeft size={16} /> Back</Link>
74 <div className="auth-form-wrap">
75 <span className="auth-seal"><Stamp size={24} /></span>
76 <span className="eyebrow">{mode === "login" ? "Welcome back" : "Issue your passport"}</span>
77 <h2>{mode === "login" ? "Continue your food map" : "Start your Tasteprint"}</h2>
78
79 <form className="auth-form" onSubmit={submit}>
80 {mode === "register" && (
81 <label>
82 <span>Name</span>
83 <input value={displayName} onChange={event => setDisplayName(event.target.value)}
84 autoComplete="name" maxLength={80} required />
85 </label>
86 )}
87 <label>
88 <span>Email</span>
89 <input type="email" value={email} onChange={event => setEmail(event.target.value)}
90 autoComplete="email" required />
91 </label>
92 <label>
93 <span>Password</span>
94 <input type="password" value={password} onChange={event => setPassword(event.target.value)}
95 autoComplete={mode === "login" ? "current-password" : "new-password"}
96 minLength={8} maxLength={72} required />
97 </label>
98 {error && <p className="form-error" role="alert">{error}</p>}
99 <button className="button button-primary button-full" type="submit" disabled={submitting}>
100 {submitting ? <LoaderCircle className="spin" size={18} /> : <ArrowRight size={18} />}
101 {mode === "login" ? "Sign in" : "Create passport"}
102 </button>
103 </form>
104
105 {mode === "login" && (
106 <button className="button button-outline button-full" type="button" onClick={() => void openDemo()} disabled={submitting}>
107 Open demo passport
108 </button>
109 )}
110
111 <p className="auth-switch">
112 {mode === "login" ? "New here?" : "Already have a passport?"}{" "}
113 <Link to={mode === "login" ? "/register" : "/login"} state={{ from: location }}>
114 {mode === "login" ? "Create an account" : "Sign in"}
115 </Link>
116 </p>
117 </div>
118 </section>
119 </main>
120 );
121 }
122