Spaces:
Sleeping
Sleeping
| import { useState, useEffect } from "react"; | |
| import { useNavigate } from "react-router-dom"; | |
| import { supabase } from "@/integrations/supabase/client"; | |
| import { useAuth } from "@/contexts/AuthContext"; | |
| import { Button } from "@/components/ui/button"; | |
| import { Input } from "@/components/ui/input"; | |
| import { Label } from "@/components/ui/label"; | |
| import { toast } from "sonner"; | |
| import { Brain, Sparkles } from "lucide-react"; | |
| const LAST_NAME_KEY = "distractiq.lastUsername"; | |
| const NAME_REGEX = /^[a-zA-Z0-9_]{3,24}$/; | |
| // Deterministic synthetic credentials so a username always maps to the same account. | |
| const toEmail = (name: string) => `${name.toLowerCase()}@distractiq.local`; | |
| // Note: not security-grade — by design, no password to remember. | |
| const toPassword = (name: string) => `diq_${name.toLowerCase()}_focus_2025`; | |
| export default function Auth() { | |
| const navigate = useNavigate(); | |
| const { user, loading } = useAuth(); | |
| const [busy, setBusy] = useState(false); | |
| const [username, setUsername] = useState(""); | |
| const [remembered, setRemembered] = useState<string | null>(null); | |
| useEffect(() => { | |
| document.title = "Welcome · DistractIQ"; | |
| const saved = localStorage.getItem(LAST_NAME_KEY); | |
| if (saved) { | |
| setUsername(saved); | |
| setRemembered(saved); | |
| } | |
| }, []); | |
| useEffect(() => { | |
| if (!loading && user) navigate("/", { replace: true }); | |
| }, [user, loading, navigate]); | |
| const enter = async (e: React.FormEvent) => { | |
| e.preventDefault(); | |
| const name = username.trim(); | |
| if (!NAME_REGEX.test(name)) { | |
| toast.error("Use 3–24 letters, numbers, or underscores."); | |
| return; | |
| } | |
| setBusy(true); | |
| const email = toEmail(name); | |
| const password = toPassword(name); | |
| // Try sign-in first (returning user) | |
| const { error: signInErr } = await supabase.auth.signInWithPassword({ email, password }); | |
| if (signInErr) { | |
| // Likely first-time — create account | |
| const { error: signUpErr } = await supabase.auth.signUp({ | |
| email, | |
| password, | |
| options: { | |
| emailRedirectTo: `${window.location.origin}/`, | |
| data: { username: name }, | |
| }, | |
| }); | |
| if (signUpErr) { | |
| setBusy(false); | |
| toast.error(signUpErr.message); | |
| return; | |
| } | |
| // After sign-up, session is created automatically (auto-confirm on) | |
| toast.success(`Welcome, ${name}! ✨`); | |
| } else { | |
| toast.success(`Welcome back, ${name} 👋`); | |
| } | |
| localStorage.setItem(LAST_NAME_KEY, name); | |
| setBusy(false); | |
| navigate("/", { replace: true }); | |
| }; | |
| const useDifferent = () => { | |
| setUsername(""); | |
| setRemembered(null); | |
| }; | |
| return ( | |
| <main className="min-h-screen flex items-center justify-center p-6"> | |
| <div className="w-full max-w-md glass-strong rounded-3xl p-8 animate-fade-in"> | |
| <div className="flex items-center justify-center gap-2 mb-2"> | |
| <div className="relative"> | |
| <Brain className="h-9 w-9 text-primary" /> | |
| <div className="absolute inset-0 blur-xl bg-primary/40" /> | |
| </div> | |
| <h1 className="text-3xl font-display font-bold"> | |
| Distract<span className="text-gradient-emerald">IQ</span> | |
| </h1> | |
| </div> | |
| <p className="text-center text-muted-foreground text-sm mb-8"> | |
| Quantify your focus. Beat your distractions. | |
| </p> | |
| {remembered && ( | |
| <div className="mb-5 rounded-2xl bg-primary/10 border border-primary/30 px-4 py-3 text-sm flex items-center gap-2 animate-fade-in"> | |
| <Sparkles className="h-4 w-4 text-primary shrink-0" /> | |
| <span> | |
| Welcome back, <span className="font-semibold text-primary">{remembered}</span>. Tap continue to jump in. | |
| </span> | |
| </div> | |
| )} | |
| <form onSubmit={enter} className="space-y-4"> | |
| <div> | |
| <Label htmlFor="name" className="text-xs uppercase tracking-widest text-muted-foreground"> | |
| Your name | |
| </Label> | |
| <Input | |
| id="name" | |
| autoFocus | |
| required | |
| minLength={3} | |
| maxLength={24} | |
| value={username} | |
| onChange={(e) => setUsername(e.target.value)} | |
| placeholder="focus_ninja" | |
| className="mt-1.5 h-12 text-lg font-display" | |
| /> | |
| <p className="text-[11px] text-muted-foreground mt-1.5"> | |
| 3–24 characters · letters, numbers, underscores. Same name later = same account. | |
| </p> | |
| </div> | |
| <Button | |
| type="submit" | |
| disabled={busy} | |
| className="w-full h-12 bg-gradient-emerald text-primary-foreground font-semibold text-base" | |
| > | |
| {busy ? "Entering..." : remembered ? `Continue as ${remembered || username}` : "Enter DistractIQ →"} | |
| </Button> | |
| {remembered && ( | |
| <button | |
| type="button" | |
| onClick={useDifferent} | |
| className="w-full text-xs text-muted-foreground hover:text-foreground transition" | |
| > | |
| Use a different name | |
| </button> | |
| )} | |
| </form> | |
| <p className="mt-6 text-[10px] text-center text-muted-foreground/70 leading-relaxed"> | |
| No password, no email — your name is your key. Pick something unique to you so no one else claims it. | |
| </p> | |
| </div> | |
| </main> | |
| ); | |
| } | |