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 ``` ## 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() -
    diff --git a/src/lib/components/admin/SegmentedControlField.svelte b/src/lib/components/admin/SegmentedControlField.svelte index 662c5b9..673e4d0 100644 --- a/src/lib/components/admin/SegmentedControlField.svelte +++ b/src/lib/components/admin/SegmentedControlField.svelte @@ -94,7 +94,9 @@ &.active { background-color: white; color: $grey-10; - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08), 0 1px 2px rgba(0, 0, 0, 0.04); + box-shadow: + 0 1px 3px rgba(0, 0, 0, 0.08), + 0 1px 2px rgba(0, 0, 0, 0.04); } &:focus { @@ -105,4 +107,4 @@ box-shadow: 0 0 0 2px $primary-color; } } - \ No newline at end of file + diff --git a/src/lib/components/admin/SelectField.svelte b/src/lib/components/admin/SelectField.svelte index cd831b5..e747cfe 100644 --- a/src/lib/components/admin/SelectField.svelte +++ b/src/lib/components/admin/SelectField.svelte @@ -38,15 +38,7 @@ {#snippet children()} - {/snippet} @@ -57,4 +49,4 @@ margin-top: 0; } } - \ No newline at end of file + diff --git a/src/lib/server/cloudinary.ts b/src/lib/server/cloudinary.ts index 53c24d9..f0ec7b7 100644 --- a/src/lib/server/cloudinary.ts +++ b/src/lib/server/cloudinary.ts @@ -78,8 +78,8 @@ export async function uploadFile( ): Promise { try { // TEMPORARY: Force Cloudinary usage for testing - const FORCE_CLOUDINARY_IN_DEV = true; // Toggle this to test - + const FORCE_CLOUDINARY_IN_DEV = true // Toggle this to test + // Use local storage in development or when Cloudinary is not configured if ((dev && !FORCE_CLOUDINARY_IN_DEV) || !isCloudinaryConfigured()) { logger.info('Using local storage for file upload') @@ -111,11 +111,11 @@ export async function uploadFile( // Check if file is SVG for logging purposes const isSvg = file.type === 'image/svg+xml' || file.name.toLowerCase().endsWith('.svg') - + // Extract filename without extension const fileNameWithoutExt = file.name.replace(/\.[^/.]+$/, '') const fileExtension = file.name.split('.').pop()?.toLowerCase() - + // Prepare upload options const uploadOptions = { ...uploadPresets[type], @@ -124,7 +124,7 @@ export async function uploadFile( // For SVG files, explicitly set format to preserve extension ...(isSvg && { format: 'svg' }) } - + // Log upload attempt for debugging logger.info('Attempting file upload:', { filename: file.name, @@ -136,14 +136,11 @@ export async function uploadFile( // Upload to Cloudinary const result = await new Promise((resolve, reject) => { - const uploadStream = cloudinary.uploader.upload_stream( - uploadOptions, - (error, result) => { - if (error) reject(error) - else if (result) resolve(result) - else reject(new Error('Upload failed')) - } - ) + const uploadStream = cloudinary.uploader.upload_stream(uploadOptions, (error, result) => { + if (error) reject(error) + else if (result) resolve(result) + else reject(new Error('Upload failed')) + }) uploadStream.end(buffer) }) @@ -170,7 +167,7 @@ export async function uploadFile( } catch (error) { logger.error('Cloudinary upload failed', error as Error) logger.mediaUpload(file.name, file.size, file.type, false) - + // Enhanced error logging if (error instanceof Error) { logger.error('Upload error details:', { @@ -209,7 +206,7 @@ export async function deleteFile(publicId: string): Promise { const result = await cloudinary.uploader.destroy(publicId, { resource_type: 'auto' }) - + return result.result === 'ok' } catch (error) { logger.error('Cloudinary delete failed', error as Error) diff --git a/src/lib/utils/content.ts b/src/lib/utils/content.ts index cfebb78..5ef0f0d 100644 --- a/src/lib/utils/content.ts +++ b/src/lib/utils/content.ts @@ -169,47 +169,49 @@ function renderTiptapContent(doc: any): string { // Render inline content (text nodes with marks) const renderInlineContent = (content: any[]): string => { - return content.map((node: any) => { - if (node.type === 'text') { - let text = escapeHtml(node.text || '') - - // Apply marks (bold, italic, etc.) - if (node.marks) { - node.marks.forEach((mark: any) => { - switch (mark.type) { - case 'bold': - text = `${text}` - break - case 'italic': - text = `${text}` - break - case 'underline': - text = `${text}` - break - case 'strike': - text = `${text}` - break - case 'code': - text = `${text}` - break - case 'link': - const href = mark.attrs?.href || '#' - const target = mark.attrs?.target || '_blank' - text = `${text}` - break - case 'highlight': - text = `${text}` - break - } - }) + return content + .map((node: any) => { + if (node.type === 'text') { + let text = escapeHtml(node.text || '') + + // Apply marks (bold, italic, etc.) + if (node.marks) { + node.marks.forEach((mark: any) => { + switch (mark.type) { + case 'bold': + text = `${text}` + break + case 'italic': + text = `${text}` + break + case 'underline': + text = `${text}` + break + case 'strike': + text = `${text}` + break + case 'code': + text = `${text}` + break + case 'link': + const href = mark.attrs?.href || '#' + const target = mark.attrs?.target || '_blank' + text = `${text}` + break + case 'highlight': + text = `${text}` + break + } + }) + } + + return text } - - return text - } - - // Handle other inline nodes - return renderNode(node) - }).join('') + + // Handle other inline nodes + return renderNode(node) + }) + .join('') } // Helper to escape HTML @@ -228,7 +230,7 @@ function renderTiptapContent(doc: any): string { // Extract text content from Edra JSON for excerpt export const getContentExcerpt = (content: any, maxLength = 200): string => { if (!content) return '' - + // Handle Tiptap format first (has type: 'doc') if (content.type === 'doc' && content.content) { return extractTiptapText(content, maxLength) @@ -263,14 +265,14 @@ function extractTiptapText(doc: any, maxLength: number): string { if (node.type === 'text') { return node.text || '' } - + if (node.content && Array.isArray(node.content)) { return node.content.map(extractFromNode).join(' ') } - + return '' } - + const text = doc.content.map(extractFromNode).join(' ').trim() if (text.length <= maxLength) return text return text.substring(0, maxLength).trim() + '...' diff --git a/src/lib/utils/date.ts b/src/lib/utils/date.ts index a97f509..9a5476b 100644 --- a/src/lib/utils/date.ts +++ b/src/lib/utils/date.ts @@ -5,4 +5,4 @@ export function formatDate(dateString: string): string { month: 'short', day: 'numeric' }) -} \ No newline at end of file +} diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 52fb3c0..7a76858 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -9,10 +9,7 @@ @jedmund is a software designer - + {#if !isAdminRoute} diff --git a/src/routes/admin/albums/[id]/edit/+page.svelte b/src/routes/admin/albums/[id]/edit/+page.svelte index e63b98e..b7bd84b 100644 --- a/src/routes/admin/albums/[id]/edit/+page.svelte +++ b/src/routes/admin/albums/[id]/edit/+page.svelte @@ -213,11 +213,14 @@ // Add photos to album via API const addedPhotos = [] - console.log('Adding photos to album:', newMedia.map(m => ({ id: m.id, filename: m.filename }))) - + console.log( + 'Adding photos to album:', + newMedia.map((m) => ({ id: m.id, filename: m.filename })) + ) + for (const media of newMedia) { console.log(`Adding photo ${media.id} (${media.filename}) to album ${album.id}`) - + const response = await fetch(`/api/albums/${album.id}/photos`, { method: 'POST', headers: { @@ -596,14 +599,10 @@ onStatusChange={handleSave} disabled={isSaving} isLoading={isSaving} - primaryAction={ - status === 'published' - ? { label: 'Save', status: 'published' } - : { label: 'Publish', status: 'published' } - } - dropdownActions={[ - { label: 'Save as Draft', status: 'draft', show: status !== 'draft' } - ]} + primaryAction={status === 'published' + ? { label: 'Save', status: 'published' } + : { label: 'Publish', status: 'published' }} + dropdownActions={[{ label: 'Save as Draft', status: 'draft', show: status !== 'draft' }]} />
    {/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()} - +