diff --git a/prd/PRD-cms-functionality.md b/prd/PRD-cms-functionality.md
index 489739f..6e35db9 100644
--- a/prd/PRD-cms-functionality.md
+++ b/prd/PRD-cms-functionality.md
@@ -180,6 +180,7 @@ The system now uses a dedicated `media_usage` table for robust tracking:
```
**Benefits:**
+
- Accurate usage tracking across all content types
- Efficient queries for usage information
- Safe bulk deletion with automatic reference cleanup
@@ -287,39 +288,46 @@ const ImageBlock = {
```typescript
// Projects
-GET /api/projects
-POST /api/projects
-GET /api/projects/[slug]
-PUT /api/projects/[id]
-DELETE /api/projects/[id]
+GET / api / projects
+POST / api / projects
+GET / api / projects / [slug]
+PUT / api / projects / [id]
+DELETE / api / projects / [id]
// Posts
-GET /api/posts
-POST /api/posts
-GET /api/posts/[slug]
-PUT /api/posts/[id]
-DELETE /api/posts/[id]
+GET / api / posts
+POST / api / posts
+GET / api / posts / [slug]
+PUT / api / posts / [id]
+DELETE / api / posts / [id]
// Albums & Photos
-GET /api/albums
-POST /api/albums
-GET /api/albums/[slug]
-PUT /api/albums/[id]
-DELETE /api/albums/[id]
-POST /api/albums/[id]/photos
-DELETE /api/photos/[id]
-PUT /api/photos/[id]/order
+GET / api / albums
+POST / api / albums
+GET / api / albums / [slug]
+PUT / api / albums / [id]
+DELETE / api / albums / [id]
+POST / api / albums / [id] / photos
+DELETE / api / photos / [id]
+PUT / api / photos / [id] / order
// Media Management
-POST /api/media/upload // Single file upload
-POST /api/media/bulk-upload // Multiple file upload
-GET /api/media // Browse with filters, pagination
-GET /api/media/[id] // Get single media item
-PUT /api/media/[id] // Update media (alt text, description)
-DELETE /api/media/[id] // Delete single media item
-DELETE /api/media/bulk-delete // Delete multiple media items
-GET /api/media/[id]/usage // Check where media is used
-POST /api/media/backfill-usage // Backfill usage tracking for existing content
+POST / api / media / upload // Single file upload
+POST / api / media / bulk - upload // Multiple file upload
+GET / api / media // Browse with filters, pagination
+GET / api / media / [id] // Get single media item
+PUT / api / media / [id] // Update media (alt text, description)
+DELETE / api / media / [id] // Delete single media item
+DELETE / api / media / bulk -
+ delete (
+ // Delete multiple media items
+ GET
+ ) /
+ api /
+ media /
+ [id] /
+ usage // Check where media is used
+POST / api / media / backfill - usage // Backfill usage tracking for existing content
```
### 8. Media Management & Cleanup
@@ -580,7 +588,7 @@ Based on requirements discussion:
- Add photography toggle to media details modal
- Add photography indicator pills in admin interface
-2. **Albums & Photos Management Interface**
+2. **Albums & Photos Management Interface**
- Album creation and management UI with photography toggle
- Bulk photo upload interface with progress
@@ -682,7 +690,7 @@ Based on requirements discussion:
### Phase 6: Content Simplification & Photo Curation
- [x] Add `isPhotography` field to Media table (migration)
-- [x] Add `isPhotography` field to Album table (migration)
+- [x] Add `isPhotography` field to Album table (migration)
- [x] Simplify post types to "post" and "essay" only
- [x] Update UniverseComposer to use simplified post types
- [x] Add photography toggle to MediaDetailsModal
@@ -712,7 +720,7 @@ Based on requirements discussion:
- [x] Replace static Work page with dynamic data
- [x] Update project detail pages
-- [x] Build Universe mixed feed component
+- [x] Build Universe mixed feed component
- [x] Create different card types for each post type
- [x] Update Photos page with dynamic albums/photos
- [x] Implement individual photo pages
@@ -755,10 +763,12 @@ Based on requirements discussion:
### Design Decisions Made (May 2024)
1. **Simplified Post Types**: Reduced from 5 types (blog, microblog, link, photo, album) to 2 types:
+
- **Post**: Simple content with optional attachments (handles previous microblog, link, photo use cases)
- **Essay**: Full editor with title/metadata + attachments (handles previous blog use cases)
2. **Photo Curation Strategy**: Dual-level curation system:
+
- **Media Level**: `isPhotography` boolean - stars individual media for photo experience
- **Album Level**: `isPhotography` boolean - marks entire albums for photo experience
- **Mixed Content**: Photography albums can contain non-photography media (Option A)
@@ -774,18 +784,21 @@ Based on requirements discussion:
### Implementation Task List
#### Phase 1: Database Updates
+
- [x] Create migration to add `isPhotography` field to Media table
- [x] Create migration to add `isPhotography` field to Album table
- [x] Update Prisma schema with new fields
- [x] Test migrations on local database
#### Phase 2: API Updates
+
- [x] Update Media API endpoints to handle `isPhotography` flag
- [x] Update Album API endpoints to handle `isPhotography` flag
- [x] Update media usage tracking to work with new flags
- [x] Add filtering capabilities for photography content
#### Phase 3: Admin Interface Updates
+
- [x] Add photography toggle to MediaDetailsModal
- [x] Add photography indicator pills for media items (grid and list views)
- [x] Add photography indicator pills for albums
@@ -793,6 +806,7 @@ Based on requirements discussion:
- [x] Add bulk photography operations (mark/unmark multiple items)
#### Phase 4: Post Type Simplification
+
- [x] Update UniverseComposer to use only "post" and "essay" types
- [x] Remove complex post type selector UI
- [x] Update post creation flows
@@ -800,6 +814,7 @@ Based on requirements discussion:
- [x] Update post display logic to handle simplified types
#### Phase 5: Album Management System
+
- [x] Create album creation/editing interface with photography toggle
- [x] Build album list view with photography indicators
- [ ] **Critical Missing Feature: Album Photo Management**
@@ -814,6 +829,7 @@ Based on requirements discussion:
- [ ] Add bulk photo upload to albums with automatic photography detection
#### Phase 6: Photography Experience
+
- [ ] Build photography album filtering in admin
- [ ] Create photography-focused views and workflows
- [ ] Add batch operations for photo curation
diff --git a/prd/PRD-enhanced-tag-system.md b/prd/PRD-enhanced-tag-system.md
index fe7cb95..028d4dd 100644
--- a/prd/PRD-enhanced-tag-system.md
+++ b/prd/PRD-enhanced-tag-system.md
@@ -24,12 +24,14 @@ Upgrade the current JSON-based tag system to a relational database model with ad
## Current State vs Target State
### Current Implementation
+
- Tags stored as JSON arrays: `tags: ['announcement', 'meta', 'cms']`
- Simple display-only functionality
- No querying capabilities
- Manual tag input with Add button
### Target Implementation
+
- Relational many-to-many tag system
- Full CRUD operations for tags
- Advanced filtering and search
@@ -82,10 +84,10 @@ model Tag {
color String? @db.VarChar(7) // Hex color
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
-
+
// Relations
posts PostTag[]
-
+
@@index([name])
@@index([slug])
}
@@ -95,11 +97,11 @@ model PostTag {
postId Int
tagId Int
createdAt DateTime @default(now())
-
+
// Relations
post Post @relation(fields: [postId], references: [id], onDelete: Cascade)
tag Tag @relation(fields: [tagId], references: [id], onDelete: Cascade)
-
+
@@unique([postId, tagId])
@@index([postId])
@@index([tagId])
@@ -117,7 +119,9 @@ model Post {
### 1. Tag Management Interface
#### Admin Tag Manager (`/admin/tags`)
+
- **Tag List View**
+
- DataTable with tag name, usage count, created date
- Search and filter capabilities
- Bulk operations (delete, merge, rename)
@@ -130,7 +134,9 @@ model Post {
- Merge with other tags functionality
#### Tag Analytics Dashboard
+
- **Usage Statistics**
+
- Most/least used tags
- Tag usage trends over time
- Orphaned tags (no posts)
@@ -144,6 +150,7 @@ model Post {
### 2. Enhanced Tag Input Component (`TagInput.svelte`)
#### Features
+
- **Typeahead Search**: Real-time search of existing tags
- **Keyboard Navigation**: Arrow keys to navigate suggestions
- **Instant Add**: Press Enter to add tag without button click
@@ -152,137 +159,150 @@ model Post {
- **Quick Actions**: Backspace to remove last tag
#### Component API
+
```typescript
interface TagInputProps {
- tags: string[] | Tag[] // Current tags
- suggestions?: Tag[] // Available tags for typeahead
- placeholder?: string // Input placeholder text
- maxTags?: number // Maximum number of tags
- allowNew?: boolean // Allow creating new tags
- size?: 'small' | 'medium' | 'large'
- disabled?: boolean
- onTagAdd?: (tag: Tag) => void
- onTagRemove?: (tag: Tag) => void
- onTagCreate?: (name: string) => void
+ tags: string[] | Tag[] // Current tags
+ suggestions?: Tag[] // Available tags for typeahead
+ placeholder?: string // Input placeholder text
+ maxTags?: number // Maximum number of tags
+ allowNew?: boolean // Allow creating new tags
+ size?: 'small' | 'medium' | 'large'
+ disabled?: boolean
+ onTagAdd?: (tag: Tag) => void
+ onTagRemove?: (tag: Tag) => void
+ onTagCreate?: (name: string) => void
}
```
#### Svelte 5 Implementation
+
```svelte
```
### 3. Post Filtering by Tags
#### Frontend Components
+
- **Tag Filter Bar**: Multi-select tag filtering
- **Tag Cloud**: Visual tag representation with usage counts
- **Search Integration**: Combine text search with tag filters
#### API Endpoints
+
```typescript
// GET /api/posts?tags=javascript,react&operation=AND
// GET /api/posts?tags=design,ux&operation=OR
interface PostsQueryParams {
- tags?: string[] // Tag names or IDs
- operation?: 'AND' | 'OR' // How to combine multiple tags
- page?: number
- limit?: number
- status?: 'published' | 'draft'
+ tags?: string[] // Tag names or IDs
+ operation?: 'AND' | 'OR' // How to combine multiple tags
+ page?: number
+ limit?: number
+ status?: 'published' | 'draft'
}
// GET /api/tags/suggest?q=java
interface TagSuggestResponse {
- tags: Array<{
- id: number
- name: string
- slug: string
- usageCount: number
- }>
+ tags: Array<{
+ id: number
+ name: string
+ slug: string
+ usageCount: number
+ }>
}
```
### 4. Related Posts Feature
#### Implementation
+
- **Algorithm**: Find posts sharing the most tags
- **Weighting**: Consider tag importance and recency
- **Exclusions**: Don't show current post in related list
- **Limit**: Show 3-6 related posts maximum
#### Component (`RelatedPosts.svelte`)
+
```svelte
```
@@ -293,24 +313,24 @@ interface TagSuggestResponse {
```typescript
// GET /api/tags - List all tags
interface TagsResponse {
- tags: Tag[]
- total: number
- page: number
- limit: number
+ tags: Tag[]
+ total: number
+ page: number
+ limit: number
}
// POST /api/tags - Create new tag
interface CreateTagRequest {
- name: string
- description?: string
- color?: string
+ name: string
+ description?: string
+ color?: string
}
// PUT /api/tags/[id] - Update tag
interface UpdateTagRequest {
- name?: string
- description?: string
- color?: string
+ name?: string
+ description?: string
+ color?: string
}
// DELETE /api/tags/[id] - Delete tag
@@ -318,23 +338,23 @@ interface UpdateTagRequest {
// POST /api/tags/merge - Merge tags
interface MergeTagsRequest {
- sourceTagIds: number[]
- targetTagId: number
+ sourceTagIds: number[]
+ targetTagId: number
}
// GET /api/tags/[id]/posts - Get posts for tag
interface TagPostsResponse {
- posts: Post[]
- tag: Tag
- total: number
+ posts: Post[]
+ tag: Tag
+ total: number
}
// GET /api/tags/analytics - Tag usage analytics
interface TagAnalyticsResponse {
- mostUsed: Array<{ tag: Tag; count: number }>
- leastUsed: Array<{ tag: Tag; count: number }>
- trending: Array<{ tag: Tag; growth: number }>
- orphaned: Tag[]
+ mostUsed: Array<{ tag: Tag; count: number }>
+ leastUsed: Array<{ tag: Tag; count: number }>
+ trending: Array<{ tag: Tag; growth: number }>
+ orphaned: Tag[]
}
```
@@ -343,20 +363,20 @@ interface TagAnalyticsResponse {
```typescript
// GET /api/posts/related?postId=123&tagIds=1,2,3&limit=4
interface RelatedPostsResponse {
- posts: Array<{
- id: number
- title: string
- slug: string
- excerpt?: string
- publishedAt: string
- tags: Tag[]
- sharedTagsCount: number // Number of tags in common
- }>
+ posts: Array<{
+ id: number
+ title: string
+ slug: string
+ excerpt?: string
+ publishedAt: string
+ tags: Tag[]
+ sharedTagsCount: number // Number of tags in common
+ }>
}
// PUT /api/posts/[id]/tags - Update post tags
interface UpdatePostTagsRequest {
- tagIds: number[]
+ tagIds: number[]
}
```
@@ -365,6 +385,7 @@ interface UpdatePostTagsRequest {
### 1. TagInput Component Features
#### Visual States
+
- **Default**: Clean input with placeholder
- **Focused**: Show suggestions dropdown
- **Typing**: Filter and highlight matches
@@ -373,6 +394,7 @@ interface UpdatePostTagsRequest {
- **Full**: Disable input when max tags reached
#### Accessibility
+
- **ARIA Labels**: Proper labeling for screen readers
- **Keyboard Navigation**: Full keyboard accessibility
- **Focus Management**: Logical tab order
@@ -381,61 +403,59 @@ interface UpdatePostTagsRequest {
### 2. Tag Display Components
#### TagPill Component
+
```svelte
-
- {tag.name}
- {#if showCount}
- ({tag.usageCount})
- {/if}
- {#if removable}
- ×
- {/if}
+ {tag.name}
+ {#if showCount}
+ ({tag.usageCount})
+ {/if}
+ {#if removable}
+ ×
+ {/if}
```
#### TagCloud Component
+
```svelte
```
### 3. Admin Interface Updates
#### Posts List with Tag Filtering
+
- **Filter Bar**: Multi-select tag filter above posts list
- **Tag Pills**: Show tags on each post item
- **Quick Filter**: Click tag to filter by that tag
- **Clear Filters**: Easy way to reset all filters
#### Posts Edit Form Integration
+
- **Replace Current**: Swap existing tag input with new TagInput
- **Preserve UX**: Maintain current metadata popover
- **Tag Management**: Quick access to create/edit tags
@@ -443,12 +463,15 @@ interface UpdatePostTagsRequest {
## Migration Strategy
### Phase 1: Database Migration (Week 1)
+
1. **Create Migration Script**
+
- Create new tables (tags, post_tags)
- Migrate existing JSON tags to relational format
- Create indexes for performance
2. **Data Migration**
+
- Extract unique tags from existing posts
- Create tag records with auto-generated slugs
- Create post_tag relationships
@@ -459,12 +482,15 @@ interface UpdatePostTagsRequest {
- Dual-write to both systems during transition
### Phase 2: API Development (Week 1-2)
+
1. **Tag Management APIs**
+
- CRUD operations for tags
- Tag suggestions and search
- Analytics endpoints
2. **Enhanced Post APIs**
+
- Update post endpoints for relational tags
- Related posts algorithm
- Tag filtering capabilities
@@ -475,12 +501,15 @@ interface UpdatePostTagsRequest {
- Data consistency checks
### Phase 3: Frontend Components (Week 2-3)
+
1. **Core Components**
+
- TagInput with typeahead
- TagPill and TagCloud
- Tag management interface
2. **Integration**
+
- Update MetadataPopover
- Add tag filtering to posts list
- Implement related posts component
@@ -491,12 +520,15 @@ interface UpdatePostTagsRequest {
- Bulk operations interface
### Phase 4: Features & Polish (Week 3-4)
+
1. **Advanced Features**
+
- Tag merging functionality
- Usage analytics
- Tag suggestions based on content
2. **Performance Optimization**
+
- Query optimization
- Caching strategies
- Load testing
@@ -509,21 +541,25 @@ interface UpdatePostTagsRequest {
## Success Metrics
### Performance
+
- Tag search responses under 50ms
- Post filtering responses under 100ms
- Page load times maintained or improved
### Usability
+
- Reduced clicks to add tags (eliminate Add button)
- Faster tag input with typeahead
- Improved content discovery through related posts
### Content Management
+
- Ability to merge duplicate tags
- Insights into tag usage patterns
- Better content organization capabilities
### Analytics
+
- Track tag usage growth over time
- Identify content gaps through tag analysis
- Measure impact on content engagement
@@ -531,18 +567,21 @@ interface UpdatePostTagsRequest {
## Technical Considerations
### Performance
+
- **Database Indexes**: Proper indexing on tag names and relationships
- **Query Optimization**: Efficient joins for tag filtering
- **Caching**: Cache popular tag lists and related posts
- **Pagination**: Handle large tag lists efficiently
### Data Integrity
+
- **Constraints**: Prevent duplicate tag names
- **Cascading Deletes**: Properly handle tag/post deletions
- **Validation**: Ensure tag names follow naming conventions
- **Backup Strategy**: Safe migration with rollback capability
### User Experience
+
- **Progressive Enhancement**: Graceful degradation if JS fails
- **Loading States**: Smooth loading indicators
- **Error Handling**: Clear error messages for users
@@ -551,12 +590,14 @@ interface UpdatePostTagsRequest {
## Future Enhancements
### Advanced Features (Post-MVP)
+
- **Hierarchical Tags**: Parent/child tag relationships
- **Tag Synonyms**: Alternative names for the same concept
- **Auto-tagging**: ML-based tag suggestions from content
- **Tag Templates**: Predefined tag sets for different content types
### Integrations
+
- **External APIs**: Import tags from external sources
- **Search Integration**: Enhanced search with tag faceting
- **Analytics**: Deep tag performance analytics
@@ -565,11 +606,13 @@ interface UpdatePostTagsRequest {
## Risk Assessment
### High Risk
+
- **Data Migration**: Complex migration of existing tag data
- **Performance Impact**: New queries might affect page load times
- **User Adoption**: Users need to learn new tag input interface
### Mitigation Strategies
+
- **Staged Rollout**: Deploy to staging first, then gradual production rollout
- **Performance Monitoring**: Continuous monitoring during migration
- **User Training**: Clear documentation and smooth UX transitions
@@ -578,6 +621,7 @@ interface UpdatePostTagsRequest {
## Success Criteria
### Must Have
+
- ✅ All existing tags migrated successfully
- ✅ Tag input works with keyboard-only navigation
- ✅ Posts can be filtered by single or multiple tags
@@ -585,12 +629,14 @@ interface UpdatePostTagsRequest {
- ✅ Performance remains acceptable (< 100ms for most operations)
### Should Have
+
- ✅ Tag management interface for admins
- ✅ Tag usage analytics and insights
- ✅ Ability to merge duplicate tags
- ✅ Tag color coding and visual improvements
### Could Have
+
- Tag auto-suggestions based on post content
- Tag trending and popularity metrics
- Advanced tag analytics and reporting
@@ -607,4 +653,4 @@ interface UpdatePostTagsRequest {
## Conclusion
-This enhanced tag system will significantly improve content organization, discoverability, and management capabilities while providing a modern, intuitive user interface built with Svelte 5 runes. The migration strategy ensures minimal disruption while delivering substantial improvements in functionality and user experience.
\ No newline at end of file
+This enhanced tag system will significantly improve content organization, discoverability, and management capabilities while providing a modern, intuitive user interface built with Svelte 5 runes. The migration strategy ensures minimal disruption while delivering substantial improvements in functionality and user experience.
diff --git a/prd/PRD-interactive-project-headers.md b/prd/PRD-interactive-project-headers.md
index 09e9e4e..268dd36 100644
--- a/prd/PRD-interactive-project-headers.md
+++ b/prd/PRD-interactive-project-headers.md
@@ -1,20 +1,24 @@
# PRD: Interactive Project Headers
## Overview
+
Implement a system for project-specific interactive headers that can be selected per project through the admin UI. Each project can have a unique, animated header component or use a generic default.
## Goals
+
- Create engaging, project-specific header experiences
- Maintain simplicity in implementation and admin UI
- Allow for creative freedom while keeping the system maintainable
- Provide a path for adding new header types over time
## Implementation Strategy
+
We will use a component-based system where each project can select from a predefined list of header components. Each header component is a fully custom Svelte component that receives the project data as props.
## Technical Implementation
### 1. Database Schema Update
+
Add a `headerType` field to the Project model in `prisma/schema.prisma`:
```prisma
@@ -25,6 +29,7 @@ model Project {
```
### 2. Component Structure
+
Create a new directory structure for header components:
```
@@ -37,66 +42,72 @@ src/lib/components/headers/
```
### 3. ProjectHeader Component (Switcher)
+
The main component that switches between different header types:
```svelte
{#if project.headerType !== 'none'}
-
+
{/if}
```
### 4. Update Project Detail Page
+
Modify `/routes/work/[slug]/+page.svelte` to use the new header system instead of the current static header.
### 5. Admin UI Integration
+
Add a select field to the project form components:
```svelte
formData.headerType = e.target.value}
+ label="Header Type"
+ name="headerType"
+ value={formData.headerType}
+ onchange={(e) => (formData.headerType = e.target.value)}
>
- No Header
- Logo on Background (Default)
- Pinterest Header
- Maitsu Header
+ No Header
+ Logo on Background (Default)
+ Pinterest Header
+ Maitsu Header
```
## Header Type Specifications
### LogoOnBackgroundHeader (Default)
+
- Current behavior: centered logo with title and subtitle
- Uses project's `logoUrl`, `backgroundColor`, and text
- Simple, clean presentation
### PinterestHeader
+
- Interactive grid of Pinterest-style cards
- Cards rearrange/animate on hover
- Could pull from project gallery or use custom assets
- Red color scheme matching Pinterest brand
### MaitsuHeader
+
- Japanese-inspired animations
- Could feature:
- Animated kanji/hiragana characters
@@ -105,11 +116,14 @@ Add a select field to the project form components:
- Uses project colors for theming
### None
+
- No header displayed
- Project content starts immediately
## Data Available to Headers
+
Each header component receives the full project object with access to:
+
- `project.logoUrl` - Project logo
- `project.backgroundColor` - Primary background color
- `project.highlightColor` - Accent color
@@ -121,12 +135,14 @@ Each header component receives the full project object with access to:
## Future Considerations
### Potential Additional Header Types
+
- **SlackHeader**: Animated emoji reactions floating up
- **FigmaHeader**: Interactive design tools/cursors
- **TypegraphicaHeader**: Kinetic typography animations
- **Custom**: Allow arbitrary component code (requires security considerations)
### Possible Enhancements
+
1. **Configuration Options**: Add a `headerConfig` JSON field for component-specific settings
2. **Asset Management**: Dedicated header assets separate from project gallery
3. **Responsive Behaviors**: Different animations for mobile vs desktop
@@ -134,6 +150,7 @@ Each header component receives the full project object with access to:
5. **A/B Testing**: Support multiple headers per project for testing
## Implementation Steps
+
1. Add `headerType` field to Prisma schema
2. Create database migration
3. Create base `ProjectHeader` switcher component
@@ -145,6 +162,7 @@ Each header component receives the full project object with access to:
9. Document how to add new header types
## Success Criteria
+
- Projects can select from multiple header types via admin UI
- Each header type provides a unique, engaging experience
- System is extensible for adding new headers
@@ -153,8 +171,9 @@ Each header component receives the full project object with access to:
- Clean separation between header components
## Technical Notes
+
- Use Svelte 5 runes syntax (`$props()`, `$state()`, etc.)
- Leverage existing animation patterns (spring physics, CSS transitions)
- Follow established SCSS variable system
- Ensure headers are responsive
-- Consider accessibility (prefers-reduced-motion)
\ No newline at end of file
+- Consider accessibility (prefers-reduced-motion)
diff --git a/prd/PRD-media-library.md b/prd/PRD-media-library.md
index 69d9f57..46e3a3f 100644
--- a/prd/PRD-media-library.md
+++ b/prd/PRD-media-library.md
@@ -1,10 +1,11 @@
# Product Requirements Document: Media Library Modal System
-## 🎉 **PROJECT STATUS: CORE IMPLEMENTATION COMPLETE!**
+## 🎉 **PROJECT STATUS: CORE IMPLEMENTATION COMPLETE!**
We have successfully implemented a comprehensive Media Library system with both direct upload workflows and library browsing capabilities. **All major components are functional and integrated throughout the admin interface.**
### 🏆 Major Achievements
+
- **✅ Complete MediaLibraryModal system** with single/multiple selection
- **✅ Enhanced upload components** (ImageUploader, GalleryUploader) with MediaLibraryModal integration
- **✅ Full form integration** across projects, posts, albums, and editor
@@ -27,18 +28,21 @@ Implement a comprehensive Media Library modal system that provides a unified int
## Goals
### Primary Goals (Direct Upload Workflow)
+
- **Enable direct file upload within forms** where content will be used (projects, posts, albums)
- **Provide immediate upload and preview** without requiring navigation to separate media management
- **Store comprehensive metadata** including alt text for accessibility and SEO
- **Support drag-and-drop and click-to-browse** for intuitive file selection
### Secondary Goals (Media Library Browser)
+
- Create a reusable media browser for **selecting previously uploaded content**
- Provide **media management interface** showing where files are referenced
- Enable **bulk operations** and **metadata editing** (especially alt text)
- Support **file organization** and **usage tracking**
### Technical Goals
+
- Maintain consistent UX across all media interactions
- Support different file type filtering based on context
- Integrate seamlessly with existing admin components
@@ -46,6 +50,7 @@ Implement a comprehensive Media Library modal system that provides a unified int
## Current State Analysis
### ✅ What We Have
+
- Complete media API (`/api/media`, `/api/media/upload`, `/api/media/bulk-upload`)
- Media management page with grid/list views and search/filtering
- Modal base component (`Modal.svelte`)
@@ -62,17 +67,20 @@ Implement a comprehensive Media Library modal system that provides a unified int
### 🎯 What We Need
#### High Priority (Remaining Tasks)
+
- **Enhanced upload features** with drag & drop zones in all upload components
- **Bulk alt text editing** in Media Library for existing content
- **Usage tracking display** showing where media is referenced
- **Performance optimizations** for large media libraries
#### Medium Priority (Polish & Advanced Features)
+
- **Image optimization options** during upload
- **Advanced search capabilities** (by alt text, usage, etc.)
- **Bulk operations** (delete multiple, bulk metadata editing)
#### Low Priority (Future Enhancements)
+
- **AI-powered alt text suggestions**
- **Duplicate detection** and management
- **Advanced analytics** and usage reporting
@@ -80,6 +88,7 @@ Implement a comprehensive Media Library modal system that provides a unified int
## Workflow Priorities
### 🥇 Primary Workflow: Direct Upload in Forms
+
This is the **main workflow** that users will use 90% of the time:
1. **User creates content** (project, post, album)
@@ -89,11 +98,13 @@ This is the **main workflow** that users will use 90% of the time:
5. **Content is saved** with proper media references
**Key Components**:
+
- `ImageUploader` - Direct drag-and-drop/click upload with preview
- `GalleryUploader` - Multiple file upload with immediate gallery preview
- `MediaMetadataForm` - Alt text and description capture during upload
### 🥈 Secondary Workflow: Browse Existing Media
+
This workflow is for **reusing previously uploaded content**:
1. **User needs to select existing media** (rare case)
@@ -103,6 +114,7 @@ This workflow is for **reusing previously uploaded content**:
5. **Media references are updated**
**Key Components**:
+
- `MediaLibraryModal` - Browse and select existing media
- `MediaSelector` - Grid interface for selection
- `MediaManager` - Edit alt text and view usage
@@ -112,22 +124,24 @@ This workflow is for **reusing previously uploaded content**:
### 1. Enhanced Upload Components (Primary)
#### ImageUploader Component
+
**Purpose**: Direct image upload with immediate preview and metadata capture
```typescript
interface ImageUploaderProps {
- label: string
- value?: Media | null
- onUpload: (media: Media) => void
- aspectRatio?: string
- required?: boolean
- error?: string
- allowAltText?: boolean // Enable alt text input
- maxFileSize?: number // MB limit
+ label: string
+ value?: Media | null
+ onUpload: (media: Media) => void
+ aspectRatio?: string
+ required?: boolean
+ error?: string
+ allowAltText?: boolean // Enable alt text input
+ maxFileSize?: number // MB limit
}
```
**Features**:
+
- Drag-and-drop upload zone with visual feedback
- Click to browse files from computer
- Immediate image preview with proper aspect ratio
@@ -137,22 +151,24 @@ interface ImageUploaderProps {
- Replace/remove functionality
#### GalleryUploader Component
+
**Purpose**: Multiple file upload with gallery preview and reordering
```typescript
interface GalleryUploaderProps {
- label: string
- value?: Media[]
- onUpload: (media: Media[]) => void
- onReorder?: (media: Media[]) => void
- maxItems?: number
- allowAltText?: boolean
- required?: boolean
- error?: string
+ label: string
+ value?: Media[]
+ onUpload: (media: Media[]) => void
+ onReorder?: (media: Media[]) => void
+ maxItems?: number
+ allowAltText?: boolean
+ required?: boolean
+ error?: string
}
```
**Features**:
+
- Multiple file drag-and-drop
- Immediate gallery preview grid
- Individual alt text inputs for each image
@@ -165,20 +181,22 @@ interface GalleryUploaderProps {
**Purpose**: Main modal component that wraps the media browser functionality
**Props Interface**:
+
```typescript
interface MediaLibraryModalProps {
- isOpen: boolean
- mode: 'single' | 'multiple'
- fileType?: 'image' | 'video' | 'all'
- onSelect: (media: Media | Media[]) => void
- onClose: () => void
- selectedIds?: number[] // Pre-selected items
- title?: string // Modal title
- confirmText?: string // Confirm button text
+ isOpen: boolean
+ mode: 'single' | 'multiple'
+ fileType?: 'image' | 'video' | 'all'
+ onSelect: (media: Media | Media[]) => void
+ onClose: () => void
+ selectedIds?: number[] // Pre-selected items
+ title?: string // Modal title
+ confirmText?: string // Confirm button text
}
```
**Features**:
+
- Modal overlay with proper focus management
- Header with title and close button
- Media browser grid with selection indicators
@@ -192,6 +210,7 @@ interface MediaLibraryModalProps {
**Purpose**: The actual media browsing interface within the modal
**Features**:
+
- Grid layout with thumbnail previews
- Individual item selection with visual feedback
- Keyboard navigation support
@@ -199,6 +218,7 @@ interface MediaLibraryModalProps {
- "Select All" / "Clear Selection" bulk actions (for multiple mode)
**Item Display**:
+
- Thumbnail image
- Filename (truncated)
- File size and dimensions
@@ -210,6 +230,7 @@ interface MediaLibraryModalProps {
**Purpose**: Handle file uploads within the modal
**Features**:
+
- Drag-and-drop upload zone
- Click to browse files
- Upload progress indicators
@@ -218,6 +239,7 @@ interface MediaLibraryModalProps {
- Automatic refresh of media grid after upload
**Validation**:
+
- File type restrictions based on context
- File size limits (10MB per file)
- Maximum number of files for bulk upload
@@ -225,22 +247,24 @@ interface MediaLibraryModalProps {
### 4. Form Integration Components
#### MediaInput Component
+
**Purpose**: Generic input field that opens media library modal
```typescript
interface MediaInputProps {
- label: string
- value?: Media | Media[] | null
- mode: 'single' | 'multiple'
- fileType?: 'image' | 'video' | 'all'
- onSelect: (media: Media | Media[] | null) => void
- placeholder?: string
- required?: boolean
- error?: string
+ label: string
+ value?: Media | Media[] | null
+ mode: 'single' | 'multiple'
+ fileType?: 'image' | 'video' | 'all'
+ onSelect: (media: Media | Media[] | null) => void
+ placeholder?: string
+ required?: boolean
+ error?: string
}
```
**Display**:
+
- Label and optional required indicator
- Preview of selected media (thumbnail + filename)
- "Browse" button to open modal
@@ -248,42 +272,46 @@ interface MediaInputProps {
- Error state display
#### ImagePicker Component
+
**Purpose**: Specialized single image selector with enhanced preview
```typescript
interface ImagePickerProps {
- label: string
- value?: Media | null
- onSelect: (media: Media | null) => void
- aspectRatio?: string // e.g., "16:9", "1:1"
- placeholder?: string
- required?: boolean
- error?: string
+ label: string
+ value?: Media | null
+ onSelect: (media: Media | null) => void
+ aspectRatio?: string // e.g., "16:9", "1:1"
+ placeholder?: string
+ required?: boolean
+ error?: string
}
```
**Display**:
+
- Large preview area with placeholder
- Image preview with proper aspect ratio
- Overlay with "Change" and "Remove" buttons on hover
- Upload progress indicator
#### GalleryManager Component
+
**Purpose**: Multiple image selection with drag-and-drop reordering
```typescript
interface GalleryManagerProps {
- label: string
- value?: Media[]
- onSelect: (media: Media[]) => void
- onReorder?: (media: Media[]) => void
- maxItems?: number
- required?: boolean
- error?: string
+ label: string
+ value?: Media[]
+ onSelect: (media: Media[]) => void
+ onReorder?: (media: Media[]) => void
+ maxItems?: number
+ required?: boolean
+ error?: string
}
```
**Display**:
+
- Grid of selected images with reorder handles
- "Add Images" button to open modal
- Individual remove buttons on each image
@@ -294,6 +322,7 @@ interface GalleryManagerProps {
### 🥇 Primary Flow: Direct Upload in Forms
#### 1. Single Image Upload (Project Featured Image)
+
1. **User creates/edits project** and reaches featured image field
2. **User drags image file** directly onto ImageUploader component OR clicks to browse
3. **File is immediately uploaded** with progress indicator
@@ -303,6 +332,7 @@ interface GalleryManagerProps {
7. **Form can be saved** with media reference and metadata
#### 2. Multiple Image Upload (Project Gallery)
+
1. **User reaches gallery section** of project form
2. **User drags multiple files** onto GalleryUploader OR clicks to browse multiple
3. **Upload progress shown** for each file individually
@@ -313,6 +343,7 @@ interface GalleryManagerProps {
8. **Form saves** with complete gallery and metadata
#### 3. Media Management and Alt Text Editing
+
1. **User visits Media Library page** to manage uploaded content
2. **User clicks on any media item** to open details modal
3. **User can edit alt text** and other metadata
@@ -322,6 +353,7 @@ interface GalleryManagerProps {
### 🥈 Secondary Flow: Browse Existing Media
#### 1. Selecting Previously Uploaded Image
+
1. **User clicks "Browse Library"** button (secondary option in forms)
2. **MediaLibraryModal opens** showing all previously uploaded media
3. **User browses or searches** existing content
@@ -329,6 +361,7 @@ interface GalleryManagerProps {
5. **Modal closes** and form shows selected media with existing alt text
#### 2. Managing Media Library
+
1. **User visits dedicated Media Library page**
2. **User can view all uploaded media** in grid/list format
3. **User can edit metadata** including alt text for any media
@@ -338,12 +371,14 @@ interface GalleryManagerProps {
## Design Specifications
### Modal Layout
+
- **Width**: 1200px max, responsive on smaller screens
- **Height**: 80vh max with scroll
- **Grid**: 4-6 columns depending on screen size
- **Item Size**: 180px × 140px thumbnails
### Visual States
+
- **Default**: Border with subtle background
- **Selected**: Blue border and checkmark overlay
- **Hover**: Slight scale and shadow effect
@@ -351,6 +386,7 @@ interface GalleryManagerProps {
- **Upload**: Progress overlay with percentage
### Colors (Using Existing Variables)
+
- **Selection**: `$blue-60` for selected state
- **Hover**: `$grey-10` background
- **Upload Progress**: `$green-60` for success, `$red-60` for error
@@ -358,17 +394,20 @@ interface GalleryManagerProps {
## API Integration
### Endpoints Used
+
- `GET /api/media` - Browse media with search/filter/pagination
- `POST /api/media/upload` - Single file upload
- `POST /api/media/bulk-upload` - Multiple file upload
### Search and Filtering
+
- **Search**: By filename (case-insensitive)
-- **Filter by Type**: image/*, video/*, all
+- **Filter by Type**: image/_, video/_, all
- **Filter by Usage**: unused only, all
- **Sort**: Most recent first
### Pagination
+
- 24 items per page
- Infinite scroll or traditional pagination
- Loading states during page changes
@@ -376,7 +415,9 @@ interface GalleryManagerProps {
## Implementation Plan
### ✅ Phase 1: Database Schema Updates (COMPLETED)
+
1. **✅ Alt Text Support**
+
- Database schema includes `altText` and `description` fields
- API endpoints support alt text in upload and update operations
@@ -385,13 +426,16 @@ interface GalleryManagerProps {
- Need dedicated tracking table for comprehensive usage analytics
### ✅ Phase 2: Direct Upload Components (COMPLETED)
+
1. **✅ ImageUploader Component**
+
- Drag-and-drop upload zone with visual feedback
- Immediate upload and preview functionality
- Alt text input integration
- MediaLibraryModal integration as secondary option
2. **✅ GalleryUploader Component**
+
- Multiple file drag-and-drop support
- Individual alt text inputs per image
- Drag-and-drop reordering functionality
@@ -404,7 +448,9 @@ interface GalleryManagerProps {
- Batch uploads with individual alt text support
### ✅ Phase 3: Form Integration (COMPLETED)
+
1. **✅ Project Forms Enhancement**
+
- Logo field enhanced with ImageUploader + Browse Library
- Featured image support with ImageUploader
- Gallery section implemented with GalleryUploader
@@ -417,7 +463,9 @@ interface GalleryManagerProps {
- Enhanced Edra editor with inline image/gallery support
### ✅ Phase 4: Media Library Management (MOSTLY COMPLETED)
+
1. **✅ Enhanced Media Library Page**
+
- Alt text editing for existing media via MediaDetailsModal
- Clickable media items with edit functionality
- Grid and list view toggles
@@ -431,7 +479,9 @@ interface GalleryManagerProps {
### 🎯 Phase 5: Remaining Enhancements (CURRENT PRIORITIES)
#### 🔥 High Priority (Next Sprint)
+
1. **Enhanced Media Library Features**
+
- **Bulk alt text editing** - Select multiple media items and edit alt text in batch
- **Usage tracking display** - Show where each media item is referenced
- **Advanced drag & drop zones** - More intuitive upload areas in all components
@@ -442,7 +492,9 @@ interface GalleryManagerProps {
- **Thumbnail optimization** for faster loading
#### 🔥 Medium Priority (Future Sprints)
+
1. **Advanced Upload Features**
+
- **Image resizing/optimization** options during upload
- **Duplicate detection** to prevent redundant uploads
- **Bulk upload improvements** with better progress tracking
@@ -453,6 +505,7 @@ interface GalleryManagerProps {
- **Advanced search** by alt text, usage status, date ranges
#### 🔥 Low Priority (Nice-to-Have)
+
1. **AI Integration**
- **Automatic alt text suggestions** using image recognition
- **Smart tagging** for better organization
@@ -463,8 +516,9 @@ interface GalleryManagerProps {
### Functional Requirements
#### Primary Workflow (Direct Upload)
+
- [x] **Drag-and-drop upload works** in all form components
-- [x] **Click-to-browse file selection** works reliably
+- [x] **Click-to-browse file selection** works reliably
- [x] **Immediate upload and preview** happens without page navigation
- [x] **Alt text input appears** and saves with uploaded media
- [x] **Upload progress** is clearly indicated with percentage
@@ -473,20 +527,23 @@ interface GalleryManagerProps {
- [x] **Gallery reordering** works with drag-and-drop after upload
#### Secondary Workflow (Media Library)
+
- [x] **Media Library Modal** opens and closes properly with smooth animations
- [x] **Single and multiple selection** modes work correctly
-- [x] **Search and filtering** return accurate results
+- [x] **Search and filtering** return accurate results
- [ ] **Usage tracking** shows where media is referenced (IN PROGRESS)
- [x] **Alt text editing** works in Media Library management
- [x] **All components are keyboard accessible**
#### Edra Editor Integration
+
- [x] **Slash commands** work for image and gallery insertion
- [x] **MediaLibraryModal integration** in editor placeholders
- [x] **Gallery management** within rich text editor
- [x] **Image replacement** functionality in editor
### Performance Requirements
+
- [x] Modal opens in under 200ms
- [x] Media grid loads in under 1 second
- [x] Search results appear in under 500ms
@@ -494,6 +551,7 @@ interface GalleryManagerProps {
- [x] No memory leaks when opening/closing modal multiple times
### UX Requirements
+
- [x] Interface is intuitive without instruction
- [x] Visual feedback is clear for all interactions
- [x] Error messages are helpful and actionable
@@ -503,23 +561,27 @@ interface GalleryManagerProps {
## Technical Considerations
### State Management
+
- Use Svelte runes for reactive state
- Maintain selection state during modal lifecycle
- Handle API loading and error states properly
### Accessibility
+
- Proper ARIA labels and roles
- Keyboard navigation support
- Focus management when modal opens/closes
- Screen reader announcements for state changes
### Performance
+
- Lazy load thumbnails as they come into view
- Debounce search input to prevent excessive API calls
- Efficient reordering without full re-renders
- Memory cleanup when modal is closed
### Error Handling
+
- Network failure recovery
- Upload failure feedback
- File validation error messages
@@ -528,6 +590,7 @@ interface GalleryManagerProps {
## Future Enhancements
### Nice-to-Have Features
+
- **Bulk Operations**: Delete multiple files, bulk tag editing
- **Advanced Search**: Search by tags, date range, file size
- **Preview Mode**: Full-size preview with navigation
@@ -537,6 +600,7 @@ interface GalleryManagerProps {
- **Alt Text Editor**: Quick alt text editing for accessibility
### Integration Opportunities
+
- **CDN Optimization**: Automatic image optimization settings
- **AI Tagging**: Automatic tag generation for uploaded images
- **Duplicate Detection**: Warn about similar/duplicate uploads
@@ -545,6 +609,7 @@ interface GalleryManagerProps {
## Development Checklist
### Core Components
+
- [x] MediaLibraryModal base structure
- [x] MediaSelector with grid layout
- [x] MediaUploader with drag-and-drop
@@ -552,6 +617,7 @@ interface GalleryManagerProps {
- [x] Pagination implementation
### Form Integration
+
- [x] MediaInput generic component (ImageUploader/GalleryUploader)
- [x] ImagePicker specialized component (ImageUploader)
- [x] GalleryManager with reordering (GalleryUploader)
@@ -560,6 +626,7 @@ interface GalleryManagerProps {
- [x] Integration with Edra editor
### Polish and Testing
+
- [x] Responsive design implementation
- [x] Accessibility testing and fixes
- [x] Performance optimization
@@ -568,9 +635,10 @@ interface GalleryManagerProps {
- [x] Mobile device testing
### 🎯 Next Priority Items
+
- [ ] **Bulk alt text editing** in Media Library
- [ ] **Usage tracking display** for media references
- [ ] **Advanced drag & drop zones** with better visual feedback
- [ ] **Performance optimizations** for large libraries
-This Media Library system will serve as the foundation for all media-related functionality in the CMS, enabling rich content creation across projects, posts, and albums.
\ No newline at end of file
+This Media Library system will serve as the foundation for all media-related functionality in the CMS, enabling rich content creation across projects, posts, and albums.
diff --git a/prd/PRD-storybook-integration.md b/prd/PRD-storybook-integration.md
index cbb3082..dbdf45b 100644
--- a/prd/PRD-storybook-integration.md
+++ b/prd/PRD-storybook-integration.md
@@ -16,6 +16,7 @@ Implement Storybook as our component development and documentation platform to i
## Current State Analysis
### ✅ What We Have
+
- Comprehensive admin UI component library (Button, Input, Modal, etc.)
- Media Library components (MediaLibraryModal, ImagePicker, GalleryManager, etc.)
- SCSS-based styling system with global variables
@@ -24,6 +25,7 @@ Implement Storybook as our component development and documentation platform to i
- Vite build system
### 🎯 What We Need
+
- Storybook installation and configuration
- Stories for existing components
- Visual regression testing setup
@@ -45,6 +47,7 @@ npm install --save-dev @storybook/svelte-vite @storybook/addon-essentials
```
**Expected File Structure**:
+
```
.storybook/
├── main.js # Storybook configuration
@@ -62,131 +65,135 @@ src/
### 2. Configuration Requirements
#### Main Configuration (.storybook/main.js)
+
```javascript
export default {
- stories: ['../src/**/*.stories.@(js|jsx|ts|tsx|svelte)'],
- addons: [
- '@storybook/addon-essentials', // Controls, actions, viewport, etc.
- '@storybook/addon-svelte-csf', // Svelte Component Story Format
- '@storybook/addon-a11y', // Accessibility testing
- '@storybook/addon-design-tokens', // Design system tokens
- ],
- framework: {
- name: '@storybook/svelte-vite',
- options: {}
- },
- viteFinal: async (config) => {
- // Integrate with existing Vite config
- // Import SCSS variables and aliases
- return mergeConfig(config, {
- resolve: {
- alias: {
- '$lib': path.resolve('./src/lib'),
- '$components': path.resolve('./src/lib/components'),
- '$icons': path.resolve('./src/assets/icons'),
- '$illos': path.resolve('./src/assets/illos'),
- }
- },
- css: {
- preprocessorOptions: {
- scss: {
- additionalData: `
+ stories: ['../src/**/*.stories.@(js|jsx|ts|tsx|svelte)'],
+ addons: [
+ '@storybook/addon-essentials', // Controls, actions, viewport, etc.
+ '@storybook/addon-svelte-csf', // Svelte Component Story Format
+ '@storybook/addon-a11y', // Accessibility testing
+ '@storybook/addon-design-tokens' // Design system tokens
+ ],
+ framework: {
+ name: '@storybook/svelte-vite',
+ options: {}
+ },
+ viteFinal: async (config) => {
+ // Integrate with existing Vite config
+ // Import SCSS variables and aliases
+ return mergeConfig(config, {
+ resolve: {
+ alias: {
+ $lib: path.resolve('./src/lib'),
+ $components: path.resolve('./src/lib/components'),
+ $icons: path.resolve('./src/assets/icons'),
+ $illos: path.resolve('./src/assets/illos')
+ }
+ },
+ css: {
+ preprocessorOptions: {
+ scss: {
+ additionalData: `
@import './src/assets/styles/variables.scss';
@import './src/assets/styles/fonts.scss';
@import './src/assets/styles/themes.scss';
@import './src/assets/styles/globals.scss';
`
- }
- }
- }
- });
- }
-};
+ }
+ }
+ }
+ })
+ }
+}
```
#### Preview Configuration (.storybook/preview.js)
+
```javascript
-import '../src/assets/styles/reset.css';
-import '../src/assets/styles/globals.scss';
+import '../src/assets/styles/reset.css'
+import '../src/assets/styles/globals.scss'
export const parameters = {
- actions: { argTypesRegex: '^on[A-Z].*' },
- controls: {
- matchers: {
- color: /(background|color)$/i,
- date: /Date$/,
- },
- },
- backgrounds: {
- default: 'light',
- values: [
- { name: 'light', value: '#ffffff' },
- { name: 'dark', value: '#333333' },
- { name: 'admin', value: '#f5f5f5' },
- ],
- },
- viewport: {
- viewports: {
- mobile: { name: 'Mobile', styles: { width: '375px', height: '667px' } },
- tablet: { name: 'Tablet', styles: { width: '768px', height: '1024px' } },
- desktop: { name: 'Desktop', styles: { width: '1440px', height: '900px' } },
- },
- },
-};
+ actions: { argTypesRegex: '^on[A-Z].*' },
+ controls: {
+ matchers: {
+ color: /(background|color)$/i,
+ date: /Date$/
+ }
+ },
+ backgrounds: {
+ default: 'light',
+ values: [
+ { name: 'light', value: '#ffffff' },
+ { name: 'dark', value: '#333333' },
+ { name: 'admin', value: '#f5f5f5' }
+ ]
+ },
+ viewport: {
+ viewports: {
+ mobile: { name: 'Mobile', styles: { width: '375px', height: '667px' } },
+ tablet: { name: 'Tablet', styles: { width: '768px', height: '1024px' } },
+ desktop: { name: 'Desktop', styles: { width: '1440px', height: '900px' } }
+ }
+ }
+}
```
### 3. Component Story Standards
#### Story File Format
+
Each component should have a corresponding `.stories.js` file following this structure:
```javascript
// Button.stories.js
-import Button from '../lib/components/admin/Button.svelte';
+import Button from '../lib/components/admin/Button.svelte'
export default {
- title: 'Admin/Button',
- component: Button,
- tags: ['autodocs'],
- argTypes: {
- variant: {
- control: { type: 'select' },
- options: ['primary', 'secondary', 'ghost', 'danger']
- },
- size: {
- control: { type: 'select' },
- options: ['small', 'medium', 'large']
- },
- disabled: {
- control: 'boolean'
- },
- onclick: { action: 'clicked' }
- }
-};
+ title: 'Admin/Button',
+ component: Button,
+ tags: ['autodocs'],
+ argTypes: {
+ variant: {
+ control: { type: 'select' },
+ options: ['primary', 'secondary', 'ghost', 'danger']
+ },
+ size: {
+ control: { type: 'select' },
+ options: ['small', 'medium', 'large']
+ },
+ disabled: {
+ control: 'boolean'
+ },
+ onclick: { action: 'clicked' }
+ }
+}
export const Primary = {
- args: {
- variant: 'primary',
- children: 'Primary Button'
- }
-};
+ args: {
+ variant: 'primary',
+ children: 'Primary Button'
+ }
+}
export const Secondary = {
- args: {
- variant: 'secondary',
- children: 'Secondary Button'
- }
-};
+ args: {
+ variant: 'secondary',
+ children: 'Secondary Button'
+ }
+}
export const AllVariants = {
- render: () => ({
- Component: ButtonShowcase,
- props: {}
- })
-};
+ render: () => ({
+ Component: ButtonShowcase,
+ props: {}
+ })
+}
```
#### Story Organization
+
```
src/stories/
├── admin/ # Admin interface components
@@ -208,7 +215,9 @@ src/stories/
## Implementation Plan
### Phase 1: Initial Setup (1-2 days)
+
1. **Install and Configure Storybook**
+
- Run `npx storybook@latest init`
- Configure Vite integration for SCSS and aliases
- Set up TypeScript support
@@ -220,13 +229,16 @@ src/stories/
- Test hot reloading
### Phase 2: Core Component Stories (3-4 days)
+
1. **Basic UI Components**
+
- Button (all variants, states, sizes)
- Input (text, textarea, validation states)
- Modal (different sizes, content types)
- LoadingSpinner (different sizes)
2. **Form Components**
+
- MediaInput (single/multiple modes)
- ImagePicker (different aspect ratios)
- GalleryManager (with/without items)
@@ -237,12 +249,15 @@ src/stories/
- AdminNavBar (active states)
### Phase 3: Advanced Features (2-3 days)
+
1. **Mock Data Setup**
+
- Create mock Media objects
- Set up API mocking for components that need data
- Create realistic test scenarios
2. **Accessibility Testing**
+
- Add @storybook/addon-a11y
- Test keyboard navigation
- Verify screen reader compatibility
@@ -253,7 +268,9 @@ src/stories/
- Configure CI integration
### Phase 4: Documentation and Polish (1-2 days)
+
1. **Component Documentation**
+
- Add JSDoc comments to components
- Create usage examples
- Document props and events
@@ -267,6 +284,7 @@ src/stories/
## Success Criteria
### Functional Requirements
+
- [ ] Storybook runs successfully with `npm run storybook`
- [ ] All existing components have basic stories
- [ ] SCSS variables and global styles work correctly
@@ -275,6 +293,7 @@ src/stories/
- [ ] TypeScript support is fully functional
### Quality Requirements
+
- [ ] Stories cover all major component variants
- [ ] Interactive controls work for all props
- [ ] Actions are properly logged for events
@@ -282,6 +301,7 @@ src/stories/
- [ ] Components are responsive across viewport sizes
### Developer Experience Requirements
+
- [ ] Story creation is straightforward and documented
- [ ] Mock data is easily accessible and realistic
- [ ] Component API is clearly documented
@@ -290,12 +310,14 @@ src/stories/
## Integration with Existing Workflow
### Development Workflow
+
1. **Component Development**: Start new components in Storybook
2. **Testing**: Test all states and edge cases in stories
3. **Documentation**: Stories serve as living documentation
4. **Review**: Use Storybook for design/code reviews
### Project Structure Integration
+
```
package.json # Add storybook scripts
├── "storybook": "storybook dev -p 6006"
@@ -309,35 +331,40 @@ src/
```
### Scripts and Commands
+
```json
{
- "scripts": {
- "dev": "vite dev",
- "storybook": "storybook dev -p 6006",
- "build-storybook": "storybook build",
- "storybook:test": "test-storybook"
- }
+ "scripts": {
+ "dev": "vite dev",
+ "storybook": "storybook dev -p 6006",
+ "build-storybook": "storybook build",
+ "storybook:test": "test-storybook"
+ }
}
```
## Technical Considerations
### SCSS Integration
+
- Import global variables in Storybook preview
- Ensure component styles render correctly
- Test responsive breakpoints
### SvelteKit Compatibility
+
- Handle SvelteKit-specific imports (like `$app/stores`)
- Mock SvelteKit modules when needed
- Ensure aliases work in Storybook context
### TypeScript Support
+
- Configure proper type checking
- Use TypeScript for story definitions where beneficial
- Ensure IntelliSense works for story arguments
### Performance
+
- Optimize bundle size for faster story loading
- Use lazy loading for large story collections
- Configure appropriate caching
@@ -345,16 +372,19 @@ src/
## Future Enhancements
### Advanced Testing
+
- **Visual Regression Testing**: Use Chromatic for automated visual testing
- **Interaction Testing**: Add @storybook/addon-interactions for user flow testing
- **Accessibility Automation**: Automated a11y testing in CI/CD
### Design System Evolution
+
- **Design Tokens**: Implement design tokens addon
- **Figma Integration**: Connect with Figma designs
- **Component Status**: Track component implementation status
### Collaboration Features
+
- **Published Storybook**: Deploy Storybook for team access
- **Design Review Process**: Use Storybook for design approvals
- **Documentation Site**: Evolve into full design system documentation
@@ -362,30 +392,35 @@ src/
## Risks and Mitigation
### Technical Risks
+
- **Build Conflicts**: Vite configuration conflicts
- - *Mitigation*: Careful configuration merging and testing
+ - _Mitigation_: Careful configuration merging and testing
- **SCSS Import Issues**: Global styles not loading
- - *Mitigation*: Test SCSS integration early in setup
+ - _Mitigation_: Test SCSS integration early in setup
### Workflow Risks
+
- **Adoption Resistance**: Team not using Storybook
- - *Mitigation*: Start with high-value components, show immediate benefits
+ - _Mitigation_: Start with high-value components, show immediate benefits
- **Maintenance Overhead**: Stories become outdated
- - *Mitigation*: Include story updates in component change process
+ - _Mitigation_: Include story updates in component change process
## Success Metrics
### Development Efficiency
+
- Reduced time to develop new components
- Faster iteration on component designs
- Fewer bugs in component edge cases
### Code Quality
+
- Better component API consistency
- Improved accessibility compliance
- More comprehensive component testing
### Team Collaboration
+
- Faster design review cycles
- Better communication between design and development
- More consistent component usage across the application
@@ -394,4 +429,4 @@ src/
Implementing Storybook will significantly improve our component development workflow, provide better documentation, and create a foundation for a mature design system. The investment in setup and story creation will pay dividends in development speed, component quality, and team collaboration.
-The implementation should be done incrementally, starting with the most commonly used components and gradually expanding coverage. This approach minimizes risk while providing immediate value to the development process.
\ No newline at end of file
+The implementation should be done incrementally, starting with the most commonly used components and gradually expanding coverage. This approach minimizes risk while providing immediate value to the development process.
diff --git a/src/app.html b/src/app.html
index 2f0b73f..e1f2aae 100644
--- a/src/app.html
+++ b/src/app.html
@@ -3,7 +3,10 @@
-
+
diff --git a/src/lib/components/ImagePost.svelte b/src/lib/components/ImagePost.svelte
index 7eedc53..88a2c99 100644
--- a/src/lib/components/ImagePost.svelte
+++ b/src/lib/components/ImagePost.svelte
@@ -10,8 +10,7 @@
} = $props()
// Convert string array to slideshow items
- const slideshowItems = $derived(images.map(url => ({ url, alt })))
+ const slideshowItems = $derived(images.map((url) => ({ url, alt })))
-
diff --git a/src/lib/components/Mention.svelte b/src/lib/components/Mention.svelte
index 62c5dd2..6c5eaaf 100644
--- a/src/lib/components/Mention.svelte
+++ b/src/lib/components/Mention.svelte
@@ -7,13 +7,7 @@
source?: string
}
- let {
- href = '',
- title = '',
- sourceType = '',
- date = '',
- source = ''
- }: Props = $props()
+ let { href = '', title = '', sourceType = '', date = '', source = '' }: Props = $props()
diff --git a/src/lib/components/NavDropdown.svelte b/src/lib/components/NavDropdown.svelte
index 088ad78..57f4996 100644
--- a/src/lib/components/NavDropdown.svelte
+++ b/src/lib/components/NavDropdown.svelte
@@ -31,9 +31,10 @@
currentPath === '/'
? navItems[0]
: currentPath === '/about'
- ? navItems[4]
- : navItems.find((item) => currentPath.startsWith(item.href === '/' ? '/work' : item.href)) ||
- navItems[0]
+ ? navItems[4]
+ : navItems.find((item) =>
+ currentPath.startsWith(item.href === '/' ? '/work' : item.href)
+ ) || navItems[0]
)
// Get background color based on variant
diff --git a/src/lib/components/PostContent.svelte b/src/lib/components/PostContent.svelte
index 73e3492..05d4e26 100644
--- a/src/lib/components/PostContent.svelte
+++ b/src/lib/components/PostContent.svelte
@@ -60,6 +60,8 @@
}
&.essay {
+ max-width: 100%; // Full width for essays
+
.post-body {
font-size: 1rem;
line-height: 1.5;
@@ -75,6 +77,8 @@
}
&.blog {
+ max-width: 100%; // Full width for blog posts (legacy essays)
+
.post-body {
font-size: 1rem;
line-height: 1.5;
diff --git a/src/lib/components/UniverseCard.svelte b/src/lib/components/UniverseCard.svelte
index bb0e9f7..3785c27 100644
--- a/src/lib/components/UniverseCard.svelte
+++ b/src/lib/components/UniverseCard.svelte
@@ -57,10 +57,10 @@
{#if type === 'album'}
-
- {:else}
-
- {/if}
+
+ {:else}
+
+ {/if}
diff --git a/src/lib/components/UniversePostCard.svelte b/src/lib/components/UniversePostCard.svelte
index c611bf4..fbbbcae 100644
--- a/src/lib/components/UniversePostCard.svelte
+++ b/src/lib/components/UniversePostCard.svelte
@@ -23,7 +23,6 @@
{/if}
-
{#if post.content}
{getContentExcerpt(post.content, 150)}
diff --git a/src/lib/components/admin/PhotoPostForm.svelte b/src/lib/components/admin/PhotoPostForm.svelte
index 5946c96..3e64088 100644
--- a/src/lib/components/admin/PhotoPostForm.svelte
+++ b/src/lib/components/admin/PhotoPostForm.svelte
@@ -124,7 +124,7 @@
.split(',')
.map((tag) => tag.trim())
.filter(Boolean)
- : [],
+ : []
}
const url = mode === 'edit' ? `/api/posts/${postId}` : '/api/posts'
@@ -159,7 +159,6 @@
}
}
-
async function handlePublish() {
status = 'published'
await handleSave()
diff --git a/src/lib/components/admin/ProjectForm.svelte b/src/lib/components/admin/ProjectForm.svelte
index 925fda2..34309a8 100644
--- a/src/lib/components/admin/ProjectForm.svelte
+++ b/src/lib/components/admin/ProjectForm.svelte
@@ -59,7 +59,8 @@
role: data.role || '',
projectType: data.projectType || 'work',
externalUrl: data.externalUrl || '',
- featuredImage: data.featuredImage && data.featuredImage.trim() !== '' ? data.featuredImage : null,
+ featuredImage:
+ data.featuredImage && data.featuredImage.trim() !== '' ? data.featuredImage : null,
backgroundColor: data.backgroundColor || '',
highlightColor: data.highlightColor || '',
logoUrl: data.logoUrl && data.logoUrl.trim() !== '' ? data.logoUrl : '',
@@ -140,7 +141,8 @@
role: formData.role,
projectType: formData.projectType,
externalUrl: formData.externalUrl,
- featuredImage: formData.featuredImage && formData.featuredImage !== '' ? formData.featuredImage : null,
+ featuredImage:
+ formData.featuredImage && formData.featuredImage !== '' ? formData.featuredImage : null,
logoUrl: formData.logoUrl && formData.logoUrl !== '' ? formData.logoUrl : null,
backgroundColor: formData.backgroundColor,
highlightColor: formData.highlightColor,
@@ -222,11 +224,9 @@
onStatusChange={handleStatusChange}
disabled={isSaving}
isLoading={isSaving}
- primaryAction={
- formData.status === 'published'
- ? { label: 'Save', status: 'published' }
- : { label: 'Publish', status: 'published' }
- }
+ primaryAction={formData.status === 'published'
+ ? { label: 'Save', status: 'published' }
+ : { label: 'Publish', status: 'published' }}
dropdownActions={[
{ label: 'Save as Draft', status: 'draft', show: formData.status !== 'draft' },
{ label: 'List Only', status: 'list-only', show: formData.status !== 'list-only' },
diff --git a/src/lib/components/admin/ProjectImagesForm.svelte b/src/lib/components/admin/ProjectImagesForm.svelte
index bb56332..f304c15 100644
--- a/src/lib/components/admin/ProjectImagesForm.svelte
+++ b/src/lib/components/admin/ProjectImagesForm.svelte
@@ -135,4 +135,4 @@
align-items: center;
margin-bottom: $unit-3x;
}
-
\ No newline at end of file
+
diff --git a/src/lib/components/admin/ProjectMetadataForm.svelte b/src/lib/components/admin/ProjectMetadataForm.svelte
index c0a1861..a2447ff 100644
--- a/src/lib/components/admin/ProjectMetadataForm.svelte
+++ b/src/lib/components/admin/ProjectMetadataForm.svelte
@@ -12,7 +12,6 @@
}
let { formData = $bindable(), validationErrors, onSave }: Props = $props()
-
{/if}
diff --git a/src/routes/admin/albums/new/+page.svelte b/src/routes/admin/albums/new/+page.svelte
index f730092..5295cb2 100644
--- a/src/routes/admin/albums/new/+page.svelte
+++ b/src/routes/admin/albums/new/+page.svelte
@@ -102,13 +102,13 @@
// Add selected photos to the newly created album
if (albumPhotos.length > 0) {
console.log(`Adding ${albumPhotos.length} photos to newly created album ${album.id}`)
-
+
try {
const addedPhotos = []
for (let i = 0; i < albumPhotos.length; i++) {
const media = albumPhotos[i]
console.log(`Adding photo ${media.id} to album ${album.id}`)
-
+
const photoResponse = await fetch(`/api/albums/${album.id}/photos`, {
method: 'POST',
headers: {
@@ -123,7 +123,11 @@
if (!photoResponse.ok) {
const errorData = await photoResponse.text()
- console.error(`Failed to add photo ${media.filename}:`, photoResponse.status, errorData)
+ console.error(
+ `Failed to add photo ${media.filename}:`,
+ photoResponse.status,
+ errorData
+ )
// Continue with other photos even if one fails
} else {
const photo = await photoResponse.json()
@@ -131,8 +135,10 @@
console.log(`Successfully added photo ${photo.id} to album`)
}
}
-
- console.log(`Successfully added ${addedPhotos.length} out of ${albumPhotos.length} photos to album`)
+
+ console.log(
+ `Successfully added ${addedPhotos.length} out of ${albumPhotos.length} photos to album`
+ )
} catch (photoError) {
console.error('Error adding photos to album:', photoError)
// Don't fail the whole creation - just log the error
diff --git a/src/routes/admin/posts/+page.svelte b/src/routes/admin/posts/+page.svelte
index 2e542d5..d4bd43e 100644
--- a/src/routes/admin/posts/+page.svelte
+++ b/src/routes/admin/posts/+page.svelte
@@ -30,40 +30,37 @@
let error = $state('')
let total = $state(0)
let postTypeCounts = $state
>({})
+ let statusCounts = $state>({})
// Filter state
- let selectedFilter = $state('all')
+ let selectedTypeFilter = $state('all')
+ let selectedStatusFilter = $state('all')
// Composer state
let showInlineComposer = $state(true)
+ let isInteractingWithFilters = $state(false)
// Create filter options
- const filterOptions = $derived([
+ const typeFilterOptions = $derived([
{ value: 'all', label: 'All posts' },
{ value: 'post', label: 'Posts' },
{ value: 'essay', label: 'Essays' }
])
+ const statusFilterOptions = $derived([
+ { value: 'all', label: 'All statuses' },
+ { value: 'published', label: 'Published' },
+ { value: 'draft', label: 'Draft' }
+ ])
+
const postTypeIcons: Record = {
post: '💭',
- essay: '📝',
- // Legacy types for backward compatibility
- blog: '📝',
- microblog: '💭',
- link: '🔗',
- photo: '📷',
- album: '🖼️'
+ essay: '📝'
}
const postTypeLabels: Record = {
post: 'Post',
- essay: 'Essay',
- // Legacy types for backward compatibility
- blog: 'Essay',
- microblog: 'Post',
- link: 'Post',
- photo: 'Post',
- album: 'Album'
+ essay: 'Essay'
}
onMount(async () => {
@@ -94,24 +91,29 @@
posts = data.posts || []
total = data.pagination?.total || posts.length
- // Calculate post type counts and normalize types
- const counts: Record = {
+ // Calculate post type counts
+ const typeCounts: Record = {
all: posts.length,
post: 0,
essay: 0
}
posts.forEach((post) => {
- // Normalize legacy types to simplified types
- if (post.postType === 'blog') {
- counts.essay = (counts.essay || 0) + 1
- } else if (['microblog', 'link', 'photo'].includes(post.postType)) {
- counts.post = (counts.post || 0) + 1
- } else {
- counts[post.postType] = (counts[post.postType] || 0) + 1
+ if (post.postType === 'post') {
+ typeCounts.post++
+ } else if (post.postType === 'essay') {
+ typeCounts.essay++
}
})
- postTypeCounts = counts
+ postTypeCounts = typeCounts
+
+ // Calculate status counts
+ const statusCountsTemp: Record = {
+ all: posts.length,
+ published: posts.filter((p) => p.status === 'published').length,
+ draft: posts.filter((p) => p.status === 'draft').length
+ }
+ statusCounts = statusCountsTemp
// Apply initial filter
applyFilter()
@@ -124,18 +126,26 @@
}
function applyFilter() {
- if (selectedFilter === 'all') {
- filteredPosts = posts
- } else if (selectedFilter === 'post') {
- filteredPosts = posts.filter((post) => ['post', 'microblog'].includes(post.postType))
- } else if (selectedFilter === 'essay') {
- filteredPosts = posts.filter((post) => ['essay', 'blog'].includes(post.postType))
- } else {
- filteredPosts = posts.filter((post) => post.postType === selectedFilter)
+ let filtered = posts
+
+ // Apply type filter
+ if (selectedTypeFilter !== 'all') {
+ filtered = filtered.filter((post) => post.postType === selectedTypeFilter)
}
+
+ // Apply status filter
+ if (selectedStatusFilter !== 'all') {
+ filtered = filtered.filter((post) => post.status === selectedStatusFilter)
+ }
+
+ filteredPosts = filtered
}
- function handleFilterChange() {
+ function handleTypeFilterChange() {
+ applyFilter()
+ }
+
+ function handleStatusFilterChange() {
applyFilter()
}
@@ -168,11 +178,18 @@
{#snippet left()}
+
{/snippet}
@@ -187,10 +204,11 @@
📝
No posts found
- {#if selectedFilter === 'all'}
+ {#if selectedTypeFilter === 'all' && selectedStatusFilter === 'all'}
Create your first post to get started!
{:else}
- No {selectedFilter}s found. Try a different filter or create a new {selectedFilter}.
+ No posts found matching the current filters. Try adjusting your filters or create a new
+ post.
{/if}
diff --git a/src/routes/admin/projects/+page.svelte b/src/routes/admin/projects/+page.svelte
index 082bcd0..6e0cfd2 100644
--- a/src/routes/admin/projects/+page.svelte
+++ b/src/routes/admin/projects/+page.svelte
@@ -34,22 +34,22 @@
let statusCounts = $state>({})
// Filter state
- let selectedStatusFilter = $state('all')
let selectedTypeFilter = $state('all')
+ let selectedStatusFilter = $state('all')
// Create filter options
- const statusFilterOptions = $derived([
+ const typeFilterOptions = $derived([
{ value: 'all', label: 'All projects' },
+ { value: 'work', label: 'Work' },
+ { value: 'labs', label: 'Labs' }
+ ])
+
+ const statusFilterOptions = $derived([
+ { value: 'all', label: 'All statuses' },
{ value: 'published', label: 'Published' },
{ value: 'draft', label: 'Draft' }
])
- const typeFilterOptions = [
- { value: 'all', label: 'All types' },
- { value: 'work', label: 'Work' },
- { value: 'labs', label: 'Labs' }
- ]
-
onMount(async () => {
await loadProjects()
// Handle clicks outside dropdowns
@@ -204,13 +204,6 @@
{#snippet left()}
-
+
{/snippet}
diff --git a/src/routes/api/albums/[id]/+server.ts b/src/routes/api/albums/[id]/+server.ts
index bf032e4..b034c62 100644
--- a/src/routes/api/albums/[id]/+server.ts
+++ b/src/routes/api/albums/[id]/+server.ts
@@ -140,9 +140,11 @@ export const PUT: RequestHandler = async (event) => {
date: body.date !== undefined ? (body.date ? new Date(body.date) : null) : existing.date,
location: body.location !== undefined ? body.location : existing.location,
coverPhotoId: body.coverPhotoId !== undefined ? body.coverPhotoId : existing.coverPhotoId,
- isPhotography: body.isPhotography !== undefined ? body.isPhotography : existing.isPhotography,
+ isPhotography:
+ body.isPhotography !== undefined ? body.isPhotography : existing.isPhotography,
status: body.status !== undefined ? body.status : existing.status,
- showInUniverse: body.showInUniverse !== undefined ? body.showInUniverse : existing.showInUniverse
+ showInUniverse:
+ body.showInUniverse !== undefined ? body.showInUniverse : existing.showInUniverse
}
})
diff --git a/src/routes/api/media/[id]/+server.ts b/src/routes/api/media/[id]/+server.ts
index c9011b1..2cdd381 100644
--- a/src/routes/api/media/[id]/+server.ts
+++ b/src/routes/api/media/[id]/+server.ts
@@ -70,7 +70,8 @@ export const PUT: RequestHandler = async (event) => {
data: {
altText: body.altText !== undefined ? body.altText : existing.altText,
description: body.description !== undefined ? body.description : existing.description,
- isPhotography: body.isPhotography !== undefined ? body.isPhotography : existing.isPhotography
+ isPhotography:
+ body.isPhotography !== undefined ? body.isPhotography : existing.isPhotography
}
})
diff --git a/src/routes/api/media/bulk-delete/+server.ts b/src/routes/api/media/bulk-delete/+server.ts
index e63a995..6ce3c01 100644
--- a/src/routes/api/media/bulk-delete/+server.ts
+++ b/src/routes/api/media/bulk-delete/+server.ts
@@ -50,7 +50,7 @@ export const DELETE: RequestHandler = async (event) => {
for (const media of mediaRecords) {
try {
let deleted = false
-
+
// Check if it's a Cloudinary URL
if (media.url.includes('cloudinary.com')) {
const publicId = extractPublicId(media.url)
@@ -64,7 +64,10 @@ export const DELETE: RequestHandler = async (event) => {
// Local storage deletion
deleted = await deleteFileLocally(media.url)
if (!deleted) {
- logger.warn('Failed to delete from local storage', { url: media.url, mediaId: media.id })
+ logger.warn('Failed to delete from local storage', {
+ url: media.url,
+ mediaId: media.id
+ })
}
}
@@ -114,8 +117,8 @@ export const DELETE: RequestHandler = async (event) => {
})
// Count successful storage deletions
- const successfulStorageDeletions = storageDeleteResults.filter(r => r.deleted).length
- const failedStorageDeletions = storageDeleteResults.filter(r => !r.deleted)
+ const successfulStorageDeletions = storageDeleteResults.filter((r) => r.deleted).length
+ const failedStorageDeletions = storageDeleteResults.filter((r) => !r.deleted)
logger.info('Bulk media deletion completed', {
deletedCount: deleteResult.count,
diff --git a/src/routes/api/projects/[id]/+server.ts b/src/routes/api/projects/[id]/+server.ts
index 44327cc..68908a0 100644
--- a/src/routes/api/projects/[id]/+server.ts
+++ b/src/routes/api/projects/[id]/+server.ts
@@ -82,13 +82,17 @@ export const PUT: RequestHandler = async (event) => {
year: body.year !== undefined ? body.year : existing.year,
client: body.client !== undefined ? body.client : existing.client,
role: body.role !== undefined ? body.role : existing.role,
- featuredImage: body.featuredImage !== undefined ? body.featuredImage : existing.featuredImage,
+ featuredImage:
+ body.featuredImage !== undefined ? body.featuredImage : existing.featuredImage,
logoUrl: body.logoUrl !== undefined ? body.logoUrl : existing.logoUrl,
gallery: body.gallery !== undefined ? body.gallery : existing.gallery,
externalUrl: body.externalUrl !== undefined ? body.externalUrl : existing.externalUrl,
- caseStudyContent: body.caseStudyContent !== undefined ? body.caseStudyContent : existing.caseStudyContent,
- backgroundColor: body.backgroundColor !== undefined ? body.backgroundColor : existing.backgroundColor,
- highlightColor: body.highlightColor !== undefined ? body.highlightColor : existing.highlightColor,
+ caseStudyContent:
+ body.caseStudyContent !== undefined ? body.caseStudyContent : existing.caseStudyContent,
+ backgroundColor:
+ body.backgroundColor !== undefined ? body.backgroundColor : existing.backgroundColor,
+ highlightColor:
+ body.highlightColor !== undefined ? body.highlightColor : existing.highlightColor,
projectType: body.projectType !== undefined ? body.projectType : existing.projectType,
displayOrder: body.displayOrder !== undefined ? body.displayOrder : existing.displayOrder,
status: body.status !== undefined ? body.status : existing.status,
@@ -195,7 +199,7 @@ export const PATCH: RequestHandler = async (event) => {
// Build update data object with only provided fields
const updateData: any = {}
-
+
// Handle status update specially
if (body.status !== undefined) {
updateData.status = body.status