CARONTE v1.0 - Plataforma de Gestão Social
This commit is contained in:
6
frontend/.eslintrc.json
Normal file
6
frontend/.eslintrc.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"extends": "next/core-web-vitals",
|
||||
"rules": {
|
||||
"react/no-unescaped-entities": "off"
|
||||
}
|
||||
}
|
||||
36
frontend/.gitignore
vendored
Normal file
36
frontend/.gitignore
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
.yarn/install-state.gz
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# local env files
|
||||
.env*.local
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
36
frontend/README.md
Normal file
36
frontend/README.md
Normal file
@@ -0,0 +1,36 @@
|
||||
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
|
||||
|
||||
## Getting Started
|
||||
|
||||
First, run the development server:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
# or
|
||||
yarn dev
|
||||
# or
|
||||
pnpm dev
|
||||
# or
|
||||
bun dev
|
||||
```
|
||||
|
||||
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
||||
|
||||
You can start editing the page by modifying `app/page.js`. The page auto-updates as you edit the file.
|
||||
|
||||
This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
|
||||
|
||||
## Learn More
|
||||
|
||||
To learn more about Next.js, take a look at the following resources:
|
||||
|
||||
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
||||
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
||||
|
||||
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
|
||||
|
||||
## Deploy on Vercel
|
||||
|
||||
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
|
||||
|
||||
Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
|
||||
7
frontend/jsconfig.json
Normal file
7
frontend/jsconfig.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
}
|
||||
}
|
||||
4
frontend/next.config.mjs
Normal file
4
frontend/next.config.mjs
Normal file
@@ -0,0 +1,4 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {};
|
||||
|
||||
export default nextConfig;
|
||||
5910
frontend/package-lock.json
generated
Normal file
5910
frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
23
frontend/package.json
Normal file
23
frontend/package.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"name": "frontend",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"lucide-react": "^0.563.0",
|
||||
"next": "14.2.35",
|
||||
"react": "^18",
|
||||
"react-dom": "^18"
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint": "^8",
|
||||
"eslint-config-next": "14.2.35",
|
||||
"postcss": "^8",
|
||||
"tailwindcss": "^3.4.1"
|
||||
}
|
||||
}
|
||||
8
frontend/postcss.config.mjs
Normal file
8
frontend/postcss.config.mjs
Normal file
@@ -0,0 +1,8 @@
|
||||
/** @type {import('postcss-load-config').Config} */
|
||||
const config = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
108
frontend/src/app/dashboard/page.js
Normal file
108
frontend/src/app/dashboard/page.js
Normal file
@@ -0,0 +1,108 @@
|
||||
'use client'
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import Sidebar from '@/components/Sidebar'
|
||||
import { ArrowRight, Clock, AlertTriangle } from 'lucide-react'
|
||||
|
||||
export default function Dashboard() {
|
||||
const router = useRouter()
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
useEffect(() => {
|
||||
if (!localStorage.getItem('token')) { router.push('/login'); return }
|
||||
setLoading(false)
|
||||
}, [router])
|
||||
|
||||
if (loading) return <div className="flex items-center justify-center h-screen bg-[var(--void)]"><span className="text-5xl boat-float">🚣</span></div>
|
||||
|
||||
const cards = [
|
||||
{ label: 'Famílias', value: 3, emoji: '⚱️' },
|
||||
{ label: 'Pendências', value: 12, emoji: '📜' },
|
||||
{ label: 'Benefícios', value: 8, emoji: '🔮' },
|
||||
{ label: 'Pergaminhos', value: 5, emoji: '📄' },
|
||||
]
|
||||
|
||||
const familias = [
|
||||
{ id: 1, nome: 'Família Silva', falecido: 'José Carlos Silva', progresso: 35, pendentes: 8, data: '15/01/2026' },
|
||||
{ id: 2, nome: 'Família Oliveira', falecido: 'Maria Oliveira', progresso: 60, pendentes: 4, data: '20/01/2026' },
|
||||
{ id: 3, nome: 'Família Santos', falecido: 'Antônio Santos', progresso: 15, pendentes: 12, data: '01/02/2026' },
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen bg-[var(--void)]">
|
||||
<Sidebar />
|
||||
<main className="ml-60 flex-1 p-8">
|
||||
{/* Header */}
|
||||
<div className="mb-10 fade-in">
|
||||
<p className="text-[10px] tracking-[0.4em] text-[var(--gold-dim)] uppercase font-myth mb-2">🏛️ Ágora</p>
|
||||
<h1 className="font-myth text-2xl font-bold text-[var(--marble)] tracking-wider">Visão do Submundo</h1>
|
||||
<div className="ornament-line-simple max-w-[200px] mt-3" />
|
||||
</div>
|
||||
|
||||
{/* Stats */}
|
||||
<div className="grid grid-cols-4 gap-4 mb-8">
|
||||
{cards.map((c, i) => (
|
||||
<div key={i} className="card-hades p-5 text-center group hover:scale-[1.02] transition-all duration-300">
|
||||
<span className="text-2xl block mb-2">{c.emoji}</span>
|
||||
<p className="font-myth-decorative text-2xl gold-text">{c.value}</p>
|
||||
<p className="text-[var(--smoke)] text-[10px] tracking-[0.15em] font-myth uppercase mt-1">{c.label}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Treasure */}
|
||||
<div className="card-hades p-6 mb-8 relative overflow-hidden group">
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-[var(--ember)]/[0.03] to-transparent" />
|
||||
<div className="flex items-center gap-5 relative z-10">
|
||||
<span className="text-4xl group-hover:scale-110 transition-transform">🔱</span>
|
||||
<div>
|
||||
<p className="text-[10px] tracking-[0.3em] text-[var(--gold-dim)] uppercase font-myth">Tesouro a Recuperar</p>
|
||||
<p className="text-3xl font-myth-decorative gold-text mt-1">R$ 45.000</p>
|
||||
<p className="text-[var(--smoke)] text-xs mt-1">em benefícios identificados para suas famílias</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Families header */}
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<div>
|
||||
<p className="text-[10px] tracking-[0.4em] text-[var(--gold-dim)] uppercase font-myth mb-1">⚱️ Núcleos</p>
|
||||
<h2 className="font-myth text-lg text-[var(--marble)] tracking-wider">Famílias</h2>
|
||||
</div>
|
||||
<button className="btn-outline px-4 py-2 text-[10px]">+ NOVA FAMÍLIA</button>
|
||||
</div>
|
||||
|
||||
{/* Families list */}
|
||||
<div className="space-y-3">
|
||||
{familias.map((f) => (
|
||||
<div key={f.id} onClick={() => router.push(`/familia/${f.id}`)}
|
||||
className="card-hades p-5 cursor-pointer group hover:scale-[1.005] transition-all duration-300">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-lg">⚱️</span>
|
||||
<h3 className="font-myth text-sm font-semibold text-[var(--marble)] tracking-wider">{f.nome}</h3>
|
||||
{f.pendentes > 5 && (
|
||||
<span className="flex items-center gap-1 text-[9px] text-[var(--ember)] bg-[var(--ember)]/10 px-2 py-0.5 font-myth tracking-wider">
|
||||
<AlertTriangle size={9} /> {f.pendentes}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-[var(--ash)] text-xs ml-8 mt-1">Falecido(a): {f.falecido}</p>
|
||||
<div className="flex items-center gap-4 mt-3 ml-8">
|
||||
<div className="flex-1 max-w-[200px] progress-styx h-1.5">
|
||||
<div className="progress-styx-fill h-full transition-all" style={{ width: `${f.progresso}%` }} />
|
||||
</div>
|
||||
<span className="text-xs gold-text font-myth font-bold">{f.progresso}%</span>
|
||||
<span className="text-[10px] text-[var(--smoke)] flex items-center gap-1"><Clock size={9} /> {f.data}</span>
|
||||
</div>
|
||||
</div>
|
||||
<ArrowRight className="text-[var(--smoke)] group-hover:text-[var(--gold)] transition" size={16} />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
104
frontend/src/app/familia/[id]/beneficios/page.js
Normal file
104
frontend/src/app/familia/[id]/beneficios/page.js
Normal file
@@ -0,0 +1,104 @@
|
||||
'use client'
|
||||
import { useState } from 'react'
|
||||
import { useParams, useRouter } from 'next/navigation'
|
||||
import Sidebar from '@/components/Sidebar'
|
||||
import { Search, DollarSign, Clock, CheckCircle2, AlertTriangle, Loader2 } from 'lucide-react'
|
||||
|
||||
const beneficiosData = [
|
||||
{ id: 1, tipo: 'FGTS', instituicao: 'Caixa Econômica Federal', valor_estimado: 18500, valor_sacado: 0, status: 'identificado', prazo: 'Sem prazo', icon: '🏦', desc: 'Saldo de FGTS do último vínculo empregatício' },
|
||||
{ id: 2, tipo: 'PIS/PASEP', instituicao: 'Caixa Econômica Federal', valor_estimado: 3200, valor_sacado: 0, status: 'identificado', prazo: '5 anos', icon: '💳', desc: 'Abono salarial e cotas do PIS acumuladas' },
|
||||
{ id: 3, tipo: 'Pensão por Morte', instituicao: 'INSS', valor_estimado: 2800, valor_sacado: 0, status: 'em_processo', prazo: '90 dias (retroativa)', icon: '🏛️', desc: 'Benefício mensal para dependentes — R$2.800/mês' },
|
||||
{ id: 4, tipo: 'Seguro de Vida', instituicao: 'Bradesco Seguros', valor_estimado: 15000, valor_sacado: 0, status: 'identificado', prazo: '3 anos', icon: '🛡️', desc: 'Apólice de seguro de vida em grupo (empregador)' },
|
||||
{ id: 5, tipo: 'Restituição IR', instituicao: 'Receita Federal', valor_estimado: 4300, valor_sacado: 4300, status: 'sacado', prazo: '5 anos', icon: '📄', desc: 'Restituição de imposto de renda pendente' },
|
||||
{ id: 6, tipo: 'DPVAT', instituicao: 'Seguradora Líder', valor_estimado: 0, valor_sacado: 0, status: 'nao_aplicavel', prazo: '3 anos', icon: '🚗', desc: 'Não aplicável — causa da morte não foi acidente de trânsito' },
|
||||
]
|
||||
|
||||
const statusConfig = {
|
||||
identificado: { label: 'Identificado', color: 'text-blue-400', bg: 'bg-blue-500/10 border-blue-500/30' },
|
||||
em_processo: { label: 'Em Processo', color: 'text-yellow-400', bg: 'bg-yellow-500/10 border-yellow-500/30' },
|
||||
sacado: { label: 'Sacado ✓', color: 'text-green-400', bg: 'bg-green-500/10 border-green-500/30' },
|
||||
nao_aplicavel: { label: 'N/A', color: 'text-gray-500', bg: 'bg-white/5 border-white/10' },
|
||||
}
|
||||
|
||||
export default function BeneficiosPage() {
|
||||
const router = useRouter()
|
||||
const params = useParams()
|
||||
const [beneficios, setBeneficios] = useState(beneficiosData)
|
||||
const [scanning, setScanning] = useState(false)
|
||||
|
||||
const totalEstimado = beneficios.filter(b => b.status !== 'nao_aplicavel').reduce((a, b) => a + b.valor_estimado, 0)
|
||||
const totalSacado = beneficios.reduce((a, b) => a + b.valor_sacado, 0)
|
||||
const aProcurar = totalEstimado - totalSacado
|
||||
|
||||
const scan = () => {
|
||||
setScanning(true)
|
||||
setTimeout(() => {
|
||||
setScanning(false)
|
||||
alert('✅ Scan completo! Nenhum novo benefício encontrado.')
|
||||
}, 3000)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen bg-[#0a0a0f]">
|
||||
<Sidebar />
|
||||
<main className="ml-64 flex-1 p-8">
|
||||
<button onClick={() => router.push(`/familia/${params.id}`)} className="text-gray-400 hover:text-white text-sm mb-6">← Voltar à Família</button>
|
||||
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-white">Scanner de Benefícios</h1>
|
||||
<p className="text-gray-400 mt-1">Benefícios identificados para os herdeiros</p>
|
||||
</div>
|
||||
<button onClick={scan} disabled={scanning}
|
||||
className="flex items-center gap-2 px-6 py-3 bg-gradient-to-r from-purple-600 to-purple-500 text-white rounded-xl hover:from-purple-500 hover:to-purple-400 transition disabled:opacity-50 font-medium">
|
||||
{scanning ? <><Loader2 size={18} className="animate-spin" /> Escaneando...</> : <><Search size={18} /> Escanear Benefícios</>}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-8">
|
||||
<div className="glass bg-gradient-to-br from-blue-500/10 to-blue-600/5 border-blue-500/20 p-6">
|
||||
<p className="text-gray-400 text-sm">Total Estimado</p>
|
||||
<p className="text-2xl font-bold text-blue-400 mt-1">R$ {totalEstimado.toLocaleString('pt-BR')}</p>
|
||||
</div>
|
||||
<div className="glass bg-gradient-to-br from-green-500/10 to-green-600/5 border-green-500/20 p-6">
|
||||
<p className="text-gray-400 text-sm">Já Sacado</p>
|
||||
<p className="text-2xl font-bold text-green-400 mt-1">R$ {totalSacado.toLocaleString('pt-BR')}</p>
|
||||
</div>
|
||||
<div className="glass bg-gradient-to-br from-yellow-500/10 to-yellow-600/5 border-yellow-500/20 p-6">
|
||||
<p className="text-gray-400 text-sm">A Recuperar</p>
|
||||
<p className="text-2xl font-bold text-yellow-400 mt-1">R$ {aProcurar.toLocaleString('pt-BR')}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
{beneficios.map((b) => {
|
||||
const cfg = statusConfig[b.status]
|
||||
return (
|
||||
<div key={b.id} className={`glass p-6 ${b.status === 'nao_aplicavel' ? 'opacity-40' : ''}`}>
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex items-start gap-4">
|
||||
<span className="text-3xl">{b.icon}</span>
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-white">{b.tipo}</h3>
|
||||
<p className="text-gray-400 text-sm">{b.instituicao}</p>
|
||||
<p className="text-gray-500 text-xs mt-1">{b.desc}</p>
|
||||
<div className="flex items-center gap-4 mt-3">
|
||||
{b.valor_estimado > 0 && (
|
||||
<span className="flex items-center gap-1 text-sm text-yellow-400">
|
||||
<DollarSign size={14} /> R$ {b.valor_estimado.toLocaleString('pt-BR')}
|
||||
</span>
|
||||
)}
|
||||
<span className="flex items-center gap-1 text-xs text-gray-500"><Clock size={12} /> Prazo: {b.prazo}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<span className={`text-xs px-3 py-1 rounded-full border ${cfg.bg} ${cfg.color}`}>{cfg.label}</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
115
frontend/src/app/familia/[id]/checklist/page.js
Normal file
115
frontend/src/app/familia/[id]/checklist/page.js
Normal file
@@ -0,0 +1,115 @@
|
||||
'use client'
|
||||
import { useState } from 'react'
|
||||
import { useParams, useRouter } from 'next/navigation'
|
||||
import Sidebar from '@/components/Sidebar'
|
||||
import { CheckCircle2, Circle, Clock, AlertTriangle, Lock, Star } from 'lucide-react'
|
||||
|
||||
const checklistData = [
|
||||
{ fase: '⚡ Imediato (24-48h)', items: [
|
||||
{ id: 1, titulo: 'Obter Certidão de Óbito', desc: 'Registrar no cartório mais próximo', status: 'concluido', prazo: null },
|
||||
{ id: 2, titulo: 'Comunicar ao empregador', desc: 'Solicitar rescisão e verbas trabalhistas', status: 'concluido', prazo: null },
|
||||
{ id: 3, titulo: 'Comunicar ao banco', desc: 'Bloquear contas e informar o falecimento', status: 'em_andamento', prazo: '2026-02-15' },
|
||||
{ id: 4, titulo: 'Cancelar assinaturas recorrentes', desc: 'Streaming, seguros, planos de saúde', status: 'pendente', prazo: null },
|
||||
]},
|
||||
{ fase: '📋 1ª Semana', items: [
|
||||
{ id: 5, titulo: 'Requerer Pensão por Morte (INSS)', desc: 'Prazo de 90 dias para retroatividade', status: 'em_andamento', prazo: '2026-04-10', urgente: true },
|
||||
{ id: 6, titulo: 'Sacar FGTS do falecido', desc: 'Levar certidão de óbito à Caixa', status: 'pendente', prazo: null },
|
||||
{ id: 7, titulo: 'Sacar PIS/PASEP', desc: 'Caixa (PIS) ou Banco do Brasil (PASEP)', status: 'pendente', prazo: null },
|
||||
{ id: 8, titulo: 'Consultar seguros de vida', desc: 'Verificar apólices ativas na SUSEP', status: 'pendente', prazo: null },
|
||||
]},
|
||||
{ fase: '📅 30 Dias', items: [
|
||||
{ id: 9, titulo: 'Transferência de veículos', desc: 'DETRAN - transferir para herdeiros', status: 'bloqueado', prazo: null, depende: 'Inventário' },
|
||||
{ id: 10, titulo: 'Comunicar Receita Federal', desc: 'Declaração final de espólio', status: 'pendente', prazo: '2026-04-30' },
|
||||
{ id: 11, titulo: 'Consultar restituição IR', desc: 'Verificar se há restituição pendente', status: 'pendente', prazo: null },
|
||||
]},
|
||||
{ fase: '⚖️ 60 Dias (Inventário)', items: [
|
||||
{ id: 12, titulo: 'Iniciar inventário', desc: 'Judicial ou extrajudicial (cartório)', status: 'pendente', prazo: '2026-03-10', urgente: true },
|
||||
{ id: 13, titulo: 'Avaliação de bens', desc: 'Imóveis, veículos, investimentos', status: 'bloqueado', prazo: null, depende: 'Inventário iniciado' },
|
||||
{ id: 14, titulo: 'Cálculo ITCMD', desc: 'Imposto sobre herança (varia por UF)', status: 'bloqueado', prazo: null, depende: 'Avaliação' },
|
||||
{ id: 15, titulo: 'Partilha de bens', desc: 'Divisão entre herdeiros', status: 'bloqueado', prazo: null, depende: 'ITCMD pago' },
|
||||
]}
|
||||
]
|
||||
|
||||
const statusConfig = {
|
||||
concluido: { icon: <CheckCircle2 size={20} />, color: 'text-green-400', bg: 'bg-green-500/10', label: 'Concluído' },
|
||||
em_andamento: { icon: <Clock size={20} />, color: 'text-yellow-400', bg: 'bg-yellow-500/10', label: 'Em andamento' },
|
||||
pendente: { icon: <Circle size={20} />, color: 'text-gray-400', bg: 'bg-white/5', label: 'Pendente' },
|
||||
bloqueado: { icon: <Lock size={20} />, color: 'text-red-400', bg: 'bg-red-500/10', label: 'Bloqueado' },
|
||||
}
|
||||
|
||||
export default function ChecklistPage() {
|
||||
const router = useRouter()
|
||||
const params = useParams()
|
||||
const [checklist, setChecklist] = useState(checklistData)
|
||||
|
||||
const toggleItem = (faseIdx, itemIdx) => {
|
||||
const updated = [...checklist]
|
||||
const item = updated[faseIdx].items[itemIdx]
|
||||
if (item.status === 'bloqueado') return
|
||||
const cycle = { pendente: 'em_andamento', em_andamento: 'concluido', concluido: 'pendente' }
|
||||
item.status = cycle[item.status] || 'pendente'
|
||||
setChecklist(updated)
|
||||
}
|
||||
|
||||
const total = checklist.reduce((a, f) => a + f.items.length, 0)
|
||||
const done = checklist.reduce((a, f) => a + f.items.filter(i => i.status === 'concluido').length, 0)
|
||||
const pct = Math.round((done / total) * 100)
|
||||
|
||||
const nextStep = checklist.flatMap(f => f.items).find(i => i.status === 'pendente' || i.status === 'em_andamento')
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen bg-[#0a0a0f]">
|
||||
<Sidebar />
|
||||
<main className="ml-64 flex-1 p-8">
|
||||
<button onClick={() => router.push(`/familia/${params.id}`)} className="text-gray-400 hover:text-white text-sm mb-6">← Voltar à Família</button>
|
||||
|
||||
<h1 className="text-3xl font-bold text-white mb-2">Checklist Burocrático</h1>
|
||||
<p className="text-gray-400 mb-6">{done} de {total} itens concluídos ({pct}%)</p>
|
||||
|
||||
<div className="w-full bg-white/5 rounded-full h-3 mb-8">
|
||||
<div className="bg-gradient-to-r from-purple-600 to-green-400 h-3 rounded-full transition-all" style={{ width: `${pct}%` }} />
|
||||
</div>
|
||||
|
||||
{nextStep && (
|
||||
<div className="glass bg-gradient-to-r from-purple-500/10 to-blue-500/10 border-purple-500/20 p-6 mb-8">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<Star className="text-yellow-400" size={20} />
|
||||
<span className="text-sm text-yellow-400 font-medium">PRÓXIMO PASSO SUGERIDO</span>
|
||||
</div>
|
||||
<p className="text-white text-lg font-semibold">{nextStep.titulo}</p>
|
||||
<p className="text-gray-400 text-sm mt-1">{nextStep.desc}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-8">
|
||||
{checklist.map((fase, fi) => (
|
||||
<div key={fi}>
|
||||
<h2 className="text-lg font-bold text-white mb-4">{fase.fase}</h2>
|
||||
<div className="space-y-3">
|
||||
{fase.items.map((item, ii) => {
|
||||
const cfg = statusConfig[item.status]
|
||||
return (
|
||||
<div key={item.id} onClick={() => toggleItem(fi, ii)}
|
||||
className={`glass p-4 flex items-center gap-4 cursor-pointer hover:bg-white/10 transition ${item.status === 'bloqueado' ? 'opacity-50 cursor-not-allowed' : ''}`}>
|
||||
<div className={cfg.color}>{cfg.icon}</div>
|
||||
<div className="flex-1">
|
||||
<p className={`font-medium ${item.status === 'concluido' ? 'text-gray-500 line-through' : 'text-white'}`}>{item.titulo}</p>
|
||||
<p className="text-gray-500 text-sm">{item.desc}</p>
|
||||
{item.depende && <p className="text-red-400/60 text-xs mt-1">🔒 Depende de: {item.depende}</p>}
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
{item.urgente && <span className="flex items-center gap-1 text-xs text-yellow-400 bg-yellow-500/10 px-2 py-1 rounded-full"><AlertTriangle size={12} /> Urgente</span>}
|
||||
{item.prazo && <span className="text-xs text-gray-500 flex items-center gap-1"><Clock size={12} /> {new Date(item.prazo).toLocaleDateString('pt-BR')}</span>}
|
||||
<span className={`text-xs px-2 py-1 rounded-full ${cfg.bg} ${cfg.color}`}>{cfg.label}</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
110
frontend/src/app/familia/[id]/documentos/page.js
Normal file
110
frontend/src/app/familia/[id]/documentos/page.js
Normal file
@@ -0,0 +1,110 @@
|
||||
'use client'
|
||||
import { useState } from 'react'
|
||||
import { useParams, useRouter } from 'next/navigation'
|
||||
import Sidebar from '@/components/Sidebar'
|
||||
import { FileText, Download, Upload, Plus, Clock, CheckCircle2, File } from 'lucide-react'
|
||||
|
||||
const docsData = [
|
||||
{ id: 1, nome: 'Procuração para Inventariante', tipo: 'gerado', categoria: 'Procuração', data: '2026-02-01', tamanho: '45 KB' },
|
||||
{ id: 2, nome: 'Requerimento Saque FGTS', tipo: 'gerado', categoria: 'Requerimento', data: '2026-02-03', tamanho: '38 KB' },
|
||||
{ id: 3, nome: 'Requerimento Pensão por Morte', tipo: 'gerado', categoria: 'Requerimento', data: '2026-02-05', tamanho: '52 KB' },
|
||||
{ id: 4, nome: 'Certidão de Óbito', tipo: 'upload', categoria: 'Certidão', data: '2026-01-12', tamanho: '1.2 MB' },
|
||||
{ id: 5, nome: 'RG do Falecido', tipo: 'upload', categoria: 'Documento Pessoal', data: '2026-01-12', tamanho: '890 KB' },
|
||||
]
|
||||
|
||||
const templatesDocs = [
|
||||
{ id: 'procuracao', nome: 'Procuração para Herdeiro', desc: 'Procuração para representar a família no inventário' },
|
||||
{ id: 'fgts', nome: 'Requerimento Saque FGTS', desc: 'Formulário para saque do FGTS por falecimento' },
|
||||
{ id: 'pensao', nome: 'Requerimento Pensão por Morte', desc: 'Pedido de pensão por morte ao INSS' },
|
||||
{ id: 'alvara', nome: 'Petição de Alvará Judicial', desc: 'Para liberação de valores sem inventário (Lei 6.858/80)' },
|
||||
{ id: 'comunicacao', nome: 'Comunicação de Óbito ao Banco', desc: 'Carta formal para instituição financeira' },
|
||||
]
|
||||
|
||||
export default function DocumentosPage() {
|
||||
const router = useRouter()
|
||||
const params = useParams()
|
||||
const [docs, setDocs] = useState(docsData)
|
||||
const [showGerador, setShowGerador] = useState(false)
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen bg-[#0a0a0f]">
|
||||
<Sidebar />
|
||||
<main className="ml-64 flex-1 p-8">
|
||||
<button onClick={() => router.push(`/familia/${params.id}`)} className="text-gray-400 hover:text-white text-sm mb-6">← Voltar à Família</button>
|
||||
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-white">Documentos</h1>
|
||||
<p className="text-gray-400 mt-1">{docs.length} documentos</p>
|
||||
</div>
|
||||
<div className="flex gap-3">
|
||||
<button className="flex items-center gap-2 px-4 py-2 bg-white/5 text-gray-300 rounded-xl hover:bg-white/10 transition border border-white/10">
|
||||
<Upload size={16} /> Upload
|
||||
</button>
|
||||
<button onClick={() => setShowGerador(!showGerador)} className="flex items-center gap-2 px-4 py-2 bg-gradient-to-r from-purple-600 to-purple-500 text-white rounded-xl hover:from-purple-500 hover:to-purple-400 transition font-medium">
|
||||
<Plus size={16} /> Gerar Documento
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{showGerador && (
|
||||
<div className="glass bg-gradient-to-r from-purple-500/5 to-blue-500/5 border-purple-500/20 p-6 mb-8">
|
||||
<h2 className="text-lg font-semibold text-white mb-4">📄 Gerador de Documentos</h2>
|
||||
<p className="text-gray-400 text-sm mb-4">Selecione um modelo para gerar automaticamente:</p>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
{templatesDocs.map((t) => (
|
||||
<div key={t.id} onClick={() => { alert(`✅ Documento "${t.nome}" gerado com sucesso!`); setShowGerador(false) }}
|
||||
className="p-4 bg-white/5 rounded-xl cursor-pointer hover:bg-purple-500/10 hover:border-purple-500/30 border border-white/5 transition">
|
||||
<p className="text-white font-medium">{t.nome}</p>
|
||||
<p className="text-gray-500 text-xs mt-1">{t.desc}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 mb-8">
|
||||
<div className="glass p-4 flex items-center gap-3">
|
||||
<div className="w-10 h-10 bg-purple-500/20 rounded-xl flex items-center justify-center text-purple-400"><FileText size={20} /></div>
|
||||
<div>
|
||||
<p className="text-2xl font-bold text-white">{docs.filter(d => d.tipo === 'gerado').length}</p>
|
||||
<p className="text-gray-500 text-xs">Gerados pelo sistema</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="glass p-4 flex items-center gap-3">
|
||||
<div className="w-10 h-10 bg-blue-500/20 rounded-xl flex items-center justify-center text-blue-400"><Upload size={20} /></div>
|
||||
<div>
|
||||
<p className="text-2xl font-bold text-white">{docs.filter(d => d.tipo === 'upload').length}</p>
|
||||
<p className="text-gray-500 text-xs">Enviados por você</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
{docs.map((doc) => (
|
||||
<div key={doc.id} className="glass p-4 flex items-center justify-between hover:bg-white/10 transition">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className={`w-10 h-10 rounded-xl flex items-center justify-center ${doc.tipo === 'gerado' ? 'bg-purple-500/20 text-purple-400' : 'bg-blue-500/20 text-blue-400'}`}>
|
||||
{doc.tipo === 'gerado' ? <FileText size={20} /> : <File size={20} />}
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-white font-medium">{doc.nome}</p>
|
||||
<div className="flex items-center gap-3 mt-1">
|
||||
<span className="text-xs text-gray-500">{doc.categoria}</span>
|
||||
<span className="text-xs text-gray-600">•</span>
|
||||
<span className="text-xs text-gray-500 flex items-center gap-1"><Clock size={10} /> {new Date(doc.data).toLocaleDateString('pt-BR')}</span>
|
||||
<span className="text-xs text-gray-600">•</span>
|
||||
<span className="text-xs text-gray-500">{doc.tamanho}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button className="flex items-center gap-2 px-3 py-2 bg-white/5 text-gray-300 rounded-lg hover:bg-white/10 transition text-sm">
|
||||
<Download size={14} /> Baixar
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
96
frontend/src/app/familia/[id]/page.js
Normal file
96
frontend/src/app/familia/[id]/page.js
Normal file
@@ -0,0 +1,96 @@
|
||||
'use client'
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useRouter, useParams } from 'next/navigation'
|
||||
import Sidebar from '@/components/Sidebar'
|
||||
import { User, CheckSquare, Search, FileText, ArrowRight, Clock, MapPin, Briefcase } from 'lucide-react'
|
||||
|
||||
const mockFamilias = {
|
||||
1: { nome: 'Família Silva', falecido: { nome: 'José Carlos Silva', cpf: '***.***.***-45', nascimento: '1955-03-12', obito: '2026-01-10', emprego: true, veiculo: true, imovel: true, previdencia: false }, membros: [{ nome: 'Ana Silva', parentesco: 'Cônjuge', papel: 'Inventariante' }, { nome: 'Pedro Silva', parentesco: 'Filho', papel: 'Herdeiro' }, { nome: 'Carla Silva', parentesco: 'Filha', papel: 'Herdeira' }], progresso: 35 },
|
||||
2: { nome: 'Família Oliveira', falecido: { nome: 'Maria Oliveira', cpf: '***.***.***-78', nascimento: '1948-07-22', obito: '2026-01-18', emprego: false, veiculo: false, imovel: true, previdencia: true }, membros: [{ nome: 'Carlos Oliveira', parentesco: 'Cônjuge', papel: 'Inventariante' }, { nome: 'Lucia Oliveira', parentesco: 'Filha', papel: 'Herdeira' }], progresso: 60 },
|
||||
3: { nome: 'Família Santos', falecido: { nome: 'Antônio Santos', cpf: '***.***.***-12', nascimento: '1960-11-05', obito: '2026-01-28', emprego: true, veiculo: true, imovel: true, previdencia: true }, membros: [{ nome: 'Rosa Santos', parentesco: 'Cônjuge', papel: 'Inventariante' }, { nome: 'Marcos Santos', parentesco: 'Filho', papel: 'Herdeiro' }, { nome: 'Julia Santos', parentesco: 'Filha', papel: 'Herdeira' }, { nome: 'Dr. Roberto Lima', parentesco: '-', papel: 'Advogado' }], progresso: 15 }
|
||||
}
|
||||
|
||||
export default function FamiliaPage() {
|
||||
const router = useRouter()
|
||||
const params = useParams()
|
||||
const id = params.id
|
||||
const familia = mockFamilias[id] || mockFamilias[1]
|
||||
const f = familia.falecido
|
||||
|
||||
const tabs = [
|
||||
{ label: 'Checklist', icon: <CheckSquare size={18} />, href: `/familia/${id}/checklist` },
|
||||
{ label: 'Benefícios', icon: <Search size={18} />, href: `/familia/${id}/beneficios` },
|
||||
{ label: 'Documentos', icon: <FileText size={18} />, href: `/familia/${id}/documentos` },
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen bg-[#0a0a0f]">
|
||||
<Sidebar />
|
||||
<main className="ml-64 flex-1 p-8">
|
||||
<button onClick={() => router.push('/dashboard')} className="text-gray-400 hover:text-white text-sm mb-6 flex items-center gap-1">← Voltar ao Dashboard</button>
|
||||
|
||||
<div className="flex items-center gap-4 mb-8">
|
||||
<div className="w-16 h-16 bg-purple-500/20 rounded-2xl flex items-center justify-center text-3xl">🚣</div>
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-white">{familia.nome}</h1>
|
||||
<p className="text-gray-400">Progresso geral: <span className="text-purple-400 font-semibold">{familia.progresso}%</span></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="w-full bg-white/5 rounded-full h-3 mb-8">
|
||||
<div className="bg-gradient-to-r from-purple-600 to-purple-400 h-3 rounded-full transition-all" style={{ width: `${familia.progresso}%` }} />
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8">
|
||||
<div className="glass p-6">
|
||||
<h2 className="text-lg font-semibold text-white mb-4 flex items-center gap-2"><User size={20} className="text-purple-400" /> Dados do Falecido</h2>
|
||||
<div className="space-y-3 text-sm">
|
||||
<div className="flex justify-between"><span className="text-gray-400">Nome</span><span className="text-white">{f.nome}</span></div>
|
||||
<div className="flex justify-between"><span className="text-gray-400">CPF</span><span className="text-white font-mono">{f.cpf}</span></div>
|
||||
<div className="flex justify-between"><span className="text-gray-400">Nascimento</span><span className="text-white">{new Date(f.nascimento).toLocaleDateString('pt-BR')}</span></div>
|
||||
<div className="flex justify-between"><span className="text-gray-400">Óbito</span><span className="text-white">{new Date(f.obito).toLocaleDateString('pt-BR')}</span></div>
|
||||
<div className="border-t border-white/5 pt-3 mt-3">
|
||||
<p className="text-gray-400 mb-2">Situação:</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{f.emprego && <span className="px-3 py-1 bg-blue-500/10 text-blue-400 rounded-full text-xs flex items-center gap-1"><Briefcase size={12} /> Empregado</span>}
|
||||
{f.veiculo && <span className="px-3 py-1 bg-green-500/10 text-green-400 rounded-full text-xs">🚗 Veículo</span>}
|
||||
{f.imovel && <span className="px-3 py-1 bg-yellow-500/10 text-yellow-400 rounded-full text-xs">🏠 Imóvel</span>}
|
||||
{f.previdencia && <span className="px-3 py-1 bg-purple-500/10 text-purple-400 rounded-full text-xs">💰 Previdência</span>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="glass p-6">
|
||||
<h2 className="text-lg font-semibold text-white mb-4 flex items-center gap-2"><User size={20} className="text-purple-400" /> Membros da Família</h2>
|
||||
<div className="space-y-3">
|
||||
{familia.membros.map((m, i) => (
|
||||
<div key={i} className="flex items-center justify-between p-3 bg-white/5 rounded-xl">
|
||||
<div>
|
||||
<p className="text-white font-medium">{m.nome}</p>
|
||||
<p className="text-gray-500 text-xs">{m.parentesco}</p>
|
||||
</div>
|
||||
<span className={`text-xs px-3 py-1 rounded-full ${m.papel === 'Inventariante' ? 'bg-purple-500/20 text-purple-400' : m.papel === 'Advogado' ? 'bg-blue-500/20 text-blue-400' : 'bg-white/10 text-gray-300'}`}>{m.papel}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<button className="mt-4 w-full py-2 border border-dashed border-white/10 rounded-xl text-gray-500 hover:text-white hover:border-purple-500/30 transition text-sm">+ Convidar Membro</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2 className="text-xl font-bold text-white mb-4">Módulos</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
{tabs.map((t, i) => (
|
||||
<div key={i} onClick={() => router.push(t.href)} className="glass p-6 cursor-pointer hover:bg-white/10 transition group flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-purple-400">{t.icon}</span>
|
||||
<span className="text-white font-medium">{t.label}</span>
|
||||
</div>
|
||||
<ArrowRight className="text-gray-600 group-hover:text-purple-400 transition" size={18} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
BIN
frontend/src/app/favicon.ico
Normal file
BIN
frontend/src/app/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
BIN
frontend/src/app/fonts/GeistMonoVF.woff
Normal file
BIN
frontend/src/app/fonts/GeistMonoVF.woff
Normal file
Binary file not shown.
BIN
frontend/src/app/fonts/GeistVF.woff
Normal file
BIN
frontend/src/app/fonts/GeistVF.woff
Normal file
Binary file not shown.
239
frontend/src/app/globals.css
Normal file
239
frontend/src/app/globals.css
Normal file
@@ -0,0 +1,239 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@import url('https://fonts.googleapis.com/css2?family=Cinzel+Decorative:wght@400;700;900&family=Cinzel:wght@400;500;600;700;800;900&family=Cormorant+Garamond:ital,wght@0,300;0,400;0,600;0,700;1,300;1,400&family=Inter:wght@300;400;500;600;700&display=swap');
|
||||
|
||||
:root {
|
||||
--void: #050508;
|
||||
--abyss: #08080f;
|
||||
--obsidian: #0c0c14;
|
||||
--onyx: #12121c;
|
||||
--gold: #b8943f;
|
||||
--gold-bright: #d4ad4a;
|
||||
--gold-glow: #e8c55a;
|
||||
--gold-dim: #7a6228;
|
||||
--ember: #c44b1a;
|
||||
--ember-glow: #ff6b2b;
|
||||
--soul: #4a7aff;
|
||||
--soul-dim: #2a4a9a;
|
||||
--bone: #c8c0b0;
|
||||
--ash: #6a645a;
|
||||
--smoke: #3a3630;
|
||||
--marble: #e0dcd4;
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
|
||||
body {
|
||||
background: var(--void);
|
||||
color: var(--bone);
|
||||
font-family: 'Inter', sans-serif;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.font-myth { font-family: 'Cinzel', serif; }
|
||||
.font-myth-decorative { font-family: 'Cinzel Decorative', serif; }
|
||||
.font-elegant { font-family: 'Cormorant Garamond', serif; }
|
||||
|
||||
/* === GOLD TEXT === */
|
||||
.gold-text {
|
||||
background: linear-gradient(135deg, var(--gold-dim) 0%, var(--gold) 30%, var(--gold-glow) 50%, var(--gold) 70%, var(--gold-dim) 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
/* === UNDERWORLD BACKGROUNDS === */
|
||||
.bg-underworld {
|
||||
background:
|
||||
radial-gradient(ellipse 120% 60% at 50% 110%, rgba(74, 40, 10, 0.08) 0%, transparent 70%),
|
||||
radial-gradient(ellipse 80% 40% at 20% 80%, rgba(196, 75, 26, 0.04) 0%, transparent 60%),
|
||||
radial-gradient(ellipse 80% 40% at 80% 80%, rgba(196, 75, 26, 0.04) 0%, transparent 60%),
|
||||
linear-gradient(180deg, var(--void) 0%, var(--abyss) 40%, #0a0a12 70%, #0d0910 100%);
|
||||
}
|
||||
|
||||
.bg-styx {
|
||||
background:
|
||||
radial-gradient(ellipse 100% 30% at 50% 100%, rgba(42, 74, 154, 0.1) 0%, transparent 70%),
|
||||
linear-gradient(180deg, transparent 60%, rgba(42, 74, 154, 0.03) 100%);
|
||||
}
|
||||
|
||||
/* === EMBER / FIRE GLOW === */
|
||||
@keyframes emberPulse {
|
||||
0%, 100% { opacity: 0.4; filter: blur(30px); }
|
||||
50% { opacity: 0.7; filter: blur(40px); }
|
||||
}
|
||||
|
||||
@keyframes emberFloat {
|
||||
0% { transform: translateY(0) scale(1); opacity: 0.6; }
|
||||
50% { transform: translateY(-20px) scale(1.1); opacity: 0.9; }
|
||||
100% { transform: translateY(-40px) scale(0.8); opacity: 0; }
|
||||
}
|
||||
|
||||
.ember-particle {
|
||||
position: absolute;
|
||||
width: 3px;
|
||||
height: 3px;
|
||||
background: var(--ember-glow);
|
||||
border-radius: 50%;
|
||||
animation: emberFloat 4s ease-out infinite;
|
||||
}
|
||||
|
||||
/* === GREEK ORNAMENTS === */
|
||||
.ornament-line {
|
||||
height: 1px;
|
||||
background: linear-gradient(90deg, transparent, var(--gold-dim), var(--gold), var(--gold-dim), transparent);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.ornament-line::after {
|
||||
content: '◆';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
color: var(--gold);
|
||||
font-size: 8px;
|
||||
background: var(--void);
|
||||
padding: 0 12px;
|
||||
}
|
||||
|
||||
.ornament-line-simple {
|
||||
height: 1px;
|
||||
background: linear-gradient(90deg, transparent, rgba(184, 148, 63, 0.3), transparent);
|
||||
}
|
||||
|
||||
/* === CARDS === */
|
||||
.card-hades {
|
||||
background: linear-gradient(180deg, rgba(18, 18, 28, 0.95), rgba(8, 8, 15, 0.98));
|
||||
border: 1px solid rgba(184, 148, 63, 0.08);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.card-hades::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 1px;
|
||||
background: linear-gradient(90deg, transparent, rgba(184, 148, 63, 0.3), transparent);
|
||||
}
|
||||
|
||||
.card-hades:hover {
|
||||
border-color: rgba(184, 148, 63, 0.2);
|
||||
box-shadow: 0 0 40px rgba(184, 148, 63, 0.05), inset 0 1px 0 rgba(184, 148, 63, 0.1);
|
||||
}
|
||||
|
||||
.card-shrine {
|
||||
background: rgba(12, 12, 20, 0.9);
|
||||
border: 1px solid rgba(184, 148, 63, 0.06);
|
||||
backdrop-filter: blur(20px);
|
||||
}
|
||||
|
||||
/* === BUTTONS === */
|
||||
.btn-gold {
|
||||
background: linear-gradient(135deg, var(--gold-dim), var(--gold), var(--gold-bright));
|
||||
color: #0a0a0f;
|
||||
font-family: 'Cinzel', serif;
|
||||
letter-spacing: 0.15em;
|
||||
font-weight: 700;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.btn-gold::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.2), transparent);
|
||||
transition: left 0.5s;
|
||||
}
|
||||
|
||||
.btn-gold:hover::before { left: 100%; }
|
||||
.btn-gold:hover { box-shadow: 0 0 30px rgba(184, 148, 63, 0.3); }
|
||||
|
||||
.btn-outline {
|
||||
border: 1px solid rgba(184, 148, 63, 0.25);
|
||||
color: var(--gold);
|
||||
font-family: 'Cinzel', serif;
|
||||
letter-spacing: 0.15em;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.btn-outline:hover {
|
||||
background: rgba(184, 148, 63, 0.08);
|
||||
border-color: rgba(184, 148, 63, 0.4);
|
||||
}
|
||||
|
||||
/* === ANIMATIONS === */
|
||||
@keyframes boatFloat {
|
||||
0%, 100% { transform: translateY(0px) rotate(0deg); }
|
||||
33% { transform: translateY(-6px) rotate(0.8deg); }
|
||||
66% { transform: translateY(3px) rotate(-0.5deg); }
|
||||
}
|
||||
|
||||
@keyframes fogDrift {
|
||||
0% { transform: translateX(-10%) scaleX(1); opacity: 0.3; }
|
||||
50% { transform: translateX(5%) scaleX(1.05); opacity: 0.5; }
|
||||
100% { transform: translateX(-10%) scaleX(1); opacity: 0.3; }
|
||||
}
|
||||
|
||||
@keyframes soulGlow {
|
||||
0%, 100% { opacity: 0.2; filter: blur(20px); }
|
||||
50% { opacity: 0.4; filter: blur(25px); }
|
||||
}
|
||||
|
||||
@keyframes fadeInUp {
|
||||
from { opacity: 0; transform: translateY(30px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
@keyframes twinkle {
|
||||
0%, 100% { opacity: 0.15; }
|
||||
50% { opacity: 0.8; }
|
||||
}
|
||||
|
||||
.boat-float { animation: boatFloat 6s ease-in-out infinite; }
|
||||
.fade-in { animation: fadeInUp 1s ease-out forwards; }
|
||||
.fade-in-delay { animation: fadeInUp 1s ease-out 0.3s forwards; opacity: 0; }
|
||||
.fade-in-delay-2 { animation: fadeInUp 1s ease-out 0.6s forwards; opacity: 0; }
|
||||
|
||||
/* === INPUTS === */
|
||||
.input-hades {
|
||||
background: rgba(5, 5, 8, 0.8);
|
||||
border: 1px solid rgba(184, 148, 63, 0.1);
|
||||
color: var(--bone);
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.input-hades:focus {
|
||||
border-color: rgba(184, 148, 63, 0.35);
|
||||
box-shadow: 0 0 20px rgba(184, 148, 63, 0.05);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.input-hades::placeholder { color: var(--smoke); }
|
||||
|
||||
/* === PROGRESS === */
|
||||
.progress-styx {
|
||||
background: rgba(5, 5, 8, 0.6);
|
||||
border: 1px solid rgba(184, 148, 63, 0.06);
|
||||
}
|
||||
|
||||
.progress-styx-fill {
|
||||
background: linear-gradient(90deg, var(--gold-dim), var(--gold), var(--gold-bright));
|
||||
box-shadow: 0 0 10px rgba(184, 148, 63, 0.3);
|
||||
}
|
||||
|
||||
/* === SCROLLBAR === */
|
||||
::-webkit-scrollbar { width: 4px; }
|
||||
::-webkit-scrollbar-track { background: var(--void); }
|
||||
::-webkit-scrollbar-thumb { background: rgba(184, 148, 63, 0.2); }
|
||||
::-webkit-scrollbar-thumb:hover { background: rgba(184, 148, 63, 0.4); }
|
||||
17
frontend/src/app/layout.js
Normal file
17
frontend/src/app/layout.js
Normal file
@@ -0,0 +1,17 @@
|
||||
import { Inter } from 'next/font/google'
|
||||
import './globals.css'
|
||||
|
||||
const inter = Inter({ subsets: ['latin'] })
|
||||
|
||||
export const metadata = {
|
||||
title: 'CARONTE - Guia Pós-Óbito',
|
||||
description: 'O barqueiro que guia famílias pelo rio burocrático pós-óbito no Brasil',
|
||||
}
|
||||
|
||||
export default function RootLayout({ children }) {
|
||||
return (
|
||||
<html lang="pt-BR">
|
||||
<body className={inter.className}>{children}</body>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
78
frontend/src/app/login/page.js
Normal file
78
frontend/src/app/login/page.js
Normal file
@@ -0,0 +1,78 @@
|
||||
'use client'
|
||||
import { useState } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import Link from 'next/link'
|
||||
import { Eye, EyeOff } from 'lucide-react'
|
||||
|
||||
export default function LoginPage() {
|
||||
const router = useRouter()
|
||||
const [email, setEmail] = useState('')
|
||||
const [senha, setSenha] = useState('')
|
||||
const [show, setShow] = useState(false)
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
const login = async (e) => {
|
||||
e.preventDefault()
|
||||
setLoading(true)
|
||||
setTimeout(() => {
|
||||
localStorage.setItem('token', 'demo-token')
|
||||
router.push('/dashboard')
|
||||
}, 1200)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-underworld flex items-center justify-center px-4 relative">
|
||||
{/* Stars */}
|
||||
<div className="fixed inset-0 pointer-events-none">
|
||||
{[...Array(40)].map((_, i) => (
|
||||
<div key={i} className="absolute rounded-full bg-white"
|
||||
style={{ width: `${1 + Math.random()}px`, height: `${1 + Math.random()}px`,
|
||||
top: `${Math.random() * 100}%`, left: `${Math.random() * 100}%`, opacity: 0.15,
|
||||
animation: `twinkle ${4 + Math.random() * 6}s ease-in-out ${Math.random() * 5}s infinite` }} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="w-full max-w-sm relative z-10 fade-in">
|
||||
<div className="text-center mb-12">
|
||||
<span className="text-6xl boat-float block mb-6 filter drop-shadow-xl">🚣</span>
|
||||
<h1 className="font-myth-decorative text-2xl gold-text tracking-[0.2em]">CARONTE</h1>
|
||||
<div className="ornament-line-simple max-w-[60px] mx-auto my-4" />
|
||||
<p className="font-elegant italic text-[var(--ash)] text-lg">"A travessia começa aqui."</p>
|
||||
</div>
|
||||
|
||||
<form onSubmit={login} className="card-hades p-8">
|
||||
<h2 className="font-myth text-sm text-center text-[var(--gold-dim)] mb-8 tracking-[0.3em] uppercase">Entrar no Submundo</h2>
|
||||
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<label className="text-[10px] tracking-[0.2em] text-[var(--gold-dim)] uppercase font-myth block mb-2">Email</label>
|
||||
<input type="email" value={email} onChange={e => setEmail(e.target.value)}
|
||||
className="w-full px-4 py-3 input-hades text-sm" placeholder="seu@email.com" required />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="text-[10px] tracking-[0.2em] text-[var(--gold-dim)] uppercase font-myth block mb-2">Senha</label>
|
||||
<div className="relative">
|
||||
<input type={show ? 'text' : 'password'} value={senha} onChange={e => setSenha(e.target.value)}
|
||||
className="w-full px-4 py-3 input-hades text-sm pr-12" placeholder="••••••••" required />
|
||||
<button type="button" onClick={() => setShow(!show)} className="absolute right-3 top-1/2 -translate-y-1/2 text-[var(--smoke)] hover:text-[var(--ash)] transition">
|
||||
{show ? <EyeOff size={14} /> : <Eye size={14} />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" disabled={loading} className="w-full py-3.5 btn-gold text-xs disabled:opacity-50">
|
||||
{loading ? '⏳ Cruzando o Styx...' : 'ENTRAR'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="ornament-line-simple my-6" />
|
||||
|
||||
<p className="text-center text-[var(--smoke)] text-sm">
|
||||
Primeira travessia? <Link href="/registro" className="text-[var(--gold)] hover:text-[var(--gold-bright)] transition">Criar conta</Link>
|
||||
</p>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
199
frontend/src/app/page.js
Normal file
199
frontend/src/app/page.js
Normal file
@@ -0,0 +1,199 @@
|
||||
'use client'
|
||||
import Link from 'next/link'
|
||||
import { ArrowRight, Shield } from 'lucide-react'
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<div className="min-h-screen bg-underworld relative">
|
||||
|
||||
{/* Fog layers */}
|
||||
<div className="fixed inset-0 pointer-events-none z-0">
|
||||
<div className="absolute bottom-0 left-0 right-0 h-[400px] bg-gradient-to-t from-[rgba(196,75,26,0.03)] to-transparent" style={{ animation: 'fogDrift 20s ease-in-out infinite' }} />
|
||||
<div className="absolute bottom-0 left-0 right-0 h-[300px] bg-gradient-to-t from-[rgba(42,74,154,0.04)] to-transparent" style={{ animation: 'fogDrift 15s ease-in-out infinite reverse' }} />
|
||||
</div>
|
||||
|
||||
{/* Stars */}
|
||||
<div className="fixed inset-0 pointer-events-none z-0">
|
||||
{[...Array(80)].map((_, i) => (
|
||||
<div key={i} className="absolute rounded-full bg-white"
|
||||
style={{
|
||||
width: `${1 + Math.random() * 1.5}px`, height: `${1 + Math.random() * 1.5}px`,
|
||||
top: `${Math.random() * 50}%`, left: `${Math.random() * 100}%`,
|
||||
opacity: 0.15 + Math.random() * 0.3,
|
||||
animation: `twinkle ${4 + Math.random() * 6}s ease-in-out ${Math.random() * 5}s infinite`
|
||||
}} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Soul orbs */}
|
||||
<div className="fixed inset-0 pointer-events-none z-0">
|
||||
{[...Array(5)].map((_, i) => (
|
||||
<div key={i} className="absolute rounded-full"
|
||||
style={{
|
||||
width: `${60 + Math.random() * 100}px`, height: `${60 + Math.random() * 100}px`,
|
||||
top: `${30 + Math.random() * 50}%`, left: `${Math.random() * 100}%`,
|
||||
background: `radial-gradient(circle, ${i % 2 === 0 ? 'rgba(196,75,26,0.06)' : 'rgba(74,122,255,0.04)'}, transparent 70%)`,
|
||||
animation: `soulGlow ${6 + Math.random() * 4}s ease-in-out ${Math.random() * 3}s infinite`
|
||||
}} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Nav */}
|
||||
<nav className="relative z-20 flex items-center justify-between px-10 py-7">
|
||||
<div className="flex items-center gap-4">
|
||||
<span className="text-4xl boat-float filter drop-shadow-lg">🚣</span>
|
||||
<div>
|
||||
<span className="font-myth-decorative text-xl gold-text tracking-[0.2em]">CARONTE</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-3">
|
||||
<Link href="/login" className="btn-outline px-6 py-2.5 text-xs tracking-[0.2em]">ENTRAR</Link>
|
||||
<Link href="/registro" className="btn-gold px-6 py-2.5 text-xs">COMEÇAR</Link>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
{/* Hero */}
|
||||
<section className="relative z-10 max-w-4xl mx-auto px-8 pt-20 pb-12 text-center">
|
||||
<div className="fade-in">
|
||||
<div className="ornament-line max-w-[200px] mx-auto mb-16" />
|
||||
|
||||
<p className="font-myth text-[var(--gold-dim)] text-[11px] tracking-[0.5em] uppercase mb-8">⚱️ Guia para famílias em luto</p>
|
||||
|
||||
<h1 className="font-myth-decorative text-5xl md:text-7xl font-bold leading-[1.15] mb-8 tracking-wide">
|
||||
<span className="gold-text">Perdeu</span>
|
||||
<span className="text-[var(--marble)]"> alguém</span>
|
||||
<span className="gold-text">?</span><br />
|
||||
<span className="text-[var(--bone)] text-4xl md:text-5xl font-myth font-normal tracking-wider">Nós guiamos a travessia.</span>
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div className="fade-in-delay">
|
||||
<p className="font-elegant text-2xl md:text-3xl text-[var(--ash)] max-w-2xl mx-auto mb-6 italic leading-relaxed">
|
||||
"Assim como o barqueiro guiava as almas<br />pelo Rio Styx até o descanso eterno —<br />nós guiamos sua família pela burocracia."
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="fade-in-delay-2">
|
||||
<p className="text-[var(--smoke)] max-w-lg mx-auto mb-14 text-sm leading-relaxed">
|
||||
Checklist inteligente · Scanner de benefícios esquecidos · Gerador de documentos.<br />
|
||||
Tudo que precisa ser feito após a perda — em um só lugar.
|
||||
</p>
|
||||
|
||||
<Link href="/registro" className="group inline-flex items-center gap-3 btn-gold px-10 py-4 text-sm">
|
||||
INICIAR A TRAVESSIA <ArrowRight className="group-hover:translate-x-1 transition" size={18} />
|
||||
</Link>
|
||||
|
||||
<p className="text-[var(--smoke)] text-xs mt-4 tracking-wider">Gratuito · Sem cartão de crédito</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Divider with ember */}
|
||||
<div className="relative z-10 max-w-4xl mx-auto px-8 py-16">
|
||||
<div className="ornament-line" />
|
||||
</div>
|
||||
|
||||
{/* Numbers */}
|
||||
<section className="relative z-10 max-w-4xl mx-auto px-8 pb-20">
|
||||
<div className="grid grid-cols-3 gap-6">
|
||||
{[
|
||||
{ num: '1.4M', label: 'óbitos por ano', sub: 'no Brasil', icon: '💀' },
|
||||
{ num: '500h', label: 'de burocracia', sub: 'por família enlutada', icon: '⏳' },
|
||||
{ num: 'R$ Bi', label: 'em benefícios', sub: 'esquecidos todo ano', icon: '🔱' },
|
||||
].map((s, i) => (
|
||||
<div key={i} className="card-hades p-8 text-center group hover:scale-[1.02] transition-all duration-500">
|
||||
<span className="text-3xl block mb-4 group-hover:scale-110 transition-transform">{s.icon}</span>
|
||||
<p className="font-myth-decorative text-3xl md:text-4xl font-bold gold-text mb-2">{s.num}</p>
|
||||
<p className="text-[var(--bone)] text-sm font-myth tracking-wider">{s.label}</p>
|
||||
<p className="text-[var(--smoke)] text-xs mt-1">{s.sub}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Features - The Three Pillars */}
|
||||
<section className="relative z-10 max-w-5xl mx-auto px-8 pb-24">
|
||||
<div className="text-center mb-16">
|
||||
<p className="font-myth text-[var(--gold-dim)] text-[11px] tracking-[0.5em] uppercase mb-4">🏛️ Os Três Pilares do Submundo</p>
|
||||
<h2 className="font-myth text-3xl md:text-4xl font-bold text-[var(--marble)] tracking-wide">As armas de Caronte</h2>
|
||||
<div className="ornament-line-simple max-w-[300px] mx-auto mt-6" />
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
|
||||
{[
|
||||
{
|
||||
icon: '🗺️', title: 'O Mapa do', titleGold: 'Submundo',
|
||||
desc: 'Checklist completo de cada passo após o óbito. Certidões, INSS, bancos, inventário — com prazos, dependências e prioridades. Como um mapa que revela cada caminho no labirinto burocrático.',
|
||||
detail: '15 etapas · 4 fases · Prazos automáticos'
|
||||
},
|
||||
{
|
||||
icon: '🔮', title: 'O Oráculo dos', titleGold: 'Tesouros',
|
||||
desc: 'Nossa IA vasculha benefícios esquecidos: FGTS, PIS/PASEP, seguros de vida, pensões, restituições. Milhares de reais que famílias nunca souberam que tinham direito.',
|
||||
detail: '7 tipos de benefício · Média: R$15.000 recuperados'
|
||||
},
|
||||
{
|
||||
icon: '📜', title: 'Os Pergaminhos', titleGold: 'Sagrados',
|
||||
desc: 'Procurações, requerimentos e petições gerados automaticamente. Documentos jurídicos prontos para uso — sem precisar de advogado para as tarefas mais simples.',
|
||||
detail: '5 modelos · PDF pronto para imprimir'
|
||||
},
|
||||
].map((f, i) => (
|
||||
<div key={i} className="card-hades p-8 group hover:scale-[1.02] transition-all duration-500 flex flex-col">
|
||||
<span className="text-4xl block mb-6 group-hover:scale-110 transition-transform">{f.icon}</span>
|
||||
<h3 className="font-myth text-lg text-[var(--bone)] mb-1 tracking-wider">
|
||||
{f.title} <span className="gold-text">{f.titleGold}</span>
|
||||
</h3>
|
||||
<p className="text-[var(--ash)] text-sm leading-relaxed mt-3 flex-1">{f.desc}</p>
|
||||
<div className="ornament-line-simple mt-6 mb-4" />
|
||||
<p className="text-[var(--gold-dim)] text-[10px] tracking-[0.2em] font-myth uppercase">{f.detail}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Testimonial / Quote */}
|
||||
<section className="relative z-10 py-20">
|
||||
<div className="max-w-3xl mx-auto px-8 text-center">
|
||||
<div className="card-hades p-12 relative">
|
||||
<span className="absolute -top-6 left-1/2 -translate-x-1/2 text-5xl">🔥</span>
|
||||
<p className="font-elegant text-2xl md:text-3xl text-[var(--bone)] italic leading-relaxed mt-4">
|
||||
"Quando meu pai faleceu, eu não sabia por onde começar. Foram meses de agonia em filas, cartórios e telefones. Se o Caronte existisse naquele momento, teria mudado tudo."
|
||||
</p>
|
||||
<div className="ornament-line-simple max-w-[100px] mx-auto my-6" />
|
||||
<p className="text-[var(--gold-dim)] font-myth text-xs tracking-[0.2em] uppercase">— A dor que nos inspirou a criar</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Final CTA */}
|
||||
<section className="relative z-10 py-24">
|
||||
<div className="max-w-2xl mx-auto text-center px-8">
|
||||
<span className="text-7xl boat-float block mb-8 filter drop-shadow-2xl">🚣</span>
|
||||
<h2 className="font-myth text-3xl md:text-4xl font-bold text-[var(--marble)] mb-4 tracking-wide">A travessia não precisa<br />ser solitária</h2>
|
||||
<p className="font-elegant text-xl text-[var(--ash)] italic mb-10">"Não deixe a burocracia transformar o luto em pesadelo."</p>
|
||||
<Link href="/registro" className="inline-flex items-center gap-3 btn-gold px-10 py-4 text-sm">
|
||||
COMEÇAR AGORA — GRATUITO
|
||||
</Link>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Footer */}
|
||||
<footer className="relative z-10 border-t border-[var(--gold)]/5 py-8 px-10">
|
||||
<div className="max-w-5xl mx-auto flex justify-between items-center">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-lg">🚣</span>
|
||||
<span className="font-myth text-xs gold-text tracking-[0.2em]">CARONTE</span>
|
||||
</div>
|
||||
<p className="text-[var(--smoke)] text-[11px] tracking-wider">
|
||||
© 2026 — Nenhum benefício esquecido. Nenhum prazo perdido.
|
||||
</p>
|
||||
<div className="flex items-center gap-2">
|
||||
<Shield size={12} className="text-[var(--gold-dim)]" />
|
||||
<span className="text-[var(--smoke)] text-[11px]">LGPD</span>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
{/* River Styx glow at bottom */}
|
||||
<div className="fixed bottom-0 left-0 right-0 h-[2px] bg-gradient-to-r from-transparent via-[var(--gold-dim)] to-transparent opacity-20 z-30" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
89
frontend/src/app/registro/page.js
Normal file
89
frontend/src/app/registro/page.js
Normal file
@@ -0,0 +1,89 @@
|
||||
'use client'
|
||||
import { useState } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import Link from 'next/link'
|
||||
import { UserPlus, Eye, EyeOff } from 'lucide-react'
|
||||
|
||||
export default function RegistroPage() {
|
||||
const router = useRouter()
|
||||
const [nome, setNome] = useState('')
|
||||
const [email, setEmail] = useState('')
|
||||
const [senha, setSenha] = useState('')
|
||||
const [show, setShow] = useState(false)
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
const registro = async (e) => {
|
||||
e.preventDefault()
|
||||
setLoading(true)
|
||||
setTimeout(() => {
|
||||
localStorage.setItem('token', 'demo-token')
|
||||
router.push('/dashboard')
|
||||
}, 1500)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen styx-bg flex items-center justify-center px-4 relative">
|
||||
<div className="absolute inset-0 overflow-hidden pointer-events-none">
|
||||
{[...Array(30)].map((_, i) => (
|
||||
<div key={i} className="absolute w-[2px] h-[2px] bg-white/20 rounded-full"
|
||||
style={{ top: `${Math.random() * 100}%`, left: `${Math.random() * 100}%`, animation: `twinkle ${3 + Math.random() * 4}s ease-in-out infinite` }} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="w-full max-w-md relative z-10">
|
||||
<div className="text-center mb-10">
|
||||
<span className="text-5xl boat-float block mb-4">🚣</span>
|
||||
<h1 className="font-myth text-3xl font-bold gold-text tracking-wider">CARONTE</h1>
|
||||
<p className="font-elegant italic text-[var(--text-muted)] mt-2">"Toda travessia começa com o primeiro passo."</p>
|
||||
</div>
|
||||
|
||||
<div className="greek-line mb-8 max-w-[100px] mx-auto" />
|
||||
|
||||
<form onSubmit={registro} className="card-temple p-8 glow-gold">
|
||||
<h2 className="font-myth text-xl text-center text-[var(--marble)] mb-6 tracking-wider">CRIAR CONTA</h2>
|
||||
|
||||
<div className="space-y-5">
|
||||
<div>
|
||||
<label className="text-[10px] tracking-[0.2em] text-[var(--gold)] uppercase font-myth block mb-2">Nome Completo</label>
|
||||
<input type="text" value={nome} onChange={e => setNome(e.target.value)}
|
||||
className="w-full px-4 py-3 bg-black/30 border border-[var(--gold)]/20 text-[var(--text)] focus:border-[var(--gold)]/50 focus:outline-none transition text-sm"
|
||||
placeholder="Seu nome" required />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="text-[10px] tracking-[0.2em] text-[var(--gold)] uppercase font-myth block mb-2">Email</label>
|
||||
<input type="email" value={email} onChange={e => setEmail(e.target.value)}
|
||||
className="w-full px-4 py-3 bg-black/30 border border-[var(--gold)]/20 text-[var(--text)] focus:border-[var(--gold)]/50 focus:outline-none transition text-sm"
|
||||
placeholder="seu@email.com" required />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="text-[10px] tracking-[0.2em] text-[var(--gold)] uppercase font-myth block mb-2">Senha</label>
|
||||
<div className="relative">
|
||||
<input type={show ? 'text' : 'password'} value={senha} onChange={e => setSenha(e.target.value)}
|
||||
className="w-full px-4 py-3 bg-black/30 border border-[var(--gold)]/20 text-[var(--text)] focus:border-[var(--gold)]/50 focus:outline-none transition text-sm pr-12"
|
||||
placeholder="••••••••" required />
|
||||
<button type="button" onClick={() => setShow(!show)} className="absolute right-3 top-1/2 -translate-y-1/2 text-[var(--text-muted)]">
|
||||
{show ? <EyeOff size={16} /> : <Eye size={16} />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" disabled={loading}
|
||||
className="w-full py-3 bg-gradient-to-r from-[var(--gold-dark)] to-[var(--gold)] text-black font-bold font-myth tracking-wider hover:from-[var(--gold)] hover:to-[var(--gold-light)] transition disabled:opacity-50 flex items-center justify-center gap-2">
|
||||
{loading ? '⏳ Preparando a travessia...' : <><UserPlus size={16} /> INICIAR TRAVESSIA</>}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="greek-line mt-6 mb-4" />
|
||||
|
||||
<p className="text-center text-[var(--text-muted)] text-sm">
|
||||
Já tem conta? <Link href="/login" className="text-[var(--gold)] hover:text-[var(--gold-light)] font-medium">Entrar</Link>
|
||||
</p>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div className="absolute bottom-0 left-0 right-0 h-32 bg-gradient-to-t from-[var(--water)]/20 to-transparent pointer-events-none" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
59
frontend/src/components/Sidebar.js
Normal file
59
frontend/src/components/Sidebar.js
Normal file
@@ -0,0 +1,59 @@
|
||||
'use client'
|
||||
import Link from 'next/link'
|
||||
import { usePathname, useRouter } from 'next/navigation'
|
||||
import { LayoutDashboard, Users, LogOut } from 'lucide-react'
|
||||
|
||||
export default function Sidebar() {
|
||||
const pathname = usePathname()
|
||||
const router = useRouter()
|
||||
|
||||
const logout = () => { localStorage.removeItem('token'); router.push('/login') }
|
||||
|
||||
const links = [
|
||||
{ href: '/dashboard', label: 'Ágora', icon: <LayoutDashboard size={16} />, emoji: '🏛️' },
|
||||
{ href: '/dashboard', label: 'Famílias', icon: <Users size={16} />, emoji: '⚱️' },
|
||||
]
|
||||
|
||||
return (
|
||||
<aside className="fixed left-0 top-0 h-screen w-60 bg-[var(--void)]/98 backdrop-blur-xl flex flex-col z-40 border-r border-[var(--gold)]/5">
|
||||
{/* Logo */}
|
||||
<div className="p-6 pb-4">
|
||||
<Link href="/dashboard" className="flex items-center gap-3">
|
||||
<span className="text-2xl boat-float">🚣</span>
|
||||
<div>
|
||||
<div className="font-myth-decorative text-sm gold-text tracking-[0.15em]">CARONTE</div>
|
||||
<div className="text-[9px] tracking-[0.15em] text-[var(--smoke)] uppercase">Guia do Submundo</div>
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="ornament-line-simple mx-4" />
|
||||
|
||||
{/* Nav */}
|
||||
<nav className="flex-1 px-3 py-4 space-y-0.5">
|
||||
<p className="text-[9px] tracking-[0.3em] text-[var(--smoke)] uppercase font-myth px-3 mb-3">Navegação</p>
|
||||
{links.map((l, i) => {
|
||||
const active = pathname === l.href
|
||||
return (
|
||||
<Link key={i} href={l.href}
|
||||
className={`flex items-center gap-3 px-3 py-2.5 text-sm transition-all ${active ? 'text-[var(--gold)] bg-[var(--gold)]/5 border-l-2 border-[var(--gold)]' : 'text-[var(--ash)] hover:text-[var(--bone)] hover:bg-white/[0.02]'}`}>
|
||||
<span className="text-base">{l.emoji}</span>
|
||||
<span className="font-myth text-xs tracking-wider">{l.label}</span>
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
</nav>
|
||||
|
||||
<div className="p-4">
|
||||
<div className="card-shrine p-3 mb-3 text-center">
|
||||
<p className="font-elegant italic text-[11px] text-[var(--smoke)] leading-relaxed">"A travessia é mais leve<br/>quando não se está só."</p>
|
||||
</div>
|
||||
<div className="ornament-line-simple mb-3" />
|
||||
<button onClick={logout} className="flex items-center gap-2 px-3 py-2 text-[var(--smoke)] hover:text-red-400/80 transition w-full text-xs">
|
||||
<LogOut size={13} />
|
||||
<span>Encerrar</span>
|
||||
</button>
|
||||
</div>
|
||||
</aside>
|
||||
)
|
||||
}
|
||||
49
frontend/src/lib/api.js
Normal file
49
frontend/src/lib/api.js
Normal file
@@ -0,0 +1,49 @@
|
||||
const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8070';
|
||||
|
||||
async function request(path, options = {}) {
|
||||
const token = typeof window !== 'undefined' ? localStorage.getItem('token') : null;
|
||||
const headers = { ...options.headers };
|
||||
if (token) headers['Authorization'] = `Bearer ${token}`;
|
||||
if (options.body && typeof options.body === 'object' && !(options.body instanceof FormData)) {
|
||||
headers['Content-Type'] = 'application/json';
|
||||
options.body = JSON.stringify(options.body);
|
||||
}
|
||||
const res = await fetch(`${API_URL}${path}`, { ...options, headers });
|
||||
if (res.status === 401) {
|
||||
if (typeof window !== 'undefined') {
|
||||
localStorage.removeItem('token');
|
||||
window.location.href = '/login';
|
||||
}
|
||||
throw new Error('Não autorizado');
|
||||
}
|
||||
if (!res.ok) {
|
||||
const err = await res.json().catch(() => ({}));
|
||||
throw new Error(err.detail || 'Erro na requisição');
|
||||
}
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export const api = {
|
||||
login: (email, senha) => {
|
||||
const form = new URLSearchParams();
|
||||
form.append('username', email);
|
||||
form.append('password', senha);
|
||||
return request('/api/v1/auth/login', { method: 'POST', body: form, headers: { 'Content-Type': 'application/x-www-form-urlencoded' } });
|
||||
},
|
||||
registro: (data) => request('/api/v1/auth/registro', { method: 'POST', body: data }),
|
||||
me: () => request('/api/v1/auth/me'),
|
||||
dashboard: () => request('/api/v1/dashboard/'),
|
||||
familias: () => request('/api/v1/familias/'),
|
||||
familia: (id) => request(`/api/v1/familias/${id}`),
|
||||
criarFamilia: (data) => request('/api/v1/familias/', { method: 'POST', body: data }),
|
||||
membros: (famId) => request(`/api/v1/familias/${famId}/membros`),
|
||||
falecidos: (famId) => request(`/api/v1/familias/${famId}/falecidos`),
|
||||
checklist: (famId) => request(`/api/v1/familias/${famId}/checklist/`),
|
||||
updateChecklist: (famId, itemId, status) => request(`/api/v1/familias/${famId}/checklist/${itemId}`, { method: 'PUT', body: { status } }),
|
||||
proximoPasso: (famId) => request(`/api/v1/familias/${famId}/checklist/proximo`),
|
||||
beneficios: (famId) => request(`/api/v1/familias/${famId}/beneficios/`),
|
||||
scanBeneficios: (famId) => request(`/api/v1/familias/${famId}/beneficios/scan`, { method: 'POST' }),
|
||||
documentos: (famId) => request(`/api/v1/familias/${famId}/documentos/`),
|
||||
gerarDocumento: (famId, tipo, falecidoId) => request(`/api/v1/familias/${famId}/documentos/gerar`, { method: 'POST', body: { tipo, falecido_id: falecidoId } }),
|
||||
downloadUrl: (famId, docId) => `${API_URL}/api/v1/familias/${famId}/documentos/${docId}/download`,
|
||||
};
|
||||
17
frontend/tailwind.config.js
Normal file
17
frontend/tailwind.config.js
Normal file
@@ -0,0 +1,17 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: [
|
||||
"./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
"./src/components/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
"./src/app/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
],
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
background: "var(--background)",
|
||||
foreground: "var(--foreground)",
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
};
|
||||
Reference in New Issue
Block a user