From ae11fec2ccdbc5a3cfdff931084f40d42d6a8c30 Mon Sep 17 00:00:00 2001 From: Kelly Lim Date: Wed, 8 Oct 2025 23:16:12 +0800 Subject: [PATCH 1/7] feat(learner): add contentDuration to learningUnits table and update seed --- .../migration.sql | 8 ++++++++ prisma/schema.prisma | 11 ++++++----- prisma/seed.ts | 4 ++++ 3 files changed, 18 insertions(+), 5 deletions(-) create mode 100644 prisma/migrations/20251008112310_update_content_duration/migration.sql diff --git a/prisma/migrations/20251008112310_update_content_duration/migration.sql b/prisma/migrations/20251008112310_update_content_duration/migration.sql new file mode 100644 index 00000000..eb19e5bc --- /dev/null +++ b/prisma/migrations/20251008112310_update_content_duration/migration.sql @@ -0,0 +1,8 @@ +/* + Warnings: + + - Added the required column `content_duration` to the `learning_units` table without a default value. This is not possible if the table is not empty. + +*/ +-- AlterTable +ALTER TABLE "public"."learning_units" ADD COLUMN "content_duration" DECIMAL(65,30) NOT NULL; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 241fb920..01158af3 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -55,11 +55,12 @@ model LearningUnit { updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(3) // Domain-Specific Fields. - title String @map("title") - contentType ContentType @map("content_type") - contentURL String @map("content_url") - summary String @map("summary") - createdBy String @map("created_by") + title String @map("title") + contentType ContentType @map("content_type") + contentURL String @map("content_url") + contentDuration Decimal @map("content_duration") + summary String @map("summary") + createdBy String @map("created_by") // Relations. collectionId BigInt @map("collection_id") diff --git a/prisma/seed.ts b/prisma/seed.ts index c0d684b6..634cd913 100644 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -130,6 +130,7 @@ const learningUnits: Prisma.LearningUnitCreateInput[] = [ questionAnswers: { create: questionAnswers, }, + contentDuration: 300, }, { id: 2, @@ -157,6 +158,7 @@ const learningUnits: Prisma.LearningUnitCreateInput[] = [ questionAnswers: { create: questionAnswers, }, + contentDuration: 300, }, { id: 3, @@ -184,6 +186,7 @@ const learningUnits: Prisma.LearningUnitCreateInput[] = [ questionAnswers: { create: questionAnswers, }, + contentDuration: 300, }, { id: 4, @@ -211,6 +214,7 @@ const learningUnits: Prisma.LearningUnitCreateInput[] = [ questionAnswers: { create: questionAnswers, }, + contentDuration: 300, }, ]; From 1e3ced49d16f5ea44c1a722e9fb204741705789d Mon Sep 17 00:00:00 2001 From: Kelly Lim Date: Thu, 9 Oct 2025 15:55:21 +0800 Subject: [PATCH 2/7] feat(learner): add progress state for time remaining --- src/lib/states/player.svelte.ts | 7 ++ src/routes/(protected)/(core)/+page.server.ts | 11 ++- src/routes/(protected)/(core)/+page.svelte | 72 ++++++++++++++++--- 3 files changed, 78 insertions(+), 12 deletions(-) diff --git a/src/lib/states/player.svelte.ts b/src/lib/states/player.svelte.ts index c682293b..9f777931 100644 --- a/src/lib/states/player.svelte.ts +++ b/src/lib/states/player.svelte.ts @@ -62,6 +62,13 @@ export class Player extends EventTarget { }; this.#audio.ontimeupdate = () => { this.#progress = this.#audio?.currentTime || 0; + this.dispatchEvent( + new CustomEvent('progress', { + detail: { + currentTime: this.#audio?.currentTime, + }, + }), + ); }; this.#audio.onended = () => { this.#isPlaying = false; diff --git a/src/routes/(protected)/(core)/+page.server.ts b/src/routes/(protected)/(core)/+page.server.ts index 3d496469..7f742518 100644 --- a/src/routes/(protected)/(core)/+page.server.ts +++ b/src/routes/(protected)/(core)/+page.server.ts @@ -24,6 +24,7 @@ export const load: PageServerLoad = async (event) => { select: { id: true, title: true, + contentDuration: true, contentURL: true, createdAt: true, createdBy: true, @@ -58,8 +59,16 @@ export const load: PageServerLoad = async (event) => { throw error(500); } + const journeys = learningJourneys.map((journey) => ({ + ...journey, + learningUnit: { + ...journey.learningUnit, + contentDuration: journey.learningUnit.contentDuration.toNumber(), + }, + })); + return { - learningJourneys, + learningJourneys: journeys, username: user.name, }; }; diff --git a/src/routes/(protected)/(core)/+page.svelte b/src/routes/(protected)/(core)/+page.svelte index 156d2435..c5d62f02 100644 --- a/src/routes/(protected)/(core)/+page.svelte +++ b/src/routes/(protected)/(core)/+page.svelte @@ -1,4 +1,6 @@
@@ -49,9 +91,17 @@ player={{ isactive: player.currentTrack?.id === learningJourney.learningUnit.id, isplaying: player.isPlaying, - onplay: () => handleLearningUnitPlay(learningJourney), + onplay: () => + handleLearningUnitPlay( + learningJourney, + progressState.find((ps) => ps.id === learningJourney.learningUnit.id)?.progress, + ), onpause: handleLearningUnitPause, onresume: handleLearningUnitResume, + timeremaining: (() => { + const ps = progressState.find((ps) => ps.id === learningJourney.learningUnit.id); + return Math.max((ps?.contentDuration ?? 0) - (ps?.progress ?? 0), 0); + })(), }} /> {/each} From a877f8731d4f5ceca82972026d702df11ef84963 Mon Sep 17 00:00:00 2001 From: Kelly Lim Date: Thu, 9 Oct 2025 17:08:41 +0800 Subject: [PATCH 3/7] feat(learner): add time remaining as props --- .../LearningUnit/LearningUnit.svelte | 31 +++++++++---------- 1 file changed, 14 insertions(+), 17 deletions(-) diff --git a/src/lib/components/LearningUnit/LearningUnit.svelte b/src/lib/components/LearningUnit/LearningUnit.svelte index 03200f50..45c9a557 100644 --- a/src/lib/components/LearningUnit/LearningUnit.svelte +++ b/src/lib/components/LearningUnit/LearningUnit.svelte @@ -5,6 +5,7 @@ import { Badge, type BadgeProps } from '$lib/components/Badge/index.js'; import { Button } from '$lib/components/Button/index.js'; + import { ProgressSpinner } from '$lib/components/ProgressSpinner/index.js'; export interface Props { /** @@ -52,6 +53,10 @@ * A callback to resume playback. */ onresume: () => void; + /** + * The time remaining in the playback. + */ + timeremaining: number; } | null; } @@ -77,6 +82,11 @@ player?.onresume(); }; + + const formatTimeRemaining = (timeInSeconds: number): string => { + const minutes = Math.ceil(timeInSeconds / 60); + return `${minutes}m left`; + }; - - - - - - 23m left + + + {player.timeremaining ? formatTimeRemaining(player.timeremaining) : '0m left'} + {/if} From 873bbedec369377ac01f2653d3ae758861c9831e Mon Sep 17 00:00:00 2001 From: Kelly Lim Date: Mon, 13 Oct 2025 14:43:03 +0800 Subject: [PATCH 4/7] feat(learner): update ProgressSpinner Component --- .../LearningUnit/LearningUnit.svelte | 9 +++- .../ProgressSpinner/ProgressSpinner.svelte | 51 +++++++++++++++++++ src/lib/components/ProgressSpinner/index.ts | 4 ++ src/routes/(protected)/(core)/+page.svelte | 3 ++ 4 files changed, 66 insertions(+), 1 deletion(-) create mode 100644 src/lib/components/ProgressSpinner/ProgressSpinner.svelte create mode 100644 src/lib/components/ProgressSpinner/index.ts diff --git a/src/lib/components/LearningUnit/LearningUnit.svelte b/src/lib/components/LearningUnit/LearningUnit.svelte index 45c9a557..5fe15d89 100644 --- a/src/lib/components/LearningUnit/LearningUnit.svelte +++ b/src/lib/components/LearningUnit/LearningUnit.svelte @@ -57,6 +57,10 @@ * The time remaining in the playback. */ timeremaining: number; + /** + * The duration of the playback. + */ + duration: number; } | null; } @@ -135,7 +139,10 @@ {/if}
- + {player.timeremaining ? formatTimeRemaining(player.timeremaining) : '0m left'} diff --git a/src/lib/components/ProgressSpinner/ProgressSpinner.svelte b/src/lib/components/ProgressSpinner/ProgressSpinner.svelte new file mode 100644 index 00000000..c11c00bb --- /dev/null +++ b/src/lib/components/ProgressSpinner/ProgressSpinner.svelte @@ -0,0 +1,51 @@ + + + + + + + + diff --git a/src/lib/components/ProgressSpinner/index.ts b/src/lib/components/ProgressSpinner/index.ts new file mode 100644 index 00000000..1bc79c35 --- /dev/null +++ b/src/lib/components/ProgressSpinner/index.ts @@ -0,0 +1,4 @@ +export { + default as ProgressSpinner, + type Props as ProgressSpinnerProps, +} from './ProgressSpinner.svelte'; diff --git a/src/routes/(protected)/(core)/+page.svelte b/src/routes/(protected)/(core)/+page.svelte index c5d62f02..3bb3cb6f 100644 --- a/src/routes/(protected)/(core)/+page.svelte +++ b/src/routes/(protected)/(core)/+page.svelte @@ -98,6 +98,9 @@ ), onpause: handleLearningUnitPause, onresume: handleLearningUnitResume, + duration: + progressState.find((ps) => ps.id === learningJourney.learningUnit.id) + ?.contentDuration ?? 0, timeremaining: (() => { const ps = progressState.find((ps) => ps.id === learningJourney.learningUnit.id); return Math.max((ps?.contentDuration ?? 0) - (ps?.progress ?? 0), 0); From ede45bd9c2fbb149d64ab2c57003213e83ce7ae1 Mon Sep 17 00:00:00 2001 From: Kelly Lim Date: Mon, 13 Oct 2025 15:59:38 +0800 Subject: [PATCH 5/7] feat: update progress arc from rounded to flat --- src/lib/components/ProgressSpinner/ProgressSpinner.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/components/ProgressSpinner/ProgressSpinner.svelte b/src/lib/components/ProgressSpinner/ProgressSpinner.svelte index c11c00bb..83599438 100644 --- a/src/lib/components/ProgressSpinner/ProgressSpinner.svelte +++ b/src/lib/components/ProgressSpinner/ProgressSpinner.svelte @@ -42,7 +42,7 @@ stroke="#E2E8F0" fill="none" stroke-width="3.5px" - stroke-linecap="round" + stroke-linecap="butt" stroke-dasharray={dashArray} stroke-dashoffset={dashOffset} transform="rotate(-90 12 12)" From 33b985cf9fc69345582e991a4640998182fc0391 Mon Sep 17 00:00:00 2001 From: Kelly Lim Date: Mon, 13 Oct 2025 17:40:37 +0800 Subject: [PATCH 6/7] feat: update text for plural and singular and add less than --- src/lib/components/LearningUnit/LearningUnit.svelte | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/lib/components/LearningUnit/LearningUnit.svelte b/src/lib/components/LearningUnit/LearningUnit.svelte index 5fe15d89..0ea2e506 100644 --- a/src/lib/components/LearningUnit/LearningUnit.svelte +++ b/src/lib/components/LearningUnit/LearningUnit.svelte @@ -88,8 +88,12 @@ }; const formatTimeRemaining = (timeInSeconds: number): string => { + if (timeInSeconds > 0 && timeInSeconds < 60) { + return '< 1 min left'; + } const minutes = Math.ceil(timeInSeconds / 60); - return `${minutes}m left`; + const unit = minutes === 1 ? 'min' : 'mins'; + return `${minutes} ${unit} left`; }; From e5d0790c695797236d0eec0368598b303105d211 Mon Sep 17 00:00:00 2001 From: Kelly Lim Date: Mon, 13 Oct 2025 17:46:12 +0800 Subject: [PATCH 7/7] feat: update 0 mins left to formatTimeRemaining function --- src/lib/components/LearningUnit/LearningUnit.svelte | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/lib/components/LearningUnit/LearningUnit.svelte b/src/lib/components/LearningUnit/LearningUnit.svelte index 0ea2e506..fb7ec499 100644 --- a/src/lib/components/LearningUnit/LearningUnit.svelte +++ b/src/lib/components/LearningUnit/LearningUnit.svelte @@ -88,9 +88,8 @@ }; const formatTimeRemaining = (timeInSeconds: number): string => { - if (timeInSeconds > 0 && timeInSeconds < 60) { - return '< 1 min left'; - } + if (timeInSeconds <= 0) return '0 mins left'; + if (timeInSeconds < 60) return '< 1 min left'; const minutes = Math.ceil(timeInSeconds / 60); const unit = minutes === 1 ? 'min' : 'mins'; return `${minutes} ${unit} left`; @@ -148,7 +147,7 @@ currentTime={player ? player.duration - player.timeremaining : 0} /> - {player.timeremaining ? formatTimeRemaining(player.timeremaining) : '0m left'} + {formatTimeRemaining(player.timeremaining ?? 0)}