mirror of
https://github.com/kikootwo/ReadMeABook.git
synced 2026-06-03 04:40:09 +00:00
ac2ad8aac2
Implements pure CSS card stack animations for BookDate recommendations, including smooth exit and advance transitions. Adds local caching of library cover thumbnails during scans, updates database schema and API to serve cached covers, and enhances BookDate to support 'favorites' scope with a book picker modal. Updates admin settings validation logic for Prowlarr, improves indexer state management, and documents new features and backend changes.
206 lines
7.9 KiB
Markdown
206 lines
7.9 KiB
Markdown
# BookDate Card Stack Animations
|
|
|
|
**Status:** ✅ Implemented | Pure CSS card stack with smooth exit/advance animations
|
|
|
|
## Overview
|
|
Visual card stack (3 visible cards) with GPU-accelerated animations. Top card swipes away, remaining cards advance forward smoothly.
|
|
|
|
## Key Components
|
|
|
|
### CardStack.tsx
|
|
- **Location:** `src/components/bookdate/CardStack.tsx`
|
|
- **Purpose:** Orchestrates 3-card stack rendering and animation lifecycle
|
|
- **Props:**
|
|
- `recommendations: any[]` - Full recommendations array
|
|
- `currentIndex: number` - Index of current top card
|
|
- `onSwipe: (action, markedAsKnown?) => void` - Swipe handler (API call)
|
|
- `onSwipeComplete: () => void` - Called after animations finish
|
|
|
|
**Animation Flow:**
|
|
1. User swipes → `handleSwipeStart` triggered
|
|
2. Exit animation starts (400ms) → API call
|
|
3. Exit completes → `visibleCards` array updated to exclude exited card
|
|
4. Advance animation starts (350ms) → Cards move from positions 1,2,3 to 0,1,2
|
|
5. Advance completes → `onSwipeComplete` called → `currentIndex` incremented
|
|
|
|
**State Management:**
|
|
- `isExiting: boolean` - Exit animation in progress
|
|
- `exitDirection: 'left' | 'right' | 'up'` - Which exit animation to play
|
|
- `isAdvancing: boolean` - Advance animation in progress
|
|
|
|
**Visible Cards Logic:**
|
|
- **Normal:** Shows cards at `[currentIndex, currentIndex+1, currentIndex+2]` with `stackPosition` 0, 1, 2
|
|
- **During Advance:** Shows cards at `[currentIndex+1, currentIndex+2, currentIndex+3]` with `stackPosition` 0, 1, 2 and `fromPosition` 1, 2, 3
|
|
- This excludes the exited card and prevents snapping
|
|
- `fromPosition` determines which advance animation to apply
|
|
|
|
### RecommendationCard.tsx Updates
|
|
- **New Props:**
|
|
- `stackPosition?: number` - 0=top, 1=middle, 2=bottom (default: 0)
|
|
- `isAnimating?: boolean` - Disables gestures during animations (default: false)
|
|
- `isDraggable?: boolean` - Only top card accepts input (default: true)
|
|
|
|
**Behavior:**
|
|
- Swipe handlers disabled when `!isDraggable || isAnimating`
|
|
- Desktop buttons hidden when `stackPosition !== 0`
|
|
- Drag offset only updates for top card
|
|
|
|
### page.tsx Updates
|
|
- **Changed:** Import `CardStack` instead of `RecommendationCard`
|
|
- **Added:** `handleSwipeComplete()` callback
|
|
- **Modified:** `handleSwipe()` no longer increments `currentIndex` (delegated to `handleSwipeComplete`)
|
|
|
|
## CSS Animations
|
|
|
|
**Location:** `src/app/globals.css`
|
|
|
|
### Exit Animations (400ms, ease-in-out)
|
|
```css
|
|
.animate-exit-left /* translate(-150%, 50px) rotate(-25deg) */
|
|
.animate-exit-right /* translate(150%, 50px) rotate(25deg) */
|
|
.animate-exit-up /* translate(0, -120%) scale(0.8) */
|
|
```
|
|
|
|
### Advance Animations (350ms, bounce easing)
|
|
```css
|
|
.animate-advance-to-top /* scale(0.95→1.0), translateY(-12px→0) */
|
|
.animate-advance-to-middle /* scale(0.90→0.95), translateY(-24px→-12px) */
|
|
.animate-enter /* scale(0.85→0.90), translateY(-36px→-24px) */
|
|
```
|
|
|
|
### Stack Position Classes (Static)
|
|
```css
|
|
.card-stack-position-0 /* z-50, scale(1.0), translateY(0), opacity(1.0) */
|
|
.card-stack-position-1 /* z-40, scale(0.95), translateY(-12px), opacity(0.95) */
|
|
.card-stack-position-2 /* z-30, scale(0.90), translateY(-24px), opacity(0.90) */
|
|
```
|
|
|
|
### Performance Optimizations
|
|
```css
|
|
.card-stack-container /* perspective: 1000px, preserve-3d */
|
|
.card-stack-item /* will-change: transform, opacity */
|
|
```
|
|
|
|
## Animation Timing
|
|
|
|
| Phase | Duration | Easing | Description |
|
|
|-------|----------|--------|-------------|
|
|
| Exit | 400ms | ease-in-out | Top card swipes away |
|
|
| Advance | 350ms | cubic-bezier(0.34, 1.56, 0.64, 1) | Cards move forward (slight bounce) |
|
|
| Total | 750ms | - | Full swipe cycle |
|
|
|
|
**Staggering:** Advance animations start after exit completes (sequential, not overlapping).
|
|
|
|
## Edge Cases Handled
|
|
|
|
### Rapid Swipes
|
|
- **Problem:** User swipes again during animation
|
|
- **Solution:** `isAnimating` flag blocks gestures and button clicks
|
|
- **Code:** `CardStack.handleSwipeStart()` checks `isExiting || isAdvancing`
|
|
|
|
### <3 Cards Remaining
|
|
- **Problem:** Not enough cards to fill stack
|
|
- **Solution:** `CardStack` renders only available cards (0-3)
|
|
- **Behavior:** Stack naturally shrinks as user approaches end
|
|
|
|
### Undo Functionality
|
|
- **Problem:** Undo reverses card to top, but animations may be in progress
|
|
- **Solution:** `useEffect` in `CardStack` resets animation states when `currentIndex` changes externally
|
|
- **Code:** `useEffect(() => { setIsExiting(false); ... }, [currentIndex])`
|
|
|
|
### Empty State
|
|
- **Problem:** No cards to render
|
|
- **Solution:** `CardStack` returns `null`, `page.tsx` shows empty state UI
|
|
- **Trigger:** `currentIndex >= recommendations.length`
|
|
|
|
## Mobile Performance
|
|
|
|
**Target:** 60fps on mobile devices
|
|
|
|
**Optimizations:**
|
|
- GPU-accelerated properties only (`transform`, `opacity`, not `left/top/width`)
|
|
- `will-change: transform, opacity` hints browser to optimize
|
|
- `backface-visibility: hidden` prevents rendering artifacts
|
|
- No layout shift (cards positioned absolutely)
|
|
|
|
**Tested On:**
|
|
- Chrome (desktop + mobile)
|
|
- Safari (iOS + macOS)
|
|
- Firefox
|
|
|
|
## User Experience
|
|
|
|
**Visual Hierarchy:**
|
|
- Top card: Full size, interactive, clear visuals
|
|
- Card 2: 95% scale, 95% opacity, visible but de-emphasized
|
|
- Card 3: 90% scale, 90% opacity, subtle depth cue
|
|
|
|
**Swipe Directions:**
|
|
- Left: Reject (red overlay, rotate left)
|
|
- Right: Request (green overlay, confirm toast, rotate right)
|
|
- Up: Dismiss (blue overlay, shrink up)
|
|
|
|
**Toast Confirmation:**
|
|
- Right swipe triggers toast modal
|
|
- User chooses: "Request" or "Mark as Liked"
|
|
- Card exit animation plays after choice
|
|
|
|
## Integration with Existing Features
|
|
|
|
### Settings Widget
|
|
- **Status:** No changes required
|
|
- **Behavior:** Opens over card stack, gestures disabled when modal open
|
|
|
|
### Undo Button
|
|
- **Status:** Works with stack
|
|
- **Behavior:** Triggers `loadRecommendations()` → Cards re-render from API
|
|
- **Animation:** No special animation (instant reset to fresh state)
|
|
|
|
### Progress Indicator
|
|
- **Status:** No changes required
|
|
- **Display:** Shows `currentIndex + 1 / recommendations.length`
|
|
|
|
### Desktop Buttons
|
|
- **Status:** Updated to disable during animations
|
|
- **Code:** `disabled={isAnimating}` in `RecommendationCard.tsx:217-234`
|
|
|
|
## Troubleshooting
|
|
|
|
### Cards Not Stacking
|
|
- **Check:** CSS classes applied correctly in `CardStack.tsx`
|
|
- **Verify:** `card-stack-position-{0,1,2}` classes present in `globals.css`
|
|
- **Debug:** Inspect z-index values (50, 40, 30)
|
|
|
|
### Cards Snapping Instead of Animating
|
|
- **Root Cause:** Exited card still in `visibleCards` array during advance phase
|
|
- **Fix:** During `isAdvancing`, `visibleCards` starts from `currentIndex + 1` (skips exited card)
|
|
- **Verify:** Check `CardStack.tsx:71-97` - advance branch excludes card at `currentIndex`
|
|
|
|
### Animations Not Playing
|
|
- **Check:** Exit/advance animation classes applied during state transitions
|
|
- **Verify:** `animationClass` computed correctly based on `card.fromPosition` during advance
|
|
- **Debug:** Console log `isExiting`, `exitDirection`, `isAdvancing`, `visibleCards`
|
|
|
|
### Gestures Not Working
|
|
- **Check:** `isDraggable` prop passed correctly (only true for top card)
|
|
- **Verify:** `isAnimating` not stuck in true state
|
|
- **Debug:** Check `CardStack` animation state machine
|
|
|
|
### Performance Issues
|
|
- **Check:** Animations targeting only `transform` and `opacity`
|
|
- **Verify:** `will-change` applied to `.card-stack-item`
|
|
- **Test:** Chrome DevTools Performance tab (60fps target)
|
|
|
|
## Related Files
|
|
|
|
- **Documentation:** `documentation/features/bookdate-prd.md` (BookDate feature spec)
|
|
- **Components:** `src/components/bookdate/LoadingScreen.tsx`, `SettingsWidget.tsx`
|
|
- **API:** `src/app/api/bookdate/swipe/route.ts`
|
|
|
|
## Future Enhancements (Not Implemented)
|
|
|
|
- **Preload Card 4:** Load image for 4th card in stack (currently loads on-demand)
|
|
- **Spring Physics:** Replace CSS easing with spring animations for more natural feel
|
|
- **Haptic Feedback:** Vibrate on swipe (requires Web Vibration API)
|
|
- **Parallax Effect:** Cards shift slightly on device tilt (requires DeviceOrientation API)
|