fix: protect /setup route - redirect to login if organization already exists

This commit is contained in:
Erik Silva
2026-01-20 23:59:01 -03:00
parent 1619a813e2
commit c43118a0d8
2 changed files with 418 additions and 402 deletions

View File

@@ -0,0 +1,406 @@
"use client";
import React, { useState } from "react";
import { motion, AnimatePresence } from "framer-motion";
import {
Building2,
Palette,
User,
ArrowRight,
ArrowLeft,
CheckCircle2,
CloudUpload,
Globe,
Layout,
Loader2,
ShieldCheck
} from "lucide-react";
import { uploadFile } from "@/app/actions/upload";
import { createOrganization } from "@/app/actions/setup";
import { useRouter } from "next/navigation";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Alert, AlertDescription } from "@/components/ui/alert";
export default function SetupClient() {
const router = useRouter();
const [step, setStep] = useState(1);
const [isUploading, setIsUploading] = useState(false);
const [isSubmitting, setIsSubmitting] = useState(false);
const totalSteps = 3;
// Form State
const [formData, setFormData] = useState({
name: "",
cnpj: "",
logoUrl: "",
primaryColor: "#2563eb",
adminName: "",
adminEmail: "",
adminPassword: "",
confirmPassword: ""
});
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setFormData(prev => ({ ...prev, [e.target.name]: e.target.value }));
};
const handleFileUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
setIsUploading(true);
const data = new FormData();
data.append("file", file);
const result = await uploadFile(data);
if (result.success && result.url) {
setFormData(prev => ({ ...prev, logoUrl: result.url || "" }));
} else {
alert("Erro ao fazer upload da logo");
}
setIsUploading(false);
};
const handleSubmit = async () => {
if (formData.adminPassword !== formData.confirmPassword) {
alert("As senhas não coincidem!");
return;
}
setIsSubmitting(true);
const result = await createOrganization(formData);
if (result.success) {
router.push("/dashboard");
router.refresh();
} else {
alert(result.error || "Erro ao finalizar instalação");
setIsSubmitting(false);
}
};
const nextStep = () => {
if (step === totalSteps) {
handleSubmit();
} else {
setStep((s) => Math.min(s + 1, totalSteps));
}
};
const prevStep = () => setStep((s) => Math.max(s - 1, 1));
return (
<div className="min-h-screen bg-[#f8fafc] flex items-center justify-center p-6 bg-[radial-gradient(ellipse_at_top_right,_var(--tw-gradient-stops))] from-blue-50 via-slate-50 to-white font-sans">
<div className="max-w-4xl w-full">
{/* Header Section */}
<div className="text-center mb-10">
<motion.div
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
>
<Badge variant="secondary" className="mb-4 px-3 py-1">
<ShieldCheck size={14} className="mr-1" />
Instalação do Portal
</Badge>
</motion.div>
<motion.h1
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.1 }}
className="text-4xl font-bold text-slate-900 tracking-tight mb-2"
>
Configuração Master
</motion.h1>
<motion.p
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.2 }}
className="text-slate-500"
>
Este portal está cru. Vamos realizar o setup inicial da sua organização.
</motion.p>
</div>
{/* Progress Bar */}
<div className="mb-12 relative">
<div className="flex justify-between items-center max-w-2xl mx-auto relative z-10">
{[1, 2, 3].map((i) => (
<div
key={i}
className={`w-10 h-10 rounded-full flex items-center justify-center transition-all duration-500 ${step >= i ? "bg-blue-600 text-white shadow-lg shadow-blue-200" : "bg-white text-slate-400 border border-slate-200"
}`}
>
{step > i ? <CheckCircle2 size={24} /> : <span>{i}</span>}
</div>
))}
</div>
<div className="absolute top-1/2 left-0 w-full h-0.5 bg-slate-200 -translate-y-1/2 max-w-2xl mx-auto right-0" />
<motion.div
className="absolute top-1/2 left-0 h-0.5 bg-blue-600 -translate-y-1/2 max-w-2xl mx-auto right-0 origin-left"
initial={{ scaleX: 0 }}
animate={{ scaleX: (step - 1) / (totalSteps - 1) }}
transition={{ duration: 0.5 }}
/>
</div>
{/* Form Card */}
<Card className="border-0 shadow-xl bg-white/80 backdrop-blur-md">
<CardContent className="p-8 md:p-12 min-h-[500px] flex flex-col justify-between">
<AnimatePresence mode="wait">
{step === 1 && (
<motion.div
key="step1"
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -20 }}
className="space-y-8"
>
<div className="flex items-center gap-4 mb-8">
<div className="p-3 bg-blue-50 text-blue-600 rounded-2xl">
<Building2 size={32} />
</div>
<div>
<h2 className="text-2xl font-semibold text-slate-800">Identidade da Organização</h2>
<p className="text-slate-500">Defina o nome oficial e a logo que aparecerá no portal.</p>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="space-y-2">
<Label htmlFor="name">Nome da Organização</Label>
<Input
id="name"
name="name"
value={formData.name}
onChange={handleChange}
type="text"
placeholder="Ex: Instituto Esperança"
className="h-12"
/>
</div>
<div className="space-y-2">
<Label htmlFor="cnpj">CNPJ (Opcional)</Label>
<Input
id="cnpj"
name="cnpj"
value={formData.cnpj}
onChange={handleChange}
type="text"
placeholder="00.000.000/0000-00"
className="h-12"
/>
</div>
<div className="md:col-span-2 space-y-2">
<Label>Logo da ONG</Label>
<div className="relative border-2 border-dashed border-slate-200 rounded-2xl p-8 flex flex-col items-center justify-center text-slate-400 hover:border-blue-400 hover:bg-blue-50 transition-all cursor-pointer group">
<input
type="file"
className="absolute inset-0 opacity-0 cursor-pointer"
onChange={handleFileUpload}
accept="image/*"
/>
{isUploading ? (
<div className="flex flex-col items-center">
<Loader2 size={48} className="mb-4 text-blue-500 animate-spin" />
<p className="text-sm font-medium">Enviando...</p>
</div>
) : formData.logoUrl ? (
<div className="flex flex-col items-center">
<img src={formData.logoUrl} alt="Logo" className="h-32 object-contain mb-4 rounded-lg" />
<p className="text-sm text-blue-600 font-medium whitespace-nowrap overflow-hidden text-ellipsis max-w-xs"> Logo enviada com sucesso!</p>
</div>
) : (
<>
<CloudUpload size={48} className="mb-4 group-hover:text-blue-500 transition-colors" />
<p className="text-sm font-medium">Arraste sua logo ou clique para buscar</p>
<p className="text-xs mt-1">PNG, JPG até 5MB</p>
</>
)}
</div>
</div>
</div>
</motion.div>
)}
{step === 2 && (
<motion.div
key="step2"
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -20 }}
className="space-y-8"
>
<div className="flex items-center gap-4 mb-8">
<div className="p-3 bg-indigo-50 text-indigo-600 rounded-2xl">
<Palette size={32} />
</div>
<div>
<h2 className="text-2xl font-semibold text-slate-800">Personalização Visual</h2>
<p className="text-slate-500">Adapte o portal às cores da sua marca.</p>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="space-y-2">
<Label>Cor Principal</Label>
<div className="flex gap-4 items-center">
<input
name="primaryColor"
value={formData.primaryColor}
onChange={handleChange}
type="color"
className="w-16 h-16 rounded-xl border-none p-0 overflow-hidden cursor-pointer"
/>
<div className="space-y-1">
<p className="text-sm font-medium text-slate-600">Selecione o tom da marca</p>
<p className="text-xs text-slate-400 italic">Isso aplicará o tema dinâmico.</p>
</div>
</div>
</div>
<div className="space-y-2">
<Label className="italic opacity-50">Ambiente de Instalação</Label>
<div className="h-12 bg-slate-50 border rounded-md text-slate-400 flex items-center gap-2 px-3 cursor-not-allowed">
<Globe size={16} />
Detectado via domínio atual
</div>
</div>
</div>
<div className="p-6 bg-slate-50 rounded-2xl border border-slate-100 space-y-4">
<h3 className="font-semibold text-slate-800 flex items-center gap-2">
<Layout size={18} className="text-blue-500" />
Preview em tempo real
</h3>
<div className="aspect-video bg-white rounded-xl shadow-sm border border-slate-200 flex flex-col items-center justify-center p-4">
<div className="w-full h-8 bg-slate-100 rounded-lg mb-4 flex items-center px-4 gap-2">
<div className="w-4 h-4 rounded-full" style={{ backgroundColor: formData.primaryColor }} />
<div className="w-24 h-2 bg-slate-200 rounded" />
</div>
<Button style={{ backgroundColor: formData.primaryColor }}>Botão Exemplo</Button>
</div>
</div>
</motion.div>
)}
{step === 3 && (
<motion.div
key="step3"
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -20 }}
className="space-y-8"
>
<div className="flex items-center gap-4 mb-8">
<div className="p-3 bg-emerald-50 text-emerald-600 rounded-2xl">
<User size={32} />
</div>
<div>
<h2 className="text-2xl font-semibold text-slate-800">Administrador Master</h2>
<p className="text-slate-500">Crie a conta que terá controle total sobre o portal.</p>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="space-y-2">
<Label htmlFor="adminName">Nome do Responsável</Label>
<Input
id="adminName"
name="adminName"
value={formData.adminName}
onChange={handleChange}
type="text"
placeholder="João Silva"
className="h-12"
/>
</div>
<div className="space-y-2">
<Label htmlFor="adminEmail">E-mail de Acesso</Label>
<Input
id="adminEmail"
name="adminEmail"
value={formData.adminEmail}
onChange={handleChange}
type="email"
placeholder="admin@ong.org"
className="h-12"
/>
</div>
<div className="space-y-2">
<Label htmlFor="adminPassword">Senha</Label>
<Input
id="adminPassword"
name="adminPassword"
value={formData.adminPassword}
onChange={handleChange}
type="password"
placeholder="••••••••"
className="h-12"
/>
</div>
<div className="space-y-2">
<Label htmlFor="confirmPassword">Confirmar Senha</Label>
<Input
id="confirmPassword"
name="confirmPassword"
value={formData.confirmPassword}
onChange={handleChange}
type="password"
placeholder="••••••••"
className="h-12"
/>
</div>
</div>
<Alert>
<CheckCircle2 size={18} />
<AlertDescription>
Ao concluir, o banco de dados será populado e este ambiente será bloqueado para novos setups.
</AlertDescription>
</Alert>
</motion.div>
)}
</AnimatePresence>
<div className="flex justify-between items-center mt-12 pt-8 border-t border-slate-100">
<Button
variant="ghost"
onClick={prevStep}
disabled={isSubmitting}
className={step === 1 ? "invisible" : ""}
>
<ArrowLeft size={18} className="mr-2" />
Voltar
</Button>
<Button
onClick={nextStep}
disabled={isSubmitting || isUploading}
className="h-12 px-6"
>
{isSubmitting ? (
<>
<Loader2 size={18} className="animate-spin mr-2" />
Finalizando...
</>
) : (
<>
{step === totalSteps ? "Finalizar Instalação" : "Próximo Passo"}
<ArrowRight size={18} className="ml-2" />
</>
)}
</Button>
</div>
</CardContent>
</Card>
{/* Footer Support */}
<div className="mt-8 text-center text-slate-400 text-sm">
Portal de Transparência v1.0 Setup de Instalação
</div>
</div>
</div>
);
}

