Skip to content

sitecrate-admin

sitecrate-admin is the internal control panel at admin.sitecrate.ca. It reads from and writes to the same Supabase projects table that sitecrate.ca’s intake form populates, and calls studio-website’s send-email function for all email — it has no email function of its own.

It is a React 18 SPA. src/App.jsx holds the view components; navigation uses a view state variable, not a router.

Auth: Supabase Auth (email + password). The session is managed by the Supabase JS client. On sign-in, the role is fetched from the user_roles table (linked by user_id). If active = false, the user is signed out immediately with a “deactivated” message. The role is stored in AuthContext (src/lib/auth.jsx) and available app-wide via useAuth().

Roles — features are fully hidden (not just disabled) for roles without access:

RoleAccess
adminEverything
engineerTechnical — stage, preview URL, build brief, admin notes
salesClient-facing — client info editing, revenue, analytics, comms
ModulePurpose
src/lib/auth.jsxAuthContext, AuthProvider, useAuth
src/lib/constants.jsPRICES, STAGES, labels, endpoint URLs
src/lib/utils.jsfmtDate, fmtCurrency, daysSince, isNew, isStuck, calcRevenue, deriveSlug
src/components/UsersPanel.jsxAdmin-only team management
src/components/ActivityPanel.jsxCombined project activity + email log
viewComponentPurpose
homeSitesHomeRevenue bar + grid of client cards + New project button
sitecrateSitecrateShellGA4 analytics for sitecrate.ca (7d / 30d)
clientClientSiteShellProject info strip, quick actions, pipeline kanban, email history

NewProjectModal+ New project in the SitesHome topbar. Creates projects without the intake form (for clients from calls/emails). Requires business name, client name, email; tier/billing/industry/domain/timeline optional. Generates status_token via crypto.randomUUID(), inserts to Supabase, and navigates to ClientSiteShell. Wrapped in try/finally so saving always resets.

DetailPanel — overlay opened from a kanban card. Stage selector (new → brief → building → review → live), slug input → Set .sitecrate.ca (writes https://{slug}.sitecrate.ca to preview_url), freeform preview URL (for live or non-sitecrate.ca URLs), client status-link copy, read-only brief, client feedback, admin notes, save. Shows an amber warning after save if the stage moved to review but no preview URL was set (the email was not sent).

BuildBriefModal⚡ Build brief in the ClientSiteShell action bar. Derives the slug via deriveSlug(preview_url, business_name) (strips .sitecrate.ca, falls back to a slugified business name, final fallback 'client'). Three copyable sections: preview URL, git clone command, and a Claude Code prompt pre-filled with all intake data. Tier-aware — Tier 1 prompt for presence, Tier 2 for growth.

Auto-sent on DetailPanel save. Both call https://sitecrate.ca/.netlify/functions/send-email:

TransitionConditionEmail type
reviewpreview URL must be setreview to client
live(none)launch to client

The review type returns HTTP 400 if preview_url is missing (server-side guard).

calcRevenue(projects) returns { mrr, oneTime, pipeline, customPipeline }:

  • mrr — live projects with monthly billing
  • oneTime — live projects with one-time billing
  • pipeline — non-live presence/growth projects, valued at face price
  • customPipeline — count of non-live platform projects (custom-quoted, excluded from pipeline $ to avoid misleading $0 entries)
PRICES = {
presence: { 'one-time': 997, monthly: 49 },
growth: { 'one-time': 3997, monthly: 149 },
platform: { 'one-time': 0, monthly: 0 },
// Legacy — kept so old projects show correct values
starter: { 'one-time': 1900, monthly: 89 },
standard: { 'one-time': 3400, monthly: 149 },
}
STAGE_WARN_DAYS = { new: 1, brief: 3, building: 7, review: 5, live: 999 }

isStuck(p)daysSince(p.stage_changed_at) > STAGE_WARN_DAYS[p.stage].

The Supabase channel projects-changes subscribes to postgres_changes on the projects table using import.meta.env.VITE_SUPABASE_SCHEMA || 'public', so staging real-time updates work correctly against the staging schema.

All three functions require a Bearer JWT and verify the caller against user_roles (an active row is required) before doing anything. Clients pass Authorization: Bearer ${session.access_token}.

FunctionEndpointAuthOutput
ga-report.jsPOST /.netlify/functions/ga-reportadmin or sales{ summary, pages, sources, events }
email-log.jsPOST /.netlify/functions/email-logany active memberLast 20 Resend emails matching subject
admin-users.jsPOST /.netlify/functions/admin-usersadmin onlyUser CRUD (list/create/update)

The studio send-email function is called with { type, token } (review/launch on stage change); it resolves the project from the DB by token server-side.

Terminal window
npm run dev # local dev server (port 5174 — main site uses 5173)
npm run build # production build → dist/
npm run preview # serve dist/ locally
npm run lint # ESLint — must pass before merging any PR