"use client"; import { useState, useRef } from "react"; import { useTranslations } from "next-intl"; import { toast } from "sonner"; import { Camera, Loader2, X } from "lucide-react"; import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar"; import { Button } from "@/components/ui/button"; export function AvatarUploader({ name, image, hasCustomAvatar, onChange, }: { name: string; image: string | null; hasCustomAvatar: boolean; onChange: (image: string | null, hasCustomAvatar: boolean) => void; }) { const t = useTranslations("settingsForm"); const [uploading, setUploading] = useState(false); const [preview, setPreview] = useState(null); const inputRef = useRef(null); async function handleFile(file: File) { setUploading(true); setPreview(URL.createObjectURL(file)); try { const presignRes = await fetch("/api/v1/upload/avatar-presign", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ contentType: file.type, fileSize: file.size }), }); if (!presignRes.ok) { const err = await presignRes.json() as { error?: string }; toast.error(err.error ?? t("avatarUploadFailed")); return; } const { url } = await presignRes.json() as { url: string; key: string }; await fetch(url, { method: "PUT", body: file, headers: { "Content-Type": file.type } }); // The presigned PUT URL is already publicUrl/bucket/key with query-string // signing params appended — strip those to get the plain public GET URL. const saveRes = await fetch("/api/v1/users/me", { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ avatarUrl: url.split("?")[0] }), }); if (!saveRes.ok) { toast.error(t("avatarUploadFailed")); return; } const saved = await saveRes.json() as { avatarUrl?: string }; onChange(saved.avatarUrl ?? url.split("?")[0]!, true); toast.success(t("avatarUploadSuccess")); } finally { setUploading(false); } } async function handleRemove() { setUploading(true); try { const res = await fetch("/api/v1/users/me", { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ avatarUrl: null }), }); if (!res.ok) { toast.error(t("avatarUploadFailed")); return; } const saved = await res.json() as { avatarUrl?: string }; setPreview(null); onChange(saved.avatarUrl ?? null, false); toast.success(t("avatarRemoved")); } finally { setUploading(false); } } return (
{name.slice(0, 2).toUpperCase()} {uploading && (
)}
{ const file = e.target.files?.[0]; if (file) void handleFile(file); e.target.value = ""; }} /> {hasCustomAvatar && ( )}
); }