8.0 KiB
Project Card Thumbnail Enhancement
Task 22.3: Update project card to display thumbnails
Status: ✅ Complete
Requirements: 2.1.7, 2.1.8
Overview
Enhanced the project card thumbnail display in ProjectsView.vue with loading states, lazy loading, smooth transitions, and robust error handling for optimal performance and user experience.
Implementation Details
1. Loading Skeleton
Added a loading skeleton that displays while thumbnails are being fetched:
<div
v-if="isThumbnailLoading(project.id)"
class="w-full h-full animate-pulse bg-gradient-to-br from-muted to-muted/50"
>
<div class="w-full h-full flex items-center justify-center">
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-primary/30"></div>
</div>
</div>
Features:
- Pulsing gradient background animation
- Spinning loader icon
- Smooth visual feedback during load
2. Lazy Loading with Intersection Observer
Implemented viewport-based lazy loading using the Intersection Observer API:
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
const projectId = parseInt(entry.target.getAttribute('data-project-id') || '0')
const project = projects.find(p => p.id === projectId)
if (project?.thumbnail_url && !thumbnailBlobUrls.value.has(projectId)) {
loadThumbnail(projectId, project.thumbnail_url)
observer.unobserve(entry.target)
}
}
})
},
{
rootMargin: '50px', // Start loading 50px before visible
threshold: 0.01
}
)
Benefits:
- Thumbnails only load when cards are near the viewport
- Reduces initial page load time
- Improves performance with many projects
- 50px rootMargin for smooth preloading
- Includes fallback for browsers without Intersection Observer support
3. Native Lazy Loading
Added native browser lazy loading as an additional optimization layer:
<img
:src="getThumbnailUrl(project.id)"
:alt="project.name"
loading="lazy"
class="w-full h-full object-cover transition-opacity duration-300"
/>
4. Smooth Fade-in Transition
Implemented smooth opacity transition when thumbnails load:
<img
:class="{
'opacity-0': !isThumbnailLoaded(project.id),
'opacity-100': isThumbnailLoaded(project.id)
}"
@load="onThumbnailLoad(project.id)"
@error="onThumbnailError(project.id)"
/>
Features:
- Images start invisible (opacity-0)
- Fade to full opacity on load
- 300ms transition duration
- Prevents flash of unstyled content
5. Enhanced Fallback Display
Improved the fallback display for projects without thumbnails:
<div class="w-full h-full flex items-center justify-center bg-gradient-to-br from-primary/10 to-primary/5">
<div class="text-center">
<div class="text-4xl font-bold text-primary/40 mb-1">
{{ getProjectInitials(project.name) }}
</div>
<FolderOpen class="h-8 w-8 mx-auto text-primary/30" />
</div>
</div>
Features:
- Displays project initials (first 2 letters or first letter of first 2 words)
- Shows folder icon for visual context
- Gradient background for aesthetic appeal
- Consistent with overall design system
6. State Management
Added comprehensive state tracking for thumbnail loading:
const thumbnailBlobUrls = ref<Map<number, string>>(new Map())
const thumbnailLoadingStates = ref<Map<number, boolean>>(new Map())
const thumbnailLoadedStates = ref<Map<number, boolean>>(new Map())
const thumbnailErrorStates = ref<Map<number, boolean>>(new Map())
Helper Methods:
getThumbnailUrl(projectId)- Returns blob URL for projectisThumbnailLoading(projectId)- Checks if thumbnail is loadingisThumbnailLoaded(projectId)- Checks if thumbnail has loadedonThumbnailLoad(projectId)- Handles successful load eventonThumbnailError(projectId)- Handles load error eventloadThumbnail(projectId, url)- Fetches thumbnail and creates blob URLloadAllThumbnails()- Sets up Intersection ObservergetProjectInitials(name)- Generates initials for fallback display
7. Error Handling
Robust error handling for failed thumbnail loads:
const onThumbnailError = (projectId: number) => {
thumbnailErrorStates.value.set(projectId, true)
thumbnailLoadingStates.value.set(projectId, false)
// Revoke the blob URL on error
const blobUrl = thumbnailBlobUrls.value.get(projectId)
if (blobUrl) {
URL.revokeObjectURL(blobUrl)
thumbnailBlobUrls.value.delete(projectId)
}
}
Features:
- Catches image load errors
- Revokes blob URLs on error
- Falls back to placeholder display
- Tracks error state per project
8. Memory Management
Proper cleanup to prevent memory leaks:
onUnmounted(() => {
// Clean up all blob URLs to prevent memory leaks
thumbnailBlobUrls.value.forEach(url => URL.revokeObjectURL(url))
thumbnailBlobUrls.value.clear()
thumbnailLoadingStates.value.clear()
thumbnailLoadedStates.value.clear()
thumbnailErrorStates.value.clear()
})
Features:
- Revokes all blob URLs on unmount
- Clears all state maps
- Prevents duplicate loading with state checks
- Revokes old URLs before creating new ones
Performance Optimizations
- Intersection Observer - Viewport-based loading with 50px margin
- Native Lazy Loading - Browser-level optimization
- Blob URL Caching - Prevents re-fetching already loaded thumbnails
- State Checks - Prevents duplicate loading attempts
- Memory Cleanup - Proper blob URL revocation
- Smooth Transitions - CSS-based opacity transitions (no JavaScript animation)
Requirements Coverage
Requirement 2.1.7
"THE VFX_System SHALL display the project thumbnail on project cards in the projects list page"
✅ Satisfied - Thumbnails display in the project card header section with proper aspect ratio, object-fit, and authenticated access via blob URLs.
Requirement 2.1.8
"WHEN no thumbnail is uploaded, THE VFX_System SHALL display a default placeholder image or project initials"
✅ Satisfied - Projects without thumbnails show a visually appealing fallback with project initials and a folder icon on a gradient background.
Testing
Manual Testing Steps
- Start backend:
cd backend && uvicorn main:app --reload - Start frontend:
cd frontend && npm run dev - Navigate to Projects page
- Upload thumbnails for some projects via Project Settings
- Verify:
- Loading skeleton appears during thumbnail load
- Thumbnails fade in smoothly when loaded
- Projects without thumbnails show initials + folder icon
- Scroll performance is smooth with many projects
- Thumbnails only load when cards are near viewport
- Error handling works if thumbnail URL is invalid
Test File
Created frontend/test-project-card-thumbnails.html with comprehensive test documentation and verification checklist.
Files Modified
frontend/src/views/ProjectsView.vue- Enhanced thumbnail display with loading states, lazy loading, and error handling
Files Created
frontend/test-project-card-thumbnails.html- Test documentationfrontend/docs/project-card-thumbnail-enhancement.md- This document
Browser Compatibility
- ✅ Modern browsers with Intersection Observer support
- ✅ Fallback for browsers without Intersection Observer
- ✅ Native lazy loading where supported
- ✅ Graceful degradation for older browsers
Future Enhancements
Potential improvements for future iterations:
- Progressive Image Loading - Load low-quality placeholder first, then high-quality
- WebP Support - Serve WebP format for better compression
- Thumbnail Caching - Use Service Worker for offline caching
- Skeleton Shimmer - More sophisticated loading animation
- Retry Logic - Automatic retry on failed loads
Conclusion
Task 22.3 has been successfully completed with all requirements satisfied. The implementation provides a smooth, performant, and visually appealing thumbnail display system for project cards with robust error handling and memory management.