View File

@@ -1,406 +1,16 @@
"use client"; import { prisma } from "@/lib/db";
import { redirect } from "next/navigation";
import SetupClient from "./SetupClient";
import React, { useState } from "react"; export default async function SetupPage() {
import { motion, AnimatePresence } from "framer-motion"; // Verifica se já existe alguma organização cadastrada
import { const existingOrg = await prisma.organization.findFirst();
Building2,
Palette,
User,
ArrowRight,
ArrowLeft,
CheckCircle2,
CloudUpload,
Globe,
Layout,
Loader2,
ShieldCheck
} from "lucide-react";
import { uploadFile } from "@/app/actions/upload";
import { createOrganization } from "@/app/actions/setup";
import { useRouter } from "next/navigation";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Alert, AlertDescription } from "@/components/ui/alert";
export default function SetupPage() { // Se já existe, redireciona para o login (setup já foi feito)
const router = useRouter(); if (existingOrg) {
const [step, setStep] = useState(1); redirect("/");
const [isUploading, setIsUploading] = useState(false); }
const [isSubmitting, setIsSubmitting] = useState(false);
const totalSteps = 3;
// Form State // Se não existe, mostra o setup
const [formData, setFormData] = useState({ return <SetupClient />;
name: "",
cnpj: "",
logoUrl: "",
primaryColor: "#2563eb",
adminName: "",
adminEmail: "",
adminPassword: "",
confirmPassword: ""
});
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setFormData(prev => ({ ...prev, [e.target.name]: e.target.value }));
};
const handleFileUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
setIsUploading(true);
const data = new FormData();
data.append("file", file);
const result = await uploadFile(data);
if (result.success && result.url) {
setFormData(prev => ({ ...prev, logoUrl: result.url || "" }));
} else {
alert("Erro ao fazer upload da logo");
}
setIsUploading(false);
};
const handleSubmit = async () => {
if (formData.adminPassword !== formData.confirmPassword) {
alert("As senhas não coincidem!");
return;
}
setIsSubmitting(true);
const result = await createOrganization(formData);
if (result.success) {
router.push("/dashboard");
router.refresh();
} else {
alert(result.error || "Erro ao finalizar instalação");
setIsSubmitting(false);
}
};
const nextStep = () => {
if (step === totalSteps) {
handleSubmit();
} else {
setStep((s) => Math.min(s + 1, totalSteps));
}
};
const prevStep = () => setStep((s) => Math.max(s - 1, 1));
return (
<div className="min-h-screen bg-[#f8fafc] flex items-center justify-center p-6 bg-[radial-gradient(ellipse_at_top_right,_var(--tw-gradient-stops))] from-blue-50 via-slate-50 to-white font-sans">
<div className="max-w-4xl w-full">
{/* Header Section */}
<div className="text-center mb-10">
<motion.div
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
>
<Badge variant="secondary" className="mb-4 px-3 py-1">
<ShieldCheck size={14} className="mr-1" />
Instalação do Portal
</Badge>
</motion.div>
<motion.h1
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.1 }}
className="text-4xl font-bold text-slate-900 tracking-tight mb-2"
>
Configuração Master
</motion.h1>
<motion.p
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.2 }}
className="text-slate-500"
>
Este portal está cru. Vamos realizar o setup inicial da sua organização.
</motion.p>
</div>
{/* Progress Bar */}
<div className="mb-12 relative">
<div className="flex justify-between items-center max-w-2xl mx-auto relative z-10">
{[1, 2, 3].map((i) => (
<div
key={i}
className={`w-10 h-10 rounded-full flex items-center justify-center transition-all duration-500 ${step >= i ? "bg-blue-600 text-white shadow-lg shadow-blue-200" : "bg-white text-slate-400 border border-slate-200"
}`}
>
{step > i ? <CheckCircle2 size={24} /> : <span>{i}</span>}
</div>
))}
</div>
<div className="absolute top-1/2 left-0 w-full h-0.5 bg-slate-200 -translate-y-1/2 max-w-2xl mx-auto right-0" />
<motion.div
className="absolute top-1/2 left-0 h-0.5 bg-blue-600 -translate-y-1/2 max-w-2xl mx-auto right-0 origin-left"
initial={{ scaleX: 0 }}
animate={{ scaleX: (step - 1) / (totalSteps - 1) }}
transition={{ duration: 0.5 }}
/>
</div>
{/* Form Card */}
<Card className="border-0 shadow-xl bg-white/80 backdrop-blur-md">
<CardContent className="p-8 md:p-12 min-h-[500px] flex flex-col justify-between">
<AnimatePresence mode="wait">
{step === 1 && (
<motion.div
key="step1"
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -20 }}
className="space-y-8"
>
<div className="flex items-center gap-4 mb-8">
<div className="p-3 bg-blue-50 text-blue-600 rounded-2xl">
<Building2 size={32} />
</div>
<div>
<h2 className="text-2xl font-semibold text-slate-800">Identidade da Organização</h2>
<p className="text-slate-500">Defina o nome oficial e a logo que aparecerá no portal.</p>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="space-y-2">
<Label htmlFor="name">Nome da Organização</Label>
<Input
id="name"
name="name"
value={formData.name}
onChange={handleChange}
type="text"
placeholder="Ex: Instituto Esperança"
className="h-12"
/>
</div>
<div className="space-y-2">
<Label htmlFor="cnpj">CNPJ (Opcional)</Label>
<Input
id="cnpj"
name="cnpj"
value={formData.cnpj}
onChange={handleChange}
type="text"
placeholder="00.000.000/0000-00"
className="h-12"
/>
</div>
<div className="md:col-span-2 space-y-2">
<Label>Logo da ONG</Label>
<div className="relative border-2 border-dashed border-slate-200 rounded-2xl p-8 flex flex-col items-center justify-center text-slate-400 hover:border-blue-400 hover:bg-blue-50 transition-all cursor-pointer group">
<input
type="file"
className="absolute inset-0 opacity-0 cursor-pointer"
onChange={handleFileUpload}
accept="image/*"
/>
{isUploading ? (
<div className="flex flex-col items-center">
<Loader2 size={48} className="mb-4 text-blue-500 animate-spin" />
<p className="text-sm font-medium">Enviando...</p>
</div>
) : formData.logoUrl ? (
<div className="flex flex-col items-center">
<img src={formData.logoUrl} alt="Logo" className="h-32 object-contain mb-4 rounded-lg" />
<p className="text-sm text-blue-600 font-medium whitespace-nowrap overflow-hidden text-ellipsis max-w-xs"> Logo enviada com sucesso!</p>
</div>
) : (
<>
<CloudUpload size={48} className="mb-4 group-hover:text-blue-500 transition-colors" />
<p className="text-sm font-medium">Arraste sua logo ou clique para buscar</p>
<p className="text-xs mt-1">PNG, JPG até 5MB</p>
</>
)}
</div>
</div>
</div>
</motion.div>
)}
{step === 2 && (
<motion.div
key="step2"
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -20 }}
className="space-y-8"
>
<div className="flex items-center gap-4 mb-8">
<div className="p-3 bg-indigo-50 text-indigo-600 rounded-2xl">
<Palette size={32} />
</div>
<div>
<h2 className="text-2xl font-semibold text-slate-800">Personalização Visual</h2>
<p className="text-slate-500">Adapte o portal às cores da sua marca.</p>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="space-y-2">
<Label>Cor Principal</Label>
<div className="flex gap-4 items-center">
<input
name="primaryColor"
value={formData.primaryColor}
onChange={handleChange}
type="color"
className="w-16 h-16 rounded-xl border-none p-0 overflow-hidden cursor-pointer"
/>
<div className="space-y-1">
<p className="text-sm font-medium text-slate-600">Selecione o tom da marca</p>
<p className="text-xs text-slate-400 italic">Isso aplicará o tema dinâmico.</p>
</div>
</div>
</div>
<div className="space-y-2">
<Label className="italic opacity-50">Ambiente de Instalação</Label>
<div className="h-12 bg-slate-50 border rounded-md text-slate-400 flex items-center gap-2 px-3 cursor-not-allowed">
<Globe size={16} />
Detectado via domínio atual
</div>
</div>
</div>
<div className="p-6 bg-slate-50 rounded-2xl border border-slate-100 space-y-4">
<h3 className="font-semibold text-slate-800 flex items-center gap-2">
<Layout size={18} className="text-blue-500" />
Preview em tempo real
</h3>
<div className="aspect-video bg-white rounded-xl shadow-sm border border-slate-200 flex flex-col items-center justify-center p-4">
<div className="w-full h-8 bg-slate-100 rounded-lg mb-4 flex items-center px-4 gap-2">
<div className="w-4 h-4 rounded-full" style={{ backgroundColor: formData.primaryColor }} />
<div className="w-24 h-2 bg-slate-200 rounded" />
</div>
<Button style={{ backgroundColor: formData.primaryColor }}>Botão Exemplo</Button>
</div>
</div>
</motion.div>
)}
{step === 3 && (
<motion.div
key="step3"
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -20 }}
className="space-y-8"
>
<div className="flex items-center gap-4 mb-8">
<div className="p-3 bg-emerald-50 text-emerald-600 rounded-2xl">
<User size={32} />
</div>
<div>
<h2 className="text-2xl font-semibold text-slate-800">Administrador Master</h2>
<p className="text-slate-500">Crie a conta que terá controle total sobre o portal.</p>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="space-y-2">
<Label htmlFor="adminName">Nome do Responsável</Label>
<Input
id="adminName"
name="adminName"
value={formData.adminName}
onChange={handleChange}
type="text"
placeholder="João Silva"
className="h-12"
/>
</div>
<div className="space-y-2">
<Label htmlFor="adminEmail">E-mail de Acesso</Label>
<Input
id="adminEmail"
name="adminEmail"
value={formData.adminEmail}
onChange={handleChange}
type="email"
placeholder="admin@ong.org"
className="h-12"
/>
</div>
<div className="space-y-2">
<Label htmlFor="adminPassword">Senha</Label>
<Input
id="adminPassword"
name="adminPassword"
value={formData.adminPassword}
onChange={handleChange}
type="password"
placeholder="••••••••"
className="h-12"
/>
</div>
<div className="space-y-2">
<Label htmlFor="confirmPassword">Confirmar Senha</Label>
<Input
id="confirmPassword"
name="confirmPassword"
value={formData.confirmPassword}
onChange={handleChange}
type="password"
placeholder="••••••••"
className="h-12"
/>
</div>
</div>
<Alert>
<CheckCircle2 size={18} />
<AlertDescription>
Ao concluir, o banco de dados será populado e este ambiente será bloqueado para novos setups.
</AlertDescription>
</Alert>
</motion.div>
)}
</AnimatePresence>
<div className="flex justify-between items-center mt-12 pt-8 border-t border-slate-100">
<Button
variant="ghost"
onClick={prevStep}
disabled={isSubmitting}
className={step === 1 ? "invisible" : ""}
>
<ArrowLeft size={18} className="mr-2" />
Voltar
</Button>
<Button
onClick={nextStep}
disabled={isSubmitting || isUploading}
className="h-12 px-6"
>
{isSubmitting ? (
<>
<Loader2 size={18} className="animate-spin mr-2" />
Finalizando...
</>
) : (
<>
{step === totalSteps ? "Finalizar Instalação" : "Próximo Passo"}
<ArrowRight size={18} className="ml-2" />
</>
)}
</Button>
</div>
</CardContent>
</Card>
{/* Footer Support */}
<div className="mt-8 text-center text-slate-400 text-sm">
Portal de Transparência v1.0 Setup de Instalação
</div>
</div>
</div>
);
} }