11 KiB
Slice 5: Frontend Shell + Segment Renderers
Role
You are a frontend TypeScript/Vue developer building the read-only public UI for a FastAPI-based academic submission website. The backend is complete — you are building the Vue 3 / Tailwind CSS v4 frontend that consumes its API.
Objective
Create the read-only frontend shell: sidebar navigation, responsive layout, data fetching composable, TypeScript types, and all segment renderers. No admin functionality in this slice. All files must pass vue-tsc --noEmit (strict TypeScript).
Architecture Reference
Read PLAN.md before starting. It contains the full architecture context: color system, layout specs, component hierarchy, and design decisions. Use it as the source of truth for any details not covered in this prompt.
UI Decision-Making Policy
When you encounter any ambiguity in visual design, layout, spacing, interaction patterns, or component behavior — use AskUserQuestion BEFORE implementing. This is the prioritized method of resolution. Do not guess or pick defaults for subjective UI choices. Examples of when to ask:
- Exact spacing/padding values not specified in the prompt
- Animation/transition details (duration, easing, direction)
- Empty state presentation (what to show when no segments exist)
- Icon choices (hamburger style, close button style)
- Typography sizing not explicitly stated
- Any "or" choices in the prompt (e.g., "
<iframe>or<embed>") - Hover/focus state details beyond what's specified
Only proceed without asking when the prompt or PLAN.md gives an unambiguous, specific instruction.
Coding Standards (mandatory)
- Vue 3
<script setup lang="ts">single-file components - Strict TypeScript — no
any, all props/emits typed - Tailwind CSS v4 utility classes (no separate CSS files per component)
- Composables use
ref/computed/onMountedfrom Vue — no Options API - Components are small and focused — one responsibility per file
- Use
@/path alias for imports (configured in vite.config.ts assrc/) - No external state management library — use composables with reactive state
Step 0: Verify Existing Setup
Before writing any code, verify the frontend builds:
cd frontend && npm run build
Must exit cleanly. If it fails, fix before proceeding.
Context: Backend API
The backend is running at http://localhost:8000 (proxied via Vite dev server at /api).
API Endpoints Used by This Slice
GET /api/site → { "title": string }
GET /api/segments → Array of SegmentResponse
GET /api/assets/{file} → Static file (binary)
SegmentResponse Shape (from backend models.py)
{
"id": "uuid-string",
"type": "markdown" | "pdf" | "video" | "audio" | "iframe" | "gallery",
"sort_order": 0,
"title": "Section Title",
"content": "markdown text or URL or empty",
"metadata": {},
"created_at": "2025-01-01T00:00:00",
"updated_at": "2025-01-01T00:00:00"
}
Content conventions by type:
markdown—contentis raw markdown textpdf—contentis the asset URL (e.g.,/api/assets/uuid.pdf)video—contentis the asset URL (e.g.,/api/assets/uuid.mp4)audio—contentis the asset URL (e.g.,/api/assets/uuid.mp3)iframe—contentis the embed URL (any external URL)gallery—metadatacontains{ "images": ["/api/assets/uuid.png", ...] }
Existing Frontend Files
frontend/src/style.css (already has theme):
@import "tailwindcss";
@plugin "@tailwindcss/typography";
@theme {
--color-primary-50: #fffbeb;
--color-primary-100: #fff3c4;
--color-primary-200: #fce588;
--color-primary-300: #ffcd00;
--color-primary-400: #e6b800;
--color-primary-500: #cc9900;
--color-primary-600: #997300;
--color-primary-700: #664d00;
}
frontend/vite.config.ts — has @ alias to src/, API proxy to localhost:8000.
frontend/src/main.ts — mounts App.vue at #app.
frontend/src/App.vue — placeholder, will be replaced.
UI Design Reference
Color System
- Primary: golden yellow
#ffcd00(primary-300) for accents and active states - Neutrals: Tailwind
slate— dark sidebar (slate-900), light content area (slate-50) - Active nav item:
primary-300left border + subtle bg highlight
Layout — Sidebar Navigation
Desktop (≥1024px):
- Fixed left sidebar,
w-64,bg-slate-900, full height - Sidebar header: site title styled in
primary-300, font bold - Nav items: segment titles as anchor links,
text-slate-300default,text-whiteon hover - Active segment:
border-l-2 border-primary-300 bg-slate-800+text-white - Main content:
ml-64, scrollable, segments as full-width cards onbg-slate-50
Tablet (768–1023px):
- Sidebar off-screen by default, toggle button to slide it in as overlay
Mobile (<768px):
- No sidebar visible.
MobileHeaderwith hamburger icon at top - Hamburger opens slide-out overlay nav (full-height,
bg-slate-900) - Close button or tap-outside to dismiss
- Segments stack vertically, full-width
Step 1: Create TypeScript Types
frontend/src/types/segment.ts
export type SegmentType = "markdown" | "pdf" | "video" | "audio" | "iframe" | "gallery";
export interface Segment {
id: string;
type: SegmentType;
sort_order: number;
title: string;
content: string;
metadata: Record<string, unknown>;
created_at: string;
updated_at: string;
}
export interface SiteInfo {
title: string;
}
Step 2: Create Data Fetching Composable
frontend/src/composables/useSegments.ts
Exports a composable function useSegments() that returns:
segments: Ref<Segment[]>— list of segments, ordered bysort_ordersiteTitle: Ref<string>— site title (default:"Handin")loading: Ref<boolean>— true while initial fetch is in progresserror: Ref<string | null>— error message if fetch failsrefresh: () => Promise<void>— re-fetch both endpoints
On mount (onMounted), fetch both GET /api/site and GET /api/segments in parallel using Promise.all. Use native fetch() — no axios.
Step 3: Create Layout Components
frontend/src/components/layout/AppShell.vue
Top-level layout component. Structure:
- Contains
SidebarNav(desktop) andMobileHeader(mobile) - Main content area as a
<slot /> - Uses CSS breakpoints: sidebar visible
lg:block, hidden belowlg - Mobile header visible below
lg, hidden atlg+
frontend/src/components/layout/SidebarNav.vue
Props:
segments: Segment[]siteTitle: stringactiveId: string | null
Emits:
navigate(id: string)— when a nav item is clicked
Renders:
- Site title in header area (
text-primary-300 font-bold text-lg) - List of segment titles as clickable items
- Active item has left border accent and bg highlight
- Scroll overflow if many segments
frontend/src/components/layout/MobileHeader.vue
Props:
siteTitle: stringsegments: Segment[]activeId: string | null
Manages its own open/closed state for the slide-out menu. Contains:
- Top bar with site title and hamburger button
- Slide-out overlay with same nav items as
SidebarNav - Click outside or close button dismisses the overlay
Step 4: Create Segment Renderers
frontend/src/components/segments/SegmentRenderer.vue
Props: segment: Segment
A switch component that renders the correct sub-renderer based on segment.type. Use a v-if/v-else-if chain or a dynamic component approach.
frontend/src/components/segments/SegmentList.vue
Props: segments: Segment[]
Renders a vertical list of segments, each wrapped in a card-like container. Each segment card:
- Has an
idattribute matching the segment ID (for anchor scroll) - White background, rounded, subtle shadow
- Title as heading, then the renderer below
Individual Renderers
Each takes a segment: Segment prop:
MarkdownSegment.vue
- Render
segment.contentas HTML usingmarkedlibrary - Wrap output in a
proseclass div (Tailwind typography plugin) - Use
v-htmlwith the parsed markdown
PdfSegment.vue
- Render an
<iframe>or<embed>pointing tosegment.content(the PDF URL) - Full-width, reasonable height (e.g.,
h-[600px]oraspect-[4/3])
VideoSegment.vue
- Render a
<video>element withcontrols,srcpointing tosegment.content - Full-width, responsive
AudioSegment.vue
- Render an
<audio>element withcontrols,srcpointing tosegment.content - Full-width
IframeSegment.vue
- Render an
<iframe>withsrcfromsegment.content - Full-width, reasonable height, border-none
- Add
sandboxandallowattributes for security
GallerySegment.vue
- Read image URLs from
segment.metadata.images(cast tostring[]) - Render as a responsive grid of
<img>elements - Grid: 2 columns on mobile, 3 on tablet, 4 on desktop
- Images have
object-cover, rounded corners
Step 5: Create HomePage View
frontend/src/views/HomePage.vue
Wires everything together:
- Uses
useSegments()composable - Tracks
activeId(string or null) — updates on scroll or nav click - Shows loading spinner/skeleton while
loadingis true - Shows error message if
erroris set - Renders
AppShellwithSegmentListin the main slot
Scroll tracking: Use IntersectionObserver to detect which segment is currently visible and update activeId for sidebar highlighting.
Step 6: Update App.vue
Replace the placeholder App.vue with:
<script setup lang="ts">
import HomePage from "@/views/HomePage.vue";
</script>
<template>
<HomePage />
</template>
Verification
After implementation, run:
cd frontend && npm run build
Must succeed with zero errors. Then verify visually:
cd frontend && npm run dev
Check:
- Sidebar renders with site title and segment nav items (populate via backend or verify empty state)
- Desktop: sidebar fixed left, content scrolls independently
- Mobile (narrow viewport): hamburger menu, slide-out nav works
- Each segment type renders correctly when data is present
- Active segment highlights in sidebar on scroll
- No TypeScript errors (
npm run type-check)
Files to Create/Modify
| Action | File |
|---|---|
| CREATE | frontend/src/types/segment.ts |
| CREATE | frontend/src/composables/useSegments.ts |
| CREATE | frontend/src/components/layout/AppShell.vue |
| CREATE | frontend/src/components/layout/SidebarNav.vue |
| CREATE | frontend/src/components/layout/MobileHeader.vue |
| CREATE | frontend/src/components/segments/SegmentRenderer.vue |
| CREATE | frontend/src/components/segments/SegmentList.vue |
| CREATE | frontend/src/components/segments/MarkdownSegment.vue |
| CREATE | frontend/src/components/segments/PdfSegment.vue |
| CREATE | frontend/src/components/segments/VideoSegment.vue |
| CREATE | frontend/src/components/segments/AudioSegment.vue |
| CREATE | frontend/src/components/segments/IframeSegment.vue |
| CREATE | frontend/src/components/segments/GallerySegment.vue |
| CREATE | frontend/src/views/HomePage.vue |
| MODIFY | frontend/src/App.vue |