feat: initial project structure (Model Project) - Backend + Multi-Frontend + Docker

This commit is contained in:
VPS 4 Builder
2026-05-15 10:39:35 +02:00
commit 3beac0e0f2
288 changed files with 78761 additions and 0 deletions
File diff suppressed because it is too large Load Diff
+200
View File
@@ -0,0 +1,200 @@
<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Admin Login - Sistema Nuvem</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap"
rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css" rel="stylesheet">
<style>
:root {
--primary: #06b6d4;
--primary-dark: #0891b2;
--bg-dark: #0f172a;
--bg-card: #1e293b;
--text-light: #f1f5f9;
--text-muted: #94a3b8;
--glass-border: rgba(255, 255, 255, 0.1);
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Inter', sans-serif;
background: var(--bg-dark);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
.login-card {
background: var(--bg-card);
border: 1px solid var(--glass-border);
border-radius: 24px;
padding: 48px 36px;
width: 100%;
max-width: 420px;
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
animation: slideUp 0.5s ease-out;
}
@keyframes slideUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.login-icon {
width: 70px;
height: 70px;
margin: 0 auto 20px;
background: linear-gradient(135deg, var(--primary), var(--primary-dark));
border-radius: 18px;
display: flex;
align-items: center;
justify-content: center;
font-size: 30px;
color: white;
box-shadow: 0 10px 30px rgba(6, 182, 212, 0.3);
}
h1 {
text-align: center;
color: var(--text-light);
font-size: 1.4rem;
font-weight: 700;
margin-bottom: 4px;
}
.subtitle {
text-align: center;
color: var(--text-muted);
font-size: 0.85rem;
margin-bottom: 32px;
}
.form-floating {
margin-bottom: 16px;
}
.form-floating>.form-control {
background: rgba(255, 255, 255, 0.05);
border: 1px solid var(--glass-border);
border-radius: 12px;
color: var(--text-light);
height: 56px;
}
.form-floating>.form-control:focus {
background: rgba(255, 255, 255, 0.08);
border-color: var(--primary);
box-shadow: 0 0 0 3px rgba(6, 182, 212, 0.15);
color: var(--text-light);
}
.form-floating>label {
color: var(--text-muted);
}
.form-floating>.form-control:focus~label,
.form-floating>.form-control:not(:placeholder-shown)~label {
color: var(--primary);
}
.btn-login {
width: 100%;
padding: 14px;
background: linear-gradient(135deg, var(--primary), var(--primary-dark));
border: none;
border-radius: 12px;
color: white;
font-weight: 600;
font-size: 0.95rem;
cursor: pointer;
transition: all 0.3s;
margin-top: 8px;
}
.btn-login:hover {
transform: translateY(-2px);
box-shadow: 0 10px 30px rgba(6, 182, 212, 0.4);
}
.alert {
border-radius: 12px;
font-size: 0.85rem;
display: none;
}
</style>
</head>
<body>
<div class="login-card">
<div class="login-icon"><i class="bi bi-cloud-fill"></i></div>
<h1>Painel Administrativo</h1>
<p class="subtitle">Sistema Nuvem - Promoções & Sorteio</p>
<div id="alertBox" class="alert alert-danger" role="alert"></div>
<form id="loginForm">
<div class="form-floating">
<input type="text" class="form-control" id="username" placeholder="Usuário" required>
<label for="username">Usuário</label>
</div>
<div class="form-floating">
<input type="password" class="form-control" id="password" placeholder="Senha" required>
<label for="password">Senha</label>
</div>
<button type="submit" class="btn btn-login" id="btnLogin">
<i class="bi bi-box-arrow-in-right me-2"></i>ENTRAR
</button>
</form>
</div>
<script>
document.getElementById('loginForm').addEventListener('submit', async function (e) {
e.preventDefault();
const btn = document.getElementById('btnLogin');
const alertBox = document.getElementById('alertBox');
alertBox.style.display = 'none';
btn.disabled = true;
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>Entrando...';
try {
const res = await fetch('/admin/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
username: document.getElementById('username').value,
password: document.getElementById('password').value
})
});
const data = await res.json();
if (data.success) {
window.location.href = data.redirect || '/admin/dashboard';
} else {
alertBox.textContent = data.error || 'Credenciais inválidas';
alertBox.style.display = 'block';
}
} catch (err) {
alertBox.textContent = 'Erro de conexão';
alertBox.style.display = 'block';
} finally {
btn.disabled = false;
btn.innerHTML = '<i class="bi bi-box-arrow-in-right me-2"></i>ENTRAR';
}
});
</script>
</body>
</html>
+58
View File
@@ -0,0 +1,58 @@
<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Acesso Bloqueado</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap" rel="stylesheet">
<style>
body {
font-family: 'Inter', sans-serif;
background-color: #fce7f3;
color: #1e293b;
display: flex;
align-items: center;
justify-content: center;
height: 100vh;
margin: 0;
text-align: center;
}
.container {
background-color: white;
padding: 40px;
border-radius: 12px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
max-width: 90%;
width: 400px;
}
h1 {
color: #be123c;
margin-bottom: 20px;
}
p {
margin-bottom: 30px;
color: #475569;
}
a {
display: inline-block;
background-color: #be123c;
color: white;
padding: 12px 24px;
text-decoration: none;
border-radius: 8px;
font-weight: bold;
transition: background 0.3s;
}
a:hover {
background-color: #9f1239;
}
</style>
</head>
<body>
<div class="container">
<h1>🚫 Site Bloqueado</h1>
<p>O WiFi da Alemão Conveniência não permite acesso a este tipo de conteúdo.</p>
<a href="/">Voltar às Promoções</a>
</div>
</body>
</html>
File diff suppressed because it is too large Load Diff
+323
View File
@@ -0,0 +1,323 @@
<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Minha Área - Alemão Conveniência</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&display=swap"
rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css" rel="stylesheet">
<style>
:root {
--primary: #f59e0b;
--primary-dark: #d97706;
--bg-dark: #0f172a;
--bg-card: #1e293b;
--bg-section: #131c2e;
--text-light: #f1f5f9;
--text-muted: #94a3b8;
--glass-border: rgba(255, 255, 255, 0.08);
--success: #10b981;
--gradient-primary: linear-gradient(135deg, #f59e0b, #d97706);
}
body {
font-family: 'Inter', sans-serif;
background: var(--bg-dark);
color: var(--text-light);
min-height: 100vh;
}
.navbar-custom {
background: rgba(15, 23, 42, 0.9);
backdrop-filter: blur(20px);
border-bottom: 1px solid var(--glass-border);
padding: 16px 0;
position: fixed;
top: 0;
width: 100%;
z-index: 100;
}
.brand-icon {
width: 36px;
height: 36px;
background: var(--gradient-primary);
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
}
.main-content {
padding-top: 100px;
padding-bottom: 60px;
}
.welcome-section {
margin-bottom: 40px;
}
.welcome-section h1 {
font-weight: 800;
font-size: 2rem;
margin-bottom: 8px;
}
.ticket-card {
background: var(--bg-card);
border: 1px solid var(--glass-border);
border-radius: 20px;
padding: 24px;
margin-bottom: 24px;
transition: transform 0.3s;
}
.ticket-card:hover {
transform: translateY(-5px);
}
.ticket-number {
font-family: 'monospace';
font-size: 1.5rem;
font-weight: 700;
color: var(--primary);
letter-spacing: 2px;
}
.badge-active {
background: rgba(16, 185, 129, 0.1);
color: var(--success);
border: 1px solid rgba(16, 185, 129, 0.2);
padding: 4px 12px;
border-radius: 99px;
font-size: 0.75rem;
font-weight: 600;
}
.logout-btn {
color: var(--text-muted);
text-decoration: none;
font-weight: 500;
font-size: 0.9rem;
transition: color 0.3s;
}
.logout-btn:hover {
color: #ef4444;
}
.empty-state {
text-align: center;
padding: 60px 20px;
background: rgba(255, 255, 255, 0.02);
border-radius: 24px;
border: 1px dashed var(--glass-border);
}
.empty-state i {
font-size: 48px;
color: var(--text-muted);
margin-bottom: 20px;
display: block;
}
.form-check-input:checked {
background-color: #25d366;
border-color: #25d366;
}
.optin-card {
background: var(--bg-card);
border: 1px solid var(--glass-border);
border-radius: 20px;
padding: 24px;
}
.optin-card .wa-icon {
width: 40px;
height: 40px;
background: rgba(37,211,102,0.12);
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
color: #25d366;
}
</style>
</head>
<body>
<nav class="navbar-custom">
<div class="container d-flex justify-content-between align-items-center">
<div class="d-flex align-items-center gap-2">
<div class="brand-icon">
<i class="bi bi-person-fill text-white"></i>
</div>
<span class="fw-bold fs-5">Minha Área</span>
</div>
<a href="/user/logout" class="logout-btn">
<i class="bi bi-box-arrow-right me-1"></i> Sair
</a>
</div>
</nav>
<div class="container main-content">
<div class="welcome-section">
<h1 id="user-name">Olá, Carregando...</h1>
<p class="text-muted">Acompanhe seus tickets para o sorteio e promoções ativas.</p>
</div>
<div class="row">
<div class="col-lg-8">
<h4 class="mb-4">Meus Tickets da Sorte</h4>
<div id="tickets-container">
<div class="text-center p-5">
<div class="spinner-border text-warning" role="status"></div>
</div>
</div>
</div>
<div class="col-lg-4">
<div class="card bg-card border-0 rounded-4 p-4 mb-4">
<h5 class="mb-3">Informações</h5>
<p class="small text-muted mb-4">Esta área é exclusiva para clientes cadastrados no WiFi da Alemão
Conveniência.</p>
<a href="/" class="btn btn-outline-warning rounded-pill w-100">Ver Promoções</a>
</div>
<div class="optin-card mb-4">
<div class="d-flex align-items-center gap-3 mb-3">
<div class="wa-icon"><i class="bi bi-whatsapp"></i></div>
<div>
<div class="fw-semibold" style="font-size:.95rem">Notificações WhatsApp</div>
<div class="text-muted" style="font-size:.78rem">Sorteios, promoções e avisos</div>
</div>
</div>
<div class="d-flex justify-content-between align-items-center">
<span id="optin-label" class="small text-muted">Carregando...</span>
<div class="form-check form-switch mb-0">
<input class="form-check-input" type="checkbox" id="optin-toggle"
style="width:2.5em;height:1.3em;cursor:pointer" disabled
onchange="toggleOptin(this.checked)">
</div>
</div>
<div id="optin-feedback" class="small mt-2" style="min-height:1.2em"></div>
</div>
</div>
</div>
</div>
<script>
async function loadUserData() {
try {
// Fetch real tickets from the API
const res = await fetch('/api/raffle/my-tickets');
const tickets = await res.json();
const container = document.getElementById('tickets-container');
if (!res.ok) {
container.innerHTML = `<div class="alert alert-danger">Erro ao carregar dados. Por favor, faça login novamente.</div>`;
return;
}
// Update welcome message
try {
const meRes = await fetch('/api/me');
if (meRes.ok) {
const meData = await meRes.json();
document.getElementById('user-name').textContent = `Olá, ${meData.nome}!`;
} else {
const userName = tickets.length > 0 ? tickets[0].usuario_nome : "Cliente Especial";
document.getElementById('user-name').textContent = `Olá, ${userName}!`;
}
} catch (e) {
const userName = tickets.length > 0 ? tickets[0].usuario_nome : "Cliente Especial";
document.getElementById('user-name').textContent = `Olá, ${userName}!`;
}
if (tickets.length === 0) {
container.innerHTML = `
<div class="empty-state mt-4">
<i class="bi bi-gift"></i>
<h5>Você ainda não tem tickets</h5>
<p class="text-muted small">Visite a loja e use o Wi-Fi para ganhar chances no sorteio!</p>
<a href="/" class="btn btn-warning rounded-pill mt-3">Ver Promoções</a>
</div>
`;
return;
}
// Render ticket list
container.innerHTML = tickets.map(ticket => `
<div class="ticket-card d-flex justify-content-between align-items-center">
<div>
<span class="text-muted small d-block mb-1">${ticket.raffle_titulo || 'SORTEIO GERAL'}</span>
<div class="ticket-number">#AL-${ticket.numero.toString().padStart(4, '0')}</div>
</div>
<div class="text-end">
<span class="badge-active mb-2 d-inline-block">ATIVO</span>
<div class="text-muted small">${new Date(ticket.data_compra).toLocaleDateString('pt-BR')}</div>
</div>
</div>
`).join('');
} catch (err) {
console.error(err);
document.getElementById('tickets-container').innerHTML = `<div class="alert alert-danger">Erro de conexão com o servidor.</div>`;
}
}
loadUserData();
loadOptinStatus();
async function loadOptinStatus() {
try {
const res = await fetch('/api/me');
if (!res.ok) return;
const user = await res.json();
const toggle = document.getElementById('optin-toggle');
const label = document.getElementById('optin-label');
toggle.checked = user.whatsapp_optin !== false;
toggle.disabled = false;
label.textContent = toggle.checked ? 'Ativado' : 'Desativado';
label.style.color = toggle.checked ? '#25d366' : '';
} catch (e) {
document.getElementById('optin-label').textContent = 'Indisponível';
}
}
async function toggleOptin(value) {
const toggle = document.getElementById('optin-toggle');
const label = document.getElementById('optin-label');
const feedback = document.getElementById('optin-feedback');
toggle.disabled = true;
feedback.textContent = '';
try {
const res = await fetch('/api/me/optin', {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ optin: value }),
});
if (!res.ok) throw new Error('Falha');
label.textContent = value ? 'Ativado' : 'Desativado';
label.style.color = value ? '#25d366' : '#94a3b8';
feedback.style.color = '#25d366';
feedback.textContent = value ? '✓ Notificações ativadas' : '✓ Notificações desativadas';
} catch (e) {
toggle.checked = !value; // reverte visualmente
feedback.style.color = '#ef4444';
feedback.textContent = 'Erro ao salvar. Tente novamente.';
} finally {
toggle.disabled = false;
setTimeout(() => { feedback.textContent = ''; }, 3000);
}
}
</script>
</body>
</html>
+410
View File
@@ -0,0 +1,410 @@
<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Alemão Conveniência - Área do Usuário</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&display=swap" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css" rel="stylesheet">
<style>
:root {
--primary: #f59e0b;
--primary-dark: #d97706;
--bg-dark: #0f172a;
--bg-card: #1e293b;
--text-light: #f1f5f9;
--text-muted: #94a3b8;
--glass-border: rgba(255, 255, 255, 0.08);
--gradient-primary: linear-gradient(135deg, #f59e0b, #d97706);
}
body {
font-family: 'Inter', sans-serif;
background: var(--bg-dark);
color: var(--text-light);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
.login-card {
background: var(--bg-card);
border: 1px solid var(--glass-border);
border-radius: 24px;
padding: 40px;
width: 100%;
max-width: 450px;
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
}
.brand-icon {
width: 48px;
height: 48px;
background: var(--gradient-primary);
border-radius: 14px;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
margin: 0 auto 20px;
}
h2 {
font-weight: 800;
text-align: center;
margin-bottom: 4px;
font-size: 1.5rem;
}
p.subtitle {
text-align: center;
color: var(--text-muted);
margin-bottom: 28px;
font-size: 0.9rem;
}
/* ── Tabs ─────────────────────────────────────────────── */
.tab-switcher {
display: flex;
background: rgba(15, 23, 42, 0.5);
border-radius: 12px;
padding: 4px;
margin-bottom: 28px;
gap: 4px;
}
.tab-btn {
flex: 1;
padding: 10px;
border: none;
border-radius: 9px;
font-size: 0.85rem;
font-weight: 600;
cursor: pointer;
transition: all 0.25s;
background: transparent;
color: var(--text-muted);
}
.tab-btn.active {
background: var(--gradient-primary);
color: white;
box-shadow: 0 4px 12px rgba(217, 119, 6, 0.35);
}
/* ── Form ─────────────────────────────────────────────── */
.form-label {
color: var(--text-muted);
font-weight: 500;
font-size: 0.82rem;
margin-bottom: 6px;
}
.form-control {
background: rgba(15, 23, 42, 0.5);
border: 1px solid var(--glass-border);
color: white;
padding: 12px 16px;
border-radius: 12px;
transition: all 0.3s;
font-size: 0.92rem;
}
.form-control::placeholder { color: #475569; }
.form-control:focus {
background: rgba(15, 23, 42, 0.8);
border-color: var(--primary);
box-shadow: 0 0 0 3px rgba(245, 158, 11, 0.12);
color: white;
}
.btn-primary {
background: var(--gradient-primary);
border: none;
padding: 14px;
border-radius: 12px;
font-weight: 700;
width: 100%;
margin-top: 16px;
transition: all 0.3s;
font-size: 0.9rem;
letter-spacing: 0.03em;
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 10px 20px -4px rgba(217, 119, 6, 0.4);
}
.btn-primary:disabled {
opacity: 0.6;
transform: none;
}
/* ── Alert ────────────────────────────────────────────── */
.alert-box {
padding: 12px 16px;
border-radius: 10px;
font-size: 0.85rem;
margin-bottom: 20px;
display: none;
text-align: center;
border: 1px solid;
}
.alert-box.error {
color: #f87171;
background: rgba(239, 68, 68, 0.08);
border-color: rgba(239, 68, 68, 0.2);
}
.alert-box.success {
color: #4ade80;
background: rgba(74, 222, 128, 0.08);
border-color: rgba(74, 222, 128, 0.2);
}
/* ── Input group icon ─────────────────────────────────── */
.input-icon {
position: relative;
}
.input-icon .bi {
position: absolute;
left: 14px;
top: 50%;
transform: translateY(-50%);
color: #475569;
font-size: 1rem;
pointer-events: none;
}
.input-icon .form-control {
padding-left: 40px;
}
.back-link {
display: block;
text-align: center;
margin-top: 24px;
color: var(--text-muted);
text-decoration: none;
font-size: 0.875rem;
transition: color 0.3s;
}
.back-link:hover { color: var(--primary); }
.panel { display: none; }
.panel.active { display: block; }
</style>
</head>
<body>
<div class="login-card">
<div class="brand-icon">
<i class="bi bi-person-fill text-white"></i>
</div>
<h2>Área do Cliente</h2>
<p class="subtitle">Acesse seus tickets e promoções</p>
<!-- Tabs -->
<div class="tab-switcher">
<button class="tab-btn active" onclick="switchTab('login')">
<i class="bi bi-box-arrow-in-right me-1"></i> Entrar
</button>
<button class="tab-btn" onclick="switchTab('register')">
<i class="bi bi-person-plus me-1"></i> Cadastrar
</button>
</div>
<!-- Alert -->
<div id="alert-box" class="alert-box error"></div>
<!-- ── Login Panel ───────────────────────────────────── -->
<div id="panel-login" class="panel active">
<form id="loginForm" autocomplete="on">
<div class="mb-3">
<label class="form-label">CPF</label>
<div class="input-icon">
<i class="bi bi-card-text"></i>
<input type="text" class="form-control" id="login-cpf" placeholder="000.000.000-00" required autocomplete="username">
</div>
</div>
<div class="mb-4">
<label class="form-label">Telefone</label>
<div class="input-icon">
<i class="bi bi-phone"></i>
<input type="text" class="form-control" id="login-telefone" placeholder="(00) 00000-0000" required autocomplete="tel">
</div>
</div>
<button type="submit" class="btn btn-primary" id="btn-login">
ACESSAR MINHA CONTA
</button>
</form>
<p style="text-align:center; color:var(--text-muted); font-size:0.82rem; margin-top:20px;">
Ainda não tem cadastro?
<a href="#" onclick="switchTab('register'); return false;" style="color:var(--primary); text-decoration:none; font-weight:600;">Cadastre-se</a>
</p>
</div>
<!-- ── Register Panel ────────────────────────────────── -->
<div id="panel-register" class="panel">
<form id="registerForm" autocomplete="on">
<div class="mb-3">
<label class="form-label">Nome completo</label>
<div class="input-icon">
<i class="bi bi-person"></i>
<input type="text" class="form-control" id="reg-nome" placeholder="Seu nome" required autocomplete="name">
</div>
</div>
<div class="mb-3">
<label class="form-label">E-mail</label>
<div class="input-icon">
<i class="bi bi-envelope"></i>
<input type="email" class="form-control" id="reg-email" placeholder="seu@email.com" required autocomplete="email">
</div>
</div>
<div class="mb-3">
<label class="form-label">CPF</label>
<div class="input-icon">
<i class="bi bi-card-text"></i>
<input type="text" class="form-control" id="reg-cpf" placeholder="000.000.000-00" required>
</div>
</div>
<div class="mb-4">
<label class="form-label">Telefone (WhatsApp)</label>
<div class="input-icon">
<i class="bi bi-whatsapp"></i>
<input type="text" class="form-control" id="reg-telefone" placeholder="(00) 00000-0000" required autocomplete="tel">
</div>
</div>
<button type="submit" class="btn btn-primary" id="btn-register">
CRIAR MINHA CONTA
</button>
</form>
<p style="text-align:center; color:var(--text-muted); font-size:0.82rem; margin-top:20px;">
Já tem cadastro?
<a href="#" onclick="switchTab('login'); return false;" style="color:var(--primary); text-decoration:none; font-weight:600;">Entrar</a>
</p>
</div>
<a href="/" class="back-link">
<i class="bi bi-arrow-left me-1"></i> Voltar para o início
</a>
</div>
<script src="https://unpkg.com/imask"></script>
<script>
// ── Masks ─────────────────────────────────────────────
IMask(document.getElementById('login-cpf'), { mask: '000.000.000-00' });
IMask(document.getElementById('login-telefone'),{ mask: '(00) 00000-0000' });
IMask(document.getElementById('reg-cpf'), { mask: '000.000.000-00' });
IMask(document.getElementById('reg-telefone'), { mask: '(00) 00000-0000' });
// ── Tab switcher ──────────────────────────────────────
function switchTab(tab) {
document.querySelectorAll('.tab-btn').forEach((b, i) => {
b.classList.toggle('active', (tab === 'login' && i === 0) || (tab === 'register' && i === 1));
});
document.getElementById('panel-login').classList.toggle('active', tab === 'login');
document.getElementById('panel-register').classList.toggle('active', tab === 'register');
clearAlert();
}
// ── Alert helper ──────────────────────────────────────
function showAlert(msg, type = 'error') {
const box = document.getElementById('alert-box');
box.textContent = msg;
box.className = 'alert-box ' + type;
box.style.display = 'block';
}
function clearAlert() {
const box = document.getElementById('alert-box');
box.style.display = 'none';
}
function setLoading(btnId, loading) {
const btn = document.getElementById(btnId);
btn.disabled = loading;
btn.textContent = loading ? 'Aguarde...' : btn.dataset.label;
}
// Save original labels
document.getElementById('btn-login').dataset.label = 'ACESSAR MINHA CONTA';
document.getElementById('btn-register').dataset.label = 'CRIAR MINHA CONTA';
// ── Login submit ──────────────────────────────────────
document.getElementById('loginForm').addEventListener('submit', async (e) => {
e.preventDefault();
clearAlert();
setLoading('btn-login', true);
try {
const res = await fetch('/api/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
cpf: document.getElementById('login-cpf').value,
telefone: document.getElementById('login-telefone').value,
})
});
const data = await res.json();
if (data.success) {
window.location.href = data.redirect;
} else {
showAlert(data.error || 'Erro ao realizar login');
}
} catch {
showAlert('Erro de conexão com o servidor');
} finally {
setLoading('btn-login', false);
}
});
// ── Register submit ───────────────────────────────────
document.getElementById('registerForm').addEventListener('submit', async (e) => {
e.preventDefault();
clearAlert();
setLoading('btn-register', true);
try {
const res = await fetch('/api/register', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
nome: document.getElementById('reg-nome').value.trim(),
email: document.getElementById('reg-email').value.trim().toLowerCase(),
cpf: document.getElementById('reg-cpf').value,
telefone: document.getElementById('reg-telefone').value,
})
});
const data = await res.json();
if (data.success) {
showAlert('Conta criada! Redirecionando...', 'success');
setTimeout(() => { window.location.href = data.redirect; }, 800);
} else {
showAlert(data.error || 'Erro ao criar conta');
}
} catch {
showAlert('Erro de conexão com o servidor');
} finally {
setLoading('btn-register', false);
}
});
</script>
</body>
</html>