Project Architecture
Version 1 · Initial architecture doc based on codebase exploration
QDG CMS — Project Architecture
Stack
React 19.1 + TypeScript ~5.8.3, built with Vite 7 (vite.config.ts: @vitejs/plugin-react), styled with Tailwind CSS 3.4, linted with ESLint 9 (flat config, eslint.config.js).
Key dependencies: axios (API client), react-router-dom v7 (routing), @tiptap/* (rich-text editor), cropperjs (image cropping), lucide-react (icons), jszip (bulk export of image size variants). No global state-management library (no Redux/Zustand/Recoil) — React built-ins and Context are used instead. No UI component library (no MUI) — plain Tailwind markup throughout, by explicit design choice.
Folder layout (src/)
A hybrid layered + folder-by-feature structure:
pages/ one component per route
router/ MainRoutes, AuthRoutes (disabled), ProtectedRoute
layouts/ MainLayout (active), AuthLayout (unused)
components/
common/ LogoutButton, ThemeToggle, UserBadge
shared/ generic UI primitives (DropdownMenu, Pagination, Toast, ...)
features/ folder-per-feature: create-story, image-editor, image-gallery,
media, story-list, video-gallery, video-uploader
services/api/ axios-based API client layer, one file per resource
context/ AuthContext, ThemeContext
config/ axiosInstance, envConfig, sessionExpiry
hooks/ useCropper, useMediaTray, useTheme
types/ per-domain TS interfaces (auth, image, lookup, media, story, user, video)
data/ static/mock data
utils/ centralAuth, clipboard, imageFormat, storyMappings, videoFormat
components/features/create-story/ is the largest feature folder (~20 files): editor toolbar, image/video trays, linked-story table, validation modal, scheduling, classification, attribution sections.
Routing
react-router-dom v7 with createBrowserRouter, defined in src/App.tsx:
/ → MainLayout → ProtectedRoute → MainRoutes
/dashboard/create-story
/dashboard/story-list
/dashboard/image-gallery
/dashboard/image-editor
/dashboard/video-gallery
/dashboard/video-uploader
/dashboard/create-user
ProtectedRoute.tsx blocks rendering while the auth check is loading and redirects unauthenticated users externally (to the centralized auth portal), not to a local /login route. AuthRoutes, AuthLayout, and the local LoginPage/TotpVerificationPage are dead code — retained but unused, since login/2FA moved out of the app entirely.
State management
No global store. State is handled with:
- Local component state (
useState/useEffect) within pages/features. - Two app-wide React Contexts:
AuthContext(session/current user) andThemeContext(light/dark theme, backed byuseTheme). - Custom hooks encapsulating feature-specific stateful logic:
useCropper(Cropper.js lifecycle/config),useMediaTray(attached image/video tray state in the story editor).
Auth architecture
Cookie-session broker pattern, deliberately kept out of this app:
- No tokens stored client-side;
qdg_sessionis an HttpOnly cookie set by the backend. - Login/2FA fully delegated to
auth.qdatasite.com(src/utils/centralAuth.tsdoes a hardwindow.location.hrefredirect since it's cross-origin, not a client-side route). - A token returned from that redirect is exchanged once for the session cookie (
POST /Auth/exchange-token). localStorage'sqdg_session_expires_atis advisory only — used to skip an unnecessary/Auth/mecall and to drive an auto-logout timer; the cookie/server session is authoritative.
See API Documents for the full request/response flow.
Notable design decisions
- Two-phase presigned-URL upload to Wasabi — file bytes never pass through the CMS API; the frontend gets a presigned URL, uploads directly to Wasabi, then registers the resulting object key with the API. Documented inline in the code as an explicit architectural decision (31 Aug 2026). See API Documents and Database.
- Media size variants — photos store 5 independently-uploaded size-variant object keys (small/medium/hd/hires/large); videos have none.
- Cropper.js wrapped in
useCropper— replicates the crop-box behavior of the legacy Appsmith widget it replaces, exporting crops to canvas at exact pixel sizes per configured variant. - TipTap rich-text editor — story content editor with table/link/underline/text-align/color/highlight extensions plus a custom
tiptapLineHeightextension. - Legacy Appsmith migration context — many components/comments explicitly replicate a prior Appsmith widget set's behavior (crop box,
CustomStoryLinkwidget, form layouts); this explains some non-obvious UI/behavioral choices. - Mock-to-real migration in progress —
imageService/videoService/storyFormServiceare legacy mock layers being phased out in favor ofmediaService/storyServiceagainst the real API (see API Documents).
Build & deployment
npm run build→tsc -b && vite build..github/workflows/deploy.yml: self-hosted runner,npm ci && npm run build, deploysdist/to an on-prem IIS site (C:\inetpub\wwwroot\QdgCms), with automatic backup/rollback and a post-deploy health check againsthttp://localhost:8013. Deploys from thedevbranch, manual trigger (workflow_dispatch) only. Build and deploy are merged into a single job to avoid GitHub Actions artifact-storage quota issues.