27.8 kB
RodrigoBrasil2024's picture
<!DOCTYPE html> <html lang="pt-br"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Clube de Fidelidade | Seu Programa de Recompensas</title> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> <script src="https://cdn.tailwindcss.com"></script> <style> :root { --primary: #4f46e5; --primary-light: #6366f1; --secondary: #10b981; --dark: #1e293b; --light: #f8fafc; --success: #10b981; --error: #ef4444; --warning: #f59e0b; } body { font-family: 'Inter', sans-serif; background-color: #f1f5f9; color: var(--dark); } .card { transition: all 0.3s ease; box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); } .card:hover { transform: translateY(-2px); box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); } .reward-card { border-left: 4px solid var(--primary); } .history-item { border-left: 3px solid var(--primary); transition: all 0.2s; } .history-item:hover { background-color: #f8fafc; } .pulse { animation: pulse 2s infinite; } @keyframes pulse { 0% { box-shadow: 0 0 0 0 rgba(79, 70, 229, 0.4); } 70% { box-shadow: 0 0 0 10px rgba(79, 70, 229, 0); } 100% { box-shadow: 0 0 0 0 rgba(79, 70, 229, 0); } } .loading { display: inline-block; width: 20px; height: 20px; border: 3px solid rgba(255,255,255,.3); border-radius: 50%; border-top-color: #fff; animation: spin 1s ease-in-out infinite; } @keyframes spin { to { transform: rotate(360deg); } } .fade-in { animation: fadeIn 0.5s ease-in; } @keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } } </style> </head> <body class="min-h-screen flex items-center justify-center p-4"> <div class="w-full max-w-3xl"> <!-- Header --> <div class="text-center mb-8"> <h1 class="text-3xl font-bold text-indigo-600 mb-2"> <i class="fas fa-crown mr-2"></i>Clube de Fidelidade </h1> <p class="text-slate-600">Acumule pontos e resgate recompensas exclusivas</p> </div> <!-- Main Card --> <div id="mainCard" class="bg-white rounded-xl card p-6 mb-6 fade-in"> <div id="loginSection"> <h2 class="text-xl font-semibold text-slate-800 mb-4">Consulte seus pontos</h2> <div class="mb-4"> <label for="inpTelCliente" class="block text-sm font-medium text-slate-700 mb-1">Telefone (com DDD)</label> <div class="relative"> <div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none"> <i class="fas fa-phone text-slate-400"></i> </div> <input type="text" id="inpTelCliente" maxlength="15" placeholder="Ex: (11) 98765-4321" class="pl-10 w-full rounded-lg border border-slate-300 py-2 px-3 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 transition" onkeypress="formatPhone(this)" > </div> </div> <button onclick="consultarCliente()" id="btnConsultar" class="w-full bg-indigo-600 hover:bg-indigo-700 text-white font-medium py-2 px-4 rounded-lg transition flex items-center justify-center" > <span id="btnText">Consultar pontos</span> <span id="btnLoading" class="hidden ml-2"> <div class="loading"></div> </span> </button> </div> <div id="clientPanel" class="hidden"> <!-- Content will be loaded here --> </div> </div> <!-- Rewards Section (hidden initially) --> <div id="rewardsSection" class="hidden"> <h3 class="text-lg font-semibold text-slate-800 mb-4 flex items-center"> <i class="fas fa-gift text-indigo-500 mr-2"></i> Recompensas disponíveis </h3> <div id="rewardsContainer" class="grid grid-cols-1 md:grid-cols-2 gap-4"> <!-- Rewards will be loaded here --> </div> </div> <!-- History Section (hidden initially) --> <div id="historySection" class="hidden mt-8"> <h3 class="text-lg font-semibold text-slate-800 mb-4 flex items-center"> <i class="fas fa-history text-indigo-500 mr-2"></i> Histórico de pontos </h3> <div class="bg-white rounded-xl card p-4"> <div id="historyContainer"> <!-- History will be loaded here --> </div> </div> </div> </div> <!-- Modal --> <div id="modal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 hidden"> <div class="bg-white rounded-xl p-6 max-w-md w-full mx-4"> <div class="flex justify-between items-center mb-4"> <h3 id="modalTitle" class="text-xl font-semibold"></h3> <button onclick="closeModal()" class="text-slate-500 hover:text-slate-700"> <i class="fas fa-times"></i> </button> </div> <div id="modalContent" class="mb-4"></div> <div class="flex justify-end space-x-3"> <button onclick="closeModal()" class="px-4 py-2 rounded-lg border border-slate-300 text-slate-700 hover:bg-slate-50 transition">Cancelar</button> <button id="modalActionBtn" onclick="modalAction()" class="px-4 py-2 rounded-lg bg-indigo-600 text-white hover:bg-indigo-700 transition"></button> </div> </div> </div> <script> // Global variables let currentClient = null; let currentRewards = []; // Format phone input function formatPhone(input) { let value = input.value.replace(/\D/g, ''); if (value.length > 11) { value = value.substring(0, 11); } if (value.length > 0) { value = '(' + value; } if (value.length > 3) { value = value.substring(0, 3) + ') ' + value.substring(3); } if (value.length > 10) { value = value.substring(0, 10) + '-' + value.substring(10); } input.value = value; } // Only numbers helper function soNumeros(str) { return str.replace(/\D/g, ''); } // Show loading state function setLoading(loading) { const btn = document.getElementById('btnConsultar'); const btnText = document.getElementById('btnText'); const btnLoading = document.getElementById('btnLoading'); if (loading) { btn.disabled = true; btnText.textContent = 'Consultando...'; btnLoading.classList.remove('hidden'); } else { btn.disabled = false; btnText.textContent = 'Consultar pontos'; btnLoading.classList.add('hidden'); } } // Show modal function showModal(title, content, actionText, actionCallback) { document.getElementById('modalTitle').textContent = title; document.getElementById('modalContent').innerHTML = content; document.getElementById('modalActionBtn').textContent = actionText; // Store the callback in the modal element document.getElementById('modal').actionCallback = actionCallback; document.getElementById('modal').classList.remove('hidden'); } // Close modal function closeModal() { document.getElementById('modal').classList.add('hidden'); } // Modal action function modalAction() { const callback = document.getElementById('modal').actionCallback; if (callback) { callback(); } closeModal(); } // Show message function showMessage(type, message) { const mainCard = document.getElementById('mainCard'); // Remove any existing message const existingMsg = document.getElementById('messageAlert'); if (existingMsg) { existingMsg.remove(); } let icon, bgColor, textColor; switch(type) { case 'success': icon = 'fa-check-circle'; bgColor = 'bg-emerald-100'; textColor = 'text-emerald-800'; break; case 'error': icon = 'fa-exclamation-circle'; bgColor = 'bg-red-100'; textColor = 'text-red-800'; break; case 'warning': icon = 'fa-exclamation-triangle'; bgColor = 'bg-amber-100'; textColor = 'text-amber-800'; break; default: icon = 'fa-info-circle'; bgColor = 'bg-blue-100'; textColor = 'text-blue-800'; } const messageDiv = document.createElement('div'); messageDiv.id = 'messageAlert'; messageDiv.className = `${bgColor} ${textColor} p-3 rounded-lg mb-4 flex items-start fade-in`; messageDiv.innerHTML = ` <i class="fas ${icon} mt-1 mr-3"></i> <div>${message}</div> `; mainCard.insertBefore(messageDiv, mainCard.firstChild); // Auto hide after 5 seconds setTimeout(() => { if (messageDiv) { messageDiv.classList.add('opacity-0', 'transition-opacity', 'duration-300'); setTimeout(() => messageDiv.remove(), 300); } }, 5000); } // Consult client async function consultarCliente() { const tel = soNumeros(document.getElementById('inpTelCliente').value.trim()); if (tel.length < 11) { showMessage('error', 'Por favor, digite um telefone válido com DDD!'); return; } setLoading(true); try { // Simulate API call (in a real app, this would be a fetch to your backend) await new Promise(resolve => setTimeout(resolve, 1000)); // Mock response - in a real app, replace with actual API call const mockResponse = { sucesso: true, nome: "João Silva", pontos: 1250, nivel: "Ouro", membroDesde: "2022-05-15" }; // For demo purposes, fail for specific numbers if (tel === '11987654321') { throw new Error("Erro de conexão com o servidor"); } else if (tel === '11999999999') { mockResponse.sucesso = false; } if (!mockResponse.sucesso) { showMessage('error', 'Telefone não cadastrado no programa de fidelidade.'); return; } // Save client data currentClient = { telefone: tel, nome: mockResponse.nome, pontos: mockResponse.pontos, nivel: mockResponse.nivel, membroDesde: mockResponse.membroDesde }; // Show client panel showClientPanel(); } catch (error) { showMessage('error', 'Erro ao consultar: ' + error.message); } finally { setLoading(false); } } // Show client panel function showClientPanel() { document.getElementById('loginSection').classList.add('hidden'); const clientPanel = document.getElementById('clientPanel'); clientPanel.classList.remove('hidden'); clientPanel.innerHTML = ` <div class="flex flex-col md:flex-row md:items-center justify-between mb-6"> <div> <h2 class="text-xl font-semibold text-slate-800">Olá, <span class="text-indigo-600">${currentClient.nome}</span>!</h2> <p class="text-slate-600 text-sm">Membro desde ${formatDate(currentClient.membroDesde)}</p> </div> <div class="mt-3 md:mt-0"> <div class="bg-indigo-50 text-indigo-800 px-4 py-2 rounded-lg inline-flex items-center"> <i class="fas fa-medal mr-2"></i> <span>Nível ${currentClient.nivel}</span> </div> </div> </div> <div class="bg-gradient-to-r from-indigo-500 to-indigo-600 rounded-xl p-4 text-white mb-6"> <div class="flex justify-between items-center"> <div> <p class="text-sm opacity-80">Seu saldo de pontos</p> <p class="text-3xl font-bold">${currentClient.pontos}</p> </div> <div class="bg-white bg-opacity-20 p-3 rounded-full"> <i class="fas fa-coins text-2xl"></i> </div> </div> </div> <div class="flex justify-end"> <button onclick="logout()" class="text-slate-500 hover:text-slate-700 text-sm flex items-center"> <i class="fas fa-sign-out-alt mr-1"></i> Sair </button> </div> `; // Show rewards and history sections document.getElementById('rewardsSection').classList.remove('hidden'); document.getElementById('historySection').classList.remove('hidden'); // Load rewards and history loadRewards(); loadHistory(); } // Format date function formatDate(dateString) { const options = { day: '2-digit', month: '2-digit', year: 'numeric' }; return new Date(dateString).toLocaleDateString('pt-BR', options); } // Load rewards async function loadRewards() { // Simulate loading document.getElementById('rewardsContainer').innerHTML = ` <div class="col-span-2 flex justify-center py-8"> <div class="animate-spin rounded-full h-8 w-8 border-t-2 border-b-2 border-indigo-500"></div> </div> `; try { // Simulate API call await new Promise(resolve => setTimeout(resolve, 800)); // Mock data - replace with actual API call const mockRewards = [ { id: 1, titulo: "Café Grátis", descricao: "Um café especial da casa", pontos_necessarios: 200, categoria: "Bebidas", valido_ate: "2023-12-31", disponivel: true }, { id: 2, titulo: "Brinde Exclusivo", descricao: "Caneca personalizada do clube", pontos_necessarios: 500, categoria: "Produtos", valido_ate: "2023-11-30", disponivel: true }, { id: 3, titulo: "Desconto 20%", descricao: "20% de desconto em qualquer pedido", pontos_necessarios: 800, categoria: "Descontos", valido_ate: "2023-12-15", disponivel: true }, { id: 4, titulo: "Jantar para Dois", descricao: "Jantar completo para duas pessoas", pontos_necessarios: 1500, categoria: "Refeições", valido_ate: "2023-12-25", disponivel: true } ]; currentRewards = mockRewards; renderRewards(); } catch (error) { document.getElementById('rewardsContainer').innerHTML = ` <div class="col-span-2 text-center py-8 text-slate-500"> <i class="fas fa-exclamation-triangle text-amber-500 text-2xl mb-2"></i> <p>Não foi possível carregar as recompensas.</p> <button onclick="loadRewards()" class="mt-2 text-indigo-600 hover:text-indigo-800 text-sm flex items-center justify-center mx-auto"> <i class="fas fa-sync-alt mr-1"></i> Tentar novamente </button> </div> `; } } // Render rewards function renderRewards() { const container = document.getElementById('rewardsContainer'); if (currentRewards.length === 0) { container.innerHTML = ` <div class="col-span-2 text-center py-8 text-slate-500"> <i class="fas fa-gift text-3xl mb-3 text-indigo-300"></i> <p>Nenhuma recompensa disponível no momento.</p> </div> `; return; } let html = ''; currentRewards.forEach(reward => { const canRedeem = currentClient.pontos >= reward.pontos_necessarios; const validUntil = formatDate(reward.valido_ate); html += ` <div class="bg-white rounded-xl card p-5 reward-card ${canRedeem ? 'hover:border-indigo-300' : 'opacity-80'}"> <div class="flex justify-between items-start mb-2"> <h4 class="font-semibold text-slate-800">${reward.titulo}</h4> <span class="text-xs bg-slate-100 text-slate-600 px-2 py-1 rounded"> ${reward.categoria} </span> </div> <p class="text-sm text-slate-600 mb-3">${reward.descricao}</p> <div class="flex justify-between items-center"> <div> <span class="text-xs text-slate-500">Válido até ${validUntil}</span> <div class="font-bold text-indigo-600 mt-1"> <i class="fas fa-coins mr-1"></i> ${reward.pontos_necessarios} pts </div> </div> <button onclick="redeemReward(${reward.id})" class="px-3 py-1 rounded-lg text-sm ${canRedeem ? 'bg-indigo-600 hover:bg-indigo-700 text-white' : 'bg-slate-100 text-slate-400 cursor-not-allowed'}" ${canRedeem ? '' : 'disabled'} > ${canRedeem ? 'Resgatar' : 'Pontos insuficientes'} </button> </div> </div> `; }); container.innerHTML = html; } // Redeem reward function redeemReward(rewardId) { const reward = currentRewards.find(r => r.id === rewardId); if (!reward) return; showModal( 'Confirmar resgate', ` <div class="mb-4"> <p>Você está prestes a resgatar:</p> <div class="bg-indigo-50 rounded-lg p-3 mt-2"> <h4 class="font-semibold">${reward.titulo}</h4> <p class="text-sm text-slate-600">${reward.descricao}</p> <div class="mt-2 text-sm"> <span class="font-medium">Custo:</span> <span class="text-indigo-600 font-bold">${reward.pontos_necessarios} pontos</span> </div> </div> <div class="mt-4"> <p class="text-sm">Seu saldo após o resgate:</p> <p class="text-lg font-bold ${currentClient.pontos - reward.pontos_necessarios < 0 ? 'text-red-500' : 'text-indigo-600'}"> ${currentClient.pontos - reward.pontos_necessarios} pontos </p> </div> </div> `, 'Confirmar resgate', () => { // Simulate API call setTimeout(() => { // Update client points currentClient.pontos -= reward.pontos_necessarios; // Add to history addToHistory({ tipo: "Resgate", quantidade: -reward.pontos_necessarios, motivo: reward.titulo, data: new Date().toISOString() }); // Show success message showMessage('success', `Recompensa "${reward.titulo}" resgatada com sucesso!`); // Update UI showClientPanel(); }, 800); } ); } // Load history async function loadHistory() { // Simulate loading document.getElementById('historyContainer').innerHTML = ` <div class="flex justify-center py-8"> <div class="animate-spin rounded-full h-8 w-8 border-t-2 border-b-2 border-indigo-500"></div> </div> `; try { // Simulate API call await new Promise(resolve => setTimeout(resolve, 700)); // Mock data - replace with actual API call const mockHistory = [ { tipo: "Acúmulo", quantidade: 100, motivo: "Compra #12345", data: "2023-10-15T14:30:00" }, { tipo: "Acúmulo", quantidade: 150, motivo: "Compra #12346", data: "2023-10-10T12:15:00" }, { tipo: "Resgate", quantidade: -200, motivo: "Café Grátis", data: "2023-09-28T18:45:00" }, { tipo: "Bônus", quantidade: 50, motivo: "Aniversário", data: "2023-09-15T00:00:00" } ]; renderHistory(mockHistory); } catch (error) { document.getElementById('historyContainer').innerHTML = ` <div class="text-center py-8 text-slate-500"> <i class="fas fa-exclamation-triangle text-amber-500 text-2xl mb-2"></i> <p>Não foi possível carregar o histórico.</p> <button onclick="loadHistory()" class="mt-2 text-indigo-600 hover:text-indigo-800 text-sm flex items-center justify-center mx-auto"> <i class="fas fa-sync-alt mr-1"></i> Tentar novamente </button> </div> `; } } // Render history function renderHistory(history) { const container = document.getElementById('historyContainer'); if (history.length === 0) { container.innerHTML = ` <div class="text-center py-8 text-slate-500"> <i class="fas fa-history text-2xl mb-3 text-indigo-300"></i> <p>Nenhum registro no histórico.</p> </div> `; return; } let html = '<div class="space-y-3">'; history.forEach(item => { const isPositive = item.quantidade > 0; const dateTime = formatDateTime(item.data); html += ` <div class="history-item pl-4 py-3"> <div class="flex justify-between items-start"> <div> <p class="font-medium text-slate-800">${item.tipo}</p> <p class="text-sm text-slate-500">${item.motivo}</p> </div> <div class="text-right"> <p class="font-bold ${isPositive ? 'text-emerald-600' : 'text-red-600'}"> ${isPositive ? '+' : ''}${item.quantidade} pts </p> <p class="text-xs text-slate-400">${dateTime}</p> </html> vamos atualizar o painel do cliente do clube de fideliade - Follow Up Deployment
49bbdb8 verified