veza/apps/web/src/components/upload/FileUploadZone.tsx
senke 39b2b642d2 feat(web): UI premium Discord/Spotify-like — tokens, shadows, focus, layout
Plan UI premium 6–8 semaines (design system, shell, Storybook, a11y):

- Design system: DESIGN_TOKENS.md, APP_SHELL.md, FULL_LAYOUT_PAGE.md. Single source
  for layout/shell (index.css), shadows (design-system.css), durations/easing.
- Tokens: shadow-cover-depth, shadow-gold-glow, shadow-fab-glow; layout max-height
  (max-h-layout-drawer, max-h-layout-panel, max-h-layout-list). All duration-200/300/500
  replaced by --duration-fast/normal/slow. Arbitrary shadows replaced by token classes.
- Shell & player: Sidebar, Header, GlobalPlayer, MiniPlayer, PlayerQueue, PlayerControls,
  AudioPlayer use tokens; focus-visible on Sidebar, PlayerQueue, DropdownMenuTrigger/Item,
  TabsTrigger. Typography: text-[10px]/[9px] → text-xs where applicable.
- ESLint: no-restricted-syntax (warn) for w-/h-/rounded-/shadow-/text-/spacing arbitrary.
- Scripts: report-arbitrary-values.mjs, capture/compare/generate visual; visual-complete.spec.ts.
- Stories full layout: Dashboard, Playlists, Library, Settings, Profile in DashboardLayout.stories.
- .cursorrules + README: DESIGN_TOKENS, APP_SHELL, visual commands, no arbitrary without justification.
- apps/web/.gitignore: e2e test artifacts (test-results-visual, playwright-report-visual).

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-08 17:15:58 +01:00

120 lines
3.7 KiB
TypeScript

import React, { useCallback, useState } from 'react';
import { UploadCloud } from 'lucide-react';
import { useToast } from '../../components/feedback/ToastProvider';
interface FileUploadZoneProps {
onFilesSelected: (files: File[]) => void;
acceptedFormats?: string[];
maxSizeInMB?: number;
}
export const FileUploadZone: React.FC<FileUploadZoneProps> = ({
onFilesSelected,
acceptedFormats = ['.wav', '.mp3', '.aiff', '.flac', '.zip'],
maxSizeInMB = 500,
}) => {
const { addToast } = useToast();
const [isDragging, setIsDragging] = useState(false);
const validateFile = (file: File): boolean => {
// Check size
if (file.size > maxSizeInMB * 1024 * 1024) {
addToast(`File ${file.name} exceeds ${maxSizeInMB}MB limit`, 'error');
return false;
}
// Check extension (simple check)
const ext = `.${file.name.split('.').pop()?.toLowerCase()}`;
if (!acceptedFormats.includes(ext)) {
addToast(`Format ${ext} not supported for ${file.name}`, 'error');
return false;
}
return true;
};
const handleDrop = useCallback(
(e: React.DragEvent) => {
e.preventDefault();
setIsDragging(false);
const droppedFiles = Array.from(e.dataTransfer.files);
const validFiles = droppedFiles.filter(validateFile);
if (validFiles.length > 0) {
onFilesSelected(validFiles);
}
},
[onFilesSelected],
);
const handleFileInput = (e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.files) {
const selectedFiles = Array.from(e.target.files);
const validFiles = selectedFiles.filter(validateFile);
if (validFiles.length > 0) {
onFilesSelected(validFiles);
}
}
};
return (
<div
onDragOver={(e) => {
e.preventDefault();
setIsDragging(true);
}}
onDragLeave={() => setIsDragging(false)}
onDrop={handleDrop}
className={`
border-2 border-dashed rounded-xl p-12 flex flex-col items-center justify-center text-center transition-all duration-[var(--duration-normal)] group cursor-pointer
${
isDragging
? 'border-primary bg-primary/10 scale-[1.02]'
: 'border-border/50 bg-muted/30 hover:bg-muted/50 hover:border-border/50'
}
`}
>
<input
type="file"
multiple
className="hidden"
id="file-upload-input"
onChange={handleFileInput}
accept={acceptedFormats.join(',')}
/>
<label
htmlFor="file-upload-input"
className="cursor-pointer w-full flex flex-col items-center"
>
<div
className={`
w-20 h-20 rounded-full flex items-center justify-center mb-6 transition-all duration-[var(--duration-normal)] shadow-lg
${isDragging ? 'bg-kodo-cyan text-black' : 'bg-muted text-primary group-hover:bg-border'}
`}
>
<UploadCloud className="w-10 h-10" />
</div>
<h3 className="text-xl font-display font-bold text-white mb-2">
{isDragging ? 'Drop Files Here' : 'Drag & Drop or Click'}
</h3>
<p className="text-muted-foreground mb-6 max-w-sm">
Upload audio stems, project archives, or sample packs.
</p>
<div className="flex flex-wrap justify-center gap-2 max-w-md">
{acceptedFormats.map((fmt) => (
<span
key={fmt}
className="px-2 py-1 bg-black/30 rounded text-xs font-mono text-muted-foreground border border-white/5 uppercase"
>
{fmt.replace('.', '')}
</span>
))}
</div>
<p className="mt-4 text-xs text-muted-foreground">
Max file size: {maxSizeInMB}MB
</p>
</label>
</div>
);
};