feat(UI): Enhance form functionality and add progress bar for improved user experience

This commit is contained in:
2026-04-13 17:04:23 -03:00
parent 71e1ee1c11
commit fcbfc599a1
3 changed files with 622 additions and 1 deletions

View File

@@ -8,6 +8,7 @@
</head> </head>
<body> <body>
<div id="progress-bar"></div>
<header id="site-header"> <header id="site-header">
<nav id="main-nav"> <nav id="main-nav">
<a href="#" class="nav-logo">Brew<span>Box</span></a> <a href="#" class="nav-logo">Brew<span>Box</span></a>
@@ -135,6 +136,14 @@
Completá el formulario y recibís tu primer envío en menos de 72hs. Completá el formulario y recibís tu primer envío en menos de 72hs.
Estudiantes obtienen un descuento del 20%. Estudiantes obtienen un descuento del 20%.
</p> </p>
<div class="plan-preview" id="plan-preview">
<div class="plan-preview-inner">
<span class="plan-preview-label">Plan seleccionado</span>
<span class="plan-preview-name" id="plan-preview-name"></span>
<span class="plan-preview-price" id="plan-preview-price"></span>
<ul class="plan-preview-features" id="plan-preview-features"></ul>
</div>
</div>
</div> </div>
<form action="#" method="post"> <form action="#" method="post">
@@ -144,15 +153,24 @@
<div class="field-group"> <div class="field-group">
<label for="name">Nombre</label> <label for="name">Nombre</label>
<input type="text" id="name" name="name" required /> <input type="text" id="name" name="name" required />
<span class="field-error-msg"
>Ingresá al menos 2 caracteres</span
>
</div> </div>
<div class="field-group"> <div class="field-group">
<label for="lastname">Apellido</label> <label for="lastname">Apellido</label>
<input type="text" id="lastname" name="lastname" required /> <input type="text" id="lastname" name="lastname" required />
<span class="field-error-msg"
>Ingresá al menos 2 caracteres</span
>
</div> </div>
</div> </div>
<div class="field-group"> <div class="field-group">
<label for="email">Correo electrónico</label> <label for="email">Correo electrónico</label>
<input type="email" id="email" name="email" required /> <input type="email" id="email" name="email" required />
<span class="field-error-msg"
>Ingresá un correo electrónico válido</span
>
</div> </div>
<div class="field-group checkbox-group"> <div class="field-group checkbox-group">
<input type="checkbox" id="student" name="student" /> <input type="checkbox" id="student" name="student" />
@@ -178,6 +196,7 @@
<div class="field-group"> <div class="field-group">
<label for="reason">¿Por qué te suscribís?</label> <label for="reason">¿Por qué te suscribís?</label>
<textarea id="reason" name="reason" rows="4"></textarea> <textarea id="reason" name="reason" rows="4"></textarea>
<span class="char-counter" id="reason-counter">0 / 300</span>
</div> </div>
</fieldset> </fieldset>
@@ -191,6 +210,16 @@
<option value="debit">Tarjeta de débito</option> <option value="debit">Tarjeta de débito</option>
</select> </select>
</div> </div>
<div class="field-group" id="cuotas-group" style="display: none">
<label for="cuotas">Cantidad de cuotas</label>
<input
type="number"
id="cuotas"
name="cuotas"
min="1"
max="12"
placeholder="1"
/>
<div class="field-group field-group--icon"> <div class="field-group field-group--icon">
<label for="card">Número de tarjeta</label> <label for="card">Número de tarjeta</label>
<div class="input-icon-wrapper"> <div class="input-icon-wrapper">
@@ -213,10 +242,12 @@
type="text" type="text"
id="card" id="card"
name="card" name="card"
placeholder="1234 5678 9012 3456" placeholder="XXXX-XXXX-XXXX-XXXX"
required required
/> />
<span class="card-brand" id="card-brand"></span>
</div> </div>
<span class="field-error-msg">Número de tarjeta inválido</span>
</div> </div>
<div class="field-row"> <div class="field-row">
<div class="field-group"> <div class="field-group">
@@ -228,6 +259,7 @@
placeholder="MM/AA" placeholder="MM/AA"
required required
/> />
<span class="field-error-msg">Formato MM/AA</span>
</div> </div>
<div class="field-group"> <div class="field-group">
<label for="cvv">CVV</label> <label for="cvv">CVV</label>
@@ -238,6 +270,7 @@
placeholder="123" placeholder="123"
required required
/> />
<span class="field-error-msg">Ingresá un CVV válido</span>
</div> </div>
</div> </div>
</fieldset> </fieldset>
@@ -314,6 +347,31 @@
<button type="submit" class="cta-button">Suscribirme</button> <button type="submit" class="cta-button">Suscribirme</button>
</div> </div>
</form> </form>
<div id="form-confirmation">
<div class="confirmation-icon">
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
>
<polyline points="20 6 9 17 4 12"></polyline>
</svg>
</div>
<p class="confirmation-title">¡Bienvenido a BrewBox!</p>
<p class="confirmation-text">
Tu suscripción fue registrada. En las próximas horas vas a recibir
un email de confirmación con los detalles de tu primer envío.
</p>
<p class="confirmation-detail">
Primer envío estimado: <span id="confirm-date"></span>
</p>
</div>
</section> </section>
</main> </main>

