865 lines
27 KiB
TypeScript
865 lines
27 KiB
TypeScript
"use client";
|
|
|
|
import confetti from "canvas-confetti";
|
|
import {
|
|
Check,
|
|
CheckCircle,
|
|
CheckCircle2,
|
|
Coffee,
|
|
Copy,
|
|
Heart,
|
|
Loader2,
|
|
Lock,
|
|
Pizza,
|
|
User,
|
|
} from "lucide-react";
|
|
import { useTranslations } from "next-intl";
|
|
import { useEffect, useState } from "react";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { Button } from "@/components/ui/button";
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
} from "@/components/ui/dialog";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Textarea } from "@/components/ui/textarea";
|
|
import type { DonationResponse, DonationTier } from "@/types/donation";
|
|
|
|
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 (
|
|
!Number.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>
|
|
</>
|
|
);
|
|
}
|