From 5c2a6438cd37589c199fdb42697d074f6b9fce79 Mon Sep 17 00:00:00 2001 From: R0m1k3 Date: Tue, 28 Apr 2026 13:22:25 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20redesign=20complet=20de=20l'application?= =?UTF-8?q?=20bas=C3=A9=20sur=20les=20maquettes=20Stitch?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Refonte du design system (CSS variables, composants glass/card/button/badge), nouvelle page PodcastDetails avec route /podcast/:id, lecteur audio immersif avec visualiseur audio, animations, thème dark/light, responsive mobile/desktop. Co-Authored-By: Claude Opus 4.7 --- .claude/settings.local.json | 8 +- frontend/index.html | 10 +- frontend/src/App.tsx | 99 ++-- frontend/src/components/AudioPlayer.tsx | 539 +++++++------------ frontend/src/components/EpisodeCard.tsx | 179 +++---- frontend/src/components/EpisodeDetails.tsx | 1 - frontend/src/components/Header.tsx | 82 +-- frontend/src/components/Navigation.tsx | 134 +++-- frontend/src/components/PodcastCard.tsx | 177 +++---- frontend/src/components/SuccessModal.tsx | 2 +- frontend/src/pages/AddPodcast.tsx | 307 ++++------- frontend/src/pages/Dashboard.tsx | 282 ++++------ frontend/src/pages/Library.tsx | 138 ++--- frontend/src/pages/Login.tsx | 2 +- frontend/src/pages/PodcastDetails.tsx | 207 ++++++++ frontend/src/pages/Trending.tsx | 217 +++----- frontend/src/services/episodeService.ts | 10 + frontend/src/styles/globals.css | 577 +++++++++++++-------- frontend/src/vite-env.d.ts | 1 + frontend/tailwind.config.js | 61 +-- 20 files changed, 1463 insertions(+), 1570 deletions(-) create mode 100644 frontend/src/pages/PodcastDetails.tsx create mode 100644 frontend/src/vite-env.d.ts 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 && ( -