mirror of
https://github.com/R0m1k3/podcastic.git
synced 2026-10-11 17:26:20 +02:00
Merge branch 'main' of https://github.com/R0m1k3/podcastic
This commit is contained in:
commit
35f34d9d44
7 files changed
+81
-28
No files matched your search
@@ -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
@@ -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:
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user