veza/apps/web/src/features/tracks/components/TrackStemsSection.tsx
senke 871a0f2a05
Some checks failed
Backend API CI / test-integration (push) Failing after 0s
Frontend CI / test (push) Failing after 0s
Storybook Audit / Build & audit Storybook (push) Failing after 0s
Backend API CI / test-unit (push) Failing after 0s
feat(v0.10.7): Collaboration Temps Réel F481-F483
- F481: Co-listening sessions (WebSocket sync, ListenTogether page)
- F482: Stem sharing (upload/list/download wav,aiff,flac)
- F483: Collaborative rooms (type collaborative, max 10, invite-only)
- Roadmap: v0.10.7 → DONE
2026-03-10 13:34:16 +01:00

124 lines
4 KiB
TypeScript

import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { Upload, Download, Loader2, Disc3 } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Card } from '@/components/ui/card';
import { trackStemService, type TrackStem } from '../services/trackStemService';
import type { Track } from '../types/track';
interface TrackStemsSectionProps {
track: Track;
isCreator: boolean;
}
function formatBytes(bytes: number): string {
if (bytes < 1024) return `${bytes} B`;
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
}
export function TrackStemsSection({ track, isCreator }: TrackStemsSectionProps) {
const queryClient = useQueryClient();
const { data: stems, isLoading, error } = useQuery({
queryKey: ['track-stems', track.id],
queryFn: () => trackStemService.listStems(track.id),
});
const uploadMutation = useMutation({
mutationFn: ({ file, name }: { file: File; name?: string }) =>
trackStemService.uploadStem(track.id, file, name),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['track-stems', track.id] });
},
});
const handleUpload = () => {
const input = document.createElement('input');
input.type = 'file';
input.accept = '.wav,.aiff,.aif,.flac';
input.onchange = async (e) => {
const file = (e.target as HTMLInputElement).files?.[0];
if (!file) return;
uploadMutation.mutate({ file, name: file.name.replace(/\.[^.]+$/, '') });
};
input.click();
};
const handleDownload = async (stem: TrackStem) => {
await trackStemService.downloadStem(track.id, stem.name, stem.format);
};
if (isLoading) {
return (
<Card variant="glass" className="p-6">
<div className="flex items-center gap-3 text-muted-foreground">
<Loader2 className="h-5 w-5 animate-spin" />
Loading stems...
</div>
</Card>
);
}
if (error) {
return (
<Card variant="glass" className="p-6">
<p className="text-destructive">Failed to load stems.</p>
</Card>
);
}
const hasStems = stems && stems.length > 0;
const showUpload = isCreator;
return (
<Card variant="glass" className="p-6">
<div className="flex items-center justify-between gap-3 mb-6">
<div className="flex items-center gap-3">
<div className="flex justify-center w-8 h-8 rounded-lg bg-primary/10">
<Disc3 className="w-4 h-4 text-primary" />
</div>
<h3 className="text-heading-3">Stems</h3>
</div>
{showUpload && (
<Button
onClick={handleUpload}
disabled={uploadMutation.isPending}
size="sm"
variant="outline"
>
{uploadMutation.isPending ? (
<Loader2 className="w-4 h-4 animate-spin mr-2" />
) : (
<Upload className="w-4 h-4 mr-2" />
)}
Upload
</Button>
)}
</div>
{!hasStems && !showUpload ? (
<p className="text-muted-foreground">No stems available.</p>
) : !hasStems ? (
<p className="text-muted-foreground">Upload stems (WAV, AIFF, FLAC) to share with collaborators.</p>
) : (
<ul className="space-y-2">
{stems!.map((stem) => (
<li
key={stem.id}
className="flex items-center justify-between gap-4 py-2 px-3 rounded-lg bg-muted/30 hover:bg-muted/50"
>
<span className="font-medium truncate">{stem.name}</span>
<span className="text-sm text-muted-foreground">{stem.format} · {formatBytes(stem.size_bytes)}</span>
<Button
variant="ghost"
size="sm"
onClick={() => handleDownload(stem)}
>
<Download className="w-4 h-4" />
</Button>
</li>
))}
</ul>
)}
</Card>
);
}