Frontend Documentation
Overview
The frontend is built with React 18, TypeScript, and modern tooling following best practices.
Project Structure
frontend/src/
├── __tests__/ # Unit tests
├── _generated/ # Generated types from OpenAPI
├── auth/ # Authentication context and hooks
├── client/ # API client hooks (TanStack Query)
├── components/ # Reusable UI components
├── hooks/ # Custom React hooks
├── i18n/ # Internationalization
├── lib/ # Utility functions
├── pages/ # Page components (organized by domain)
│ ├── admin/ # Admin panel pages
│ ├── auth/ # Authentication pages
│ ├── client/ # Client portal pages
│ ├── contacts/ # Contact management pages
│ ├── sessions/ # Session management pages
│ ├── quotes/ # Quote management pages
│ ├── invoices/ # Invoice management pages
│ ├── contracts/ # Contract management pages
│ ├── users/ # User-related pages
│ ├── settings/ # Settings pages
│ ├── dashboard/ # Dashboard pages
│ └── ... # Other domain pages
├── router/ # Route guards
├── router.tsx # Router configuration
└── store/ # Zustand state management
Page Organization: Pages are organized by domain for better maintainability and logical grouping. Each domain contains related pages (list, view, form pages) grouped together.
Key Concepts
Authentication
AuthProvider: Authentication context provideruseAuth: Hook to access authentication state- Token management and refresh
- User status checks
API Client
- TanStack Query v5 for data fetching
- Type-safe API calls using generated types
- Automatic caching and refetching
- Error handling
Structure: client/{module}/use{Module}.ts
Conventions de clés React Query : Les hooks utilisent des conventions uniformes pour les clés de cache. Voir QUOTES_INVOICES_HOOKS.md pour un exemple complet.
TanStack Query v5 Mutation Callbacks: All onSuccess and onError callbacks in useMutation hooks must accept 4 arguments: (data/error, variables, context, mutation).
Example:
import { useSessions } from "@/client/sessions/useSessions";
const { data, isLoading, error } = useSessions();
const createSession = useCreateSession({
onSuccess: (data, variables, context, mutation) => {
toast.success("Session created successfully");
void queryClient.invalidateQueries({ queryKey: ["sessions"] });
},
onError: (error, variables, context, mutation) => {
toast.error(
error instanceof Error ? error.message : "Failed to create session"
);
},
});
Routing
- TanStack Router for type-safe routing
- Route guards for authentication
- Status-based redirects
Navigation par entité
Chaque entité (quotes, invoices, contracts) a son propre scope de navigation indépendant :
- Routes de vue :
/quotes/:id,/invoices/:id,/contracts/:id- Pas de paramètres de recherche - Routes d'édition :
/quotes/:id/edit,/invoices/:id/edit,/contracts/:id/edit- Pas de paramètres de recherche - Routes de création :
/quotes/new?sessionId=...&contactId=...- Paramètres de recherche pour pré-remplir les formulaires uniquement
Le breadcrumb et le bouton "back" pointent toujours vers la liste de l'entité, pas vers le contexte parent (session/contact).
Les données de session et contact sont récupérées depuis l'entité elle-même (quote.session_id, quote.contact_id).
Voir QUOTES_INVOICES_HOOKS.md pour plus de détails.
Forms
- TanStack Form for form management
- Zod for validation
- Auto-fill on page load
Example:
const form = useForm({
defaultValues: { name: "" },
onSubmit: async ({ value }) => {
await createSession.mutateAsync(value);
},
});
State Management
- Zustand for global state
- Auth store: User and token
- UI store: Sidebar, theme
- WebSocket store: Connection state
See: frontend/src/store/README.md for detailed documentation
Components
The frontend uses a component architecture based on shadcn/ui primitives with business-specific wrappers.
Component Architecture:
-
shadcn/ui Primitives (
components/ui/): Base UI components from shadcn/ui- Installed components:
Alert,AlertDialog,Avatar,Badge,Breadcrumb,Button,Card,Checkbox,Command,Dialog,DropdownMenu,Input,Label,Popover,Progress,RadioGroup,ScrollArea,Select,Separator,Sheet,Skeleton,Table,Tabs,Textarea,Tooltip - These are copied into the project (not npm dependencies) and can be customized
- All use Radix UI primitives and Tailwind CSS for styling
- Follow PascalCase naming convention (e.g.,
Button.tsx,Dialog.tsx)
- Installed components:
-
Business Components (
components/ui/): Domain-specific wrappers around shadcn primitives- Examples:
ConfirmationDialog(wrapsDialog),StatusBadge(wrapsBadge),ProgressionBar(wrapsProgress) - Encapsulate business logic and provide simplified APIs for common use cases
- Should use shadcn primitives internally, not custom implementations
- Examples:
-
Layout Components (
components/layout/): Navigation and page structureSidebar: Desktop sidebar navigation (collapsible)MobileSidebar: Mobile sidebar usingSheetshadcn componentHeader: Top navigation barUserDropdown: Desktop user menu dropdownUserMenuMobile: Full-screen mobile user menu
-
Feature Components (
components/{feature}/): Feature-specific components- Organized by domain/feature (e.g.,
components/search/,components/files/,components/moodboard/,components/landing/)
- Organized by domain/feature (e.g.,
-
Molecule Components (
components/molecules/): Reusable composite components- Examples:
StatusBadge,PageHeader,DataField,PayInvoiceButton,PayInvoiceDialog
- Examples:
-
Organism Components (
components/organisms/): Complex layout components- Examples:
FormPageLayout,ViewPageLayout,ListPageLayout
- Examples:
Layout Components:
Sidebar: Desktop sidebar navigation (collapsible)MobileSidebar: Mobile sidebar usingSheetshadcn component with slide-in animationUserDropdown: Desktop user menu dropdownUserMenuMobile: Full-screen mobile user menu with expandable sub-menus- Automatically used on mobile devices (< 1024px)
- Scrollable content for long menus
- Supports nested menu sections (Mon compte, Paramètres, Système)
StatusBadge: Reusable component for displaying status labels with optional tooltips and dynamic variants. Always use StatusBadge instead of manually creating status badges.
Freemium UI Components:
PlanLimitBanner(components/ui/PlanLimitBanner.tsx): Displays a warning/block banner when the user approaches or reaches a plan limit (e.g., max sessions, max contacts). Shows amber warning at ≥ 80%, red block at 100%.PremiumFeatureGate(components/ui/PremiumFeatureGate.tsx): Wraps content with a blurred overlay + upgrade CTA when the user's plan doesn't include a required permission.
The branded Studio page is a paid feature controlled by the USE_STUDIO_PAGE permission:
- hide Studio-specific entry points in navigation when the permission is missing
- gate
/settings/public-profilewithPremiumFeatureGate - when the permission is missing, canonical public URLs must fall back to classic app-host routes (
app.aaperture.com)
Moodboard Components (components/moodboard/):
MoodboardItemCard: Unified card for a moodboard item (image or inspiration link). Handles zoom cursor, hover scale, drag handle icon, category label, and delete button. Accepts arenderBottomBarrender prop for custom bottom bars.SortableMoodboardItem: Wraps a card with@dnd-kit/sortablefor drag-and-drop reordering.getLinkPlatformFromUrl(url): Utility to detect Instagram vs Pinterest from a URL.- Used by
MoodboardTab(user view),ClientPortalSessionViewPage(client-portal), andClientSessionPage(legacy client).
Stripe Payment Components:
PayInvoiceButton: Button component that opens a payment dialog for invoicesPayInvoiceDialog: Dialog component for processing invoice payments with Stripe ElementsPaymentForm: Form component with Stripe Elements for secure card input
Usage Example:
import { PayInvoiceButton } from "@/components/molecules";
<PayInvoiceButton
invoiceId={invoice.id}
amount={invoice.amount}
currency="eur"
onPaymentSuccess={() => {
// Refresh invoice data
void queryClient.invalidateQueries({ queryKey: ["invoices", invoice.id] });
}}
/>;
Freemium / Plan Permissions
Use useMyPermissions() and usePlansWithDetails() from client/permissions/usePermissions.ts to read plan data.
const { data: myPermissions } = useMyPermissions();
// myPermissions.planCode — "FREE" | "PRO" | "ENTERPRISE"
// myPermissions.limits — [{ key: "max_sessions", value: 10 }, ...]
// myPermissions.permissions — ["UPLOAD_FILE", ...]
Plan feature constants (labels, formatters) are in lib/plan-features.ts:
LIMIT_LABELS— human-readable label per limit keyNOTABLE_PERMISSIONS— list of plan-gated features shown in UIMARKETING_PRICING_PLANS— marketing/pricing copy aligned with the real plan matrixformatLimitValue(value, key)— formats-1as "Illimité(e)s"
Stripe Integration
- Stripe Elements for secure payment processing
- Payment intents for invoice payments
- Subscription management hooks
- Webhook event handling (backend)
Environment Variables:
VITE_STRIPE_PUBLISHABLE_KEY- Stripe publishable key (required for frontend)
API Client: client/stripe/useStripe.ts
useMySubscription()- Get current subscriptionusePaymentHistory()- Get payment historyuseCreateSubscription()- Create a subscriptionuseCancelSubscription()- Cancel a subscriptionuseCreateInvoicePayment()- Create payment intent for invoice
Internationalization
- react-i18next for translations
- English and French supported
- Translation keys in
i18n/locales/{lang}/translation.json
Usage:
const { t } = useTranslation();
return <h1>{t("page.title")}</h1>;
File Uploads & Downloads
useFileUpload(infrontend/src/hooks/useFileUpload.ts) centralizes the presigned upload flow: it requests an upload URL via the relevant presigned endpoint, performs the PUT to R2, and then creates the database record.useFileDownload(infrontend/src/hooks/useFileDownload.ts) handles download clicks: it requests/storage/signed-url/:key, opens the short-lived link in a new tab, and surfaces toast errors. Use this hook whenever you need to let a user download any stored file.FileListautomatically usesuseFileDownloadfor its download button, so most pages only need to passfilesand (optionally)onDelete. OverrideonDownloadonly for specialized behavior.- Frontend code must never store signed URLs in state or props. Always request them on demand via
useFileDownloadto keep links fresh and avoid CORS issues whenR2_PUBLIC_URLis not configured. - Proposal default cover : la cover par défaut des proposals se configure sur
/settings/proposal-defaultsvia un upload de fichier (presigned URL versproposal-defaults/{userId}/..., puis sauvegarde de la clé dans les user settings). Le builder proposal utiliseuserSettings.proposalDefaultCoverImageUrl(URL signée fournie par l’API à partir de la clé).
Development Guidelines
Adding a New Page
- Create page directory in the appropriate domain:
pages/{domain}/{PageName}/{PageName}.tsx- Choose the domain based on the page's purpose (e.g.,
contacts/,sessions/,quotes/,invoices/,contracts/,users/,settings/,admin/,auth/, etc.) - If creating a new domain, create the domain directory first
- Choose the domain based on the page's purpose (e.g.,
- Add route in
router.tsx - Add page title in
components/layout/usePageTitle.ts - Add translations in
i18n/locales/
Example: Creating a new contact form page:
- Create:
pages/contacts/ContactFormPage/ContactFormPage.tsx - Add route:
/contacts/newand/contacts/$id/edit - Add page title configuration
- Add translations for the form
Client portal — two distinct systems
The application has two separate client portal systems:
Legacy client access (/client) — token-based
- Public routes with a short-lived
tokenquery param. /client?token=...— legacy client dashboard./client/sessions/:sessionId?token=...— session view with moodboard.- Resolve tokens with
useClientAccess(token, ...)fromclient/client-access/useClientAccess.ts. tokenmust be preserved in all navigation links.- Keep actions stacked on mobile (
flex-col sm:flex-row) and design for touch first.
Authenticated client portal (/client-portal) — JWT-based
- Clients create an account (
/client-portal/register) and authenticate with email+password. - All API calls go through
client-auth/api.ts(usesAuthorization: Bearer <jwt>). - Auth state managed by
ClientAuthProvideranduseClientAuth. - Hooks live in
client-auth/useClientData.tsandclient-auth/useClientFiles.ts. - Pages live in
pages/client-portal/. - Features: session list, session detail with moodboard (read + upload if
READ_WRITEaccess), documents, messaging.
Public hosts and local development
The product now uses three host families:
- marketing host:
- local:
https://lvh.me:5173 - production:
https://aaperture.com
- local:
- authenticated app host:
- local:
https://app.lvh.me:5173 - production:
https://app.aaperture.com
- local:
- tenant public hosts:
- local:
https://{slug}.lvh.me:5173 - production:
https://{slug}.aaperture.com
- local:
Use tenant hosts for public booking, studio page, public client access, and public lead forms whenever a canonical public URL is needed.
PublicHostService remains the source of truth for URL generation. Do not reconstruct tenant URLs manually in React. This matters especially because the paid Studio page option can disable tenant-host URLs and force app-host fallbacks.
Route shapes stay stable:
- studio page:
/ - booking:
/booking - authenticated client portal entry:
/portal/login - public lead form:
/public/lead-forms/:formId
Backward compatibility rule:
- App-host public URLs that were already shared must continue to resolve.
- New copy/share UI should prefer the tenant host when available.
client-auth/ key exports:
useClientSession(sessionId)— fetch single sessionuseClientSessionMoodboard(sessionId)— fetch moodboard with itemsuseCreateClientSessionMoodboardItem(sessionId)— add item (image or link)useUploadClientSessionMoodboardImages(sessionId)— upload images to R2useUpdateClientSessionMoodboardItem(sessionId)— edit category / reorderuseDeleteClientSessionMoodboardItem(sessionId)— delete item
Adding a New API Hook
- Create hook file:
client/{module}/use{Module}.ts - Use TanStack Query mutations/queries
- Use generated types from
_generated/types.gen.ts - Export from
client/{module}/index.ts
Creating Reusable Components
- Create component in
components/ui/orcomponents/{feature}/ - Make it responsive (mobile + desktop)
- Add TypeScript types
- Use i18n for all text
- Follow existing component patterns
Form Best Practices
- Use TanStack Form
- Validate with Zod
- Fill form on page load with existing data
- Show validation errors
- Disable submit button during submission
Styling
- Tailwind CSS: Utility-first CSS framework for all styling
- shadcn/ui: All base UI components use shadcn/ui primitives
- Migration to shadcn/ui completed ✅
- Components are copied into the project and can be customized
- Consistent design system across the application
- Design Patterns: Follow existing component patterns and shadcn/ui conventions
- Responsive Design: All components must be mobile-responsive (see Mobile Responsiveness section)
- Component Variants: Use consistent variant patterns (e.g., Button variants:
default,destructive,outline,secondary,ghost,link)
Mobile Responsiveness
Mobile-First Approach:
- All components should be mobile-responsive
- Use Tailwind breakpoints:
sm:,md:,lg:(1024px),xl: - Mobile sidebar uses slide-in animation
- User menu automatically switches to full-screen on mobile
- Touch-friendly interactions (larger tap targets, proper spacing)
Mobile Navigation:
MobileSidebar: Slide-in sidebar for mobile navigationUserMenuMobile: Full-screen user menu with expandable sections- Automatic detection:
UserDropdowndetects screen size and usesUserMenuMobileon mobile
Type Safety
- Always use types from
_generated/types.gen.ts - Don't create duplicate types
- Use TypeScript strictly
Testing
Run tests:
npm run test
Write tests for:
- Components
- Hooks
- Utility functions
- API client
Environment Variables
See .env.example for required variables:
- API base URL
- WebSocket URL
- Feature flags