Calendar/wwwroot/poc-booking.html

1565 lines
44 KiB
HTML
Raw Normal View History

<!DOCTYPE html>
<html lang="da">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Book tid - KARINA KNUDSEN®</title>
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<style>
@font-face { font-family: 'Poppins'; src: url('fonts/Poppins-Regular.woff') format('woff'); font-weight: 400; }
@font-face { font-family: 'Poppins'; src: url('fonts/Poppins-Medium.woff') format('woff'); font-weight: 500; }
@font-face { font-family: 'Poppins'; src: url('fonts/Poppins-SemiBold.woff') format('woff'); font-weight: 600; }
@font-face { font-family: 'Poppins'; src: url('fonts/Poppins-Bold.woff') format('woff'); font-weight: 700; }
</style>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@phosphor-icons/web@2.1.2/src/regular/style.css">
<style>
:root {
--color-teal: #00897b;
--color-teal-light: #e0f2f1;
--color-surface: #ffffff;
--color-background: #f5f5f5;
--color-border: #e0e0e0;
--color-text: #333;
--color-text-secondary: #666;
--color-text-muted: #999;
--color-green: #43a047;
--color-amber: #f59e0b;
--font-family: 'Poppins', -apple-system, sans-serif;
--font-mono: 'JetBrains Mono', monospace;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: var(--font-family);
background: var(--color-background);
color: var(--color-text);
min-height: 100vh;
}
/* ==========================================
LAYOUT
========================================== */
swp-booking-page {
display: grid;
grid-template-columns: 1fr 380px;
max-width: 1200px;
margin: 0 auto;
min-height: 100vh;
gap: 0;
}
@media (max-width: 900px) {
swp-booking-page {
grid-template-columns: 1fr;
}
swp-booking-sidebar {
display: none;
}
}
/* ==========================================
MAIN CONTENT
========================================== */
swp-booking-main {
display: flex;
flex-direction: column;
padding: 40px;
background: var(--color-surface);
}
swp-salon-header {
display: flex;
align-items: center;
gap: 16px;
margin-bottom: 40px;
}
swp-salon-logo {
width: 56px;
height: 56px;
background: linear-gradient(135deg, var(--color-teal) 0%, #00695c 100%);
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
font-weight: 700;
color: white;
}
swp-salon-info h1 {
font-size: 20px;
font-weight: 600;
margin-bottom: 2px;
}
swp-salon-info p {
font-size: 13px;
color: var(--color-text-secondary);
}
/* ==========================================
ACCORDION STEPS
========================================== */
swp-steps-container {
display: flex;
flex-direction: column;
gap: 12px;
}
swp-step {
display: block;
background: var(--color-background);
border-radius: 12px;
overflow: hidden;
transition: all 200ms ease;
}
swp-step.disabled {
opacity: 0.5;
pointer-events: none;
}
swp-step-header {
display: flex;
align-items: center;
gap: 16px;
padding: 20px 24px;
cursor: pointer;
transition: background 150ms ease;
}
swp-step:not(.disabled) swp-step-header:hover {
background: color-mix(in srgb, var(--color-teal) 5%, var(--color-background));
}
swp-step-icon {
font-size: 22px;
color: var(--color-text-muted);
flex-shrink: 0;
transition: all 200ms ease;
}
swp-step.completed swp-step-icon {
color: var(--color-green);
}
swp-step.active swp-step-icon {
color: var(--color-teal);
}
swp-step-title {
flex: 1;
}
swp-step-title h3 {
font-size: 15px;
font-weight: 600;
margin-bottom: 2px;
}
swp-step-title p {
font-size: 13px;
color: var(--color-text-secondary);
}
swp-step.completed swp-step-title p {
color: var(--color-teal);
font-weight: 500;
}
swp-step-action {
font-size: 13px;
color: var(--color-teal);
font-weight: 500;
opacity: 0;
transition: opacity 150ms ease;
}
swp-step.completed:hover swp-step-action {
opacity: 1;
}
swp-step-chevron {
color: var(--color-text-muted);
font-size: 20px;
transition: transform 300ms ease-out;
}
swp-step.active swp-step-chevron {
transform: rotate(180deg);
}
swp-step-content {
display: block;
overflow: hidden;
height: 0;
}
swp-step-content-inner {
display: flex;
flex-direction: column;
gap: 16px;
padding: 0 24px 24px;
}
/* Floating Videre button */
swp-floating-next {
position: fixed;
bottom: 0;
left: 0;
right: 0;
display: flex;
justify-content: center;
padding: 20px;
background: linear-gradient(to top, var(--color-surface) 60%, transparent);
pointer-events: none;
opacity: 0;
transform: translateY(20px);
transition: opacity 250ms ease, transform 250ms ease;
z-index: 100;
}
swp-floating-next.visible {
opacity: 1;
transform: translateY(0);
pointer-events: auto;
}
swp-next-btn {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 14px 32px;
background: var(--color-teal);
color: white;
font-size: 15px;
font-weight: 500;
font-family: var(--font-family);
border: none;
border-radius: 10px;
cursor: pointer;
box-shadow: 0 4px 20px rgba(0, 137, 123, 0.3);
transition: background 150ms ease, transform 150ms ease;
}
swp-next-btn:hover {
background: #00695c;
transform: scale(1.02);
}
swp-next-btn i {
font-size: 18px;
}
/* ==========================================
SERVICES GRID
========================================== */
swp-services-section {
display: flex;
flex-direction: column;
gap: 12px;
}
swp-section-title {
display: block;
font-size: 12px;
font-weight: 600;
color: var(--color-text-secondary);
text-transform: uppercase;
letter-spacing: 0.5px;
}
swp-services-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 10px;
}
@media (max-width: 600px) {
swp-services-grid {
grid-template-columns: 1fr;
}
}
swp-service-item {
display: flex;
align-items: center;
gap: 12px;
padding: 14px 16px;
background: var(--color-surface);
border: 2px solid transparent;
border-radius: 10px;
cursor: pointer;
transition: all 150ms ease;
}
swp-service-item:hover {
border-color: var(--color-border);
}
swp-service-item.selected {
border-color: var(--color-teal);
background: var(--color-teal-light);
}
swp-service-check {
width: 22px;
height: 22px;
border-radius: 6px;
border: 2px solid var(--color-border);
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
transition: all 150ms ease;
}
swp-service-item.selected swp-service-check {
background: var(--color-teal);
border-color: var(--color-teal);
color: white;
}
swp-service-check i {
font-size: 14px;
opacity: 0;
}
swp-service-item.selected swp-service-check i {
opacity: 1;
}
swp-service-details {
flex: 1;
min-width: 0;
}
swp-service-name {
display: block;
font-size: 14px;
font-weight: 500;
margin-bottom: 2px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
swp-service-meta {
font-size: 12px;
color: var(--color-text-secondary);
}
swp-service-price {
font-size: 14px;
font-weight: 600;
font-family: var(--font-mono);
color: var(--color-text);
white-space: nowrap;
}
/* ==========================================
EMPLOYEE SELECTION
========================================== */
swp-employee-grid {
display: flex;
flex-wrap: wrap;
gap: 12px;
}
swp-employee-item {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 16px;
background: var(--color-surface);
border: 2px solid transparent;
border-radius: 10px;
cursor: pointer;
transition: all 150ms ease;
}
swp-employee-item:hover {
border-color: var(--color-border);
}
swp-employee-item.selected {
border-color: var(--color-teal);
background: var(--color-teal-light);
}
swp-employee-avatar {
width: 40px;
height: 40px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
font-weight: 600;
color: white;
flex-shrink: 0;
}
swp-employee-item.no-pref swp-employee-avatar {
background: var(--color-border);
color: var(--color-text-secondary);
}
swp-employee-info {
display: flex;
flex-direction: column;
}
swp-employee-name {
font-size: 14px;
font-weight: 500;
}
swp-employee-role {
font-size: 12px;
color: var(--color-text-secondary);
}
/* ==========================================
DATE & TIME
========================================== */
swp-datetime-layout {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 24px;
}
@media (max-width: 600px) {
swp-datetime-layout {
grid-template-columns: 1fr;
}
}
swp-calendar {
display: block;
background: var(--color-surface);
border-radius: 10px;
padding: 16px;
}
swp-calendar-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 16px;
}
swp-calendar-month {
font-size: 15px;
font-weight: 600;
}
swp-calendar-nav {
display: flex;
gap: 4px;
}
swp-calendar-nav button {
width: 32px;
height: 32px;
border: none;
background: var(--color-background);
border-radius: 8px;
cursor: pointer;
color: var(--color-text-secondary);
display: flex;
align-items: center;
justify-content: center;
transition: all 150ms ease;
}
swp-calendar-nav button:hover {
background: var(--color-teal);
color: white;
}
swp-calendar-grid {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 4px;
}
swp-weekday {
text-align: center;
font-size: 11px;
font-weight: 600;
color: var(--color-text-muted);
text-transform: uppercase;
padding: 8px 0;
}
swp-day {
aspect-ratio: 1;
display: flex;
align-items: center;
justify-content: center;
font-size: 13px;
font-weight: 500;
border-radius: 8px;
cursor: pointer;
transition: all 150ms ease;
}
swp-day:hover:not(.disabled):not(.selected) {
background: var(--color-background);
}
swp-day.today {
color: var(--color-teal);
font-weight: 600;
}
swp-day.selected {
background: var(--color-teal);
color: white;
}
swp-day.disabled {
color: var(--color-border);
cursor: default;
}
swp-day.other-month {
color: var(--color-text-muted);
}
swp-time-section {
display: block;
}
swp-time-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 8px;
}
swp-time-slot {
padding: 12px 8px;
text-align: center;
font-size: 14px;
font-weight: 500;
font-family: var(--font-mono);
background: var(--color-surface);
border-radius: 8px;
cursor: pointer;
transition: all 150ms ease;
}
swp-time-slot:hover:not(.disabled) {
background: var(--color-teal-light);
}
swp-time-slot.selected {
background: var(--color-teal);
color: white;
}
swp-time-slot.disabled {
color: var(--color-text-muted);
cursor: default;
text-decoration: line-through;
}
/* ==========================================
CONTACT FORM
========================================== */
swp-form-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
}
@media (max-width: 600px) {
swp-form-grid {
grid-template-columns: 1fr;
}
}
swp-form-field {
display: flex;
flex-direction: column;
gap: 6px;
}
swp-form-field.full-width {
grid-column: 1 / -1;
}
swp-form-label {
font-size: 13px;
font-weight: 500;
color: var(--color-text);
}
swp-form-label span {
font-weight: 400;
color: var(--color-text-muted);
}
swp-form-input {
display: block;
width: 100%;
padding: 12px 14px;
font-size: 14px;
font-family: var(--font-family);
color: var(--color-text);
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: 8px;
transition: all 150ms ease;
}
swp-form-input:focus {
outline: none;
border-color: var(--color-teal);
box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-teal) 15%, transparent);
}
swp-form-textarea {
display: block;
width: 100%;
padding: 12px 14px;
font-size: 14px;
font-family: var(--font-family);
color: var(--color-text);
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: 8px;
resize: vertical;
min-height: 80px;
}
swp-form-textarea:focus {
outline: none;
border-color: var(--color-teal);
box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-teal) 15%, transparent);
}
/* ==========================================
SIDEBAR
========================================== */
swp-booking-sidebar {
display: flex;
flex-direction: column;
background: var(--color-background);
border-left: 1px solid var(--color-border);
padding: 40px 32px;
}
swp-sidebar-title {
display: block;
font-size: 13px;
font-weight: 600;
color: var(--color-text-secondary);
text-transform: uppercase;
letter-spacing: 0.5px;
margin-bottom: 20px;
}
swp-cart-empty {
display: block;
text-align: center;
padding: 40px 20px;
color: var(--color-text-muted);
font-size: 14px;
}
swp-cart-empty i {
font-size: 48px;
margin-bottom: 12px;
display: block;
}
swp-cart-items {
display: flex;
flex-direction: column;
gap: 12px;
margin-bottom: 24px;
}
swp-cart-item {
display: flex;
justify-content: space-between;
align-items: flex-start;
padding: 14px 16px;
background: var(--color-surface);
border-radius: 10px;
}
swp-cart-item-info {
display: flex;
flex-direction: column;
gap: 4px;
}
swp-cart-item-name {
font-size: 14px;
font-weight: 500;
}
swp-cart-item-duration {
font-size: 12px;
color: var(--color-text-secondary);
}
swp-cart-item-price {
font-size: 14px;
font-weight: 600;
font-family: var(--font-mono);
}
swp-cart-details {
display: flex;
flex-direction: column;
gap: 12px;
padding: 16px 0;
border-top: 1px solid var(--color-border);
margin-bottom: 24px;
}
swp-cart-detail {
display: flex;
align-items: center;
gap: 12px;
font-size: 13px;
}
swp-cart-detail i {
font-size: 18px;
color: var(--color-teal);
width: 20px;
}
swp-cart-detail span {
color: var(--color-text-secondary);
}
swp-cart-detail strong {
color: var(--color-text);
font-weight: 500;
}
swp-cart-total {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px;
background: var(--color-teal-light);
border-radius: 12px;
margin-bottom: 24px;
}
swp-cart-total-label {
font-size: 15px;
font-weight: 600;
}
swp-cart-total-price {
font-size: 24px;
font-weight: 700;
font-family: var(--font-mono);
color: var(--color-teal);
}
swp-book-btn {
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
width: 100%;
padding: 16px 24px;
background: var(--color-teal);
color: white;
font-size: 15px;
font-weight: 600;
font-family: var(--font-family);
border: none;
border-radius: 12px;
cursor: pointer;
transition: all 150ms ease;
}
swp-book-btn:hover:not(:disabled) {
background: #00695c;
}
swp-book-btn:disabled {
background: var(--color-border);
cursor: not-allowed;
}
swp-book-btn i {
font-size: 20px;
}
/* ==========================================
SUCCESS OVERLAY
========================================== */
swp-success-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.5);
display: none;
align-items: center;
justify-content: center;
z-index: 1000;
}
swp-success-overlay.active {
display: flex;
}
swp-success-modal {
background: var(--color-surface);
border-radius: 20px;
padding: 48px;
text-align: center;
max-width: 400px;
margin: 20px;
}
swp-success-icon {
width: 80px;
height: 80px;
background: color-mix(in srgb, var(--color-green) 15%, transparent);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto 24px;
}
swp-success-icon i {
font-size: 40px;
color: var(--color-green);
}
swp-success-modal h2 {
font-size: 22px;
font-weight: 600;
margin-bottom: 12px;
}
swp-success-modal p {
font-size: 14px;
color: var(--color-text-secondary);
line-height: 1.6;
}
</style>
</head>
<body>
<swp-booking-page>
<!-- Main Content -->
<swp-booking-main>
<swp-salon-header>
<swp-salon-logo>SB</swp-salon-logo>
<swp-salon-info>
<h1>KARINA KNUDSEN®</h1>
<p>Amager Strandvej 22f, 2300 Kbh S</p>
</swp-salon-info>
</swp-salon-header>
<swp-steps-container>
<!-- Step 1: Services -->
<swp-step class="active" data-step="1">
<swp-step-header>
<swp-step-icon><i class="ph ph-scissors"></i></swp-step-icon>
<swp-step-title>
<h3>Vælg ydelse</h3>
<p id="step1Summary">Vælg hvad du vil have lavet</p>
</swp-step-title>
<swp-step-action>Rediger</swp-step-action>
<swp-step-chevron><i class="ph ph-caret-down"></i></swp-step-chevron>
</swp-step-header>
<swp-step-content>
<swp-step-content-inner>
<div id="servicesContent"></div>
</swp-step-content-inner>
</swp-step-content>
</swp-step>
<!-- Step 2: Employee -->
<swp-step class="disabled" data-step="2">
<swp-step-header>
<swp-step-icon><i class="ph ph-user-circle"></i></swp-step-icon>
<swp-step-title>
<h3>Vælg medarbejder</h3>
<p id="step2Summary">Valgfrit</p>
</swp-step-title>
<swp-step-action>Rediger</swp-step-action>
<swp-step-chevron><i class="ph ph-caret-down"></i></swp-step-chevron>
</swp-step-header>
<swp-step-content>
<swp-step-content-inner>
<div id="employeesContent"></div>
</swp-step-content-inner>
</swp-step-content>
</swp-step>
<!-- Step 3: Date & Time -->
<swp-step class="disabled" data-step="3">
<swp-step-header>
<swp-step-icon><i class="ph ph-calendar"></i></swp-step-icon>
<swp-step-title>
<h3>Vælg dato og tid</h3>
<p id="step3Summary">Find en ledig tid</p>
</swp-step-title>
<swp-step-action>Rediger</swp-step-action>
<swp-step-chevron><i class="ph ph-caret-down"></i></swp-step-chevron>
</swp-step-header>
<swp-step-content>
<swp-step-content-inner>
<div id="datetimeContent"></div>
</swp-step-content-inner>
</swp-step-content>
</swp-step>
<!-- Step 4: Contact -->
<swp-step class="disabled" data-step="4">
<swp-step-header>
<swp-step-icon><i class="ph ph-identification-card"></i></swp-step-icon>
<swp-step-title>
<h3>Dine oplysninger</h3>
<p id="step4Summary">Kontaktinformation</p>
</swp-step-title>
<swp-step-action>Rediger</swp-step-action>
<swp-step-chevron><i class="ph ph-caret-down"></i></swp-step-chevron>
</swp-step-header>
<swp-step-content>
<swp-step-content-inner>
<swp-form-grid>
<swp-form-field>
<swp-form-label>Fornavn</swp-form-label>
<swp-form-input type="text" id="firstName" placeholder="Dit fornavn" is="input">
</swp-form-field>
<swp-form-field>
<swp-form-label>Efternavn</swp-form-label>
<swp-form-input type="text" id="lastName" placeholder="Dit efternavn" is="input">
</swp-form-field>
<swp-form-field>
<swp-form-label>Telefon</swp-form-label>
<swp-form-input type="tel" id="phone" placeholder="12 34 56 78" is="input">
</swp-form-field>
<swp-form-field>
<swp-form-label>Email</swp-form-label>
<swp-form-input type="email" id="email" placeholder="din@email.dk" is="input">
</swp-form-field>
<swp-form-field class="full-width">
<swp-form-label>Noter <span>(valgfrit)</span></swp-form-label>
<swp-form-textarea id="notes" placeholder="Særlige ønsker..." is="textarea"></swp-form-textarea>
</swp-form-field>
</swp-form-grid>
</swp-step-content-inner>
</swp-step-content>
</swp-step>
</swp-steps-container>
</swp-booking-main>
<!-- Sidebar -->
<swp-booking-sidebar>
<swp-sidebar-title>Din booking</swp-sidebar-title>
<swp-cart-empty id="cartEmpty">
<i class="ph ph-shopping-cart"></i>
Vælg en ydelse for at starte
</swp-cart-empty>
<swp-cart-items id="cartItems"></swp-cart-items>
<swp-cart-details id="cartDetails" style="display: none;">
<swp-cart-detail id="detailEmployee">
<i class="ph ph-user-circle"></i>
<span>Medarbejder:</span>
<strong>Ikke valgt</strong>
</swp-cart-detail>
<swp-cart-detail id="detailDate">
<i class="ph ph-calendar"></i>
<span>Dato:</span>
<strong>Ikke valgt</strong>
</swp-cart-detail>
<swp-cart-detail id="detailTime">
<i class="ph ph-clock"></i>
<span>Tid:</span>
<strong>Ikke valgt</strong>
</swp-cart-detail>
</swp-cart-details>
<swp-cart-total id="cartTotal" style="display: none;">
<swp-cart-total-label>Total</swp-cart-total-label>
<swp-cart-total-price id="totalPrice">0 kr.</swp-cart-total-price>
</swp-cart-total>
<swp-book-btn id="bookBtn" disabled>
<i class="ph ph-check-circle"></i>
Book tid
</swp-book-btn>
</swp-booking-sidebar>
</swp-booking-page>
<!-- Floating Next Button -->
<swp-floating-next id="floatingNext">
<swp-next-btn id="nextBtn">Videre <i class="ph ph-arrow-right"></i></swp-next-btn>
</swp-floating-next>
<!-- Success Modal -->
<swp-success-overlay id="successOverlay">
<swp-success-modal>
<swp-success-icon>
<i class="ph ph-check"></i>
</swp-success-icon>
<h2>Booking bekræftet!</h2>
<p>Du modtager en bekræftelse på email og SMS. Vi glæder os til at se dig!</p>
</swp-success-modal>
</swp-success-overlay>
<script>
// ==========================================
// STATE
// ==========================================
const state = {
services: [],
employee: null,
date: null,
time: null,
customer: {}
};
// ==========================================
// DATA
// ==========================================
const servicesData = {
"Klip": [
{ id: "s1", name: "Dameklip", duration: 60, price: 725 },
{ id: "s2", name: "Herreklip", duration: 60, price: 645 },
{ id: "s3", name: "Børneklip (0-12 år)", duration: 45, price: 475 }
],
"Farve": [
{ id: "s4", name: "Bundfarve", duration: 90, price: 785 },
{ id: "s5", name: "Helfarve kort hår", duration: 105, price: 950 },
{ id: "s6", name: "Striber/Highlights", duration: 120, price: 1465 }
],
"Styling": [
{ id: "s7", name: "Vask & føn", duration: 40, price: 450 },
{ id: "s8", name: "Håropsætning", duration: 60, price: 850 }
],
"Behandlinger": [
{ id: "s9", name: "Olaplex", duration: 60, price: 550 },
{ id: "s10", name: "Kurbehandling", duration: 40, price: 365 }
]
};
const employeesData = [
{ id: null, name: "Ingen præference", role: "Første ledige", color: null },
{ id: "EMP001", name: "Camilla", role: "Master Stylist", color: "#9c27b0" },
{ id: "EMP002", name: "Isabella", role: "Master Stylist", color: "#e91e63" },
{ id: "EMP003", name: "Alexander", role: "Frisør", color: "#3f51b5" },
{ id: "EMP004", name: "Viktor", role: "Frisør", color: "#009688" }
];
// ==========================================
// INIT
// ==========================================
function init() {
renderServices();
renderEmployees();
renderCalendar();
renderTimeSlots();
setupStepHeaders();
setupFloatingButton();
setupFormListeners();
initFirstStep();
}
// ==========================================
// STEP NAVIGATION
// ==========================================
function getActiveStep() {
const active = document.querySelector('swp-step.active');
return active ? parseInt(active.dataset.step) : null;
}
function animateStepContent(stepEl, expand) {
const content = stepEl.querySelector('swp-step-content');
const inner = content.querySelector('swp-step-content-inner');
if (expand) {
// Measure target height
content.style.height = 'auto';
const targetHeight = content.offsetHeight;
content.style.height = '0px';
// Animate open
content.animate([
{ height: '0px' },
{ height: targetHeight + 'px' }
], {
duration: 300,
easing: 'ease-out',
fill: 'forwards'
}).onfinish = () => {
content.style.height = 'auto';
};
} else {
const currentHeight = content.offsetHeight;
// Animate close
content.animate([
{ height: currentHeight + 'px' },
{ height: '0px' }
], {
duration: 300,
easing: 'ease-out',
fill: 'forwards'
}).onfinish = () => {
content.style.height = '0px';
};
}
}
function goToStep(stepNumber) {
// Hide floating button immediately
document.getElementById('floatingNext').classList.remove('visible');
// Close current active step
const currentActive = document.querySelector('swp-step.active');
if (currentActive) {
currentActive.classList.remove('active');
animateStepContent(currentActive, false);
}
// Open new step
const nextStep = document.querySelector(`swp-step[data-step="${stepNumber}"]`);
if (nextStep && !nextStep.classList.contains('disabled')) {
nextStep.classList.add('active');
animateStepContent(nextStep, true);
}
// Update floating button after animation
setTimeout(updateFloatingButton, 350);
}
function updateFloatingButton() {
const activeStep = getActiveStep();
const floatingNext = document.getElementById('floatingNext');
// Don't show on step 4 (last step)
if (activeStep === 4) {
floatingNext.classList.remove('visible');
return;
}
// Check if current step has selection
let hasSelection = false;
if (activeStep === 1) {
hasSelection = state.services.length > 0;
} else if (activeStep === 2) {
hasSelection = true; // Always has default selection
} else if (activeStep === 3) {
hasSelection = state.date && state.time;
}
if (hasSelection) {
floatingNext.classList.add('visible');
} else {
floatingNext.classList.remove('visible');
}
}
function setupFloatingButton() {
document.getElementById('nextBtn').addEventListener('click', () => {
const activeStep = getActiveStep();
if (activeStep && activeStep < 4) {
goToStep(activeStep + 1);
}
});
}
// ==========================================
// SERVICES
// ==========================================
function renderServices() {
const container = document.getElementById('servicesContent');
container.innerHTML = Object.entries(servicesData).map(([category, services]) => `
<swp-services-section>
<swp-section-title>${category}</swp-section-title>
<swp-services-grid>
${services.map(s => `
<swp-service-item data-id="${s.id}" data-name="${s.name}" data-duration="${s.duration}" data-price="${s.price}">
<swp-service-check><i class="ph ph-check"></i></swp-service-check>
<swp-service-details>
<swp-service-name>${s.name}</swp-service-name>
<swp-service-meta>${s.duration} min</swp-service-meta>
</swp-service-details>
<swp-service-price>${s.price} kr.</swp-service-price>
</swp-service-item>
`).join('')}
</swp-services-grid>
</swp-services-section>
`).join('');
container.querySelectorAll('swp-service-item').forEach(item => {
item.addEventListener('click', () => toggleService(item));
});
}
function toggleService(item) {
const id = item.dataset.id;
const idx = state.services.findIndex(s => s.id === id);
if (idx > -1) {
state.services.splice(idx, 1);
item.classList.remove('selected');
} else {
state.services.push({
id,
name: item.dataset.name,
duration: parseInt(item.dataset.duration),
price: parseInt(item.dataset.price)
});
item.classList.add('selected');
}
updateSidebar();
updateSteps();
}
// ==========================================
// EMPLOYEES
// ==========================================
function renderEmployees() {
const container = document.getElementById('employeesContent');
container.innerHTML = `<swp-employee-grid>
${employeesData.map(e => `
<swp-employee-item data-id="${e.id || ''}" class="${e.id === null ? 'no-pref selected' : ''}">
<swp-employee-avatar style="${e.color ? `background:${e.color}` : ''}">
${e.id === null ? '<i class="ph ph-shuffle"></i>' : e.name[0]}
</swp-employee-avatar>
<swp-employee-info>
<swp-employee-name>${e.name}</swp-employee-name>
<swp-employee-role>${e.role}</swp-employee-role>
</swp-employee-info>
</swp-employee-item>
`).join('')}
</swp-employee-grid>`;
container.querySelectorAll('swp-employee-item').forEach(item => {
item.addEventListener('click', () => {
container.querySelectorAll('swp-employee-item').forEach(i => i.classList.remove('selected'));
item.classList.add('selected');
state.employee = item.dataset.id || null;
updateSidebar();
updateSteps();
});
});
}
// ==========================================
// CALENDAR
// ==========================================
let currentMonth = new Date();
function renderCalendar() {
const container = document.getElementById('datetimeContent');
const monthNames = ['Januar', 'Februar', 'Marts', 'April', 'Maj', 'Juni', 'Juli', 'August', 'September', 'Oktober', 'November', 'December'];
container.innerHTML = `
<swp-datetime-layout>
<swp-calendar>
<swp-calendar-header>
<swp-calendar-month>${monthNames[currentMonth.getMonth()]} ${currentMonth.getFullYear()}</swp-calendar-month>
<swp-calendar-nav>
<button id="prevMonth"><i class="ph ph-caret-left"></i></button>
<button id="nextMonth"><i class="ph ph-caret-right"></i></button>
</swp-calendar-nav>
</swp-calendar-header>
<swp-calendar-grid>
<swp-weekday>Ma</swp-weekday>
<swp-weekday>Ti</swp-weekday>
<swp-weekday>On</swp-weekday>
<swp-weekday>To</swp-weekday>
<swp-weekday>Fr</swp-weekday>
<swp-weekday></swp-weekday>
<swp-weekday></swp-weekday>
${generateCalendarDays()}
</swp-calendar-grid>
</swp-calendar>
<swp-time-section>
<swp-section-title>Ledige tider</swp-section-title>
<swp-time-grid id="timeGrid"></swp-time-grid>
</swp-time-section>
</swp-datetime-layout>
`;
document.getElementById('prevMonth').addEventListener('click', () => {
currentMonth.setMonth(currentMonth.getMonth() - 1);
renderCalendar();
});
document.getElementById('nextMonth').addEventListener('click', () => {
currentMonth.setMonth(currentMonth.getMonth() + 1);
renderCalendar();
});
container.querySelectorAll('swp-day:not(.disabled)').forEach(day => {
day.addEventListener('click', () => selectDate(day));
});
renderTimeSlots();
}
function generateCalendarDays() {
const firstDay = new Date(currentMonth.getFullYear(), currentMonth.getMonth(), 1);
const lastDay = new Date(currentMonth.getFullYear(), currentMonth.getMonth() + 1, 0);
const today = new Date();
let html = '';
let startDay = firstDay.getDay() - 1;
if (startDay < 0) startDay = 6;
for (let i = 0; i < startDay; i++) {
const d = new Date(firstDay.getTime() - (startDay - i) * 86400000);
html += `<swp-day class="other-month disabled">${d.getDate()}</swp-day>`;
}
for (let i = 1; i <= lastDay.getDate(); i++) {
const date = new Date(currentMonth.getFullYear(), currentMonth.getMonth(), i);
const dateStr = date.toISOString().split('T')[0];
const isToday = date.toDateString() === today.toDateString();
const isPast = date < today && !isToday;
const isSelected = state.date === dateStr;
html += `<swp-day data-date="${dateStr}" class="${isToday ? 'today' : ''} ${isPast ? 'disabled' : ''} ${isSelected ? 'selected' : ''}">${i}</swp-day>`;
}
return html;
}
function selectDate(day) {
document.querySelectorAll('swp-day.selected').forEach(d => d.classList.remove('selected'));
day.classList.add('selected');
state.date = day.dataset.date;
renderTimeSlots();
updateSidebar();
updateSteps();
}
function renderTimeSlots() {
const grid = document.getElementById('timeGrid');
if (!grid) return;
const times = ['09:00', '09:30', '10:00', '10:30', '11:00', '11:30', '12:00', '13:00', '13:30', '14:00', '14:30', '15:00', '15:30', '16:00', '16:30'];
const taken = ['10:30', '14:00'];
grid.innerHTML = times.map(t => `
<swp-time-slot data-time="${t}" class="${taken.includes(t) ? 'disabled' : ''} ${state.time === t ? 'selected' : ''}">${t}</swp-time-slot>
`).join('');
grid.querySelectorAll('swp-time-slot:not(.disabled)').forEach(slot => {
slot.addEventListener('click', () => {
grid.querySelectorAll('swp-time-slot').forEach(s => s.classList.remove('selected'));
slot.classList.add('selected');
state.time = slot.dataset.time;
updateSidebar();
updateSteps();
});
});
}
// ==========================================
// STEPS NAVIGATION
// ==========================================
function setupStepHeaders() {
document.querySelectorAll('swp-step-header').forEach(header => {
header.addEventListener('click', () => {
const step = header.closest('swp-step');
if (step.classList.contains('disabled')) return;
const isActive = step.classList.contains('active');
const stepNumber = parseInt(step.dataset.step);
// Close current active step
const currentActive = document.querySelector('swp-step.active');
if (currentActive && currentActive !== step) {
currentActive.classList.remove('active');
animateStepContent(currentActive, false);
}
// Toggle clicked step
if (!isActive) {
step.classList.add('active');
animateStepContent(step, true);
} else {
step.classList.remove('active');
animateStepContent(step, false);
}
// Update floating button
setTimeout(updateFloatingButton, 350);
});
});
}
function initFirstStep() {
const step1 = document.querySelector('swp-step[data-step="1"]');
const content = step1.querySelector('swp-step-content');
content.style.height = 'auto';
}
function updateSteps() {
const step1 = document.querySelector('swp-step[data-step="1"]');
const step2 = document.querySelector('swp-step[data-step="2"]');
const step3 = document.querySelector('swp-step[data-step="3"]');
const step4 = document.querySelector('swp-step[data-step="4"]');
// Step 1 completion
if (state.services.length > 0) {
step1.classList.add('completed', 'has-selection');
document.getElementById('step1Summary').textContent = state.services.map(s => s.name).join(', ');
step2.classList.remove('disabled');
} else {
step1.classList.remove('completed', 'has-selection');
document.getElementById('step1Summary').textContent = 'Vælg hvad du vil have lavet';
step2.classList.add('disabled');
}
// Step 2 - always has selection (default "Ingen præference")
const empName = state.employee ? employeesData.find(e => e.id === state.employee)?.name : 'Ingen præference';
step2.classList.add('completed', 'has-selection');
document.getElementById('step2Summary').textContent = empName;
if (state.services.length > 0) {
step3.classList.remove('disabled');
}
// Step 3 completion
if (state.date && state.time) {
step3.classList.add('completed', 'has-selection');
const date = new Date(state.date);
const options = { weekday: 'short', day: 'numeric', month: 'short' };
document.getElementById('step3Summary').textContent = `${date.toLocaleDateString('da-DK', options)} kl. ${state.time}`;
step4.classList.remove('disabled');
} else {
step3.classList.remove('completed', 'has-selection');
document.getElementById('step3Summary').textContent = 'Find en ledig tid';
}
// Step 4
updateFormSummary();
updateBookButton();
updateFloatingButton();
}
// ==========================================
// FORM
// ==========================================
function setupFormListeners() {
['firstName', 'lastName', 'phone', 'email', 'notes'].forEach(id => {
const el = document.getElementById(id);
if (el) {
el.addEventListener('input', () => {
state.customer[id] = el.value;
updateFormSummary();
updateBookButton();
});
}
});
}
function updateFormSummary() {
const step4 = document.querySelector('swp-step[data-step="4"]');
const { firstName, lastName, phone } = state.customer;
if (firstName && phone) {
step4.classList.add('completed');
document.getElementById('step4Summary').textContent = `${firstName} ${lastName || ''} · ${phone}`;
} else {
step4.classList.remove('completed');
document.getElementById('step4Summary').textContent = 'Kontaktinformation';
}
}
// ==========================================
// SIDEBAR
// ==========================================
function updateSidebar() {
const cartEmpty = document.getElementById('cartEmpty');
const cartItems = document.getElementById('cartItems');
const cartDetails = document.getElementById('cartDetails');
const cartTotal = document.getElementById('cartTotal');
if (state.services.length === 0) {
cartEmpty.style.display = 'block';
cartItems.style.display = 'none';
cartDetails.style.display = 'none';
cartTotal.style.display = 'none';
} else {
cartEmpty.style.display = 'none';
cartItems.style.display = 'flex';
cartDetails.style.display = 'flex';
cartTotal.style.display = 'flex';
cartItems.innerHTML = state.services.map(s => `
<swp-cart-item>
<swp-cart-item-info>
<swp-cart-item-name>${s.name}</swp-cart-item-name>
<swp-cart-item-duration>${s.duration} min</swp-cart-item-duration>
</swp-cart-item-info>
<swp-cart-item-price>${s.price} kr.</swp-cart-item-price>
</swp-cart-item>
`).join('');
const total = state.services.reduce((sum, s) => sum + s.price, 0);
document.getElementById('totalPrice').textContent = `${total} kr.`;
// Update details
const empName = state.employee ? employeesData.find(e => e.id === state.employee)?.name : 'Ingen præference';
document.querySelector('#detailEmployee strong').textContent = empName;
if (state.date) {
const date = new Date(state.date);
const options = { weekday: 'long', day: 'numeric', month: 'long' };
document.querySelector('#detailDate strong').textContent = date.toLocaleDateString('da-DK', options);
} else {
document.querySelector('#detailDate strong').textContent = 'Ikke valgt';
}
document.querySelector('#detailTime strong').textContent = state.time || 'Ikke valgt';
}
}
function updateBookButton() {
const btn = document.getElementById('bookBtn');
const { firstName, phone } = state.customer;
const canBook = state.services.length > 0 && state.date && state.time && firstName && phone;
btn.disabled = !canBook;
}
// ==========================================
// BOOKING
// ==========================================
document.getElementById('bookBtn').addEventListener('click', () => {
document.getElementById('successOverlay').classList.add('active');
});
document.getElementById('successOverlay').addEventListener('click', (e) => {
if (e.target.id === 'successOverlay') {
location.reload();
}
});
// Init
init();
</script>
</body>
</html>