diff --git a/next.config.ts b/next.config.ts index 145c047..09c5cde 100644 --- a/next.config.ts +++ b/next.config.ts @@ -10,7 +10,11 @@ const nextConfig: NextConfig = { reactCompiler: true, images: { - remotePatterns: [new URL("https://lh3.googleusercontent.com/**")], + remotePatterns: [ + new URL("https://lh3.googleusercontent.com/**"), + { protocol: "https", hostname: "**" }, + { protocol: "http", hostname: "**" }, + ], }, devIndicators: { position: "bottom-right", diff --git a/src/features/payers/components/details/payer-header-card.tsx b/src/features/payers/components/details/payer-header-card.tsx index 383a9aa..acb9ec5 100644 --- a/src/features/payers/components/details/payer-header-card.tsx +++ b/src/features/payers/components/details/payer-header-card.tsx @@ -52,6 +52,7 @@ export function PayerHeaderCard({ const [confirmOpen, setConfirmOpen] = useState(false); const avatarSrc = getAvatarSrc(payer.avatarUrl); + const isDataUrl = avatarSrc.startsWith("data:"); const createdAtLabel = formatDate(payer.createdAt); const isAdmin = payer.role === PAYER_ROLE_ADMIN; @@ -109,6 +110,7 @@ export function PayerHeaderCard({
{`Avatar {`Avatar { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = (e) => { + const img = new window.Image(); + img.onload = () => { + let { width, height } = img; + if (width > height) { + if (width > AVATAR_MAX_SIZE) { + height = Math.round((height * AVATAR_MAX_SIZE) / width); + width = AVATAR_MAX_SIZE; + } + } else { + if (height > AVATAR_MAX_SIZE) { + width = Math.round((width * AVATAR_MAX_SIZE) / height); + height = AVATAR_MAX_SIZE; + } + } + const canvas = document.createElement("canvas"); + canvas.width = width; + canvas.height = height; + const ctx = canvas.getContext("2d"); + if (!ctx) { + reject(new Error("Canvas não disponível")); + return; + } + ctx.drawImage(img, 0, 0, width, height); + resolve(canvas.toDataURL("image/jpeg", 0.85)); + }; + img.onerror = () => reject(new Error("Falha ao carregar imagem")); + img.src = e.target?.result as string; + }; + reader.onerror = () => reject(new Error("Falha ao ler arquivo")); + reader.readAsDataURL(file); + }); +} + type PayerCreatePayload = Parameters[0]; interface PayerDialogProps { @@ -77,8 +117,10 @@ export function PayerDialog({ }: PayerDialogProps) { const [errorMessage, setErrorMessage] = useState(null); const [isPending, startTransition] = useTransition(); + const [uploadedAvatar, setUploadedAvatar] = useState(null); + const [isProcessingImage, setIsProcessingImage] = useState(false); + const fileInputRef = useRef(null); - // Use controlled state hook for dialog open state const [dialogOpen, setDialogOpen] = useControlledState( open, false, @@ -90,26 +132,47 @@ export function PayerDialog({ [payer, avatarOptions], ); - // Use form state hook for form management const { formState, resetForm, updateField } = useFormState(initialState); + // Avatares da biblioteca excluem data URLs (que ficam no círculo de upload) const availableAvatars = useMemo(() => { - const set = new Set([ - ...avatarOptions, - initialState.avatarUrl, - DEFAULT_PAYER_AVATAR, - ]); + const set = new Set([...avatarOptions, DEFAULT_PAYER_AVATAR]); + if (initialState.avatarUrl && !initialState.avatarUrl.startsWith("data:")) { + set.add(initialState.avatarUrl); + } return Array.from(set).sort((a, b) => a.localeCompare(b, "pt-BR", { sensitivity: "base" }), ); }, [avatarOptions, initialState.avatarUrl]); - // Reset form when dialog opens + const handleFileChange = async (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (!file) return; + setIsProcessingImage(true); + try { + const base64 = await resizeImageToBase64(file); + setUploadedAvatar(base64); + updateField("avatarUrl", base64); + } catch { + toast.error("Não foi possível processar a imagem."); + } finally { + setIsProcessingImage(false); + if (fileInputRef.current) fileInputRef.current.value = ""; + } + }; + useEffect(() => { if (dialogOpen) { resetForm(initialState); setErrorMessage(null); + setIsProcessingImage(false); + // Se o avatar atual for um upload anterior, restaura no círculo + setUploadedAvatar( + initialState.avatarUrl.startsWith("data:") + ? initialState.avatarUrl + : null, + ); } }, [dialogOpen, initialState, resetForm]); @@ -161,6 +224,9 @@ export function PayerDialog({ const submitLabel = mode === "create" ? "Salvar pagador" : "Atualizar pagador"; + const isUploadSelected = + uploadedAvatar !== null && formState.avatarUrl === uploadedAvatar; + return ( {trigger ? {trigger} : null} @@ -255,6 +321,7 @@ export function PayerDialog({
{availableAvatars.map((avatar) => { const isSelected = avatar === formState.avatarUrl; + const src = getAvatarSrc(avatar); return (
diff --git a/src/shared/components/navigation/navbar/navbar-user.tsx b/src/shared/components/navigation/navbar/navbar-user.tsx index fe1743c..d5523cd 100644 --- a/src/shared/components/navigation/navbar/navbar-user.tsx +++ b/src/shared/components/navigation/navbar/navbar-user.tsx @@ -17,11 +17,6 @@ import { version } from "@/package.json"; import { FeedbackDialogBody } from "@/shared/components/navigation/navbar/feedback-dialog"; import { Badge } from "@/shared/components/ui/badge"; import { Dialog, DialogTrigger } from "@/shared/components/ui/dialog"; -import { - Tooltip, - TooltipContent, - TooltipTrigger, -} from "@/shared/components/ui/tooltip"; import { DropdownMenu, DropdownMenuContent, @@ -30,6 +25,11 @@ import { DropdownMenuTrigger, } from "@/shared/components/ui/dropdown-menu"; import { Spinner } from "@/shared/components/ui/spinner"; +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from "@/shared/components/ui/tooltip"; import { authClient } from "@/shared/lib/auth/client"; import { getAvatarSrc } from "@/shared/lib/payers/utils"; import type { UpdateCheckResult } from "@/shared/lib/version/check-update"; @@ -68,6 +68,7 @@ export function NavbarUser({ const avatarSrc = pagadorAvatarUrl ? getAvatarSrc(pagadorAvatarUrl) : user.image || getAvatarSrc(null); + const isDataUrl = avatarSrc.startsWith("data:"); async function handleLogout() { await authClient.signOut({ @@ -91,6 +92,7 @@ export function NavbarUser({
{`Avatar {user.name}
- {user.name} + + {user.name} +