website refactor
This commit is contained in:
@@ -1,106 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { Card } from '@/ui/Card';
|
||||
import { Image } from '@/ui/Image';
|
||||
import { ImagePlaceholder } from '@/ui/ImagePlaceholder';
|
||||
import { Stack } from '@/ui/primitives/Stack';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { motion } from 'framer-motion';
|
||||
import React from 'react';
|
||||
|
||||
export interface MediaCardProps {
|
||||
src?: string;
|
||||
alt?: string;
|
||||
title?: string;
|
||||
subtitle?: string;
|
||||
aspectRatio?: string;
|
||||
isLoading?: boolean;
|
||||
error?: string;
|
||||
onClick?: () => void;
|
||||
actions?: React.ReactNode;
|
||||
}
|
||||
|
||||
export function MediaCard({
|
||||
src,
|
||||
alt = 'Media asset',
|
||||
title,
|
||||
subtitle,
|
||||
aspectRatio = '16/9',
|
||||
isLoading,
|
||||
error,
|
||||
onClick,
|
||||
actions,
|
||||
}: MediaCardProps) {
|
||||
return (
|
||||
<Stack
|
||||
as={motion.div}
|
||||
whileHover={{ y: -4 }}
|
||||
transition={{ duration: 0.2, ease: [0.25, 0.1, 0.25, 1] }} // Fast ease-out
|
||||
h="full"
|
||||
gap={0}
|
||||
>
|
||||
<Card
|
||||
bg="bg-charcoal/40"
|
||||
border
|
||||
borderColor="border-charcoal-outline/30"
|
||||
rounded="lg"
|
||||
overflow="hidden"
|
||||
cursor={onClick ? 'pointer' : 'default'}
|
||||
onClick={onClick}
|
||||
group
|
||||
h="full"
|
||||
p={0}
|
||||
>
|
||||
<Stack position="relative" w="full" aspectRatio={aspectRatio} gap={0}>
|
||||
{isLoading ? (
|
||||
<ImagePlaceholder variant="loading" aspectRatio={aspectRatio} rounded="none" />
|
||||
) : error ? (
|
||||
<ImagePlaceholder variant="error" message={error} aspectRatio={aspectRatio} rounded="none" />
|
||||
) : src ? (
|
||||
<Stack w="full" h="full" overflow="hidden" gap={0}>
|
||||
<Image
|
||||
src={src}
|
||||
alt={alt}
|
||||
objectFit="cover"
|
||||
fullWidth
|
||||
fullHeight
|
||||
/>
|
||||
</Stack>
|
||||
) : (
|
||||
<ImagePlaceholder aspectRatio={aspectRatio} rounded="none" />
|
||||
)}
|
||||
|
||||
{actions && (
|
||||
<Stack
|
||||
position="absolute"
|
||||
top="2"
|
||||
right="2"
|
||||
direction="row"
|
||||
gap={2}
|
||||
opacity={0}
|
||||
groupHoverOpacity={1}
|
||||
transition
|
||||
>
|
||||
{actions}
|
||||
</Stack>
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
{(title || subtitle) && (
|
||||
<Stack p={3} borderTop borderStyle="solid" borderColor="border-charcoal-outline/20" gap={0}>
|
||||
{title && (
|
||||
<Text block size="sm" weight="semibold" truncate color="text-white">
|
||||
{title}
|
||||
</Text>
|
||||
)}
|
||||
{subtitle && (
|
||||
<Text block size="xs" color="text-gray-400" truncate mt={0.5}>
|
||||
{subtitle}
|
||||
</Text>
|
||||
)}
|
||||
</Stack>
|
||||
)}
|
||||
</Card>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
@@ -1,8 +1,13 @@
|
||||
'use client';
|
||||
|
||||
import { IconButton } from '@/ui/IconButton';
|
||||
import { Input } from '@/ui/Input';
|
||||
import { Stack } from '@/ui/primitives/Stack';
|
||||
import { Select } from '@/ui/Select';
|
||||
import { ControlBar } from '@/ui/ControlBar';
|
||||
import { SegmentedControl } from '@/ui/SegmentedControl';
|
||||
import { Icon } from '@/ui/Icon';
|
||||
import { Grid, List, Search } from 'lucide-react';
|
||||
import React from 'react';
|
||||
|
||||
export interface MediaFiltersBarProps {
|
||||
searchQuery: string;
|
||||
@@ -24,56 +29,41 @@ export function MediaFiltersBar({
|
||||
onViewModeChange,
|
||||
}: MediaFiltersBarProps) {
|
||||
return (
|
||||
<Stack
|
||||
direction={{ base: 'col', md: 'row' }}
|
||||
align={{ base: 'stretch', md: 'center' }}
|
||||
justify="between"
|
||||
gap={4}
|
||||
p={4}
|
||||
bg="bg-charcoal/20"
|
||||
border
|
||||
borderColor="border-charcoal-outline/20"
|
||||
rounded="xl"
|
||||
>
|
||||
<Stack flexGrow={1} maxWidth={{ md: 'md' }} gap={0}>
|
||||
<Input
|
||||
placeholder="Search media assets..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => onSearchChange(e.target.value)}
|
||||
icon={<Search size={18} />}
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
<Stack direction="row" align="center" gap={3}>
|
||||
<Stack w="40" gap={0}>
|
||||
<Select
|
||||
value={category}
|
||||
onChange={(e) => onCategoryChange(e.target.value)}
|
||||
options={categories}
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
{onViewModeChange && (
|
||||
<Stack direction="row" bg="bg-charcoal/40" p={1} rounded="lg" border borderColor="border-charcoal-outline/20" gap={0}>
|
||||
<IconButton
|
||||
icon={Grid}
|
||||
variant={viewMode === 'grid' ? 'primary' : 'ghost'}
|
||||
size="sm"
|
||||
onClick={() => onViewModeChange('grid')}
|
||||
color={viewMode === 'grid' ? 'text-white' : 'text-gray-400'}
|
||||
backgroundColor={viewMode === 'grid' ? 'bg-blue-600' : undefined}
|
||||
<div style={{ marginBottom: '1.5rem' }}>
|
||||
<ControlBar
|
||||
leftContent={
|
||||
<div style={{ maxWidth: '32rem', width: '100%' }}>
|
||||
<Input
|
||||
placeholder="Search media assets..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => onSearchChange(e.target.value)}
|
||||
icon={<Icon icon={Search} size={4} intent="low" />}
|
||||
fullWidth
|
||||
/>
|
||||
<IconButton
|
||||
icon={List}
|
||||
variant={viewMode === 'list' ? 'primary' : 'ghost'}
|
||||
size="sm"
|
||||
onClick={() => onViewModeChange('list')}
|
||||
color={viewMode === 'list' ? 'text-white' : 'text-gray-400'}
|
||||
backgroundColor={viewMode === 'list' ? 'bg-blue-600' : undefined}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '1rem' }}>
|
||||
<div style={{ width: '10rem' }}>
|
||||
<Select
|
||||
value={category}
|
||||
onChange={(e) => onCategoryChange(e.target.value)}
|
||||
options={categories}
|
||||
/>
|
||||
</Stack>
|
||||
)}
|
||||
</Stack>
|
||||
</Stack>
|
||||
</div>
|
||||
|
||||
{onViewModeChange && (
|
||||
<SegmentedControl
|
||||
options={[
|
||||
{ id: 'grid', label: '', icon: <Icon icon={Grid} size={4} /> },
|
||||
{ id: 'list', label: '', icon: <Icon icon={List} size={4} /> },
|
||||
]}
|
||||
activeId={viewMode}
|
||||
onChange={(id) => onViewModeChange(id as 'grid' | 'list')}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</ControlBar>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
'use client';
|
||||
|
||||
import { Box } from '@/ui/primitives/Box';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { useState } from 'react';
|
||||
import { MediaCard } from './MediaCard';
|
||||
import { MediaCard } from '@/ui/MediaCard';
|
||||
import { MediaFiltersBar } from './MediaFiltersBar';
|
||||
import { MediaGrid } from './MediaGrid';
|
||||
import { Grid } from '@/ui/Grid';
|
||||
import { MediaViewerModal } from './MediaViewerModal';
|
||||
import { SectionHeader } from '@/ui/SectionHeader';
|
||||
import { EmptyState } from '@/ui/EmptyState';
|
||||
import { Search } from 'lucide-react';
|
||||
import React from 'react';
|
||||
|
||||
export interface MediaAsset {
|
||||
id: string;
|
||||
@@ -60,17 +63,11 @@ export function MediaGallery({
|
||||
};
|
||||
|
||||
return (
|
||||
<Box display="flex" flexDirection="col" gap={6}>
|
||||
<Box>
|
||||
<Text as="h1" size="3xl" weight="bold" color="text-white">
|
||||
{title}
|
||||
</Text>
|
||||
{description && (
|
||||
<Text block size="sm" color="text-gray-400" mt={1}>
|
||||
{description}
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '1.5rem' }}>
|
||||
<SectionHeader
|
||||
title={title}
|
||||
description={description}
|
||||
/>
|
||||
|
||||
<MediaFiltersBar
|
||||
searchQuery={searchQuery}
|
||||
@@ -83,7 +80,7 @@ export function MediaGallery({
|
||||
/>
|
||||
|
||||
{filteredAssets.length > 0 ? (
|
||||
<MediaGrid columns={viewMode === 'grid' ? { base: 1, sm: 2, md: 3, lg: 4 } : { base: 1 }}>
|
||||
<Grid cols={viewMode === 'grid' ? { base: 1, sm: 2, md: 3, lg: 4 } : 1} gap={4}>
|
||||
{filteredAssets.map((asset) => (
|
||||
<MediaCard
|
||||
key={asset.id}
|
||||
@@ -93,11 +90,14 @@ export function MediaGallery({
|
||||
onClick={() => setViewerAsset(asset)}
|
||||
/>
|
||||
))}
|
||||
</MediaGrid>
|
||||
</Grid>
|
||||
) : (
|
||||
<Box py={20} center bg="bg-charcoal/10" rounded="xl" border borderStyle="dashed" borderColor="border-charcoal-outline/20">
|
||||
<Text color="text-gray-500">No media assets found matching your criteria.</Text>
|
||||
</Box>
|
||||
<EmptyState
|
||||
icon={Search}
|
||||
title="No media assets found"
|
||||
description="Try adjusting your search or filters"
|
||||
variant="minimal"
|
||||
/>
|
||||
)}
|
||||
|
||||
<MediaViewerModal
|
||||
@@ -109,6 +109,6 @@ export function MediaGallery({
|
||||
onNext={handleNext}
|
||||
onPrev={handlePrev}
|
||||
/>
|
||||
</Box>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { Grid } from '@/ui/primitives/Grid';
|
||||
import { Grid } from '@/ui/Grid';
|
||||
import React from 'react';
|
||||
|
||||
export interface MediaGridProps {
|
||||
children: React.ReactNode;
|
||||
columns?: {
|
||||
columns?: number | {
|
||||
base?: number;
|
||||
sm?: number;
|
||||
md?: number;
|
||||
lg?: number;
|
||||
xl?: number;
|
||||
};
|
||||
gap?: 0 | 1 | 2 | 3 | 4 | 6 | 8 | 12 | 16;
|
||||
gap?: number;
|
||||
}
|
||||
|
||||
export function MediaGrid({
|
||||
@@ -20,10 +20,8 @@ export function MediaGrid({
|
||||
}: MediaGridProps) {
|
||||
return (
|
||||
<Grid
|
||||
cols={(columns.base ?? 1) as any}
|
||||
mdCols={columns.md as any}
|
||||
lgCols={columns.lg as any}
|
||||
gap={gap as any}
|
||||
cols={columns}
|
||||
gap={gap}
|
||||
>
|
||||
{children}
|
||||
</Grid>
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
|
||||
import { IconButton } from '@/ui/IconButton';
|
||||
import { Image } from '@/ui/Image';
|
||||
import { Box } from '@/ui/primitives/Box';
|
||||
import { Modal } from '@/ui/Modal';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import { ChevronLeft, ChevronRight, Download, X } from 'lucide-react';
|
||||
import { ChevronLeft, ChevronRight, Download } from 'lucide-react';
|
||||
import React from 'react';
|
||||
|
||||
export interface MediaViewerModalProps {
|
||||
isOpen: boolean;
|
||||
@@ -27,143 +27,61 @@ export function MediaViewerModal({
|
||||
onPrev,
|
||||
}: MediaViewerModalProps) {
|
||||
return (
|
||||
<AnimatePresence>
|
||||
{isOpen && (
|
||||
<Box
|
||||
position="fixed"
|
||||
inset="0"
|
||||
zIndex={100}
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
p={{ base: 4, md: 8 }}
|
||||
>
|
||||
{/* Backdrop with frosted blur */}
|
||||
<Box
|
||||
as={motion.div}
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.2, ease: 'easeOut' }}
|
||||
position="absolute"
|
||||
inset="0"
|
||||
bg="bg-black/80"
|
||||
blur="md"
|
||||
onClick={onClose}
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
title={title}
|
||||
size="xl"
|
||||
footer={
|
||||
<div style={{ width: '100%', textAlign: 'center' }}>
|
||||
<Text size="xs" variant="low" uppercase>
|
||||
Precision Racing Media Viewer
|
||||
</Text>
|
||||
</div>
|
||||
}
|
||||
actions={
|
||||
<IconButton
|
||||
icon={Download}
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={() => src && window.open(src, '_blank')}
|
||||
title="Download"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<div style={{ position: 'relative', display: 'flex', alignItems: 'center', justifyContent: 'center', minHeight: '20rem' }}>
|
||||
{src ? (
|
||||
<Image
|
||||
src={src}
|
||||
alt={alt}
|
||||
objectFit="contain"
|
||||
/>
|
||||
) : (
|
||||
<Text variant="low">No image selected</Text>
|
||||
)}
|
||||
|
||||
{/* Content Container */}
|
||||
<Box
|
||||
as={motion.div}
|
||||
initial={{ opacity: 0, scale: 0.95 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
exit={{ opacity: 0, scale: 0.95 }}
|
||||
transition={{ duration: 0.2, ease: [0.25, 0.1, 0.25, 1] }}
|
||||
position="relative"
|
||||
zIndex={10}
|
||||
w="full"
|
||||
maxWidth="6xl"
|
||||
maxHeight="full"
|
||||
display="flex"
|
||||
flexDirection="col"
|
||||
>
|
||||
{/* Header */}
|
||||
<Box
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
justifyContent="between"
|
||||
mb={4}
|
||||
color="text-white"
|
||||
>
|
||||
<Box>
|
||||
{title && (
|
||||
<Text size="lg" weight="semibold" color="text-white">
|
||||
{title}
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
<Box display="flex" gap={2}>
|
||||
<IconButton
|
||||
icon={Download}
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={() => src && window.open(src, '_blank')}
|
||||
color="text-white"
|
||||
backgroundColor="bg-white/10"
|
||||
/>
|
||||
<IconButton
|
||||
icon={X}
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={onClose}
|
||||
color="text-white"
|
||||
backgroundColor="bg-white/10"
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Image Area */}
|
||||
<Box
|
||||
position="relative"
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
bg="bg-black/40"
|
||||
rounded="xl"
|
||||
overflow="hidden"
|
||||
border
|
||||
borderColor="border-white/10"
|
||||
flexGrow={1}
|
||||
minHeight="0"
|
||||
>
|
||||
{src ? (
|
||||
<Image
|
||||
src={src}
|
||||
alt={alt}
|
||||
objectFit="contain"
|
||||
fullWidth
|
||||
fullHeight
|
||||
/>
|
||||
) : (
|
||||
<Box p={20}>
|
||||
<Text color="text-gray-500">No image selected</Text>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Navigation Controls */}
|
||||
{onPrev && (
|
||||
<Box position="absolute" left="4" top="1/2" translateY="-1/2">
|
||||
<IconButton
|
||||
icon={ChevronLeft}
|
||||
variant="secondary"
|
||||
onClick={onPrev}
|
||||
color="text-white"
|
||||
backgroundColor="bg-black/50"
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
{onNext && (
|
||||
<Box position="absolute" right="4" top="1/2" translateY="-1/2">
|
||||
<IconButton
|
||||
icon={ChevronRight}
|
||||
variant="secondary"
|
||||
onClick={onNext}
|
||||
color="text-white"
|
||||
backgroundColor="bg-black/50"
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{/* Footer / Info */}
|
||||
<Box mt={4} display="flex" justifyContent="center">
|
||||
<Text size="xs" color="text-gray-400" uppercase letterSpacing="widest">
|
||||
Precision Racing Media Viewer
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
{/* Navigation Controls */}
|
||||
{onPrev && (
|
||||
<div style={{ position: 'absolute', left: '1rem', top: '50%', transform: 'translateY(-50%)' }}>
|
||||
<IconButton
|
||||
icon={ChevronLeft}
|
||||
variant="secondary"
|
||||
onClick={onPrev}
|
||||
title="Previous"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{onNext && (
|
||||
<div style={{ position: 'absolute', right: '1rem', top: '50%', transform: 'translateY(-50%)' }}>
|
||||
<IconButton
|
||||
icon={ChevronRight}
|
||||
variant="secondary"
|
||||
onClick={onNext}
|
||||
title="Next"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,187 +0,0 @@
|
||||
import { Button } from '@/ui/Button';
|
||||
import { Icon } from '@/ui/Icon';
|
||||
import { Box } from '@/ui/primitives/Box';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { AlertCircle, CheckCircle2, File, Upload, X } from 'lucide-react';
|
||||
import React, { useRef, useState } from 'react';
|
||||
|
||||
export interface UploadDropzoneProps {
|
||||
onFilesSelected: (files: File[]) => void;
|
||||
accept?: string;
|
||||
multiple?: boolean;
|
||||
maxSize?: number; // in bytes
|
||||
isLoading?: boolean;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export function UploadDropzone({
|
||||
onFilesSelected,
|
||||
accept,
|
||||
multiple = false,
|
||||
maxSize,
|
||||
isLoading,
|
||||
error,
|
||||
}: UploadDropzoneProps) {
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const [selectedFiles, setSelectedFiles] = useState<File[]>([]);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const handleDragOver = (e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
setIsDragging(true);
|
||||
};
|
||||
|
||||
const handleDragLeave = () => {
|
||||
setIsDragging(false);
|
||||
};
|
||||
|
||||
const handleDrop = (e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
setIsDragging(false);
|
||||
const files = Array.from(e.dataTransfer.files);
|
||||
validateAndSelectFiles(files);
|
||||
};
|
||||
|
||||
const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (e.target.files) {
|
||||
const files = Array.from(e.target.files);
|
||||
validateAndSelectFiles(files);
|
||||
}
|
||||
};
|
||||
|
||||
const validateAndSelectFiles = (files: File[]) => {
|
||||
let filteredFiles = files;
|
||||
|
||||
if (accept) {
|
||||
const acceptedTypes = accept.split(',').map(t => t.trim());
|
||||
filteredFiles = filteredFiles.filter(file => {
|
||||
return acceptedTypes.some(type => {
|
||||
if (type.startsWith('.')) {
|
||||
return file.name.endsWith(type);
|
||||
}
|
||||
if (type.endsWith('/*')) {
|
||||
return file.type.startsWith(type.replace('/*', ''));
|
||||
}
|
||||
return file.type === type;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (maxSize) {
|
||||
filteredFiles = filteredFiles.filter(file => file.size <= maxSize);
|
||||
}
|
||||
|
||||
if (!multiple) {
|
||||
filteredFiles = filteredFiles.slice(0, 1);
|
||||
}
|
||||
|
||||
setSelectedFiles(filteredFiles);
|
||||
onFilesSelected(filteredFiles);
|
||||
};
|
||||
|
||||
const removeFile = (index: number) => {
|
||||
const newFiles = [...selectedFiles];
|
||||
newFiles.splice(index, 1);
|
||||
setSelectedFiles(newFiles);
|
||||
onFilesSelected(newFiles);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box w="full">
|
||||
<Box
|
||||
onDragOver={handleDragOver}
|
||||
onDragLeave={handleDragLeave}
|
||||
onDrop={handleDrop}
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
display="flex"
|
||||
flexDirection="col"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
p={8}
|
||||
border
|
||||
borderStyle="dashed"
|
||||
borderColor={isDragging ? 'border-blue-500' : error ? 'border-amber-500' : 'border-charcoal-outline'}
|
||||
bg={isDragging ? 'bg-blue-500/5' : 'bg-charcoal-outline/10'}
|
||||
rounded="xl"
|
||||
cursor="pointer"
|
||||
transition
|
||||
hoverBg="bg-charcoal-outline/20"
|
||||
>
|
||||
<Box as="input"
|
||||
type="file"
|
||||
ref={fileInputRef}
|
||||
onChange={handleFileSelect}
|
||||
accept={accept}
|
||||
multiple={multiple}
|
||||
display="none"
|
||||
/>
|
||||
|
||||
<Icon
|
||||
icon={isLoading ? Upload : (selectedFiles.length > 0 ? CheckCircle2 : Upload)}
|
||||
size={10}
|
||||
color={isDragging ? 'text-blue-500' : error ? 'text-amber-500' : 'text-gray-500'}
|
||||
animate={isLoading ? 'pulse' : 'none'}
|
||||
mb={4}
|
||||
/>
|
||||
|
||||
<Text weight="semibold" size="lg" mb={1}>
|
||||
{isDragging ? 'Drop files here' : 'Click or drag to upload'}
|
||||
</Text>
|
||||
|
||||
<Text size="sm" color="text-gray-500" align="center">
|
||||
{accept ? `Accepted formats: ${accept}` : 'All file types accepted'}
|
||||
{maxSize && ` (Max ${Math.round(maxSize / 1024 / 1024)}MB)`}
|
||||
</Text>
|
||||
|
||||
{error && (
|
||||
<Box display="flex" alignItems="center" gap={2} mt={4} color="text-amber-500">
|
||||
<Icon icon={AlertCircle} size={4} />
|
||||
<Text size="sm" weight="medium">{error}</Text>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{selectedFiles.length > 0 && (
|
||||
<Box mt={4} display="flex" flexDirection="col" gap={2}>
|
||||
{selectedFiles.map((file, index) => (
|
||||
<Box
|
||||
key={`${file.name}-${index}`}
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
justifyContent="between"
|
||||
p={3}
|
||||
bg="bg-charcoal-outline/20"
|
||||
rounded="lg"
|
||||
border
|
||||
borderColor="border-charcoal-outline/30"
|
||||
>
|
||||
<Box display="flex" alignItems="center" gap={3}>
|
||||
<Icon icon={File} size={5} color="text-gray-400" />
|
||||
<Box>
|
||||
<Text block size="sm" weight="medium" truncate maxWidth="200px">
|
||||
{file.name}
|
||||
</Text>
|
||||
<Text block size="xs" color="text-gray-500">
|
||||
{Math.round(file.size / 1024)} KB
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
removeFile(index);
|
||||
}}
|
||||
p={1}
|
||||
h="auto"
|
||||
>
|
||||
<Icon icon={X} size={4} />
|
||||
</Button>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user