feat: tier selector step with radio cards, white CTA buttons, 4-step form
This commit is contained in:
+101
-16
@@ -30,9 +30,32 @@
|
||||
.btn{display:inline-flex;align-items:center;gap:.5rem;padding:.7rem 1.5rem;border-radius:var(--radius-sm);font-weight:600;font-size:.9rem;text-decoration:none;cursor:pointer;transition:all .2s;border:none;font-family:inherit}
|
||||
.btn-primary{background:var(--cta-bg);color:#fff;box-shadow:0 0 24px var(--accent-glow),var(--cta-shadow)}
|
||||
.btn-primary:hover{background:linear-gradient(135deg,#4a8ff7,#7ab8fc);transform:translateY(-2px);box-shadow:0 0 32px rgba(59,130,246,0.35)}
|
||||
/* White CTA button for Get a Quote / Get Pricing */
|
||||
.btn-cta{background:#fff;color:#0a0a0b;border:1px solid rgba(255,255,255,.15);box-shadow:0 4px 20px rgba(0,0,0,.2),0 0 0 1px rgba(255,255,255,.1)}
|
||||
.btn-cta:hover{background:#f0f0f5;transform:translateY(-2px);box-shadow:0 8px 30px rgba(0,0,0,.25),0 0 0 1px rgba(255,255,255,.2)}
|
||||
.light .btn-cta{background:#1d1d1f;color:#fff;border-color:rgba(255,255,255,.1);box-shadow:0 4px 20px rgba(0,0,0,.15)}
|
||||
.light .btn-cta:hover{background:#2d2d2f}
|
||||
.btn-ghost{background:transparent;color:var(--text);border:1px solid var(--border)}
|
||||
.btn-ghost:hover{border-color:var(--border-hover);background:var(--bg-card)}
|
||||
.btn-sm{padding:.4rem 1rem;font-size:.8rem}
|
||||
|
||||
/* ── Tier Selector (Stripe-inspired radio cards) ── */
|
||||
.tier-selector{display:grid;grid-template-columns:1fr 1fr;gap:.65rem;margin-bottom:1rem}
|
||||
.tier-option{position:relative;background:var(--bg-card);border:1.5px solid var(--border);border-radius:10px;padding:1rem;cursor:pointer;transition:all .2s;user-select:none}
|
||||
.tier-option:hover{border-color:var(--border-hover);background:var(--bg-card-hover)}
|
||||
.tier-option.selected{border-color:var(--accent);background:rgba(59,130,246,.06);box-shadow:0 0 0 1px var(--accent)}
|
||||
.tier-option input{position:absolute;opacity:0;pointer-events:none}
|
||||
.tier-option .t-name{font-weight:700;font-size:1rem;margin-bottom:.15rem;display:flex;align-items:center;gap:.4rem}
|
||||
.tier-option .t-name .check{width:18px;height:18px;border-radius:50%;border:2px solid var(--border);display:inline-flex;align-items:center;justify-content:center;flex-shrink:0;transition:all .2s;font-size:10px;color:transparent}
|
||||
.tier-option.selected .t-name .check{border-color:var(--accent);background:var(--accent);color:#fff}
|
||||
.tier-option .t-tag{font-size:.72rem;color:var(--text-muted)}
|
||||
.tier-option .t-specs{display:flex;gap:.4rem;flex-wrap:wrap;margin-top:.5rem}
|
||||
.tier-option .t-specs span{font-size:.7rem;padding:.15rem .45rem;border-radius:4px;background:rgba(255,255,255,.04);color:var(--text-dim)}
|
||||
.tier-option.popular{border-color:rgba(59,130,246,.3)}
|
||||
.tier-option.popular.selected{border-color:var(--accent)}
|
||||
.tier-option.popular .t-name::after{content:'Best Value';font-size:.6rem;font-weight:600;text-transform:uppercase;letter-spacing:.04em;padding:.1rem .4rem;border-radius:4px;background:rgba(59,130,246,.15);color:var(--accent);margin-left:auto}
|
||||
.lang-fr .tier-option.popular .t-name::after{content:'Meilleur Q/P'}
|
||||
|
||||
.hero{min-height:100vh;display:flex;flex-direction:column;justify-content:center;padding:6rem 2rem 4rem;max-width:1200px;margin:0 auto}
|
||||
.hero-grid{display:grid;grid-template-columns:1fr 1fr;gap:4rem;align-items:center}
|
||||
.hero-badge{display:inline-flex;align-items:center;gap:.5rem;padding:.35rem .85rem;border-radius:100px;background:rgba(59,130,246,.1);border:1px solid rgba(59,130,246,.2);font-size:.8rem;color:var(--accent);font-weight:500;margin-bottom:1.5rem;width:fit-content}
|
||||
@@ -162,7 +185,7 @@
|
||||
<a href="/check-order.html"><span class="en-only">Check Order</span><span class="fr-only">Suivi</span></a>
|
||||
<a href="/backup-drives.html"><span class="en-only">Backup Drives</span><span class="fr-only">Disques Backup</span></a>
|
||||
<button class="theme-btn" id="themeToggle" onclick="toggleTheme()" title="Toggle theme"></button>
|
||||
<a href="#contact" class="btn btn-primary"><span class="en-only">Get a Quote</span><span class="fr-only">Demander un devis</span></a>
|
||||
<a href="#contact" class="btn btn-cta"><span class="en-only">Get a Quote</span><span class="fr-only">Demander un devis</span></a>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
@@ -224,7 +247,7 @@
|
||||
<li><span class="check">✓</span> <span class="en-only">On-site install</span><span class="fr-only">Installation sur place</span></li>
|
||||
<li><span class="check">✓</span> <span class="en-only">Automated backups</span><span class="fr-only">Sauvegardes automatisées</span></li>
|
||||
</ul>
|
||||
<a href="#contact" class="btn btn-primary"><span class="en-only">Get Pricing</span><span class="fr-only">Voir le prix</span></a>
|
||||
<a href="#contact" class="btn btn-cta"><span class="en-only">Get Pricing</span><span class="fr-only">Voir le prix</span></a>
|
||||
</div>
|
||||
<div class="tier-card">
|
||||
<div class="tier-name">Atlas</div>
|
||||
@@ -248,7 +271,7 @@
|
||||
<li><span class="check">✓</span> <span class="en-only">On-site install</span><span class="fr-only">Installation sur place</span></li>
|
||||
<li><span class="check">✓</span> <span class="en-only">Automated backups</span><span class="fr-only">Sauvegardes automatisées</span></li>
|
||||
</ul>
|
||||
<a href="#contact" class="btn btn-primary"><span class="en-only">Get Pricing</span><span class="fr-only">Voir le prix</span></a>
|
||||
<a href="#contact" class="btn btn-cta"><span class="en-only">Get Pricing</span><span class="fr-only">Voir le prix</span></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -343,8 +366,9 @@
|
||||
<div class="step-dot" data-step="1"></div>
|
||||
<div class="step-dot" data-step="2"></div>
|
||||
<div class="step-dot" data-step="3"></div>
|
||||
<div class="step-dot" data-step="4"></div>
|
||||
</div>
|
||||
<div class="step-label" id="stepLabel"><span class="en-only">Step 1 of 3</span><span class="fr-only">Étape 1 sur 3</span></div>
|
||||
<div class="step-label" id="stepLabel"><span class="en-only">Step 1 of 4</span><span class="fr-only">Étape 1 sur 4</span></div>
|
||||
|
||||
<div class="form-step current" data-step="1" id="step1">
|
||||
<div class="field-wrapper" id="emailWrapper">
|
||||
@@ -358,11 +382,37 @@
|
||||
</div>
|
||||
|
||||
<div class="form-step" data-step="2" id="step2">
|
||||
<div class="field-wrapper" id="nameWrapper">
|
||||
<span class="field-icon">👤</span>
|
||||
<input class="field-input" type="text" id="name" placeholder="Name" autocomplete="name">
|
||||
<div class="step-label" style="font-size:.78rem;color:var(--text-dim);margin-bottom:1rem;text-align:center">
|
||||
<span class="en-only">Choose your server tier</span>
|
||||
<span class="fr-only">Choisissez votre offre</span>
|
||||
</div>
|
||||
<div class="field-error" id="nameError"><span class="en-only">Please enter your name.</span><span class="fr-only">Entrez votre nom.</span></div>
|
||||
<div class="tier-selector">
|
||||
<label class="tier-option" data-tier="rif" onclick="selectTier(this)">
|
||||
<input type="radio" name="tier" value="rif">
|
||||
<div class="t-name"><span class="check">✓</span>Rif</div>
|
||||
<div class="t-tag"><span class="en-only">entry-level</span><span class="fr-only">entrée de gamme</span></div>
|
||||
<div class="t-specs"><span>512 GB</span><span>Mini PC</span></div>
|
||||
</label>
|
||||
<label class="tier-option popular" data-tier="rifplus" onclick="selectTier(this)">
|
||||
<input type="radio" name="tier" value="rif+">
|
||||
<div class="t-name"><span class="check">✓</span>Rif<sup>+</sup></div>
|
||||
<div class="t-tag"><span class="en-only">best value</span><span class="fr-only">meilleur rapport</span></div>
|
||||
<div class="t-specs"><span>512+512 GB</span><span>Backup</span></div>
|
||||
</label>
|
||||
<label class="tier-option" data-tier="atlas" onclick="selectTier(this)">
|
||||
<input type="radio" name="tier" value="atlas">
|
||||
<div class="t-name"><span class="check">✓</span>Atlas</div>
|
||||
<div class="t-tag"><span class="en-only">pro</span><span class="fr-only">pro</span></div>
|
||||
<div class="t-specs"><span>1 TB</span><span>Mini PC</span></div>
|
||||
</label>
|
||||
<label class="tier-option popular" data-tier="atlasplus" onclick="selectTier(this)">
|
||||
<input type="radio" name="tier" value="atlas+">
|
||||
<div class="t-name"><span class="check">✓</span>Atlas<sup>+</sup></div>
|
||||
<div class="t-tag"><span class="en-only">maximum</span><span class="fr-only">max</span></div>
|
||||
<div class="t-specs"><span>1+1 TB</span><span>Backup</span></div>
|
||||
</label>
|
||||
</div>
|
||||
<div class="field-error" id="tierError" style="text-align:center;margin-bottom:.5rem"><span class="en-only">Please select a tier.</span><span class="fr-only">Veuillez choisir une offre.</span></div>
|
||||
<div class="btn-row">
|
||||
<button type="button" class="btn btn-apple-secondary" id="back2"><span class="en-only">Back</span><span class="fr-only">Retour</span></button>
|
||||
<button type="button" class="btn btn-primary" id="next2"><span class="en-only">Next</span><span class="fr-only">Suivant</span></button>
|
||||
@@ -370,13 +420,25 @@
|
||||
</div>
|
||||
|
||||
<div class="form-step" data-step="3" id="step3">
|
||||
<div class="field-wrapper" id="nameWrapper">
|
||||
<span class="field-icon">👤</span>
|
||||
<input class="field-input" type="text" id="name" placeholder="Name" autocomplete="name">
|
||||
</div>
|
||||
<div class="field-error" id="nameError"><span class="en-only">Please enter your name.</span><span class="fr-only">Entrez votre nom.</span></div>
|
||||
<div class="btn-row">
|
||||
<button type="button" class="btn btn-apple-secondary" id="back3"><span class="en-only">Back</span><span class="fr-only">Retour</span></button>
|
||||
<button type="button" class="btn btn-primary" id="next3"><span class="en-only">Next</span><span class="fr-only">Suivant</span></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-step" data-step="4" id="step4">
|
||||
<div class="field-wrapper" id="phoneWrapper">
|
||||
<span class="field-icon">📞</span>
|
||||
<input class="field-input" type="tel" id="phone" placeholder="Phone number">
|
||||
</div>
|
||||
<div class="field-error" id="phoneError"><span class="en-only">Please enter your phone number.</span><span class="fr-only">Entrez votre numéro.</span></div>
|
||||
<div class="btn-row">
|
||||
<button type="button" class="btn btn-apple-secondary" id="back3"><span class="en-only">Back</span><span class="fr-only">Retour</span></button>
|
||||
<button type="button" class="btn btn-apple-secondary" id="back4"><span class="en-only">Back</span><span class="fr-only">Retour</span></button>
|
||||
<button type="button" class="btn btn-primary" id="submitBtn"><span class="en-only">Submit</span><span class="fr-only">Envoyer</span></button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -440,9 +502,10 @@
|
||||
|
||||
<script>
|
||||
(function(){
|
||||
const API_URL = 'http://192.168.1.30:8581';
|
||||
const totalSteps = 3;
|
||||
const API_URL = 'http://192.168.1.126:8581';
|
||||
const totalSteps = 4;
|
||||
let currentStep = 1;
|
||||
let selectedTier = '';
|
||||
|
||||
const steps = Array.from(document.querySelectorAll('.form-step'));
|
||||
const dots = Array.from(document.querySelectorAll('.step-dot'));
|
||||
@@ -454,14 +517,24 @@
|
||||
name: { input: document.getElementById('name'), wrapper: document.getElementById('nameWrapper'), error: document.getElementById('nameError') },
|
||||
phone: { input: document.getElementById('phone'), wrapper: document.getElementById('phoneWrapper'), error: document.getElementById('phoneError') }
|
||||
};
|
||||
const tierError = document.getElementById('tierError');
|
||||
|
||||
function validateEmail(v) { return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v.trim()); }
|
||||
|
||||
window.selectTier = function(el) {
|
||||
document.querySelectorAll('.tier-option').forEach(o => o.classList.remove('selected'));
|
||||
el.classList.add('selected');
|
||||
el.querySelector('input').checked = true;
|
||||
selectedTier = el.querySelector('input').value;
|
||||
if (tierError) tierError.classList.remove('shown');
|
||||
};
|
||||
|
||||
function clearErrors() {
|
||||
Object.values(fields).forEach(f => {
|
||||
if(f.wrapper) f.wrapper.classList.remove('error');
|
||||
if(f.error) f.error.classList.remove('shown');
|
||||
});
|
||||
if (tierError) tierError.classList.remove('shown');
|
||||
}
|
||||
|
||||
function showStep(n, dir) {
|
||||
@@ -479,7 +552,6 @@
|
||||
const isFr = document.documentElement.classList.contains('lang-fr');
|
||||
label.textContent = isFr ? 'Étape ' + n + ' sur ' + totalSteps : 'Step ' + n + ' of ' + totalSteps;
|
||||
const input = stepEl ? stepEl.querySelector('input, select') : null;
|
||||
// Only auto-focus on user-initiated step changes, not on page load
|
||||
if (input && dir) setTimeout(() => input.focus(), 100);
|
||||
currentStep = n;
|
||||
}
|
||||
@@ -495,6 +567,12 @@
|
||||
}
|
||||
}
|
||||
if (n === 2) {
|
||||
if (!selectedTier) {
|
||||
if (tierError) tierError.classList.add('shown');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (n === 3) {
|
||||
const v = fields.name.input.value.trim();
|
||||
if (!v) {
|
||||
fields.name.wrapper.classList.add('error');
|
||||
@@ -502,7 +580,7 @@
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (n === 3) {
|
||||
if (n === 4) {
|
||||
const v = fields.phone.input.value.trim();
|
||||
if (!v) {
|
||||
fields.phone.wrapper.classList.add('error');
|
||||
@@ -527,6 +605,7 @@
|
||||
async function submitForm() {
|
||||
const payload = {
|
||||
email: fields.email.input.value.trim(),
|
||||
tier: selectedTier,
|
||||
name: fields.name.input.value.trim(),
|
||||
phone: fields.phone.input.value.trim()
|
||||
};
|
||||
@@ -538,11 +617,10 @@
|
||||
});
|
||||
if (resp.ok) {
|
||||
showSuccess();
|
||||
// Save email/phone to cookie so order check is remembered
|
||||
setVelaCookie(STATUS_COOKIE, JSON.stringify({email: payload.email, phone: payload.phone}), 30);
|
||||
} else {
|
||||
const isFr = document.documentElement.classList.contains('lang-fr');
|
||||
alert(isFr ? 'Échec de l\'envoi. Réessayez.' : 'Submission failed. Please try again.');
|
||||
alert(isFr ? "Échec de l'envoi. Réessayez." : 'Submission failed. Please try again.');
|
||||
}
|
||||
} catch (e) {
|
||||
const isFr = document.documentElement.classList.contains('lang-fr');
|
||||
@@ -553,6 +631,9 @@
|
||||
function resetForm() {
|
||||
success.classList.remove('visible');
|
||||
Object.values(fields).forEach(f => { if (f.input) f.input.value = ''; });
|
||||
document.querySelectorAll('.tier-option').forEach(o => o.classList.remove('selected'));
|
||||
document.querySelectorAll('.tier-option input').forEach(i => i.checked = false);
|
||||
selectedTier = '';
|
||||
clearErrors();
|
||||
currentStep = 1;
|
||||
showStep(1);
|
||||
@@ -585,8 +666,12 @@
|
||||
if (validateStep(2)) showStep(3, 'animate-forward');
|
||||
});
|
||||
document.getElementById('back3').addEventListener('click', function() { showStep(2, 'animate-back'); });
|
||||
document.getElementById('next3').addEventListener('click', function() {
|
||||
if (validateStep(3)) showStep(4, 'animate-forward');
|
||||
});
|
||||
document.getElementById('back4').addEventListener('click', function() { showStep(3, 'animate-back'); });
|
||||
document.getElementById('submitBtn').addEventListener('click', function() {
|
||||
if (validateStep(3)) submitForm();
|
||||
if (validateStep(4)) submitForm();
|
||||
});
|
||||
document.getElementById('restartBtn').addEventListener('click', resetForm);
|
||||
showStep(1);
|
||||
|
||||
Reference in New Issue
Block a user