Skip to content

Frontend Architecture

This document describes the React Single Page Application (SPA) that powers the Rondo Club frontend.

TechnologyVersionPurpose
React18UI library
React Router6Client-side routing
TanStack QueryLatestServer state management and caching
AxiosLatestHTTP client
Tailwind CSS3.4Styling
Vite5.0Build tool and dev server
src/
├── api/
│ └── client.js # Axios instance and API helpers
├── components/
│ ├── import/ # Import wizard components
│ └── layout/ # Layout wrapper component
├── constants/
│ └── app.js # Application-wide constants
├── hooks/ # Custom React hooks
├── pages/ # Route page components
│ ├── Commissies/ # Committee list and detail
│ ├── Contributie/ # Fee overview, member list, not-yet-invoiced
│ ├── DisciplineCases/ # Discipline cases list
│ ├── Feedback/ # Feedback list and detail
│ ├── Finance/ # Invoice list, invoice detail, finance settings
│ ├── People/ # People list and detail
│ ├── Settings/ # App settings, relationship types, custom fields
│ ├── Teams/ # Team list and detail
│ ├── Todos/ # Todo list
│ └── VOG/ # VOG certificate tracking
├── utils/ # Utility functions
├── App.jsx # Main routing component
├── main.jsx # Application entry point
└── index.css # Global styles (Tailwind imports)

Application bootstrap:

  • Creates React root with StrictMode
  • Configures TanStack Query client
  • Wraps app in BrowserRouter and QueryClientProvider
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 5 * 60 * 1000, // 5 minutes
retry: 1,
},
},
});

Main routing component:

  • Defines all application routes
  • Implements ProtectedRoute wrapper for authentication
  • Wraps protected routes in Layout component
PathComponentDescriptionAccess
/loginLoginPublic login pagePublic
/DashboardHome dashboardAuth
/peoplePeopleListContact listAuth
/people/:idPersonDetailView contactAuth
/teamsTeamsListTeam listAuth
/teams/:idTeamDetailView teamAuth
/commissiesCommissiesListCommittee listAuth
/commissies/:idCommissieDetailCommittee detailAuth
/todosTodosListTodo listAuth
/feedbackFeedbackListFeedback listAuth
/feedback/:idFeedbackDetailFeedback detailAuth
/vogVOGVOG certificate trackingVOG capability
/vog/:tabVOGVOG tab viewVOG capability
/tuchtzakenDisciplineCasesListDiscipline casesFairplay capability
/financien/contributieContributieFee overviewFinancieel capability
/financien/contributie/:tabContributieFee tab viewFinancieel capability
/financien/facturenFacturenInvoice listFinancieel capability
/financien/facturen/:idFactuurDetailInvoice detailFinancieel capability
/financien/instellingenFinanceSettingsFinance configurationFinancieel capability
/settingsSettingsSettings pageAuth
/settings/:tabSettingsSettings tabAuth
/settings/relationship-typesRelationshipTypesManage relationship typesAuth
/settings/custom-fieldsCustomFieldsCustom field managementAuth
/settings/feedbackFeedbackManagementFeedback settingsAuth

Capability-based route guards: Routes under /financien/ require financieel capability, /vog requires VOG capability, and /tuchtzaken requires fairplay capability. These are enforced by CapabilityRoute wrapper components (FinancieelRoute, VOGRoute, FairplayRoute).

The ProtectedRoute component checks authentication state:

  • Shows loading spinner while checking
  • Redirects to /login if not authenticated
  • Renders children if authenticated
function ProtectedRoute({ children }) {
const { isLoggedIn, isLoading } = useAuth();
if (isLoading) return <LoadingSpinner />;
if (!isLoggedIn) return <Navigate to="/login" replace />;
return children;
}

Configures Axios for WordPress REST API communication.

Configuration:

const api = axios.create({
baseURL: config.apiUrl || '/wp-json',
headers: {
'Content-Type': 'application/json',
'X-WP-Nonce': config.nonce || '',
},
});

