1565 lines
44 KiB
HTML
1565 lines
44 KiB
HTML
|
|
<!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>Hovedgaden 123, København</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>Lø</swp-weekday>
|
||
|
|
<swp-weekday>Sø</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>
|