From 2fbccfac1081e5dc5bde2e283f7fb1a5836fab18 Mon Sep 17 00:00:00 2001 From: ordinarthur <@arthurbarre.js@gmail.com> Date: Mon, 13 Apr 2026 16:14:50 +0200 Subject: [PATCH] add listen count --- src/components/layout/MobileNav.tsx | 6 +- src/components/podcast/PodcastCard.tsx | 6 +- src/pages/Home.tsx | 103 ++++++++++++-------- src/pages/Profile.tsx | 23 +++-- src/stores/player.ts | 12 +++ supabase/migrations/010_increment_plays.sql | 9 ++ 6 files changed, 106 insertions(+), 53 deletions(-) create mode 100644 supabase/migrations/010_increment_plays.sql diff --git a/src/components/layout/MobileNav.tsx b/src/components/layout/MobileNav.tsx index d286e59..04f673e 100644 --- a/src/components/layout/MobileNav.tsx +++ b/src/components/layout/MobileNav.tsx @@ -1,5 +1,5 @@ import { NavLink } from 'react-router-dom' -import { Home, Compass, Upload, Heart, User } from 'lucide-react' +import { Home, Compass, Upload, TrendingUp, User, Sparkles } from 'lucide-react' import { cn } from '@/lib/utils' import { useAuthStore } from '@/stores/auth' import { usePlayerStore } from '@/stores/player' @@ -11,11 +11,11 @@ export function MobileNav() { const links = [ { to: '/', icon: Home, label: 'Accueil' }, { to: '/explore', icon: Compass, label: 'Explorer' }, + { to: '/trending', icon: TrendingUp, label: 'Tendances' }, ...(user ? [ { to: '/upload', icon: Upload, label: 'Publier' }, - { to: '/favorites', icon: Heart, label: 'Favoris' }, - { to: `/profile/${profile?.username || user.id}`, icon: User, label: 'Profil' }, + { to: '/for-you', icon: Sparkles, label: 'Pour toi' }, ] : [{ to: '/auth', icon: User, label: 'Connexion' }]), ] diff --git a/src/components/podcast/PodcastCard.tsx b/src/components/podcast/PodcastCard.tsx index f9d7f25..035a594 100644 --- a/src/components/podcast/PodcastCard.tsx +++ b/src/components/podcast/PodcastCard.tsx @@ -1,4 +1,4 @@ -import { Play, Pause, Heart, MessageCircle, Clock, ListPlus, Download, Check, Loader2, BookmarkPlus } from 'lucide-react' +import { Play, Pause, Heart, MessageCircle, Clock, ListPlus, Download, Check, Loader2, BookmarkPlus, Headphones } from 'lucide-react' import { Link } from 'react-router-dom' import type { Podcast } from '@/types' import { formatDuration, timeAgo } from '@/lib/utils' @@ -165,6 +165,10 @@ export function PodcastCard({ podcast, progressPercent: propProgress }: PodcastC )}
+ + + {podcast.plays_count || 0} + {podcast.likes_count || 0} diff --git a/src/pages/Home.tsx b/src/pages/Home.tsx index 29d5674..00f7551 100644 --- a/src/pages/Home.tsx +++ b/src/pages/Home.tsx @@ -8,6 +8,12 @@ import { PodcastCard } from '@/components/podcast/PodcastCard' import { Button } from '@/components/ui/Button' import { normalizePodcasts } from '@/lib/utils' +function formatStat(n: number): string { + if (n >= 1000000) return `${(n / 1000000).toFixed(1).replace(/\.0$/, '')}M` + if (n >= 1000) return `${(n / 1000).toFixed(1).replace(/\.0$/, '')}k` + return n.toString() +} + interface InProgressPodcast extends Podcast { progressPercent: number } @@ -17,6 +23,7 @@ export function Home() { const [recent, setRecent] = useState([]) const [inProgress, setInProgress] = useState([]) const [loading, setLoading] = useState(true) + const [stats, setStats] = useState({ plays: 0, creators: 0, podcasts: 0 }) const { user } = useAuthStore() const navigate = useNavigate() @@ -66,6 +73,17 @@ export function Home() { setInProgress(items) } + // Fetch real stats for hero + if (!user) { + const [{ count: podcastCount }, { data: playsData }, { count: creatorCount }] = await Promise.all([ + supabase.from('podcasts').select('*', { count: 'exact', head: true }), + supabase.from('podcasts').select('plays_count'), + supabase.from('profiles').select('*', { count: 'exact', head: true }), + ]) + const totalPlays = playsData?.reduce((sum: number, p: any) => sum + (p.plays_count || 0), 0) || 0 + setStats({ plays: totalPlays, creators: creatorCount || 0, podcasts: podcastCount || 0 }) + } + setLoading(false) } load() @@ -73,55 +91,54 @@ export function Home() { return (
- {/* Hero */} -
- {/* Decorative blobs */} -
-
-
+ {/* Hero — only for visitors */} + {!user && ( +
+
+
+
-
- {/* Floating mic icon */} -
- -
+
+
+ +
-

- Votre voix compte,{' '} - partagez-la. -

-

- La plateforme de podcast ou tout le monde peut creer, ecouter et decouvrir des voix authentiques. -

+

+ Votre voix compte,{' '} + partagez-la. +

+

+ La plateforme de podcast ou tout le monde peut creer, ecouter et decouvrir des voix authentiques. +

-
- - -
+
+ + +
- {/* Stats */} -
- {[ - { icon: Headphones, label: 'Ecoutes', value: '2.4k' }, - { icon: Users, label: 'Createurs', value: '128' }, - { icon: Mic, label: 'Podcasts', value: '340' }, - ].map(({ icon: Icon, label, value }) => ( -
-
- - {value} +
+ {[ + { icon: Headphones, label: 'Ecoutes', value: formatStat(stats.plays) }, + { icon: Users, label: 'Createurs', value: formatStat(stats.creators) }, + { icon: Mic, label: 'Podcasts', value: formatStat(stats.podcasts) }, + ].map(({ icon: Icon, label, value }) => ( +
+
+ + {value} +
+ {label}
- {label} -
- ))} + ))} +
-
-
+
+ )} {/* Continue listening */} {inProgress.length > 0 && ( diff --git a/src/pages/Profile.tsx b/src/pages/Profile.tsx index c82adb8..d7faaa4 100644 --- a/src/pages/Profile.tsx +++ b/src/pages/Profile.tsx @@ -1,6 +1,6 @@ import { useEffect, useState } from 'react' -import { useParams } from 'react-router-dom' -import { Users, Headphones } from 'lucide-react' +import { useParams, useNavigate } from 'react-router-dom' +import { Users, Headphones, Settings } from 'lucide-react' import { supabase } from '@/lib/supabase' import { useAuthStore } from '@/stores/auth' import type { Profile as ProfileType, Podcast } from '@/types' @@ -11,6 +11,7 @@ import { PodcastCard } from '@/components/podcast/PodcastCard' export function Profile() { const { username } = useParams<{ username: string }>() const { user } = useAuthStore() + const navigate = useNavigate() const [profile, setProfile] = useState(null) const [podcasts, setPodcasts] = useState([]) const [isFollowing, setIsFollowing] = useState(false) @@ -116,8 +117,8 @@ export function Profile() { {followingCount} abonnements
- {!isOwn && user && ( -
+
+ {!isOwn && user && ( -
- )} + )} + {isOwn && ( + + )} +
diff --git a/src/stores/player.ts b/src/stores/player.ts index 0fd3d85..661d6ad 100644 --- a/src/stores/player.ts +++ b/src/stores/player.ts @@ -215,6 +215,7 @@ export const usePlayerStore = create((set, get) => ({ // Resume same podcast (already loaded) if (current?.id === podcast.id) { + // Don't count resume as a new play if (external && embed?.platform === 'youtube' && ytPlayer) { try { ytPlayer.playVideo() } catch { /* ignore */ } startYtProgressTracking() @@ -241,6 +242,17 @@ export const usePlayerStore = create((set, get) => ({ if (audio) { audio.pause(); audio.removeAttribute('src') } destroyAll() + // Count as a play: increment plays_count via RPC (SECURITY DEFINER bypasses RLS) + supabase.rpc('increment_plays', { podcast_id: podcast.id }) + .then(({ error }) => { if (error) console.error('[player] increment_plays RPC failed — did you run migration 010?', error) }) + const userId = useAuthStore.getState().user?.id + if (userId) { + supabase.from('listen_history').upsert( + { user_id: userId, podcast_id: podcast.id, progress_seconds: 0, completed: false, listened_at: new Date().toISOString() }, + { onConflict: 'user_id,podcast_id' } + ).then(() => {}, () => {}) + } + // ── YouTube: use IFrame API for full control ── if (external && embed?.platform === 'youtube') { set({ audio: null, current: podcast, isPlaying: false, isExternal: true, progress: 0, duration: podcast.duration_seconds || 0 }) diff --git a/supabase/migrations/010_increment_plays.sql b/supabase/migrations/010_increment_plays.sql new file mode 100644 index 0000000..7acf2a9 --- /dev/null +++ b/supabase/migrations/010_increment_plays.sql @@ -0,0 +1,9 @@ +-- RPC to atomically increment plays_count +CREATE OR REPLACE FUNCTION increment_plays(podcast_id uuid) +RETURNS void AS $$ +BEGIN + UPDATE podcasts + SET plays_count = plays_count + 1 + WHERE id = podcast_id; +END; +$$ LANGUAGE plpgsql SECURITY DEFINER;