Interceptors:

  • Request: Updates nonce from window.rondoConfig before each request
  • Response: Handles 401 (redirect to login) and 403 (log error)

Two exported objects wrap common API calls:

wpApi - WordPress standard endpoints:

wpApi.getPeople(params)
wpApi.getPerson(id, params)
wpApi.createPerson(data)
wpApi.updatePerson(id, data)
wpApi.deletePerson(id)
// ... similar for teams, dates, taxonomies

prmApi - Custom PRM endpoints:

prmApi.getDashboard()
prmApi.search(query)
prmApi.getReminders(daysAhead)
prmApi.getCurrentUser()
prmApi.sideloadGravatar(personId, email)
prmApi.uploadPersonPhoto(personId, file)
// ... and more

The app receives configuration from WordPress via window.rondoConfig:

window.rondoConfig = {
apiUrl: '/wp-json',
nonce: 'abc123...',
isLoggedIn: true,
userId: 1,
loginUrl: '/wp-login.php',
logoutUrl: '/wp-login.php?action=logout',
siteName: 'My CRM',
};

This is injected by functions.php during page load.

Returns authentication state from WordPress config:

const { isLoggedIn, userId, loginUrl, logoutUrl, isLoading } = useAuth();

People data hooks with TanStack Query:

HookPurpose
usePeople(params)Fetch all people (paginated)
usePerson(id)Fetch single person
usePersonTimeline(id)Fetch person’s timeline
usePersonDates(id)Fetch person’s dates
useCreatePerson()Create person mutation
useUpdatePerson()Update person mutation
useDeletePerson()Delete person mutation
useCreateNote()Create note mutation
useDeleteNote()Delete note mutation
useCreateActivity()Create activity mutation
useDeleteDate()Delete date mutation

Query Key Structure:

peopleKeys = {
all: ['people'],
lists: () => [...all, 'list'],
list: (filters) => [...lists(), filters],
details: () => [...all, 'detail'],
detail: (id) => [...details(), id],
timeline: (id) => [...detail(id), 'timeline'],
dates: (id) => [...detail(id), 'dates'],
};

Dashboard and utility hooks:

HookPurpose
useDashboard()Fetch dashboard summary
useReminders(daysAhead)Fetch upcoming reminders
useSearch(query)Global search (min 2 chars)

useDocumentTitle (src/hooks/useDocumentTitle.js)

Section titled “useDocumentTitle (src/hooks/useDocumentTitle.js)”

Document title management:

HookPurpose
useDocumentTitle(title)Set specific page title
useRouteTitle(customTitle)Auto-set title based on route

useWorkspaces (src/hooks/useWorkspaces.js)

Section titled “useWorkspaces (src/hooks/useWorkspaces.js)”

Workspace and sharing hooks for multi-user collaboration:

HookPurpose
useWorkspaces()Fetch all workspaces for current user
useWorkspace(id)Fetch single workspace with members
useCreateWorkspace()Create workspace mutation
useUpdateWorkspace()Update workspace mutation
useDeleteWorkspace()Delete workspace mutation
useAddWorkspaceMember()Add member to workspace
useRemoveWorkspaceMember()Remove member from workspace
useUpdateWorkspaceMember()Update member role
useWorkspaceInvites(workspaceId)Fetch pending invites
useCreateWorkspaceInvite()Create and send invite
useRevokeWorkspaceInvite()Revoke pending invite
useValidateInvite(token)Validate invite token (public)
useAcceptInvite()Accept invite and join workspace

Query Key Structure:

['workspaces'] // List all workspaces
['workspaces', id] // Single workspace
['workspaces', workspaceId, 'invites'] // Workspace invites
['invite', token] // Invite validation

Direct sharing hooks for sharing individual posts with specific users:

