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);

View file

@ -864,7 +864,7 @@
<swp-salon-logo>SB</swp-salon-logo>
<swp-salon-info>
<h1>KARINA KNUDSEN®</h1>
<p>Hovedgaden 123, København</p>
<p>Amager Strandvej 22f, 2300 Kbh S</p>
</swp-salon-info>
</swp-salon-header>

View file

@ -756,6 +756,18 @@
font-weight: 600;
}
/* Toggle with description */
swp-toggle-info {
display: flex;
flex-direction: column;
gap: 2px;
}
swp-toggle-desc {
font-size: 12px;
color: var(--color-text-secondary);
}
/* Profile Boxes */
swp-profile-boxes {
display: grid;
@ -2137,6 +2149,31 @@
</swp-toggle-row>
</swp-card>
<swp-card draggable="true" data-card-id="betalingsindstillinger">
<swp-drag-handle><svg viewBox="0 0 24 24"><path d="M13 6v5h5V8l4 4-4 4v-3h-5v5h3l-4 4-4-4h3v-5H6v3l-4-4 4-4v3h5V6H8l4-4 4 4h-3z"/></svg></swp-drag-handle>
<swp-section-label>Betalingsindstillinger</swp-section-label>
<swp-toggle-row>
<swp-toggle-info>
<swp-toggle-label>Kræv forudbetaling</swp-toggle-label>
<swp-toggle-desc>Kunden skal betale fuldt beløb ved booking</swp-toggle-desc>
</swp-toggle-info>
<swp-toggle-slider data-value="no">
<swp-toggle-option>Ja</swp-toggle-option>
<swp-toggle-option>Nej</swp-toggle-option>
</swp-toggle-slider>
</swp-toggle-row>
<swp-toggle-row>
<swp-toggle-info>
<swp-toggle-label>Tillad delvis betaling</swp-toggle-label>
<swp-toggle-desc>Kunden kan vælge at betale et depositum</swp-toggle-desc>
</swp-toggle-info>
<swp-toggle-slider data-value="no">
<swp-toggle-option>Ja</swp-toggle-option>
<swp-toggle-option>Nej</swp-toggle-option>
</swp-toggle-slider>
</swp-toggle-row>
</swp-card>
<swp-card draggable="true" data-card-id="praeferencer">
<swp-drag-handle><svg viewBox="0 0 24 24"><path d="M13 6v5h5V8l4 4-4 4v-3h-5v5h3l-4 4-4-4h3v-5H6v3l-4-4 4-4v3h5V6H8l4-4 4 4h-3z"/></svg></swp-drag-handle>
<swp-section-label>Præferencer</swp-section-label>

View file

@ -1358,6 +1358,10 @@
<i class="ph ph-bell"></i>
Påmindelser
</swp-tab>
<swp-tab data-tab="payments">
<i class="ph ph-credit-card"></i>
Betalinger
</swp-tab>
</swp-tab-bar>
<!-- ==========================================
@ -1714,7 +1718,7 @@
</swp-edit-row>
<swp-edit-row>
<swp-edit-label>Adresse</swp-edit-label>
<swp-edit-value contenteditable="true">Hovedgaden 123, 2100 København Ø</swp-edit-value>
<swp-edit-value contenteditable="true">Amager Strandvej 22f, 2300 København S</swp-edit-value>
</swp-edit-row>
</swp-edit-section>
</swp-card-content>
@ -1994,6 +1998,146 @@ Tak for din handel!</swp-edit-textarea>
</swp-two-column-grid>
</swp-tab-content>
<!-- ==========================================
TAB: BETALINGER
========================================== -->
<swp-tab-content data-tab="payments">
<swp-card>
<swp-card-header>
<swp-card-title>
<i class="ph ph-wallet"></i>
Betalingsmetoder i butik
</swp-card-title>
</swp-card-header>
<swp-card-content>
<swp-section-intro>Vælg hvilke betalingsmetoder dine kunder kan bruge ved checkout i butikken.</swp-section-intro>
<swp-toggle-row>
<swp-toggle-info>
<swp-toggle-label>Kontant</swp-toggle-label>
<swp-toggle-desc>Modtag kontant betaling</swp-toggle-desc>
</swp-toggle-info>
<swp-toggle-slider data-value="yes">
<swp-toggle-option>Ja</swp-toggle-option>
<swp-toggle-option>Nej</swp-toggle-option>
</swp-toggle-slider>
</swp-toggle-row>
<swp-toggle-row>
<swp-toggle-info>
<swp-toggle-label>Dankort / Visa / Mastercard</swp-toggle-label>
<swp-toggle-desc>Betalingskort via terminal</swp-toggle-desc>
</swp-toggle-info>
<swp-toggle-slider data-value="yes">
<swp-toggle-option>Ja</swp-toggle-option>
<swp-toggle-option>Nej</swp-toggle-option>
</swp-toggle-slider>
</swp-toggle-row>
<swp-toggle-row>
<swp-toggle-info>
<swp-toggle-label>MobilePay</swp-toggle-label>
<swp-toggle-desc>Betaling via MobilePay</swp-toggle-desc>
</swp-toggle-info>
<swp-toggle-slider data-value="yes">
<swp-toggle-option>Ja</swp-toggle-option>
<swp-toggle-option>Nej</swp-toggle-option>
</swp-toggle-slider>
</swp-toggle-row>
<swp-toggle-row>
<swp-toggle-info>
<swp-toggle-label>Gavekort</swp-toggle-label>
<swp-toggle-desc>Indløs gavekort som betaling</swp-toggle-desc>
</swp-toggle-info>
<swp-toggle-slider data-value="yes">
<swp-toggle-option>Ja</swp-toggle-option>
<swp-toggle-option>Nej</swp-toggle-option>
</swp-toggle-slider>
</swp-toggle-row>
<swp-toggle-row>
<swp-toggle-info>
<swp-toggle-label>Faktura</swp-toggle-label>
<swp-toggle-desc>Send faktura til kunden</swp-toggle-desc>
</swp-toggle-info>
<swp-toggle-slider data-value="yes">
<swp-toggle-option>Ja</swp-toggle-option>
<swp-toggle-option>Nej</swp-toggle-option>
</swp-toggle-slider>
</swp-toggle-row>
</swp-card-content>
</swp-card>
<swp-card>
<swp-card-header>
<swp-card-title>
<i class="ph ph-globe"></i>
Online betaling
</swp-card-title>
</swp-card-header>
<swp-card-content>
<swp-toggle-row>
<swp-toggle-info>
<swp-toggle-label>Modtag betaling ved online booking</swp-toggle-label>
<swp-toggle-desc>Kunder betaler når de booker online</swp-toggle-desc>
</swp-toggle-info>
<swp-toggle-slider data-value="no" id="onlinePaymentToggle">
<swp-toggle-option>Ja</swp-toggle-option>
<swp-toggle-option>Nej</swp-toggle-option>
</swp-toggle-slider>
</swp-toggle-row>
<div id="onlinePaymentSettings" style="display: none;">
<swp-section-divider></swp-section-divider>
<swp-info-box>
<i class="ph ph-info"></i>
<p>Online betalinger håndteres via Stripe. Beløbet overføres automatisk til din bankkonto efter den valgte periode.</p>
</swp-info-box>
<swp-edit-section>
<swp-edit-row class="wide-label">
<swp-edit-label>Registreringsnummer</swp-edit-label>
<swp-edit-value contenteditable="true" class="mono">1234</swp-edit-value>
</swp-edit-row>
<swp-edit-row class="wide-label">
<swp-edit-label>Kontonummer</swp-edit-label>
<swp-edit-value contenteditable="true" class="mono">12345678</swp-edit-value>
</swp-edit-row>
<swp-edit-row class="wide-label">
<swp-edit-label>Udbetaling</swp-edit-label>
<swp-edit-select>
<select>
<option value="7" selected>Hver uge</option>
<option value="14">Hver 2. uge</option>
<option value="28">Hver 4. uge</option>
</select>
</swp-edit-select>
</swp-edit-row>
</swp-edit-section>
</div>
</swp-card-content>
</swp-card>
<swp-card>
<swp-card-header>
<swp-card-title>
<i class="ph ph-percent"></i>
Gebyr & tillæg
</swp-card-title>
</swp-card-header>
<swp-card-content>
<swp-toggle-row>
<swp-toggle-info>
<swp-toggle-label>Vis kortgebyr til kunden</swp-toggle-label>
<swp-toggle-desc>Vis kortgebyr som separat linje på kvittering</swp-toggle-desc>
</swp-toggle-info>
<swp-toggle-slider data-value="no">
<swp-toggle-option>Ja</swp-toggle-option>
<swp-toggle-option>Nej</swp-toggle-option>
</swp-toggle-slider>
</swp-toggle-row>
</swp-card-content>
</swp-card>
</swp-tab-content>
<!-- ==========================================
DRAWER: REDIGER BESKED
========================================== -->
@ -2165,7 +2309,7 @@ Vil du ændre din tid? {booking_link}</swp-message-editor>
</swp-email-appointment-row>
<swp-email-appointment-row>
<i class="ph ph-map-pin"></i>
<span>Hovedgaden 123, 2100 København Ø</span>
<span>Amager Strandvej 22f, 2300 København S</span>
</swp-email-appointment-row>
</swp-email-appointment-card>
@ -2176,7 +2320,7 @@ Vil du ændre din tid? {booking_link}</swp-message-editor>
<swp-email-footer>
<swp-email-footer-logo>KARINA KNUDSEN®</swp-email-footer-logo>
<p>Hovedgaden 123, 2100 København Ø</p>
<p>Amager Strandvej 22f, 2300 København S</p>
<p>Tlf: 70 20 30 40 · <a href="#">info@salonbeauty.dk</a></p>
<p style="margin-top: 12px; font-size: 11px;">
<a href="#">Afmeld emails</a>
@ -2475,6 +2619,31 @@ Vil du ændre din tid? {booking_link}</swp-message-editor>
if (emailDrawer.classList.contains('open')) closeEmailDrawer();
}
});
// ==========================================
// ONLINE PAYMENT TOGGLE
// ==========================================
const onlinePaymentToggle = document.getElementById('onlinePaymentToggle');
const onlinePaymentSettings = document.getElementById('onlinePaymentSettings');
function updateOnlinePaymentVisibility() {
if (onlinePaymentToggle && onlinePaymentSettings) {
const isEnabled = onlinePaymentToggle.dataset.value === 'yes';
onlinePaymentSettings.style.display = isEnabled ? 'block' : 'none';
}
}
if (onlinePaymentToggle) {
onlinePaymentToggle.querySelectorAll('swp-toggle-option').forEach((option, index) => {
option.addEventListener('click', () => {
onlinePaymentToggle.dataset.value = index === 0 ? 'yes' : 'no';
updateOnlinePaymentVisibility();
});
});
// Initial state
updateOnlinePaymentVisibility();
}
</script>
</body>