- Added DashboardModule, DashboardController, and DashboardService to manage dashboard functionality. - Created DashboardOverview component to display statistics and recent activity. - Introduced API hooks for fetching dashboard data and handling state. - Updated AGENTS.md to reflect new page structure for modular organization. Signed-off-by: Innei <tukon479@gmail.com>
14 KiB
AGENTS
Commands
Development Commands
# Start development server (runs both web and SSR)
pnpm dev
# Start only web development server
pnpm --filter web dev
# Start only SSR development server
pnpm --filter @afilmory/ssr dev
# Build production version
pnpm build
# Build manifest from storage (generates photo metadata)
pnpm run build:manifest
# Force rebuild all photos and metadata
pnpm run build:manifest -- --force
# Force regenerate thumbnails only
pnpm run build:manifest -- --force-thumbnails
# Force regenerate manifest only
pnpm run build:manifest -- --force-manifest
Database Commands (SSR app)
# Generate database migrations
pnpm --filter @afilmory/ssr db:generate
# Run database migrations
pnpm --filter @afilmory/ssr db:migrate
Code Quality Commands
# Lint and fix code
pnpm lint
# Format code
pnpm format
# Type check (web app)
pnpm --filter web type-check
Architecture
Design Patterns & Application Structure
Hybrid SPA + SSR Architecture: The application uses a unique architecture where Next.js serves as both a hosting platform for the Vite-built SPA and a dynamic SEO/OG meta generator:
- Production: Next.js serves the pre-built SPA static assets and provides dynamic routes for SEO
- Development: Both servers run concurrently with the SSR app proxying to the SPA for seamless development
Key Design Patterns:
- Adapter Pattern: Builder system uses adapters for different storage providers (S3, GitHub)
- Factory Pattern: Photo processing pipeline with configurable workers and storage adapters
- Observer Pattern: Manifest changes trigger SSR meta tag updates for social sharing
- Singleton Pattern: PhotoLoader class provides global access to manifest data
Monorepo Structure
This is a pnpm workspace with multiple applications and packages:
apps/web/- Main frontend React application (Vite + React 19 SPA)apps/ssr/- Next.js 15 application serving as SPA host + dynamic SEO/OG generatorpackages/builder/- Photo processing and manifest generation tool with adapter patternpackages/webgl-viewer/- High-performance WebGL-based photo viewer componentpackages/data/- Shared data access layer and PhotoLoader singletonpackages/components/- Reusable UI components across apps
Next.js as SPA Host & SEO Provider
Dual Server Architecture:
- Development Mode:
apps/ssr/src/app/[...all]/route.tscatches all SPA routes and serves index.html with injected manifest data - Production Mode: Next.js serves pre-built Vite SPA assets while providing dynamic OG image generation
Dynamic SEO Implementation:
apps/ssr/src/index.html.ts- Pre-compiled HTML template with manifest data injected aswindow.__MANIFEST__- Dynamic OG images generated per photo via Next.js API routes (
/og/[photoId]/route.ts) - HTML meta tags dynamically replaced for social media sharing
Configuration Architecture
Two-Layer Configuration System:
-
Builder Config (
builder.config.ts) - Infrastructure/Processing Layer:- Controls photo processing, storage connections, and build performance
- Handles remote git repository sync for manifest/thumbnails
- Configures multi-process/cluster processing for large photo sets
-
Site Config (
site.config.ts+config.json) - Presentation/Content Layer:{ name: "Gallery Name", description: "...", author: { name: "...", url: "...", avatar: "..." }, social: { twitter: "...", github: "..." }, map: ["maplibre"] // Map provider configuration }- Controls site branding, author info, social links
- Merged with user
config.jsonusing es-toolkit/compat - Consumed by both SPA and SSR for consistent branding
Manifest Generation & Data Flow
Builder Pipeline (packages/builder/src/cli.ts):
- Storage Sync: Downloads photos from S3/GitHub with incremental change detection
- Format Processing: HEIC→JPEG, TIFF→web formats, Live Photo detection
- Multi-threaded Processing: Configurable worker pools or cluster mode for performance
- EXIF & Metadata Extraction: Camera settings, GPS, Fujifilm recipes, tone analysis
- Thumbnail Generation: Multiple sizes with blurhash placeholders
- Manifest Serialization: Generates
photos-manifest.jsonwith full metadata - Remote Sync: Pushes updates to git repository if configured
SPA Data Consumption (packages/data/src/index.ts):
class PhotoLoader {
constructor() {
this.photos = __MANIFEST__.data // Injected via window global
this.cameras = __MANIFEST__.cameras
this.lenses = __MANIFEST__.lenses
// Creates lookup maps and provides data access layer
}
}
Data Flow:
- Builder generates manifest →
photos-manifest.json - SSR injects manifest into HTML →
window.__MANIFEST__ - SPA PhotoLoader singleton consumes global data
- React components access photos via
photoLoader.getPhotos()
Key Technologies
- Frontend: React 19, TypeScript, Vite, Tailwind CSS, Jotai (state), TanStack Query
- Backend: Next.js 15 (SPA host + SEO), Drizzle ORM, PostgreSQL
- Image Processing: Sharp, exiftool-vendored, HEIC conversion, blurhash generation
- Storage: S3-compatible (AWS/MinIO), GitHub repository storage
- Build System: pnpm workspaces, concurrent dev servers, cluster-based processing
Development Workflow
- Concurrent Development:
pnpm devruns both SPA (Vite) and SSR (Next.js) servers - Hot Reloading: SPA changes reflect immediately, SSR provides SEO preview
- Manifest Building:
pnpm run build:manifestprocesses photos and updates data - Type Safety: Shared types between builder, SPA, and SSR ensure consistency
- Page Structure: Keep files under
pages/as thin routing shells; move reusable UI/logic intomodules/<domain>/**(e.g., dashboard overview lives inmodules/dashboard/components).
Code Quality Rules
- Avoid code duplication - extract common types and components
- Keep components focused - use hooks and component composition
- Follow React best practices - proper Context usage, state management
- Use TypeScript strictly - leverage type safety throughout
i18n Guidelines
- Use flat keys with
.separation (e.g.,exif.camera.model) - Support pluralization with
_oneand_othersuffixes - Modify English first, then other languages (ESLint auto-removes unused keys)
- CRITICAL: Avoid nested key conflicts in flat structure
- ❌ WRONG:
"action.tag.mode.and": "AND"+"action.tag.mode.and.tooltip": "..." - ✅ CORRECT:
"action.tag.mode.and": "AND"+"action.tag.tooltip.and": "..." - Rule: A key cannot be both a string value AND a parent object
- Each key must be completely independent in the flat structure
- ❌ WRONG:
Testing Strategy
- Check README.md and package.json scripts for test commands
- Verify builds work with
pnpm build - Test photo processing with
pnpm run build:manifest - Validate types with
pnpm --filter web type-check
Cursor Rules Integration
Code Quality Standards
- Avoid code duplication - extract common types and components when used multiple times
- Keep components focused - use hooks and component splitting for large logic blocks
- Master React philosophy - proper Context usage, component composition, state management to prevent re-renders
Glassmorphic Depth Design System
Follow uses a sophisticated glassmorphic depth design system for elevated UI components (modals, toasts, floating panels, etc.). This design provides visual hierarchy through layered transparency and subtle color accents.
Design Principles
- Multi-layer Depth: Create visual depth through stacked transparent layers
- Subtle Color Accents: Use brand colors at very low opacity (5-20%) for borders, glows, and backgrounds
- Refined Blur: Heavy backdrop blur (backdrop-blur-2xl) for frosted glass effect
- Minimal Shadows: Combine multiple soft shadows with accent colors for depth perception
- Smooth Animations: Use Spring presets for all transitions
Color Usage
IMPORTANT: Tailwind CSS 4 uses color-mix() by default for /opacity syntax. Always prefer Tailwind classes over inline styles.
Tailwind CSS Classes (Preferred)
- Borders:
border-accent/20instead ofborderColor: 'color-mix(...)' - Backgrounds:
bg-accent/5,bg-accent/[0.03](use bracket notation for custom percentages) - Text Colors:
text-accent/80 - Any solid color with opacity: Use Tailwind
/opacitysyntax
When to Use Inline Styles with color-mix()
Only use explicit color-mix(in srgb, var(--color-accent) X%, transparent) for:
- Gradients:
linear-gradient,radial-gradient(Tailwind doesn't support color-mix in gradients) - Box Shadows: Tailwind doesn't support color-mix in shadows yet
- Complex multi-color blending: When you need more than simple opacity
Examples
✅ CORRECT - Use Tailwind Classes:
<div className="border-accent/20 bg-accent/5">
<div className="bg-accent/[0.03]"> {/* Custom percentage */}
Content
</div>
</div>
❌ WRONG - Unnecessary inline styles:
<div
style={{
borderColor: 'color-mix(in srgb, var(--color-accent) 20%, transparent)',
background: 'color-mix(in srgb, var(--color-accent) 5%, transparent)',
}}
>
✅ CORRECT - Inline styles for gradients/shadows:
<div
className="border-accent/20"
style={{
background: 'linear-gradient(to right, color-mix(in srgb, var(--color-accent) 8%, transparent), color-mix(in srgb, var(--color-accent) 5%, transparent))',
boxShadow: '0 8px 32px color-mix(in srgb, var(--color-accent) 8%, transparent)',
}}
>
Color Usage Summary
- Borders:
border-accent/20(Tailwind class) - Solid Backgrounds:
bg-accent/5orbg-accent/[0.03](Tailwind class) - Gradient Backgrounds: Inline style with
linear-gradient+color-mix() - Shadows: Inline style with
color-mix() - Inner Glow: Inline style with gradient +
color-mix()
Component Structure
<div
className="rounded-2xl border border-accent/20 backdrop-blur-2xl"
style={{
backgroundImage:
"linear-gradient(to bottom right, color-mix(in srgb, var(--color-background) 98%, transparent), color-mix(in srgb, var(--color-background) 95%, transparent))",
boxShadow:
"0 8px 32px color-mix(in srgb, var(--color-accent) 8%, transparent), 0 4px 16px color-mix(in srgb, var(--color-accent) 6%, transparent), 0 2px 8px rgba(0, 0, 0, 0.1)",
}}
>
{/* Inner glow layer */}
<div
className="pointer-events-none absolute inset-0 rounded-2xl"
style={{
background:
"linear-gradient(to bottom right, color-mix(in srgb, var(--color-accent) 5%, transparent), transparent, color-mix(in srgb, var(--color-accent) 5%, transparent))",
}}
/>
{/* Content */}
<div className="relative">{/* Your content here */}</div>
</div>
Interactive Elements
IMPORTANT: Prefer CSS-driven hover effects over JavaScript event handlers for better performance and cleaner code.
Radix UI Menu Items (using data-highlighted)
For Radix UI components that support data-highlighted attribute:
<DropdownMenuItem
className="rounded-lg transition-all duration-200 data-[highlighted]:text-accent"
style={{
// Use CSS custom properties for dynamic background on highlight
['--highlight-bg' as any]: 'linear-gradient(to right, color-mix(in srgb, var(--color-accent) 8%, transparent), color-mix(in srgb, var(--color-accent) 5%, transparent))'
}}
onSelect={() => {}}
>
Menu Item
</DropdownMenuItem>
// Add this CSS for highlighted state (in tailwind.css):
// @layer components {
// [data-highlighted] { background: var(--highlight-bg); }
// }
Custom Buttons (using CSS classes)
✅ PREFERRED - CSS-driven hover:
<button className="glassmorphic-btn border-accent/20 text-text-secondary ...">
Button Text
</button>
// In tailwind.css:
// @layer components {
// .glassmorphic-btn:hover {
// background: linear-gradient(to right,
// color-mix(in srgb, var(--color-accent) 8%, transparent),
// color-mix(in srgb, var(--color-accent) 5%, transparent)
// ) !important;
// color: var(--color-accent) !important;
// }
// }
❌ AVOID - JavaScript event handlers (use only when CSS cannot achieve the effect):
<button
onMouseEnter={(e) => {
e.currentTarget.style.background = "..."
e.currentTarget.style.color = "var(--color-accent)"
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = "transparent"
e.currentTarget.style.color = ""
}}
>
Button Text
</button>
Dividers
Use gradient dividers within glass containers:
<div
className="mx-4 h-px"
style={{
background: "linear-gradient(to right, transparent, color-mix(in srgb, var(--color-accent) 20%, transparent), transparent)",
}}
/>
Animation Guidelines
- Entry animations:
initial={{ y: 8, opacity: 0 }}→animate={{ y: 0, opacity: 1 }} - Use
Spring.presets.snappyfor quick interactions - Use
Spring.presets.smoothfor larger movements - Keep scale animations subtle (1.0 ↔ 1.02)
When to Use
Apply this design system to:
- Toast notifications
- Modal dialogs
- Floating panels and popovers
- Ambient UI prompts
- Contextual menus
- Elevated cards with actions
Design Guidelines
UI/UX Guidelines
- Follow Glassmorphic Depth Design System
- Use Apple UIKit color system via tailwind-uikit-colors package
- Prefer semantic color names:
text-primary,fill-secondary,material-thin, etc. - Follow system colors:
red,blue,green,mint,teal,cyan,indigo,purple,pink,brown,gray - Use material design principles with opacity-based fills and proper contrast
i18n Development Rules
- Use flat keys with dot notation:
exif.camera.model - Support pluralization:
_oneand_othersuffixes - Always modify English (
en.json) first, then other languages - Avoid key conflicts in flat structure (e.g.,
exif.custom.renderedvsexif.custom.rendered.custom) - ESLint automatically removes unused keys from non-English files
Important Notes
- This is a photo gallery application that processes and displays photos from cloud storage
- The builder tool handles complex image processing workflows
- WebGL viewer provides high-performance photo viewing experience
- Map integration shows photo locations from GPS EXIF data
- Live Photo support for iOS/Apple device videos