HookPurpose
useShares(postType, postId)Fetch users a post is shared with
useAddShare()Share post with a user mutation
useRemoveShare()Remove share from a user mutation
useUserSearch(query)Search users for sharing (min 2 chars)

Query Key Structure:

['shares', postType, postId] // Shares for a specific post
['users', 'search', query] // User search results

Usage Example:

const { data: shares } = useShares('people', personId);
const addShare = useAddShare();
const removeShare = useRemoveShare();
const { data: users } = useUserSearch('john');
// Add a share
await addShare.mutateAsync({
postType: 'people',
postId: 123,
userId: 456,
permission: 'view' // or 'edit'
});
// Remove a share
await removeShare.mutateAsync({
postType: 'people',
postId: 123,
userId: 456
});

useVersionCheck (src/hooks/useVersionCheck.js)

Section titled “useVersionCheck (src/hooks/useVersionCheck.js)”

Version checking for PWA/mobile app cache invalidation:

const { hasUpdate, currentVersion, latestVersion, reload, checkVersion } = useVersionCheck({
checkInterval: 5 * 60 * 1000, // Check every 5 minutes (default)
});
PropertyTypeDescription
hasUpdatebooleanTrue when a new version is available
currentVersionstringVersion loaded with the current page
latestVersionstringLatest version from server (when update available)
reloadfunctionTriggers a page reload to get new version
checkVersionfunctionManually trigger a version check

Check triggers:

  • Initial check 5 seconds after mount
  • Periodic check every 5 minutes (configurable)
  • When tab becomes visible (user returns to app)

Backend endpoint: /rondo/v1/version returns { version: "1.42.0" }

FunctionPurpose
decodeHtml(html)Decode HTML entities
getTeamName(team)Get decoded team name
getPersonName(person)Get decoded person name
getPersonInitial(person)Get first initial for avatars
sanitizePersonAcf(acfData, overrides)Sanitize ACF data for API
export const APP_NAME = 'Rondo Club';

All server data is managed via TanStack Query:

  • Automatic caching - 5 minute stale time by default
  • Cache invalidation - Mutations automatically invalidate related queries
  • Background refetching - Data stays fresh
  • Loading/error states - Handled consistently

Minimal client state via:

  • Component state - useState for form inputs, UI state
  • URL state - Route parameters, search params
  • Zustand - Available for complex client state (currently unused)
Terminal window
npm run dev
  • Vite dev server at http://localhost:5173
  • Hot Module Replacement (HMR) enabled
  • Theme auto-detects dev server when WP_DEBUG is true
Terminal window
npm run build
  • Output to dist/ directory
  • Generates manifest.json for WordPress asset loading
  • CSS and JS are hashed for cache busting

Key settings from vite.config.js:

  • Path alias: @src/
  • Output: dist/assets/
  • Manifest: Enabled for WordPress integration

All styling uses Tailwind utility classes. Configuration in tailwind.config.js.

src/index.css includes:

  • Tailwind directives (@tailwind base/components/utilities)
  • Custom component classes
  • CSS variables for theming

When the app is installed as a PWA or loaded in a mobile browser (Add to Home Screen), browser caching can prevent users from receiving updates. The version check system addresses this:

  1. Version Endpoint: /rondo/v1/version returns the current theme version
  2. Periodic Checking: useVersionCheck hook polls for new versions
  3. Update Banner: When a new version is detected, a banner appears at the top of the screen with a “Reload” button

How it works:

  1. On app load, the current version is stored from window.rondoConfig.version
  2. Every 5 minutes (and when the user returns to the tab), the hook fetches /rondo/v1/version
  3. If the server version differs from the loaded version, hasUpdate becomes true
  4. The UpdateBanner component renders at the top of App.jsx when an update is available
  5. User clicks “Reload” → window.location.reload(true) forces a fresh load

Note: The version is embedded in both the HTML response (via rondoConfig) and the asset filenames (via Vite’s hash-based naming), ensuring a reload fetches all new assets.