Skip to content

Commit a7b6a0d

Browse files
committed
feat: Refactor player and episode components, add error handling and progress saving
- Removed README.md content. - Added EpisodeItem component for displaying podcast episodes with play, queue, share, and remove functionalities. - Enhanced Player component to include error handling for audio playback and improved progress saving logic. - Updated storageService to handle playback updates with additional episode metadata for better history management. - Modified rssService to implement fallback mechanisms for fetching trending podcasts. - Introduced a new SVG icon for the application. - Updated manifest and index.html to use the new icon and improve app branding. - Changed local storage keys to reflect new application name "AuraPod".
1 parent 614612d commit a7b6a0d

File tree

10 files changed

+475
-252
lines changed

10 files changed

+475
-252
lines changed

App.tsx

Lines changed: 164 additions & 145 deletions
Large diffs are not rendered by default.

README.md

Lines changed: 0 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +0,0 @@
1-
<div align="center">
2-
<img width="1200" height="475" alt="GHBanner" src="https://github.com/user-attachments/assets/0aa67016-6eaf-458a-adb2-6e31a0763ed6" />
3-
</div>
4-
5-
# Run and deploy your AI Studio app
6-
7-
This contains everything you need to run your app locally.
8-
9-
View your app in AI Studio: https://ai.studio/apps/drive/1L5_4m-UYyph3sVR1HVNfPFFzRJUOEvEM
10-
11-
## Run Locally
12-
13-
**Prerequisites:** Node.js
14-
15-
16-
1. Install dependencies:
17-
`npm install`
18-
2. Set the `GEMINI_API_KEY` in [.env.local](.env.local) to your Gemini API key
19-
3. Run the app:
20-
`npm run dev`

components/EpisodeItem.tsx

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
2+
import React, { useState } from 'react';
3+
import { Episode } from '../types';
4+
5+
interface EpisodeItemProps {
6+
episode: Partial<Episode> & { id: string; title: string; image?: string; podcastTitle?: string; duration?: string; pubDate?: string; description?: string };
7+
progress: number;
8+
isHistory?: boolean;
9+
isQueue?: boolean;
10+
isActive?: boolean;
11+
onPlay: () => void;
12+
onQueue?: () => void;
13+
onRemove?: () => void;
14+
onShare?: () => void;
15+
}
16+
17+
const EpisodeItem: React.FC<EpisodeItemProps> = ({
18+
episode,
19+
progress,
20+
isHistory,
21+
isQueue,
22+
isActive,
23+
onPlay,
24+
onQueue,
25+
onRemove,
26+
onShare
27+
}) => {
28+
const [showFullDesc, setShowFullDesc] = useState(false);
29+
30+
return (
31+
<div className={`group relative bg-zinc-50 dark:bg-zinc-900/40 hover:bg-white dark:hover:bg-zinc-900/60 p-4 md:p-6 rounded-3xl border border-zinc-100 dark:border-zinc-800/50 transition-all duration-300 shadow-sm ${isActive ? 'ring-2 ring-indigo-500' : ''}`}>
32+
<div className="flex gap-4 md:gap-8 items-start">
33+
{/* Thumbnail & Play Overlay */}
34+
<div className="relative shrink-0 w-20 h-20 md:w-32 md:h-32">
35+
<img src={episode.image || episode.podcastImage} className="w-full h-full object-cover rounded-2xl shadow-md" alt="" />
36+
<div className="absolute inset-0 bg-indigo-600/40 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center rounded-2xl">
37+
<button
38+
onClick={onPlay}
39+
className="w-10 h-10 md:w-12 md:h-12 bg-white text-zinc-950 rounded-full flex items-center justify-center shadow-2xl transform scale-75 group-hover:scale-100 transition duration-300"
40+
>
41+
<i className="fa-solid fa-play ml-1"></i>
42+
</button>
43+
</div>
44+
</div>
45+
46+
{/* Content */}
47+
<div className="flex-1 min-w-0 flex flex-col justify-center py-1">
48+
<div className="flex items-center gap-2 mb-2">
49+
{episode.podcastTitle && (
50+
<span className="text-[10px] font-bold text-indigo-600 dark:text-indigo-400 uppercase tracking-widest truncate max-w-[150px]">{episode.podcastTitle}</span>
51+
)}
52+
{episode.pubDate && (
53+
<>
54+
<span className="text-zinc-300 dark:text-zinc-700"></span>
55+
<span className="text-[10px] text-zinc-500 font-medium">{new Date(episode.pubDate).toLocaleDateString()}</span>
56+
</>
57+
)}
58+
</div>
59+
60+
<h4
61+
onClick={onPlay}
62+
className="text-base md:text-xl font-bold text-zinc-900 dark:text-white truncate mb-2 group-hover:text-indigo-600 transition cursor-pointer leading-tight"
63+
>
64+
{episode.title}
65+
</h4>
66+
67+
{/* Description Reveal */}
68+
{episode.description && !isQueue && (
69+
<div className="relative">
70+
<p
71+
className={`text-xs md:text-sm text-zinc-500 dark:text-zinc-400 leading-relaxed font-medium ${showFullDesc ? '' : 'line-clamp-2'}`}
72+
dangerouslySetInnerHTML={{ __html: episode.description }}
73+
></p>
74+
<button
75+
onClick={() => setShowFullDesc(!showFullDesc)}
76+
className="text-[10px] font-bold text-indigo-500 hover:text-indigo-600 transition mt-2 flex items-center gap-1.5"
77+
>
78+
{showFullDesc ? 'Show Less' : 'Read More'}
79+
<i className={`fa-solid fa-chevron-${showFullDesc ? 'up' : 'down'}`}></i>
80+
</button>
81+
</div>
82+
)}
83+
84+
<div className="mt-4 flex items-center gap-6 text-zinc-400">
85+
{episode.duration && (
86+
<span className="text-[10px] font-bold uppercase tracking-tight flex items-center gap-2">
87+
<i className="fa-regular fa-clock"></i> {episode.duration}
88+
</span>
89+
)}
90+
91+
{onQueue && (
92+
<button onClick={onQueue} className="text-[10px] font-bold uppercase tracking-tight hover:text-indigo-600 transition flex items-center gap-2">
93+
<i className="fa-solid fa-plus"></i> Queue
94+
</button>
95+
)}
96+
97+
{onShare && (
98+
<button onClick={onShare} className="text-[10px] font-bold uppercase tracking-tight hover:text-indigo-600 transition flex items-center gap-2">
99+
<i className="fa-solid fa-share-nodes"></i> Share
100+
</button>
101+
)}
102+
103+
{onRemove && (
104+
<button onClick={onRemove} className="text-[10px] font-bold uppercase tracking-tight hover:text-red-500 transition flex items-center gap-2 ml-auto">
105+
<i className="fa-solid fa-trash-can"></i> Remove
106+
</button>
107+
)}
108+
</div>
109+
</div>
110+
</div>
111+
112+
{/* Progress Bar at bottom of card */}
113+
{progress > 0 && (
114+
<div className="absolute bottom-0 left-0 right-0 h-1 bg-zinc-100 dark:bg-zinc-800/50 rounded-b-3xl overflow-hidden">
115+
<div
116+
className="h-full bg-gradient-to-r from-indigo-500 to-indigo-400 transition-all duration-500"
117+
style={{ width: `${progress}%` }}
118+
></div>
119+
</div>
120+
)}
121+
</div>
122+
);
123+
};
124+
125+
export default EpisodeItem;

0 commit comments

Comments
 (0)