feat: VideoPage — ajouter à une playlist (owned + edit-permitted)
All checks were successful
CI/CD — Build & Deploy / Build & Deploy (push) Successful in 23s
All checks were successful
CI/CD — Build & Deploy / Build & Deploy (push) Successful in 23s
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { useParams, Link } from 'react-router-dom';
|
import { useParams, Link } from 'react-router-dom';
|
||||||
import { apiFetch, ApiError } from '../lib/api';
|
import { apiFetch, ApiError } from '../lib/api';
|
||||||
|
import { useAuthContext } from '../context/AuthContext';
|
||||||
import VideoPlayer from '../components/VideoPlayer';
|
import VideoPlayer from '../components/VideoPlayer';
|
||||||
|
|
||||||
interface Video {
|
interface Video {
|
||||||
@@ -20,8 +21,14 @@ interface VideoResponse {
|
|||||||
data: { video: Video };
|
data: { video: Video };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface Playlist {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
}
|
||||||
|
|
||||||
export default function VideoPage() {
|
export default function VideoPage() {
|
||||||
const { id } = useParams<{ id: string }>();
|
const { id } = useParams<{ id: string }>();
|
||||||
|
const { user } = useAuthContext();
|
||||||
const [video, setVideo] = useState<Video | null>(null);
|
const [video, setVideo] = useState<Video | null>(null);
|
||||||
const [error, setError] = useState<'forbidden' | 'not_found' | 'unknown' | null>(null);
|
const [error, setError] = useState<'forbidden' | 'not_found' | 'unknown' | null>(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
@@ -106,6 +113,8 @@ export default function VideoPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{user && <AddToPlaylist videoId={video.id} />}
|
||||||
|
|
||||||
<Link to="/" className="self-start font-mono text-xs text-od-muted hover:text-od-text transition-colors">
|
<Link to="/" className="self-start font-mono text-xs text-od-muted hover:text-od-text transition-colors">
|
||||||
← Retour aux vidéos
|
← Retour aux vidéos
|
||||||
</Link>
|
</Link>
|
||||||
@@ -113,3 +122,70 @@ export default function VideoPage() {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Ajouter à une playlist ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function AddToPlaylist({ videoId }: { videoId: string }) {
|
||||||
|
const [playlists, setPlaylists] = useState<Playlist[]>([]);
|
||||||
|
const [selected, setSelected] = useState('');
|
||||||
|
const [adding, setAdding] = useState(false);
|
||||||
|
const [status, setStatus] = useState<'idle' | 'ok' | 'already' | 'error'>('idle');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
apiFetch<{ success: boolean; data: { owned: Playlist[]; shared: (Playlist & { permission: string })[] } }>(
|
||||||
|
'/playlists'
|
||||||
|
).then((res) => {
|
||||||
|
const editable = [
|
||||||
|
...res.data.owned,
|
||||||
|
...res.data.shared.filter((p) => p.permission === 'edit'),
|
||||||
|
];
|
||||||
|
setPlaylists(editable);
|
||||||
|
if (editable.length > 0) setSelected(editable[0].id);
|
||||||
|
}).catch(() => {});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
async function handleAdd() {
|
||||||
|
if (!selected || adding) return;
|
||||||
|
setAdding(true);
|
||||||
|
setStatus('idle');
|
||||||
|
try {
|
||||||
|
await apiFetch(`/playlists/${selected}/videos`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ videoId }),
|
||||||
|
});
|
||||||
|
setStatus('ok');
|
||||||
|
} catch (e) {
|
||||||
|
setStatus(e instanceof ApiError && e.status === 409 ? 'already' : 'error');
|
||||||
|
}
|
||||||
|
setAdding(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (playlists.length === 0) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-2 rounded border border-od-border bg-od-surface p-4">
|
||||||
|
<p className="font-mono text-xs text-od-muted uppercase tracking-widest">Ajouter à une playlist</p>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<select
|
||||||
|
value={selected}
|
||||||
|
onChange={(e) => { setSelected(e.target.value); setStatus('idle'); }}
|
||||||
|
className="flex-1 rounded border border-od-border bg-od-bg px-3 py-2 text-sm text-od-text outline-none focus:border-od-accent"
|
||||||
|
>
|
||||||
|
{playlists.map((p) => (
|
||||||
|
<option key={p.id} value={p.id}>{p.title}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
<button
|
||||||
|
onClick={handleAdd}
|
||||||
|
disabled={adding}
|
||||||
|
className="rounded border border-od-accent px-4 py-2 font-mono text-xs text-od-accent hover:bg-od-accent hover:text-od-bg transition-colors disabled:opacity-40"
|
||||||
|
>
|
||||||
|
{adding ? '…' : '+ Ajouter'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{status === 'ok' && <p className="font-mono text-xs text-od-ok">Vidéo ajoutée.</p>}
|
||||||
|
{status === 'already'&& <p className="font-mono text-xs text-od-muted">Déjà dans cette playlist.</p>}
|
||||||
|
{status === 'error' && <p className="font-mono text-xs text-od-crit">Erreur — réessaie.</p>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user