feat: initial project structure (Model Project) - Backend + Multi-Frontend + Docker
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -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>
|
||||
@@ -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
@@ -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>
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user