Initial commit from template

This commit is contained in:
Lumina
2025-12-23 04:19:57 +01:00
commit b3d8fe8dfe
76 changed files with 10491 additions and 0 deletions

View File

@@ -0,0 +1,142 @@
---
name: supabase-storage
description: Supabase Storage fuer Datei-Upload und Download. Nutze diesen Skill fuer Bucket-Erstellung, File Upload, Download, Signed URLs und Storage Policies. Aktiviert bei Begriffen wie "Upload", "Download", "Datei", "File", "Bild hochladen", "Storage", "Bucket", "S3", "Bilder", "Avatar", "Dokumente".
---
# Supabase Storage Skill
Dieser Skill hilft bei allen Storage-Operationen mit Supabase.
## Bucket erstellen
```typescript
// Server-side mit Admin Client
const { data, error } = await supabaseAdmin.storage.createBucket('avatars', {
public: true,
fileSizeLimit: 1024 * 1024 * 2, // 2MB
allowedMimeTypes: ['image/png', 'image/jpeg', 'image/webp']
});
```
Oder via SQL:
```sql
INSERT INTO storage.buckets (id, name, public, file_size_limit, allowed_mime_types)
VALUES (
'avatars',
'avatars',
true,
2097152,
ARRAY['image/png', 'image/jpeg', 'image/webp']
);
```
## File Upload
```typescript
async function uploadFile(file: File, bucket: string, path: string) {
const { data, error } = await supabase.storage
.from(bucket)
.upload(path, file, {
cacheControl: '3600',
upsert: false // true = ueberschreiben erlaubt
});
if (error) throw error;
// Public URL holen
const { data: urlData } = supabase.storage
.from(bucket)
.getPublicUrl(path);
return urlData.publicUrl;
}
```
## File Download
```typescript
// Als Blob
const { data, error } = await supabase.storage
.from('documents')
.download('path/to/file.pdf');
// Public URL (oeffentliche Buckets)
const { data } = supabase.storage
.from('avatars')
.getPublicUrl('user123/avatar.jpg');
// Signed URL (private Buckets, zeitlich begrenzt)
const { data, error } = await supabase.storage
.from('private-docs')
.createSignedUrl('secret.pdf', 3600); // 1 Stunde
```
## File Management
```typescript
// Liste Dateien
const { data, error } = await supabase.storage
.from('bucket')
.list('folder/', {
limit: 100,
sortBy: { column: 'created_at', order: 'desc' }
});
// Loeschen
const { error } = await supabase.storage
.from('bucket')
.remove(['path/file1.jpg', 'path/file2.jpg']);
// Verschieben
const { error } = await supabase.storage
.from('bucket')
.move('old/path.jpg', 'new/path.jpg');
```
## Storage Policies (RLS)
```sql
-- User koennen eigene Dateien hochladen
CREATE POLICY "Users upload own files"
ON storage.objects FOR INSERT
TO authenticated
WITH CHECK (
bucket_id = 'user-files' AND
(storage.foldername(name))[1] = auth.uid()::text
);
-- User koennen eigene Dateien lesen
CREATE POLICY "Users read own files"
ON storage.objects FOR SELECT
TO authenticated
USING (
bucket_id = 'user-files' AND
(storage.foldername(name))[1] = auth.uid()::text
);
-- User koennen eigene Dateien loeschen
CREATE POLICY "Users delete own files"
ON storage.objects FOR DELETE
TO authenticated
USING (
bucket_id = 'user-files' AND
(storage.foldername(name))[1] = auth.uid()::text
);
-- Oeffentlicher Lesezugriff
CREATE POLICY "Public read"
ON storage.objects FOR SELECT
USING (bucket_id = 'public-assets');
```
## React Upload Component
Siehe `templates/FileUpload.tsx` fuer eine fertige Component.
## Best Practices
1. Ordnerstruktur: `{user_id}/{type}/{filename}`
2. File Size Limits setzen
3. MIME Types einschraenken
4. Signed URLs fuer private Dateien
5. Unique Filenames (z.B. mit Timestamp)

View File

@@ -0,0 +1,126 @@
'use client';
import { useState, useCallback } from 'react';
import { supabase } from '@/lib/supabase';
interface FileUploadProps {
bucket: string;
folder?: string;
accept?: string;
maxSize?: number; // in bytes
onUpload: (url: string, path: string) => void;
onError?: (error: Error) => void;
}
export function FileUpload({
bucket,
folder = 'uploads',
accept = 'image/*',
maxSize = 5 * 1024 * 1024, // 5MB default
onUpload,
onError,
}: FileUploadProps) {
const [uploading, setUploading] = useState(false);
const [progress, setProgress] = useState(0);
const [dragOver, setDragOver] = useState(false);
const uploadFile = useCallback(
async (file: File) => {
// Validierung
if (file.size > maxSize) {
onError?.(new Error(`Datei zu gross. Maximum: ${maxSize / 1024 / 1024}MB`));
return;
}
setUploading(true);
setProgress(0);
try {
// Unique filename generieren
const fileExt = file.name.split('.').pop();
const fileName = `${Date.now()}-${Math.random().toString(36).substring(2)}.${fileExt}`;
const filePath = `${folder}/${fileName}`;
// Upload
const { error: uploadError } = await supabase.storage
.from(bucket)
.upload(filePath, file, {
cacheControl: '3600',
upsert: false,
});
if (uploadError) throw uploadError;
// URL holen
const { data } = supabase.storage.from(bucket).getPublicUrl(filePath);
onUpload(data.publicUrl, filePath);
setProgress(100);
} catch (error) {
console.error('Upload error:', error);
onError?.(error instanceof Error ? error : new Error('Upload fehlgeschlagen'));
} finally {
setUploading(false);
}
},
[bucket, folder, maxSize, onUpload, onError]
);
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (file) uploadFile(file);
};
const handleDrop = (e: React.DragEvent) => {
e.preventDefault();
setDragOver(false);
const file = e.dataTransfer.files?.[0];
if (file) uploadFile(file);
};
return (
<div
className={`
relative border-2 border-dashed rounded-lg p-6 text-center
transition-colors cursor-pointer
${dragOver ? 'border-primary bg-primary/5' : 'border-muted-foreground/25'}
${uploading ? 'pointer-events-none opacity-50' : 'hover:border-primary'}
`}
onDragOver={(e) => {
e.preventDefault();
setDragOver(true);
}}
onDragLeave={() => setDragOver(false)}
onDrop={handleDrop}
>
<input
type="file"
accept={accept}
onChange={handleChange}
disabled={uploading}
className="absolute inset-0 w-full h-full opacity-0 cursor-pointer"
/>
{uploading ? (
<div className="space-y-2">
<div className="w-full bg-muted rounded-full h-2">
<div
className="bg-primary h-2 rounded-full transition-all"
style={{ width: `${progress}%` }}
/>
</div>
<p className="text-sm text-muted-foreground">Uploading...</p>
</div>
) : (
<div className="space-y-2">
<p className="text-sm font-medium">
Datei hierher ziehen oder klicken zum Auswaehlen
</p>
<p className="text-xs text-muted-foreground">
Max. {maxSize / 1024 / 1024}MB
</p>
</div>
)}
</div>
);
}