Files
myeasycms-v2/apps/dev-tool/app/components/components/file-uploader-story.tsx
Giancarlo Buomprisco ad427365c9 Storybook (#328)
* feat(docs): add interactive examples and API references for Button, Card, and LoadingFallback components
- Updated dependencies
- Set `retries` to a fixed value of 3 for consistent test retries across environments.
- Increased `timeout` from 60 seconds to 120 seconds to allow more time for tests to complete.
- Reduced `expect` timeout from 10 seconds to 5 seconds for quicker feedback on assertions.
2025-08-22 07:35:44 +08:00

599 lines
21 KiB
TypeScript

'use client';
import { useState } from 'react';
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@kit/ui/card';
import { FileUploader } from '@kit/ui/file-uploader';
import { Label } from '@kit/ui/label';
import { Separator } from '@kit/ui/separator';
import { toast } from '@kit/ui/sonner';
import { Switch } from '@kit/ui/switch';
import { generatePropsString, useStoryControls } from '../lib/story-utils';
import { ComponentStoryLayout } from './story-layout';
import { SimpleStorySelect } from './story-select';
// Mock Supabase client for the story
const createMockSupabaseClient = () => ({
storage: {
from: (bucket: string) => ({
upload: async (path: string, file: File, options: any) => {
// Simulate upload delay
await new Promise((resolve) =>
setTimeout(resolve, 1000 + Math.random() * 2000),
);
// Simulate occasional upload errors
if (Math.random() < 0.1) {
return {
error: {
message: 'Upload failed: Network error',
},
};
}
return {
error: null,
data: {
path: `${bucket}/${path}`,
},
};
},
}),
},
});
interface FileUploaderControls {
maxFiles: number;
maxFileSize: number; // in MB for easier control
allowedMimeTypes: 'images' | 'documents' | 'all';
showSuccessToast: boolean;
}
const maxFilesOptions = [
{ value: '1', label: '1 file', description: 'Single file upload' },
{ value: '3', label: '3 files', description: 'Small batch' },
{ value: '5', label: '5 files', description: 'Medium batch' },
{ value: '10', label: '10 files', description: 'Large batch' },
];
const maxFileSizeOptions = [
{ value: '1', label: '1 MB', description: 'Small files' },
{ value: '5', label: '5 MB', description: 'Medium files' },
{ value: '10', label: '10 MB', description: 'Large files' },
{ value: '50', label: '50 MB', description: 'Very large files' },
];
const mimeTypeOptions = [
{ value: 'images', label: 'Images only', description: 'image/* types' },
{
value: 'documents',
label: 'Documents',
description: 'pdf, doc, txt files',
},
{ value: 'all', label: 'All types', description: 'No restrictions' },
];
const getMimeTypes = (type: string): string[] => {
switch (type) {
case 'images':
return ['image/*'];
case 'documents':
return [
'application/pdf',
'application/msword',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'text/plain',
];
case 'all':
default:
return [];
}
};
export function FileUploaderStory() {
const { controls, updateControl } = useStoryControls<FileUploaderControls>({
maxFiles: 3,
maxFileSize: 5, // MB
allowedMimeTypes: 'images',
showSuccessToast: true,
});
const [uploadedFiles, setUploadedFiles] = useState<string[]>([]);
const [mockClient] = useState(() => createMockSupabaseClient());
const handleUploadSuccess = (files: string[]) => {
setUploadedFiles((prev) => [...prev, ...files]);
if (controls.showSuccessToast) {
toast.success(`Successfully uploaded ${files.length} file(s)!`);
}
};
const generateCode = () => {
const allowedMimeTypes = getMimeTypes(controls.allowedMimeTypes);
const maxFileSizeBytes = controls.maxFileSize * 1024 * 1024;
const propsString = generatePropsString(
{
maxFiles: controls.maxFiles,
bucketName: '"uploads"',
path: '"user-files"',
allowedMimeTypes: JSON.stringify(allowedMimeTypes),
maxFileSize: maxFileSizeBytes,
client: 'supabaseClient',
onUploadSuccess: 'handleUploadSuccess',
},
{
maxFiles: 1,
bucketName: '"uploads"',
path: undefined,
allowedMimeTypes: '[]',
maxFileSize: Number.POSITIVE_INFINITY,
client: undefined,
onUploadSuccess: undefined,
},
);
// Format props for better readability
const formattedProps = propsString
.trim()
.split(' ')
.map((prop) => ` ${prop}`)
.join('\n');
return `import { FileUploader } from '@kit/ui/file-uploader';
import { useSupabase } from '@kit/supabase/hooks/use-supabase';
const supabase = useSupabase();
const handleUploadSuccess = (files: string[]) => {
console.log('Uploaded files:', files);
};
<FileUploader
${formattedProps}
/>`;
};
const renderPreview = () => (
<div className="space-y-4">
<FileUploader
maxFiles={controls.maxFiles}
bucketName="demo-bucket"
path="user-files"
allowedMimeTypes={getMimeTypes(controls.allowedMimeTypes)}
maxFileSize={controls.maxFileSize * 1024 * 1024} // Convert MB to bytes
client={mockClient as any}
onUploadSuccess={handleUploadSuccess}
/>
{uploadedFiles.length > 0 && (
<div className="bg-muted/20 mt-6 rounded-lg border p-4">
<h4 className="mb-2 font-semibold">Successfully Uploaded Files:</h4>
<ul className="space-y-1">
{uploadedFiles.map((file, index) => (
<li
key={index}
className="text-muted-foreground flex items-center text-sm"
>
<span className="mr-2 inline-block h-2 w-2 rounded-full bg-green-500"></span>
{file}
</li>
))}
</ul>
</div>
)}
</div>
);
const renderControls = () => (
<>
<div className="space-y-2">
<Label htmlFor="maxFiles">Max Files</Label>
<SimpleStorySelect
value={controls.maxFiles.toString()}
onValueChange={(value) => updateControl('maxFiles', parseInt(value))}
options={maxFilesOptions}
/>
</div>
<div className="space-y-2">
<Label htmlFor="maxFileSize">Max File Size</Label>
<SimpleStorySelect
value={controls.maxFileSize.toString()}
onValueChange={(value) =>
updateControl('maxFileSize', parseInt(value))
}
options={maxFileSizeOptions}
/>
</div>
<div className="space-y-2">
<Label htmlFor="allowedMimeTypes">Allowed File Types</Label>
<SimpleStorySelect
value={controls.allowedMimeTypes}
onValueChange={(value) =>
updateControl('allowedMimeTypes', value as any)
}
options={mimeTypeOptions}
/>
</div>
<Separator />
<div className="flex items-center justify-between">
<Label htmlFor="showSuccessToast">Show Success Toast</Label>
<Switch
id="showSuccessToast"
checked={controls.showSuccessToast}
onCheckedChange={(checked) =>
updateControl('showSuccessToast', checked)
}
/>
</div>
</>
);
const renderExamples = () => (
<div className="space-y-6">
<Card>
<CardHeader>
<CardTitle>Image Upload</CardTitle>
<CardDescription>
Configured for image files only with preview
</CardDescription>
</CardHeader>
<CardContent>
<FileUploader
maxFiles={1}
bucketName="images"
allowedMimeTypes={['image/*']}
maxFileSize={5 * 1024 * 1024} // 5MB
client={mockClient as any}
onUploadSuccess={(files) => toast.success('Image uploaded!')}
/>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Document Upload</CardTitle>
<CardDescription>
Multiple document types with larger file size limit
</CardDescription>
</CardHeader>
<CardContent>
<FileUploader
maxFiles={5}
bucketName="documents"
path="user-docs"
allowedMimeTypes={[
'application/pdf',
'application/msword',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'text/plain',
]}
maxFileSize={10 * 1024 * 1024} // 10MB
client={mockClient as any}
onUploadSuccess={(files) =>
toast.success(`${files.length} documents uploaded!`)
}
/>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Batch Upload</CardTitle>
<CardDescription>
Multiple files with no type restrictions
</CardDescription>
</CardHeader>
<CardContent>
<FileUploader
maxFiles={10}
bucketName="general"
allowedMimeTypes={[]} // No restrictions
maxFileSize={50 * 1024 * 1024} // 50MB
client={mockClient as any}
onUploadSuccess={(files) =>
toast.success(`Batch upload complete: ${files.length} files`)
}
/>
</CardContent>
</Card>
</div>
);
const renderApiReference = () => (
<Card>
<CardHeader>
<CardTitle>FileUploader Component</CardTitle>
<CardDescription>
Complete API reference for FileUploader component
</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-6">
<div>
<h4 className="mb-3 text-lg font-semibold">FileUploader</h4>
<p className="text-muted-foreground mb-3 text-sm">
A drag-and-drop file uploader with preview, progress tracking, and
Supabase integration.
</p>
<div className="overflow-x-auto">
<table className="border-border w-full border-collapse border">
<thead>
<tr className="border-b">
<th className="p-3 text-left font-medium">Prop</th>
<th className="p-3 text-left font-medium">Type</th>
<th className="p-3 text-left font-medium">Default</th>
<th className="p-3 text-left font-medium">Description</th>
</tr>
</thead>
<tbody>
<tr className="border-b">
<td className="p-3 font-mono text-sm">maxFiles</td>
<td className="p-3 font-mono text-sm">number</td>
<td className="p-3 font-mono text-sm">1</td>
<td className="p-3">Maximum number of files allowed</td>
</tr>
<tr className="border-b">
<td className="p-3 font-mono text-sm">bucketName</td>
<td className="p-3 font-mono text-sm">string</td>
<td className="p-3 font-mono text-sm">-</td>
<td className="p-3">Supabase storage bucket name</td>
</tr>
<tr className="border-b">
<td className="p-3 font-mono text-sm">path</td>
<td className="p-3 font-mono text-sm">string</td>
<td className="p-3 font-mono text-sm">undefined</td>
<td className="p-3">
Optional path prefix for uploaded files
</td>
</tr>
<tr className="border-b">
<td className="p-3 font-mono text-sm">allowedMimeTypes</td>
<td className="p-3 font-mono text-sm">string[]</td>
<td className="p-3 font-mono text-sm">[]</td>
<td className="p-3">
Array of allowed MIME types (empty = all)
</td>
</tr>
<tr className="border-b">
<td className="p-3 font-mono text-sm">maxFileSize</td>
<td className="p-3 font-mono text-sm">number</td>
<td className="p-3 font-mono text-sm">Infinity</td>
<td className="p-3">Maximum file size in bytes</td>
</tr>
<tr className="border-b">
<td className="p-3 font-mono text-sm">client</td>
<td className="p-3 font-mono text-sm">SupabaseClient</td>
<td className="p-3 font-mono text-sm">-</td>
<td className="p-3">Supabase client instance</td>
</tr>
<tr className="border-b">
<td className="p-3 font-mono text-sm">onUploadSuccess</td>
<td className="p-3 font-mono text-sm">
{'(files: string[]) => void'}
</td>
<td className="p-3 font-mono text-sm">undefined</td>
<td className="p-3">Callback when upload succeeds</td>
</tr>
<tr className="border-b">
<td className="p-3 font-mono text-sm">cacheControl</td>
<td className="p-3 font-mono text-sm">number</td>
<td className="p-3 font-mono text-sm">3600</td>
<td className="p-3">Cache control in seconds</td>
</tr>
<tr>
<td className="p-3 font-mono text-sm">className</td>
<td className="p-3 font-mono text-sm">string</td>
<td className="p-3 font-mono text-sm">-</td>
<td className="p-3">Additional CSS classes</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</CardContent>
</Card>
);
const renderUsageGuidelines = () => (
<div className="grid gap-6">
<Card>
<CardHeader>
<CardTitle>When to Use FileUploader</CardTitle>
<CardDescription>
Best practices for file upload interfaces
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<h4 className="text-sm font-semibold text-green-700">
Use FileUploader For
</h4>
<ul className="ml-4 space-y-1 text-sm">
<li> Profile picture and avatar uploads</li>
<li> Document attachment uploads</li>
<li> Image gallery uploads</li>
<li> File import features</li>
<li> Media content uploads</li>
</ul>
</div>
<div className="space-y-2">
<h4 className="text-sm font-semibold text-red-700">
Consider Alternatives For
</h4>
<ul className="ml-4 space-y-1 text-sm">
<li> Very large files (use chunked upload)</li>
<li> Real-time collaborative editing</li>
<li> Direct database uploads (use proper storage)</li>
<li> Temporary file sharing (use different patterns)</li>
</ul>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Configuration Guidelines</CardTitle>
<CardDescription>
How to configure FileUploader effectively
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<h4 className="text-sm font-semibold">File Size Limits</h4>
<ul className="ml-4 space-y-1 text-sm">
<li>
<strong>Images:</strong> 1-5MB for web, 10-20MB for high
quality
</li>
<li>
<strong>Documents:</strong> 10-50MB depending on content
</li>
<li>
<strong>Videos:</strong> 100MB+ (consider chunked upload)
</li>
<li>
<strong>Audio:</strong> 10-50MB for high quality
</li>
</ul>
</div>
<div className="space-y-2">
<h4 className="text-sm font-semibold">MIME Type Patterns</h4>
<ul className="ml-4 space-y-1 text-sm">
<li>
Use wildcards: <code>image/*</code>, <code>video/*</code>
</li>
<li>
Specific types: <code>application/pdf</code>
</li>
<li>
Multiple types: <code>['image/jpeg', 'image/png']</code>
</li>
<li> Empty array allows all file types</li>
</ul>
</div>
<div className="space-y-2">
<h4 className="text-sm font-semibold">Bucket Organization</h4>
<ul className="ml-4 space-y-1 text-sm">
<li> Use separate buckets for different content types</li>
<li>
Organize with path prefixes: <code>user-id/category</code>
</li>
<li> Consider public vs private bucket access</li>
<li> Set up proper RLS policies for security</li>
</ul>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>User Experience Best Practices</CardTitle>
<CardDescription>
Creating intuitive upload experiences
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<h4 className="text-sm font-semibold">Visual Feedback</h4>
<ul className="ml-4 space-y-1 text-sm">
<li> Clear drag-and-drop zones with visual cues</li>
<li> Progress indicators during upload</li>
<li> Success/error states with appropriate messaging</li>
<li> File previews when possible (especially images)</li>
</ul>
</div>
<div className="space-y-2">
<h4 className="text-sm font-semibold">Error Handling</h4>
<ul className="ml-4 space-y-1 text-sm">
<li> Clear error messages for size/type violations</li>
<li> Retry mechanisms for network failures</li>
<li> Partial upload recovery when possible</li>
<li> Graceful degradation for unsupported browsers</li>
</ul>
</div>
<div className="space-y-2">
<h4 className="text-sm font-semibold">Accessibility</h4>
<ul className="ml-4 space-y-1 text-sm">
<li> Keyboard navigation support</li>
<li> Screen reader compatible labels</li>
<li> Focus management during upload process</li>
<li> Alternative input methods (click to select)</li>
</ul>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Security Considerations</CardTitle>
<CardDescription>Keeping file uploads secure</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<h4 className="text-sm font-semibold">File Validation</h4>
<ul className="ml-4 space-y-1 text-sm">
<li> Always validate file types on both client and server</li>
<li> Check file contents, not just extensions</li>
<li> Scan for malware when possible</li>
<li> Limit file sizes to prevent DoS attacks</li>
</ul>
</div>
<div className="space-y-2">
<h4 className="text-sm font-semibold">Storage Security</h4>
<ul className="ml-4 space-y-1 text-sm">
<li> Use Row Level Security (RLS) policies</li>
<li> Separate public and private content</li>
<li> Generate unique file names to prevent conflicts</li>
<li> Set up proper bucket permissions</li>
</ul>
</div>
<div className="space-y-2">
<h4 className="text-sm font-semibold">User Privacy</h4>
<ul className="ml-4 space-y-1 text-sm">
<li> Don't store sensitive files in public buckets</li>
<li> Implement file deletion capabilities</li>
<li> Consider data retention policies</li>
<li> Respect user privacy preferences</li>
</ul>
</div>
</CardContent>
</Card>
</div>
);
return (
<div className="space-y-4">
<div className="rounded-lg border border-yellow-200 bg-yellow-50 p-3">
<p className="text-sm text-yellow-800">
<strong>Note:</strong> This story uses a mock Supabase client for
demonstration. In your application, use a real Supabase client with
proper authentication and storage configuration.
</p>
</div>
<ComponentStoryLayout
preview={renderPreview()}
controls={renderControls()}
generatedCode={generateCode()}
examples={renderExamples()}
apiReference={renderApiReference()}
usageGuidelines={renderUsageGuidelines()}
/>
</div>
);
}