Skip to content

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

ConcernChoice
FrameworkReact 18.3 (function components + hooks only)
Build toolVite 5.4 (@vitejs/plugin-react)
LanguageJavaScript / JSX — no TypeScript
Routingreact-router-dom v6 (createBrowserRouter)
StylingTailwind CSS 3.4 + PostCSS/Autoprefixer
Dialogssweetalert2 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 croppingreact-image-crop v11 (current cropper; react-easy-crop remains in package.json but the shared cropper is react-image-crop)
Iconslucide-react
Sanitisationdompurify
Color pickerreact-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

PathComponent
/loginpages/auth/LoginPage
/registerpages/auth/RegisterPage
/forgot-passwordpages/auth/ForgotPasswordPage
/reset-passwordpages/auth/ResetPasswordPage

2.2 MainLayout routes (global shell: Sidebar + Header)

PathComponentNotes
/Redirects to /dashboard
/dashboardDashboardPagePlatform stats
/eventsEventsPageEvent list
/events/newEventsPage (create mode)Same component, create flow
/usersUsersPageAdmin users + roles
/participantsParticipantsPageCross-event participants
/activitiesActivitiesPageCross-event activities
/settingsSettingsPageGlobal settings
/avatars/globalGlobalAvatarsGlobal 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
analyticsEventAnalytics
setup/generalSetupGeneral
setup/datesSetupDates
setup/brandingSetupBranding
setup/appearanceSetupAppearance
setup/hostSetupHost
setup/socialSetupSocial
setup/seoSetupSeo
setup/domainSetupDomain
setup/email-brandingSetupEmailBranding
setup/email-templatesSetupEmailTemplates
setup/default-messagesSetupDefaultMessages
setup/featuresFeatureSettings
setup/integrationsSetupIntegrations
setup/avatarsSetupAvatars
setup/membersSetupMembers
registration/settingsRegistrationSettings
registration/form-builderFormBuilder
teams/settingsTeamSettings
teams/listAllTeams
rewards/skusAllSkus
rewards/instructionsRewardInstructions
rewards/discountsRewardDiscounts
rewards/couponsCouponManager
donations/configDonationConfig
donations/fundraiserFundraiserGoals
leaderboard/generalLeaderboardGeneral
leaderboard/tabsLeaderboardTabs
leaderboard/highlightsLeaderboardHighlights
leaderboard/syncLeaderboardSync
achievements/groupsAchievementGroups
achievements/listAchievementList
achievements/new, achievements/:achievementIdAchievementForm
content/pagesPageList
content/pages/new, content/pages/:pageIdPageBuilder
content/menu-setupMenuSetup
content/faqFaqManager
content/landing-faqLandingFaqManager
content/rulesRulesManager
operations/participantsEventParticipants
operations/activity-managerEventActivities
operations/port-usersPortUsers
communications/automationAutomationRules
communications/email-logsEmailLogs
payments/gatewayPaymentGateway
payments/transactionsTransactionHistory
* (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 badge on a group renders a count pill: Communications carries badge: 2 and Operations badge: 12 (static values in the menu data).
  • Placeholder fallback — any menu item without a registered route lands on EventSectionPlaceholder via the * catch-all ("coming soon" panel), so unfinished sections are navigable without crashing.

Full menu with feature gates (a blank Feature gate = always visible):

GroupItemSection keyFeature gate
(top level)Event Dashboardevent-dashboard
(top level)Analyticsanalytics
SetupGeneral Infosetup.general
Dates & Schedulesetup.dates
Branding & Imagessetup.branding
Appearancesetup.appearance
Event Hostsetup.host
Social & SEOsetup.social
SEO Templatessetup.seo
Custom Domainsetup.domain
Email Brandingsetup.email-branding
Email Templatessetup.email-templates
Default Messagessetup.default-messages
Feature Settingssetup.features
Integrationssetup.integrations
Avatarssetup.avatars
Members & Accesssetup.members
RegistrationRegistration Settingsregistration.settings
Form Builderregistration.form-builderregistration.form_builder
Teams (group gate: teams.enabled)Team Settingsteams.settings
All Teamsteams.list
RewardsAll SKUsrewards.skus
Instructionsrewards.instructions
Discountsrewards.discountsrewards.discounts
Coupon Managerrewards.couponsrewards.coupon_manager
DonationsDonation Configdonations.config
Fundraiser Goalsdonations.fundraiser
ContentPagescontent.pages
Menu Setupcontent.menu-setup
FAQ Managercontent.faq
Landing Page FAQcontent.landing-faq
Rules Managercontent.rules
LeaderboardGeneral Settingsleaderboard.general
Event Highlightsleaderboard.highlightsleaderboard.event_highlights
Sync Settingsleaderboard.syncleaderboard.gallery_sync
Achievements (group gate: achievements.enabled)Achievement Groupsachievements.groups
Achievement Listachievements.list
Communications (badge 2)Notificationscommunications.notifications(unrouted → placeholder)
Automation Rulescommunications.automation
Email Logscommunications.email-logs
PaymentsPayment Gatewaypayments.gateway
Transaction Historypayments.transactions
Operations (badge 12)Participantsoperations.participants
Activity Manageroperations.activity-manageroperations.activity_manager
Re-sync Activitiesoperations.resyncoperations.resync_activities (unrouted → placeholder)
Port Usersoperations.port-usersoperations.port_users
Developer ToolsConfiguration Storedeveloper.config-store(unrouted → placeholder)
Custom CSS / JSdeveloper.custom-code(unrouted → placeholder)
OneSignal Setupdeveloper.onesignal(unrouted → placeholder)

4. Feature gating in the UI

src/utils/featureGate.js is the single gate for menu/tab visibility:

  • isFeatureEnabled(fs, key) — returns true when 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 (a featureKey on 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 from event.featureSettings, loaded by useEventById and distributed through EventLayout'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: FormBuilder shows/hides its Merchandise and Upgrade tabs based on registration.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)

PageEndpointsBehavior
DashboardPageGET /dashboard/statsPlatform-wide KPI cards.
EventsPageGET /events (via useEvents)Event list; /events/new renders the same page in create mode.
UsersPageuseAdminUsers: CRUD /users, PUT /users/:id/events; useRoles: /roles CRUDAdmin user management incl. per-user event assignment and role management.
ParticipantsPageGET /participants, GET /participants/statsCross-event participant listing with stats.
ActivitiesPageGET /activities + statsCross-event activity listing.
GlobalAvatars/avatars/global CRUD + reorder + category-orderGlobal avatar pool (gender/category-matched auto-assignment source).
SettingsPageGlobal admin settings.

5.2 Setup

PageEndpointsNotable UX
SetupGeneralPUT /events/:id/generalCore event fields (name, mode, etc.).
SetupDatesPUT /events/:id/datesAll 8 event date fields; client-side dateValidation.js — Save disabled until dates are consistent.
SetupBrandingPOST /events/:id/images (upload), PATCH /events/:id/images/:slot/url, DELETE slotUploading 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.
SetupAppearanceGET/PUT /events/:id/appearanceDesign 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.
SetupHostGET/PUT /events/:id/host-config + search-users + create-userPick or create the host account; host avatar cascade is backend-side.
SetupSocialPOST (upload) /events/:id/social-seoSocial links + share image (multipart).
SetupSeoGET/PUT /events/:id/meta-templates/:type + meta-tokensPer-page-type SEO templates with token insertion.
SetupDomainGET/POST/DELETE /events/:id/domain + verify-dns/verify-txt/syncCustom-domain lifecycle with DNS and TXT verification steps.
SetupEmailBrandingGET/PUT /events/:id/email-brandingHeader/footer/sender branding for WL emails.
SetupEmailTemplatesGET/PUT /events/:id/email-templatesLive 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.
SetupDefaultMessagesGET/PUT /events/:id/default-messagesDefault copy blocks (share text etc.); new admin copy fields extend event_default_message, not new config rows.
FeatureSettingsGET/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.
SetupIntegrationsGET /events/:id/integrations; Google connect/select/properties/create/disconnect; POST/DELETE tagTag providers (GA4/GTM/Meta Pixel/Contentsquare) with OAuth-based GA4 property flow.
SetupAvatarsPer-event avatar CRUD + reorderPer-event overrides of the global avatar pool.
SetupMembersGET/POST/PATCH/DELETE /events/:id/members + candidatesPer-event admin roles (admin_user_events.role); viewer = read-only (enforced server-side).

5.3 Registration

PageEndpointsNotable UX
RegistrationSettingsPUT /events/:id/registrationRegistration gates, windows, toggles.
FormBuilderGET/PUT /events/:id/registration/form-schemaTabs: Registration Form / Merchandise / Qualification / Success Page / Account Details / Upgrade. Merchandise + Upgrade tabs gated on registration.merchandise. Full subsystem in Section 8.

5.4 Teams

PageEndpointsNotable UX
TeamSettingsPUT /events/:id/teams/settingsTeam mode configuration.
AllTeamsRoster CRUD + move member / change owner + rename / delete teamRename 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

PageEndpointsNotable UX
AllSkusSKU CRUD, PATCH visibility, PUT /rewards/reorder, importDrag-reorder persists sort_id; deleting a purchased SKU returns a friendly 409 (FK protection).
RewardInstructionsPUT /events/:id/reward-instructionsAlso feeds the WL upgrade/merch step intro (coreInstructions).
RewardDiscounts/reward-discounts CRUDGated by rewards.discounts.
CouponManagerCoupon CRUD + default-config + importGated by rewards.coupon_manager; includes returning-participant default coupon config.

5.6 Donations

PageEndpoints
DonationConfigGET/PUT /events/:id/donation-config
FundraiserGoalsGET/PUT /events/:id/fundraiser-config — preset chips persist removals (list-aware merge); deadline is server-resolved from event end.

5.7 Content

PageEndpointsNotable UX
PageListPage CRUD + status/active toggle + duplicate + reorderEntry point to the Page Builder.
PageBuilderPage load/saveFull drag-drop block builder — Section 7.
MenuSetupGET /pages + PUT /events/:id/menuWL public-site navigation composed from builder pages.
FaqManagerGET/PUT /faqFull FAQ page (WL builder page, slug faq).
LandingFaqManagerGET/PUT /landing-faqShort home-page FAQ — separate store from FAQ Manager.
RulesManagerGET/PUT /rulesEvent rules content.

5.8 Leaderboard

PageEndpointsNotable UX
LeaderboardGeneralGET/PUT /leaderboard/generalGeneral leaderboard settings.
LeaderboardTabsGET /leaderboard/tabs, PUT tabs/:idPer-tab configuration.
LeaderboardHighlightsHighlights CRUD + reorderEditing 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.
LeaderboardSyncGET/PUT /leaderboard/syncGallery/sync settings; gated by leaderboard.gallery_sync.

5.9 Achievements (group gated by achievements.enabled)

PageEndpointsNotable UX
AchievementGroupsCRUD + reorderGroup ordering via drag.
AchievementListList + PATCH visibility + duplicate + delete
AchievementFormCreate/editThree image slots (badge / more-info / sponsor) using ImageUploadWithCropper.

5.10 Communications

PageEndpointsNotable UX
AutomationRulesGET /automation-rules, PUT /automation-rules/:ruleName, POST …/testToggle/edit automated sends; test-send action.
EmailLogsGET /email-logsSent-mail audit trail.
NotificationsMenu item exists, no route — placeholder page.

5.11 Payments

PageEndpointsNotable UX
PaymentGatewayGET/PUT /payment-gateways + /payment-gateway-credentialsPer-event gateway selection backed by a shared credentials library; new events fall back to the shared TEST Stripe credential until configured.
TransactionHistoryvia useEventTransactionsPayment listing/filtering.

5.12 Operations

PageEndpointsNotable UX
EventParticipantsuseEventParticipants — 12 endpoints: list / stats / filter-options + PATCH remarks & featured + strava-sync + ebib + payments & audit modalsThe heaviest operational page; per-row modals for payments and audit history; UI actions gated via event.featureSettings + featureGate.js.
EventActivitiesuseEventActivities — list / stats / filter-options / batch-audit + PATCH reviewActivity review workflow with batch audit. Gated by operations.activity_manager.
PortUsersPOST /port-users/runs + revert5-step wizard: Source → Audience → Mapping → Conflicts → Review; runs are revertible. Gated by operations.port_users.
Re-sync ActivitiesMenu item only (gate operations.resync_activities); no route — placeholder.

5.13 Analytics & Dashboard

PageEndpointsNotable UX
EventDashboardEvent-scoped statsLanding page of the event shell.
EventAnalyticsGET /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 as Authorization: Bearer ….
  • Content-Type: application/json is set unless the body is FormData (the browser then sets the multipart boundary).
  • 204 No Content resolves to null.
  • 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) plus api.upload(endpoint, formData) — a POST multipart helper.

CRITICAL — the FormData PUT trap. Never pass FormData to api.put — it gets JSON-serialized to "{}" and the backend receives an empty payload. For any multipart update, use api.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:

  1. Event data from useOutletContext() (provided by EventLayout), or a page-specific GET.
  2. Local form state: useState(getInitialForm()) where getInitialForm() derives the form from the fetched shape (and provides defaults).
  3. Dirty tracking: isDirty computed by JSON.stringify comparison of current form vs initial.
  4. A SaveBar appears when dirty; Save PUTs, then resets the baseline; Discard confirms via swalDiscardConfirm.

6.4 Image upload & cropper system

  • src/utils/imageUploadConfig.jsIMAGE_UPLOAD_CONFIGS: ~40 slot types, each with label, description, recommendedSize, cropAspect, folder, maxSizeMB, formats (PNG allowed where transparency matters). getImageConfig(type) falls back to generic_upload. IMAGE_FIELD_MAPPING + resolveImageType translate legacy field names only — never add canonical slot keys to the mapping.
  • ImageUploadWithCropper (components/shared/) — the universal widget: validates MIME + size → opens ImageCropModal (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 systemblockDefaults.js:

  • genId(prefix = 'blk') — monotonic base-36 id generator seeded from Date.now(). Always mint block/column ids via genId() — never a raw counter or bare Date.now(); duplicate ids break selection and drag-drop.
  • createDefaultBlock(type) builds a block from the BLOCK_TYPES registry (~80 types, including the my_* personal-data set which is hiddenFromPicker), organized by BLOCK_CATEGORIES (basic / media / content / layout / live) with COLUMN_PRESETS for layout blocks.
  • Every block type has a matching editor component in content/sections/blocks/*BlockEditor.jsx.

WorkspaceBlockEditorWorkspace.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 panelbuilder/ai/: useAiChat.js drives a chat that streams block operations (aiStreamParser.js), validates proposed blocks (aiBlockValidator.js) and renders in AiChatPanel.jsx.

Preview iframeBuilderPreviewIframe 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>:3000localhost: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 protocolRegistrationPreviewIframe / UpgradePreviewIframe host the WL site's /registration-preview?preview=1 and speak an fb-* postMessage protocol (~50 ms debounce):

DirectionMessages
parent → iframefb-init, fb-fields, fb-selection, fb-upgrade
iframe → parentfb-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 mappingsrc/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 componentsFormEditorWorkspace, 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/AuthContext holds user, permissions, loading, isAuthenticated.
  • Startup: GET /auth/me with 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 in localStorage['auth_token'].
  • Logout: POST /auth/logout + local token clear.
  • ProtectedRoute renders a spinner while auth resolves, then redirects to /login if unauthenticated.
  • usePermissions (GET /permissions) and useRoles (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 /apihttp://128.199.72.46:8001 (the admin backend on this box).

.env:

VarValuePurpose
VITE_API_URL/api/v1Admin backend base (same-origin, nginx routes it)
VITE_WL_API_URLhttps://wl-api.togoparts.comProduction WL API — used by email template preview/test-send and other WL-API calls
VITE_PUBLIC_SITE_URLhttp://128.199.72.46:3000WL 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 build

or 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 itemscommunications.notifications, operations.resync (Re-sync Activities) and all three developer.* items (Configuration Store, Custom CSS/JS, OneSignal Setup) exist in eventSidebarMenu.js but have no route; they fall through to the * catch-all and render EventSectionPlaceholder ("coming soon"). This is intentional scaffolding, not a bug.
  • EventOnboardingPage is unroutedsrc/pages/EventOnboardingPage.jsx is a complete page with no entry in router.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 root IMPLEMENTATION_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 in docs/ are canonical.
  • react-easy-crop still in package.json — the shared cropper migrated to react-image-crop v11 (free-form + ratio-lock); the old dependency lingers. Use ImageCropModal/ImageUploadWithCropper, never react-easy-crop, for new work.
  • Feature-flag default-visible semantics — an absent flag renders the menu item (isFeatureEnabled defaults to true). Adding a new featureKey to 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.
  • whereColumn in 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.

Organiser guide and developer documentation for the TogoActive platform.