add progression on podcasts
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 20s
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 20s
This commit is contained in:
parent
143e453f78
commit
a7c4aa5608
@ -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 (
|
||||
<article className="group lift bg-surface rounded-3xl shadow-organic-sm hover:shadow-organic overflow-hidden">
|
||||
|
||||
57
src/hooks/useListeningProgress.ts
Normal file
57
src/hooks/useListeningProgress.ts
Normal file
@ -0,0 +1,57 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { supabase } from '@/lib/supabase'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
|
||||
type ProgressMap = Map<string, { seconds: number; percent: number }>
|
||||
|
||||
let cachedProgress: ProgressMap = new Map()
|
||||
let lastFetch = 0
|
||||
|
||||
export function useListeningProgress() {
|
||||
const user = useAuthStore((s) => s.user)
|
||||
const [progress, setProgress] = useState<ProgressMap>(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
|
||||
}
|
||||
@ -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() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center gap-2 mt-5">
|
||||
{progressInfo && progressInfo.percent > 0 && (
|
||||
<div className="mt-5">
|
||||
<div className="h-1.5 bg-border-light rounded-full overflow-hidden">
|
||||
<div className="h-full bg-primary rounded-full transition-all" style={{ width: `${Math.min(progressInfo.percent, 100)}%` }} />
|
||||
</div>
|
||||
<p className="text-xs text-text-secondary mt-1">{formatDuration(progressInfo.seconds)} / {formatDuration(podcast.duration_seconds)}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center gap-2 mt-3">
|
||||
<Button
|
||||
onClick={() => isActive ? toggle() : play(podcast)}
|
||||
size="lg"
|
||||
>
|
||||
{isActive && isPlaying ? <Pause size={18} /> : <Play size={18} className="ml-0.5" />}
|
||||
{isActive && isPlaying ? 'Pause' : 'Écouter'}
|
||||
{isActive && isPlaying ? 'Pause' : progressInfo && !isActive ? `Reprendre à ${formatDuration(progressInfo.seconds)}` : 'Écouter'}
|
||||
</Button>
|
||||
<Button
|
||||
variant={isLiked ? 'accent' : 'secondary'}
|
||||
|
||||
@ -4,6 +4,7 @@ import { isExternalUrl, getEmbedInfo } from '@/lib/embed'
|
||||
import { publicUrl } from '@/lib/storage'
|
||||
import { supabase } from '@/lib/supabase'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { invalidateProgressCache } from '@/hooks/useListeningProgress'
|
||||
|
||||
// ──── YouTube IFrame API ────
|
||||
declare global {
|
||||
@ -84,7 +85,10 @@ async function saveProgressNow(podcastId: string, seconds: number, duration: num
|
||||
{ onConflict: 'user_id,podcast_id' }
|
||||
)
|
||||
if (error) console.error('[player] save progress failed:', error)
|
||||
else console.log('[player] saved progress:', podcastId, Math.floor(seconds), 'seconds')
|
||||
else {
|
||||
console.log('[player] saved progress:', podcastId, Math.floor(seconds), 'seconds')
|
||||
invalidateProgressCache()
|
||||
}
|
||||
}
|
||||
|
||||
function debouncedSaveProgress(podcastId: string, seconds: number, duration: number) {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user