View File

@@ -0,0 +1,308 @@
// Smooth scrolling for anchor links
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
anchor.addEventListener('click', function(e) {
e.preventDefault();
const target = document.querySelector(this.getAttribute('href'));
if (target) {
target.scrollIntoView({ behavior: 'smooth' });
}
});
});
// Scroll progress bar
const progressBar = document.getElementById('progress-bar');
window.addEventListener('scroll', () => {
const scrollTop = window.scrollY;
const docHeight = document.documentElement.scrollHeight - window.innerHeight;
const progress = (scrollTop / docHeight) * 100;
progressBar.style.width = progress + '%';
});
// Student discount logic
const studentCheckbox = document.getElementById('student');
const plans = {
normal: { full: 10, label: 'Normal' },
premium: { full: 15, label: 'Premium' }
};
function updatePrices() {
const isStudent = studentCheckbox.checked;
document.querySelectorAll('.radio-group label').forEach(lbl => {
const input = lbl.querySelector('input[type="radio"]');
if (!input) return;
const plan = plans[input.value];
if (!plan) return;
const discounted = (plan.full * 0.7).toFixed(2);
if (isStudent) {
lbl.innerHTML = `
<input type="radio" name="plan" value="${input.value}" ${input.checked ? 'checked' : ''} ${input.required ? 'required' : ''}>
${plan.label}
<span class="price-original">$${plan.full}/mes</span>
<span class="price-discount">20% OFF</span>
<span class="price-new">$${discounted}/mes</span>
`;
} else {
lbl.innerHTML = `
<input type="radio" name="plan" value="${input.value}" ${input.checked ? 'checked' : ''} ${input.required ? 'required' : ''}>
${plan.label}$${plan.full}/mes
`;
}
});
}
studentCheckbox.addEventListener('change', updatePrices);
// Character counter for textarea
const textarea = document.getElementById('reason');
const counter = document.getElementById('reason-counter');
const MAX_CHARS = 300;
textarea.addEventListener('input', () => {
const len = textarea.value.length;
counter.textContent = `${len} / ${MAX_CHARS}`;
counter.classList.remove('near-limit', 'at-limit');
if (len >= MAX_CHARS) {
textarea.value = textarea.value.slice(0, MAX_CHARS);
counter.classList.add('at-limit');
} else if (len >= MAX_CHARS * 0.8) {
counter.classList.add('near-limit');
}
});
// Form validation
function setFieldState(input, isValid) {
const group = input.closest('.field-group');
if (!group) return;
group.classList.remove('error', 'success');
if (input.value.trim() === '') return;
group.classList.add(isValid ? 'success' : 'error');
}
// Name and Lastname
['name', 'lastname'].forEach(id => {
document.getElementById(id).addEventListener('input', function() {
setFieldState(this, this.value.trim().length >= 2);
});
});
// Email
document.getElementById('email').addEventListener('input', function() {
const valid = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(this.value);
setFieldState(this, valid);
});
// MMAA validation
document.getElementById('expiry').addEventListener('input', function() {
this.value = this.value.replace(/[^\d]/g, '').slice(0, 4);
if (this.value.length > 2) {
this.value = this.value.slice(0, 2) + '/' + this.value.slice(2);
}
const valid = /^(0[1-9]|1[0-2])\/\d{2}$/.test(this.value);
setFieldState(this, valid);
});
// CVV
document.getElementById('cvv').addEventListener('input', function() {
this.value = this.value.replace(/\D/g, '').slice(0, 4);
setFieldState(this, this.value.length >= 3);
});
// Plan preview logic
const planData = {
normal: {
name: 'Normal',
price: '$10/mes',
priceStudent: '$7.00/mes',
features: [
'250 g por envío',
'1 café de origen único por mes',
'Envío gratuito',
'Guía de preparación'
]
},
premium: {
name: 'Premium',
price: '$15/mes',
priceStudent: '$10.50/mes',
features: [
'500 g por envío',
'2 cafés de origen único por mes',
'Envío gratuito',
'Guía de preparación',
'Acceso a lotes exclusivos'
]
}
};
const planPreview = document.getElementById('plan-preview');
const planPreviewName = document.getElementById('plan-preview-name');
const planPreviewPrice = document.getElementById('plan-preview-price');
const planPreviewFeatures = document.getElementById('plan-preview-features');
function updatePlanPreview() {
const selected = document.querySelector('input[name="plan"]:checked');
if (!selected) {
planPreview.classList.remove('visible');
return;
}
const plan = planData[selected.value];
const isStudent = document.getElementById('student').checked;
planPreviewName.textContent = plan.name;
planPreviewPrice.textContent = isStudent ? plan.priceStudent : plan.price;
planPreviewFeatures.innerHTML = plan.features
.map(f => `<li>${f}</li>`)
.join('');
planPreview.classList.add('visible');
}
document.querySelectorAll('input[name="plan"]').forEach(radio => {
radio.addEventListener('change', updatePlanPreview);
});
// Update prices and preview when student checkbox changes
studentCheckbox.addEventListener('change', () => {
updatePrices();
updatePlanPreview();
});
// Credit card validation and brand detection
const cardInput = document.getElementById('card');
const cardBrand = document.getElementById('card-brand');
const cardPatterns = {
Visa: /^4/,
Mastercard: /^5[1-5]|^2[2-7]/,
Amex: /^3[47]/,
Naranja: /^589562/,
Cabal: /^604201|^589657/,
};
function detectCardBrand(number) {
const clean = number.replace(/\s/g, '');
for (const [brand, pattern] of Object.entries(cardPatterns)) {
if (pattern.test(clean)) return brand;
}
return null;
}
function validateCard(number) {
const clean = number.replace(/\s/g, '');
if (clean.length < 13) return false;
// Luhn algorithm
let sum = 0;
let alternate = false;
for (let i = clean.length - 1; i >= 0; i--) {
let n = parseInt(clean[i]);
if (alternate) {
n *= 2;
if (n > 9) n -= 9;
}
sum += n;
alternate = !alternate;
}
return sum % 10 === 0;
}
cardInput.addEventListener('input', function() {
// Remove non-digits and limit to 16 characters
let val = this.value.replace(/\D/g, '').slice(0, 16);
this.value = val.replace(/(.{4})/g, '$1 ').trim();
const brand = detectCardBrand(this.value);
const valid = validateCard(this.value);
if (brand) {
cardBrand.textContent = brand;
cardBrand.classList.add('visible');
} else {
cardBrand.classList.remove('visible');
}
setFieldState(this, valid);
});
// Payment cuotas logic
const paymentSelect = document.getElementById('payment');
const cuotasGroup = document.getElementById('cuotas-group');
const cuotasInput = document.getElementById('cuotas');
paymentSelect.addEventListener('change', function() {
if (this.value === 'credit') {
cuotasGroup.style.display = 'flex';
cuotasInput.setAttribute('required', '');
} else {
cuotasGroup.style.display = 'none';
cuotasInput.removeAttribute('required');
cuotasInput.value = '';
}
});
// Cuotas validation
cuotasInput.addEventListener('input', function() {
const val = parseInt(this.value);
setFieldState(this, val >= 1 && val <= 12);
});
// Form submission logic
const form = document.querySelector('form');
const submitBtn = document.querySelector('.cta-button[type="submit"]');
const confirmation = document.getElementById('form-confirmation');
const confirmDate = document.getElementById('confirm-date');
const subsCounter = document.querySelector('.hero-stats .stat-item:last-child .stat-number');
form.addEventListener('submit', function(e) {
e.preventDefault();
// loading state
submitBtn.classList.add('loading');
submitBtn.innerHTML = '<span class="spinner"></span> Procesando...';
setTimeout(() => {
// Hidden form
form.style.transition = 'opacity 0.3s ease';
form.style.opacity = '0';
setTimeout(() => {
form.style.display = 'none';
// Calculate delivery date (7 days from now)
const delivery = new Date();
delivery.setDate(delivery.getDate() + 7);
confirmDate.textContent = delivery.toLocaleDateString('es-AR', {
day: 'numeric',
month: 'long',
year: 'numeric'
});
// Show confirmation
confirmation.style.display = 'flex';
requestAnimationFrame(() => {
requestAnimationFrame(() => {
confirmation.classList.add('visible');
});
});
// +1 to subscriber counter
if (subsCounter) {
const current = parseInt(subsCounter.textContent);
subsCounter.textContent = current + 1;
}
}, 300);
}, 2000); // simulate processing delay
});

