Appearance
02 — Admin Frontend (React SPA)
The v3 Admin Frontend is a React 18 + Vite 5 single-page application located at /var/www/togoactive-development/admin-frontend. It is the operator console for the TogoActive platform: platform-wide dashboards (events, users, participants, activities) plus a deep per-event management shell covering setup, registration, teams, rewards, donations, content, leaderboards, achievements, communications, payments and operations. It is plain JavaScript (JSX, no TypeScript), styled with Tailwind CSS, and talks exclusively to the admin backend Laravel API under /api/v1. Two subsystems — the Page Builder and the Registration Form Builder — embed the WL public site (Next.js) in preview iframes and drive it over postMessage, so admins edit against a pixel-accurate render of the real public site.
Related docs: ./01-architecture-overview.md · ./03-admin-backend-api.md · ./05-wl-frontend.md · ./08-event-setup-guide.md · ./09-developer-guide.md
1. Stack & repo layout
| Concern | Choice |
|---|---|
| Framework | React 18.3 (function components + hooks only) |
| Build tool | Vite 5.4 (@vitejs/plugin-react) |
| Language | JavaScript / JSX — no TypeScript |
| Routing | react-router-dom v6 (createBrowserRouter) |
| Styling | Tailwind CSS 3.4 + PostCSS/Autoprefixer |
| Dialogs | sweetalert2 via the local src/utils/swal.js wrapper (never native alert/confirm/prompt) |
| Drag & drop | @dnd-kit/* (core, sortable, modifiers, utilities) |
| Rich text | @tiptap/* (starter-kit, link, underline, text-align, color, text-style) |
| Image cropping | react-image-crop v11 (current cropper; react-easy-crop remains in package.json but the shared cropper is react-image-crop) |
| Icons | lucide-react |
| Sanitisation | dompurify |
| Color picker | react-colorful |
Directory layout under src/:
src/
main.jsx # entry: mounts RouterProvider
router.jsx # ALL routes (createBrowserRouter)
index.css # Tailwind entry
layouts/ # MainLayout, EventLayout
pages/ # top-level pages (DashboardPage, EventsPage, …) + pages/auth/*
components/
Sidebar.jsx, Header.jsx, ProtectedRoute.jsx, …
shared/ # ImageUploadWithCropper, ImageCropModal, SetupHelpPanel, …
avatars/ # GlobalAvatars
event-manage/ # everything under /events/:eventId
EventSidebar.jsx, EventHeader.jsx, EventDashboard.jsx, EventSectionPlaceholder.jsx
setup/ registration/ teams/ rewards/ donations/ leaderboard/
achievements/ content/ communications/ payments/ operations/ analytics/
content/builder/ # Page Builder subsystem
registration/builder/ # Form Builder subsystem
contexts/ # AuthContext
hooks/ # useEvents, useEventById, useAdminUsers, useFeatureSettings, …
data/ # eventSidebarMenu.js, fieldTypeRegistry.js, …
utils/ # api.js, swal.js, featureGate.js, sectionMapping.js,
# imageUploadConfig.js, regFieldPreview.js, dateValidation.js, …Per-event feature pages live under src/components/event-manage/<group>/, not src/pages/ — src/pages/ holds only the global (non-event) pages and auth screens.
2. Routing
All routes are defined in src/router.jsx with createBrowserRouter and mounted in src/main.jsx. Everything except the auth screens is wrapped in <ProtectedRoute /> (spinner while auth resolves, redirect to /login when unauthenticated).
2.1 Public (auth) routes
| Path | Component |
|---|---|
/login | pages/auth/LoginPage |
/register | pages/auth/RegisterPage |
/forgot-password | pages/auth/ForgotPasswordPage |
/reset-password | pages/auth/ResetPasswordPage |
2.2 MainLayout routes (global shell: Sidebar + Header)
| Path | Component | Notes |
|---|---|---|
/ | — | Redirects to /dashboard |
/dashboard | DashboardPage | Platform stats |
/events | EventsPage | Event list |
/events/new | EventsPage (create mode) | Same component, create flow |
/users | UsersPage | Admin users + roles |
/participants | ParticipantsPage | Cross-event participants |
/activities | ActivitiesPage | Cross-event activities |
/settings | SettingsPage | Global settings |
/avatars/global | GlobalAvatars | Global avatar pool |
pages/EventOnboardingPage.jsx exists in the tree but is not routed anywhere (see Known quirks).
2.3 EventLayout routes (per-event shell: EventSidebar + EventHeader)
Base path /events/:eventId. EventLayout fetches the event (via useEventById) and exposes it to children through useOutletContext() — every event page reads event data from context rather than refetching.
| Route (relative) | Component |
|---|---|
| (index) | EventDashboard |
analytics | EventAnalytics |
setup/general | SetupGeneral |
setup/dates | SetupDates |
setup/branding | SetupBranding |
setup/appearance | SetupAppearance |
setup/host | SetupHost |
setup/social | SetupSocial |
setup/seo | SetupSeo |
setup/domain | SetupDomain |
setup/email-branding | SetupEmailBranding |
setup/email-templates | SetupEmailTemplates |
setup/default-messages | SetupDefaultMessages |
setup/features | FeatureSettings |
setup/integrations | SetupIntegrations |
setup/avatars | SetupAvatars |
setup/members | SetupMembers |
registration/settings | RegistrationSettings |
registration/form-builder | FormBuilder |
teams/settings | TeamSettings |
teams/list | AllTeams |
rewards/skus | AllSkus |
rewards/instructions | RewardInstructions |
rewards/discounts | RewardDiscounts |
rewards/coupons | CouponManager |
donations/config | DonationConfig |
donations/fundraiser | FundraiserGoals |
leaderboard/general | LeaderboardGeneral |
leaderboard/tabs | LeaderboardTabs |
leaderboard/highlights | LeaderboardHighlights |
leaderboard/sync | LeaderboardSync |
achievements/groups | AchievementGroups |
achievements/list | AchievementList |
achievements/new, achievements/:achievementId | AchievementForm |
content/pages | PageList |
content/pages/new, content/pages/:pageId | PageBuilder |
content/menu-setup | MenuSetup |
content/faq | FaqManager |
content/landing-faq | LandingFaqManager |
content/rules | RulesManager |
operations/participants | EventParticipants |
operations/activity-manager | EventActivities |
operations/port-users | PortUsers |
communications/automation | AutomationRules |
communications/email-logs | EmailLogs |
payments/gateway | PaymentGateway |
payments/transactions | TransactionHistory |
* (catch-all) | EventSectionPlaceholder ("coming soon") |
2.4 Section keys ↔ paths
src/utils/sectionMapping.js translates between sidebar section keys and URL sub-paths: pathToSectionKey(subPath) and sectionKeyToPath(sectionKey). Sidebar items use dotted keys (setup.general, rewards.coupons, …); the mapping produces /events/:id/setup/general etc. Menu items whose keys map to no registered route (communications.notifications, operations.resync, all developer.* keys) resolve to the catch-all and render the EventSectionPlaceholder.
3. Navigation & menus
3.1 Main sidebar (src/components/Sidebar.jsx)
Flat, ungated list: Dashboard, Events, Users, Participants, Activities, Settings. No feature gating applies at this level.
3.2 Event sidebar — canonical menu reference
Component: src/components/event-manage/EventSidebar.jsx; data: src/data/eventSidebarMenu.js. Behavior:
- Accordion groups — parents with children expand/collapse; a group with
children: null(Event Dashboard, Analytics) is a direct link. - Flyout when collapsed — with the sidebar collapsed to icons, hovering a group opens a flyout panel with its children.
- Badges — a numeric
badgeon a group renders a count pill: Communications carriesbadge: 2and Operationsbadge: 12(static values in the menu data). - Placeholder fallback — any menu item without a registered route lands on
EventSectionPlaceholdervia the*catch-all ("coming soon" panel), so unfinished sections are navigable without crashing.
Full menu with feature gates (a blank Feature gate = always visible):
| Group | Item | Section key | Feature gate |
|---|---|---|---|
| (top level) | Event Dashboard | event-dashboard | — |
| (top level) | Analytics | analytics | — |
| Setup | General Info | setup.general | — |
| Dates & Schedule | setup.dates | — | |
| Branding & Images | setup.branding | — | |
| Appearance | setup.appearance | — | |
| Event Host | setup.host | — | |
| Social & SEO | setup.social | — | |
| SEO Templates | setup.seo | — | |
| Custom Domain | setup.domain | — | |
| Email Branding | setup.email-branding | — | |
| Email Templates | setup.email-templates | — | |
| Default Messages | setup.default-messages | — | |
| Feature Settings | setup.features | — | |
| Integrations | setup.integrations | — | |
| Avatars | setup.avatars | — | |
| Members & Access | setup.members | — | |
| Registration | Registration Settings | registration.settings | — |
| Form Builder | registration.form-builder | registration.form_builder | |
Teams (group gate: teams.enabled) | Team Settings | teams.settings | — |
| All Teams | teams.list | — | |
| Rewards | All SKUs | rewards.skus | — |
| Instructions | rewards.instructions | — | |
| Discounts | rewards.discounts | rewards.discounts | |
| Coupon Manager | rewards.coupons | rewards.coupon_manager | |
| Donations | Donation Config | donations.config | — |
| Fundraiser Goals | donations.fundraiser | — | |
| Content | Pages | content.pages | — |
| Menu Setup | content.menu-setup | — | |
| FAQ Manager | content.faq | — | |
| Landing Page FAQ | content.landing-faq | — | |
| Rules Manager | content.rules | — | |
| Leaderboard | General Settings | leaderboard.general | — |
| Event Highlights | leaderboard.highlights | leaderboard.event_highlights | |
| Sync Settings | leaderboard.sync | leaderboard.gallery_sync | |
Achievements (group gate: achievements.enabled) | Achievement Groups | achievements.groups | — |
| Achievement List | achievements.list | — | |
| Communications (badge 2) | Notifications | communications.notifications | — (unrouted → placeholder) |
| Automation Rules | communications.automation | — | |
| Email Logs | communications.email-logs | — | |
| Payments | Payment Gateway | payments.gateway | — |
| Transaction History | payments.transactions | — | |
| Operations (badge 12) | Participants | operations.participants | — |
| Activity Manager | operations.activity-manager | operations.activity_manager | |
| Re-sync Activities | operations.resync | operations.resync_activities (unrouted → placeholder) | |
| Port Users | operations.port-users | operations.port_users | |
| Developer Tools | Configuration Store | developer.config-store | — (unrouted → placeholder) |
| Custom CSS / JS | developer.custom-code | — (unrouted → placeholder) | |
| OneSignal Setup | developer.onesignal | — (unrouted → placeholder) |
4. Feature gating in the UI
src/utils/featureGate.js is the single gate for menu/tab visibility:
isFeatureEnabled(fs, key)— returnstruewhen the key is absent from the feature-settings map (default = visible), otherwise the flag's boolean. New features are therefore visible until a mode default or admin override says otherwise.filterByFeature(groups, fs)— filters both groups (afeatureKeyon the group, e.g.teams.enabled, hides the whole accordion) and children; a group whose children are all filtered out is dropped entirely.- The feature-settings map (
fs) comes fromevent.featureSettings, loaded byuseEventByIdand distributed throughEventLayout's outlet context. - Gating is per-event feature configuration, not user permission — role enforcement is server-side (
CheckEventAccess; viewers are read-only at the API level). The sidebar shows the same menu to every role. - In-page gating exists too:
FormBuildershows/hides its Merchandise and Upgrade tabs based onregistration.merchandise. - When flags are saved in Setup → Feature Settings, the page calls
refetch({ silent: true })on the event so the sidebar re-filters immediately without a loading flash.
Flags themselves (~130 features / 8 groups, mode-aware defaults for Standard / Seasonal / Donation events) are managed by the backend FeatureRegistry/FeatureService; see ./03-admin-backend-api.md.
5. Page-by-page reference
All endpoints below are relative to the API base /api/v1. Pages follow the settings-page state pattern (Section 6) unless noted.
5.1 Global pages (MainLayout)
| Page | Endpoints | Behavior |
|---|---|---|
DashboardPage | GET /dashboard/stats | Platform-wide KPI cards. |
EventsPage | GET /events (via useEvents) | Event list; /events/new renders the same page in create mode. |
UsersPage | useAdminUsers: CRUD /users, PUT /users/:id/events; useRoles: /roles CRUD | Admin user management incl. per-user event assignment and role management. |
ParticipantsPage | GET /participants, GET /participants/stats | Cross-event participant listing with stats. |
ActivitiesPage | GET /activities + stats | Cross-event activity listing. |
GlobalAvatars | /avatars/global CRUD + reorder + category-order | Global avatar pool (gender/category-matched auto-assignment source). |
SettingsPage | — | Global admin settings. |
5.2 Setup
| Page | Endpoints | Notable UX |
|---|---|---|
SetupGeneral | PUT /events/:id/general | Core event fields (name, mode, etc.). |
SetupDates | PUT /events/:id/dates | All 8 event date fields; client-side dateValidation.js — Save disabled until dates are consistent. |
SetupBranding | POST /events/:id/images (upload), PATCH /events/:id/images/:slot/url, DELETE slot | Uploading alone does NOT persist — the bucket upload returns a URL that must then be PATCHed onto the slot. Slot keys are canonical; never remap them through IMAGE_FIELD_MAPPING. |
SetupAppearance | GET/PUT /events/:id/appearance | Design tokens (colors, fonts, radii, header, hero CTA) edited via sub-cards ThemeColorsCard/FontCard/RadiiCard/HeaderCard/HeroCtaCard; live WL preview iframe; copy-from-event and reset actions. |
SetupHost | GET/PUT /events/:id/host-config + search-users + create-user | Pick or create the host account; host avatar cascade is backend-side. |
SetupSocial | POST (upload) /events/:id/social-seo | Social links + share image (multipart). |
SetupSeo | GET/PUT /events/:id/meta-templates/:type + meta-tokens | Per-page-type SEO templates with token insertion. |
SetupDomain | GET/POST/DELETE /events/:id/domain + verify-dns/verify-txt/sync | Custom-domain lifecycle with DNS and TXT verification steps. |
SetupEmailBranding | GET/PUT /events/:id/email-branding | Header/footer/sender branding for WL emails. |
SetupEmailTemplates | GET/PUT /events/:id/email-templates | Live preview iframe + "Send Test" — both go through getWlApiUrl() (VITE_WL_API_URL), which points at the production WL API (wl-api.togoparts.com), not this box. A stale deploy there produces preview-only bugs that don't reproduce locally. |
SetupDefaultMessages | GET/PUT /events/:id/default-messages | Default copy blocks (share text etc.); new admin copy fields extend event_default_message, not new config rows. |
FeatureSettings | GET/PUT /events/:id/feature-settings, POST …/reset (via useFeatureSettings) | Search with / keyboard shortcut; All/Enabled/Disabled/Overridden tabs; per-flag reset; mode banner (Standard/Seasonal/Donation); save silently refetches the event so the sidebar updates. |
SetupIntegrations | GET /events/:id/integrations; Google connect/select/properties/create/disconnect; POST/DELETE tag | Tag providers (GA4/GTM/Meta Pixel/Contentsquare) with OAuth-based GA4 property flow. |
SetupAvatars | Per-event avatar CRUD + reorder | Per-event overrides of the global avatar pool. |
SetupMembers | GET/POST/PATCH/DELETE /events/:id/members + candidates | Per-event admin roles (admin_user_events.role); viewer = read-only (enforced server-side). |
5.3 Registration
| Page | Endpoints | Notable UX |
|---|---|---|
RegistrationSettings | PUT /events/:id/registration | Registration gates, windows, toggles. |
FormBuilder | GET/PUT /events/:id/registration/form-schema | Tabs: Registration Form / Merchandise / Qualification / Success Page / Account Details / Upgrade. Merchandise + Upgrade tabs gated on registration.merchandise. Full subsystem in Section 8. |
5.4 Teams
| Page | Endpoints | Notable UX |
|---|---|---|
TeamSettings | PUT /events/:id/teams/settings | Team mode configuration. |
AllTeams | Roster CRUD + move member / change owner + rename / delete team | Rename must sync the denormalized challenge_team_leaderboard.team_name — the backend endpoint handles this; never rename via a raw update path. Delete of teams with dependencies surfaces a friendly error. |
5.5 Rewards
| Page | Endpoints | Notable UX |
|---|---|---|
AllSkus | SKU CRUD, PATCH visibility, PUT /rewards/reorder, import | Drag-reorder persists sort_id; deleting a purchased SKU returns a friendly 409 (FK protection). |
RewardInstructions | PUT /events/:id/reward-instructions | Also feeds the WL upgrade/merch step intro (coreInstructions). |
RewardDiscounts | /reward-discounts CRUD | Gated by rewards.discounts. |
CouponManager | Coupon CRUD + default-config + import | Gated by rewards.coupon_manager; includes returning-participant default coupon config. |
5.6 Donations
| Page | Endpoints |
|---|---|
DonationConfig | GET/PUT /events/:id/donation-config |
FundraiserGoals | GET/PUT /events/:id/fundraiser-config — preset chips persist removals (list-aware merge); deadline is server-resolved from event end. |
5.7 Content
| Page | Endpoints | Notable UX |
|---|---|---|
PageList | Page CRUD + status/active toggle + duplicate + reorder | Entry point to the Page Builder. |
PageBuilder | Page load/save | Full drag-drop block builder — Section 7. |
MenuSetup | GET /pages + PUT /events/:id/menu | WL public-site navigation composed from builder pages. |
FaqManager | GET/PUT /faq | Full FAQ page (WL builder page, slug faq). |
LandingFaqManager | GET/PUT /landing-faq | Short home-page FAQ — separate store from FAQ Manager. |
RulesManager | GET/PUT /rules | Event rules content. |
5.8 Leaderboard
| Page | Endpoints | Notable UX |
|---|---|---|
LeaderboardGeneral | GET/PUT /leaderboard/general | General leaderboard settings. |
LeaderboardTabs | GET /leaderboard/tabs, PUT tabs/:id | Per-tab configuration. |
LeaderboardHighlights | Highlights CRUD + reorder | Editing opens the reusable BlockEditorWorkspace (same engine as the Page Builder) full-screen; content stored per-highlight as a block tree; a ShortCodes dropdown inserts {{token}} placeholders. Gated by leaderboard.event_highlights. |
LeaderboardSync | GET/PUT /leaderboard/sync | Gallery/sync settings; gated by leaderboard.gallery_sync. |
5.9 Achievements (group gated by achievements.enabled)
| Page | Endpoints | Notable UX |
|---|---|---|
AchievementGroups | CRUD + reorder | Group ordering via drag. |
AchievementList | List + PATCH visibility + duplicate + delete | — |
AchievementForm | Create/edit | Three image slots (badge / more-info / sponsor) using ImageUploadWithCropper. |
5.10 Communications
| Page | Endpoints | Notable UX |
|---|---|---|
AutomationRules | GET /automation-rules, PUT /automation-rules/:ruleName, POST …/test | Toggle/edit automated sends; test-send action. |
EmailLogs | GET /email-logs | Sent-mail audit trail. |
| Notifications | — | Menu item exists, no route — placeholder page. |
5.11 Payments
| Page | Endpoints | Notable UX |
|---|---|---|
PaymentGateway | GET/PUT /payment-gateways + /payment-gateway-credentials | Per-event gateway selection backed by a shared credentials library; new events fall back to the shared TEST Stripe credential until configured. |
TransactionHistory | via useEventTransactions | Payment listing/filtering. |
5.12 Operations
| Page | Endpoints | Notable UX |
|---|---|---|
EventParticipants | useEventParticipants — 12 endpoints: list / stats / filter-options + PATCH remarks & featured + strava-sync + ebib + payments & audit modals | The heaviest operational page; per-row modals for payments and audit history; UI actions gated via event.featureSettings + featureGate.js. |
EventActivities | useEventActivities — list / stats / filter-options / batch-audit + PATCH review | Activity review workflow with batch audit. Gated by operations.activity_manager. |
PortUsers | POST /port-users/runs + revert | 5-step wizard: Source → Audience → Mapping → Conflicts → Review; runs are revertible. Gated by operations.port_users. |
| Re-sync Activities | — | Menu item only (gate operations.resync_activities); no route — placeholder. |
5.13 Analytics & Dashboard
| Page | Endpoints | Notable UX |
|---|---|---|
EventDashboard | Event-scoped stats | Landing page of the event shell. |
EventAnalytics | GET /events/:id/analytics?range= | Real-DB-only analytics with fault-tolerant tabs (one failing tab doesn't break the page). |
6. Shared patterns & conventions
6.1 src/utils/api.js — the only HTTP client
- Base URL:
import.meta.env.VITE_API_URL || 'http://localhost:8001/api/v1'. - Auth token from
localStorage['auth_token'], sent asAuthorization: Bearer …. Content-Type: application/jsonis set unless the body isFormData(the browser then sets the multipart boundary).204 No Contentresolves tonull.- Global 401 handler: clears the token and redirects once to
/login?reason=session_expired&next=<current-path>. - Surface:
api.get/post/put/patch/delete(endpoint, body)plusapi.upload(endpoint, formData)— a POST multipart helper.
CRITICAL — the FormData PUT trap. Never pass
FormDatatoapi.put— it gets JSON-serialized to"{}"and the backend receives an empty payload. For any multipart update, useapi.upload(endpoint, fd)(POST) and append Laravel's method spoof:fd.append('_method', 'PUT').
6.2 src/utils/swal.js — dialogs
Wrapper around sweetalert2 exporting swalConfirm, swalDiscardConfirm, swalSaveConfirm, swalSuccess, swalError, swalInfo, swalPrompt, swalTypeToConfirm, plus escapeHtml for interpolated content. Brand primary is #7E1FF6. Never use native alert/confirm/prompt anywhere in the admin frontend.
6.3 Settings-page state pattern
Nearly every setup/settings page follows the same shape:
- Event data from
useOutletContext()(provided byEventLayout), or a page-specificGET. - Local form state:
useState(getInitialForm())wheregetInitialForm()derives the form from the fetched shape (and provides defaults). - Dirty tracking:
isDirtycomputed byJSON.stringifycomparison of current form vs initial. - A
SaveBarappears when dirty; SavePUTs, then resets the baseline; Discard confirms viaswalDiscardConfirm.
6.4 Image upload & cropper system
src/utils/imageUploadConfig.js—IMAGE_UPLOAD_CONFIGS: ~40 slot types, each withlabel,description,recommendedSize,cropAspect,folder,maxSizeMB,formats(PNG allowed where transparency matters).getImageConfig(type)falls back togeneric_upload.IMAGE_FIELD_MAPPING+resolveImageTypetranslate legacy field names only — never add canonical slot keys to the mapping.ImageUploadWithCropper(components/shared/) — the universal widget: validates MIME + size → opensImageCropModal(react-image-crop v11; free-form crop with a ratio-lock toggle) →api.upload('/events/:id/media/upload', fd)→ returns a CDN URL.- Cropping is mandatory in this flow; every image type routes through the same modal.
- Remember the Branding rule: for image slots, the upload gives you a URL, but persisting it is a separate
PATCH /events/:id/images/:slot/url.
6.5 SectionCard convention (local copies, not a library)
SectionCard, ToggleCard, Toggle and SaveBar are local per-page copies — a deliberate convention, not a shared component library. When building a new settings page, copy them from a neighboring page and keep the API identical. By contrast, SetupHelpPanel is shared (components/shared/): the right-hand help rail with a completion ring, scroll-synced section navigation and contextual tips.
7. The Page Builder (components/event-manage/content/builder/)
The Page Builder edits WL public-site pages as a block tree.
Block system — blockDefaults.js:
genId(prefix = 'blk')— monotonic base-36 id generator seeded fromDate.now(). Always mint block/column ids viagenId()— never a raw counter or bareDate.now(); duplicate ids break selection and drag-drop.createDefaultBlock(type)builds a block from theBLOCK_TYPESregistry (~80 types, including themy_*personal-data set which ishiddenFromPicker), organized byBLOCK_CATEGORIES(basic / media / content / layout / live) withCOLUMN_PRESETSfor layout blocks.- Every block type has a matching editor component in
content/sections/blocks/*BlockEditor.jsx.
Workspace — BlockEditorWorkspace.jsx is the reusable 3-panel editor: BlockList (tree) · BuilderPreviewIframe (live WL render) · settings drawer. It supports single/multi selection, clipboard (blockClipboard.js), undo/redo (useBlockBuilder.js), tree operations (blockTreeUtils.js), page templates (pageTemplates.js), style presets (stylePresets.js), palette (colorPalette.js) and dynamic data bindings (dynamicData.js). PageBuilder wraps it with meta/SEO/routing panels, templates, block palette, keyboard shortcuts and import/export. The Leaderboard Highlights editor reuses the same workspace.
AI panel — builder/ai/: useAiChat.js drives a chat that streams block operations (aiStreamParser.js), validates proposed blocks (aiBlockValidator.js) and renders in AiChatPanel.jsx.
Preview iframe — BuilderPreviewIframe loads the WL site's /builder-preview?preview=1 and pushes the block tree over postMessage, so the preview is the actual Next.js renderer. Preview URL resolution order: event custom domain → VITE_PUBLIC_SITE_URL → <current hostname>:3000 → localhost:3000.
8. The Registration Form Builder (components/event-manage/registration/builder/)
Three-panel editor for the registration flow: field tree · live WL preview iframe · settings drawer.
Iframe protocol — RegistrationPreviewIframe / UpgradePreviewIframe host the WL site's /registration-preview?preview=1 and speak an fb-* postMessage protocol (~50 ms debounce):
| Direction | Messages |
|---|---|
| parent → iframe | fb-init, fb-fields, fb-selection, fb-upgrade |
| iframe → parent | fb-ready, fb-field-clicked, fb-field-bounds, fb-field-hover |
Clicking a field in the preview selects it in the editor; bounds/hover messages drive selection outlines drawn by the parent.
Field mapping — src/utils/regFieldPreview.js converts the admin-side field shape (built by formSchemaConverter.js) into the WL RegField shape, so the iframe renders unsaved edits through the real public FieldRenderer. Field types come from src/data/fieldTypeRegistry.js; new-field defaults from formBuilderDefaults.js — these defaults mirror the production test.json schema exactly (do not invent defaults).
UI components — FormEditorWorkspace, FieldTreeList/FieldList/FieldCard/FieldItem, FieldEditor with FieldEditorSections/* (General, Options, Validation, Condition, Buttons, ShareLink, SocialIcons, DonationAmount, NextSteps, ChildFields, SectionImage), AddFieldModal, StepBlocksEditor, AccountDetailsEditor, MerchandiseContentEditor, UpgradeEditorWorkspace.
Tabs — Registration Form / Merchandise / Qualification / Success Page / Account Details / Upgrade. Merchandise and Upgrade tabs only render when the registration.merchandise feature flag is enabled. Persistence: GET/PUT /events/:id/registration/form-schema (v1 supports top-level fields only — no nested groups).
9. Auth & session
contexts/AuthContextholdsuser,permissions,loading,isAuthenticated.- Startup:
GET /auth/mewith a transient-failure retry — a flaky network probe does not log the user out; only a definitive 401 does. - Login:
POST /auth/login { email, password, remember_me }→ token stored inlocalStorage['auth_token']. - Logout:
POST /auth/logout+ local token clear. ProtectedRouterenders a spinner while auth resolves, then redirects to/loginif unauthenticated.usePermissions(GET /permissions) anduseRoles(roles CRUD) back the Users page.- Session expiry is handled globally in
api.js: any 401 clears the token and redirects (once) to/login?reason=session_expired&next=<path>, so the user returns where they were after re-login. - Note the division of labor: feature flags control what per-event UI is visible; roles/permissions are enforced by the backend (
CheckEventAccess— viewers are read-only). The frontend does not hide event sections by role.
10. Build, env & serving
Scripts (package.json): dev (Vite dev server), build (→ dist/), preview.
vite.config.js: dev server binds 0.0.0.0; dev proxy /api → http://128.199.72.46:8001 (the admin backend on this box).
.env:
| Var | Value | Purpose |
|---|---|---|
VITE_API_URL | /api/v1 | Admin backend base (same-origin, nginx routes it) |
VITE_WL_API_URL | https://wl-api.togoparts.com | Production WL API — used by email template preview/test-send and other WL-API calls |
VITE_PUBLIC_SITE_URL | http://128.199.72.46:3000 | WL public site for preview iframes (dev-only value) |
Serving is static. The deployed admin is served from dist/ (nginx, e.g. v3.togoactive.com) — there is no HMR / dev server in the served deployment. After every source edit you must run:
bash
cd /var/www/togoactive-development/admin-frontend && npm run buildor your change simply won't appear. (The admin backend needs no build step.)
The remote WL API dependency. Because VITE_WL_API_URL points at the production WL API server (a different machine from this dev/staging box), features that call it — email template live preview, test sends, the donation picker — execute against whatever code is deployed there. The database is shared, so config edits show up everywhere, but code must be pushed and deployed per-server. If a preview behaves differently from the WL site itself, suspect a stale deploy on wl-api.togoparts.com before debugging the frontend.
11. Known quirks
- Unrouted menu items —
communications.notifications,operations.resync(Re-sync Activities) and all threedeveloper.*items (Configuration Store, Custom CSS/JS, OneSignal Setup) exist ineventSidebarMenu.jsbut have no route; they fall through to the*catch-all and renderEventSectionPlaceholder("coming soon"). This is intentional scaffolding, not a bug. EventOnboardingPageis unrouted —src/pages/EventOnboardingPage.jsxis a complete page with no entry inrouter.jsx; nothing links to it.- Doc/artifact files in the repo — working notes were committed alongside the code:
src/MIGRATION_IMAGE_UPLOAD.md, and at the repo rootIMPLEMENTATION_SUMMARY.md,HELPTIP_PROGRESS.md,USAGE_CHEATSHEET.md,QUICK_START.md. They are historical artifacts — do not treat them as current documentation; this file and its siblings indocs/are canonical. react-easy-cropstill inpackage.json— the shared cropper migrated toreact-image-cropv11 (free-form + ratio-lock); the old dependency lingers. UseImageCropModal/ImageUploadWithCropper, neverreact-easy-crop, for new work.- Feature-flag default-visible semantics — an absent flag renders the menu item (
isFeatureEnableddefaults totrue). Adding a newfeatureKeyto the menu before the backend registers the flag will not hide anything until the flag exists and is off. - Sidebar badges are static — the Communications (2) and Operations (12) badges are hard-coded numbers in
eventSidebarMenu.js, not live counts. whereColumnin eager loading — backend caveat that leaks into frontend expectations: list endpoints use DB subquery selects for computed columns; don't assume relations carry aggregate fields.