Replaced hardcoded Portuguese text with next-intl translation keys in the support page, header, and donation card components to enable internationalization and support multiple languages. This improves the app's accessibility and localization capabilities.
866 lines
27 KiB
TypeScript
866 lines
27 KiB
TypeScript
"use client";
|
|
|
|
import { useState, useEffect } from "react";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { Textarea } from "@/components/ui/textarea";
|
|
import confetti from "canvas-confetti";
|
|
import {
|
|
Heart,
|
|
Coffee,
|
|
Pizza,
|
|
Loader2,
|
|
Copy,
|
|
Check,
|
|
Lock,
|
|
User,
|
|
CheckCircle,
|
|
CheckCircle2,
|
|
DollarSign,
|
|
} from "lucide-react";
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
} from "@/components/ui/dialog";
|
|
import { DonationTier, DonationResponse } from "@/types/donation";
|
|
import { useTranslations } from "next-intl";
|
|
|
|
interface DonationCardProps extends DonationTier {}
|
|
|
|
export function DonationCard({
|
|
id,
|
|
amount,
|
|
title,
|
|
description,
|
|
icon,
|
|
badge,
|
|
highlighted,
|
|
}: DonationCardProps) {
|
|
// Estados para escolha inicial
|
|
const [showChoiceModal, setShowChoiceModal] = useState(false);
|
|
const [showDonorForm, setShowDonorForm] = useState(false);
|
|
const [donorName, setDonorName] = useState("");
|
|
const [donorEmail, setDonorEmail] = useState("");
|
|
const [donorPhone, setDonorPhone] = useState("");
|
|
const [donorTaxId, setDonorTaxId] = useState("");
|
|
const [donorMessage, setDonorMessage] = useState("");
|
|
const [customAmount, setCustomAmount] = useState("");
|
|
|
|
// Estados para PIX
|
|
const [loading, setLoading] = useState(false);
|
|
const [showPixModal, setShowPixModal] = useState(false);
|
|
const [pixData, setPixData] = useState<{
|
|
pixId: string;
|
|
brCode: string;
|
|
brCodeBase64: string;
|
|
expiresAt?: string;
|
|
} | null>(null);
|
|
const [copied, setCopied] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [timeRemaining, setTimeRemaining] = useState<string | null>(null);
|
|
const [paymentConfirmed, setPaymentConfirmed] = useState(false);
|
|
const [showThankYouModal, setShowThankYouModal] = useState(false);
|
|
|
|
const t = useTranslations("donation");
|
|
|
|
const icons = {
|
|
coffee: Coffee,
|
|
pizza: Pizza,
|
|
heart: Heart,
|
|
};
|
|
|
|
const Icon = icons[icon];
|
|
|
|
// Confetti ao abrir modal de agradecimento
|
|
useEffect(() => {
|
|
if (showThankYouModal) {
|
|
confetti({
|
|
particleCount: 100,
|
|
spread: 70,
|
|
origin: { y: 0.6 },
|
|
});
|
|
}
|
|
}, [showThankYouModal]);
|
|
|
|
// Atualizar timer e verificar pagamento automaticamente
|
|
useEffect(() => {
|
|
if (!pixData?.expiresAt || !pixData?.pixId) {
|
|
setTimeRemaining(null);
|
|
return;
|
|
}
|
|
|
|
const updateTimer = () => {
|
|
try {
|
|
const expires = new Date(pixData!.expiresAt!);
|
|
const now = new Date();
|
|
const diff = expires.getTime() - now.getTime();
|
|
|
|
if (diff <= 0) {
|
|
setTimeRemaining("Expirado");
|
|
return;
|
|
}
|
|
|
|
const minutes = Math.floor(diff / 60000);
|
|
const seconds = Math.floor((diff % 60000) / 1000);
|
|
|
|
setTimeRemaining(`${minutes}:${seconds.toString().padStart(2, "0")}`);
|
|
} catch {
|
|
setTimeRemaining(null);
|
|
}
|
|
};
|
|
|
|
// Verificar pagamento via API
|
|
const checkPaymentConfirmation = async () => {
|
|
if (!pixData?.pixId) return;
|
|
|
|
try {
|
|
const response = await fetch(
|
|
`/api/donations/status?pixId=${pixData.pixId}`
|
|
);
|
|
const data = await response.json();
|
|
|
|
if (data.paid) {
|
|
console.log("✅ Pagamento confirmado!");
|
|
setPaymentConfirmed(true);
|
|
|
|
// Confetti celebration!
|
|
confetti({
|
|
particleCount: 100,
|
|
spread: 70,
|
|
origin: { y: 0.6 },
|
|
});
|
|
|
|
setTimeout(() => {
|
|
setShowPixModal(false);
|
|
setShowThankYouModal(true);
|
|
// Limpar localStorage
|
|
localStorage.removeItem("currentPixId");
|
|
}, 1000);
|
|
clearInterval(timerInterval);
|
|
}
|
|
} catch (error) {
|
|
console.error("Erro ao verificar pagamento:", error);
|
|
}
|
|
};
|
|
|
|
// Atualizar timer imediatamente
|
|
updateTimer();
|
|
|
|
// Atualizar timer e verificar pagamento a cada 2 segundos
|
|
const timerInterval = setInterval(() => {
|
|
updateTimer();
|
|
checkPaymentConfirmation();
|
|
}, 2000);
|
|
|
|
return () => clearInterval(timerInterval);
|
|
}, [pixData?.expiresAt, pixData?.pixId]);
|
|
|
|
// Funções de formatação
|
|
const formatCPF = (value: string): string => {
|
|
const numbers = value.replace(/\D/g, "");
|
|
if (numbers.length <= 11) {
|
|
return numbers
|
|
.replace(/(\d{3})(\d)/, "$1.$2")
|
|
.replace(/(\d{3})(\d)/, "$1.$2")
|
|
.replace(/(\d{3})(\d{1,2})$/, "$1-$2");
|
|
}
|
|
return value; // Não formatar se for CNPJ (14 dígitos)
|
|
};
|
|
|
|
const formatCNPJ = (value: string): string => {
|
|
const numbers = value.replace(/\D/g, "");
|
|
if (numbers.length > 11) {
|
|
return numbers
|
|
.replace(/^(\d{2})(\d)/, "$1.$2")
|
|
.replace(/^(\d{2})\.(\d{3})(\d)/, "$1.$2.$3")
|
|
.replace(/\.(\d{3})(\d)/, ".$1/$2")
|
|
.replace(/(\d{4})(\d)/, "$1-$2");
|
|
}
|
|
return value;
|
|
};
|
|
|
|
const formatPhone = (value: string): string => {
|
|
const numbers = value.replace(/\D/g, "");
|
|
if (numbers.length <= 10) {
|
|
// Telefone fixo (10 dígitos)
|
|
return numbers.replace(/^(\d{2})(\d{4})(\d{4})$/, "($1) $2-$3");
|
|
} else {
|
|
// Celular (11 dígitos)
|
|
return numbers.replace(/^(\d{2})(\d{5})(\d{4})$/, "($1) $2-$3");
|
|
}
|
|
};
|
|
|
|
const handleDonateClick = () => {
|
|
setShowChoiceModal(true);
|
|
setError(null);
|
|
};
|
|
|
|
const handleAnonymousDonation = async () => {
|
|
setLoading(true);
|
|
setError(null);
|
|
setShowChoiceModal(false);
|
|
|
|
try {
|
|
const requestBody: any = {
|
|
tier: id,
|
|
anonymous: true,
|
|
};
|
|
|
|
// Adicionar valor customizado se for custom
|
|
if (id === "custom" && customAmount) {
|
|
requestBody.customAmount = parseFloat(customAmount);
|
|
}
|
|
|
|
const response = await fetch("/api/donations/create", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify(requestBody),
|
|
});
|
|
|
|
const data: DonationResponse = await response.json();
|
|
|
|
if (data.success && data.brCode && data.brCodeBase64 && data.pixId) {
|
|
setPixData({
|
|
pixId: data.pixId,
|
|
brCode: data.brCode,
|
|
brCodeBase64: data.brCodeBase64,
|
|
expiresAt: data.expiresAt,
|
|
});
|
|
// Salvar pixId no localStorage para verificação
|
|
localStorage.setItem("currentPixId", data.pixId);
|
|
setShowPixModal(true);
|
|
} else {
|
|
setError(data.error || "Erro ao processar doação");
|
|
setShowChoiceModal(true);
|
|
}
|
|
} catch (error) {
|
|
console.error("Donation error:", error);
|
|
setError("Erro ao processar doação. Tente novamente.");
|
|
setShowChoiceModal(true);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleIdentifiedClick = () => {
|
|
setShowChoiceModal(false);
|
|
setShowDonorForm(true);
|
|
setError(null);
|
|
};
|
|
|
|
const handleSubmitForm = async () => {
|
|
// Validação
|
|
if (!donorName.trim() || donorName.trim().length < 3) {
|
|
setError("Nome deve ter no mínimo 3 caracteres");
|
|
return;
|
|
}
|
|
|
|
if (!donorPhone.trim()) {
|
|
setError("Telefone é obrigatório");
|
|
return;
|
|
}
|
|
|
|
if (!donorEmail.trim()) {
|
|
setError("Email é obrigatório");
|
|
return;
|
|
}
|
|
|
|
if (!donorTaxId.trim()) {
|
|
setError("CPF/CNPJ é obrigatório");
|
|
return;
|
|
}
|
|
|
|
if (donorMessage && donorMessage.length > 280) {
|
|
setError("Mensagem deve ter no máximo 280 caracteres");
|
|
return;
|
|
}
|
|
|
|
setLoading(true);
|
|
setError(null);
|
|
|
|
try {
|
|
const requestBody: any = {
|
|
tier: id,
|
|
anonymous: false,
|
|
donorName: donorName.trim(),
|
|
donorEmail: donorEmail.trim(),
|
|
donorPhone: donorPhone.replace(/\D/g, ""), // Apenas números
|
|
donorTaxId: donorTaxId.replace(/\D/g, ""), // Apenas números
|
|
donorMessage: donorMessage.trim() || undefined,
|
|
};
|
|
|
|
// Adicionar valor customizado se for custom
|
|
if (id === "custom" && customAmount) {
|
|
requestBody.customAmount = parseFloat(customAmount);
|
|
}
|
|
|
|
const response = await fetch("/api/donations/create", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify(requestBody),
|
|
});
|
|
|
|
const data: DonationResponse = await response.json();
|
|
|
|
if (data.success && data.brCode && data.brCodeBase64 && data.pixId) {
|
|
setPixData({
|
|
pixId: data.pixId,
|
|
brCode: data.brCode,
|
|
brCodeBase64: data.brCodeBase64,
|
|
expiresAt: data.expiresAt,
|
|
});
|
|
// Salvar pixId no localStorage
|
|
localStorage.setItem("currentPixId", data.pixId);
|
|
setShowDonorForm(false);
|
|
setShowPixModal(true);
|
|
} else {
|
|
setError(data.error || "Erro ao processar doação");
|
|
}
|
|
} catch (error) {
|
|
console.error("Donation error:", error);
|
|
setError("Erro ao processar doação. Tente novamente.");
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleCopyPixCode = async () => {
|
|
if (pixData?.brCode) {
|
|
try {
|
|
await navigator.clipboard.writeText(pixData.brCode);
|
|
setCopied(true);
|
|
setTimeout(() => setCopied(false), 2000);
|
|
} catch (error) {
|
|
console.error("Erro ao copiar código PIX:", error);
|
|
}
|
|
}
|
|
};
|
|
|
|
// Funções para doação customizada
|
|
const { DONATION_PRODUCTS } = require("@/types/donation");
|
|
const customProduct = DONATION_PRODUCTS.custom;
|
|
|
|
const isValidCustomAmount = () => {
|
|
const amount = parseFloat(customAmount);
|
|
return (
|
|
!isNaN(amount) &&
|
|
amount >= customProduct.minAmount &&
|
|
amount <= customProduct.maxAmount
|
|
);
|
|
};
|
|
|
|
const formatCurrency = (value: string): string => {
|
|
// Permite vírgula durante digitação, mas converte para ponto
|
|
// Remove tudo exceto números, vírgulas e pontos
|
|
const cleaned = value.replace(/[^\d.,]/g, "");
|
|
|
|
// Se tem vírgula, troca por ponto
|
|
const normalized = cleaned.replace(/,/g, ".");
|
|
|
|
// Garante apenas um separador decimal
|
|
const parts = normalized.split(".");
|
|
if (parts.length > 2) {
|
|
return parts[0] + "." + parts.slice(1).join("");
|
|
}
|
|
|
|
// Limita 2 casas decimais
|
|
if (parts[1]?.length > 2) {
|
|
return parts[0] + "." + parts[1].substring(0, 2);
|
|
}
|
|
|
|
return normalized;
|
|
};
|
|
|
|
const handleCustomAmountChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
const formatted = formatCurrency(e.target.value);
|
|
|
|
// Se o usuário digitou vírgula e ainda não há ponto, adiciona vírgula temporariamente
|
|
const rawValue = e.target.value;
|
|
if (
|
|
rawValue.includes(",") &&
|
|
!rawValue.includes(".") &&
|
|
!rawValue.replace(/[^\d,]/g, "").includes(".")
|
|
) {
|
|
// Permite a vírgula momentaneamente, mas converte ao processar
|
|
setCustomAmount(formatted);
|
|
} else {
|
|
setCustomAmount(formatted);
|
|
}
|
|
};
|
|
|
|
const handleCustomDonationClick = () => {
|
|
if (!customAmount) {
|
|
setError("Digite um valor");
|
|
return;
|
|
}
|
|
const amount = parseFloat(customAmount);
|
|
if (!isValidCustomAmount()) {
|
|
setError(
|
|
`Valor deve estar entre R$ ${customProduct.minAmount.toFixed(
|
|
2
|
|
)} e R$ ${customProduct.maxAmount.toFixed(2)}`
|
|
);
|
|
return;
|
|
}
|
|
handleDonateClick();
|
|
};
|
|
|
|
return (
|
|
<>
|
|
<div
|
|
className={`
|
|
relative p-8 rounded-2xl border bg-card backdrop-blur-lg
|
|
hover:scale-105 transition-all duration-300
|
|
${
|
|
highlighted
|
|
? "border-primary/50 shadow-[0_0_30px_rgba(34,197,94,0.3)]"
|
|
: "border-border/50"
|
|
}
|
|
`}
|
|
>
|
|
{badge && (
|
|
<Badge className="absolute -top-3 left-1/2 -translate-x-1/2 bg-primary">
|
|
{badge}
|
|
</Badge>
|
|
)}
|
|
|
|
<div className="flex flex-col items-center text-center space-y-4">
|
|
<div className="p-4 rounded-full bg-muted/50 border border-border/30">
|
|
<Icon className="w-12 h-12 text-primary" />
|
|
</div>
|
|
|
|
<h3 className="text-2xl font-bold">{title}</h3>
|
|
<p className="text-muted-foreground">{description}</p>
|
|
|
|
{id === "custom" ? (
|
|
<>
|
|
<div className="w-full space-y-4">
|
|
<div className="space-y-2">
|
|
<label
|
|
htmlFor="customAmount"
|
|
className="text-sm text-muted-foreground"
|
|
>
|
|
{t("form.value")}
|
|
</label>
|
|
<Input
|
|
id="customAmount"
|
|
type="text"
|
|
inputMode="decimal"
|
|
placeholder={t("form.chooseAmount")}
|
|
value={customAmount}
|
|
onChange={handleCustomAmountChange}
|
|
className="text-lg text-center"
|
|
/>
|
|
<p className="text-xs text-muted-foreground">
|
|
{t("form.min")}: R$ {customProduct.minAmount.toFixed(2)} |{" "}
|
|
{t("form.max")}: R$ {customProduct.maxAmount.toFixed(2)}
|
|
</p>
|
|
</div>
|
|
{customAmount && !isValidCustomAmount() && (
|
|
<p className="text-xs text-red-500">
|
|
{parseFloat(customAmount) < customProduct.minAmount
|
|
? `${t(
|
|
"form.minAmount"
|
|
)} ${customProduct.minAmount.toFixed(2)}`
|
|
: `${t(
|
|
"form.maxAmount"
|
|
)} ${customProduct.maxAmount.toFixed(2)}`}
|
|
</p>
|
|
)}
|
|
</div>
|
|
|
|
<Button
|
|
size="lg"
|
|
className="w-full"
|
|
onClick={handleCustomDonationClick}
|
|
disabled={loading || !isValidCustomAmount()}
|
|
>
|
|
{loading ? (
|
|
<>
|
|
<Loader2 className="mr-2 h-5 w-5 animate-spin" />
|
|
{t("form.processing")}
|
|
</>
|
|
) : customAmount ? (
|
|
`${t("form.donate")} ${parseFloat(
|
|
customAmount || "0"
|
|
).toFixed(2)}`
|
|
) : (
|
|
t("form.chooseAmount")
|
|
)}
|
|
</Button>
|
|
</>
|
|
) : (
|
|
<>
|
|
{amount && (
|
|
<div className="text-4xl font-bold text-primary">
|
|
R$ {amount.toFixed(2)}
|
|
</div>
|
|
)}
|
|
|
|
<Button
|
|
size="lg"
|
|
className="w-full"
|
|
onClick={handleDonateClick}
|
|
disabled={loading}
|
|
>
|
|
{loading ? (
|
|
<>
|
|
<Loader2 className="mr-2 h-5 w-5 animate-spin" />
|
|
{t("form.processing")}
|
|
</>
|
|
) : (
|
|
t("form.supportWithPix")
|
|
)}
|
|
</Button>
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Modal de Escolha (Anônimo vs Identificado) */}
|
|
<Dialog open={showChoiceModal} onOpenChange={setShowChoiceModal}>
|
|
<DialogContent className="max-w-4xl">
|
|
<DialogHeader>
|
|
<DialogTitle className="text-center text-2xl">
|
|
{t("form.howToContribute")}
|
|
</DialogTitle>
|
|
</DialogHeader>
|
|
|
|
<div className="grid md:grid-cols-2 gap-4 mt-6">
|
|
{/* Card Anônimo */}
|
|
<div
|
|
className="p-6 rounded-xl border border-border/50 bg-card hover:border-primary/30 transition-all cursor-pointer"
|
|
onClick={handleAnonymousDonation}
|
|
>
|
|
<div className="flex items-center gap-3 mb-3">
|
|
<div className="p-2 rounded-full bg-muted">
|
|
<Lock className="h-5 w-5" />
|
|
</div>
|
|
<h3 className="text-xl font-bold">
|
|
{t("form.anonymousTitle")}
|
|
</h3>
|
|
</div>
|
|
<p className="text-muted-foreground mb-4">
|
|
{t("form.anonymousDesc")}
|
|
</p>
|
|
<Button className="w-full" variant="outline" disabled={loading}>
|
|
{t("form.anonymousButton")}
|
|
</Button>
|
|
</div>
|
|
|
|
{/* Card Identificado */}
|
|
<div
|
|
className="p-6 rounded-xl border-2 border-primary/50 bg-primary/5 hover:border-primary transition-all cursor-pointer relative"
|
|
onClick={handleIdentifiedClick}
|
|
>
|
|
<Badge className="absolute -top-3 right-6 bg-primary">
|
|
{t("form.recommended")}
|
|
</Badge>
|
|
<div className="flex items-center gap-3 mb-3">
|
|
<div className="p-2 rounded-full bg-primary/10">
|
|
<User className="h-5 w-5 text-primary" />
|
|
</div>
|
|
<h3 className="text-xl font-bold">
|
|
{t("form.identifiedTitle")}
|
|
</h3>
|
|
</div>
|
|
<p className="text-muted-foreground mb-4">
|
|
{t("form.identifiedDesc")}
|
|
</p>
|
|
|
|
<div className="space-y-2 mb-4">
|
|
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
|
<CheckCircle className="h-4 w-4 text-primary" />
|
|
<span>{t("form.thankByEmail")}</span>
|
|
</div>
|
|
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
|
<CheckCircle className="h-4 w-4 text-primary" />
|
|
<span>{t("form.earlyAccess")}</span>
|
|
</div>
|
|
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
|
<CheckCircle className="h-4 w-4 text-primary" />
|
|
<span>{t("form.contributionHistory")}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<Button className="w-full" disabled={loading}>
|
|
{t("form.identifyMe")}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</DialogContent>
|
|
</Dialog>
|
|
|
|
{/* Modal de Formulário do Doador Identificado */}
|
|
<Dialog open={showDonorForm} onOpenChange={setShowDonorForm}>
|
|
<DialogContent className="max-w-md">
|
|
<DialogHeader>
|
|
<DialogTitle>{t("form.donorInfo")}</DialogTitle>
|
|
</DialogHeader>
|
|
|
|
<div className="space-y-4">
|
|
{error && (
|
|
<div className="p-3 bg-destructive/10 border border-destructive/50 rounded-lg text-destructive text-sm">
|
|
{error}
|
|
</div>
|
|
)}
|
|
|
|
<div>
|
|
<label className="text-sm font-medium mb-2 block">
|
|
{t("form.name")}{" "}
|
|
<span className="text-muted-foreground text-xs">
|
|
({t("form.nameHint")})
|
|
</span>
|
|
</label>
|
|
<Input
|
|
value={donorName}
|
|
onChange={(e) => setDonorName(e.target.value)}
|
|
placeholder={t("form.namePlaceholder")}
|
|
maxLength={50}
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="text-sm font-medium mb-2 block">
|
|
{t("form.email")}
|
|
</label>
|
|
<Input
|
|
type="email"
|
|
value={donorEmail}
|
|
onChange={(e) => setDonorEmail(e.target.value)}
|
|
placeholder={t("form.emailPlaceholder")}
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="text-sm font-medium mb-2 block">
|
|
{t("form.phone")}{" "}
|
|
<span className="text-muted-foreground text-xs">
|
|
({t("form.phoneHint")})
|
|
</span>
|
|
</label>
|
|
<Input
|
|
value={donorPhone}
|
|
onChange={(e) => setDonorPhone(formatPhone(e.target.value))}
|
|
placeholder={t("form.phonePlaceholder")}
|
|
maxLength={15}
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="text-sm font-medium mb-2 block">
|
|
{t("form.taxId")}{" "}
|
|
<span className="text-muted-foreground text-xs">
|
|
({t("form.taxIdHint")})
|
|
</span>
|
|
</label>
|
|
<Input
|
|
value={donorTaxId}
|
|
onChange={(e) => {
|
|
const value = e.target.value.replace(/\D/g, "");
|
|
if (value.length <= 11) {
|
|
setDonorTaxId(formatCPF(value));
|
|
} else {
|
|
setDonorTaxId(formatCNPJ(value));
|
|
}
|
|
}}
|
|
placeholder={t("form.taxIdPlaceholder")}
|
|
maxLength={18}
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="text-sm font-medium mb-2 block">
|
|
{t("form.message")}{" "}
|
|
<span className="text-muted-foreground text-xs">
|
|
({t("form.messageHint")})
|
|
</span>
|
|
</label>
|
|
<Textarea
|
|
value={donorMessage}
|
|
onChange={(e) => {
|
|
if (e.target.value.length <= 280) {
|
|
setDonorMessage(e.target.value);
|
|
}
|
|
}}
|
|
placeholder={t("form.messagePlaceholder")}
|
|
rows={3}
|
|
maxLength={280}
|
|
/>
|
|
<p className="text-xs text-muted-foreground text-right mt-1">
|
|
{donorMessage.length}/280
|
|
</p>
|
|
</div>
|
|
|
|
<div className="flex gap-2">
|
|
<Button
|
|
onClick={() => {
|
|
setShowDonorForm(false);
|
|
setShowChoiceModal(true);
|
|
setError(null);
|
|
}}
|
|
variant="outline"
|
|
className="flex-1"
|
|
disabled={loading}
|
|
>
|
|
{t("form.close")}
|
|
</Button>
|
|
<Button
|
|
onClick={handleSubmitForm}
|
|
className="flex-1"
|
|
disabled={loading}
|
|
>
|
|
{loading ? (
|
|
<>
|
|
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
|
{t("form.processing")}
|
|
</>
|
|
) : (
|
|
t("form.generatePix")
|
|
)}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</DialogContent>
|
|
</Dialog>
|
|
|
|
{/* Modal de QR Code PIX */}
|
|
<Dialog open={showPixModal} onOpenChange={setShowPixModal}>
|
|
<DialogContent className="max-w-md">
|
|
<DialogHeader>
|
|
<DialogTitle className="text-center">
|
|
{t("form.qrTitle")}
|
|
</DialogTitle>
|
|
</DialogHeader>
|
|
|
|
{pixData && (
|
|
<div className="space-y-4">
|
|
<div className="flex justify-center p-4 bg-white rounded-lg">
|
|
<img
|
|
src={pixData.brCodeBase64}
|
|
alt="QR Code PIX"
|
|
className="w-64 h-64"
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<p className="text-sm text-muted-foreground text-center">
|
|
{t("form.copyCode")}
|
|
</p>
|
|
<div className="flex gap-2">
|
|
<Input
|
|
value={pixData.brCode}
|
|
readOnly
|
|
className="font-mono text-sm"
|
|
/>
|
|
<Button
|
|
onClick={handleCopyPixCode}
|
|
variant="outline"
|
|
size="sm"
|
|
>
|
|
{copied ? (
|
|
<>
|
|
<Check className="h-4 w-4" />
|
|
{t("form.copied")}
|
|
</>
|
|
) : (
|
|
<>
|
|
<Copy className="h-4 w-4" />
|
|
{t("form.copy")}
|
|
</>
|
|
)}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
{timeRemaining && (
|
|
<p className="text-xs text-center text-muted-foreground">
|
|
⏱️ {t("form.remainingTime")} {timeRemaining}
|
|
</p>
|
|
)}
|
|
|
|
<div className="flex gap-2">
|
|
<Button
|
|
onClick={() => setShowPixModal(false)}
|
|
variant="outline"
|
|
className="flex-1"
|
|
>
|
|
{t("form.close")}
|
|
</Button>
|
|
<Button
|
|
onClick={() => (window.location.href = "/apoiar/obrigado")}
|
|
className="flex-1"
|
|
>
|
|
{t("form.paid")}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</DialogContent>
|
|
</Dialog>
|
|
|
|
{/* Modal de Agradecimento */}
|
|
<Dialog open={showThankYouModal} onOpenChange={setShowThankYouModal}>
|
|
<DialogContent className="max-w-md">
|
|
<div className="text-center space-y-6 py-4">
|
|
{/* Ícone de sucesso animado */}
|
|
<div className="flex justify-center">
|
|
<div className="relative">
|
|
<div className="absolute inset-0 bg-green-500/20 rounded-full animate-ping" />
|
|
<div className="relative p-4 rounded-full bg-green-500/10 border-4 border-green-500">
|
|
<CheckCircle2 className="w-16 h-16 text-green-500" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Título e mensagem */}
|
|
<div>
|
|
<h2 className="text-2xl font-bold mb-2">
|
|
{t("form.paymentConfirmed")} 🎉
|
|
</h2>
|
|
<p className="text-muted-foreground">
|
|
{t("form.paymentConfirmedDesc")}
|
|
</p>
|
|
</div>
|
|
|
|
{/* Mensagem especial */}
|
|
<div className="p-4 rounded-lg bg-primary/5 border border-primary/20">
|
|
<p className="text-sm text-muted-foreground">
|
|
{t("form.contributionMatters")} ❤️
|
|
</p>
|
|
</div>
|
|
|
|
{/* Botões */}
|
|
<div className="flex gap-3">
|
|
<Button
|
|
onClick={() => {
|
|
setShowThankYouModal(false);
|
|
window.location.href = "/apoiar/obrigado";
|
|
}}
|
|
className="flex-1"
|
|
size="lg"
|
|
>
|
|
{t("form.viewThankYouPage")}
|
|
</Button>
|
|
<Button
|
|
onClick={() => {
|
|
setShowThankYouModal(false);
|
|
window.location.href = "/";
|
|
}}
|
|
variant="outline"
|
|
className="flex-1"
|
|
size="lg"
|
|
>
|
|
{t("form.backToHome")}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</DialogContent>
|
|
</Dialog>
|
|
</>
|
|
);
|
|
}
|