131 lines
4.0 KiB
TypeScript
131 lines
4.0 KiB
TypeScript
'use client'
|
|
|
|
import { useState, useRef } from 'react'
|
|
|
|
export interface AvatarUploadProps {
|
|
currentAvatarUrl?: string
|
|
onUpload: (file: File) => void | Promise<void>
|
|
isLoading?: boolean
|
|
size?: 'sm' | 'md' | 'lg'
|
|
}
|
|
|
|
export function AvatarUpload({
|
|
currentAvatarUrl,
|
|
onUpload,
|
|
isLoading = false,
|
|
size = 'lg'
|
|
}: AvatarUploadProps) {
|
|
const [preview, setPreview] = useState<string | null>(null)
|
|
const [isDragging, setIsDragging] = useState(false)
|
|
const fileInputRef = useRef<HTMLInputElement>(null)
|
|
|
|
const sizeClasses = {
|
|
sm: 'w-24 h-24',
|
|
md: 'w-32 h-32',
|
|
lg: 'w-40 h-40'
|
|
}
|
|
|
|
const handleFileSelect = (file: File) => {
|
|
if (file && file.type.startsWith('image/')) {
|
|
const reader = new FileReader()
|
|
reader.onload = (e) => {
|
|
setPreview(e.target?.result as string)
|
|
}
|
|
reader.readAsDataURL(file)
|
|
onUpload(file)
|
|
}
|
|
}
|
|
|
|
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
const file = e.target.files?.[0]
|
|
if (file) handleFileSelect(file)
|
|
}
|
|
|
|
const handleDragOver = (e: React.DragEvent) => {
|
|
e.preventDefault()
|
|
setIsDragging(true)
|
|
}
|
|
|
|
const handleDragLeave = (e: React.DragEvent) => {
|
|
e.preventDefault()
|
|
setIsDragging(false)
|
|
}
|
|
|
|
const handleDrop = (e: React.DragEvent) => {
|
|
e.preventDefault()
|
|
setIsDragging(false)
|
|
const file = e.dataTransfer.files?.[0]
|
|
if (file) handleFileSelect(file)
|
|
}
|
|
|
|
const displayUrl = preview || currentAvatarUrl
|
|
|
|
return (
|
|
<div className="flex flex-col items-center gap-4">
|
|
{/* Avatar Preview */}
|
|
<div
|
|
className={`${sizeClasses[size]} relative rounded-full overflow-hidden bg-zinc-800 border-4 border-zinc-900 shadow-xl`}
|
|
onDragOver={handleDragOver}
|
|
onDragLeave={handleDragLeave}
|
|
onDrop={handleDrop}
|
|
>
|
|
{displayUrl ? (
|
|
<img
|
|
src={displayUrl}
|
|
alt="Avatar"
|
|
className="w-full h-full object-cover"
|
|
/>
|
|
) : (
|
|
<div className="w-full h-full flex items-center justify-center">
|
|
<svg className="w-16 h-16 text-zinc-600" fill="currentColor" viewBox="0 0 20 20">
|
|
<path fillRule="evenodd" d="M10 9a3 3 0 100-6 3 3 0 000 6zm-7 9a7 7 0 1114 0H3z" clipRule="evenodd" />
|
|
</svg>
|
|
</div>
|
|
)}
|
|
|
|
{/* Upload Overlay */}
|
|
<div
|
|
className={`absolute inset-0 bg-black/60 flex items-center justify-center transition-opacity ${
|
|
isDragging || isLoading ? 'opacity-100' : 'opacity-0 hover:opacity-100'
|
|
}`}
|
|
>
|
|
{isLoading ? (
|
|
<div className="w-8 h-8 border-3 border-white border-t-transparent rounded-full animate-spin" />
|
|
) : (
|
|
<svg className="w-10 h-10 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 9a2 2 0 012-2h.93a2 2 0 001.664-.89l.812-1.22A2 2 0 0110.07 4h3.86a2 2 0 011.664.89l.812 1.22A2 2 0 0018.07 7H19a2 2 0 012 2v9a2 2 0 01-2 2H5a2 2 0 01-2-2V9z" />
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 13a3 3 0 11-6 0 3 3 0 016 0z" />
|
|
</svg>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Upload Button */}
|
|
<div className="text-center">
|
|
<input
|
|
ref={fileInputRef}
|
|
type="file"
|
|
accept="image/*"
|
|
onChange={handleInputChange}
|
|
className="hidden"
|
|
disabled={isLoading}
|
|
/>
|
|
<button
|
|
type="button"
|
|
onClick={() => fileInputRef.current?.click()}
|
|
disabled={isLoading}
|
|
className="px-6 py-2 bg-zinc-800 hover:bg-zinc-700 disabled:bg-zinc-800 disabled:text-zinc-500 text-white text-sm font-medium rounded-lg transition"
|
|
>
|
|
{isLoading ? 'Uploading...' : 'Change Avatar'}
|
|
</button>
|
|
<p className="mt-2 text-xs text-zinc-500">
|
|
Click or drag and drop an image
|
|
</p>
|
|
<p className="text-xs text-zinc-600">
|
|
JPG, PNG, GIF up to 5MB
|
|
</p>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|