critical priority medium complexity frontend pending frontend specialist Tier 3

Acceptance Criteria

Widget renders a horizontal scroll list with earned badges appearing before locked badges
Each badge card uses the badge-card-widget component without duplication of badge rendering logic
Earned count and locked count match the counts in badge-bloc state at all times
Badge cards have a minimum touch target of 44×44 logical pixels (pt) on both iOS and Android
All text and icon elements within badge cards meet WCAG 2.2 AA contrast requirements
VoiceOver and TalkBack can navigate each badge card in the shelf sequentially using swipe gestures
Each badge card exposes a Semantics label: '[Badge name], [earned / X of Y criteria met]'
The scroll container itself has a Semantics label: 'Badge shelf, [N] badges' and is identified as a scrollable list
An empty state message is shown when no badges exist for the peer mentor
Loading state renders a shimmer or skeleton row while badge-bloc is fetching
Tapping a badge card opens badge-detail-modal (task-011) correctly
Widget does not overflow its parent container vertically; horizontal overflow is scrollable

Technical Requirements

frameworks
Flutter
BLoC
data models
Badge
BadgeProgress
EarnedBadge
performance requirements
ListView.builder used for efficient rendering — no fixed-length children list
Scroll performance maintains 60fps with up to 50 badge cards
Badge images/icons are cached to avoid redundant network requests
ui components
badge-card-widget (reused per card)
ListView.builder with scrollDirection: Axis.horizontal
Semantics with scrollable: true for the list container
MergeSemantics or ExcludeSemantics as appropriate for child card content
Shimmer/skeleton widget for loading state
Empty state widget

Execution Context

Execution Tier
Tier 3

Tier 3 - 413 tasks

Can start after Tier 2 completes

Implementation Notes

Sort badge list client-side after receiving from bloc: earned badges sorted by earnedAt descending, then locked badges sorted by progressPercent descending (highest progress first to motivate completion). Use ListView.builder(scrollDirection: Axis.horizontal, itemCount: sortedBadges.length) — never a Row with fixed children. Wrap the entire ListView in a Semantics widget with label, scrollable: true, and explicitChildNodes: true so each child's Semantics is individually exposed. Set each badge-card-widget's container constraints to a minimum size of 44×44 using ConstrainedBox or SizedBox to guarantee touch target compliance.

Use CachedNetworkImage (or Flutter's Image.network with cacheWidth) for badge icons. Integrate with badge-detail-modal by passing the BadgeModel to a route or callback — keep navigation logic out of the widget itself (use a callback parameter).

Testing Requirements

Write widget tests (flutter_test): (1) earned badges render before locked badges, (2) badge count in widget matches mocked bloc state, (3) empty state widget renders when bloc emits empty list, (4) loading state renders skeleton when bloc is in loading state, (5) tapping a badge card invokes the detail modal navigation. Use MockBadgeBloc (mockito or bloc_test) to inject controlled states. Assert Semantics tree includes expected labels using tester.getSemantics(). Perform manual TalkBack/VoiceOver test on device to verify swipe navigation through all cards.

Integration test: mount shelf within a profile page scaffold and confirm layout does not overflow. Minimum 80% widget test coverage.

Component
Badge Shelf Widget
ui medium
Epic Risks (2)
medium impact medium prob integration

The badge-earned-celebration overlay must appear within 2 seconds of the triggering activity being saved, but badge evaluation runs server-side in an edge function triggered by a database webhook. Network latency, edge function cold start, and Supabase Realtime delivery delays could cause the overlay to appear late or not at all, breaking the motivational loop.

Mitigation & Contingency

Mitigation: Implement an optimistic UI path: after activity save, badge-bloc immediately checks whether any badge thresholds are crossed client-side using cached stats and badge definitions, showing the overlay speculatively before server confirmation. The server result then reconciles. Subscribe to Supabase Realtime on the earned_badges table for authoritative confirmation.

Contingency: If Realtime delivery is unreliable in production, add a polling fallback: badge-bloc polls for new earned badges 3 seconds after an activity save and shows the overlay if a new record is detected, accepting up to 5-second latency as a fallback SLA.

high impact low prob technical

The celebration overlay uses animation for positive reinforcement, but motion sensitivity (prefers-reduced-motion) and screen reader users require a non-animated or text-only alternative. Failing to handle this risks excluding Blindeforbundet users or triggering vestibular discomfort for motion-sensitive volunteers.

Mitigation & Contingency

Mitigation: Check MediaQuery.disableAnimations in badge-earned-celebration-overlay and skip animation entirely when true, showing a static card instead. Add an ExcludeSemantics wrapper around the decorative animation widget and a separate Semantics node with a live region announcement of the badge name and congratulatory message.

Contingency: If accessibility issues are identified in TestFlight testing with Blindeforbundet's test group, fast-track a patch that defaults to the static card path and gates the animation behind a user preference setting in notification preferences.