"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"; import { uploadToPresignedPost } from "@/lib/upload-client"; 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, fields, key } = await presignRes.json() as { url: string; fields: Record; key: string }; const uploaded = await uploadToPresignedPost(url, fields, file); if (!uploaded) { toast.error(t("avatarUploadFailed")); return; } // Send back the storage key we were issued, not a client-derived URL — // the server validates the key belongs to us and builds the public URL // itself, so a client can't set avatarUrl to an arbitrary/unowned target. const saveRes = await fetch("/api/v1/users/me", { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ avatarKey: key }), }); if (!saveRes.ok) { toast.error(t("avatarUploadFailed")); return; } const saved = await saveRes.json() as { avatarUrl?: string }; onChange(saved.avatarUrl ?? null, 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 && ( )}
); }