Files
arcadia-admin/app/lib/resources.ts
jules f8cbf142b5 init: arcadia-admin — admin webapp for arcadia-core, cloned from vibespace
Initial commit. Spun up via the docs/STARTER.md recipe: cp from vibespace,
reset git, rename package, set brand to "Arcadia Admin" with Shield icon
in app/lib/identity.ts.

Inherits the full Crema sibling-lib wiring including @crema/arcadia-client
(typed HTTP + Phoenix Channels realtime against arcadia-core) and
@crema/arcadia-auth-ui (login/signup/password-reset/2FA forms). The /login
route already renders <LoginForm>; <ArcadiaProvider> in app/root.tsx reads
VITE_ARCADIA_URL (default localhost:4000) and VITE_ARCADIA_TENANT (default
"default").

CLAUDE.md and README rewritten to frame this as the admin app for
arcadia-core. docs/STARTER.md removed — arcadia-admin is a leaf consumer,
not a downstream starter.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 21:28:39 +10:00

158 lines
3.9 KiB
TypeScript

// Resource store — example domain entity.
// Backed by localStorage today, but written so each call is a single function
// you can swap with `api.get/post/put/del` once you have a real backend.
import { useEffect, useSyncExternalStore } from "react"
export type Resource = {
id: string
name: string
status: "active" | "paused" | "archived"
owner: string
createdAt: number
updatedAt: number
}
const STORAGE_KEY = "crema.resources"
const CHANGE_EVENT = "crema:resources-change"
function newId() {
return `r-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 6)}`
}
function readFromStorage(): Resource[] {
if (typeof window === "undefined") return []
try {
const raw = localStorage.getItem(STORAGE_KEY)
if (!raw) return []
const parsed = JSON.parse(raw)
if (!Array.isArray(parsed)) return []
return parsed.filter(
(r): r is Resource =>
r &&
typeof r.id === "string" &&
typeof r.name === "string" &&
["active", "paused", "archived"].includes(r.status) &&
typeof r.owner === "string" &&
typeof r.createdAt === "number" &&
typeof r.updatedAt === "number",
)
} catch {
return []
}
}
function write(items: Resource[]) {
if (typeof window === "undefined") return
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(items))
window.dispatchEvent(new CustomEvent(CHANGE_EVENT))
} catch {
/* quota */
}
}
// CRUD — these mirror what `api.get/post/put/del` would look like.
export function listResources(): Resource[] {
return readFromStorage()
}
export function createResource(input: {
name: string
owner: string
status?: Resource["status"]
}): Resource {
const now = Date.now()
const r: Resource = {
id: newId(),
name: input.name,
owner: input.owner,
status: input.status ?? "active",
createdAt: now,
updatedAt: now,
}
write([r, ...readFromStorage()])
return r
}
export function updateResource(
id: string,
patch: Partial<Omit<Resource, "id" | "createdAt">>,
): Resource | null {
const items = readFromStorage()
let updated: Resource | null = null
const next = items.map((r) => {
if (r.id !== id) return r
updated = { ...r, ...patch, updatedAt: Date.now() }
return updated
})
if (updated) write(next)
return updated
}
export function deleteResource(id: string) {
write(readFromStorage().filter((r) => r.id !== id))
}
let cached: Resource[] | null = null
function subscribe(cb: () => void) {
const onChange = () => {
cached = null
cb()
}
window.addEventListener(CHANGE_EVENT, onChange)
window.addEventListener("storage", (e) => {
if (e.key === STORAGE_KEY) onChange()
})
return () => window.removeEventListener(CHANGE_EVENT, onChange)
}
function getSnapshot(): Resource[] {
if (!cached) cached = readFromStorage()
return cached
}
function getServerSnapshot(): Resource[] {
return []
}
export function useResources(): Resource[] {
const v = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot)
useEffect(() => {
cached = null
}, [])
return v
}
/** Seed a few rows on first load so the table isn't empty. */
export function seedResourcesIfEmpty() {
if (typeof window === "undefined") return
if (localStorage.getItem(STORAGE_KEY)) return
const now = Date.now()
const seed: Resource[] = [
{
id: newId(),
name: "Acme dashboard",
status: "active",
owner: "Atlas",
createdAt: now - 86_400_000 * 3,
updatedAt: now - 3600_000,
},
{
id: newId(),
name: "Onboarding pipeline",
status: "paused",
owner: "Forge",
createdAt: now - 86_400_000 * 7,
updatedAt: now - 86_400_000,
},
{
id: newId(),
name: "Q1 report draft",
status: "archived",
owner: "Inkwell",
createdAt: now - 86_400_000 * 30,
updatedAt: now - 86_400_000 * 14,
},
]
write(seed)
}