Add components and hooks for event theme management
Some checks failed
Build and Push Docker Images / changes (push) Successful in 5s
Build and Push Docker Images / build-backend (push) Has been skipped
Build and Push Docker Images / build-frontend (push) Failing after 49s

Introduced the `usePresignedUpload` hook for file uploads and multiple components for event theme management, including form handling, asset uploads, and UI enhancements. These additions support creating, editing, and managing event themes effectively.
This commit is contained in:
2025-03-13 08:36:20 +01:00
parent f2f8d85775
commit 4044d85410
7 changed files with 1137 additions and 0 deletions

View File

@@ -0,0 +1,220 @@
// src/components/ui/image-uploader.tsx
"use client";
import React, { useRef, useState } from "react";
import Image from "next/image";
import { usePresignedUpload } from "@/hooks/usePresignedUpload";
import { Button } from "@/components/ui/button";
import { Progress } from "@/components/ui/progress";
import { Loader2, RefreshCw, Upload, X } from "lucide-react";
export interface ImageUploaderProps {
id: string;
label?: string;
imageUrl?: string | null;
purpose?: string;
className?: string;
onChange?: (url: string | null) => void;
aspectRatio?: "square" | "wide" | "tall";
maxWidth?: number;
maxHeight?: number;
required?: boolean;
}
export function ImageUploader({
id,
label,
imageUrl: initialImageUrl,
purpose = "theme",
className = "",
onChange,
aspectRatio = "square",
maxWidth = 800,
maxHeight = 800,
required = false,
}: ImageUploaderProps) {
const [existingImage, setExistingImage] = useState<string | null | undefined>(
initialImageUrl,
);
const fileInputRef = useRef<HTMLInputElement>(null);
const {
file,
preview,
isUploading,
progress,
error,
fileUrl,
selectFile,
uploadFile,
reset,
} = usePresignedUpload({
purpose,
onUploadSuccess: (url) => {
setExistingImage(null);
if (onChange) onChange(url);
},
});
// Set aspect ratio class
const getAspectRatioClass = () => {
switch (aspectRatio) {
case "wide":
return "aspect-video";
case "tall":
return "aspect-[3/4]";
case "square":
default:
return "aspect-square";
}
};
// Handle file selection
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0] || null;
selectFile(file);
// Reset the input value so the same file can be selected again if needed
if (event.target.value) event.target.value = "";
};
// Handle file upload
const handleUpload = async () => {
await uploadFile();
};
// Clear the selection or existing image
const handleClear = () => {
if (existingImage) {
setExistingImage(null);
if (onChange) onChange(null);
} else if (file || preview || fileUrl) {
reset();
if (onChange && fileUrl) onChange(null);
}
};
// Trigger the file input click
const handleSelectClick = () => {
fileInputRef.current?.click();
};
// Determine the current image to display
const currentImage = fileUrl || preview || existingImage;
return (
<div className={`w-full space-y-2 ${className}`}>
{label && (
<div className="flex items-center justify-between">
<label htmlFor={id} className="text-sm font-medium">
{label} {required && <span className="text-red-500">*</span>}
</label>
{currentImage && (
<Button
type="button"
variant="destructive"
size="sm"
onClick={handleClear}
className="h-7 px-2"
>
<X className="h-4 w-4 mr-1" /> Clear
</Button>
)}
</div>
)}
<div
className={`relative border-2 border-dashed border-gray-300 rounded-md ${getAspectRatioClass()} overflow-hidden bg-gray-50 hover:bg-gray-100 transition-colors`}
>
{/* Hidden file input */}
<input
ref={fileInputRef}
type="file"
id={id}
accept="image/*"
className="hidden"
onChange={handleFileChange}
/>
{/* Preview image */}
{currentImage ? (
<div className="w-full h-full relative group">
<Image
src={currentImage}
alt="Selected image"
fill
className="object-cover"
/>
<div className="absolute inset-0 bg-black bg-opacity-0 group-hover:bg-opacity-40 transition-all flex items-center justify-center opacity-0 group-hover:opacity-100">
<Button
type="button"
variant="secondary"
size="sm"
onClick={handleSelectClick}
>
<RefreshCw className="h-4 w-4 mr-1" /> Change
</Button>
</div>
</div>
) : (
// Upload placeholder
<div
className="w-full h-full flex flex-col items-center justify-center p-4 cursor-pointer"
onClick={handleSelectClick}
>
<Upload className="h-8 w-8 text-gray-400 mb-2" />
<p className="text-sm text-center text-gray-500">
Click to select an image
</p>
<p className="text-xs text-center text-gray-400 mt-1">
Recommended: {maxWidth}x{maxHeight} pixels
</p>
</div>
)}
{/* Upload progress overlay */}
{isUploading && (
<div className="absolute inset-0 bg-black bg-opacity-50 flex flex-col items-center justify-center">
<Loader2 className="h-8 w-8 text-white animate-spin mb-2" />
<p className="text-white text-sm mb-2">Uploading...</p>
<div className="w-3/4">
<Progress value={progress} className="h-2" />
</div>
</div>
)}
</div>
{error && <p className="text-red-500 text-sm">{error}</p>}
{file && !fileUrl && !isUploading && (
<div className="flex items-center space-x-2">
<Button
type="button"
onClick={handleUpload}
className="w-full"
disabled={isUploading}
>
{isUploading ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Uploading...
</>
) : (
<>
<Upload className="mr-2 h-4 w-4" />
Upload
</>
)}
</Button>
<Button
type="button"
variant="outline"
onClick={handleClear}
disabled={isUploading}
>
Cancel
</Button>
</div>
)}
</div>
);
}

View File

@@ -0,0 +1,31 @@
"use client"
import * as React from "react"
import * as ProgressPrimitive from "@radix-ui/react-progress"
import { cn } from "@/lib/utils"
function Progress({
className,
value,
...props
}: React.ComponentProps<typeof ProgressPrimitive.Root>) {
return (
<ProgressPrimitive.Root
data-slot="progress"
className={cn(
"bg-primary/20 relative h-2 w-full overflow-hidden rounded-full",
className
)}
{...props}
>
<ProgressPrimitive.Indicator
data-slot="progress-indicator"
className="bg-primary h-full w-full flex-1 transition-all"
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
/>
</ProgressPrimitive.Root>
)
}
export { Progress }