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:
2026-09-05 00:33:28 -05:00
co-authored by Claude Sonnet 5
parent 8cea3f8b92
commit 1ae967c8a4
13 changed files with 411 additions and 20 deletions
+141
View File
@@ -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>
)
}