View File

@@ -23,6 +23,16 @@ a {
color: inherit; color: inherit;
} }
input[type="number"]::-webkit-outer-spin-button,
input[type="number"]::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
input[type="number"] {
-moz-appearance: textfield;
}
/* Navbar */ /* Navbar */
#site-header { #site-header {
position: sticky; position: sticky;
@@ -537,6 +547,7 @@ label {
input[type="text"], input[type="text"],
input[type="email"], input[type="email"],
input[type="number"],
textarea, textarea,
select { select {
background: rgba(255, 255, 255, 0.04); background: rgba(255, 255, 255, 0.04);
@@ -595,6 +606,7 @@ textarea {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 0.6rem; gap: 0.6rem;
flex-wrap: wrap;
font-size: 0.88rem; font-size: 0.88rem;
letter-spacing: 0.03em; letter-spacing: 0.03em;
text-transform: none; text-transform: none;
@@ -735,6 +747,238 @@ textarea {
} }
} }
.price-original {
text-decoration: line-through;
color: rgba(245, 240, 232, 0.25);
font-size: 0.85rem;
}
.price-discount {
color: #c8a96e;
font-size: 0.72rem;
letter-spacing: 0.1em;
border: 1px solid rgba(200, 169, 110, 0.3);
padding: 0.1rem 0.4rem;
border-radius: 2px;
white-space: nowrap;
}
.price-new {
color: #f5f0e8;
font-size: 0.95rem;
}
.char-counter {
font-size: 0.68rem;
letter-spacing: 0.08em;
color: rgba(245, 240, 232, 0.2);
text-align: right;
transition: color 0.2s;
}
.char-counter.near-limit {
color: #c8a96e;
}
.char-counter.at-limit {
color: rgba(220, 80, 80, 0.7);
}
.field-group.error input,
.field-group.error textarea,
.field-group.error select {
border-bottom-color: rgba(220, 80, 80, 0.6);
background: rgba(220, 80, 80, 0.04);
}
.field-group.success input,
.field-group.success textarea,
.field-group.success select {
border-bottom-color: rgba(100, 200, 130, 0.5);
background: rgba(100, 200, 130, 0.03);
}
.field-error-msg {
font-size: 0.68rem;
letter-spacing: 0.05em;
color: rgba(220, 80, 80, 0.7);
margin-top: 0.25rem;
display: none;
}
.field-group.error .field-error-msg {
display: block;
}
.plan-preview {
margin-top: 2rem;
border-top: 1px solid rgba(255, 255, 255, 0.06);
padding-top: 1.5rem;
opacity: 0;
transform: translateY(8px);
transition: opacity 0.3s ease, transform 0.3s ease;
pointer-events: none;
}
.plan-preview.visible {
opacity: 1;
transform: translateY(0);
pointer-events: auto;
}
.plan-preview-label {
display: block;
font-size: 0.65rem;
letter-spacing: 0.2em;
text-transform: uppercase;
color: rgba(245, 240, 232, 0.25);
margin-bottom: 0.5rem;
}
.plan-preview-name {
display: block;
font-size: 1.3rem;
font-weight: normal;
color: #f5f0e8;
margin-bottom: 0.25rem;
}
.plan-preview-price {
display: block;
font-size: 0.85rem;
color: #c8a96e;
margin-bottom: 1.25rem;
letter-spacing: 0.05em;
}
.plan-preview-features {
list-style: none;
display: flex;
flex-direction: column;
gap: 0.6rem;
}
.plan-preview-features li {
display: flex;
align-items: flex-start;
gap: 0.75rem;
font-size: 0.8rem;
color: rgba(245, 240, 232, 0.45);
line-height: 1.4;
}
.plan-preview-features li::before {
content: '';
display: block;
width: 12px;
min-width: 12px;
height: 1px;
background: #c8a96e;
margin-top: 0.65em;
}
.card-brand {
position: absolute;
right: 0.75rem;
font-size: 0.65rem;
letter-spacing: 0.1em;
text-transform: uppercase;
color: #c8a96e;
opacity: 0;
transition: opacity 0.2s;
pointer-events: none;
}
.card-brand.visible {
opacity: 1;
}
/* Loading State */
.spinner {
width: 18px;
height: 18px;
border: 2px solid rgba(10, 10, 10, 0.3);
border-top-color: #0a0a0a;
border-radius: 50%;
animation: spin 0.7s linear infinite;
display: inline-block;
vertical-align: middle;
margin-right: 0.5rem;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.cta-button.loading {
opacity: 0.8;
cursor: not-allowed;
pointer-events: none;
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
}
/* Confirmación */
#form-confirmation {
display: none;
flex-direction: column;
gap: 1.5rem;
padding: 3rem 0;
opacity: 0;
transform: translateY(12px);
transition: opacity 0.4s ease, transform 0.4s ease;
}
#form-confirmation.visible {
opacity: 1;
transform: translateY(0);
}
.confirmation-icon {
width: 48px;
height: 48px;
border: 1px solid rgba(200, 169, 110, 0.3);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
.confirmation-icon svg {
color: #c8a96e;
}
.confirmation-title {
font-size: 1.8rem;
font-weight: normal;
color: #f5f0e8;
letter-spacing: -0.01em;
line-height: 1.2;
}
.confirmation-text {
font-size: 0.9rem;
line-height: 1.8;
color: rgba(245, 240, 232, 0.4);
max-width: 38ch;
}
.confirmation-detail {
font-size: 0.72rem;
letter-spacing: 0.1em;
text-transform: uppercase;
color: rgba(245, 240, 232, 0.2);
border-top: 1px solid rgba(255, 255, 255, 0.06);
padding-top: 1.5rem;
}
.confirmation-detail span {
color: #c8a96e;
}
/* Footer */ /* Footer */
#site-footer { #site-footer {
@@ -801,4 +1045,15 @@ textarea {
.footer-links { .footer-links {
gap: 1.25rem; gap: 1.25rem;
} }
}
#progress-bar {
position: fixed;
top: 0;
left: 0;
width: 0%;
height: 2px;
background: #c8a96e;
z-index: 9999;
transition: width 0.1s linear;
} }