diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 432201a..8baa07d 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -6,6 +6,7 @@ - [System Architecture](#system-architecture) - [Production DevOps Platform](#production-devops-platform) - [Frontend Architecture](#frontend-architecture) +- [Landing Page 3D Experience](#landing-page-3d-experience) - [Multi-Format Ingestion & Rendering](#multi-format-ingestion--rendering) - [Backend Architecture](#backend-architecture) - [Document Storage & Data Model](#document-storage--data-model) @@ -368,6 +369,74 @@ graph TB --- +## Landing Page 3D Experience + +The marketing landing page (`frontend/src/pages/LandingPage.js`) is a cinematic, **scroll-reactive 3D experience** rendered with **Three.js** through **React Three Fiber (R3F)** and **Drei**. The scene lives in `frontend/src/components/three/HeroExperience.js` and is lazy-loaded (via `React.lazy`) so the first paint of the hero copy never blocks on the Three.js bundle (~240 kB gzipped, split into its own chunk). + +### Design goals + +- **One continuous background, not two boxes.** A single WebGL canvas sits behind the *entire* page and the camera travels through it as the user scrolls — replacing the earlier approach of two isolated canvases (hero + CTA). +- **Readable content over a moving scene.** Content surfaces are **solid dark panels**; the 3D shows through the open areas (hero, CTA, inter-section gaps), keeping copy crisp. +- **Zero binary assets.** Every shape is primitive geometry, lighting is rendered into an in-memory cube with `` panels (no `.hdr`), and particles use ``. Nothing extra ships in the bundle. +- **Graceful degradation** for reduced-motion, no-WebGL, and low-power devices. + +### Compositing: sticky canvas + negative-margin overlay + +The page can't use `position: fixed` for the background because the app's route wrapper (`.page-transition`) applies `will-change: transform`, which creates a containing block that would trap a fixed child. Instead the canvas is pinned with `position: sticky`: + +``` +LandingPage root (position: relative; overflow-x: clip) ← clip, NOT hidden, +│ so it isn't a scroll +│ container (sticky-safe) +├── 3D background (position: sticky; top: 0; height: 100svh; z-index: 0) +│ └── ← single full-viewport WebGL canvas +│ └── vignette + film-grain scrims (legibility) +│ +└── Foreground content (position: relative; z-index: 1; margin-top: -100svh) + └── hero · stats · features · … · CTA (solid panels, transparent sections) +``` + +The foreground is pulled up over the sticky background with `margin-top: -100svh`, so the background stays pinned to the viewport for the whole scroll while content scrolls on top of it. + +### Scroll-driven camera + +Page scroll is normalized to a `0 → 1` progress value, **stored in a ref** and updated by a `passive` scroll listener — it is **never** put in React state, so scrolling triggers **no re-renders**. A `ScrollCamera` component reads that ref every frame (`useFrame`) and lerps the camera down a vertical column of objects: + +- **Descend** — `camera.y` travels from `0` (hero core) to `-9` (CTA core), passing frosted-glass "document" panels distributed along the way. +- **Breathe** — `camera.z` eases back at the scroll midpoint (`sin(p·π)`) for a cinematic dolly. +- **Sway** — a small lateral `camera.x` drift, plus pointer-parallax on the whole group (`Rig`). + +```mermaid +flowchart LR + SCROLL[window scroll] -->|passive listener| REF[scrollRef 0..1] + REF -->|read each frame| UF[useFrame / ScrollCamera] + UF --> CAM[camera dolly y/z/x + lookAt] + CAM --> R[R3F renders single sticky canvas] + PTR[pointermove] -->|parallax| RIG[Rig group rotation] --> R +``` + +### Scene composition (`HeroExperience.js`) + +- **Two glowing cores** (`` icosahedra + additive halo) anchor the hero (top) and CTA (bottom). +- **Frosted-glass panels** (`` with `meshPhysicalMaterial` transmission) drift along the column via ``. +- **Metallic accents** (octahedron / dodecahedron / torus-knot) and a `` field add depth. +- **Procedural lighting** via `` + `` rectangles/ring rendered to an in-memory cube — no image-based lighting files. + +### Capability detection & fallbacks + +| Condition | Behavior | +| --- | --- | +| `prefers-reduced-motion` | All animation frozen; render loop switches to `frameloop="demand"`; camera holds its initial framing. | +| No WebGL context | Canvas is never mounted; a pure-CSS warm-glow backdrop (`Fallback`) is shown so the layout/copy stay intact. | +| Low-power device (≤4 cores / ≤4 GB) | Lighter scene — fewer panels/accents, no shadows or antialias, lower DPR, smaller environment resolution. | +| GPU context loss | `webglcontextlost` is `preventDefault`ed so the browser can restore instead of blanking; a `CanvasErrorBoundary` swaps in the CSS fallback on hard failure. | + +### Testing + +Because jsdom has no WebGL, the snapshot suite (`frontend/src/__tests__/07_snapshots.test.js`) mocks `HeroExperience` with a deterministic stub and polyfills the browser APIs the page relies on (`matchMedia`, `IntersectionObserver`, `ResizeObserver`, scroll helpers) in `setupTests.js`. This lets the full `LandingPage` render and snapshot deterministically without a GPU. + +--- + ## Multi-Format Ingestion & Rendering DocuThinker extracts and renders **many** document formats entirely in the browser. All of the parsing happens client-side in `frontend/src/components/UploadModal.js` (`extractDocument`), and the viewer in `frontend/src/pages/Home.js` picks a render path from the resulting `fileType`. The guiding principle is a clean separation between **what the AI sees** and **what the user sees**: diff --git a/README.md b/README.md index 7512602..0f086c6 100644 --- a/README.md +++ b/README.md @@ -155,6 +155,9 @@ DocuThinker is built with **120+ technologies** spanning frontend, backend, AI/M - **Material-UI (MUI) 6**: React component library for UI development. - **Tailwind CSS**: Utility-first CSS framework for rapid styling. - **Emotion**: CSS-in-JS styling engine (used by MUI). + - **Three.js**: WebGL 3D engine powering the landing page's procedural scene. + - **React Three Fiber (`@react-three/fiber`)**: React renderer for Three.js — declarative scene graph for the landing hero. + - **Drei (`@react-three/drei`)**: R3F helpers (`Float`, `RoundedBox`, `MeshDistortMaterial`, `Environment`, `Lightformer`, `Sparkles`) used to build the scene without binary assets. - **Axios**: Promise-based HTTP client for API requests. - **React Router DOM 6**: Declarative client-side routing. - **Context API**: Built-in React state management. @@ -373,6 +376,8 @@ DocuThinker is built with **120+ technologies** spanning frontend, backend, AI/M Material UI Tailwind CSS Emotion + Three.js + React Three Fiber React Router Axios Webpack @@ -540,6 +545,8 @@ DocuThinker is built with **120+ technologies** spanning frontend, backend, AI/M Landing Page

+The landing page is an interactive **3D experience** built with **Three.js** via **React Three Fiber** and **Drei**. A single full-page WebGL canvas sits behind the page and its camera dollies through the procedural scene as you scroll. It also degrades gracefully — honoring `prefers-reduced-motion` and falling back to a pure-CSS backdrop where WebGL isn't available. + ### **Document Upload Page**

diff --git a/frontend/src/__tests__/07_snapshots.test.js b/frontend/src/__tests__/07_snapshots.test.js new file mode 100644 index 0000000..a8f6164 --- /dev/null +++ b/frontend/src/__tests__/07_snapshots.test.js @@ -0,0 +1,116 @@ +/** + * Snapshot coverage for the presentational surface of the app. + * + * These lock in the rendered DOM for the static pages, the auth pages, the + * chrome (Navbar/Footer/Spinner), and the redesigned 3D LandingPage. Heavy + * runtime-only widgets (upload/chat modals that pull in axios + firebase + + * file APIs) are intentionally excluded — they have no deterministic initial + * render to snapshot. + * + * To intentionally update a snapshot after a UI change: `npx jest -u`. + */ +import React from "react"; +import { render, screen } from "@testing-library/react"; +import { MemoryRouter } from "react-router-dom"; +import { renderWithRouter } from "../test-utils"; + +// The hero's WebGL scene can't run in jsdom; swap in a deterministic stub so +// LandingPage snapshots are stable and don't depend on three.js / a GPU. +jest.mock("../components/three/HeroExperience", () => ({ + __esModule: true, + default: () =>

, +})); + +// WebAuthn/passkey helpers touch browser-only APIs missing in jsdom; stub them +// so the auth pages render their default (no-passkey) state. +jest.mock("../utils/passkeys", () => ({ + __esModule: true, + isPasskeySupported: () => false, + isPlatformAuthenticatorAvailable: async () => false, + authenticateWithPasskey: jest.fn(), + registerPasskey: jest.fn(), +})); + +import Spinner from "../components/Spinner"; +import Footer from "../components/Footer"; +import Navbar from "../components/Navbar"; +import NotFoundPage from "../pages/NotFoundPage"; +import PrivacyPolicy from "../pages/PrivacyPolicy"; +import TermsOfService from "../pages/TermsOfService"; +import HowToUse from "../pages/HowToUse"; +import Login from "../pages/Login"; +import Register from "../pages/Register"; +import ForgotPassword from "../pages/ForgotPassword"; +import LandingPage from "../pages/LandingPage"; + +describe("Component snapshots", () => { + it("Spinner renders consistently", () => { + const { container } = render(); + expect(container).toMatchSnapshot(); + }); + + it("Footer renders consistently", () => { + const { container } = renderWithRouter(