Three rules cleaned in parallel passes — 187 fewer warnings, 0 TS
errors, 0 behaviour change beyond one incidental auth bugfix
flagged below.
storybook/no-redundant-story-name (23 → 0) — 14 stories files
Storybook v7+ infers the story name from the variable name, so
`name: 'Default'` next to `export const Default: Story = …` is
pure noise. Removed only when the name was redundant ;
preserved when the label was a French translation
('Par défaut', 'Chargement', 'Avec erreur', etc.) since those
are intentional.
react-refresh/only-export-components (25 → 0) — 21 files
Each warning marks a file that exports a React component AND a
hook / context / constant / barrel re-export. Suppressed
per-line with the suppression-with-justification pattern :
// eslint-disable-next-line react-refresh/only-export-components -- <kind>; refactor would split a tightly-coupled API
The justification matters — every comment names the specific
thing being co-located (hook / context / CVA constant / lazy
registry / route config / test util / backward-compat barrel).
Splitting these would create 21 new files for a HMR-only DX
win that's already a non-issue in practice.
@typescript-eslint/no-non-null-assertion (139 → 0) — 43 files
Distribution of fixes :
~85 cases : refactored to explicit guard
`if (!x) throw new Error('invariant: …')`
or hoisted into local with narrowing.
~36 cases : helper extraction (one tooltip test had 16
`wrapper!` patterns reduced to a single
`getWrapper()` helper).
~18 cases : suppressed with specific reason :
static literal arrays where index is provably
in bounds, mock fixtures with structural
guarantees, filter-then-map patterns where the
filter excludes the null branch.
One incidental find : services/api/auth.ts threw on missing
tokens but didn't guard `user` ; added the missing check while
refactoring the `user!` to a guard.
baseline post-commit : 921 warnings, 0 errors, 0 TS errors.
The remaining buckets are no-restricted-syntax (757, design-system
guardrail), no-explicit-any (115), exhaustive-deps (49).
CI --max-warnings will be lowered to 921 in the follow-up commit.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
126 lines
4.1 KiB
TypeScript
126 lines
4.1 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 { useTranslation } from '@/hooks/useTranslation';
|
|
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 { t } = useTranslation();
|
|
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" />
|
|
{t('tracks.stemsSection.loading')}
|
|
</div>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
if (error) {
|
|
return (
|
|
<Card variant="glass" className="p-6">
|
|
<p className="text-destructive">{t('tracks.stemsSection.loadError')}</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">{t('tracks.stemsSection.title')}</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" />
|
|
)}
|
|
{t('tracks.stemsSection.upload')}
|
|
</Button>
|
|
)}
|
|
</div>
|
|
|
|
{!hasStems && !showUpload ? (
|
|
<p className="text-muted-foreground">{t('tracks.stemsSection.empty')}</p>
|
|
) : !hasStems ? (
|
|
<p className="text-muted-foreground">{t('tracks.stemsSection.uploadHelp')}</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>
|
|
);
|
|
}
|