mirror of
https://github.com/R0m1k3/Socialflow.git
synced 2026-10-24 15:38:10 +02:00
Refonte UI : SQL, musiques, historique, comptes connectés, utilisateurs
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017ADkrVcVFTT14hbB148HYS
This commit is contained in:
11 files changed
+745
-973
No files matched your search
+121
-136
@@ -1,8 +1,18 @@
|
||||
import { useState } from "react";
|
||||
import { Page } from "@/components/layout/app-shell";
|
||||
import { PageHeader } from "@/components/layout/page-header";
|
||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import Sidebar from "@/components/sidebar";
|
||||
import TopBar from "@/components/topbar";
|
||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
|
||||
import { EmptyState } from "@/components/empty-state";
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from "@/components/ui/alert-dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import { Trash2, Music, Upload, Loader2, Play, Pause } from "lucide-react";
|
||||
@@ -20,10 +30,10 @@ interface AudioTrack {
|
||||
}
|
||||
|
||||
export default function AudioAdmin() {
|
||||
const [sidebarOpen, setSidebarOpen] = useState(false);
|
||||
const [isUploading, setIsUploading] = useState(false);
|
||||
const [playingTrackId, setPlayingTrackId] = useState<number | null>(null);
|
||||
const [audioElement, setAudioElement] = useState<HTMLAudioElement | null>(null);
|
||||
const [trackToDelete, setTrackToDelete] = useState<AudioTrack | null>(null);
|
||||
|
||||
const { toast } = useToast();
|
||||
const queryClient = useQueryClient();
|
||||
@@ -149,140 +159,115 @@ export default function AudioAdmin() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex h-screen overflow-hidden bg-background">
|
||||
{sidebarOpen && (
|
||||
<div
|
||||
className="fixed inset-0 bg-black/50 z-40 lg:hidden"
|
||||
onClick={() => setSidebarOpen(false)}
|
||||
<Page width="default">
|
||||
<input
|
||||
type="file"
|
||||
id="audio-upload"
|
||||
className="hidden"
|
||||
accept="audio/*"
|
||||
multiple
|
||||
onChange={handleFileUpload}
|
||||
disabled={isUploading}
|
||||
/>
|
||||
<PageHeader
|
||||
icon={Music}
|
||||
title="Musiques"
|
||||
description="Les pistes proposées lors de la création d'un Reel."
|
||||
actions={
|
||||
<Button asChild disabled={isUploading} className="cursor-pointer">
|
||||
<label htmlFor="audio-upload">
|
||||
{isUploading ? <Loader2 className="h-4 w-4 animate-spin" /> : <Upload className="h-4 w-4" />}
|
||||
{isUploading ? "Import en cours…" : "Importer des musiques"}
|
||||
</label>
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
|
||||
{isLoading ? (
|
||||
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{[1, 2, 3].map((i) => (
|
||||
<div key={i} className="skeleton h-[76px] rounded-xl" />
|
||||
))}
|
||||
</div>
|
||||
) : tracks.length === 0 ? (
|
||||
<EmptyState
|
||||
icon={Music}
|
||||
title="Aucune musique"
|
||||
description="Importez des fichiers audio (MP3, WAV…) pour les proposer dans vos Reels."
|
||||
action={
|
||||
<Button asChild className="cursor-pointer">
|
||||
<label htmlFor="audio-upload">
|
||||
<Upload className="h-4 w-4" /> Importer des musiques
|
||||
</label>
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<p className="mb-3 text-sm text-muted-foreground">{tracks.length} piste(s)</p>
|
||||
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{tracks.map((track) => {
|
||||
const playing = playingTrackId === track.id;
|
||||
return (
|
||||
<div
|
||||
key={track.id}
|
||||
className={`flex items-center gap-3 rounded-xl border bg-card p-3 shadow-soft transition-colors ${playing ? "border-primary/50" : ""}`}
|
||||
data-testid={`track-${track.id}`}
|
||||
>
|
||||
<Button
|
||||
variant={playing ? "default" : "secondary"}
|
||||
size="icon"
|
||||
className="h-11 w-11 shrink-0 rounded-full"
|
||||
onClick={() => togglePlay(track)}
|
||||
aria-label={playing ? "Pause" : "Écouter"}
|
||||
>
|
||||
{playing ? <Pause className="h-4 w-4" /> : <Play className="h-4 w-4" />}
|
||||
</Button>
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="truncate text-sm font-medium" title={track.title}>{track.title}</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Ajoutée le {format(new Date(track.createdAt), "d MMM yyyy", { locale: fr })}
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="shrink-0 text-muted-foreground hover:bg-destructive/10 hover:text-destructive"
|
||||
onClick={() => setTrackToDelete(track)}
|
||||
disabled={deleteMutation.isPending}
|
||||
aria-label="Supprimer"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className={`
|
||||
fixed lg:static inset-y-0 left-0 z-50 transform transition-transform duration-300 ease-in-out
|
||||
${sidebarOpen ? 'translate-x-0' : '-translate-x-full lg:translate-x-0'}
|
||||
`}>
|
||||
<Sidebar onLinkClick={() => setSidebarOpen(false)} />
|
||||
</div>
|
||||
|
||||
<main className="flex-1 overflow-y-auto w-full">
|
||||
<TopBar onMenuClick={() => setSidebarOpen(!sidebarOpen)} />
|
||||
|
||||
<div className="p-4 sm:p-6 lg:p-8 max-w-7xl mx-auto space-y-8">
|
||||
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold flex items-center gap-3">
|
||||
<Music className="w-8 h-8 text-primary" />
|
||||
Bibliothèque Audio
|
||||
</h1>
|
||||
<p className="text-muted-foreground mt-2">
|
||||
Gérez les musiques disponibles pour la création de Reels
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<input
|
||||
type="file"
|
||||
id="audio-upload"
|
||||
className="hidden"
|
||||
accept="audio/*"
|
||||
multiple
|
||||
onChange={handleFileUpload}
|
||||
disabled={isUploading}
|
||||
/>
|
||||
<label htmlFor="audio-upload">
|
||||
<Button
|
||||
asChild
|
||||
disabled={isUploading}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
<span>
|
||||
{isUploading ? (
|
||||
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||||
) : (
|
||||
<Upload className="w-4 h-4 mr-2" />
|
||||
)}
|
||||
{isUploading ? "Upload en cours..." : "Ajouter un fichier audio"}
|
||||
</span>
|
||||
</Button>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Card className="border-border/50 shadow-sm">
|
||||
<CardHeader>
|
||||
<CardTitle>Musiques ({tracks.length})</CardTitle>
|
||||
<CardDescription>
|
||||
Ces pistes pourront être sélectionnées par les utilisateurs lors de la création d'un nouveau Reel.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{isLoading ? (
|
||||
<div className="flex justify-center p-8">
|
||||
<Loader2 className="w-8 h-8 animate-spin text-primary" />
|
||||
</div>
|
||||
) : tracks.length === 0 ? (
|
||||
<div className="text-center p-12 border-2 border-dashed border-border rounded-xl">
|
||||
<Music className="w-12 h-12 text-muted-foreground mx-auto mb-4 opacity-50" />
|
||||
<h3 className="text-lg font-medium mb-1">Aucune musique</h3>
|
||||
<p className="text-muted-foreground">
|
||||
Vous n'avez pas encore ajouté de piste audio. Les musiques ajoutées ici seront utilisables pour les Reels.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
|
||||
{tracks.map((track) => (
|
||||
<div
|
||||
key={track.id}
|
||||
className="bg-card border border-border/50 rounded-xl p-4 flex flex-col gap-4 hover:border-primary/30 transition-colors shadow-sm"
|
||||
>
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="p-3 bg-primary/10 rounded-lg shrink-0">
|
||||
<Music className="w-6 h-6 text-primary" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="font-semibold text-sm truncate" title={track.title}>{track.title}</p>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
{format(new Date(track.createdAt), "dd MMM yyyy", { locale: fr })}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between pt-2 border-t border-border/50">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className={playingTrackId === track.id ? "text-primary bg-primary/10" : ""}
|
||||
onClick={() => togglePlay(track)}
|
||||
>
|
||||
{playingTrackId === track.id ? (
|
||||
<><Pause className="w-4 h-4 mr-2" /> Pause</>
|
||||
) : (
|
||||
<><Play className="w-4 h-4 mr-2" /> Écouter</>
|
||||
)}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="text-destructive hover:text-destructive hover:bg-destructive/10"
|
||||
onClick={() => {
|
||||
if (confirm("Voulez-vous vraiment supprimer cette musique ?")) {
|
||||
deleteMutation.mutate(track.id);
|
||||
}
|
||||
}}
|
||||
disabled={deleteMutation.isPending}
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
<AlertDialog open={!!trackToDelete} onOpenChange={(open) => !open && setTrackToDelete(null)}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Supprimer cette musique ?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
« {trackToDelete?.title} » ne sera plus proposée dans les nouveaux Reels.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Annuler</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
||||
onClick={() => {
|
||||
if (trackToDelete) deleteMutation.mutate(trackToDelete.id);
|
||||
setTrackToDelete(null);
|
||||
}}
|
||||
>
|
||||
Supprimer
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user