This commit is contained in:
Michael committed 2026-04-22 14:40:01 +02:00
commit 35f34d9d44
7 files changed
+81 -28

No files matched your search

+49
View File
@@ -0,0 +1,49 @@
name: Build & Publish Docker Image
on:
push:
branches: [main]
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
build-and-push:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata (tags, labels)
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=raw,value=latest
type=sha,prefix=sha-
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
+1 -5
View File
@@ -1,5 +1,3 @@
version: '3.8'
services: services:
mongodb: mongodb:
image: mongo:8 image: mongo:8
@@ -31,9 +29,7 @@ services:
retries: 5 retries: 5
app: app:
build: image: ghcr.io/r0m1k3/podcastic:latest
context: .
dockerfile: Dockerfile
container_name: podcastic-app container_name: podcastic-app
restart: unless-stopped restart: unless-stopped
ports: ports:
+3 -4
View File
@@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import { Episode } from '../services/episodeService'; import { Episode } from '../services/episodeService';
import { Play, Pause, Calendar, Activity, Check, CheckCircle2, Circle } from 'lucide-react'; import { Play, Pause, Calendar, Activity, CheckCircle2, Circle } from 'lucide-react';
import { useAudio } from '../context/AudioContext'; import { useAudio } from '../context/AudioContext';
interface EpisodeCardProps { interface EpisodeCardProps {
@@ -75,7 +75,6 @@ export default function EpisodeCard({ episode, onPlay, onDetails, onToggleRead }
<div className="absolute top-3 left-3 flex items-center gap-2 z-10"> <div className="absolute top-3 left-3 flex items-center gap-2 z-10">
{isCompleted && ( {isCompleted && (
<div className="px-2.5 py-1 rounded-lg bg-emerald-500/90 backdrop-blur-md border border-emerald-300/30 text-[9px] font-black text-white uppercase tracking-widest flex items-center gap-1 shadow-lg"> <div className="px-2.5 py-1 rounded-lg bg-emerald-500/90 backdrop-blur-md border border-emerald-300/30 text-[9px] font-black text-white uppercase tracking-widest flex items-center gap-1 shadow-lg">
<Check className="w-3 h-3" />
LU LU
</div> </div>
)} )}
@@ -112,7 +111,7 @@ export default function EpisodeCard({ episode, onPlay, onDetails, onToggleRead }
<div className="flex-1 flex flex-col"> <div className="flex-1 flex flex-col">
<div className="flex items-center gap-2 mb-2"> <div className="flex items-center gap-2 mb-2">
<span className="text-[9px] font-black text-[var(--accent-primary)] uppercase tracking-[0.2em] truncate flex-1"> <span className="text-[9px] font-black text-[var(--accent-primary)] uppercase tracking-[0.2em] truncate flex-1">
{podcast?.author || 'Artiste Premium'} {podcast?.author || ''}
</span> </span>
</div> </div>
@@ -140,7 +139,7 @@ export default function EpisodeCard({ episode, onPlay, onDetails, onToggleRead }
? <CheckCircle2 className="w-3.5 h-3.5" /> ? <CheckCircle2 className="w-3.5 h-3.5" />
: <Circle className="w-3.5 h-3.5" /> : <Circle className="w-3.5 h-3.5" />
} }
<span>{isCompleted ? 'Lu' : 'Lu ?'}</span> <span>{isCompleted ? 'Lu' : 'Marquer'}</span>
</button> </button>
)} )}
</div> </div>
+1 -1
View File
@@ -40,7 +40,7 @@ export default function Header({ title, subtitle, user, onLogout }: HeaderProps)
<div className="flex items-center gap-4 sm:gap-6"> <div className="flex items-center gap-4 sm:gap-6">
<div className="hidden sm:flex flex-col items-end"> <div className="hidden sm:flex flex-col items-end">
<span className="text-sm font-bold tracking-wide">{user.username}</span> <span className="text-sm font-bold tracking-wide">{user.username}</span>
<span className="text-[10px] font-medium text-[var(--text-secondary)] uppercase tracking-widest">Premium Member</span> <span className="text-[10px] font-medium text-[var(--text-secondary)] uppercase tracking-widest">Membre</span>
</div> </div>
<div className="relative group/profile"> <div className="relative group/profile">
<div className="w-12 h-12 rounded-2xl p-[1px] bg-gradient-to-tr from-[var(--accent-primary)] to-[var(--accent-secondary)] transition-all"> <div className="w-12 h-12 rounded-2xl p-[1px] bg-gradient-to-tr from-[var(--accent-primary)] to-[var(--accent-secondary)] transition-all">
+1 -1
View File
@@ -26,7 +26,7 @@ export default function Navigation() {
</div> </div>
<div className="flex flex-col"> <div className="flex flex-col">
<span className="text-xl font-display font-extrabold tracking-tight">PODCASTIC</span> <span className="text-xl font-display font-extrabold tracking-tight">PODCASTIC</span>
<span className="text-[10px] text-[var(--text-secondary)] font-bold uppercase tracking-[0.2em]">Premium Audio</span> <span className="text-[10px] text-[var(--text-secondary)] font-bold uppercase tracking-[0.2em]">Your audio space</span>
</div> </div>
</Link> </Link>
+25 -16
View File
@@ -1,5 +1,5 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useQuery } from '@tanstack/react-query'; import { useQuery, useQueryClient } from '@tanstack/react-query';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { episodeService, Episode } from '../services/episodeService'; import { episodeService, Episode } from '../services/episodeService';
import { authService } from '../services/authService'; import { authService } from '../services/authService';
@@ -13,6 +13,7 @@ import { Sparkles, Play, Clock, TrendingUp, Loader, RefreshCcw } from 'lucide-re
export default function Dashboard() { export default function Dashboard() {
const { playEpisode, currentEpisode, closePlayer, setUserId } = useAudio(); const { playEpisode, currentEpisode, closePlayer, setUserId } = useAudio();
const queryClient = useQueryClient();
const [user, setUser] = useState<any>(null); const [user, setUser] = useState<any>(null);
const [detailsEpisode, setDetailsEpisode] = useState<Episode | null>(null); const [detailsEpisode, setDetailsEpisode] = useState<Episode | null>(null);
const [isDetailsOpen, setIsDetailsOpen] = useState(false); const [isDetailsOpen, setIsDetailsOpen] = useState(false);
@@ -41,21 +42,29 @@ export default function Dashboard() {
}; };
const handleToggleRead = async (episode: Episode, completed: boolean) => { const handleToggleRead = async (episode: Episode, completed: boolean) => {
const newProgress = { position: completed ? episode.duration : 0, isCompleted: completed };
// Optimistic update: local state + all cached query pages
setEpisodes(prev =>
prev.map(ep => ep._id === episode._id ? { ...ep, progress: newProgress } : ep)
);
queryClient.setQueriesData<any>(
{ queryKey: ['episodes', 'latest'], exact: false },
(old: any) => {
if (!old?.episodes) return old;
return {
...old,
episodes: old.episodes.map((ep: Episode) =>
ep._id === episode._id ? { ...ep, progress: newProgress } : ep
),
};
}
);
try { try {
await episodeService.saveProgress( await episodeService.saveProgress(episode._id, newProgress.position, completed);
episode._id,
completed ? episode.duration : 0,
completed
);
setEpisodes(prev =>
prev.map(ep =>
ep._id === episode._id
? { ...ep, progress: { position: completed ? ep.duration : 0, isCompleted: completed } }
: ep
)
);
} catch (error) { } catch (error) {
console.error('Failed to toggle read state:', error); console.error('Failed to toggle read state:', error);
// Revert on error
queryClient.invalidateQueries({ queryKey: ['episodes', 'latest'] });
} }
}; };
@@ -163,7 +172,7 @@ export default function Dashboard() {
<div className="relative z-10"> <div className="relative z-10">
<div className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-[var(--accent-glow)] border border-[var(--border-color)] text-[10px] font-black text-[var(--accent-primary)] uppercase tracking-widest mb-4"> <div className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-[var(--accent-glow)] border border-[var(--border-color)] text-[10px] font-black text-[var(--accent-primary)] uppercase tracking-widest mb-4">
<Sparkles className="w-3 h-3" /> <Sparkles className="w-3 h-3" />
Podcastic Premium Podcastic
</div> </div>
<h2 className="text-4xl lg:text-5xl font-display font-black mb-4 tracking-tight leading-tight max-w-2xl text-[var(--text-primary)]"> <h2 className="text-4xl lg:text-5xl font-display font-black mb-4 tracking-tight leading-tight max-w-2xl text-[var(--text-primary)]">
Découvrez les nouveautés de votre univers audio. Découvrez les nouveautés de votre univers audio.
@@ -181,9 +190,9 @@ export default function Dashboard() {
Reprendre : {episodesInProgress[0].title.substring(0, 20)}... Reprendre : {episodesInProgress[0].title.substring(0, 20)}...
</button> </button>
) : ( ) : (
<Link to="/trending" className="neon-button flex items-center gap-2"> <Link to="/add" className="neon-button flex items-center gap-2">
<TrendingUp className="w-4 h-4" /> <TrendingUp className="w-4 h-4" />
Découvrir Découvrir des podcasts
</Link> </Link>
)} )}
<Link to="/trending" className="px-6 py-3 rounded-2xl bg-[var(--bg-secondary)] text-[var(--text-secondary)] font-bold text-sm hover:bg-[var(--accent-primary)]/10 transition-all border border-[var(--border-color)] flex items-center justify-center"> <Link to="/trending" className="px-6 py-3 rounded-2xl bg-[var(--bg-secondary)] text-[var(--text-secondary)] font-bold text-sm hover:bg-[var(--accent-primary)]/10 transition-all border border-[var(--border-color)] flex items-center justify-center">
+1 -1
View File
@@ -61,7 +61,7 @@ export default function Login() {
<h1 className="text-5xl font-display font-black mb-3 tracking-tighter">PODCASTIC</h1> <h1 className="text-5xl font-display font-black mb-3 tracking-tighter">PODCASTIC</h1>
<div className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-[var(--bg-secondary)] border border-[var(--border-color)] text-[10px] font-black text-[var(--accent-primary)] uppercase tracking-[0.2em]"> <div className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-[var(--bg-secondary)] border border-[var(--border-color)] text-[10px] font-black text-[var(--accent-primary)] uppercase tracking-[0.2em]">
<Sparkles className="w-3 h-3" /> <Sparkles className="w-3 h-3" />
Expérience Audio Premium Votre espace audio
</div> </div>
</div> </div>