Files
chinese-family-tree-2/components/ui/image-upload.tsx
T
freedakgmail 33cf0c50d1 0.0.0.2
2025-11-22 20:08:07 +08:00

131 lines
3.4 KiB
TypeScript

"use client"
import { useState, useRef, useEffect } from "react"
import { Button } from "@/components/ui/button"
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import { Upload, X, Loader2 } from "lucide-react"
import { db } from "@/lib/db"
import { v4 as uuidv4 } from "uuid"
interface ImageUploadProps {
value?: string // The image ID
onChange: (imageId: string | undefined) => void
className?: string
}
export function ImageUpload({ value, onChange, className }: ImageUploadProps) {
const [previewUrl, setPreviewUrl] = useState<string | undefined>(undefined)
const [isLoading, setIsLoading] = useState(false)
const fileInputRef = useRef<HTMLInputElement>(null)
// Load preview if value (imageId) exists
useEffect(() => {
let objectUrl: string | undefined
const loadPreview = async () => {
if (!value) {
setPreviewUrl(undefined)
return
}
try {
const image = await db.images.get(value)
if (image) {
objectUrl = URL.createObjectURL(image.blob)
setPreviewUrl(objectUrl)
}
} catch (error) {
console.error("Failed to load image:", error)
}
}
loadPreview()
return () => {
if (objectUrl) URL.revokeObjectURL(objectUrl)
}
}, [value])
const handleFileSelect = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0]
if (!file) return
setIsLoading(true)
try {
// Basic validation
if (!file.type.startsWith("image/")) {
alert("请选择图片文件")
return
}
if (file.size > 5 * 1024 * 1024) {
alert("图片大小不能超过 5MB")
return
}
// Save to DB
const imageId = uuidv4()
await db.images.add({
id: imageId,
blob: file,
mimeType: file.type,
createdAt: new Date().toISOString(),
})
onChange(imageId)
} catch (error) {
console.error("Failed to save image:", error)
alert("图片上传失败")
} finally {
setIsLoading(false)
// Reset input
if (fileInputRef.current) fileInputRef.current.value = ""
}
}
const handleRemove = () => {
onChange(undefined)
}
return (
<div className={`flex items-center gap-4 ${className}`}>
<Avatar className="h-20 w-20 border-2 border-dashed border-muted-foreground/50">
<AvatarImage src={previewUrl} className="object-cover" />
<AvatarFallback className="bg-transparent">
{isLoading ? <Loader2 className="h-6 w-6 animate-spin" /> : <Upload className="h-6 w-6 text-muted-foreground" />}
</AvatarFallback>
</Avatar>
<div className="flex flex-col gap-2">
<input
type="file"
ref={fileInputRef}
className="hidden"
accept="image/*"
onChange={handleFileSelect}
/>
<Button
type="button"
variant="outline"
size="sm"
onClick={() => fileInputRef.current?.click()}
disabled={isLoading}
>
{value ? "更换头像" : "上传头像"}
</Button>
{value && (
<Button
type="button"
variant="ghost"
size="sm"
className="text-destructive hover:text-destructive"
onClick={handleRemove}
>
<X className="mr-2 h-3 w-3" />
</Button>
)}
</div>
</div>
)
}