<mxfile host="app.diagrams.net" modified="2026-06-02T00:00:00.000Z" agent="Claude" version="24.7.17">
  <diagram id="c4-component-frontend" name="C4 Component - Frontend SPA">
    <mxGraphModel dx="1400" dy="900" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1600" pageHeight="1200" math="0" shadow="0">
      <root>
        <mxCell id="0"/>
        <mxCell id="1" parent="0"/>

        <mxCell id="title" value="Component Diagram — Frontend SPA" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;whiteSpace=wrap;fontSize=20;fontStyle=1;fontColor=#1F2937;" vertex="1" parent="1">
          <mxGeometry x="380" y="20" width="740" height="40" as="geometry"/>
        </mxCell>
        <mxCell id="subtitle" value="[C4 Level 3 — Component | Container: Single-Page Application (React 19 + TypeScript)]" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;whiteSpace=wrap;fontSize=12;fontStyle=2;fontColor=#6B7280;" vertex="1" parent="1">
          <mxGeometry x="200" y="55" width="1100" height="25" as="geometry"/>
        </mxCell>

        <!-- Visitor -->
        <mxCell id="visitor" value="&lt;b&gt;Visitor&lt;/b&gt;&lt;br&gt;&lt;br&gt;Uses web browser&lt;br&gt;to browse portfolio" style="shape=mxgraph.c4.person2;whiteSpace=wrap;html=1;align=center;fillColor=#08427B;fontColor=#ffffff;strokeColor=#073B6F;fontSize=11;" vertex="1" parent="1">
          <mxGeometry x="700" y="85" width="180" height="150" as="geometry"/>
        </mxCell>

        <!-- Container boundary -->
        <mxCell id="fe_boundary" value="Single-Page Application [Container: React 19 + TypeScript + Vite]" style="swimlane;fontStyle=1;fillColor=none;strokeColor=#438DD5;startSize=32;rounded=1;arcSize=3;fontSize=12;fontColor=#438DD5;strokeWidth=2;dashed=1;dashPattern=8 4;" vertex="1" parent="1">
          <mxGeometry x="50" y="300" width="1480" height="820" as="geometry"/>
        </mxCell>

        <!-- Navigation -->
        <mxCell id="comp_nav" value="&lt;b&gt;Navigation&lt;/b&gt;&lt;br&gt;[Component: React + Wouter]&lt;br&gt;&lt;br&gt;Top-level routing bar.&lt;br&gt;Anchor-scroll on home, Link on sub-pages." style="rounded=1;whiteSpace=wrap;html=1;fillColor=#85BBF0;fontColor=#23445D;strokeColor=#6196CA;arcSize=5;fontSize=10;align=center;" vertex="1" parent="fe_boundary">
          <mxGeometry x="40" y="60" width="270" height="120" as="geometry"/>
        </mxCell>

        <!-- HeroSection -->
        <mxCell id="comp_hero" value="&lt;b&gt;HeroSection&lt;/b&gt;&lt;br&gt;[Component: React]&lt;br&gt;&lt;br&gt;Landing page hero.&lt;br&gt;Intro text, CTA buttons." style="rounded=1;whiteSpace=wrap;html=1;fillColor=#85BBF0;fontColor=#23445D;strokeColor=#6196CA;arcSize=5;fontSize=10;align=center;" vertex="1" parent="fe_boundary">
          <mxGeometry x="370" y="60" width="230" height="120" as="geometry"/>
        </mxCell>

        <!-- HowItsMadeSection -->
        <mxCell id="comp_him" value="&lt;b&gt;HowItsMadeSection&lt;/b&gt;&lt;br&gt;[Component: React]&lt;br&gt;&lt;br&gt;Tech stack showcase.&lt;br&gt;Links to docs via VITE_DOCS_URL." style="rounded=1;whiteSpace=wrap;html=1;fillColor=#85BBF0;fontColor=#23445D;strokeColor=#6196CA;arcSize=5;fontSize=10;align=center;" vertex="1" parent="fe_boundary">
          <mxGeometry x="660" y="60" width="250" height="120" as="geometry"/>
        </mxCell>

        <!-- ContactForm -->
        <mxCell id="comp_contact" value="&lt;b&gt;ContactForm&lt;/b&gt;&lt;br&gt;[Component: React + TanStack Query]&lt;br&gt;&lt;br&gt;useContactForm hook.&lt;br&gt;Calls POST /api/contact." style="rounded=1;whiteSpace=wrap;html=1;fillColor=#85BBF0;fontColor=#23445D;strokeColor=#6196CA;arcSize=5;fontSize=10;align=center;" vertex="1" parent="fe_boundary">
          <mxGeometry x="980" y="60" width="260" height="110" as="geometry"/>
        </mxCell>

        <!-- CVDownloadButton -->
        <mxCell id="comp_cv" value="&lt;b&gt;CVDownloadButton&lt;/b&gt;&lt;br&gt;[Component: React]&lt;br&gt;&lt;br&gt;EN/NL selector.&lt;br&gt;Calls GET /api/download-cv." style="rounded=1;whiteSpace=wrap;html=1;fillColor=#85BBF0;fontColor=#23445D;strokeColor=#6196CA;arcSize=5;fontSize=10;align=center;" vertex="1" parent="fe_boundary">
          <mxGeometry x="1290" y="60" width="160" height="110" as="geometry"/>
        </mxCell>

        <!-- useProjects hook -->
        <mxCell id="comp_projects" value="&lt;b&gt;useProjects hook&lt;/b&gt;&lt;br&gt;[Component: TanStack Query]&lt;br&gt;&lt;br&gt;Fetches GET /api/projects.&lt;br&gt;Static initialData fallback." style="rounded=1;whiteSpace=wrap;html=1;fillColor=#85BBF0;fontColor=#23445D;strokeColor=#6196CA;arcSize=5;fontSize=10;align=center;" vertex="1" parent="fe_boundary">
          <mxGeometry x="40" y="290" width="260" height="110" as="geometry"/>
        </mxCell>

        <!-- API Client -->
        <mxCell id="comp_apiclient" value="&lt;b&gt;API Client&lt;/b&gt;&lt;br&gt;[Component: openapi-fetch]&lt;br&gt;&lt;br&gt;Type-safe HTTP client.&lt;br&gt;Generated from OpenAPI schema." style="rounded=1;whiteSpace=wrap;html=1;fillColor=#85BBF0;fontColor=#23445D;strokeColor=#6196CA;arcSize=5;fontSize=10;align=center;" vertex="1" parent="fe_boundary">
          <mxGeometry x="370" y="290" width="260" height="110" as="geometry"/>
        </mxCell>

        <!-- Vite Build -->
        <mxCell id="comp_vite" value="&lt;b&gt;Vite Build&lt;/b&gt;&lt;br&gt;[Component: Vite + esbuild]&lt;br&gt;&lt;br&gt;Bakes VITE_DOCS_URL into bundle.&lt;br&gt;Outputs dist/public/." style="rounded=1;whiteSpace=wrap;html=1;fillColor=#85BBF0;fontColor=#23445D;strokeColor=#6196CA;arcSize=5;fontSize=10;align=center;" vertex="1" parent="fe_boundary">
          <mxGeometry x="700" y="290" width="260" height="110" as="geometry"/>
        </mxCell>

        <!-- nginx proxy -->
        <mxCell id="comp_nginx" value="&lt;b&gt;nginx Reverse Proxy&lt;/b&gt;&lt;br&gt;[Component: nginx 1.25]&lt;br&gt;&lt;br&gt;Serves static assets.&lt;br&gt;Proxies /api/* to backend:8000." style="rounded=1;whiteSpace=wrap;html=1;fillColor=#85BBF0;fontColor=#23445D;strokeColor=#6196CA;arcSize=5;fontSize=10;align=center;" vertex="1" parent="fe_boundary">
          <mxGeometry x="1020" y="290" width="260" height="110" as="geometry"/>
        </mxCell>

        <!-- useProjectPageCMS hook -->
        <mxCell id="comp_cms_hook" value="&lt;b&gt;useProjectPageCMS Hook&lt;/b&gt;&lt;br&gt;[Component: TanStack Query]&lt;br&gt;&lt;br&gt;Fetches project-page-v2 content&lt;br&gt;from Strapi /cms/api/project-pages.&lt;br&gt;Falls back to static projectV2Data&lt;br&gt;on error." style="rounded=1;whiteSpace=wrap;html=1;fillColor=#85BBF0;fontColor=#23445D;strokeColor=#6196CA;arcSize=5;fontSize=10;align=center;" vertex="1" parent="fe_boundary">
          <mxGeometry x="40" y="490" width="280" height="130" as="geometry"/>
        </mxCell>

        <!-- Feature flag note -->
        <mxCell id="note_ff" value="&lt;i&gt;Route /projects-v2 in App.tsx is gated&lt;br&gt;by the &lt;b&gt;projectPageV2&lt;/b&gt; feature flag&lt;br&gt;(Azure App Configuration).&lt;/i&gt;" style="text;html=1;strokeColor=#D1D5DB;fillColor=#FFFDE7;align=left;verticalAlign=middle;whiteSpace=wrap;fontSize=9;rounded=1;arcSize=5;" vertex="1" parent="fe_boundary">
          <mxGeometry x="370" y="510" width="300" height="80" as="geometry"/>
        </mxCell>

        <!-- External: API Backend -->
        <mxCell id="ext_api" value="&lt;b&gt;API Backend&lt;/b&gt;&lt;br&gt;[Container]&lt;br&gt;&lt;br&gt;FastAPI, port 8000" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#999999;fontColor=#ffffff;strokeColor=#8C8C8C;arcSize=5;fontSize=10;align=center;" vertex="1" parent="1">
          <mxGeometry x="1220" y="1000" width="220" height="100" as="geometry"/>
        </mxCell>

        <!-- External: Docs -->
        <mxCell id="ext_docs" value="&lt;b&gt;Documentation Site&lt;/b&gt;&lt;br&gt;[Container]&lt;br&gt;&lt;br&gt;MkDocs, VITE_DOCS_URL" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#999999;fontColor=#ffffff;strokeColor=#8C8C8C;arcSize=5;fontSize=10;align=center;" vertex="1" parent="1">
          <mxGeometry x="140" y="1000" width="220" height="100" as="geometry"/>
        </mxCell>

        <!-- External: CMS (Strapi v5) -->
        <mxCell id="ext_cms" value="&lt;b&gt;CMS&lt;/b&gt;&lt;br&gt;[Container: Strapi v5]&lt;br&gt;&lt;br&gt;cms.dev.sven-relijveld.com" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#999999;fontColor=#ffffff;strokeColor=#8C8C8C;arcSize=5;fontSize=10;align=center;" vertex="1" parent="1">
          <mxGeometry x="550" y="1000" width="220" height="100" as="geometry"/>
        </mxCell>

        <!-- Relationships -->
        <mxCell id="e1" value="Browses" style="edgeStyle=orthogonalEdgeStyle;rounded=1;html=1;strokeColor=#707070;strokeWidth=1.5;fontSize=9;" edge="1" source="visitor" target="comp_nav" parent="1">
          <mxGeometry relative="1" as="geometry">
            <Array as="points">
              <mxPoint x="700" y="265"/>
              <mxPoint x="175" y="265"/>
            </Array>
          </mxGeometry>
        </mxCell>
        <mxCell id="e2" value="Route changes" style="edgeStyle=orthogonalEdgeStyle;rounded=1;html=1;strokeColor=#707070;strokeWidth=1.5;fontSize=9;" edge="1" source="comp_nav" target="comp_hero" parent="fe_boundary">
          <mxGeometry relative="1" as="geometry"/>
        </mxCell>
        <mxCell id="e3" value="Uses" style="edgeStyle=orthogonalEdgeStyle;rounded=1;html=1;strokeColor=#707070;strokeWidth=1.5;fontSize=9;" edge="1" source="comp_contact" target="comp_apiclient" parent="fe_boundary">
          <mxGeometry relative="1" as="geometry">
            <Array as="points">
              <mxPoint x="1110" y="245"/>
              <mxPoint x="500" y="245"/>
            </Array>
          </mxGeometry>
        </mxCell>
        <mxCell id="e4" value="Uses" style="edgeStyle=orthogonalEdgeStyle;rounded=1;html=1;strokeColor=#707070;strokeWidth=1.5;fontSize=9;" edge="1" source="comp_cv" target="comp_apiclient" parent="fe_boundary">
          <mxGeometry relative="1" as="geometry">
            <Array as="points">
              <mxPoint x="1370" y="250"/>
              <mxPoint x="500" y="250"/>
            </Array>
          </mxGeometry>
        </mxCell>
        <mxCell id="e5" value="Uses" style="edgeStyle=orthogonalEdgeStyle;rounded=1;html=1;strokeColor=#707070;strokeWidth=1.5;fontSize=9;" edge="1" source="comp_projects" target="comp_apiclient" parent="fe_boundary">
          <mxGeometry relative="1" as="geometry"/>
        </mxCell>
        <mxCell id="e6" value="HTTP /api/*" style="edgeStyle=orthogonalEdgeStyle;rounded=1;html=1;strokeColor=#707070;strokeWidth=1.5;fontSize=9;" edge="1" source="comp_apiclient" target="comp_nginx" parent="fe_boundary">
          <mxGeometry relative="1" as="geometry"/>
        </mxCell>
        <mxCell id="e7" value="Proxies to" style="edgeStyle=orthogonalEdgeStyle;rounded=1;html=1;strokeColor=#707070;strokeWidth=1.5;fontSize=9;" edge="1" source="comp_nginx" target="ext_api" parent="1">
          <mxGeometry relative="1" as="geometry"/>
        </mxCell>
        <mxCell id="e8" value="Opens link" style="edgeStyle=orthogonalEdgeStyle;rounded=1;html=1;strokeColor=#707070;strokeWidth=1.5;fontSize=9;" edge="1" source="comp_nav" target="ext_docs" parent="1">
          <mxGeometry relative="1" as="geometry">
            <Array as="points">
              <mxPoint x="175" y="1050"/>
            </Array>
          </mxGeometry>
        </mxCell>
        <!-- useProjectPageCMS -> CMS (Strapi v5) -->
        <mxCell id="e9" value="GET /cms/api/project-pages" style="edgeStyle=orthogonalEdgeStyle;rounded=1;html=1;strokeColor=#707070;strokeWidth=1.5;fontSize=9;" edge="1" source="comp_cms_hook" target="ext_cms" parent="1">
          <mxGeometry relative="1" as="geometry"/>
        </mxCell>

        <!-- Legend -->
        <mxCell id="legend_bg" value="" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#F9FAFB;strokeColor=#D1D5DB;arcSize=5;" vertex="1" parent="1">
          <mxGeometry x="30" y="90" width="310" height="165" as="geometry"/>
        </mxCell>
        <mxCell id="leg_title" value="Legend" style="text;html=1;strokeColor=none;fillColor=none;fontSize=11;fontStyle=1;fontColor=#374151;" vertex="1" parent="1">
          <mxGeometry x="45" y="98" width="60" height="20" as="geometry"/>
        </mxCell>
        <mxCell id="leg_p" value="" style="shape=mxgraph.c4.person2;fillColor=#08427B;fontColor=#ffffff;strokeColor=#073B6F;" vertex="1" parent="1">
          <mxGeometry x="50" y="124" width="22" height="26" as="geometry"/>
        </mxCell>
        <mxCell id="leg_p_l" value="Person" style="text;html=1;strokeColor=none;fillColor=none;fontSize=10;" vertex="1" parent="1">
          <mxGeometry x="80" y="129" width="80" height="18" as="geometry"/>
        </mxCell>
        <mxCell id="leg_c" value="" style="rounded=1;fillColor=#85BBF0;strokeColor=#6196CA;" vertex="1" parent="1">
          <mxGeometry x="50" y="156" width="22" height="16" as="geometry"/>
        </mxCell>
        <mxCell id="leg_c_l" value="Component (this container)" style="text;html=1;strokeColor=none;fillColor=none;fontSize=10;" vertex="1" parent="1">
          <mxGeometry x="80" y="156" width="200" height="18" as="geometry"/>
        </mxCell>
        <mxCell id="leg_e" value="" style="rounded=1;fillColor=#999999;strokeColor=#8C8C8C;" vertex="1" parent="1">
          <mxGeometry x="50" y="180" width="22" height="16" as="geometry"/>
        </mxCell>
        <mxCell id="leg_e_l" value="External Container" style="text;html=1;strokeColor=none;fillColor=none;fontSize=10;" vertex="1" parent="1">
          <mxGeometry x="80" y="180" width="140" height="18" as="geometry"/>
        </mxCell>
        <mxCell id="leg_b" value="" style="rounded=1;fillColor=none;strokeColor=#438DD5;dashed=1;dashPattern=8 4;strokeWidth=2;" vertex="1" parent="1">
          <mxGeometry x="50" y="204" width="22" height="16" as="geometry"/>
        </mxCell>
        <mxCell id="leg_b_l" value="Container Boundary" style="text;html=1;strokeColor=none;fillColor=none;fontSize=10;" vertex="1" parent="1">
          <mxGeometry x="80" y="204" width="140" height="18" as="geometry"/>
        </mxCell>
        <mxCell id="leg_b2" value="" style="rounded=1;fillColor=none;strokeColor=#438DD5;dashed=1;dashPattern=8 4;strokeWidth=2;" vertex="1" parent="1">
          <mxGeometry x="50" y="228" width="22" height="16" as="geometry"/>
        </mxCell>
        <mxCell id="leg_b2_l" value="Container Boundary" style="text;html=1;strokeColor=none;fillColor=none;fontSize=10;" vertex="1" parent="1">
          <mxGeometry x="80" y="228" width="140" height="18" as="geometry"/>
        </mxCell>
      </root>
    </mxGraphModel>
  </diagram>
</mxfile>
