fix: protect /setup route - redirect to login if organization already exists
This commit is contained in:
406
src/app/setup/SetupClient.tsx
Normal file
406
src/app/setup/SetupClient.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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";
|
||||
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 async function SetupPage() {
|
||||
// Verifica se já existe alguma organização cadastrada
|
||||
const existingOrg = await prisma.organization.findFirst();
|
||||
|
||||
export default function SetupPage() {
|
||||
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;
|
||||
// Se já existe, redireciona para o login (setup já foi feito)
|
||||
if (existingOrg) {
|
||||
redirect("/");
|
||||
}
|
||||
|
||||
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>
|
||||
);
|
||||
// Se não existe, mostra o setup
|
||||
return <SetupClient />;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user