diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 8f6be89..9e64c84 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -6,7 +6,13 @@ "Bash(git remote:*)", "Bash(git branch:*)", "Bash(git push:*)", - "Bash(ls -la /c/Claude/Podcastic/Dockerfile*)" + "Bash(ls -la /c/Claude/Podcastic/Dockerfile*)", + "Bash(npm run *)", + "WebFetch(domain:stitch.withgoogle.com)", + "Bash(npx tsc *)", + "Bash(git add *)", + "Bash(git pull *)", + "Bash(git stash *)" ] } } diff --git a/frontend/index.html b/frontend/index.html index 7dcbc30..d2aecb5 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -1,17 +1,17 @@ - + - + - - Podcastic - Premium Podcast Streaming + + Podcastic - Streaming Audio Premium - +
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 2e0a043..e45a0fe 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,31 +1,31 @@ -import { BrowserRouter, Routes, Route, Navigate, useLocation } from 'react-router-dom'; +import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { useState, useEffect } from 'react'; import './styles/globals.css'; import { authService } from './services/authService'; import { AudioProvider, useAudio } from './context/AudioContext'; +import { ThemeProvider } from './context/ThemeContext'; import Login from './pages/Login'; import Dashboard from './pages/Dashboard'; import Trending from './pages/Trending'; import AddPodcast from './pages/AddPodcast'; import Library from './pages/Library'; +import PodcastDetails from './pages/PodcastDetails'; import Navigation from './components/Navigation'; import AudioPlayer from './components/AudioPlayer'; const queryClient = new QueryClient({ defaultOptions: { queries: { - staleTime: 1000 * 60 * 5, // 5 minutes - gcTime: 1000 * 60 * 30, // 30 minutes (formerly cacheTime) + staleTime: 1000 * 60 * 5, + gcTime: 1000 * 60 * 30, + retry: 1, }, }, }); -// App Content Wrapper to access AudioContext function AppContent() { const { currentEpisode, closePlayer } = useAudio(); - const location = useLocation(); - const isDashboard = location.pathname === '/dashboard' || location.pathname === '/'; const [user, setUser] = useState(null); useEffect(() => { @@ -34,58 +34,37 @@ function AppContent() { authService.getMe().then(r => setUser(r.user)).catch(() => {}); }, [currentEpisode]); - return ( - <> - - } /> - - - - } - /> - - - - } - /> - - - - } - /> - - - - } - /> - } /> - } /> - + const isDashboard = window.location.pathname === '/dashboard' || window.location.pathname === '/'; + return ( + + } /> + + } /> + + } /> + + } /> + + } /> + + } /> + } /> + } /> + + {/* Audio player — shown when not on dashboard (dashboard has inline player) */} {currentEpisode && !isDashboard && ( - + )} - + ); } -// Protected Route Component function ProtectedRoute({ children }: { children: React.ReactNode }) { const { accessToken } = authService.getTokens(); @@ -96,8 +75,8 @@ function ProtectedRoute({ children }: { children: React.ReactNode }) { return (
-
-
+
+
{children}
@@ -108,11 +87,13 @@ function ProtectedRoute({ children }: { children: React.ReactNode }) { export default function App() { return ( - - - - - + + + + + + + ); } diff --git a/frontend/src/components/AudioPlayer.tsx b/frontend/src/components/AudioPlayer.tsx index de638c3..4cc428e 100644 --- a/frontend/src/components/AudioPlayer.tsx +++ b/frontend/src/components/AudioPlayer.tsx @@ -3,19 +3,11 @@ import { createPortal } from 'react-dom'; import { Episode } from '../services/episodeService'; import { useAudio } from '../context/AudioContext'; import { - Play, - Pause, - Volume2, - VolumeX, - ChevronDown, - Maximize2, - X, - RotateCcw, - RotateCw, + Play, Pause, Volume2, VolumeX, ChevronDown, + Maximize2, X, RotateCcw, RotateCw, Headphones, } from 'lucide-react'; import AlertModal from './AlertModal'; import AudioVisualizer from './AudioVisualizer'; -import { useTheme } from '../context/ThemeContext'; interface AudioPlayerProps { episode: Episode | null; @@ -26,50 +18,31 @@ interface AudioPlayerProps { export default function AudioPlayer({ episode, onClose, userId, mode = 'floating' }: AudioPlayerProps) { const { - isPlaying, - currentTime, - duration, - volume, - playbackSpeed, - isMuted, - isResuming, - error, - togglePlay, - seek, - seekRelative, - setVolume, - toggleMute, - changeSpeed, - setError, - setUserId, + isPlaying, currentTime, duration, volume, playbackSpeed, + isMuted, isResuming, error, + togglePlay, seek, seekRelative, setVolume, toggleMute, changeSpeed, + setError, setUserId, } = useAudio(); - const { theme } = useTheme(); const [isExpanded, setIsExpanded] = useState(false); const [showMiniBar, setShowMiniBar] = useState(false); - const inlineWrapperRef = useRef(null); + const inlineRef = useRef(null); - // Sync userId into context so context can save/load progress - useEffect(() => { - setUserId(userId ?? null); - }, [userId, setUserId]); + useEffect(() => { setUserId(userId ?? null); }, [userId, setUserId]); - // IntersectionObserver: show mini-bar when inline player scrolls out of view useEffect(() => { - if (mode !== 'inline' || !inlineWrapperRef.current) return; - const observer = new IntersectionObserver( - ([entry]) => { - setShowMiniBar(!entry.isIntersecting); - }, + if (mode !== 'inline' || !inlineRef.current) return; + const obs = new IntersectionObserver( + ([entry]) => setShowMiniBar(!entry.isIntersecting), { threshold: 0, rootMargin: '-40px 0px 0px 0px' } ); - observer.observe(inlineWrapperRef.current); - return () => observer.disconnect(); + obs.observe(inlineRef.current); + return () => obs.disconnect(); }, [mode, episode?._id]); const podcast = typeof episode?.podcastId === 'object' ? episode.podcastId : null; - const formatTime = (s: number) => { + const fmt = (s: number) => { if (!isFinite(s)) return '0:00'; const m = Math.floor(s / 60); const sec = Math.floor(s % 60); @@ -79,388 +52,266 @@ export default function AudioPlayer({ episode, onClose, userId, mode = 'floating if (!episode) return null; const progress = duration > 0 ? (currentTime / duration) * 100 : 0; - const inverseTheme = theme === 'light' ? 'dark-theme' : 'light-theme'; - const handleProgressChange = (e: React.ChangeEvent) => { - seek(parseFloat(e.target.value)); - }; + const handleProgress = (e: React.ChangeEvent) => seek(parseFloat(e.target.value)); + const handleVolume = (e: React.ChangeEvent) => setVolume(parseFloat(e.target.value)); - const handleVolumeChange = (e: React.ChangeEvent) => { - setVolume(parseFloat(e.target.value)); - }; + const closeAlert = () => { setError(null); onClose(); }; - // ── INLINE MODE (embedded in hero card) ── - if (mode === 'inline') { - return ( - <> -
- { - setError(null); - onClose(); - }} - /> - - {/* Glow backdrop */} -
- -
- {/* Artwork */} -
- {/* Halo aura derrière l'artwork */} - {isPlaying && ( -