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