Summary
Build course progress tracking UI: persist playback position and completion per course, offer "Resume where you left off" on the course detail page, and show progress bars on course cards. This issue defines the frontend data contract and ships the UI with an optimistic local layer; the matching persistence endpoints are a small paired backend task.
Current state
- There is no progress tracking anywhere. A purchased course plays through
components/atoms/dashboard/vid-player-box.jsx — a Vidstack MediaPlayer fed a single data?.video URL with no position persistence, no onTimeUpdate/onEnded handlers, and hardcoded demo subtitle/thumbnail tracks pointing at files.vidstack.io.
- Courses are currently single-video (one
course.video field), not multi-lesson — the contract below must handle that today while being forward-compatible with lessons.
app/dashboard/courses/[courseId]/CourseDetailPageClient.jsx gates playback on canAccess (useHasCourse from hooks/usePurchase.js, which checks user.purchasedCourses), but renders the player from time zero every visit.
components/molecules/dashboard/cards/courseCard.jsx shows thumbnail/category/price/bookmark — no progress indicator. @radix-ui/react-progress (^1.1.4) is already installed and wrapped by the shadcn components/ui/progress primitive.
- Out of scope (owned by an existing open issue): the dashboard-home widgets with placeholder values, e.g. the hardcoded
chartData in components/organisms/dashboard/LearningProgress.jsx. Do not touch those here; expose your data via the hook so that issue can consume it later.
What to build
- Frontend contract (document it in the PR; align with the paired backend issue before merging):
GET /api/progress/courses → { success, progress: [{ courseId, percent, positionSeconds, durationSeconds, completed, lessonId?, updatedAt }] }
PUT /api/progress/course/:courseId with { positionSeconds, durationSeconds, lessonId?, completed? } → upserted record.
lessonId is optional/null for today's single-video courses; the shape supports arrays of lessons later without a breaking change.
hooks/useCourseProgress.js:
- Reads progress for one course (and a
useAllCourseProgress variant for card grids — one batched fetch, not N requests).
reportProgress(positionSeconds, durationSeconds) — throttled/debounced writes (every ~15s of playback, plus on pause and on unmount via visibilitychange/cleanup), optimistic local state, and a localStorage write-behind (dnb:progress:<userId>:<courseId>) so progress survives offline/failed PUTs and is flushed on next load.
- Completion rule: mark
completed: true when position/duration ≥ 0.9 (90% watched) or on the player's ended event; completion is sticky (never un-completes by scrubbing back).
- Player integration: extend
VidPlayerBox with optional startTime and onProgress/onEnded props using Vidstack's media events (onTimeUpdate, onEnded, or a MediaPlayer ref + subscription). Course detail wires these to the hook. While in there, remove or make conditional the hardcoded files.vidstack.io subtitle/thumbnail demo URLs (they 404-spam for real courses).
- Resume UX on
CourseDetailPageClient.jsx: if saved progress exists and is >30s and <90%, show "Resume from 12:34" and "Start over" affordances before/over the player; picking resume seeds startTime.
- Progress bars on cards: in
courseCard.jsx, for courses the user owns (user.purchasedCourses), render a slim progress bar (shadcn/Radix Progress) with percent, and a "Completed" check/badge state at 100%. Unowned courses keep the current price badge — no layout jank for the common case.
- Graceful degradation: if the backend endpoints 404 (not yet deployed), the hook must operate on
localStorage only, silently — the UI works either way. Feature-detect once per session rather than erroring on every write.
Acceptance criteria
Pointers
components/atoms/dashboard/vid-player-box.jsx, app/dashboard/courses/[courseId]/CourseDetailPageClient.jsx, components/molecules/dashboard/cards/courseCard.jsx, hooks/usePurchase.js, hooks/useAuth.js (user identity for storage keys), lib/config/axios.config.js.
- Vidstack media events/state: https://vidstack.io/docs/player/core-concepts/state-management
- Gotcha:
courseCard.jsx is rendered in grids — keep useAllCourseProgress at the page level and pass percent down as a prop to avoid N fetches. Respect the existing open issue boundary: no changes to components/organisms/dashboard/LearningProgress.jsx or other dashboard-home widgets.
- Lockfile must stay npm-compatible; PRs target
dev.
Difficulty
Medium — clean scope with an existing player and card to extend, but requires a well-designed sync contract, throttling/offline write-behind, and forward compatibility with a future lessons model.
🏆 GrantFox OSS — Official Campaign | FWC26. Apply for this issue through the GrantFox campaign page. The maintainer assigns one contributor before work starts; unassigned PRs may not be reviewed. PRs target the dev branch. Quality bar: CI must stay green.
💬 Questions or need help? Reach the maintainers and other contributors on the DeenBridge Telegram: https://t.me/+nst9lXNj1wc4ZDE0
Summary
Build course progress tracking UI: persist playback position and completion per course, offer "Resume where you left off" on the course detail page, and show progress bars on course cards. This issue defines the frontend data contract and ships the UI with an optimistic local layer; the matching persistence endpoints are a small paired backend task.
Current state
components/atoms/dashboard/vid-player-box.jsx— a VidstackMediaPlayerfed a singledata?.videoURL with no position persistence, noonTimeUpdate/onEndedhandlers, and hardcoded demo subtitle/thumbnail tracks pointing atfiles.vidstack.io.course.videofield), not multi-lesson — the contract below must handle that today while being forward-compatible with lessons.app/dashboard/courses/[courseId]/CourseDetailPageClient.jsxgates playback oncanAccess(useHasCoursefromhooks/usePurchase.js, which checksuser.purchasedCourses), but renders the player from time zero every visit.components/molecules/dashboard/cards/courseCard.jsxshows thumbnail/category/price/bookmark — no progress indicator.@radix-ui/react-progress(^1.1.4) is already installed and wrapped by the shadcncomponents/ui/progressprimitive.chartDataincomponents/organisms/dashboard/LearningProgress.jsx. Do not touch those here; expose your data via the hook so that issue can consume it later.What to build
GET /api/progress/courses→{ success, progress: [{ courseId, percent, positionSeconds, durationSeconds, completed, lessonId?, updatedAt }] }PUT /api/progress/course/:courseIdwith{ positionSeconds, durationSeconds, lessonId?, completed? }→ upserted record.lessonIdis optional/null for today's single-video courses; the shape supports arrays of lessons later without a breaking change.hooks/useCourseProgress.js:useAllCourseProgressvariant for card grids — one batched fetch, not N requests).reportProgress(positionSeconds, durationSeconds)— throttled/debounced writes (every ~15s of playback, plus on pause and on unmount viavisibilitychange/cleanup), optimistic local state, and alocalStoragewrite-behind (dnb:progress:<userId>:<courseId>) so progress survives offline/failed PUTs and is flushed on next load.completed: truewhenposition/duration ≥ 0.9(90% watched) or on the player'sendedevent; completion is sticky (never un-completes by scrubbing back).VidPlayerBoxwith optionalstartTimeandonProgress/onEndedprops using Vidstack's media events (onTimeUpdate,onEnded, or aMediaPlayerref + subscription). Course detail wires these to the hook. While in there, remove or make conditional the hardcodedfiles.vidstack.iosubtitle/thumbnail demo URLs (they 404-spam for real courses).CourseDetailPageClient.jsx: if saved progress exists and is >30s and <90%, show "Resume from 12:34" and "Start over" affordances before/over the player; picking resume seedsstartTime.courseCard.jsx, for courses the user owns (user.purchasedCourses), render a slim progress bar (shadcn/RadixProgress) with percent, and a "Completed" check/badge state at 100%. Unowned courses keep the current price badge — no layout jank for the common case.localStorageonly, silently — the UI works either way. Feature-detect once per session rather than erroring on every write.Acceptance criteria
/dashboard/coursesshow accurate progress bars from one batched request; unowned cards are visually unchanged.files.vidstack.iodemo tracks no longer load for real course videos.npm run lintandnpm run buildpass; CI green.Pointers
components/atoms/dashboard/vid-player-box.jsx,app/dashboard/courses/[courseId]/CourseDetailPageClient.jsx,components/molecules/dashboard/cards/courseCard.jsx,hooks/usePurchase.js,hooks/useAuth.js(user identity for storage keys),lib/config/axios.config.js.courseCard.jsxis rendered in grids — keepuseAllCourseProgressat the page level and pass percent down as a prop to avoid N fetches. Respect the existing open issue boundary: no changes tocomponents/organisms/dashboard/LearningProgress.jsxor other dashboard-home widgets.dev.Difficulty
Medium — clean scope with an existing player and card to extend, but requires a well-designed sync contract, throttling/offline write-behind, and forward compatibility with a future lessons model.
🏆 GrantFox OSS — Official Campaign | FWC26. Apply for this issue through the GrantFox campaign page. The maintainer assigns one contributor before work starts; unassigned PRs may not be reviewed. PRs target the
devbranch. Quality bar: CI must stay green.💬 Questions or need help? Reach the maintainers and other contributors on the DeenBridge Telegram: https://t.me/+nst9lXNj1wc4ZDE0