mirror of
https://github.com/R0m1k3/podcastic.git
synced 2026-10-11 17:26:20 +02:00
feat: implement AddPodcast page with RSS subscription and discovery search functionality
This commit is contained in:
1 parent
acd9a1a2c5
commit
ea4fdb38d5
4 files changed
+61
-24
No files matched your search
@@ -33,6 +33,7 @@ export default function AudioPlayer({ episode, onClose, userId }: AudioPlayerPro
|
|||||||
const [playbackSpeed, setPlaybackSpeed] = useState(() => parseFloat(localStorage.getItem('podcastic-speed') || '1'));
|
const [playbackSpeed, setPlaybackSpeed] = useState(() => parseFloat(localStorage.getItem('podcastic-speed') || '1'));
|
||||||
const [isMuted, setIsMuted] = useState(false);
|
const [isMuted, setIsMuted] = useState(false);
|
||||||
const [isExpanded, setIsExpanded] = useState(false);
|
const [isExpanded, setIsExpanded] = useState(false);
|
||||||
|
const [isResuming, setIsResuming] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
const podcast = typeof episode?.podcastId === 'object' ? episode.podcastId : null;
|
const podcast = typeof episode?.podcastId === 'object' ? episode.podcastId : null;
|
||||||
@@ -67,11 +68,17 @@ export default function AudioPlayer({ episode, onClose, userId }: AudioPlayerPro
|
|||||||
// Load previous progress
|
// Load previous progress
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!episode || !userId) return;
|
if (!episode || !userId) return;
|
||||||
|
setIsResuming(true);
|
||||||
episodeService.getProgress(episode._id).then((r) => {
|
episodeService.getProgress(episode._id).then((r) => {
|
||||||
if (r.progress?.position && audioRef.current) {
|
if (r.progress?.position && audioRef.current) {
|
||||||
|
console.log(`[AudioPlayer] Resuming at ${r.progress.position}s`);
|
||||||
audioRef.current.currentTime = r.progress.position;
|
audioRef.current.currentTime = r.progress.position;
|
||||||
|
setCurrentTime(r.progress.position);
|
||||||
}
|
}
|
||||||
}).catch(() => {});
|
setIsResuming(false);
|
||||||
|
}).catch(() => {
|
||||||
|
setIsResuming(false);
|
||||||
|
});
|
||||||
}, [episode, userId]);
|
}, [episode, userId]);
|
||||||
|
|
||||||
const handlePlayPause = () => {
|
const handlePlayPause = () => {
|
||||||
@@ -167,7 +174,11 @@ export default function AudioPlayer({ episode, onClose, userId }: AudioPlayerPro
|
|||||||
</div>
|
</div>
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<p className="text-sm font-bold truncate leading-tight">{episode.title}</p>
|
<p className="text-sm font-bold truncate leading-tight">{episode.title}</p>
|
||||||
<p className="text-[10px] text-[var(--text-secondary)] font-bold truncate uppercase tracking-wider">{podcast?.title || ''}</p>
|
{isResuming ? (
|
||||||
|
<p className="text-[9px] text-[var(--accent-primary)] font-black uppercase animate-pulse">Reprise de lecture...</p>
|
||||||
|
) : (
|
||||||
|
<p className="text-[10px] text-[var(--text-secondary)] font-bold truncate uppercase tracking-wider">{podcast?.title || ''}</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Episode } from '../services/episodeService';
|
import { Episode } from '../services/episodeService';
|
||||||
import { Play, Calendar } from 'lucide-react';
|
import { Play, Calendar, Activity } from 'lucide-react';
|
||||||
|
import { useAudio } from '../context/AudioContext';
|
||||||
|
|
||||||
interface EpisodeCardProps {
|
interface EpisodeCardProps {
|
||||||
episode: Episode;
|
episode: Episode;
|
||||||
@@ -9,6 +10,8 @@ interface EpisodeCardProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function EpisodeCard({ episode, onPlay, onDetails }: EpisodeCardProps) {
|
export default function EpisodeCard({ episode, onPlay, onDetails }: EpisodeCardProps) {
|
||||||
|
const { currentEpisode, isPlaying } = useAudio();
|
||||||
|
const isCurrent = currentEpisode?._id === episode._id;
|
||||||
const podcast = typeof episode.podcastId === 'object' ? episode.podcastId : null;
|
const podcast = typeof episode.podcastId === 'object' ? episode.podcastId : null;
|
||||||
const pubDate = new Date(episode.pubDate).toLocaleDateString('fr-FR', {
|
const pubDate = new Date(episode.pubDate).toLocaleDateString('fr-FR', {
|
||||||
month: 'short',
|
month: 'short',
|
||||||
@@ -57,9 +60,17 @@ export default function EpisodeCard({ episode, onPlay, onDetails }: EpisodeCardP
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Duration Badge */}
|
{/* Duration/Status Badge */}
|
||||||
<div className="absolute bottom-3 right-3 px-2 py-1 rounded-lg bg-black/60 backdrop-blur-md border border-white/10 text-[9px] font-black text-white uppercase tracking-widest">
|
<div className="absolute bottom-3 right-3 flex items-center gap-2">
|
||||||
{durationMinutes} MIN
|
{isCurrent && (
|
||||||
|
<div className="px-2 py-1 rounded-lg bg-[var(--accent-primary)] border border-[var(--accent-primary)]/20 text-[9px] font-black text-white uppercase tracking-widest flex items-center gap-1.5 shadow-glow-indigo animate-pulse">
|
||||||
|
<Activity className="w-3 h-3" />
|
||||||
|
{isPlaying ? "EN COURS" : "EN PAUSE"}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="px-2 py-1 rounded-lg bg-black/60 backdrop-blur-md border border-white/10 text-[9px] font-black text-white uppercase tracking-widest">
|
||||||
|
{durationMinutes} MIN
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -286,18 +286,18 @@ export default function AddPodcast() {
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
) : searchType === 'episodes' && episodeResults?.episodes && episodeResults.episodes.length > 0 ? (
|
) : searchType === 'episodes' && episodeResults?.episodes ? (
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-8 pb-32">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8 pb-32">
|
||||||
{episodeResults.episodes.map((episode) => (
|
{episodeResults.episodes.map((episode) => (
|
||||||
<EpisodeCard
|
<EpisodeCard
|
||||||
key={episode._id}
|
key={episode._id}
|
||||||
episode={episode}
|
episode={episode}
|
||||||
onPlay={playEpisode}
|
onPlay={playEpisode}
|
||||||
onDetails={handleOpenDetails}
|
onDetails={handleOpenDetails}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
) : searchQuery.length >= 2 ? (
|
) : searchQuery.length >= 2 && !isSearching && !isSearchingEpisodes ? (
|
||||||
<div className="premium-glass p-20 rounded-[var(--radius-panel)] text-center max-w-lg mx-auto">
|
<div className="premium-glass p-20 rounded-[var(--radius-panel)] text-center max-w-lg mx-auto">
|
||||||
<Globe className="w-12 h-12 text-slate-700 mx-auto mb-6" />
|
<Globe className="w-12 h-12 text-slate-700 mx-auto mb-6" />
|
||||||
<p className="font-bold uppercase tracking-widest text-sm mb-2">Aucun résultat sur iTunes FR</p>
|
<p className="font-bold uppercase tracking-widest text-sm mb-2">Aucun résultat sur iTunes FR</p>
|
||||||
|
|||||||
@@ -40,17 +40,32 @@
|
|||||||
|
|
||||||
/* ── Explicit Dark Theme (for nesting/inversion) ── */
|
/* ── Explicit Dark Theme (for nesting/inversion) ── */
|
||||||
.dark-theme {
|
.dark-theme {
|
||||||
--bg-primary: #080d1a;
|
--bg-primary: #000000; /* Solid Black */
|
||||||
--bg-secondary: rgba(255, 255, 255, 0.025);
|
--bg-secondary: #0a0a0a;
|
||||||
--text-primary: #ffffff;
|
--text-primary: #ffffff;
|
||||||
--text-secondary: #94a3b8;
|
--text-secondary: #94a3b8;
|
||||||
--accent-primary: #6366f1;
|
--accent-primary: #6366f1;
|
||||||
--accent-secondary: #06b6d4;
|
--accent-secondary: #06b6d4;
|
||||||
--accent-glow: rgba(99, 102, 241, 0.35);
|
--accent-glow: rgba(99, 102, 241, 0.4);
|
||||||
--border-color: rgba(255, 255, 255, 0.07);
|
--border-color: rgba(255, 255, 255, 0.15);
|
||||||
--input-bg: rgba(255, 255, 255, 0.03);
|
--input-bg: rgba(255, 255, 255, 0.05);
|
||||||
--shadow-premium: 0 25px 60px rgba(0, 0, 0, 0.6), 0 0 1px rgba(255, 255, 255, 0.1);
|
--shadow-premium: 0 30px 100px rgba(0, 0, 0, 0.9), 0 0 1px rgba(255, 255, 255, 0.2);
|
||||||
--shadow-hover: 0 35px 80px rgba(0, 0, 0, 0.8), 0 0 1px rgba(255, 255, 255, 0.2);
|
--glass-blur: none; /* No transparency for the player */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Explicit light override for dark mode player inversion */
|
||||||
|
.light-theme {
|
||||||
|
--bg-primary: #ffffff; /* Solid White */
|
||||||
|
--bg-secondary: #f8fafc;
|
||||||
|
--text-primary: #0f172a;
|
||||||
|
--text-secondary: #64748b;
|
||||||
|
--accent-primary: #6366f1;
|
||||||
|
--accent-secondary: #06b6d4;
|
||||||
|
--accent-glow: rgba(99, 102, 241, 0.15);
|
||||||
|
--border-color: #e2e8f0;
|
||||||
|
--input-bg: #f1f5f9;
|
||||||
|
--shadow-premium: 0 20px 60px rgba(0, 0, 0, 0.1), 0 0 1px rgba(0, 0, 0, 0.05);
|
||||||
|
--glass-blur: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user