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

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

View file

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

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

View file

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

View file

@ -3,7 +3,10 @@
<head>
<meta charset="utf-8" />
<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:type" content="website" />
<meta property="og:url" content="https://jedmund.com" />

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -94,7 +94,9 @@
&.active {
background-color: white;
color: $grey-10;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08), 0 1px 2px rgba(0, 0, 0, 0.04);
box-shadow:
0 1px 3px rgba(0, 0, 0, 0.08),
0 1px 2px rgba(0, 0, 0, 0.04);
}
&:focus {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -64,7 +64,10 @@ export const DELETE: RequestHandler = async (event) => {
// Local storage deletion
deleted = await deleteFileLocally(media.url)
if (!deleted) {
logger.warn('Failed to delete from local storage', { url: media.url, mediaId: media.id })
logger.warn('Failed to delete from local storage', {
url: media.url,
mediaId: media.id
})
}
}
@ -114,8 +117,8 @@ export const DELETE: RequestHandler = async (event) => {
})
// Count successful storage deletions
const successfulStorageDeletions = storageDeleteResults.filter(r => r.deleted).length
const failedStorageDeletions = storageDeleteResults.filter(r => !r.deleted)
const successfulStorageDeletions = storageDeleteResults.filter((r) => r.deleted).length
const failedStorageDeletions = storageDeleteResults.filter((r) => !r.deleted)
logger.info('Bulk media deletion completed', {
deletedCount: deleteResult.count,

View file

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