We ran the linter
This commit is contained in:
parent
c6ce13a530
commit
3ba7f6b762
30 changed files with 751 additions and 546 deletions
|
|
@ -180,6 +180,7 @@ The system now uses a dedicated `media_usage` table for robust tracking:
|
|||
```
|
||||
|
||||
**Benefits:**
|
||||
|
||||
- Accurate usage tracking across all content types
|
||||
- Efficient queries for usage information
|
||||
- Safe bulk deletion with automatic reference cleanup
|
||||
|
|
@ -287,39 +288,46 @@ const ImageBlock = {
|
|||
|
||||
```typescript
|
||||
// Projects
|
||||
GET /api/projects
|
||||
POST /api/projects
|
||||
GET /api/projects/[slug]
|
||||
PUT /api/projects/[id]
|
||||
DELETE /api/projects/[id]
|
||||
GET / api / projects
|
||||
POST / api / projects
|
||||
GET / api / projects / [slug]
|
||||
PUT / api / projects / [id]
|
||||
DELETE / api / projects / [id]
|
||||
|
||||
// Posts
|
||||
GET /api/posts
|
||||
POST /api/posts
|
||||
GET /api/posts/[slug]
|
||||
PUT /api/posts/[id]
|
||||
DELETE /api/posts/[id]
|
||||
GET / api / posts
|
||||
POST / api / posts
|
||||
GET / api / posts / [slug]
|
||||
PUT / api / posts / [id]
|
||||
DELETE / api / posts / [id]
|
||||
|
||||
// Albums & Photos
|
||||
GET /api/albums
|
||||
POST /api/albums
|
||||
GET /api/albums/[slug]
|
||||
PUT /api/albums/[id]
|
||||
DELETE /api/albums/[id]
|
||||
POST /api/albums/[id]/photos
|
||||
DELETE /api/photos/[id]
|
||||
PUT /api/photos/[id]/order
|
||||
GET / api / albums
|
||||
POST / api / albums
|
||||
GET / api / albums / [slug]
|
||||
PUT / api / albums / [id]
|
||||
DELETE / api / albums / [id]
|
||||
POST / api / albums / [id] / photos
|
||||
DELETE / api / photos / [id]
|
||||
PUT / api / photos / [id] / order
|
||||
|
||||
// Media Management
|
||||
POST /api/media/upload // Single file upload
|
||||
POST /api/media/bulk-upload // Multiple file upload
|
||||
GET /api/media // Browse with filters, pagination
|
||||
GET /api/media/[id] // Get single media item
|
||||
PUT /api/media/[id] // Update media (alt text, description)
|
||||
DELETE /api/media/[id] // Delete single media item
|
||||
DELETE /api/media/bulk-delete // Delete multiple media items
|
||||
GET /api/media/[id]/usage // Check where media is used
|
||||
POST /api/media/backfill-usage // Backfill usage tracking for existing content
|
||||
POST / api / media / upload // Single file upload
|
||||
POST / api / media / bulk - upload // Multiple file upload
|
||||
GET / api / media // Browse with filters, pagination
|
||||
GET / api / media / [id] // Get single media item
|
||||
PUT / api / media / [id] // Update media (alt text, description)
|
||||
DELETE / api / media / [id] // Delete single media item
|
||||
DELETE / api / media / bulk -
|
||||
delete (
|
||||
// Delete multiple media items
|
||||
GET
|
||||
) /
|
||||
api /
|
||||
media /
|
||||
[id] /
|
||||
usage // Check where media is used
|
||||
POST / api / media / backfill - usage // Backfill usage tracking for existing content
|
||||
```
|
||||
|
||||
### 8. Media Management & Cleanup
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -23,7 +23,6 @@
|
|||
</h2>
|
||||
{/if}
|
||||
|
||||
|
||||
{#if post.content}
|
||||
<div class="post-excerpt">
|
||||
<p>{getContentExcerpt(post.content, 150)}</p>
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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' },
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@
|
|||
}
|
||||
|
||||
let { formData = $bindable(), validationErrors, onSave }: Props = $props()
|
||||
|
||||
</script>
|
||||
|
||||
<div class="form-section">
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in a new issue