veza/apps/web/src/features/tracks/components/TrackStemsSection.tsx
senke 559cfbee3e refactor(web): zero out 3 ESLint warning buckets (storybook + react-refresh + non-null-assertion)
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>
2026-04-30 23:30:22 +02:00

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>
);
}