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,18 @@
|
||||
import type { Project } from '../domain/types'
|
||||
|
||||
/**
|
||||
* Storage abstraction the rest of the app codes against. `LocalStorageDiagramRepository`
|
||||
* and `SupabaseDiagramRepository` both implement it — the store/UI layers don't need to
|
||||
* know or care which one is active.
|
||||
*
|
||||
* Named for the "Diagram" terminology decided in organized-ideas.md §8 (renamed from
|
||||
* "Project"). This is a deliberately scoped rename: only the storage layer's naming
|
||||
* changed here. The domain type (`Project`), the Zustand store (`useProjectStore`), and
|
||||
* user-facing copy still say "Project" — that's a separate, larger mechanical rename
|
||||
* across the whole app, tracked as its own task rather than bundled into this one.
|
||||
*/
|
||||
export interface DiagramRepository {
|
||||
load(): Promise<Project | null>
|
||||
save(project: Project): Promise<void>
|
||||
clear(): Promise<void>
|
||||
}
|
||||
+4
-2
@@ -1,10 +1,12 @@
|
||||
import type { Project } from '../domain/types'
|
||||
import type { DiagramRepository } from './DiagramRepository'
|
||||
import { normalizeProject } from './exportImport'
|
||||
import type { ProjectRepository } from './ProjectRepository'
|
||||
|
||||
const STORAGE_KEY = 'av-planner:project:v1'
|
||||
|
||||
export class LocalStorageProjectRepository implements ProjectRepository {
|
||||
/** Kept around (no longer the active repository — see projectStore.ts) as a
|
||||
* reference implementation and a safe fallback if Supabase is unreachable. */
|
||||
export class LocalStorageDiagramRepository implements DiagramRepository {
|
||||
async load(): Promise<Project | null> {
|
||||
try {
|
||||
const raw = window.localStorage.getItem(STORAGE_KEY)
|
||||
@@ -1,13 +0,0 @@
|
||||
import type { Project } from '../domain/types'
|
||||
|
||||
/**
|
||||
* Storage abstraction the rest of the app codes against. Today only
|
||||
* `LocalStorageProjectRepository` implements it; a future backend would add
|
||||
* e.g. `ApiProjectRepository` (REST/GraphQL) behind this same interface so
|
||||
* the store/UI layers wouldn't need to change.
|
||||
*/
|
||||
export interface ProjectRepository {
|
||||
load(): Promise<Project | null>
|
||||
save(project: Project): Promise<void>
|
||||
clear(): Promise<void>
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import type { Project } from '../domain/types'
|
||||
import type { DiagramRepository } from './DiagramRepository'
|
||||
import { normalizeProject } from './exportImport'
|
||||
import { supabase } from './supabaseClient'
|
||||
|
||||
/**
|
||||
* Backs the app with the local (or eventually production) Supabase
|
||||
* `diagrams` table instead of localStorage. The whole `Project` domain
|
||||
* object is stored as-is in the `data` JSONB column — see the schema
|
||||
* migration's header comment for why diagrams are a document, not
|
||||
* normalized relational rows.
|
||||
*
|
||||
* Single-diagram scope for now, matching the app's current UI (one active
|
||||
* project at a time): `load()` returns the signed-in user's most recently
|
||||
* updated diagram, `save()` upserts by the diagram's own id. Multi-diagram
|
||||
* management (list/switch between several) is a later phase.
|
||||
*/
|
||||
export class SupabaseDiagramRepository implements DiagramRepository {
|
||||
async load(): Promise<Project | null> {
|
||||
const {
|
||||
data: { user },
|
||||
} = await supabase.auth.getUser()
|
||||
if (!user) return null
|
||||
|
||||
const { data: row, error } = await supabase
|
||||
.from('diagrams')
|
||||
.select('data')
|
||||
.order('updated_at', { ascending: false })
|
||||
.limit(1)
|
||||
.maybeSingle()
|
||||
|
||||
if (error) {
|
||||
console.error('Failed to load diagram from Supabase', error)
|
||||
return null
|
||||
}
|
||||
if (!row) return null
|
||||
return normalizeProject(row.data as Project)
|
||||
}
|
||||
|
||||
async save(project: Project): Promise<void> {
|
||||
const {
|
||||
data: { user },
|
||||
} = await supabase.auth.getUser()
|
||||
if (!user) {
|
||||
console.error('Cannot save diagram: not signed in')
|
||||
return
|
||||
}
|
||||
|
||||
const { error } = await supabase.from('diagrams').upsert({
|
||||
id: project.id,
|
||||
name: project.name,
|
||||
owner_id: user.id,
|
||||
data: project,
|
||||
updated_at: new Date().toISOString(),
|
||||
})
|
||||
|
||||
if (error) {
|
||||
console.error('Failed to save diagram to Supabase', error)
|
||||
}
|
||||
}
|
||||
|
||||
async clear(): Promise<void> {
|
||||
const {
|
||||
data: { user },
|
||||
} = await supabase.auth.getUser()
|
||||
if (!user) return
|
||||
|
||||
const { error } = await supabase.from('diagrams').delete().eq('owner_id', user.id)
|
||||
if (error) {
|
||||
console.error('Failed to clear diagrams from Supabase', error)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { createClient } from '@supabase/supabase-js'
|
||||
|
||||
const url = import.meta.env.VITE_SUPABASE_URL
|
||||
const anonKey = import.meta.env.VITE_SUPABASE_ANON_KEY
|
||||
|
||||
if (!url || !anonKey) {
|
||||
throw new Error(
|
||||
'Missing VITE_SUPABASE_URL / VITE_SUPABASE_ANON_KEY. Copy .env.example to .env.local and fill them in ' +
|
||||
'(run `npx supabase start` then `npx supabase status` for local values).',
|
||||
)
|
||||
}
|
||||
|
||||
export const supabase = createClient(url, anonKey)
|
||||
Reference in New Issue
Block a user