mirror of
https://github.com/Since-3/Kuma.git
synced 2026-10-12 21:41:17 +00:00
Merge pull request #42 from Since-3/feature/storage-bucket
added supabase storage bucket
This commit is contained in:
commit
7f222baf5f
2 changed files with 188 additions and 0 deletions
106
src/hooks/useImageUpload.ts
Normal file
106
src/hooks/useImageUpload.ts
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import {
|
||||
uploadFile,
|
||||
deleteFile,
|
||||
generateFilePath,
|
||||
type StorageBucket,
|
||||
} from "@/src/lib/supabase/storage";
|
||||
|
||||
interface UseImageUploadOptions {
|
||||
bucket: StorageBucket;
|
||||
maxSizeMB?: number;
|
||||
allowedTypes?: string[];
|
||||
folder?: string;
|
||||
}
|
||||
|
||||
interface UseImageUploadReturn {
|
||||
upload: (file: File, userId: string) => Promise<string>;
|
||||
remove: (path: string) => Promise<void>;
|
||||
isUploading: boolean;
|
||||
error: string | null;
|
||||
progress: number;
|
||||
}
|
||||
|
||||
const DEFAULT_MAX_SIZE_MB = 5;
|
||||
const DEFAULT_ALLOWED_TYPES = ["image/jpeg", "image/png", "image/webp", "image/gif"];
|
||||
|
||||
export function useImageUpload({
|
||||
bucket,
|
||||
maxSizeMB = DEFAULT_MAX_SIZE_MB,
|
||||
allowedTypes = DEFAULT_ALLOWED_TYPES,
|
||||
folder,
|
||||
}: UseImageUploadOptions): UseImageUploadReturn {
|
||||
const [isUploading, setIsUploading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [progress, setProgress] = useState(0);
|
||||
|
||||
const validateFile = (file: File): string | null => {
|
||||
if (!allowedTypes.includes(file.type)) {
|
||||
return `Ungültiger Dateityp. Erlaubt: ${allowedTypes.join(", ")}`;
|
||||
}
|
||||
|
||||
const maxSizeBytes = maxSizeMB * 1024 * 1024;
|
||||
if (file.size > maxSizeBytes) {
|
||||
return `Datei zu groß. Maximum: ${maxSizeMB}MB`;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const upload = async (file: File, userId: string): Promise<string> => {
|
||||
setError(null);
|
||||
setProgress(0);
|
||||
|
||||
const validationError = validateFile(file);
|
||||
if (validationError) {
|
||||
setError(validationError);
|
||||
throw new Error(validationError);
|
||||
}
|
||||
|
||||
setIsUploading(true);
|
||||
setProgress(10);
|
||||
|
||||
try {
|
||||
const path = generateFilePath(userId, file.name, folder);
|
||||
setProgress(30);
|
||||
|
||||
const result = await uploadFile({
|
||||
bucket,
|
||||
path,
|
||||
file,
|
||||
upsert: true,
|
||||
});
|
||||
|
||||
setProgress(100);
|
||||
return result.publicUrl;
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : "Upload fehlgeschlagen";
|
||||
setError(message);
|
||||
throw err;
|
||||
} finally {
|
||||
setIsUploading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const remove = async (path: string): Promise<void> => {
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
await deleteFile(bucket, path);
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : "Löschen fehlgeschlagen";
|
||||
setError(message);
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
upload,
|
||||
remove,
|
||||
isUploading,
|
||||
error,
|
||||
progress,
|
||||
};
|
||||
}
|
||||
82
src/lib/supabase/storage.ts
Normal file
82
src/lib/supabase/storage.ts
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
import { createClient } from "./client";
|
||||
|
||||
export type StorageBucket = "avatars" | "course-images" | "documents";
|
||||
|
||||
interface UploadOptions {
|
||||
bucket: StorageBucket;
|
||||
path: string;
|
||||
file: File;
|
||||
upsert?: boolean;
|
||||
}
|
||||
|
||||
interface UploadResult {
|
||||
path: string;
|
||||
publicUrl: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Uploads a file to Supabase Storage
|
||||
*/
|
||||
export async function uploadFile({
|
||||
bucket,
|
||||
path,
|
||||
file,
|
||||
upsert = true,
|
||||
}: UploadOptions): Promise<UploadResult> {
|
||||
const supabase = createClient();
|
||||
|
||||
const { data, error } = await supabase.storage.from(bucket).upload(path, file, {
|
||||
upsert,
|
||||
cacheControl: "3600",
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw new Error(`Upload failed: ${error.message}`);
|
||||
}
|
||||
|
||||
const {
|
||||
data: { publicUrl },
|
||||
} = supabase.storage.from(bucket).getPublicUrl(data.path);
|
||||
|
||||
return {
|
||||
path: data.path,
|
||||
publicUrl,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a file from Supabase Storage
|
||||
*/
|
||||
export async function deleteFile(bucket: StorageBucket, path: string): Promise<void> {
|
||||
const supabase = createClient();
|
||||
|
||||
const { error } = await supabase.storage.from(bucket).remove([path]);
|
||||
|
||||
if (error) {
|
||||
throw new Error(`Delete failed: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the public URL for a file
|
||||
*/
|
||||
export function getPublicUrl(bucket: StorageBucket, path: string): string {
|
||||
const supabase = createClient();
|
||||
|
||||
const {
|
||||
data: { publicUrl },
|
||||
} = supabase.storage.from(bucket).getPublicUrl(path);
|
||||
|
||||
return publicUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a unique file path with timestamp
|
||||
*/
|
||||
export function generateFilePath(userId: string, fileName: string, folder?: string): string {
|
||||
const timestamp = Date.now();
|
||||
const extension = fileName.split(".").pop();
|
||||
const basePath = folder ? `${folder}/${userId}` : userId;
|
||||
|
||||
return `${basePath}/${timestamp}.${extension}`;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue