---
phase: 35-bug-fixes
plan: "02"
subsystem: ui
tags: [react, tailwind, skeleton, lazy-loading, image, s3]

# Dependency graph
requires:
  - phase: 35-bug-fixes
    provides: FIX-02 image URL resolution (withImageUrls on server)
provides:
  - GearImage lazy loading with onLoad callback forwarding
  - Animated skeleton placeholder (bg-gray-100 animate-pulse) on ItemCard, CandidateCard, GlobalItemCard
  - Fade-in transition (opacity-0 to opacity-100) on image load
affects: [any future plan touching GearImage, ItemCard, CandidateCard, GlobalItemCard]

# Tech tracking
tech-stack:
  added: []
  patterns: [image-skeleton-pattern, lazy-loading-native]

key-files:
  created: []
  modified:
    - src/client/components/GearImage.tsx
    - src/client/components/ItemCard.tsx
    - src/client/components/CandidateCard.tsx
    - src/client/components/GlobalItemCard.tsx

key-decisions:
  - "FIX-03: Use browser-native loading=lazy (no library) for image deferral"
  - "FIX-03: Skeleton is absolute-positioned overlay removed on onLoad, not conditional render swap"
  - "FIX-03: Fade-in via className prop on GearImage forwarded to img elements — no wrapper div needed in GearImage itself"

patterns-established:
  - "Image skeleton pattern: relative wrapper + absolute inset-0 bg-gray-100 animate-pulse + opacity transition on GearImage className"
  - "onLoad forwarding: GearImage accepts optional onLoad prop, passes it to all three img render paths"

requirements-completed: [FIX-03]

# Metrics
duration: 3min
completed: 2026-04-19
---

# Phase 35 Plan 02: Image Lazy Loading and Skeleton Summary

**Browser-native lazy loading on all GearImage img elements with animated pulse skeleton and opacity fade-in on ItemCard, CandidateCard, and GlobalItemCard**

## Performance

- **Duration:** 3 min
- **Started:** 2026-04-19T18:04:32Z
- **Completed:** 2026-04-19T18:07:14Z
- **Tasks:** 2
- **Files modified:** 4

## Accomplishments
- Added `loading="lazy"` and optional `onLoad` prop to GearImage, forwarded to all three img render paths (cover, hasCrop, default)
- Added `useState(false)` loaded state and skeleton overlay to ItemCard, CandidateCard, and GlobalItemCard
- Images fade in via `transition-opacity duration-200` once `onLoad` fires; skeleton is removed simultaneously
- No-image placeholders (category icon on bg-gray-50) unchanged in all three cards

## Task Commits

Each task was committed atomically:

1. **Task 1: Add loading=lazy and onLoad prop to GearImage** - `2d2259a` (feat)
2. **Task 2: Add image skeleton to ItemCard, CandidateCard, GlobalItemCard** - `88db308` (feat)

**Plan metadata:** (docs commit below)

## Files Created/Modified
- `src/client/components/GearImage.tsx` - Added `onLoad?: () => void` to props, destructured and forwarded to all three `<img>` elements alongside `loading="lazy"`
- `src/client/components/ItemCard.tsx` - Added `useState` import, `loaded` state, skeleton overlay, and fade-in className on GearImage
- `src/client/components/CandidateCard.tsx` - Same skeleton pattern as ItemCard
- `src/client/components/GlobalItemCard.tsx` - Same skeleton pattern; SVG icon placeholder preserved unchanged

## Decisions Made
- Used browser-native `loading="lazy"` — no third-party library needed, zero bundle overhead
- Skeleton is an `absolute inset-0` overlay (not a conditional branch swap) so layout is stable during load
- Import order fixed for Biome's `organizeImports` rule: `@tanstack/react-router` before `react` before `react-i18next`

## Deviations from Plan

### Auto-fixed Issues

**1. [Rule 1 - Bug] Fixed Biome import order violations in all three card files**
- **Found during:** Task 2 (after adding `useState` import)
- **Issue:** Biome `organizeImports` requires alphabetical order — `"react"` placed before `"@tanstack/react-router"` caused lint errors
- **Fix:** Reordered imports: `@tanstack/react-router` → `react` → `react-i18next` in ItemCard and CandidateCard; `@tanstack/react-router` → `react` in GlobalItemCard
- **Files modified:** ItemCard.tsx, CandidateCard.tsx, GlobalItemCard.tsx
- **Verification:** `bunx @biomejs/biome check` on all 4 files — no errors
- **Committed in:** 88db308 (Task 2 commit)

---

**Total deviations:** 1 auto-fixed (Rule 1 — import order)
**Impact on plan:** Necessary for lint compliance. No logic or behavior changes.

## Issues Encountered
- Pre-existing lint errors in `scripts/crawl-all.ts`, `scripts/crawl-manufacturer.ts`, and `tests/services/manufacturer.service.test.ts` are unrelated to this plan — logged as out-of-scope, not fixed.

## User Setup Required
None - no external service configuration required.

## Next Phase Readiness
- FIX-03 resolved — all card types now show skeleton while presigned S3 URLs resolve, then fade in
- Ready for 35-03 (FIX-05: cursor pointer on clickable links)

---
*Phase: 35-bug-fixes*
*Completed: 2026-04-19*
