QDG Knowledge Base Read-only viewer QWebHub
general

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) and ThemeContext (light/dark theme, backed by useTheme).
  • 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_session is an HttpOnly cookie set by the backend.
  • Login/2FA fully delegated to auth.qdatasite.com (src/utils/centralAuth.ts does a hard window.location.href redirect 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's qdg_session_expires_at is advisory only — used to skip an unnecessary /Auth/me call 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 tiptapLineHeight extension.
  • Legacy Appsmith migration context — many components/comments explicitly replicate a prior Appsmith widget set's behavior (crop box, CustomStoryLink widget, form layouts); this explains some non-obvious UI/behavioral choices.
  • Mock-to-real migration in progress — imageService/videoService/storyFormService are legacy mock layers being phased out in favor of mediaService/storyService against 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, deploys dist/ to an on-prem IIS site (C:\inetpub\wwwroot\QdgCms), with automatic backup/rollback and a post-deploy health check against http://localhost:8013. Deploys from the dev branch, manual trigger (workflow_dispatch) only. Build and deploy are merged into a single job to avoid GitHub Actions artifact-storage quota issues.
Updated by Claude on Sept. 2, 2026, 6:21 a.m.