From a7c4aa56088222fc6d7bb5eca4a3d94427e2ac56 Mon Sep 17 00:00:00 2001 From: ordinarthur <@arthurbarre.js@gmail.com> Date: Mon, 13 Apr 2026 10:55:08 +0200 Subject: [PATCH] add progression on podcasts --- src/components/podcast/PodcastCard.tsx | 5 ++- src/hooks/useListeningProgress.ts | 57 ++++++++++++++++++++++++++ src/pages/PodcastDetail.tsx | 16 +++++++- src/stores/player.ts | 6 ++- 4 files changed, 80 insertions(+), 4 deletions(-) create mode 100644 src/hooks/useListeningProgress.ts diff --git a/src/components/podcast/PodcastCard.tsx b/src/components/podcast/PodcastCard.tsx index 8d31199..46ff985 100644 --- a/src/components/podcast/PodcastCard.tsx +++ b/src/components/podcast/PodcastCard.tsx @@ -6,6 +6,7 @@ import { publicUrl } from '@/lib/storage' import { Badge } from '@/components/ui/Badge' import { Avatar } from '@/components/ui/Avatar' import { usePlayerStore } from '@/stores/player' +import { useListeningProgress } from '@/hooks/useListeningProgress' const coverGradients = [ 'from-primary/20 via-primary/5 to-accent/10', @@ -25,11 +26,13 @@ interface PodcastCardProps { progressPercent?: number } -export function PodcastCard({ podcast, progressPercent }: PodcastCardProps) { +export function PodcastCard({ podcast, progressPercent: propProgress }: PodcastCardProps) { const play = usePlayerStore((s) => s.play) const currentId = usePlayerStore((s) => s.current?.id) const isPlaying = usePlayerStore((s) => s.isPlaying) const isActive = currentId === podcast.id + const progressMap = useListeningProgress() + const progressPercent = propProgress ?? progressMap.get(podcast.id)?.percent return (
diff --git a/src/hooks/useListeningProgress.ts b/src/hooks/useListeningProgress.ts new file mode 100644 index 0000000..beb8d22 --- /dev/null +++ b/src/hooks/useListeningProgress.ts @@ -0,0 +1,57 @@ +import { useEffect, useState } from 'react' +import { supabase } from '@/lib/supabase' +import { useAuthStore } from '@/stores/auth' + +type ProgressMap = Map + +let cachedProgress: ProgressMap = new Map() +let lastFetch = 0 + +export function useListeningProgress() { + const user = useAuthStore((s) => s.user) + const [progress, setProgress] = useState(cachedProgress) + + useEffect(() => { + if (!user) { + cachedProgress = new Map() + setProgress(cachedProgress) + return + } + + // Refetch at most every 10s + const now = Date.now() + if (now - lastFetch < 10_000 && cachedProgress.size > 0) { + setProgress(cachedProgress) + return + } + + supabase + .from('listen_history') + .select('podcast_id, progress_seconds, podcast:podcasts(duration_seconds)') + .eq('user_id', user.id) + .eq('completed', false) + .gt('progress_seconds', 5) + .then(({ data }) => { + const map: ProgressMap = new Map() + if (data) { + for (const row of data) { + const dur = (row as any).podcast?.duration_seconds || 0 + const pct = dur > 0 ? (row.progress_seconds / dur) * 100 : 0 + if (pct > 0 && pct < 95) { + map.set(row.podcast_id, { seconds: row.progress_seconds, percent: pct }) + } + } + } + cachedProgress = map + lastFetch = Date.now() + setProgress(map) + }) + }, [user]) + + return progress +} + +/** Invalidate cache so next render refetches */ +export function invalidateProgressCache() { + lastFetch = 0 +} diff --git a/src/pages/PodcastDetail.tsx b/src/pages/PodcastDetail.tsx index 6ec9362..63d4e0c 100644 --- a/src/pages/PodcastDetail.tsx +++ b/src/pages/PodcastDetail.tsx @@ -10,6 +10,7 @@ import { Avatar } from '@/components/ui/Avatar' import { Badge } from '@/components/ui/Badge' import { publicUrl } from '@/lib/storage' import { Button } from '@/components/ui/Button' +import { useListeningProgress } from '@/hooks/useListeningProgress' export function PodcastDetail() { const { id } = useParams<{ id: string }>() @@ -23,6 +24,8 @@ export function PodcastDetail() { const [loading, setLoading] = useState(true) const isActive = current?.id === id + const progressMap = useListeningProgress() + const progressInfo = id ? progressMap.get(id) : undefined useEffect(() => { if (!id) return @@ -141,13 +144,22 @@ export function PodcastDetail() { )} -
+ {progressInfo && progressInfo.percent > 0 && ( +
+
+
+
+

{formatDuration(progressInfo.seconds)} / {formatDuration(podcast.duration_seconds)}

+
+ )} + +