We ran the linter

This commit is contained in:
Justin Edmund 2025-06-11 00:54:05 -07:00
parent c6ce13a530
commit 3ba7f6b762
30 changed files with 751 additions and 546 deletions

View file

@ -180,6 +180,7 @@ The system now uses a dedicated `media_usage` table for robust tracking:
``` ```
**Benefits:** **Benefits:**
- Accurate usage tracking across all content types - Accurate usage tracking across all content types
- Efficient queries for usage information - Efficient queries for usage information
- Safe bulk deletion with automatic reference cleanup - Safe bulk deletion with automatic reference cleanup
@ -317,8 +318,15 @@ GET /api/media // Browse with filters, pagination
GET / api / media / [id] // Get single media item GET / api / media / [id] // Get single media item
PUT / api / media / [id] // Update media (alt text, description) PUT / api / media / [id] // Update media (alt text, description)
DELETE / api / media / [id] // Delete single media item DELETE / api / media / [id] // Delete single media item
DELETE /api/media/bulk-delete // Delete multiple media items DELETE / api / media / bulk -
GET /api/media/[id]/usage // Check where media is used 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 / backfill - usage // Backfill usage tracking for existing content
``` ```
@ -755,10 +763,12 @@ Based on requirements discussion:
### Design Decisions Made (May 2024) ### Design Decisions Made (May 2024)
1. **Simplified Post Types**: Reduced from 5 types (blog, microblog, link, photo, album) to 2 types: 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) - **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) - **Essay**: Full editor with title/metadata + attachments (handles previous blog use cases)
2. **Photo Curation Strategy**: Dual-level curation system: 2. **Photo Curation Strategy**: Dual-level curation system:
- **Media Level**: `isPhotography` boolean - stars individual media for photo experience - **Media Level**: `isPhotography` boolean - stars individual media for photo experience
- **Album Level**: `isPhotography` boolean - marks entire albums for photo experience - **Album Level**: `isPhotography` boolean - marks entire albums for photo experience
- **Mixed Content**: Photography albums can contain non-photography media (Option A) - **Mixed Content**: Photography albums can contain non-photography media (Option A)
@ -774,18 +784,21 @@ Based on requirements discussion:
### Implementation Task List ### Implementation Task List
#### Phase 1: Database Updates #### Phase 1: Database Updates
- [x] Create migration to add `isPhotography` field to Media table - [x] Create migration to add `isPhotography` field to Media table
- [x] Create migration to add `isPhotography` field to Album table - [x] Create migration to add `isPhotography` field to Album table
- [x] Update Prisma schema with new fields - [x] Update Prisma schema with new fields
- [x] Test migrations on local database - [x] Test migrations on local database
#### Phase 2: API Updates #### Phase 2: API Updates
- [x] Update Media API endpoints to handle `isPhotography` flag - [x] Update Media API endpoints to handle `isPhotography` flag
- [x] Update Album 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] Update media usage tracking to work with new flags
- [x] Add filtering capabilities for photography content - [x] Add filtering capabilities for photography content
#### Phase 3: Admin Interface Updates #### Phase 3: Admin Interface Updates
- [x] Add photography toggle to MediaDetailsModal - [x] Add photography toggle to MediaDetailsModal
- [x] Add photography indicator pills for media items (grid and list views) - [x] Add photography indicator pills for media items (grid and list views)
- [x] Add photography indicator pills for albums - [x] Add photography indicator pills for albums
@ -793,6 +806,7 @@ Based on requirements discussion:
- [x] Add bulk photography operations (mark/unmark multiple items) - [x] Add bulk photography operations (mark/unmark multiple items)
#### Phase 4: Post Type Simplification #### Phase 4: Post Type Simplification
- [x] Update UniverseComposer to use only "post" and "essay" types - [x] Update UniverseComposer to use only "post" and "essay" types
- [x] Remove complex post type selector UI - [x] Remove complex post type selector UI
- [x] Update post creation flows - [x] Update post creation flows
@ -800,6 +814,7 @@ Based on requirements discussion:
- [x] Update post display logic to handle simplified types - [x] Update post display logic to handle simplified types
#### Phase 5: Album Management System #### Phase 5: Album Management System
- [x] Create album creation/editing interface with photography toggle - [x] Create album creation/editing interface with photography toggle
- [x] Build album list view with photography indicators - [x] Build album list view with photography indicators
- [ ] **Critical Missing Feature: Album Photo Management** - [ ] **Critical Missing Feature: Album Photo Management**
@ -814,6 +829,7 @@ Based on requirements discussion:
- [ ] Add bulk photo upload to albums with automatic photography detection - [ ] Add bulk photo upload to albums with automatic photography detection
#### Phase 6: Photography Experience #### Phase 6: Photography Experience
- [ ] Build photography album filtering in admin - [ ] Build photography album filtering in admin
- [ ] Create photography-focused views and workflows - [ ] Create photography-focused views and workflows
- [ ] Add batch operations for photo curation - [ ] Add batch operations for photo curation

View file

