Frontend Architecture
This document describes the React Single Page Application (SPA) that powers the Rondo Club frontend.
Technology Stack
Section titled “Technology Stack”| Technology | Version | Purpose |
|---|---|---|
| React | 18 | UI library |
| React Router | 6 | Client-side routing |
| TanStack Query | Latest | Server state management and caching |
| Axios | Latest | HTTP client |
| Tailwind CSS | 3.4 | Styling |
| Vite | 5.0 | Build tool and dev server |
Directory Structure
Section titled “Directory Structure”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)Entry Points
Section titled “Entry Points”src/main.jsx
Section titled “src/main.jsx”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, }, },});src/App.jsx
Section titled “src/App.jsx”Main routing component:
- Defines all application routes
- Implements
ProtectedRoutewrapper for authentication - Wraps protected routes in
Layoutcomponent
Routing
Section titled “Routing”Route Structure
Section titled “Route Structure”| Path | Component | Description | Access |
|---|---|---|---|
/login | Login | Public login page | Public |
/ | Dashboard | Home dashboard | Auth |
/people | PeopleList | Contact list | Auth |
/people/:id | PersonDetail | View contact | Auth |
/teams | TeamsList | Team list | Auth |
/teams/:id | TeamDetail | View team | Auth |
/commissies | CommissiesList | Committee list | Auth |
/commissies/:id | CommissieDetail | Committee detail | Auth |
/todos | TodosList | Todo list | Auth |
/feedback | FeedbackList | Feedback list | Auth |
/feedback/:id | FeedbackDetail | Feedback detail | Auth |
/vog | VOG | VOG certificate tracking | VOG capability |
/vog/:tab | VOG | VOG tab view | VOG capability |
/tuchtzaken | DisciplineCasesList | Discipline cases | Fairplay capability |
/financien/contributie | Contributie | Fee overview | Financieel capability |
/financien/contributie/:tab | Contributie | Fee tab view | Financieel capability |
/financien/facturen | Facturen | Invoice list | Financieel capability |
/financien/facturen/:id | FactuurDetail | Invoice detail | Financieel capability |
/financien/instellingen | FinanceSettings | Finance configuration | Financieel capability |
/settings | Settings | Settings page | Auth |
/settings/:tab | Settings | Settings tab | Auth |
/settings/relationship-types | RelationshipTypes | Manage relationship types | Auth |
/settings/custom-fields | CustomFields | Custom field management | Auth |
/settings/feedback | FeedbackManagement | Feedback settings | Auth |
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).
Authentication
Section titled “Authentication”The ProtectedRoute component checks authentication state:
- Shows loading spinner while checking
- Redirects to
/loginif 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;}API Client
Section titled “API Client”src/api/client.js
Section titled “src/api/client.js”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.rondoConfigbefore each request - Response: Handles 401 (redirect to login) and 403 (log error)
API Helpers
Section titled “API Helpers”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, taxonomiesprmApi - Custom PRM endpoints:
prmApi.getDashboard()prmApi.search(query)prmApi.getReminders(daysAhead)prmApi.getCurrentUser()prmApi.sideloadGravatar(personId, email)prmApi.uploadPersonPhoto(personId, file)// ... and moreWordPress Configuration
Section titled “WordPress Configuration”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.
Custom Hooks
Section titled “Custom Hooks”useAuth (src/hooks/useAuth.js)
Section titled “useAuth (src/hooks/useAuth.js)”Returns authentication state from WordPress config:
const { isLoggedIn, userId, loginUrl, logoutUrl, isLoading } = useAuth();usePeople (src/hooks/usePeople.js)
Section titled “usePeople (src/hooks/usePeople.js)”People data hooks with TanStack Query:
| Hook | Purpose |
|---|---|
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'],};useDashboard (src/hooks/useDashboard.js)
Section titled “useDashboard (src/hooks/useDashboard.js)”Dashboard and utility hooks:
| Hook | Purpose |
|---|---|
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:
| Hook | Purpose |
|---|---|
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:
| Hook | Purpose |
|---|---|
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 validationuseSharing (src/hooks/useSharing.js)
Section titled “useSharing (src/hooks/useSharing.js)”Direct sharing hooks for sharing individual posts with specific users:
| Hook | Purpose |
|---|---|
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 resultsUsage Example:
const { data: shares } = useShares('people', personId);const addShare = useAddShare();const removeShare = useRemoveShare();const { data: users } = useUserSearch('john');
// Add a shareawait addShare.mutateAsync({ postType: 'people', postId: 123, userId: 456, permission: 'view' // or 'edit'});
// Remove a shareawait 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)});| Property | Type | Description |
|---|---|---|
hasUpdate | boolean | True when a new version is available |
currentVersion | string | Version loaded with the current page |
latestVersion | string | Latest version from server (when update available) |
reload | function | Triggers a page reload to get new version |
checkVersion | function | Manually 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" }
Utility Functions
Section titled “Utility Functions”src/utils/formatters.js
Section titled “src/utils/formatters.js”| Function | Purpose |
|---|---|
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 |
Constants
Section titled “Constants”src/constants/app.js
Section titled “src/constants/app.js”export const APP_NAME = 'Rondo Club';State Management
Section titled “State Management”Server State (TanStack Query)
Section titled “Server State (TanStack Query)”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
Client State
Section titled “Client State”Minimal client state via:
- Component state -
useStatefor form inputs, UI state - URL state - Route parameters, search params
- Zustand - Available for complex client state (currently unused)
Build Configuration
Section titled “Build Configuration”Development
Section titled “Development”npm run dev- Vite dev server at
http://localhost:5173 - Hot Module Replacement (HMR) enabled
- Theme auto-detects dev server when
WP_DEBUGis true
Production
Section titled “Production”npm run build- Output to
dist/directory - Generates
manifest.jsonfor WordPress asset loading - CSS and JS are hashed for cache busting
Vite Configuration
Section titled “Vite Configuration”Key settings from vite.config.js:
- Path alias:
@→src/ - Output:
dist/assets/ - Manifest: Enabled for WordPress integration
Styling
Section titled “Styling”Tailwind CSS
Section titled “Tailwind CSS”All styling uses Tailwind utility classes. Configuration in tailwind.config.js.
Global Styles
Section titled “Global Styles”src/index.css includes:
- Tailwind directives (
@tailwind base/components/utilities) - Custom component classes
- CSS variables for theming
PWA/Mobile App Support
Section titled “PWA/Mobile App Support”Version Check & Cache Invalidation
Section titled “Version Check & Cache Invalidation”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:
- Version Endpoint:
/rondo/v1/versionreturns the current theme version - Periodic Checking:
useVersionCheckhook polls for new versions - Update Banner: When a new version is detected, a banner appears at the top of the screen with a “Reload” button
How it works:
- On app load, the current version is stored from
window.rondoConfig.version - Every 5 minutes (and when the user returns to the tab), the hook fetches
/rondo/v1/version - If the server version differs from the loaded version,
hasUpdatebecomes true - The
UpdateBannercomponent renders at the top ofApp.jsxwhen an update is available - 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.
Related Documentation
Section titled “Related Documentation”- REST API - Backend API reference
- Data Model - Post types and fields
- Architecture - Overall system architecture