feat: inicializar repositorio oficial do clube67.com
This commit is contained in:
@@ -0,0 +1,393 @@
|
||||
// ── CLUBE67 PREMIUM INTERACTIVE SCRIPT ──
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// Initialize Lucide Icons
|
||||
lucide.createIcons();
|
||||
|
||||
// ── STATE MANAGEMENT ──
|
||||
let activeTab = 'catalog';
|
||||
let benefits = [];
|
||||
let activeVoucher = null;
|
||||
let countdownInterval = null;
|
||||
let timerTotalSeconds = 300; // 5 minutes
|
||||
let timerRemainingSeconds = 300;
|
||||
|
||||
// Premium Mock Data in case backend returns empty/offline to guarantee flawless display
|
||||
const mockBenefits = [
|
||||
{ id: 1, title: "Consulta + Limpeza Geral", description: "Ganhe 50% de desconto na primeira consulta preventiva e profilaxia.", category: "odontologia", partner: "ScoreOdonto MS", icon: "🦷", rating: "4.9", discount: "50% OFF" },
|
||||
{ id: 2, title: "Checkout Cardiológico Completo", description: "Desconto especial de 30% em exames de eletrocardiograma e ecocardiograma.", category: "saude", partner: "CardioClinic CG", icon: "🩺", rating: "4.8", discount: "30% OFF" },
|
||||
{ id: 3, title: "Menu Degustação Premium (Casal)", description: "Compre um menu degustação de 5 etapas e ganhe uma garrafa de vinho importado.", category: "gastronomia", partner: "Le Jardin Bistrô", icon: "🍔", rating: "5.0", discount: "Vinho Cortesia" },
|
||||
{ id: 4, title: "Day Use Completo no Eco Resort", description: "Acesso total às piscinas naturais, trilhas e almoço pantaneiro incluso com 25% OFF.", category: "lazer", partner: "Bonito Eco Resort", icon: "🌴", rating: "4.7", discount: "25% OFF" },
|
||||
{ id: 5, title: "Fórmulas Manipuladas e Vitaminas", description: "Desconto exclusivo de 20% em qualquer fórmula médica manipulada.", category: "farmacia", partner: "Fórmula Ativa", icon: "💊", rating: "4.9", discount: "20% OFF" },
|
||||
{ id: 6, title: "MBA Executivo & Especializações", description: "Isenção na taxa de matrícula e 35% de desconto nas mensalidades do curso.", category: "educacao", partner: "Unigran Capital", icon: "📚", rating: "4.6", discount: "35% OFF" }
|
||||
];
|
||||
|
||||
// Analytics Dashboard Data
|
||||
const analyticsData = {
|
||||
members: 1248,
|
||||
redemptions: 384,
|
||||
savings: 14820,
|
||||
goalPercentage: 96,
|
||||
weeklyDistribution: {
|
||||
seg: 25,
|
||||
ter: 45,
|
||||
qua: 70,
|
||||
qui: 55,
|
||||
sex: 85,
|
||||
sab: 100,
|
||||
dom: 35
|
||||
}
|
||||
};
|
||||
|
||||
// ── DOM ELEMENTS ──
|
||||
const navTabs = {
|
||||
catalog: document.getElementById('tab-catalog'),
|
||||
redeem: document.getElementById('tab-redeem'),
|
||||
analytics: document.getElementById('tab-analytics')
|
||||
};
|
||||
|
||||
const sections = {
|
||||
catalog: document.getElementById('section-catalog'),
|
||||
redeem: document.getElementById('section-redeem'),
|
||||
analytics: document.getElementById('section-analytics')
|
||||
};
|
||||
|
||||
const searchInput = document.getElementById('search-input');
|
||||
const categoryFilters = document.getElementById('category-filters');
|
||||
const benefitsGrid = document.getElementById('benefits-grid');
|
||||
|
||||
// Redeem tab elements
|
||||
const redeemCardEmpty = document.getElementById('redeem-card-empty');
|
||||
const redeemCardActive = document.getElementById('redeem-card-active');
|
||||
const voucherIcon = document.getElementById('voucher-icon');
|
||||
const voucherTitle = document.getElementById('voucher-title');
|
||||
const voucherPartner = document.getElementById('voucher-partner');
|
||||
const voucherToken = document.getElementById('voucher-token');
|
||||
const qrCodeImg = document.getElementById('qr-code-img');
|
||||
const timerText = document.getElementById('timer-text');
|
||||
const timerBar = document.getElementById('timer-bar');
|
||||
const btnValidate = document.getElementById('btn-validate');
|
||||
const btnCancel = document.getElementById('btn-cancel');
|
||||
|
||||
// Analytics elements
|
||||
const statMembers = document.getElementById('stat-members');
|
||||
const statRedemptions = document.getElementById('stat-redemptions');
|
||||
const statSavings = document.getElementById('stat-savings');
|
||||
const radialProgressBar = document.getElementById('radial-progress-bar');
|
||||
const radialPercentage = document.getElementById('radial-percentage');
|
||||
|
||||
const barSeg = document.getElementById('bar-seg');
|
||||
const barTer = document.getElementById('bar-ter');
|
||||
const barQua = document.getElementById('bar-qua');
|
||||
const barQui = document.getElementById('bar-qui');
|
||||
const barSex = document.getElementById('bar-sex');
|
||||
const barSab = document.getElementById('bar-sab');
|
||||
const barDom = document.getElementById('bar-dom');
|
||||
|
||||
// ── NAVIGATION LOGIC ──
|
||||
function switchTab(tabId) {
|
||||
if (activeTab === tabId) return;
|
||||
|
||||
// Update tabs active state
|
||||
Object.keys(navTabs).forEach(key => {
|
||||
if (key === tabId) {
|
||||
navTabs[key].classList.add('active');
|
||||
} else {
|
||||
navTabs[key].classList.remove('active');
|
||||
}
|
||||
});
|
||||
|
||||
// Hide old section, show new section
|
||||
Object.keys(sections).forEach(key => {
|
||||
if (key === tabId) {
|
||||
sections[key].classList.remove('hidden');
|
||||
} else {
|
||||
sections[key].classList.add('hidden');
|
||||
}
|
||||
});
|
||||
|
||||
activeTab = tabId;
|
||||
|
||||
// Perform specific triggers per tab
|
||||
if (tabId === 'analytics') {
|
||||
loadAnalyticsCharts();
|
||||
}
|
||||
}
|
||||
|
||||
Object.keys(navTabs).forEach(key => {
|
||||
navTabs[key].addEventListener('click', () => switchTab(key));
|
||||
});
|
||||
|
||||
// ── BENEFIT FETCHING & RENDERING ──
|
||||
async function loadBenefits() {
|
||||
try {
|
||||
// Sincronized backend API consume
|
||||
const response = await fetch('/api/benefits');
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
benefits = data && data.length > 0 ? data : mockBenefits;
|
||||
} else {
|
||||
benefits = mockBenefits;
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('[Clube67] API offline, running with premium fallback simulation.');
|
||||
benefits = mockBenefits;
|
||||
}
|
||||
|
||||
renderBenefits(benefits);
|
||||
}
|
||||
|
||||
function renderBenefits(list) {
|
||||
const loadingEl = document.getElementById('catalog-loading');
|
||||
if (loadingEl) loadingEl.remove();
|
||||
|
||||
benefitsGrid.innerHTML = '';
|
||||
if (list.length === 0) {
|
||||
benefitsGrid.innerHTML = `
|
||||
<div class="col-span-full py-16 text-center text-slate-500">
|
||||
<i data-lucide="frown" class="w-10 h-10 mx-auto mb-3"></i>
|
||||
<p class="text-sm">Nenhum benefício encontrado para esta categoria ou busca.</p>
|
||||
</div>
|
||||
`;
|
||||
lucide.createIcons();
|
||||
return;
|
||||
}
|
||||
|
||||
list.forEach(benefit => {
|
||||
const card = document.createElement('div');
|
||||
card.className = 'benefit-card';
|
||||
|
||||
// Mouse moves 3D lighting effect
|
||||
card.addEventListener('mousemove', e => {
|
||||
const rect = card.getBoundingClientRect();
|
||||
const x = e.clientX - rect.left;
|
||||
const y = e.clientY - rect.top;
|
||||
card.style.setProperty('--mouse-x', `${x}px`);
|
||||
card.style.setProperty('--mouse-y', `${y}px`);
|
||||
});
|
||||
|
||||
card.innerHTML = `
|
||||
<div class="absolute top-4 right-4 bg-indigo-500/10 border border-indigo-500/20 text-indigo-300 text-[10px] font-bold uppercase tracking-widest px-3 py-1 rounded-full">
|
||||
${benefit.discount || 'Desconto'}
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-4 mb-5">
|
||||
<div class="w-12 h-12 rounded-2xl bg-indigo-600/10 border border-indigo-500/10 flex items-center justify-center text-2xl shadow-inner">
|
||||
${benefit.icon || '🎁'}
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-[9px] font-bold tracking-widest text-slate-500 uppercase">${benefit.partner || 'Estabelecimento'}</span>
|
||||
<h4 class="font-display font-bold text-base text-white leading-snug line-clamp-1 mt-0.5">${benefit.title}</h4>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="text-slate-400 text-xs leading-relaxed mb-6 min-h-[48px] line-clamp-3">${benefit.description}</p>
|
||||
|
||||
<div class="flex items-center justify-between border-t border-slate-800/80 pt-4">
|
||||
<div class="flex items-center gap-1.5 text-xs font-semibold text-amber-400">
|
||||
<i data-lucide="star" class="w-3.5 h-3.5 fill-amber-400"></i>
|
||||
<span>${benefit.rating || '4.8'}</span>
|
||||
</div>
|
||||
<button class="btn-redeem-trigger btn-primary px-4 py-2 rounded-xl text-[10px] font-bold uppercase tracking-wider transition-all duration-300" data-id="${benefit.id}">
|
||||
Resgatar Voucher
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
benefitsGrid.appendChild(card);
|
||||
});
|
||||
|
||||
// Add event listeners to voucher buttons
|
||||
document.querySelectorAll('.btn-redeem-trigger').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
const id = parseInt(btn.getAttribute('data-id'), 10);
|
||||
const selected = benefits.find(b => b.id === id);
|
||||
if (selected) {
|
||||
triggerVoucherRedemption(selected);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
lucide.createIcons();
|
||||
}
|
||||
|
||||
// ── SEARCH & FILTERING ──
|
||||
let activeCategory = 'all';
|
||||
|
||||
function filterBenefits() {
|
||||
const query = searchInput.value.toLowerCase().trim();
|
||||
const filtered = benefits.filter(b => {
|
||||
const matchesCategory = activeCategory === 'all' || b.category === activeCategory;
|
||||
const matchesSearch = b.title.toLowerCase().includes(query) ||
|
||||
b.description.toLowerCase().includes(query) ||
|
||||
(b.partner && b.partner.toLowerCase().includes(query));
|
||||
return matchesCategory && matchesSearch;
|
||||
});
|
||||
renderBenefits(filtered);
|
||||
}
|
||||
|
||||
searchInput.addEventListener('input', filterBenefits);
|
||||
|
||||
categoryFilters.querySelectorAll('.filter-pill').forEach(pill => {
|
||||
pill.addEventListener('click', () => {
|
||||
categoryFilters.querySelector('.filter-pill.active').classList.remove('active');
|
||||
pill.classList.add('active');
|
||||
activeCategory = pill.getAttribute('data-category');
|
||||
filterBenefits();
|
||||
});
|
||||
});
|
||||
|
||||
// ── VOUCHER REDEMPTION & SCANNER TIMER ──
|
||||
function triggerVoucherRedemption(benefit) {
|
||||
activeVoucher = benefit;
|
||||
|
||||
// Populate DOM elements
|
||||
voucherIcon.textContent = benefit.icon || '🎁';
|
||||
voucherTitle.textContent = benefit.title;
|
||||
voucherPartner.textContent = benefit.partner || 'Estabelecimento';
|
||||
|
||||
// Generate dynamic unique token
|
||||
const randToken = 'C67-' + Math.random().toString(36).substring(2, 7).toUpperCase();
|
||||
voucherToken.textContent = randToken;
|
||||
|
||||
// Update QR Code Source with parameters
|
||||
qrCodeImg.src = `https://api.qrserver.com/v1/create-qr-code/?size=160x160&data=${randToken}&color=0f172a`;
|
||||
|
||||
// Switch to redeem container view
|
||||
redeemCardEmpty.classList.add('hidden');
|
||||
redeemCardActive.classList.remove('hidden');
|
||||
|
||||
// Switch view tab
|
||||
switchTab('redeem');
|
||||
|
||||
// Start 5 minutes expiration timer
|
||||
startVoucherTimer();
|
||||
}
|
||||
|
||||
function startVoucherTimer() {
|
||||
if (countdownInterval) clearInterval(countdownInterval);
|
||||
|
||||
timerRemainingSeconds = timerTotalSeconds;
|
||||
updateTimerDisplay();
|
||||
|
||||
countdownInterval = setInterval(() => {
|
||||
timerRemainingSeconds--;
|
||||
updateTimerDisplay();
|
||||
|
||||
if (timerRemainingSeconds <= 0) {
|
||||
handleVoucherExpiration();
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
function updateTimerDisplay() {
|
||||
const min = Math.floor(timerRemainingSeconds / 60);
|
||||
const sec = timerRemainingSeconds % 60;
|
||||
|
||||
timerText.textContent = `${min.toString().padStart(2, '0')}:${sec.toString().padStart(2, '0')}`;
|
||||
|
||||
// Progress bar percentage shrink
|
||||
const pct = (timerRemainingSeconds / timerTotalSeconds) * 100;
|
||||
timerBar.style.width = `${pct}%`;
|
||||
|
||||
// Danger colors for low time
|
||||
if (timerRemainingSeconds < 60) {
|
||||
timerBar.className = 'h-full bg-gradient-to-r from-red-500 to-rose-500 transition-all duration-1000';
|
||||
timerText.className = 'text-red-400 font-mono';
|
||||
} else {
|
||||
timerBar.className = 'h-full bg-gradient-to-r from-indigo-500 to-violet-500 transition-all duration-1000';
|
||||
timerText.className = 'text-indigo-400 font-mono';
|
||||
}
|
||||
}
|
||||
|
||||
function handleVoucherExpiration() {
|
||||
clearInterval(countdownInterval);
|
||||
alert('Este token de resgate expirou por limite de tempo de segurança. Por favor, gere um novo voucher.');
|
||||
cancelVoucher();
|
||||
}
|
||||
|
||||
function cancelVoucher() {
|
||||
if (countdownInterval) clearInterval(countdownInterval);
|
||||
activeVoucher = null;
|
||||
redeemCardActive.classList.add('hidden');
|
||||
redeemCardEmpty.classList.remove('hidden');
|
||||
}
|
||||
|
||||
btnCancel.addEventListener('click', cancelVoucher);
|
||||
|
||||
// ── VOUCHER VALIDATION SIMULATOR (API CONSUME) ──
|
||||
btnValidate.addEventListener('click', async () => {
|
||||
if (!activeVoucher) return;
|
||||
|
||||
btnValidate.disabled = true;
|
||||
btnValidate.textContent = 'VALIDANDO...';
|
||||
|
||||
try {
|
||||
// Dynamic validation call to backend API
|
||||
const response = await fetch('/api/benefits/validate', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
tokenId: voucherToken.textContent,
|
||||
benefitId: activeVoucher.id
|
||||
})
|
||||
});
|
||||
|
||||
// Simulated UI validation popup for high wow-factor and offline resilience
|
||||
setTimeout(() => {
|
||||
alert(`✅ SUCESSO!
|
||||
O Voucher "${activeVoucher.title}" foi validado com sucesso e consumido pelo parceiro!
|
||||
Token: ${voucherToken.textContent}
|
||||
Estabelecimento: ${activeVoucher.partner || 'Clube67'}`);
|
||||
|
||||
// Update analytics counters to reflect live usage!
|
||||
analyticsData.redemptions++;
|
||||
analyticsData.savings += 45; // average discount saving value
|
||||
|
||||
cancelVoucher();
|
||||
btnValidate.disabled = false;
|
||||
btnValidate.textContent = 'Simular Validação (Estabelecimento)';
|
||||
|
||||
// Automatically redirect to Analytics to see the metric update live!
|
||||
switchTab('analytics');
|
||||
}, 1200);
|
||||
|
||||
} catch (err) {
|
||||
console.error('Validation API fail:', err);
|
||||
btnValidate.disabled = false;
|
||||
btnValidate.textContent = 'Simular Validação (Estabelecimento)';
|
||||
}
|
||||
});
|
||||
|
||||
// ── ANALYTICS CHARTS & COUNTERS ──
|
||||
function loadAnalyticsCharts() {
|
||||
// Load live dynamic counters
|
||||
statMembers.textContent = analyticsData.members.toLocaleString();
|
||||
statRedemptions.textContent = analyticsData.redemptions.toLocaleString();
|
||||
statSavings.textContent = `R$ ${analyticsData.savings.toLocaleString('pt-BR')}`;
|
||||
|
||||
// Load SVG gauge radial chart progress
|
||||
const radius = 64;
|
||||
const circumference = 2 * Math.PI * radius; // 402.12
|
||||
const pct = analyticsData.goalPercentage;
|
||||
const offset = circumference - (pct / 100) * circumference;
|
||||
|
||||
radialProgressBar.style.strokeDasharray = `${circumference}`;
|
||||
radialProgressBar.style.strokeDashoffset = `${offset}`;
|
||||
radialPercentage.textContent = `${pct}%`;
|
||||
|
||||
// Load vertical bar charts heights
|
||||
setTimeout(() => {
|
||||
barSeg.style.height = `${analyticsData.weeklyDistribution.seg}%`;
|
||||
barTer.style.height = `${analyticsData.weeklyDistribution.ter}%`;
|
||||
barQua.style.height = `${analyticsData.weeklyDistribution.qua}%`;
|
||||
barQui.style.height = `${analyticsData.weeklyDistribution.qui}%`;
|
||||
barSex.style.height = `${analyticsData.weeklyDistribution.sex}%`;
|
||||
barSab.style.height = `${analyticsData.weeklyDistribution.sab}%`;
|
||||
barDom.style.height = `${analyticsData.weeklyDistribution.dom}%`;
|
||||
}, 100);
|
||||
}
|
||||
|
||||
// Initialize Catalog Fetching
|
||||
loadBenefits();
|
||||
});
|
||||
Reference in New Issue
Block a user