@ -24,12 +24,14 @@ Upgrade the current JSON-based tag system to a relational database model with ad
## Current State vs Target State ## Current State vs Target State
### Current Implementation ### Current Implementation
- Tags stored as JSON arrays: `tags: ['announcement', 'meta', 'cms']` - Tags stored as JSON arrays: `tags: ['announcement', 'meta', 'cms']`
- Simple display-only functionality - Simple display-only functionality
- No querying capabilities - No querying capabilities
- Manual tag input with Add button - Manual tag input with Add button
### Target Implementation ### Target Implementation
- Relational many-to-many tag system - Relational many-to-many tag system
- Full CRUD operations for tags - Full CRUD operations for tags
- Advanced filtering and search - Advanced filtering and search
@ -117,7 +119,9 @@ model Post {
### 1. Tag Management Interface ### 1. Tag Management Interface
#### Admin Tag Manager (`/admin/tags`) #### Admin Tag Manager (`/admin/tags`)
- **Tag List View** - **Tag List View**
- DataTable with tag name, usage count, created date - DataTable with tag name, usage count, created date
- Search and filter capabilities - Search and filter capabilities
- Bulk operations (delete, merge, rename) - Bulk operations (delete, merge, rename)
@ -130,7 +134,9 @@ model Post {
- Merge with other tags functionality - Merge with other tags functionality
#### Tag Analytics Dashboard #### Tag Analytics Dashboard
- **Usage Statistics** - **Usage Statistics**
- Most/least used tags - Most/least used tags
- Tag usage trends over time - Tag usage trends over time
- Orphaned tags (no posts) - Orphaned tags (no posts)
@ -144,6 +150,7 @@ model Post {
### 2. Enhanced Tag Input Component (`TagInput.svelte`) ### 2. Enhanced Tag Input Component (`TagInput.svelte`)
#### Features #### Features
- **Typeahead Search**: Real-time search of existing tags - **Typeahead Search**: Real-time search of existing tags
- **Keyboard Navigation**: Arrow keys to navigate suggestions - **Keyboard Navigation**: Arrow keys to navigate suggestions
- **Instant Add**: Press Enter to add tag without button click - **Instant Add**: Press Enter to add tag without button click
@ -152,6 +159,7 @@ model Post {
- **Quick Actions**: Backspace to remove last tag - **Quick Actions**: Backspace to remove last tag
#### Component API #### Component API
```typescript ```typescript
interface TagInputProps { interface TagInputProps {
tags: string[] | Tag[] // Current tags tags: string[] | Tag[] // Current tags
@ -168,12 +176,13 @@ interface TagInputProps {
``` ```
#### Svelte 5 Implementation #### Svelte 5 Implementation
```svelte ```svelte
<script lang="ts"> <script lang="ts">
let { let {
tags = $bindable([]), tags = $bindable([]),
suggestions = [], suggestions = [],
placeholder = "Add tags...", placeholder = 'Add tags...',
maxTags = 10, maxTags = 10,
allowNew = true, allowNew = true,
size = 'medium', size = 'medium',
@ -190,9 +199,10 @@ interface TagInputProps {
// Filtered suggestions based on input // Filtered suggestions based on input
let filteredSuggestions = $derived( let filteredSuggestions = $derived(
suggestions.filter(tag => suggestions.filter(
(tag) =>
tag.name.toLowerCase().includes(inputValue.toLowerCase()) && tag.name.toLowerCase().includes(inputValue.toLowerCase()) &&
!tags.some(t => t.id === tag.id) !tags.some((t) => t.id === tag.id)
) )
) )
@ -232,11 +242,13 @@ interface TagInputProps {
### 3. Post Filtering by Tags ### 3. Post Filtering by Tags
#### Frontend Components #### Frontend Components
- **Tag Filter Bar**: Multi-select tag filtering - **Tag Filter Bar**: Multi-select tag filtering
- **Tag Cloud**: Visual tag representation with usage counts - **Tag Cloud**: Visual tag representation with usage counts
- **Search Integration**: Combine text search with tag filters - **Search Integration**: Combine text search with tag filters
#### API Endpoints #### API Endpoints
```typescript ```typescript
// GET /api/posts?tags=javascript,react&operation=AND // GET /api/posts?tags=javascript,react&operation=AND
// GET /api/posts?tags=design,ux&operation=OR // GET /api/posts?tags=design,ux&operation=OR
@ -262,15 +274,21 @@ interface TagSuggestResponse {
### 4. Related Posts Feature ### 4. Related Posts Feature
#### Implementation #### Implementation
- **Algorithm**: Find posts sharing the most tags - **Algorithm**: Find posts sharing the most tags
- **Weighting**: Consider tag importance and recency - **Weighting**: Consider tag importance and recency
- **Exclusions**: Don't show current post in related list - **Exclusions**: Don't show current post in related list
- **Limit**: Show 3-6 related posts maximum - **Limit**: Show 3-6 related posts maximum
#### Component (`RelatedPosts.svelte`) #### Component (`RelatedPosts.svelte`)
```svelte ```svelte
<script lang="ts"> <script lang="ts">
let { postId, tags, limit = 4 }: { let {
postId,
tags,
limit = 4
}: {
postId: number postId: number
tags: Tag[] tags: Tag[]
limit?: number limit?: number
@ -279,8 +297,10 @@ interface TagSuggestResponse {
let relatedPosts = $state<Post[]>([]) let relatedPosts = $state<Post[]>([])
$effect(async () => { $effect(async () => {
const tagIds = tags.map(t => t.id) const tagIds = tags.map((t) => t.id)
const response = await fetch(`/api/posts/related?postId=${postId}&tagIds=${tagIds.join(',')}&limit=${limit}`) const response = await fetch(
`/api/posts/related?postId=${postId}&tagIds=${tagIds.join(',')}&limit=${limit}`
)
relatedPosts = await response.json() relatedPosts = await response.json()
}) })
</script> </script>
@ -365,6 +385,7 @@ interface UpdatePostTagsRequest {
### 1. TagInput Component Features ### 1. TagInput Component Features
#### Visual States #### Visual States
- **Default**: Clean input with placeholder - **Default**: Clean input with placeholder
- **Focused**: Show suggestions dropdown - **Focused**: Show suggestions dropdown
- **Typing**: Filter and highlight matches - **Typing**: Filter and highlight matches
@ -373,6 +394,7 @@ interface UpdatePostTagsRequest {
- **Full**: Disable input when max tags reached - **Full**: Disable input when max tags reached
#### Accessibility #### Accessibility
- **ARIA Labels**: Proper labeling for screen readers - **ARIA Labels**: Proper labeling for screen readers
- **Keyboard Navigation**: Full keyboard accessibility - **Keyboard Navigation**: Full keyboard accessibility
- **Focus Management**: Logical tab order - **Focus Management**: Logical tab order
@ -381,6 +403,7 @@ interface UpdatePostTagsRequest {
### 2. Tag Display Components ### 2. Tag Display Components
#### TagPill Component #### TagPill Component
```svelte ```svelte
<script lang="ts"> <script lang="ts">
let { let {
@ -412,15 +435,10 @@ interface UpdatePostTagsRequest {
``` ```
#### TagCloud Component #### TagCloud Component
```svelte ```svelte
<script lang="ts"> <script lang="ts">
let { let { tags, maxTags = 50, minFontSize = 12, maxFontSize = 24, onClick }: TagCloudProps = $props()
tags,
maxTags = 50,
minFontSize = 12,
maxFontSize = 24,
onClick
}: TagCloudProps = $props()
// Calculate font sizes based on usage // Calculate font sizes based on usage
let tagSizes = $derived(calculateTagSizes(tags, minFontSize, maxFontSize)) let tagSizes = $derived(calculateTagSizes(tags, minFontSize, maxFontSize))
@ -430,12 +448,14 @@ interface UpdatePostTagsRequest {
### 3. Admin Interface Updates ### 3. Admin Interface Updates
#### Posts List with Tag Filtering #### Posts List with Tag Filtering
- **Filter Bar**: Multi-select tag filter above posts list - **Filter Bar**: Multi-select tag filter above posts list
- **Tag Pills**: Show tags on each post item - **Tag Pills**: Show tags on each post item
- **Quick Filter**: Click tag to filter by that tag - **Quick Filter**: Click tag to filter by that tag
- **Clear Filters**: Easy way to reset all filters - **Clear Filters**: Easy way to reset all filters
#### Posts Edit Form Integration #### Posts Edit Form Integration
- **Replace Current**: Swap existing tag input with new TagInput - **Replace Current**: Swap existing tag input with new TagInput
- **Preserve UX**: Maintain current metadata popover - **Preserve UX**: Maintain current metadata popover
- **Tag Management**: Quick access to create/edit tags - **Tag Management**: Quick access to create/edit tags
@ -443,12 +463,15 @@ interface UpdatePostTagsRequest {
## Migration Strategy ## Migration Strategy
### Phase 1: Database Migration (Week 1) ### Phase 1: Database Migration (Week 1)
1. **Create Migration Script** 1. **Create Migration Script**
- Create new tables (tags, post_tags) - Create new tables (tags, post_tags)
- Migrate existing JSON tags to relational format - Migrate existing JSON tags to relational format
- Create indexes for performance - Create indexes for performance
2. **Data Migration** 2. **Data Migration**
- Extract unique tags from existing posts - Extract unique tags from existing posts
- Create tag records with auto-generated slugs - Create tag records with auto-generated slugs
- Create post_tag relationships - Create post_tag relationships
@ -459,12 +482,15 @@ interface UpdatePostTagsRequest {
- Dual-write to both systems during transition - Dual-write to both systems during transition
### Phase 2: API Development (Week 1-2) ### Phase 2: API Development (Week 1-2)
1. **Tag Management APIs** 1. **Tag Management APIs**
- CRUD operations for tags - CRUD operations for tags
- Tag suggestions and search - Tag suggestions and search
- Analytics endpoints - Analytics endpoints
2. **Enhanced Post APIs** 2. **Enhanced Post APIs**
- Update post endpoints for relational tags - Update post endpoints for relational tags
- Related posts algorithm - Related posts algorithm
- Tag filtering capabilities - Tag filtering capabilities
@ -475,12 +501,15 @@ interface UpdatePostTagsRequest {
- Data consistency checks - Data consistency checks
### Phase 3: Frontend Components (Week 2-3) ### Phase 3: Frontend Components (Week 2-3)
1. **Core Components** 1. **Core Components**
- TagInput with typeahead - TagInput with typeahead
- TagPill and TagCloud - TagPill and TagCloud
- Tag management interface - Tag management interface
2. **Integration** 2. **Integration**
- Update MetadataPopover - Update MetadataPopover
- Add tag filtering to posts list - Add tag filtering to posts list
- Implement related posts component - Implement related posts component
@ -491,12 +520,15 @@ interface UpdatePostTagsRequest {
- Bulk operations interface - Bulk operations interface
### Phase 4: Features & Polish (Week 3-4) ### Phase 4: Features & Polish (Week 3-4)
1. **Advanced Features** 1. **Advanced Features**
- Tag merging functionality - Tag merging functionality
- Usage analytics - Usage analytics
- Tag suggestions based on content - Tag suggestions based on content
2. **Performance Optimization** 2. **Performance Optimization**
- Query optimization - Query optimization
- Caching strategies - Caching strategies
- Load testing - Load testing
@ -509,21 +541,25 @@ interface UpdatePostTagsRequest {
## Success Metrics ## Success Metrics
### Performance ### Performance
- Tag search responses under 50ms - Tag search responses under 50ms
- Post filtering responses under 100ms - Post filtering responses under 100ms
- Page load times maintained or improved - Page load times maintained or improved
### Usability ### Usability
- Reduced clicks to add tags (eliminate Add button) - Reduced clicks to add tags (eliminate Add button)
- Faster tag input with typeahead - Faster tag input with typeahead
- Improved content discovery through related posts - Improved content discovery through related posts
### Content Management ### Content Management
- Ability to merge duplicate tags - Ability to merge duplicate tags
- Insights into tag usage patterns - Insights into tag usage patterns
- Better content organization capabilities - Better content organization capabilities
### Analytics ### Analytics
- Track tag usage growth over time - Track tag usage growth over time
- Identify content gaps through tag analysis - Identify content gaps through tag analysis
- Measure impact on content engagement - Measure impact on content engagement
@ -531,18 +567,21 @@ interface UpdatePostTagsRequest {
## Technical Considerations ## Technical Considerations
### Performance ### Performance
- **Database Indexes**: Proper indexing on tag names and relationships - **Database Indexes**: Proper indexing on tag names and relationships
- **Query Optimization**: Efficient joins for tag filtering - **Query Optimization**: Efficient joins for tag filtering
- **Caching**: Cache popular tag lists and related posts - **Caching**: Cache popular tag lists and related posts
- **Pagination**: Handle large tag lists efficiently - **Pagination**: Handle large tag lists efficiently
### Data Integrity ### Data Integrity
- **Constraints**: Prevent duplicate tag names - **Constraints**: Prevent duplicate tag names
- **Cascading Deletes**: Properly handle tag/post deletions - **Cascading Deletes**: Properly handle tag/post deletions
- **Validation**: Ensure tag names follow naming conventions - **Validation**: Ensure tag names follow naming conventions
- **Backup Strategy**: Safe migration with rollback capability - **Backup Strategy**: Safe migration with rollback capability
### User Experience ### User Experience
- **Progressive Enhancement**: Graceful degradation if JS fails - **Progressive Enhancement**: Graceful degradation if JS fails
- **Loading States**: Smooth loading indicators - **Loading States**: Smooth loading indicators
- **Error Handling**: Clear error messages for users - **Error Handling**: Clear error messages for users
@ -551,12 +590,14 @@ interface UpdatePostTagsRequest {
## Future Enhancements ## Future Enhancements
### Advanced Features (Post-MVP) ### Advanced Features (Post-MVP)
- **Hierarchical Tags**: Parent/child tag relationships - **Hierarchical Tags**: Parent/child tag relationships
- **Tag Synonyms**: Alternative names for the same concept - **Tag Synonyms**: Alternative names for the same concept
- **Auto-tagging**: ML-based tag suggestions from content - **Auto-tagging**: ML-based tag suggestions from content
- **Tag Templates**: Predefined tag sets for different content types - **Tag Templates**: Predefined tag sets for different content types
### Integrations ### Integrations
- **External APIs**: Import tags from external sources - **External APIs**: Import tags from external sources
- **Search Integration**: Enhanced search with tag faceting - **Search Integration**: Enhanced search with tag faceting
- **Analytics**: Deep tag performance analytics - **Analytics**: Deep tag performance analytics
@ -565,11 +606,13 @@ interface UpdatePostTagsRequest {
## Risk Assessment ## Risk Assessment
### High Risk ### High Risk
- **Data Migration**: Complex migration of existing tag data - **Data Migration**: Complex migration of existing tag data
- **Performance Impact**: New queries might affect page load times - **Performance Impact**: New queries might affect page load times
- **User Adoption**: Users need to learn new tag input interface - **User Adoption**: Users need to learn new tag input interface
### Mitigation Strategies ### Mitigation Strategies
- **Staged Rollout**: Deploy to staging first, then gradual production rollout - **Staged Rollout**: Deploy to staging first, then gradual production rollout
- **Performance Monitoring**: Continuous monitoring during migration - **Performance Monitoring**: Continuous monitoring during migration
- **User Training**: Clear documentation and smooth UX transitions - **User Training**: Clear documentation and smooth UX transitions
@ -578,6 +621,7 @@ interface UpdatePostTagsRequest {
## Success Criteria ## Success Criteria
### Must Have ### Must Have
- ✅ All existing tags migrated successfully - ✅ All existing tags migrated successfully
- ✅ Tag input works with keyboard-only navigation - ✅ Tag input works with keyboard-only navigation
- ✅ Posts can be filtered by single or multiple tags - ✅ Posts can be filtered by single or multiple tags
@ -585,12 +629,14 @@ interface UpdatePostTagsRequest {
- ✅ Performance remains acceptable (< 100ms for most operations) - ✅ Performance remains acceptable (< 100ms for most operations)
### Should Have ### Should Have
- ✅ Tag management interface for admins - ✅ Tag management interface for admins
- ✅ Tag usage analytics and insights - ✅ Tag usage analytics and insights
- ✅ Ability to merge duplicate tags - ✅ Ability to merge duplicate tags
- ✅ Tag color coding and visual improvements - ✅ Tag color coding and visual improvements
### Could Have ### Could Have
- Tag auto-suggestions based on post content - Tag auto-suggestions based on post content
- Tag trending and popularity metrics - Tag trending and popularity metrics
- Advanced tag analytics and reporting - Advanced tag analytics and reporting

View file

@ -1,20 +1,24 @@
# PRD: Interactive Project Headers # PRD: Interactive Project Headers
## Overview ## 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. 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 ## Goals
- Create engaging, project-specific header experiences - Create engaging, project-specific header experiences
- Maintain simplicity in implementation and admin UI - Maintain simplicity in implementation and admin UI
- Allow for creative freedom while keeping the system maintainable - Allow for creative freedom while keeping the system maintainable
- Provide a path for adding new header types over time - Provide a path for adding new header types over time
## Implementation Strategy ## 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. 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 ## Technical Implementation
### 1. Database Schema Update ### 1. Database Schema Update
Add a `headerType` field to the Project model in `prisma/schema.prisma`: Add a `headerType` field to the Project model in `prisma/schema.prisma`:
```prisma ```prisma
@ -25,6 +29,7 @@ model Project {
``` ```
### 2. Component Structure ### 2. Component Structure
Create a new directory structure for header components: Create a new directory structure for header components:
``` ```
@ -37,25 +42,26 @@ src/lib/components/headers/
``` ```
### 3. ProjectHeader Component (Switcher) ### 3. ProjectHeader Component (Switcher)
The main component that switches between different header types: The main component that switches between different header types:
```svelte ```svelte
<!-- ProjectHeader.svelte --> <!-- ProjectHeader.svelte -->
<script> <script>
import LogoOnBackgroundHeader from './LogoOnBackgroundHeader.svelte'; import LogoOnBackgroundHeader from './LogoOnBackgroundHeader.svelte'
import PinterestHeader from './PinterestHeader.svelte'; import PinterestHeader from './PinterestHeader.svelte'
import MaitsuHeader from './MaitsuHeader.svelte'; import MaitsuHeader from './MaitsuHeader.svelte'
let { project } = $props(); let { project } = $props()
const headers = { const headers = {
logoOnBackground: LogoOnBackgroundHeader, logoOnBackground: LogoOnBackgroundHeader,
pinterest: PinterestHeader, pinterest: PinterestHeader,
maitsu: MaitsuHeader, maitsu: MaitsuHeader
// Add more as needed // Add more as needed
}; }
const HeaderComponent = headers[project.headerType] || LogoOnBackgroundHeader; const HeaderComponent = headers[project.headerType] || LogoOnBackgroundHeader
</script> </script>
{#if project.headerType !== 'none'} {#if project.headerType !== 'none'}
@ -64,9 +70,11 @@ The main component that switches between different header types:
``` ```
### 4. Update Project Detail Page ### 4. Update Project Detail Page
Modify `/routes/work/[slug]/+page.svelte` to use the new header system instead of the current static header. Modify `/routes/work/[slug]/+page.svelte` to use the new header system instead of the current static header.
### 5. Admin UI Integration ### 5. Admin UI Integration
Add a select field to the project form components: Add a select field to the project form components:
```svelte ```svelte
@ -74,7 +82,7 @@ Add a select field to the project form components:
label="Header Type" label="Header Type"
name="headerType" name="headerType"
value={formData.headerType} value={formData.headerType}
onchange={(e) => formData.headerType = e.target.value} onchange={(e) => (formData.headerType = e.target.value)}
> >
<option value="none">No Header</option> <option value="none">No Header</option>
<option value="logoOnBackground">Logo on Background (Default)</option> <option value="logoOnBackground">Logo on Background (Default)</option>
@ -86,17 +94,20 @@ Add a select field to the project form components:
## Header Type Specifications ## Header Type Specifications
### LogoOnBackgroundHeader (Default) ### LogoOnBackgroundHeader (Default)
- Current behavior: centered logo with title and subtitle - Current behavior: centered logo with title and subtitle
- Uses project's `logoUrl`, `backgroundColor`, and text - Uses project's `logoUrl`, `backgroundColor`, and text
- Simple, clean presentation - Simple, clean presentation
### PinterestHeader ### PinterestHeader
- Interactive grid of Pinterest-style cards - Interactive grid of Pinterest-style cards
- Cards rearrange/animate on hover - Cards rearrange/animate on hover
- Could pull from project gallery or use custom assets - Could pull from project gallery or use custom assets
- Red color scheme matching Pinterest brand - Red color scheme matching Pinterest brand
### MaitsuHeader ### MaitsuHeader
- Japanese-inspired animations - Japanese-inspired animations
- Could feature: - Could feature:
- Animated kanji/hiragana characters - Animated kanji/hiragana characters
@ -105,11 +116,14 @@ Add a select field to the project form components:
- Uses project colors for theming - Uses project colors for theming
### None ### None
- No header displayed - No header displayed
- Project content starts immediately - Project content starts immediately
## Data Available to Headers ## Data Available to Headers
Each header component receives the full project object with access to: Each header component receives the full project object with access to:
- `project.logoUrl` - Project logo - `project.logoUrl` - Project logo
- `project.backgroundColor` - Primary background color - `project.backgroundColor` - Primary background color
- `project.highlightColor` - Accent color - `project.highlightColor` - Accent color
@ -121,12 +135,14 @@ Each header component receives the full project object with access to:
## Future Considerations ## Future Considerations
### Potential Additional Header Types ### Potential Additional Header Types
- **SlackHeader**: Animated emoji reactions floating up - **SlackHeader**: Animated emoji reactions floating up
- **FigmaHeader**: Interactive design tools/cursors - **FigmaHeader**: Interactive design tools/cursors
- **TypegraphicaHeader**: Kinetic typography animations - **TypegraphicaHeader**: Kinetic typography animations
- **Custom**: Allow arbitrary component code (requires security considerations) - **Custom**: Allow arbitrary component code (requires security considerations)
### Possible Enhancements ### Possible Enhancements
1. **Configuration Options**: Add a `headerConfig` JSON field for component-specific settings 1. **Configuration Options**: Add a `headerConfig` JSON field for component-specific settings
2. **Asset Management**: Dedicated header assets separate from project gallery 2. **Asset Management**: Dedicated header assets separate from project gallery
3. **Responsive Behaviors**: Different animations for mobile vs desktop 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 5. **A/B Testing**: Support multiple headers per project for testing
## Implementation Steps ## Implementation Steps
1. Add `headerType` field to Prisma schema 1. Add `headerType` field to Prisma schema
2. Create database migration 2. Create database migration
3. Create base `ProjectHeader` switcher component 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 9. Document how to add new header types
## Success Criteria ## Success Criteria
- Projects can select from multiple header types via admin UI - Projects can select from multiple header types via admin UI
- Each header type provides a unique, engaging experience - Each header type provides a unique, engaging experience
- System is extensible for adding new headers - System is extensible for adding new headers
@ -153,6 +171,7 @@ Each header component receives the full project object with access to:
- Clean separation between header components - Clean separation between header components
## Technical Notes ## Technical Notes
- Use Svelte 5 runes syntax (`$props()`, `$state()`, etc.) - Use Svelte 5 runes syntax (`$props()`, `$state()`, etc.)
- Leverage existing animation patterns (spring physics, CSS transitions) - Leverage existing animation patterns (spring physics, CSS transitions)
- Follow established SCSS variable system - Follow established SCSS variable system

View file

@ -5,6 +5,7 @@
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.** 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 ### 🏆 Major Achievements
- **✅ Complete MediaLibraryModal system** with single/multiple selection - **✅ Complete MediaLibraryModal system** with single/multiple selection
- **✅ Enhanced upload components** (ImageUploader, GalleryUploader) with MediaLibraryModal integration - **✅ Enhanced upload components** (ImageUploader, GalleryUploader) with MediaLibraryModal integration
- **✅ Full form integration** across projects, posts, albums, and editor - **✅ 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 ## Goals
### Primary Goals (Direct Upload Workflow) ### Primary Goals (Direct Upload Workflow)
- **Enable direct file upload within forms** where content will be used (projects, posts, albums) - **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 - **Provide immediate upload and preview** without requiring navigation to separate media management
- **Store comprehensive metadata** including alt text for accessibility and SEO - **Store comprehensive metadata** including alt text for accessibility and SEO
- **Support drag-and-drop and click-to-browse** for intuitive file selection - **Support drag-and-drop and click-to-browse** for intuitive file selection
### Secondary Goals (Media Library Browser) ### Secondary Goals (Media Library Browser)
- Create a reusable media browser for **selecting previously uploaded content** - Create a reusable media browser for **selecting previously uploaded content**
- Provide **media management interface** showing where files are referenced - Provide **media management interface** showing where files are referenced
- Enable **bulk operations** and **metadata editing** (especially alt text) - Enable **bulk operations** and **metadata editing** (especially alt text)
- Support **file organization** and **usage tracking** - Support **file organization** and **usage tracking**
### Technical Goals ### Technical Goals
- Maintain consistent UX across all media interactions - Maintain consistent UX across all media interactions
- Support different file type filtering based on context - Support different file type filtering based on context
- Integrate seamlessly with existing admin components - 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 ## Current State Analysis
### ✅ What We Have ### ✅ What We Have
- Complete media API (`/api/media`, `/api/media/upload`, `/api/media/bulk-upload`) - Complete media API (`/api/media`, `/api/media/upload`, `/api/media/bulk-upload`)
- Media management page with grid/list views and search/filtering - Media management page with grid/list views and search/filtering
- Modal base component (`Modal.svelte`) - Modal base component (`Modal.svelte`)
@ -62,17 +67,20 @@ Implement a comprehensive Media Library modal system that provides a unified int
### 🎯 What We Need ### 🎯 What We Need
#### High Priority (Remaining Tasks) #### High Priority (Remaining Tasks)
- **Enhanced upload features** with drag & drop zones in all upload components - **Enhanced upload features** with drag & drop zones in all upload components
- **Bulk alt text editing** in Media Library for existing content - **Bulk alt text editing** in Media Library for existing content
- **Usage tracking display** showing where media is referenced - **Usage tracking display** showing where media is referenced
- **Performance optimizations** for large media libraries - **Performance optimizations** for large media libraries
#### Medium Priority (Polish & Advanced Features) #### Medium Priority (Polish & Advanced Features)
- **Image optimization options** during upload - **Image optimization options** during upload
- **Advanced search capabilities** (by alt text, usage, etc.) - **Advanced search capabilities** (by alt text, usage, etc.)
- **Bulk operations** (delete multiple, bulk metadata editing) - **Bulk operations** (delete multiple, bulk metadata editing)
#### Low Priority (Future Enhancements) #### Low Priority (Future Enhancements)
- **AI-powered alt text suggestions** - **AI-powered alt text suggestions**
- **Duplicate detection** and management - **Duplicate detection** and management
- **Advanced analytics** and usage reporting - **Advanced analytics** and usage reporting
@ -80,6 +88,7 @@ Implement a comprehensive Media Library modal system that provides a unified int
## Workflow Priorities ## Workflow Priorities
### 🥇 Primary Workflow: Direct Upload in Forms ### 🥇 Primary Workflow: Direct Upload in Forms
This is the **main workflow** that users will use 90% of the time: This is the **main workflow** that users will use 90% of the time:
1. **User creates content** (project, post, album) 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 5. **Content is saved** with proper media references
**Key Components**: **Key Components**:
- `ImageUploader` - Direct drag-and-drop/click upload with preview - `ImageUploader` - Direct drag-and-drop/click upload with preview
- `GalleryUploader` - Multiple file upload with immediate gallery preview - `GalleryUploader` - Multiple file upload with immediate gallery preview
- `MediaMetadataForm` - Alt text and description capture during upload - `MediaMetadataForm` - Alt text and description capture during upload
### 🥈 Secondary Workflow: Browse Existing Media ### 🥈 Secondary Workflow: Browse Existing Media
This workflow is for **reusing previously uploaded content**: This workflow is for **reusing previously uploaded content**:
1. **User needs to select existing media** (rare case) 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** 5. **Media references are updated**
**Key Components**: **Key Components**:
- `MediaLibraryModal` - Browse and select existing media - `MediaLibraryModal` - Browse and select existing media
- `MediaSelector` - Grid interface for selection - `MediaSelector` - Grid interface for selection
- `MediaManager` - Edit alt text and view usage - `MediaManager` - Edit alt text and view usage
@ -112,6 +124,7 @@ This workflow is for **reusing previously uploaded content**:
### 1. Enhanced Upload Components (Primary) ### 1. Enhanced Upload Components (Primary)
#### ImageUploader Component #### ImageUploader Component
**Purpose**: Direct image upload with immediate preview and metadata capture **Purpose**: Direct image upload with immediate preview and metadata capture
```typescript ```typescript
@ -128,6 +141,7 @@ interface ImageUploaderProps {
``` ```
**Features**: **Features**:
- Drag-and-drop upload zone with visual feedback - Drag-and-drop upload zone with visual feedback
- Click to browse files from computer - Click to browse files from computer
- Immediate image preview with proper aspect ratio - Immediate image preview with proper aspect ratio
@ -137,6 +151,7 @@ interface ImageUploaderProps {
- Replace/remove functionality - Replace/remove functionality
#### GalleryUploader Component #### GalleryUploader Component
**Purpose**: Multiple file upload with gallery preview and reordering **Purpose**: Multiple file upload with gallery preview and reordering
```typescript ```typescript
@ -153,6 +168,7 @@ interface GalleryUploaderProps {
``` ```
**Features**: **Features**:
- Multiple file drag-and-drop - Multiple file drag-and-drop
- Immediate gallery preview grid - Immediate gallery preview grid
- Individual alt text inputs for each image - Individual alt text inputs for each image
@ -165,6 +181,7 @@ interface GalleryUploaderProps {
**Purpose**: Main modal component that wraps the media browser functionality **Purpose**: Main modal component that wraps the media browser functionality
**Props Interface**: **Props Interface**:
```typescript ```typescript
interface MediaLibraryModalProps { interface MediaLibraryModalProps {
isOpen: boolean isOpen: boolean
@ -179,6 +196,7 @@ interface MediaLibraryModalProps {
``` ```
**Features**: **Features**:
- Modal overlay with proper focus management - Modal overlay with proper focus management
- Header with title and close button - Header with title and close button
- Media browser grid with selection indicators - Media browser grid with selection indicators
@ -192,6 +210,7 @@ interface MediaLibraryModalProps {
**Purpose**: The actual media browsing interface within the modal **Purpose**: The actual media browsing interface within the modal
**Features**: **Features**:
- Grid layout with thumbnail previews - Grid layout with thumbnail previews
- Individual item selection with visual feedback - Individual item selection with visual feedback
- Keyboard navigation support - Keyboard navigation support
@ -199,6 +218,7 @@ interface MediaLibraryModalProps {
- "Select All" / "Clear Selection" bulk actions (for multiple mode) - "Select All" / "Clear Selection" bulk actions (for multiple mode)
**Item Display**: **Item Display**:
- Thumbnail image - Thumbnail image
- Filename (truncated) - Filename (truncated)
- File size and dimensions - File size and dimensions
@ -210,6 +230,7 @@ interface MediaLibraryModalProps {
**Purpose**: Handle file uploads within the modal **Purpose**: Handle file uploads within the modal
**Features**: **Features**:
- Drag-and-drop upload zone - Drag-and-drop upload zone
- Click to browse files - Click to browse files
- Upload progress indicators - Upload progress indicators
@ -218,6 +239,7 @@ interface MediaLibraryModalProps {
- Automatic refresh of media grid after upload - Automatic refresh of media grid after upload
**Validation**: **Validation**:
- File type restrictions based on context - File type restrictions based on context
- File size limits (10MB per file) - File size limits (10MB per file)
- Maximum number of files for bulk upload - Maximum number of files for bulk upload
@ -225,6 +247,7 @@ interface MediaLibraryModalProps {
### 4. Form Integration Components ### 4. Form Integration Components
#### MediaInput Component #### MediaInput Component
**Purpose**: Generic input field that opens media library modal **Purpose**: Generic input field that opens media library modal
```typescript ```typescript
@ -241,6 +264,7 @@ interface MediaInputProps {
``` ```
**Display**: **Display**:
- Label and optional required indicator - Label and optional required indicator
- Preview of selected media (thumbnail + filename) - Preview of selected media (thumbnail + filename)
- "Browse" button to open modal - "Browse" button to open modal
@ -248,6 +272,7 @@ interface MediaInputProps {
- Error state display - Error state display
#### ImagePicker Component #### ImagePicker Component
**Purpose**: Specialized single image selector with enhanced preview **Purpose**: Specialized single image selector with enhanced preview
```typescript ```typescript
@ -263,12 +288,14 @@ interface ImagePickerProps {
``` ```
**Display**: **Display**:
- Large preview area with placeholder - Large preview area with placeholder
- Image preview with proper aspect ratio - Image preview with proper aspect ratio
- Overlay with "Change" and "Remove" buttons on hover - Overlay with "Change" and "Remove" buttons on hover
- Upload progress indicator - Upload progress indicator
#### GalleryManager Component #### GalleryManager Component
**Purpose**: Multiple image selection with drag-and-drop reordering **Purpose**: Multiple image selection with drag-and-drop reordering
```typescript ```typescript
@ -284,6 +311,7 @@ interface GalleryManagerProps {
``` ```
**Display**: **Display**:
- Grid of selected images with reorder handles - Grid of selected images with reorder handles
- "Add Images" button to open modal - "Add Images" button to open modal
- Individual remove buttons on each image - Individual remove buttons on each image
@ -294,6 +322,7 @@ interface GalleryManagerProps {
### 🥇 Primary Flow: Direct Upload in Forms ### 🥇 Primary Flow: Direct Upload in Forms
#### 1. Single Image Upload (Project Featured Image) #### 1. Single Image Upload (Project Featured Image)
1. **User creates/edits project** and reaches featured image field 1. **User creates/edits project** and reaches featured image field
2. **User drags image file** directly onto ImageUploader component OR clicks to browse 2. **User drags image file** directly onto ImageUploader component OR clicks to browse
3. **File is immediately uploaded** with progress indicator 3. **File is immediately uploaded** with progress indicator
@ -303,6 +332,7 @@ interface GalleryManagerProps {
7. **Form can be saved** with media reference and metadata 7. **Form can be saved** with media reference and metadata
#### 2. Multiple Image Upload (Project Gallery) #### 2. Multiple Image Upload (Project Gallery)
1. **User reaches gallery section** of project form 1. **User reaches gallery section** of project form
2. **User drags multiple files** onto GalleryUploader OR clicks to browse multiple 2. **User drags multiple files** onto GalleryUploader OR clicks to browse multiple
3. **Upload progress shown** for each file individually 3. **Upload progress shown** for each file individually
@ -313,6 +343,7 @@ interface GalleryManagerProps {
8. **Form saves** with complete gallery and metadata 8. **Form saves** with complete gallery and metadata
#### 3. Media Management and Alt Text Editing #### 3. Media Management and Alt Text Editing
1. **User visits Media Library page** to manage uploaded content 1. **User visits Media Library page** to manage uploaded content
2. **User clicks on any media item** to open details modal 2. **User clicks on any media item** to open details modal
3. **User can edit alt text** and other metadata 3. **User can edit alt text** and other metadata
@ -322,6 +353,7 @@ interface GalleryManagerProps {
### 🥈 Secondary Flow: Browse Existing Media ### 🥈 Secondary Flow: Browse Existing Media
#### 1. Selecting Previously Uploaded Image #### 1. Selecting Previously Uploaded Image
1. **User clicks "Browse Library"** button (secondary option in forms) 1. **User clicks "Browse Library"** button (secondary option in forms)
2. **MediaLibraryModal opens** showing all previously uploaded media 2. **MediaLibraryModal opens** showing all previously uploaded media
3. **User browses or searches** existing content 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 5. **Modal closes** and form shows selected media with existing alt text
#### 2. Managing Media Library #### 2. Managing Media Library
1. **User visits dedicated Media Library page** 1. **User visits dedicated Media Library page**
2. **User can view all uploaded media** in grid/list format 2. **User can view all uploaded media** in grid/list format
3. **User can edit metadata** including alt text for any media 3. **User can edit metadata** including alt text for any media
@ -338,12 +371,14 @@ interface GalleryManagerProps {
## Design Specifications ## Design Specifications
### Modal Layout ### Modal Layout
- **Width**: 1200px max, responsive on smaller screens - **Width**: 1200px max, responsive on smaller screens
- **Height**: 80vh max with scroll - **Height**: 80vh max with scroll
- **Grid**: 4-6 columns depending on screen size - **Grid**: 4-6 columns depending on screen size
- **Item Size**: 180px × 140px thumbnails - **Item Size**: 180px × 140px thumbnails
### Visual States ### Visual States
- **Default**: Border with subtle background - **Default**: Border with subtle background
- **Selected**: Blue border and checkmark overlay - **Selected**: Blue border and checkmark overlay
- **Hover**: Slight scale and shadow effect - **Hover**: Slight scale and shadow effect
@ -351,6 +386,7 @@ interface GalleryManagerProps {
- **Upload**: Progress overlay with percentage - **Upload**: Progress overlay with percentage
### Colors (Using Existing Variables) ### Colors (Using Existing Variables)
- **Selection**: `$blue-60` for selected state - **Selection**: `$blue-60` for selected state
- **Hover**: `$grey-10` background - **Hover**: `$grey-10` background
- **Upload Progress**: `$green-60` for success, `$red-60` for error - **Upload Progress**: `$green-60` for success, `$red-60` for error
@ -358,17 +394,20 @@ interface GalleryManagerProps {
## API Integration ## API Integration
### Endpoints Used ### Endpoints Used
- `GET /api/media` - Browse media with search/filter/pagination - `GET /api/media` - Browse media with search/filter/pagination
- `POST /api/media/upload` - Single file upload - `POST /api/media/upload` - Single file upload
- `POST /api/media/bulk-upload` - Multiple file upload - `POST /api/media/bulk-upload` - Multiple file upload
### Search and Filtering ### Search and Filtering
- **Search**: By filename (case-insensitive) - **Search**: By filename (case-insensitive)
- **Filter by Type**: image/*, video/*, all - **Filter by Type**: image/_, video/_, all
- **Filter by Usage**: unused only, all - **Filter by Usage**: unused only, all
- **Sort**: Most recent first - **Sort**: Most recent first
### Pagination ### Pagination
- 24 items per page - 24 items per page
- Infinite scroll or traditional pagination - Infinite scroll or traditional pagination
- Loading states during page changes - Loading states during page changes
@ -376,7 +415,9 @@ interface GalleryManagerProps {
## Implementation Plan ## Implementation Plan
### ✅ Phase 1: Database Schema Updates (COMPLETED) ### ✅ Phase 1: Database Schema Updates (COMPLETED)
1. **✅ Alt Text Support** 1. **✅ Alt Text Support**
- Database schema includes `altText` and `description` fields - Database schema includes `altText` and `description` fields
- API endpoints support alt text in upload and update operations - API endpoints support alt text in upload and update operations
@ -385,13 +426,16 @@ interface GalleryManagerProps {
- Need dedicated tracking table for comprehensive usage analytics - Need dedicated tracking table for comprehensive usage analytics
### ✅ Phase 2: Direct Upload Components (COMPLETED) ### ✅ Phase 2: Direct Upload Components (COMPLETED)
1. **✅ ImageUploader Component** 1. **✅ ImageUploader Component**
- Drag-and-drop upload zone with visual feedback - Drag-and-drop upload zone with visual feedback
- Immediate upload and preview functionality - Immediate upload and preview functionality
- Alt text input integration - Alt text input integration
- MediaLibraryModal integration as secondary option - MediaLibraryModal integration as secondary option
2. **✅ GalleryUploader Component** 2. **✅ GalleryUploader Component**
- Multiple file drag-and-drop support - Multiple file drag-and-drop support
- Individual alt text inputs per image - Individual alt text inputs per image
- Drag-and-drop reordering functionality - Drag-and-drop reordering functionality
@ -404,7 +448,9 @@ interface GalleryManagerProps {
- Batch uploads with individual alt text support - Batch uploads with individual alt text support
### ✅ Phase 3: Form Integration (COMPLETED) ### ✅ Phase 3: Form Integration (COMPLETED)
1. **✅ Project Forms Enhancement** 1. **✅ Project Forms Enhancement**
- Logo field enhanced with ImageUploader + Browse Library - Logo field enhanced with ImageUploader + Browse Library
- Featured image support with ImageUploader - Featured image support with ImageUploader
- Gallery section implemented with GalleryUploader - Gallery section implemented with GalleryUploader
@ -417,7 +463,9 @@ interface GalleryManagerProps {
- Enhanced Edra editor with inline image/gallery support - Enhanced Edra editor with inline image/gallery support
### ✅ Phase 4: Media Library Management (MOSTLY COMPLETED) ### ✅ Phase 4: Media Library Management (MOSTLY COMPLETED)
1. **✅ Enhanced Media Library Page** 1. **✅ Enhanced Media Library Page**
- Alt text editing for existing media via MediaDetailsModal - Alt text editing for existing media via MediaDetailsModal
- Clickable media items with edit functionality - Clickable media items with edit functionality
- Grid and list view toggles - Grid and list view toggles
@ -431,7 +479,9 @@ interface GalleryManagerProps {
### 🎯 Phase 5: Remaining Enhancements (CURRENT PRIORITIES) ### 🎯 Phase 5: Remaining Enhancements (CURRENT PRIORITIES)
#### 🔥 High Priority (Next Sprint) #### 🔥 High Priority (Next Sprint)
1. **Enhanced Media Library Features** 1. **Enhanced Media Library Features**
- **Bulk alt text editing** - Select multiple media items and edit alt text in batch - **Bulk alt text editing** - Select multiple media items and edit alt text in batch
- **Usage tracking display** - Show where each media item is referenced - **Usage tracking display** - Show where each media item is referenced
- **Advanced drag & drop zones** - More intuitive upload areas in all components - **Advanced drag & drop zones** - More intuitive upload areas in all components
@ -442,7 +492,9 @@ interface GalleryManagerProps {
- **Thumbnail optimization** for faster loading - **Thumbnail optimization** for faster loading
#### 🔥 Medium Priority (Future Sprints) #### 🔥 Medium Priority (Future Sprints)
1. **Advanced Upload Features** 1. **Advanced Upload Features**
- **Image resizing/optimization** options during upload - **Image resizing/optimization** options during upload
- **Duplicate detection** to prevent redundant uploads - **Duplicate detection** to prevent redundant uploads
- **Bulk upload improvements** with better progress tracking - **Bulk upload improvements** with better progress tracking
@ -453,6 +505,7 @@ interface GalleryManagerProps {
- **Advanced search** by alt text, usage status, date ranges - **Advanced search** by alt text, usage status, date ranges
#### 🔥 Low Priority (Nice-to-Have) #### 🔥 Low Priority (Nice-to-Have)
1. **AI Integration** 1. **AI Integration**
- **Automatic alt text suggestions** using image recognition - **Automatic alt text suggestions** using image recognition
- **Smart tagging** for better organization - **Smart tagging** for better organization
@ -463,6 +516,7 @@ interface GalleryManagerProps {
### Functional Requirements ### Functional Requirements
#### Primary Workflow (Direct Upload) #### Primary Workflow (Direct Upload)
- [x] **Drag-and-drop upload works** in all form components - [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] **Immediate upload and preview** happens without page navigation
@ -473,6 +527,7 @@ interface GalleryManagerProps {
- [x] **Gallery reordering** works with drag-and-drop after upload - [x] **Gallery reordering** works with drag-and-drop after upload
#### Secondary Workflow (Media Library) #### Secondary Workflow (Media Library)
- [x] **Media Library Modal** opens and closes properly with smooth animations - [x] **Media Library Modal** opens and closes properly with smooth animations
- [x] **Single and multiple selection** modes work correctly - [x] **Single and multiple selection** modes work correctly
- [x] **Search and filtering** return accurate results - [x] **Search and filtering** return accurate results
@ -481,12 +536,14 @@ interface GalleryManagerProps {
- [x] **All components are keyboard accessible** - [x] **All components are keyboard accessible**
#### Edra Editor Integration #### Edra Editor Integration
- [x] **Slash commands** work for image and gallery insertion - [x] **Slash commands** work for image and gallery insertion
- [x] **MediaLibraryModal integration** in editor placeholders - [x] **MediaLibraryModal integration** in editor placeholders
- [x] **Gallery management** within rich text editor - [x] **Gallery management** within rich text editor
- [x] **Image replacement** functionality in editor - [x] **Image replacement** functionality in editor
### Performance Requirements ### Performance Requirements
- [x] Modal opens in under 200ms - [x] Modal opens in under 200ms
- [x] Media grid loads in under 1 second - [x] Media grid loads in under 1 second
- [x] Search results appear in under 500ms - [x] Search results appear in under 500ms
@ -494,6 +551,7 @@ interface GalleryManagerProps {
- [x] No memory leaks when opening/closing modal multiple times - [x] No memory leaks when opening/closing modal multiple times
### UX Requirements ### UX Requirements
- [x] Interface is intuitive without instruction - [x] Interface is intuitive without instruction
- [x] Visual feedback is clear for all interactions - [x] Visual feedback is clear for all interactions
- [x] Error messages are helpful and actionable - [x] Error messages are helpful and actionable
@ -503,23 +561,27 @@ interface GalleryManagerProps {
## Technical Considerations ## Technical Considerations
### State Management ### State Management
- Use Svelte runes for reactive state - Use Svelte runes for reactive state
- Maintain selection state during modal lifecycle - Maintain selection state during modal lifecycle
- Handle API loading and error states properly - Handle API loading and error states properly
### Accessibility ### Accessibility
- Proper ARIA labels and roles - Proper ARIA labels and roles
- Keyboard navigation support - Keyboard navigation support
- Focus management when modal opens/closes - Focus management when modal opens/closes
- Screen reader announcements for state changes - Screen reader announcements for state changes
### Performance ### Performance
- Lazy load thumbnails as they come into view - Lazy load thumbnails as they come into view
- Debounce search input to prevent excessive API calls - Debounce search input to prevent excessive API calls
- Efficient reordering without full re-renders - Efficient reordering without full re-renders
- Memory cleanup when modal is closed - Memory cleanup when modal is closed
### Error Handling ### Error Handling
- Network failure recovery - Network failure recovery
- Upload failure feedback - Upload failure feedback
- File validation error messages - File validation error messages
@ -528,6 +590,7 @@ interface GalleryManagerProps {
## Future Enhancements ## Future Enhancements
### Nice-to-Have Features ### Nice-to-Have Features
- **Bulk Operations**: Delete multiple files, bulk tag editing - **Bulk Operations**: Delete multiple files, bulk tag editing
- **Advanced Search**: Search by tags, date range, file size - **Advanced Search**: Search by tags, date range, file size
- **Preview Mode**: Full-size preview with navigation - **Preview Mode**: Full-size preview with navigation
@ -537,6 +600,7 @@ interface GalleryManagerProps {
- **Alt Text Editor**: Quick alt text editing for accessibility - **Alt Text Editor**: Quick alt text editing for accessibility
### Integration Opportunities ### Integration Opportunities
- **CDN Optimization**: Automatic image optimization settings - **CDN Optimization**: Automatic image optimization settings
- **AI Tagging**: Automatic tag generation for uploaded images - **AI Tagging**: Automatic tag generation for uploaded images
- **Duplicate Detection**: Warn about similar/duplicate uploads - **Duplicate Detection**: Warn about similar/duplicate uploads
@ -545,6 +609,7 @@ interface GalleryManagerProps {
## Development Checklist ## Development Checklist
### Core Components ### Core Components
- [x] MediaLibraryModal base structure - [x] MediaLibraryModal base structure
- [x] MediaSelector with grid layout - [x] MediaSelector with grid layout
- [x] MediaUploader with drag-and-drop - [x] MediaUploader with drag-and-drop
@ -552,6 +617,7 @@ interface GalleryManagerProps {
- [x] Pagination implementation - [x] Pagination implementation
### Form Integration ### Form Integration
- [x] MediaInput generic component (ImageUploader/GalleryUploader) - [x] MediaInput generic component (ImageUploader/GalleryUploader)
- [x] ImagePicker specialized component (ImageUploader) - [x] ImagePicker specialized component (ImageUploader)
- [x] GalleryManager with reordering (GalleryUploader) - [x] GalleryManager with reordering (GalleryUploader)
@ -560,6 +626,7 @@ interface GalleryManagerProps {
- [x] Integration with Edra editor - [x] Integration with Edra editor
### Polish and Testing ### Polish and Testing
- [x] Responsive design implementation - [x] Responsive design implementation
- [x] Accessibility testing and fixes - [x] Accessibility testing and fixes
- [x] Performance optimization - [x] Performance optimization
@ -568,6 +635,7 @@ interface GalleryManagerProps {
- [x] Mobile device testing - [x] Mobile device testing
### 🎯 Next Priority Items ### 🎯 Next Priority Items
- [ ] **Bulk alt text editing** in Media Library - [ ] **Bulk alt text editing** in Media Library
- [ ] **Usage tracking display** for media references - [ ] **Usage tracking display** for media references
- [ ] **Advanced drag & drop zones** with better visual feedback - [ ] **Advanced drag & drop zones** with better visual feedback

View file

@ -16,6 +16,7 @@ Implement Storybook as our component development and documentation platform to i
## Current State Analysis ## Current State Analysis
### ✅ What We Have ### ✅ What We Have
- Comprehensive admin UI component library (Button, Input, Modal, etc.) - Comprehensive admin UI component library (Button, Input, Modal, etc.)
- Media Library components (MediaLibraryModal, ImagePicker, GalleryManager, etc.) - Media Library components (MediaLibraryModal, ImagePicker, GalleryManager, etc.)
- SCSS-based styling system with global variables - 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 - Vite build system
### 🎯 What We Need ### 🎯 What We Need
- Storybook installation and configuration - Storybook installation and configuration
- Stories for existing components - Stories for existing components
- Visual regression testing setup - Visual regression testing setup
@ -45,6 +47,7 @@ npm install --save-dev @storybook/svelte-vite @storybook/addon-essentials
``` ```
**Expected File Structure**: **Expected File Structure**:
``` ```
.storybook/ .storybook/
├── main.js # Storybook configuration ├── main.js # Storybook configuration
@ -62,6 +65,7 @@ src/
### 2. Configuration Requirements ### 2. Configuration Requirements
#### Main Configuration (.storybook/main.js) #### Main Configuration (.storybook/main.js)
```javascript ```javascript
export default { export default {
stories: ['../src/**/*.stories.@(js|jsx|ts|tsx|svelte)'], stories: ['../src/**/*.stories.@(js|jsx|ts|tsx|svelte)'],
@ -69,7 +73,7 @@ export default {
'@storybook/addon-essentials', // Controls, actions, viewport, etc. '@storybook/addon-essentials', // Controls, actions, viewport, etc.
'@storybook/addon-svelte-csf', // Svelte Component Story Format '@storybook/addon-svelte-csf', // Svelte Component Story Format
'@storybook/addon-a11y', // Accessibility testing '@storybook/addon-a11y', // Accessibility testing
'@storybook/addon-design-tokens', // Design system tokens '@storybook/addon-design-tokens' // Design system tokens
], ],
framework: { framework: {
name: '@storybook/svelte-vite', name: '@storybook/svelte-vite',
@ -81,10 +85,10 @@ export default {
return mergeConfig(config, { return mergeConfig(config, {
resolve: { resolve: {
alias: { alias: {
'$lib': path.resolve('./src/lib'), $lib: path.resolve('./src/lib'),
'$components': path.resolve('./src/lib/components'), $components: path.resolve('./src/lib/components'),
'$icons': path.resolve('./src/assets/icons'), $icons: path.resolve('./src/assets/icons'),
'$illos': path.resolve('./src/assets/illos'), $illos: path.resolve('./src/assets/illos')
} }
}, },
css: { css: {
@ -99,50 +103,52 @@ export default {
} }
} }
} }
}); })
}
} }
};
``` ```
#### Preview Configuration (.storybook/preview.js) #### Preview Configuration (.storybook/preview.js)
```javascript ```javascript
import '../src/assets/styles/reset.css'; import '../src/assets/styles/reset.css'
import '../src/assets/styles/globals.scss'; import '../src/assets/styles/globals.scss'
export const parameters = { export const parameters = {
actions: { argTypesRegex: '^on[A-Z].*' }, actions: { argTypesRegex: '^on[A-Z].*' },
controls: { controls: {
matchers: { matchers: {
color: /(background|color)$/i, color: /(background|color)$/i,
date: /Date$/, date: /Date$/
}, }
}, },
backgrounds: { backgrounds: {
default: 'light', default: 'light',
values: [ values: [
{ name: 'light', value: '#ffffff' }, { name: 'light', value: '#ffffff' },
{ name: 'dark', value: '#333333' }, { name: 'dark', value: '#333333' },
{ name: 'admin', value: '#f5f5f5' }, { name: 'admin', value: '#f5f5f5' }
], ]
}, },
viewport: { viewport: {
viewports: { viewports: {
mobile: { name: 'Mobile', styles: { width: '375px', height: '667px' } }, mobile: { name: 'Mobile', styles: { width: '375px', height: '667px' } },
tablet: { name: 'Tablet', styles: { width: '768px', height: '1024px' } }, tablet: { name: 'Tablet', styles: { width: '768px', height: '1024px' } },
desktop: { name: 'Desktop', styles: { width: '1440px', height: '900px' } }, desktop: { name: 'Desktop', styles: { width: '1440px', height: '900px' } }
}, }
}, }
}; }
``` ```
### 3. Component Story Standards ### 3. Component Story Standards
#### Story File Format #### Story File Format
Each component should have a corresponding `.stories.js` file following this structure: Each component should have a corresponding `.stories.js` file following this structure:
```javascript ```javascript
// Button.stories.js // Button.stories.js
import Button from '../lib/components/admin/Button.svelte'; import Button from '../lib/components/admin/Button.svelte'
export default { export default {
title: 'Admin/Button', title: 'Admin/Button',
@ -162,31 +168,32 @@ export default {
}, },
onclick: { action: 'clicked' } onclick: { action: 'clicked' }
} }
}; }
export const Primary = { export const Primary = {
args: { args: {
variant: 'primary', variant: 'primary',
children: 'Primary Button' children: 'Primary Button'
} }
}; }
export const Secondary = { export const Secondary = {
args: { args: {
variant: 'secondary', variant: 'secondary',
children: 'Secondary Button' children: 'Secondary Button'
} }
}; }
export const AllVariants = { export const AllVariants = {
render: () => ({ render: () => ({
Component: ButtonShowcase, Component: ButtonShowcase,
props: {} props: {}
}) })
}; }
``` ```
#### Story Organization #### Story Organization
``` ```
src/stories/ src/stories/
├── admin/ # Admin interface components ├── admin/ # Admin interface components
@ -208,7 +215,9 @@ src/stories/
## Implementation Plan ## Implementation Plan
### Phase 1: Initial Setup (1-2 days) ### Phase 1: Initial Setup (1-2 days)
1. **Install and Configure Storybook** 1. **Install and Configure Storybook**
- Run `npx storybook@latest init` - Run `npx storybook@latest init`
- Configure Vite integration for SCSS and aliases - Configure Vite integration for SCSS and aliases
- Set up TypeScript support - Set up TypeScript support
@ -220,13 +229,16 @@ src/stories/
- Test hot reloading - Test hot reloading
### Phase 2: Core Component Stories (3-4 days) ### Phase 2: Core Component Stories (3-4 days)
1. **Basic UI Components** 1. **Basic UI Components**
- Button (all variants, states, sizes) - Button (all variants, states, sizes)
- Input (text, textarea, validation states) - Input (text, textarea, validation states)
- Modal (different sizes, content types) - Modal (different sizes, content types)
- LoadingSpinner (different sizes) - LoadingSpinner (different sizes)
2. **Form Components** 2. **Form Components**
- MediaInput (single/multiple modes) - MediaInput (single/multiple modes)
- ImagePicker (different aspect ratios) - ImagePicker (different aspect ratios)
- GalleryManager (with/without items) - GalleryManager (with/without items)
@ -237,12 +249,15 @@ src/stories/
- AdminNavBar (active states) - AdminNavBar (active states)
### Phase 3: Advanced Features (2-3 days) ### Phase 3: Advanced Features (2-3 days)
1. **Mock Data Setup** 1. **Mock Data Setup**
- Create mock Media objects - Create mock Media objects
- Set up API mocking for components that need data - Set up API mocking for components that need data
- Create realistic test scenarios - Create realistic test scenarios
2. **Accessibility Testing** 2. **Accessibility Testing**
- Add @storybook/addon-a11y - Add @storybook/addon-a11y
- Test keyboard navigation - Test keyboard navigation
- Verify screen reader compatibility - Verify screen reader compatibility
@ -253,7 +268,9 @@ src/stories/
- Configure CI integration - Configure CI integration
### Phase 4: Documentation and Polish (1-2 days) ### Phase 4: Documentation and Polish (1-2 days)
1. **Component Documentation** 1. **Component Documentation**
- Add JSDoc comments to components - Add JSDoc comments to components
- Create usage examples - Create usage examples
- Document props and events - Document props and events
@ -267,6 +284,7 @@ src/stories/
## Success Criteria ## Success Criteria
### Functional Requirements ### Functional Requirements
- [ ] Storybook runs successfully with `npm run storybook` - [ ] Storybook runs successfully with `npm run storybook`
- [ ] All existing components have basic stories - [ ] All existing components have basic stories
- [ ] SCSS variables and global styles work correctly - [ ] SCSS variables and global styles work correctly
@ -275,6 +293,7 @@ src/stories/
- [ ] TypeScript support is fully functional - [ ] TypeScript support is fully functional
### Quality Requirements ### Quality Requirements
- [ ] Stories cover all major component variants - [ ] Stories cover all major component variants
- [ ] Interactive controls work for all props - [ ] Interactive controls work for all props
- [ ] Actions are properly logged for events - [ ] Actions are properly logged for events
@ -282,6 +301,7 @@ src/stories/
- [ ] Components are responsive across viewport sizes - [ ] Components are responsive across viewport sizes
### Developer Experience Requirements ### Developer Experience Requirements
- [ ] Story creation is straightforward and documented - [ ] Story creation is straightforward and documented
- [ ] Mock data is easily accessible and realistic - [ ] Mock data is easily accessible and realistic
- [ ] Component API is clearly documented - [ ] Component API is clearly documented
@ -290,12 +310,14 @@ src/stories/
## Integration with Existing Workflow ## Integration with Existing Workflow
### Development Workflow ### Development Workflow
1. **Component Development**: Start new components in Storybook 1. **Component Development**: Start new components in Storybook
2. **Testing**: Test all states and edge cases in stories 2. **Testing**: Test all states and edge cases in stories
3. **Documentation**: Stories serve as living documentation 3. **Documentation**: Stories serve as living documentation
4. **Review**: Use Storybook for design/code reviews 4. **Review**: Use Storybook for design/code reviews
### Project Structure Integration ### Project Structure Integration
``` ```
package.json # Add storybook scripts package.json # Add storybook scripts
├── "storybook": "storybook dev -p 6006" ├── "storybook": "storybook dev -p 6006"
@ -309,6 +331,7 @@ src/
``` ```
### Scripts and Commands ### Scripts and Commands
```json ```json
{ {
"scripts": { "scripts": {
@ -323,21 +346,25 @@ src/
## Technical Considerations ## Technical Considerations
### SCSS Integration ### SCSS Integration
- Import global variables in Storybook preview - Import global variables in Storybook preview
- Ensure component styles render correctly - Ensure component styles render correctly
- Test responsive breakpoints - Test responsive breakpoints
### SvelteKit Compatibility ### SvelteKit Compatibility
- Handle SvelteKit-specific imports (like `$app/stores`) - Handle SvelteKit-specific imports (like `$app/stores`)
- Mock SvelteKit modules when needed - Mock SvelteKit modules when needed
- Ensure aliases work in Storybook context - Ensure aliases work in Storybook context
### TypeScript Support ### TypeScript Support
- Configure proper type checking - Configure proper type checking
- Use TypeScript for story definitions where beneficial - Use TypeScript for story definitions where beneficial
- Ensure IntelliSense works for story arguments - Ensure IntelliSense works for story arguments
### Performance ### Performance
- Optimize bundle size for faster story loading - Optimize bundle size for faster story loading
- Use lazy loading for large story collections - Use lazy loading for large story collections
- Configure appropriate caching - Configure appropriate caching
@ -345,16 +372,19 @@ src/
## Future Enhancements ## Future Enhancements
### Advanced Testing ### Advanced Testing
- **Visual Regression Testing**: Use Chromatic for automated visual testing - **Visual Regression Testing**: Use Chromatic for automated visual testing
- **Interaction Testing**: Add @storybook/addon-interactions for user flow testing - **Interaction Testing**: Add @storybook/addon-interactions for user flow testing
- **Accessibility Automation**: Automated a11y testing in CI/CD - **Accessibility Automation**: Automated a11y testing in CI/CD
### Design System Evolution ### Design System Evolution
- **Design Tokens**: Implement design tokens addon - **Design Tokens**: Implement design tokens addon
- **Figma Integration**: Connect with Figma designs - **Figma Integration**: Connect with Figma designs
- **Component Status**: Track component implementation status - **Component Status**: Track component implementation status
### Collaboration Features ### Collaboration Features
- **Published Storybook**: Deploy Storybook for team access - **Published Storybook**: Deploy Storybook for team access
- **Design Review Process**: Use Storybook for design approvals - **Design Review Process**: Use Storybook for design approvals
- **Documentation Site**: Evolve into full design system documentation - **Documentation Site**: Evolve into full design system documentation
@ -362,30 +392,35 @@ src/
## Risks and Mitigation ## Risks and Mitigation
### Technical Risks ### Technical Risks
- **Build Conflicts**: Vite configuration conflicts - **Build Conflicts**: Vite configuration conflicts
- *Mitigation*: Careful configuration merging and testing - _Mitigation_: Careful configuration merging and testing
- **SCSS Import Issues**: Global styles not loading - **SCSS Import Issues**: Global styles not loading
- *Mitigation*: Test SCSS integration early in setup - _Mitigation_: Test SCSS integration early in setup
### Workflow Risks ### Workflow Risks
- **Adoption Resistance**: Team not using Storybook - **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 - **Maintenance Overhead**: Stories become outdated
- *Mitigation*: Include story updates in component change process - _Mitigation_: Include story updates in component change process
## Success Metrics ## Success Metrics
### Development Efficiency ### Development Efficiency
- Reduced time to develop new components - Reduced time to develop new components
- Faster iteration on component designs - Faster iteration on component designs
- Fewer bugs in component edge cases - Fewer bugs in component edge cases
### Code Quality ### Code Quality
- Better component API consistency - Better component API consistency
- Improved accessibility compliance - Improved accessibility compliance
- More comprehensive component testing - More comprehensive component testing
### Team Collaboration ### Team Collaboration
- Faster design review cycles - Faster design review cycles
- Better communication between design and development - Better communication between design and development
- More consistent component usage across the application - More consistent component usage across the application

View file

@ -3,7 +3,10 @@
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.svg" /> <link rel="icon" href="%sveltekit.assets%/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" /> <meta
name="viewport"
content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no"
/>
<meta property="og:title" content="@jedmund" /> <meta property="og:title" content="@jedmund" />
<meta property="og:type" content="website" /> <meta property="og:type" content="website" />
<meta property="og:url" content="https://jedmund.com" /> <meta property="og:url" content="https://jedmund.com" />

View file

@ -10,8 +10,7 @@
} = $props() } = $props()
// Convert string array to slideshow items // Convert string array to slideshow items
const slideshowItems = $derived(images.map(url => ({ url, alt }))) const slideshowItems = $derived(images.map((url) => ({ url, alt })))
</script> </script>
<Slideshow items={slideshowItems} {alt} aspectRatio="4/3" /> <Slideshow items={slideshowItems} {alt} aspectRatio="4/3" />

View file

@ -7,13 +7,7 @@
source?: string source?: string
} }
let { let { href = '', title = '', sourceType = '', date = '', source = '' }: Props = $props()
href = '',
title = '',
sourceType = '',
date = '',
source = ''
}: Props = $props()
</script> </script>
<li class="mention"> <li class="mention">

View file

@ -32,8 +32,9 @@
? navItems[0] ? navItems[0]
: currentPath === '/about' : currentPath === '/about'
? navItems[4] ? navItems[4]
: navItems.find((item) => currentPath.startsWith(item.href === '/' ? '/work' : item.href)) || : navItems.find((item) =>
navItems[0] currentPath.startsWith(item.href === '/' ? '/work' : item.href)
) || navItems[0]
) )
// Get background color based on variant // Get background color based on variant

View file

@ -60,6 +60,8 @@
} }
&.essay { &.essay {
max-width: 100%; // Full width for essays
.post-body { .post-body {
font-size: 1rem; font-size: 1rem;
line-height: 1.5; line-height: 1.5;
@ -75,6 +77,8 @@
} }
&.blog { &.blog {
max-width: 100%; // Full width for blog posts (legacy essays)
.post-body { .post-body {
font-size: 1rem; font-size: 1rem;
line-height: 1.5; line-height: 1.5;

View file

@ -23,7 +23,6 @@
</h2> </h2>
{/if} {/if}
{#if post.content} {#if post.content}
<div class="post-excerpt"> <div class="post-excerpt">
<p>{getContentExcerpt(post.content, 150)}</p> <p>{getContentExcerpt(post.content, 150)}</p>

View file

@ -124,7 +124,7 @@
.split(',') .split(',')
.map((tag) => tag.trim()) .map((tag) => tag.trim())
.filter(Boolean) .filter(Boolean)
: [], : []
} }
const url = mode === 'edit' ? `/api/posts/${postId}` : '/api/posts' const url = mode === 'edit' ? `/api/posts/${postId}` : '/api/posts'
@ -159,7 +159,6 @@
} }
} }
async function handlePublish() { async function handlePublish() {
status = 'published' status = 'published'
await handleSave() await handleSave()

View file

@ -59,7 +59,8 @@
role: data.role || '', role: data.role || '',
projectType: data.projectType || 'work', projectType: data.projectType || 'work',
externalUrl: data.externalUrl || '', externalUrl: data.externalUrl || '',
featuredImage: data.featuredImage && data.featuredImage.trim() !== '' ? data.featuredImage : null, featuredImage:
data.featuredImage && data.featuredImage.trim() !== '' ? data.featuredImage : null,
backgroundColor: data.backgroundColor || '', backgroundColor: data.backgroundColor || '',
highlightColor: data.highlightColor || '', highlightColor: data.highlightColor || '',
logoUrl: data.logoUrl && data.logoUrl.trim() !== '' ? data.logoUrl : '', logoUrl: data.logoUrl && data.logoUrl.trim() !== '' ? data.logoUrl : '',
@ -140,7 +141,8 @@
role: formData.role, role: formData.role,
projectType: formData.projectType, projectType: formData.projectType,
externalUrl: formData.externalUrl, 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, logoUrl: formData.logoUrl && formData.logoUrl !== '' ? formData.logoUrl : null,
backgroundColor: formData.backgroundColor, backgroundColor: formData.backgroundColor,
highlightColor: formData.highlightColor, highlightColor: formData.highlightColor,
@ -222,11 +224,9 @@
onStatusChange={handleStatusChange} onStatusChange={handleStatusChange}
disabled={isSaving} disabled={isSaving}
isLoading={isSaving} isLoading={isSaving}
primaryAction={ primaryAction={formData.status === 'published'
formData.status === 'published'
? { label: 'Save', status: 'published' } ? { label: 'Save', status: 'published' }
: { label: 'Publish', status: 'published' } : { label: 'Publish', status: 'published' }}
}
dropdownActions={[ dropdownActions={[
{ label: 'Save as Draft', status: 'draft', show: formData.status !== 'draft' }, { label: 'Save as Draft', status: 'draft', show: formData.status !== 'draft' },
{ label: 'List Only', status: 'list-only', show: formData.status !== 'list-only' }, { label: 'List Only', status: 'list-only', show: formData.status !== 'list-only' },

View file

@ -12,7 +12,6 @@
} }
let { formData = $bindable(), validationErrors, onSave }: Props = $props() let { formData = $bindable(), validationErrors, onSave }: Props = $props()
</script> </script>
<div class="form-section"> <div class="form-section">

View file

@ -94,7 +94,9 @@
&.active { &.active {
background-color: white; background-color: white;
color: $grey-10; 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 { &:focus {

View file

@ -38,15 +38,7 @@
<FormFieldWrapper {label} {required} {helpText} {error}> <FormFieldWrapper {label} {required} {helpText} {error}>
{#snippet children()} {#snippet children()}
<Select <Select bind:value {options} {size} {variant} {fullWidth} {pill} {...restProps} />
bind:value
{options}
{size}
{variant}
{fullWidth}
{pill}
{...restProps}
/>
{/snippet} {/snippet}
</FormFieldWrapper> </FormFieldWrapper>

View file

@ -78,7 +78,7 @@ export async function uploadFile(
): Promise<UploadResult> { ): Promise<UploadResult> {
try { try {
// TEMPORARY: Force Cloudinary usage for testing // 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 // Use local storage in development or when Cloudinary is not configured
if ((dev && !FORCE_CLOUDINARY_IN_DEV) || !isCloudinaryConfigured()) { if ((dev && !FORCE_CLOUDINARY_IN_DEV) || !isCloudinaryConfigured()) {
@ -136,14 +136,11 @@ export async function uploadFile(
// Upload to Cloudinary // Upload to Cloudinary
const result = await new Promise<UploadApiResponse>((resolve, reject) => { const result = await new Promise<UploadApiResponse>((resolve, reject) => {
const uploadStream = cloudinary.uploader.upload_stream( const uploadStream = cloudinary.uploader.upload_stream(uploadOptions, (error, result) => {
uploadOptions,
(error, result) => {
if (error) reject(error) if (error) reject(error)
else if (result) resolve(result) else if (result) resolve(result)
else reject(new Error('Upload failed')) else reject(new Error('Upload failed'))
} })
)
uploadStream.end(buffer) uploadStream.end(buffer)
}) })

View file

@ -169,7 +169,8 @@ function renderTiptapContent(doc: any): string {
// Render inline content (text nodes with marks) // Render inline content (text nodes with marks)
const renderInlineContent = (content: any[]): string => { const renderInlineContent = (content: any[]): string => {
return content.map((node: any) => { return content
.map((node: any) => {
if (node.type === 'text') { if (node.type === 'text') {
let text = escapeHtml(node.text || '') let text = escapeHtml(node.text || '')
@ -209,7 +210,8 @@ function renderTiptapContent(doc: any): string {
// Handle other inline nodes // Handle other inline nodes
return renderNode(node) return renderNode(node)
}).join('') })
.join('')
} }
// Helper to escape HTML // Helper to escape HTML

View file

@ -9,10 +9,7 @@
<svelte:head> <svelte:head>
<title>@jedmund is a software designer</title> <title>@jedmund is a software designer</title>
<meta name="description" content="Justin Edmund is a software designer based in San Francisco." /> <meta name="description" content="Justin Edmund is a software designer based in San Francisco." />
<meta <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no" />
name="viewport"
content="width=device-width, initial-scale=1.0, user-scalable=no"
/>
</svelte:head> </svelte:head>
{#if !isAdminRoute} {#if !isAdminRoute}

View file

@ -213,7 +213,10 @@
// Add photos to album via API // Add photos to album via API
const addedPhotos = [] 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) { for (const media of newMedia) {
console.log(`Adding photo ${media.id} (${media.filename}) to album ${album.id}`) console.log(`Adding photo ${media.id} (${media.filename}) to album ${album.id}`)
@ -596,14 +599,10 @@
onStatusChange={handleSave} onStatusChange={handleSave}
disabled={isSaving} disabled={isSaving}
isLoading={isSaving} isLoading={isSaving}
primaryAction={ primaryAction={status === 'published'
status === 'published'
? { label: 'Save', status: 'published' } ? { label: 'Save', status: 'published' }
: { label: 'Publish', status: 'published' } : { label: 'Publish', status: 'published' }}
} dropdownActions={[{ label: 'Save as Draft', status: 'draft', show: status !== 'draft' }]}
dropdownActions={[
{ label: 'Save as Draft', status: 'draft', show: status !== 'draft' }
]}
/> />
</div> </div>
{/if} {/if}

View file

@ -123,7 +123,11 @@
if (!photoResponse.ok) { if (!photoResponse.ok) {
const errorData = await photoResponse.text() 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 // Continue with other photos even if one fails
} else { } else {
const photo = await photoResponse.json() const photo = await photoResponse.json()
@ -132,7 +136,9 @@
} }
} }
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) { } catch (photoError) {
console.error('Error adding photos to album:', photoError) console.error('Error adding photos to album:', photoError)
// Don't fail the whole creation - just log the error // Don't fail the whole creation - just log the error

View file

@ -30,40 +30,37 @@
let error = $state('') let error = $state('')
let total = $state(0) let total = $state(0)
let postTypeCounts = $state<Record<string, number>>({}) let postTypeCounts = $state<Record<string, number>>({})
let statusCounts = $state<Record<string, number>>({})
// Filter state // Filter state
let selectedFilter = $state<string>('all') let selectedTypeFilter = $state<string>('all')
let selectedStatusFilter = $state<string>('all')
// Composer state // Composer state
let showInlineComposer = $state(true) let showInlineComposer = $state(true)
let isInteractingWithFilters = $state(false)
// Create filter options // Create filter options
const filterOptions = $derived([ const typeFilterOptions = $derived([
{ value: 'all', label: 'All posts' }, { value: 'all', label: 'All posts' },
{ value: 'post', label: 'Posts' }, { value: 'post', label: 'Posts' },
{ value: 'essay', label: 'Essays' } { value: 'essay', label: 'Essays' }
]) ])
const statusFilterOptions = $derived([
{ value: 'all', label: 'All statuses' },
{ value: 'published', label: 'Published' },
{ value: 'draft', label: 'Draft' }
])
const postTypeIcons: Record<string, string> = { const postTypeIcons: Record<string, string> = {
post: '💭', post: '💭',
essay: '📝', essay: '📝'
// Legacy types for backward compatibility
blog: '📝',
microblog: '💭',
link: '🔗',
photo: '📷',
album: '🖼️'
} }
const postTypeLabels: Record<string, string> = { const postTypeLabels: Record<string, string> = {
post: 'Post', post: 'Post',
essay: 'Essay', essay: 'Essay'
// Legacy types for backward compatibility
blog: 'Essay',
microblog: 'Post',
link: 'Post',
photo: 'Post',
album: 'Album'
} }
onMount(async () => { onMount(async () => {
@ -94,24 +91,29 @@
posts = data.posts || [] posts = data.posts || []
total = data.pagination?.total || posts.length total = data.pagination?.total || posts.length
// Calculate post type counts and normalize types // Calculate post type counts
const counts: Record<string, number> = { const typeCounts: Record<string, number> = {
all: posts.length, all: posts.length,
post: 0, post: 0,
essay: 0 essay: 0
} }
posts.forEach((post) => { posts.forEach((post) => {
// Normalize legacy types to simplified types if (post.postType === 'post') {
if (post.postType === 'blog') { typeCounts.post++
counts.essay = (counts.essay || 0) + 1 } else if (post.postType === 'essay') {
} else if (['microblog', 'link', 'photo'].includes(post.postType)) { typeCounts.essay++
counts.post = (counts.post || 0) + 1
} else {
counts[post.postType] = (counts[post.postType] || 0) + 1
} }
}) })
postTypeCounts = counts postTypeCounts = typeCounts
// Calculate status counts
const statusCountsTemp: Record<string, number> = {
all: posts.length,
published: posts.filter((p) => p.status === 'published').length,
draft: posts.filter((p) => p.status === 'draft').length
}
statusCounts = statusCountsTemp
// Apply initial filter // Apply initial filter
applyFilter() applyFilter()
@ -124,18 +126,26 @@
} }
function applyFilter() { function applyFilter() {
if (selectedFilter === 'all') { let filtered = posts
filteredPosts = posts
} else if (selectedFilter === 'post') { // Apply type filter
filteredPosts = posts.filter((post) => ['post', 'microblog'].includes(post.postType)) if (selectedTypeFilter !== 'all') {
} else if (selectedFilter === 'essay') { filtered = filtered.filter((post) => post.postType === selectedTypeFilter)
filteredPosts = posts.filter((post) => ['essay', 'blog'].includes(post.postType))
} else {
filteredPosts = posts.filter((post) => post.postType === selectedFilter)
}
} }
function handleFilterChange() { // Apply status filter
if (selectedStatusFilter !== 'all') {
filtered = filtered.filter((post) => post.status === selectedStatusFilter)
}
filteredPosts = filtered
}
function handleTypeFilterChange() {
applyFilter()
}
function handleStatusFilterChange() {
applyFilter() applyFilter()
} }
@ -168,11 +178,18 @@
<AdminFilters> <AdminFilters>
{#snippet left()} {#snippet left()}
<Select <Select
bind:value={selectedFilter} bind:value={selectedTypeFilter}
options={filterOptions} options={typeFilterOptions}
size="small" size="small"
variant="minimal" variant="minimal"
onchange={handleFilterChange} onchange={handleTypeFilterChange}
/>
<Select
bind:value={selectedStatusFilter}
options={statusFilterOptions}
size="small"
variant="minimal"
onchange={handleStatusFilterChange}
/> />
{/snippet} {/snippet}
</AdminFilters> </AdminFilters>
@ -187,10 +204,11 @@
<div class="empty-icon">📝</div> <div class="empty-icon">📝</div>
<h3>No posts found</h3> <h3>No posts found</h3>
<p> <p>
{#if selectedFilter === 'all'} {#if selectedTypeFilter === 'all' && selectedStatusFilter === 'all'}
Create your first post to get started! Create your first post to get started!
{:else} {: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} {/if}
</p> </p>
</div> </div>

View file

@ -34,22 +34,22 @@
let statusCounts = $state<Record<string, number>>({}) let statusCounts = $state<Record<string, number>>({})
// Filter state // Filter state
let selectedStatusFilter = $state<string>('all')
let selectedTypeFilter = $state<string>('all') let selectedTypeFilter = $state<string>('all')
let selectedStatusFilter = $state<string>('all')
// Create filter options // Create filter options
const statusFilterOptions = $derived([ const typeFilterOptions = $derived([
{ value: 'all', label: 'All projects' }, { 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: 'published', label: 'Published' },
{ value: 'draft', label: 'Draft' } { value: 'draft', label: 'Draft' }
]) ])
const typeFilterOptions = [
{ value: 'all', label: 'All types' },
{ value: 'work', label: 'Work' },
{ value: 'labs', label: 'Labs' }
]
onMount(async () => { onMount(async () => {
await loadProjects() await loadProjects()
// Handle clicks outside dropdowns // Handle clicks outside dropdowns
@ -204,13 +204,6 @@
<!-- Filters --> <!-- Filters -->
<AdminFilters> <AdminFilters>
{#snippet left()} {#snippet left()}
<Select
bind:value={selectedStatusFilter}
options={statusFilterOptions}
size="small"
variant="minimal"
onchange={handleStatusFilterChange}
/>
<Select <Select
bind:value={selectedTypeFilter} bind:value={selectedTypeFilter}
options={typeFilterOptions} options={typeFilterOptions}
@ -218,6 +211,13 @@
variant="minimal" variant="minimal"
onchange={handleTypeFilterChange} onchange={handleTypeFilterChange}
/> />
<Select
bind:value={selectedStatusFilter}
options={statusFilterOptions}
size="small"
variant="minimal"
onchange={handleStatusFilterChange}
/>
{/snippet} {/snippet}
</AdminFilters> </AdminFilters>

View file

@ -140,9 +140,11 @@ export const PUT: RequestHandler = async (event) => {
date: body.date !== undefined ? (body.date ? new Date(body.date) : null) : existing.date, date: body.date !== undefined ? (body.date ? new Date(body.date) : null) : existing.date,
location: body.location !== undefined ? body.location : existing.location, location: body.location !== undefined ? body.location : existing.location,
coverPhotoId: body.coverPhotoId !== undefined ? body.coverPhotoId : existing.coverPhotoId, 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, status: body.status !== undefined ? body.status : existing.status,
showInUniverse: body.showInUniverse !== undefined ? body.showInUniverse : existing.showInUniverse showInUniverse:
body.showInUniverse !== undefined ? body.showInUniverse : existing.showInUniverse
} }
}) })

View file

@ -70,7 +70,8 @@ export const PUT: RequestHandler = async (event) => {
data: { data: {
altText: body.altText !== undefined ? body.altText : existing.altText, altText: body.altText !== undefined ? body.altText : existing.altText,
description: body.description !== undefined ? body.description : existing.description, description: body.description !== undefined ? body.description : existing.description,
isPhotography: body.isPhotography !== undefined ? body.isPhotography : existing.isPhotography isPhotography:
body.isPhotography !== undefined ? body.isPhotography : existing.isPhotography
} }
}) })

View file

@ -64,7 +64,10 @@ export const DELETE: RequestHandler = async (event) => {
// Local storage deletion // Local storage deletion
deleted = await deleteFileLocally(media.url) deleted = await deleteFileLocally(media.url)
if (!deleted) { 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 // Count successful storage deletions
const successfulStorageDeletions = storageDeleteResults.filter(r => r.deleted).length const successfulStorageDeletions = storageDeleteResults.filter((r) => r.deleted).length
const failedStorageDeletions = storageDeleteResults.filter(r => !r.deleted) const failedStorageDeletions = storageDeleteResults.filter((r) => !r.deleted)
logger.info('Bulk media deletion completed', { logger.info('Bulk media deletion completed', {
deletedCount: deleteResult.count, deletedCount: deleteResult.count,

View file

@ -82,13 +82,17 @@ export const PUT: RequestHandler = async (event) => {
year: body.year !== undefined ? body.year : existing.year, year: body.year !== undefined ? body.year : existing.year,
client: body.client !== undefined ? body.client : existing.client, client: body.client !== undefined ? body.client : existing.client,
role: body.role !== undefined ? body.role : existing.role, 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, logoUrl: body.logoUrl !== undefined ? body.logoUrl : existing.logoUrl,
gallery: body.gallery !== undefined ? body.gallery : existing.gallery, gallery: body.gallery !== undefined ? body.gallery : existing.gallery,
externalUrl: body.externalUrl !== undefined ? body.externalUrl : existing.externalUrl, externalUrl: body.externalUrl !== undefined ? body.externalUrl : existing.externalUrl,
caseStudyContent: body.caseStudyContent !== undefined ? body.caseStudyContent : existing.caseStudyContent, caseStudyContent:
backgroundColor: body.backgroundColor !== undefined ? body.backgroundColor : existing.backgroundColor, body.caseStudyContent !== undefined ? body.caseStudyContent : existing.caseStudyContent,
highlightColor: body.highlightColor !== undefined ? body.highlightColor : existing.highlightColor, backgroundColor:
body.backgroundColor !== undefined ? body.backgroundColor : existing.backgroundColor,
highlightColor:
body.highlightColor !== undefined ? body.highlightColor : existing.highlightColor,
projectType: body.projectType !== undefined ? body.projectType : existing.projectType, projectType: body.projectType !== undefined ? body.projectType : existing.projectType,
displayOrder: body.displayOrder !== undefined ? body.displayOrder : existing.displayOrder, displayOrder: body.displayOrder !== undefined ? body.displayOrder : existing.displayOrder,
status: body.status !== undefined ? body.status : existing.status, status: body.status !== undefined ? body.status : existing.status,