Adds waitlist feature for booking system

Introduces new waitlist functionality to allow customers to register for unavailable time slots

Includes:
- Waitlist trigger and section in booking interface
- Ability to select preferred time periods
- Date and optional message input
- Success overlay for waitlist submission

Updates salon address in multiple files
This commit is contained in:
Janus C. H. Knudsen 2026-01-02 07:39:53 +01:00
parent dacdf5d153
commit f73133b51c
4 changed files with 627 additions and 6 deletions

View file

@ -504,6 +504,192 @@
text-decoration: line-through;
}
/* ==========================================
WAITLIST
========================================== */
.waitlist-trigger {
display: flex;
align-items: center;
gap: 8px;
margin-top: 20px;
padding: 12px 16px;
width: 100%;
background: transparent;
border: 1px dashed var(--color-border);
border-radius: 8px;
font-size: 13px;
font-family: var(--font-family);
color: var(--color-text-secondary);
cursor: pointer;
transition: all 150ms ease;
}
.waitlist-trigger:hover {
border-color: var(--color-teal);
color: var(--color-teal);
background: var(--color-teal-light);
}
.waitlist-trigger i {
font-size: 18px;
}
.waitlist-section {
overflow: hidden;
height: 0;
}
.waitlist-content {
padding-top: 20px;
}
.waitlist-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 16px;
}
.waitlist-title {
font-size: 14px;
font-weight: 600;
color: var(--color-text);
}
.waitlist-close {
display: flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
background: transparent;
border: none;
border-radius: 6px;
color: var(--color-text-secondary);
cursor: pointer;
transition: all 150ms ease;
}
.waitlist-close:hover {
background: var(--color-background);
color: var(--color-text);
}
.waitlist-close i {
font-size: 18px;
}
.waitlist-desc {
font-size: 13px;
color: var(--color-text-secondary);
margin-bottom: 20px;
line-height: 1.5;
}
.waitlist-field {
margin-bottom: 16px;
}
.waitlist-field:last-child {
margin-bottom: 0;
}
.waitlist-label {
display: block;
font-size: 13px;
font-weight: 500;
color: var(--color-text);
margin-bottom: 6px;
}
.waitlist-periods {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 8px;
}
.period-option {
display: flex;
align-items: center;
gap: 10px;
padding: 12px 14px;
background: var(--color-surface);
border: 2px solid transparent;
border-radius: 8px;
cursor: pointer;
transition: all 150ms ease;
}
.period-option:hover {
border-color: var(--color-border);
}
.period-option.selected {
border-color: var(--color-teal);
background: var(--color-teal-light);
}
.period-check {
width: 20px;
height: 20px;
border-radius: 4px;
border: 2px solid var(--color-border);
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
color: white;
transition: all 150ms ease;
flex-shrink: 0;
}
.period-option.selected .period-check {
background: var(--color-teal);
border-color: var(--color-teal);
}
.period-check i {
opacity: 0;
transition: opacity 150ms ease;
}
.period-option.selected .period-check i {
opacity: 1;
}
.period-info {
flex: 1;
}
.period-name {
font-size: 13px;
font-weight: 500;
}
.period-time {
font-size: 11px;
color: var(--color-text-secondary);
}
.waitlist-submit {
margin-top: 20px;
width: 100%;
padding: 12px 20px;
background: var(--color-teal);
color: white;
font-size: 14px;
font-weight: 500;
font-family: var(--font-family);
border: none;
border-radius: 8px;
cursor: pointer;
transition: background 150ms ease;
}
.waitlist-submit:hover {
background: #00695c;
}
/* ==========================================
FORM
========================================== */
@ -847,6 +1033,39 @@
line-height: 1.5;
}
.success-icon.waitlist {
background: rgba(0, 137, 123, 0.12);
}
.success-icon.waitlist i {
color: var(--color-teal);
}
.success-btn {
display: inline-flex;
align-items: center;
gap: 6px;
margin-top: 20px;
padding: 12px 24px;
background: var(--color-teal);
color: white;
font-size: 14px;
font-weight: 500;
font-family: var(--font-family);
border: none;
border-radius: 8px;
cursor: pointer;
transition: background 150ms ease;
}
.success-btn:hover {
background: #00695c;
}
.success-btn i {
font-size: 16px;
}
/* ==========================================
LANDING VIEW
========================================== */
@ -1318,7 +1537,7 @@
<div class="landing-card">
<div class="salon-logo">KK</div>
<div class="salon-name">KARINA KNUDSEN®</div>
<div class="salon-address">Hovedgaden 123, København</div>
<div class="salon-address">Amager Strandvej 22f, 2300 Kbh S</div>
<!-- Step 1: Phone -->
<div class="landing-step" id="landingStep1">
@ -1465,7 +1684,7 @@
<div class="salon-logo">KK</div>
<div>
<div class="salon-name">KARINA KNUDSEN®</div>
<div class="salon-address">Hovedgaden 123, København</div>
<div class="salon-address">Amager Strandvej 22f, 2300 Kbh S</div>
</div>
</div>
@ -1627,6 +1846,20 @@
</div>
</div>
<!-- Waitlist success overlay -->
<div class="success-overlay" id="waitlistSuccessOverlay">
<div class="success-modal">
<div class="success-icon waitlist">
<i class="ph ph-clock-countdown"></i>
</div>
<h2>Du er på ventelisten!</h2>
<p>Tak for din tilmelding. Vi kontakter dig hurtigst muligt, hvis der bliver en ledig tid.</p>
<button class="success-btn" id="waitlistSuccessClose">
Luk <i class="ph ph-x"></i>
</button>
</div>
</div>
<script>
// ==========================================
// CONFIG
@ -1997,6 +2230,75 @@
return `<div class="${cls}" data-time="${t}">${t}</div>`;
}).join('')}
</div>
<!-- Waitlist -->
<button class="waitlist-trigger" id="waitlistTrigger">
<i class="ph ph-clock-countdown"></i>
Ingen tid der passer? Skriv dig på venteliste
</button>
<div class="waitlist-section" id="waitlistSection">
<div class="waitlist-content">
<div class="waitlist-header">
<div class="waitlist-title">Skriv dig på venteliste</div>
<button class="waitlist-close" id="waitlistClose">
<i class="ph ph-x"></i>
</button>
</div>
<p class="waitlist-desc">
Hvis der bliver en ledig tid, kontakter vi dig hurtigst muligt.
Angiv hvornår du senest ønsker en tid, og hvilke tidspunkter der passer dig.
</p>
<div class="waitlist-field">
<label class="waitlist-label">Senest dato for aftale</label>
<input type="date" class="form-input" id="waitlistDate">
</div>
<div class="waitlist-field">
<label class="waitlist-label">Hvilke tidspunkter passer dig?</label>
<div class="waitlist-periods">
<div class="period-option" data-period="morning">
<div class="period-check"><i class="ph ph-check"></i></div>
<div class="period-info">
<div class="period-name">Morgen</div>
<div class="period-time">Åbning - 10:00</div>
</div>
</div>
<div class="period-option" data-period="lateMorning">
<div class="period-check"><i class="ph ph-check"></i></div>
<div class="period-info">
<div class="period-name">Formiddag</div>
<div class="period-time">10:00 - 12:00</div>
</div>
</div>
<div class="period-option" data-period="afternoon">
<div class="period-check"><i class="ph ph-check"></i></div>
<div class="period-info">
<div class="period-name">Eftermiddag</div>
<div class="period-time">12:00 - 17:00</div>
</div>
</div>
<div class="period-option" data-period="evening">
<div class="period-check"><i class="ph ph-check"></i></div>
<div class="period-info">
<div class="period-name">Aften</div>
<div class="period-time">17:00 - Luk</div>
</div>
</div>
</div>
</div>
<div class="waitlist-field">
<label class="waitlist-label">Besked til salonen <span class="optional">(valgfrit)</span></label>
<textarea class="form-input" id="waitlistMessage" placeholder="Evt. særlige ønsker eller kommentarer..." rows="3"></textarea>
</div>
<button class="waitlist-submit" id="waitlistSubmit">
<i class="ph ph-check"></i> Skriv mig på venteliste
</button>
</div>
</div>
</div>
</div>
`;
@ -2032,6 +2334,113 @@
}
});
});
// Waitlist functionality
setupWaitlist();
}
// ==========================================
// WAITLIST
// ==========================================
let waitlistOpen = false;
const waitlistState = {
date: null,
periods: [],
message: ''
};
function setupWaitlist() {
const trigger = document.getElementById('waitlistTrigger');
const section = document.getElementById('waitlistSection');
const closeBtn = document.getElementById('waitlistClose');
const submitBtn = document.getElementById('waitlistSubmit');
if (!trigger || !section) return;
trigger.addEventListener('click', () => toggleWaitlist(true));
closeBtn.addEventListener('click', () => toggleWaitlist(false));
// Period selection
document.querySelectorAll('.period-option').forEach(option => {
option.addEventListener('click', () => {
option.classList.toggle('selected');
const period = option.dataset.period;
const idx = waitlistState.periods.indexOf(period);
if (idx > -1) {
waitlistState.periods.splice(idx, 1);
} else {
waitlistState.periods.push(period);
}
});
});
// Submit
submitBtn.addEventListener('click', handleWaitlistSubmit);
}
function toggleWaitlist(open) {
const trigger = document.getElementById('waitlistTrigger');
const section = document.getElementById('waitlistSection');
if (open) {
trigger.style.display = 'none';
section.style.height = 'auto';
const targetHeight = section.scrollHeight;
section.style.height = '0px';
section.offsetHeight;
const anim = section.animate(
[{ height: '0px' }, { height: targetHeight + 'px' }],
{ duration: ANIM_DURATION, easing: 'ease-out' }
);
anim.onfinish = () => { section.style.height = 'auto'; };
waitlistOpen = true;
} else {
const currentHeight = section.scrollHeight;
const anim = section.animate(
[{ height: currentHeight + 'px' }, { height: '0px' }],
{ duration: ANIM_DURATION, easing: 'ease-out' }
);
anim.onfinish = () => {
section.style.height = '0px';
trigger.style.display = 'flex';
};
waitlistOpen = false;
}
}
function handleWaitlistSubmit() {
const date = document.getElementById('waitlistDate').value;
const message = document.getElementById('waitlistMessage').value;
if (!date) {
alert('Vælg venligst en seneste dato');
return;
}
if (waitlistState.periods.length === 0) {
alert('Vælg mindst ét tidspunkt der passer dig');
return;
}
waitlistState.date = date;
waitlistState.message = message;
// Show success overlay
document.getElementById('waitlistSuccessOverlay').classList.add('visible');
}
function closeWaitlistSuccess() {
document.getElementById('waitlistSuccessOverlay').classList.remove('visible');
toggleWaitlist(false);
// Reset waitlist form
document.getElementById('waitlistDate').value = '';
document.getElementById('waitlistMessage').value = '';
document.querySelectorAll('.period-option').forEach(opt => opt.classList.remove('selected'));
waitlistState.periods = [];
waitlistState.date = null;
waitlistState.message = '';
}
// ==========================================
@ -2352,6 +2761,12 @@
if (e.target.id === 'successOverlay') location.reload();
});
// Waitlist success overlay close
document.getElementById('waitlistSuccessClose').addEventListener('click', closeWaitlistSuccess);
document.getElementById('waitlistSuccessOverlay').addEventListener('click', (e) => {
if (e.target.id === 'waitlistSuccessOverlay') closeWaitlistSuccess();
});
// View switching - Landing
document.getElementById('phoneNextBtn').addEventListener('click', handlePhoneNext);
document.getElementById('backToPhoneBtn').addEventListener('click', handleBackToPhone);