gohorsejobs/frontend/src/components/profile-picture-upload.tsx
2025-12-22 15:30:06 -03:00

234 lines
6.9 KiB
TypeScript

"use client";
import { useState, useRef, ChangeEvent, useEffect } from "react";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card";
import { Camera, Upload, X, Loader2 } from "lucide-react";
import { cn } from "@/lib/utils";
import { useProfile } from "@/hooks/use-profile";
interface ProfilePictureUploadProps {
initialImage?: string;
onImageChange?: (file: File | null, imageUrl: string | null) => void;
fallbackText?: string;
size?: "sm" | "md" | "lg" | "xl";
className?: string;
disabled?: boolean;
useDatabase?: boolean; // Nova prop para usar ou não o banco de dados
}
const sizeClasses = {
sm: "w-16 h-16",
md: "w-24 h-24",
lg: "w-32 h-32",
xl: "w-40 h-40",
};
export function ProfilePictureUpload({
initialImage,
onImageChange,
fallbackText = "U",
size = "lg",
className,
disabled = false,
useDatabase = true,
}: ProfilePictureUploadProps) {
const [selectedImage, setSelectedImage] = useState<string | null>(
initialImage || null
);
const [isHovering, setIsHovering] = useState(false);
const [isUploading, setIsUploading] = useState(false);
const fileInputRef = useRef<HTMLInputElement>(null);
// Usar o hook apenas se useDatabase for true
const { profileImage, saveProfileImage, removeProfileImage, isLoading } =
useDatabase
? useProfile()
: {
profileImage: null,
saveProfileImage: async () => "",
removeProfileImage: () => {},
isLoading: false,
};
// Sincronizar com a imagem do banco de dados
useEffect(() => {
if (useDatabase && profileImage && !selectedImage) {
setSelectedImage(profileImage);
}
}, [profileImage, selectedImage, useDatabase]);
const handleImageChange = async (event: ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (file) {
// Validar tipo de arquivo
const validTypes = ["image/jpeg", "image/png", "image/gif", "image/webp"];
if (!validTypes.includes(file.type)) {
alert(
"Por favor, selecione um arquivo de imagem válido (JPG, PNG, GIF ou WebP)"
);
return;
}
// Validar tamanho do arquivo (máximo 2MB)
const maxSize = 2 * 1024 * 1024; // 2MB em bytes
if (file.size > maxSize) {
alert("O arquivo deve ter no máximo 2MB");
return;
}
setIsUploading(true);
try {
let imageUrl: string;
if (useDatabase) {
// Salvar no banco de dados local
imageUrl = await saveProfileImage(file);
} else {
// Apenas criar URL local
imageUrl = URL.createObjectURL(file);
}
setSelectedImage(imageUrl);
// Chamar callback se fornecido
onImageChange?.(file, imageUrl);
} catch (error) {
console.error("Erro ao processar imagem:", error);
alert("Erro ao salvar a imagem. Tente novamente.");
} finally {
setIsUploading(false);
}
}
};
const handleClick = () => {
if (!disabled && !isUploading) {
fileInputRef.current?.click();
}
};
const handleRemoveImage = async (e: React.MouseEvent) => {
e.stopPropagation();
if (useDatabase) {
removeProfileImage();
}
setSelectedImage(null);
onImageChange?.(null, null);
// Limpar o input
if (fileInputRef.current) {
fileInputRef.current.value = "";
}
};
const isDisabled = disabled || isUploading || (useDatabase && isLoading);
return (
<Card className={cn("w-fit", className)}>
<CardContent className="p-6">
<div className="flex flex-col items-center space-y-4">
<div className="text-sm font-medium text-center">Profile Photo</div>
<div
className={cn(
"relative cursor-pointer transition-all duration-200",
sizeClasses[size],
isDisabled && "cursor-not-allowed opacity-50"
)}
onMouseEnter={() => !isDisabled && setIsHovering(true)}
onMouseLeave={() => setIsHovering(false)}
onClick={handleClick}
>
<Avatar
className={cn(
"w-full h-full border-2 border-dashed border-muted-foreground/20",
{
"border-primary/50": isHovering && !isDisabled,
}
)}
>
<AvatarImage
src={selectedImage || undefined}
alt="Foto de perfil"
/>
<AvatarFallback className="text-lg font-semibold bg-muted">
{isLoading || isUploading ? (
<Loader2 className="w-6 h-6 animate-spin" />
) : selectedImage ? null : (
fallbackText
)}
</AvatarFallback>
</Avatar>
{/* Overlay com ícone */}
<div
className={cn(
"absolute inset-0 flex items-center justify-center bg-black/50 rounded-full opacity-0 transition-opacity duration-200",
isHovering && !isDisabled && "opacity-100"
)}
>
{isUploading ? (
<Loader2 className="w-6 h-6 text-white animate-spin" />
) : (
<Camera className="w-6 h-6 text-white" />
)}
</div>
{/* Botão de remover imagem */}
{selectedImage && !isDisabled && (
<Button
size="sm"
variant="destructive"
className="absolute -top-2 -right-2 w-6 h-6 rounded-full p-0"
onClick={handleRemoveImage}
>
<X className="w-3 h-3" />
</Button>
)}
</div>
<div className="text-center">
<Button
variant="outline"
size="sm"
onClick={handleClick}
disabled={isDisabled}
className="mb-2"
>
{isUploading ? (
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
) : (
<Upload className="w-4 h-4 mr-2" />
)}
{selectedImage ? "Alterar foto" : "Adicionar foto"}
</Button>
<p className="text-xs text-muted-foreground">
JPG, PNG ou GIF. Máximo 2MB.
</p>
{useDatabase && (
<p className="text-xs text-green-600 mt-1">
💾 Salvando automaticamente
</p>
)}
</div>
{/* Input file oculto */}
<input
ref={fileInputRef}
type="file"
accept="image/jpeg,image/png,image/gif,image/webp"
onChange={handleImageChange}
className="hidden"
disabled={isDisabled}
/>
</div>
</CardContent>
</Card>
);
}