Wire the app to the Supabase backend, replacing localStorage-only
- DiagramRepository (renamed from ProjectRepository, per the organized-ideas.md §8 naming decision) now has a Supabase-backed implementation as the active repository. LocalStorageDiagramRepository stays in the codebase as a reference implementation / fallback, just no longer wired in. Only the storage layer's naming changed here -- the domain type, store, and UI copy still say "Project"; that's a separate, larger mechanical rename tracked on its own. - Minimal email/password auth gate (src/components/auth/LoginScreen.tsx) since Supabase RLS requires a real signed-in user to do anything -- this is NOT the Phase 2 experience (Google SSO, polished signup), just enough of the same schema (username + email + password) to make the backend foundation usable end to end before that phase exists. Respects the hard email-verification gate from config.toml. - .env.example documents the required VITE_SUPABASE_URL / VITE_SUPABASE_ANON_KEY (local dev values, not secrets); .env.local has the actual local values and is gitignored. Verified end-to-end against the local stack: signup creates a confirmed-pending user, the handle_new_user trigger creates their profile, sign-in is blocked until confirmed, and a signed-in session can upsert/read back its own diagram row exactly as the app's save()/load() do it. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017DUU6CnxECCDeqDNYJgr5x
This commit is contained in:
@@ -0,0 +1,141 @@
|
||||
import { useState } from 'react'
|
||||
import { supabase } from '../../data/supabaseClient'
|
||||
|
||||
/**
|
||||
* Bare-bones email/password auth gate — just enough for a real signed-in
|
||||
* session to exist so Supabase RLS (which requires auth.uid()) has someone
|
||||
* to authorize. This is NOT the Phase 2 auth experience from
|
||||
* organized-ideas.md §2 (Google SSO, polished signup, hard email-verification
|
||||
* UX) — it's the minimum viable version of the same schema (username +
|
||||
* email + password) so the backend foundation is actually usable end to end
|
||||
* before that phase exists. Confirmation email respects the same
|
||||
* hard-verification-gate config as Phase 2 will (supabase/config.toml).
|
||||
*/
|
||||
export default function LoginScreen() {
|
||||
const [mode, setMode] = useState<'sign-in' | 'sign-up'>('sign-in')
|
||||
const [email, setEmail] = useState('')
|
||||
const [password, setPassword] = useState('')
|
||||
const [username, setUsername] = useState('')
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [confirmSent, setConfirmSent] = useState(false)
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
setError(null)
|
||||
setLoading(true)
|
||||
try {
|
||||
if (mode === 'sign-in') {
|
||||
const { error } = await supabase.auth.signInWithPassword({ email, password })
|
||||
if (error) throw error
|
||||
} else {
|
||||
const { error } = await supabase.auth.signUp({
|
||||
email,
|
||||
password,
|
||||
options: { data: { username: username.trim() } },
|
||||
})
|
||||
if (error) throw error
|
||||
setConfirmSent(true)
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Something went wrong.')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
if (confirmSent) {
|
||||
return (
|
||||
<div className="flex h-screen items-center justify-center bg-slate-50">
|
||||
<div className="w-full max-w-sm rounded-lg border border-slate-200 bg-white p-6 text-center shadow-sm">
|
||||
<h1 className="text-sm font-semibold text-slate-800">Check your email</h1>
|
||||
<p className="mt-2 text-xs text-slate-500">
|
||||
We sent a confirmation link to <span className="font-medium">{email}</span>. You'll need to confirm
|
||||
before you can sign in.
|
||||
</p>
|
||||
<p className="mt-2 text-[11px] text-slate-400">
|
||||
Local dev: open Mailpit at{' '}
|
||||
<a href="http://127.0.0.1:54324" className="underline" target="_blank" rel="noreferrer">
|
||||
127.0.0.1:54324
|
||||
</a>{' '}
|
||||
to see it — no real email is sent.
|
||||
</p>
|
||||
<button
|
||||
onClick={() => {
|
||||
setConfirmSent(false)
|
||||
setMode('sign-in')
|
||||
}}
|
||||
className="mt-4 text-xs font-medium text-indigo-600 hover:underline"
|
||||
>
|
||||
Back to sign in
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-screen items-center justify-center bg-slate-50">
|
||||
<form onSubmit={handleSubmit} className="w-full max-w-sm rounded-lg border border-slate-200 bg-white p-6 shadow-sm">
|
||||
<h1 className="text-sm font-semibold text-indigo-700">AV Planner</h1>
|
||||
<p className="mt-1 text-xs text-slate-500">{mode === 'sign-in' ? 'Sign in' : 'Create an account'}</p>
|
||||
|
||||
<div className="mt-4 space-y-3">
|
||||
{mode === 'sign-up' && (
|
||||
<label className="block text-xs font-medium text-slate-600">
|
||||
Username
|
||||
<input
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
required
|
||||
className="mt-1 w-full rounded border border-slate-300 px-2 py-1.5 text-sm"
|
||||
/>
|
||||
</label>
|
||||
)}
|
||||
<label className="block text-xs font-medium text-slate-600">
|
||||
Email
|
||||
<input
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
required
|
||||
className="mt-1 w-full rounded border border-slate-300 px-2 py-1.5 text-sm"
|
||||
/>
|
||||
</label>
|
||||
<label className="block text-xs font-medium text-slate-600">
|
||||
Password
|
||||
<input
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
required
|
||||
minLength={6}
|
||||
className="mt-1 w-full rounded border border-slate-300 px-2 py-1.5 text-sm"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{error && <p className="mt-3 text-xs text-red-600">{error}</p>}
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="mt-4 w-full rounded bg-indigo-600 py-1.5 text-sm font-medium text-white hover:bg-indigo-500 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
>
|
||||
{loading ? 'Please wait…' : mode === 'sign-in' ? 'Sign in' : 'Create account'}
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setMode(mode === 'sign-in' ? 'sign-up' : 'sign-in')
|
||||
setError(null)
|
||||
}}
|
||||
className="mt-3 w-full text-center text-xs text-slate-500 hover:underline"
|
||||
>
|
||||
{mode === 'sign-in' ? "Don't have an account? Create one" : 'Already have an account? Sign in'}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user