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

202 lines
6.1 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
}
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)
const { profileImage, saveProfileImage, removeProfileImage, isLoading } = useDatabase
? useProfile()
: { profileImage: null, saveProfileImage: async () => "", removeProfileImage: () => {}, isLoading: false }
useEffect(() => {
if (useDatabase && profileImage && !selectedImage) {
setSelectedImage(profileImage)
}
}, [profileImage, selectedImage, useDatabase])
const handleImageChange = async (event: ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0]
if (file) {
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
}
const maxSize = 2 * 1024 * 1024
if (file.size > maxSize) {
alert('O arquivo deve ter no máximo 2MB')
return
}
setIsUploading(true)
try {
let imageUrl: string
if (useDatabase) {
imageUrl = await saveProfileImage(file)
} else {
imageUrl = URL.createObjectURL(file)
}
setSelectedImage(imageUrl)
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)
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>
<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>
{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
ref={fileInputRef}
type="file"
accept="image/jpeg,image/png,image/gif,image/webp"
onChange={handleImageChange}
className="hidden"
disabled={isDisabled}
/>
</div>
</CardContent>
</Card>
)
}