feat: Implementa páginas de destino dinâmicas e configuráveis com rastreamento de cliques.
This commit is contained in:
parent
06ffb93d9c
commit
70bc4dae99
@ -2,19 +2,21 @@
|
||||
<html lang="pt-BR">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0" />
|
||||
<title><%= @landing_host&.page_title.presence || 'Atendimento WhatsApp' %></title>
|
||||
<link id="pageFavicon" rel="icon" type="image/png" href="<%= @landing_host&.logo_url || 'https://iachat.hoteis1001noites.com.br/assets/images/dashboard/captain/logo.svg' %>" />
|
||||
<style>
|
||||
:root {
|
||||
--bg-1: #040b18;
|
||||
--bg-2: #031325;
|
||||
--card: #0f1729;
|
||||
--card-border: #1f2c43;
|
||||
--text-1: #e7ecf6;
|
||||
--text-2: #96a2b5;
|
||||
--btn: <%= @landing_host&.theme_color.presence || '#27c15b' %>;
|
||||
--btn-text: #f4fff7;
|
||||
--card-bg: rgba(15, 23, 41, 0.65);
|
||||
--card-border: rgba(255, 255, 255, 0.08);
|
||||
--text-1: #ffffff;
|
||||
--text-2: #a1b0c8;
|
||||
--btn: <%= @landing_host&.theme_color.presence || '#25D366' %>;
|
||||
--btn-text: #ffffff;
|
||||
--timer-bg: rgba(255, 255, 255, 0.05);
|
||||
--timer-alert: #ef4444;
|
||||
}
|
||||
|
||||
* {
|
||||
@ -23,88 +25,207 @@
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
min-height: 100vh;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||
/* prevent scroll by using 100vh absolute and hidden overflow occasionally, but flex is better */
|
||||
min-height: 100dvh;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Inter", Roboto, Helvetica, Arial, sans-serif;
|
||||
color: var(--text-1);
|
||||
background:
|
||||
radial-gradient(circle at 50% 52%, rgba(31, 150, 91, 0.18), transparent 34%),
|
||||
radial-gradient(circle at 30% 30%, rgba(16, 92, 172, 0.16), transparent 46%),
|
||||
linear-gradient(170deg, var(--bg-2), var(--bg-1));
|
||||
radial-gradient(circle at top right, rgba(37, 211, 102, 0.12), transparent 40%),
|
||||
radial-gradient(circle at bottom left, rgba(16, 92, 172, 0.15), transparent 50%),
|
||||
linear-gradient(135deg, var(--bg-1), var(--bg-2));
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
overflow: hidden; /* Força não ter scroll vertical se o conteúdo couber perfeitamente */
|
||||
}
|
||||
|
||||
.page {
|
||||
min-height: 100vh;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
padding: 20px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.card {
|
||||
width: min(100%, 480px);
|
||||
width: min(100%, 420px);
|
||||
max-height: 95vh;
|
||||
background: var(--card-bg);
|
||||
border: 1px solid var(--card-border);
|
||||
background: rgba(15, 23, 41, 0.9);
|
||||
border-radius: 20px;
|
||||
padding: 34px 28px;
|
||||
border-radius: 24px;
|
||||
padding: clamp(20px, 4vh, 32px) clamp(16px, 4vw, 24px);
|
||||
text-align: center;
|
||||
box-shadow: 0 24px 56px rgba(0, 0, 0, 0.35);
|
||||
box-shadow: 0 30px 60px rgba(0, 0, 0, 0.4), inset 0 1px 0 rgba(255, 255, 255, 0.1);
|
||||
backdrop-filter: blur(16px);
|
||||
-webkit-backdrop-filter: blur(16px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.logo-wrap {
|
||||
width: 82px;
|
||||
height: 82px;
|
||||
margin: 0 auto 18px;
|
||||
border-radius: 999px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
background: rgba(40, 215, 122, 0.16);
|
||||
width: clamp(70px, 18vw, 84px);
|
||||
height: clamp(70px, 18vw, 84px);
|
||||
margin: 0 auto clamp(12px, 3vh, 18px);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background: #ffffff; /* Fundo branco fixo para logos com fundo branco */
|
||||
border: 2px solid rgba(255,255,255,0.1);
|
||||
box-shadow: 0 8px 16px rgba(0,0,0,0.2);
|
||||
flex-shrink: 0;
|
||||
overflow: hidden; /* Garante que a imagem não vaze o círculo */
|
||||
}
|
||||
|
||||
.logo-wrap img {
|
||||
width: 58px;
|
||||
height: 58px;
|
||||
border-radius: 8px;
|
||||
object-fit: cover;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover; /* Faz a imagem preencher todo o espaço circular */
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 0;
|
||||
font-size: clamp(29px, 4.2vw, 40px);
|
||||
line-height: 1.15;
|
||||
font-size: clamp(24px, 6vw, 32px);
|
||||
font-weight: 800;
|
||||
line-height: 1.1;
|
||||
letter-spacing: -0.02em;
|
||||
text-wrap: balance;
|
||||
color: var(--text-1);
|
||||
text-shadow: 0 2px 4px rgba(0,0,0,0.3);
|
||||
}
|
||||
|
||||
p.subtitle {
|
||||
margin: 14px 0 26px;
|
||||
font-size: 18px;
|
||||
margin: clamp(8px, 2vh, 12px) 0 clamp(16px, 3vh, 24px);
|
||||
font-size: clamp(14px, 3.5vw, 16px);
|
||||
color: var(--text-2);
|
||||
line-height: 1.4;
|
||||
text-wrap: balance;
|
||||
}
|
||||
|
||||
/* Timer Area */
|
||||
.timer-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
background: var(--timer-bg);
|
||||
border: 1px solid rgba(255,255,255,0.04);
|
||||
padding: clamp(8px, 2vh, 12px) 20px;
|
||||
border-radius: 12px;
|
||||
margin-bottom: clamp(16px, 3vh, 24px);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.timer-label {
|
||||
font-size: 11px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
color: var(--text-2);
|
||||
margin-bottom: 4px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.timer-value {
|
||||
font-size: clamp(28px, 7vw, 36px);
|
||||
font-weight: 800;
|
||||
font-variant-numeric: tabular-nums;
|
||||
color: var(--btn);
|
||||
text-shadow: 0 0 15px rgba(37, 211, 102, 0.3);
|
||||
line-height: 1;
|
||||
transition: color 0.3s;
|
||||
}
|
||||
|
||||
.timer-value.alert {
|
||||
color: var(--timer-alert);
|
||||
text-shadow: 0 0 15px rgba(239, 68, 68, 0.4);
|
||||
animation: pulse-alert 2s infinite;
|
||||
}
|
||||
|
||||
.wa-button {
|
||||
width: 100%;
|
||||
border: 0;
|
||||
border-radius: 14px;
|
||||
padding: 18px 22px;
|
||||
font-size: 34px;
|
||||
padding: clamp(14px, 3.5vh, 18px) 20px;
|
||||
font-size: clamp(18px, 4.5vw, 22px);
|
||||
font-weight: 800;
|
||||
color: var(--btn-text);
|
||||
background: var(--btn);
|
||||
cursor: pointer;
|
||||
transition: transform 0.14s ease, opacity 0.14s ease;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4px 14px rgba(0,0,0,0.25), 0 0 0 0 transparent;
|
||||
transition: transform 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275), box-shadow 0.2s;
|
||||
animation: pulse-button 2.5s infinite;
|
||||
}
|
||||
|
||||
.wa-button:hover {
|
||||
transform: translateY(-1px);
|
||||
/* Shine effect */
|
||||
.wa-button::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -50%;
|
||||
left: -50%;
|
||||
width: 200%;
|
||||
height: 200%;
|
||||
background: linear-gradient(
|
||||
to right,
|
||||
rgba(255, 255, 255, 0) 0%,
|
||||
rgba(255, 255, 255, 0.3) 50%,
|
||||
rgba(255, 255, 255, 0) 100%
|
||||
);
|
||||
transform: rotate(30deg);
|
||||
animation: shine 4s infinite linear;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.wa-button:active {
|
||||
transform: translateY(0);
|
||||
opacity: 0.9;
|
||||
transform: scale(0.97);
|
||||
animation: none;
|
||||
}
|
||||
|
||||
.foot {
|
||||
margin-top: 14px;
|
||||
font-size: 14px;
|
||||
color: #647086;
|
||||
margin-top: clamp(12px, 3vh, 18px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: clamp(11px, 2.5vw, 13px);
|
||||
color: #5d6a80;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.foot svg {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
/* Animations */
|
||||
@keyframes pulse-button {
|
||||
0% { box-shadow: 0 0 0 0 rgba(37, 211, 102, 0.4); }
|
||||
70% { box-shadow: 0 0 0 10px rgba(37, 211, 102, 0); }
|
||||
100% { box-shadow: 0 0 0 0 rgba(37, 211, 102, 0); }
|
||||
}
|
||||
|
||||
@keyframes shine {
|
||||
0% { left: -100%; }
|
||||
20% { left: 100%; }
|
||||
100% { left: 100%; }
|
||||
}
|
||||
|
||||
@keyframes pulse-alert {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.6; }
|
||||
}
|
||||
|
||||
/* Extremely small height screen adjustments (iPhone SE landscape) */
|
||||
@media (max-height: 500px) {
|
||||
body { overflow-y: auto; }
|
||||
.card { padding: 16px; margin: 16px 0; }
|
||||
.logo-wrap { width: 40px; height: 40px; margin-bottom: 8px;}
|
||||
.timer-container { margin-bottom: 12px; padding: 6px; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
@ -123,9 +244,20 @@
|
||||
<img src="<%= @landing_host.logo_url.presence || 'https://iachat.hoteis1001noites.com.br/assets/images/dashboard/captain/logo.svg' %>" alt="logo" />
|
||||
</div>
|
||||
<h1><%= @landing_host.page_title %></h1>
|
||||
|
||||
<p class="subtitle"><%= @landing_host.page_subtitle&.gsub("\n", "<br>")&.html_safe %></p>
|
||||
|
||||
<div class="timer-container">
|
||||
<span class="timer-label">Oferta expira em</span>
|
||||
<span class="timer-value" id="countdownTimer">10:00</span>
|
||||
</div>
|
||||
|
||||
<button id="whatsButton" class="wa-button" type="button"><%= @landing_host.button_text.presence || 'Falar no WhatsApp' %></button>
|
||||
<div class="foot">Pagina segura · atendimento humano</div>
|
||||
|
||||
<div class="foot">
|
||||
<svg xmlns="http://www.w3.org/.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12C22 17.5228 17.5228 22 12 22ZM12 20C16.4183 20 20 16.4183 20 12C20 7.58172 16.4183 4 12 4C7.58172 4 4 7.58172 4 12C4 16.4183 7.58172 20 12 20ZM11 15H13V17H11V15ZM11 7H13V13H11V7Z"></path></svg>
|
||||
Página segura e atendimento humano
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -142,6 +274,58 @@
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const clickKey = "lp_click_id_" + window.location.hostname;
|
||||
|
||||
// --- Timer Logic ---
|
||||
function startTimer() {
|
||||
const timerElement = document.getElementById("countdownTimer");
|
||||
if (!timerElement) return;
|
||||
|
||||
// 10 minutes from now (in seconds)
|
||||
let totalSeconds = 10 * 60;
|
||||
|
||||
// Check if we already have a timer running in session storage to persist across refresh
|
||||
const sessionKey = "lp_timer_" + window.location.hostname;
|
||||
const savedTime = sessionStorage.getItem(sessionKey);
|
||||
const savedTimestamp = sessionStorage.getItem(sessionKey + "_ts");
|
||||
|
||||
if (savedTime && savedTimestamp) {
|
||||
const elapsed = Math.floor((Date.now() - parseInt(savedTimestamp)) / 1000);
|
||||
const remaining = parseInt(savedTime) - elapsed;
|
||||
if (remaining > 0) {
|
||||
totalSeconds = remaining;
|
||||
}
|
||||
}
|
||||
|
||||
const interval = setInterval(() => {
|
||||
if (totalSeconds <= 0) {
|
||||
clearInterval(interval);
|
||||
timerElement.innerText = "00:00";
|
||||
timerElement.classList.add("alert"); // Change color to red/alert
|
||||
return;
|
||||
}
|
||||
|
||||
const minutes = Math.floor(totalSeconds / 60);
|
||||
const seconds = totalSeconds % 60;
|
||||
|
||||
const formattedMinutes = minutes.toString().padStart(2, '0');
|
||||
const formattedSeconds = seconds.toString().padStart(2, '0');
|
||||
|
||||
timerElement.innerText = `${formattedMinutes}:${formattedSeconds}`;
|
||||
|
||||
// Persist in case of refresh
|
||||
sessionStorage.setItem(sessionKey, totalSeconds.toString());
|
||||
sessionStorage.setItem(sessionKey + "_ts", Date.now().toString());
|
||||
|
||||
if (totalSeconds <= 60) {
|
||||
timerElement.classList.add("alert"); // Final minute warning
|
||||
}
|
||||
|
||||
totalSeconds--;
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
startTimer();
|
||||
// -------------------
|
||||
|
||||
function getClickId() {
|
||||
const fromUrl = params.get("click_id") || params.get("clickid") || params.get("utm_id") || params.get("gclid");
|
||||
if (fromUrl) {
|
||||
|
||||
26
progresso/2026-03-03_landing_page_ux_improvement.md
Normal file
26
progresso/2026-03-03_landing_page_ux_improvement.md
Normal file
@ -0,0 +1,26 @@
|
||||
# Objetivo
|
||||
Melhorar o design e as conversões da Landing Page (`show.html.erb`), focando em interface moderna ("glassmorphism"), eliminação de scroll desnecessário, e implementação de gatilhos psicológicos de urgência (cronômetro).
|
||||
|
||||
# Contexto
|
||||
A página antiga do iachat para atendimento de WhatsApp possuía um design simples. Como arquiteto e UX expert, a refatoração visual manteve a performance de um arquivo único (HTML/CSS inline leve) mas modernizou a apresentação utilizando as variáveis de cores dinâmicas para gerar maior apelo visual e Call-to-Action.
|
||||
|
||||
# Passos Realizados
|
||||
1. Refatoração do layout com responsividade fluida via CSS `clamp()` para as fontes e paddings. O body passou a ter `overflow: hidden` na maioria das telas modernas para manter o formato "above-the-fold".
|
||||
2. Aplicação de visual premium no card de contato usando `backdrop-filter: blur()`, reduzindo contraste chapado e gerando sensação de profundidade.
|
||||
3. Adição de um Cronômetro regressivo ("Oferta expira em 10:00") que utiliza `sessionStorage` para manter a persistência entre recarregamentos de página.
|
||||
4. Animação de "brilho" (shine) contínuo e pulso na sombra (`box-shadow`) do botão do WhatsApp para destacar de forma definitiva o CTA.
|
||||
|
||||
# Arquivos Alterados
|
||||
- `app/views/public/landing_pages/show.html.erb`
|
||||
|
||||
# Variáveis / Features
|
||||
- `countdownTimer`: Elemento e classe no JS nativo adicionado.
|
||||
- `sessionKey = "lp_timer_"`: Controle por hostname para evitar resets de timer na mesma sessão.
|
||||
|
||||
# Como Validar
|
||||
1. Abrir no navegador a URL de teste local de algum Landing Host (/lp/[hostname]).
|
||||
2. Testar o redimensionamento de janela; o layout não deve forçar scroll a não ser que a altura seja criticamente pequena (ex: < 500px).
|
||||
3. Aguardar os 10 minutos (ou editar session storage) para validar a cor vermelha piscante do cronômetro finalizado.
|
||||
|
||||
# Como Reverter
|
||||
Executar no terminal raiz: `git checkout app/views/public/landing_pages/show.html.erb` caso ainda não tenha "commitado", ou usar o git revert correspondente.
|
||||
Loading…
Reference in New Issue
Block a user