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 { Badge } from '@/components/ui/Badge'
|
||||||
import { Avatar } from '@/components/ui/Avatar'
|
import { Avatar } from '@/components/ui/Avatar'
|
||||||
import { usePlayerStore } from '@/stores/player'
|
import { usePlayerStore } from '@/stores/player'
|
||||||
|
import { useListeningProgress } from '@/hooks/useListeningProgress'
|
||||||
|
|
||||||
const coverGradients = [
|
const coverGradients = [
|
||||||
'from-primary/20 via-primary/5 to-accent/10',
|
'from-primary/20 via-primary/5 to-accent/10',
|
||||||
@ -25,11 +26,13 @@ interface PodcastCardProps {
|
|||||||
progressPercent?: number
|
progressPercent?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export function PodcastCard({ podcast, progressPercent }: PodcastCardProps) {
|
export function PodcastCard({ podcast, progressPercent: propProgress }: PodcastCardProps) {
|
||||||
const play = usePlayerStore((s) => s.play)
|
const play = usePlayerStore((s) => s.play)
|
||||||
const currentId = usePlayerStore((s) => s.current?.id)
|
const currentId = usePlayerStore((s) => s.current?.id)
|
||||||
const isPlaying = usePlayerStore((s) => s.isPlaying)
|
const isPlaying = usePlayerStore((s) => s.isPlaying)
|
||||||
const isActive = currentId === podcast.id
|
const isActive = currentId === podcast.id
|
||||||
|
const progressMap = useListeningProgress()
|
||||||
|
const progressPercent = propProgress ?? progressMap.get(podcast.id)?.percent
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<article className="group lift bg-surface rounded-3xl shadow-organic-sm hover:shadow-organic overflow-hidden">
|
<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 { Badge } from '@/components/ui/Badge'
|
||||||
import { publicUrl } from '@/lib/storage'
|
import { publicUrl } from '@/lib/storage'
|
||||||
import { Button } from '@/components/ui/Button'
|
import { Button } from '@/components/ui/Button'
|
||||||
|
import { useListeningProgress } from '@/hooks/useListeningProgress'
|
||||||
|
|
||||||
export function PodcastDetail() {
|
export function PodcastDetail() {
|
||||||
const { id } = useParams<{ id: string }>()
|
const { id } = useParams<{ id: string }>()
|
||||||
@ -23,6 +24,8 @@ export function PodcastDetail() {
|
|||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
|
|
||||||
const isActive = current?.id === id
|
const isActive = current?.id === id
|
||||||
|
const progressMap = useListeningProgress()
|
||||||
|
const progressInfo = id ? progressMap.get(id) : undefined
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!id) return
|
if (!id) return
|
||||||
@ -141,13 +144,22 @@ export function PodcastDetail() {
|
|||||||
</div>
|
</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
|
<Button
|
||||||
onClick={() => isActive ? toggle() : play(podcast)}
|
onClick={() => isActive ? toggle() : play(podcast)}
|
||||||
size="lg"
|
size="lg"
|
||||||
>
|
>
|
||||||
{isActive && isPlaying ? <Pause size={18} /> : <Play size={18} className="ml-0.5" />}
|
{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>
|
||||||
<Button
|
<Button
|
||||||
variant={isLiked ? 'accent' : 'secondary'}
|
variant={isLiked ? 'accent' : 'secondary'}
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import { isExternalUrl, getEmbedInfo } from '@/lib/embed'
|
|||||||
import { publicUrl } from '@/lib/storage'
|
import { publicUrl } from '@/lib/storage'
|
||||||
import { supabase } from '@/lib/supabase'
|
import { supabase } from '@/lib/supabase'
|
||||||
import { useAuthStore } from '@/stores/auth'
|
import { useAuthStore } from '@/stores/auth'
|
||||||
|
import { invalidateProgressCache } from '@/hooks/useListeningProgress'
|
||||||
|
|
||||||
// ──── YouTube IFrame API ────
|
// ──── YouTube IFrame API ────
|
||||||
declare global {
|
declare global {
|
||||||
@ -84,7 +85,10 @@ async function saveProgressNow(podcastId: string, seconds: number, duration: num
|
|||||||
{ onConflict: 'user_id,podcast_id' }
|
{ onConflict: 'user_id,podcast_id' }
|
||||||
)
|
)
|
||||||
if (error) console.error('[player] save progress failed:', error)
|
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) {
|
function debouncedSaveProgress(podcastId: string, seconds: number, duration: number) {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user