2026-01-01 19:57:05 +01:00
|
|
|
<!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: #333333;
|
|
|
|
|
--color-text-secondary: #666666;
|
|
|
|
|
--color-text-muted: #999999;
|
|
|
|
|
--color-green: #43a047;
|
|
|
|
|
--color-red: #e53935;
|
|
|
|
|
--font-family: 'Poppins', -apple-system, sans-serif;
|
|
|
|
|
--font-mono: 'JetBrains Mono', monospace;
|
|
|
|
|
--anim-duration: 250ms;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
* { 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;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* ==========================================
|
|
|
|
|
PAGE LAYOUT
|
|
|
|
|
========================================== */
|
|
|
|
|
.booking-page {
|
|
|
|
|
display: grid;
|
|
|
|
|
grid-template-columns: 1fr 360px;
|
|
|
|
|
max-width: 1100px;
|
|
|
|
|
margin: 0 auto;
|
|
|
|
|
min-height: 100vh;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@media (max-width: 900px) {
|
|
|
|
|
.booking-page {
|
|
|
|
|
grid-template-columns: 1fr;
|
|
|
|
|
}
|
|
|
|
|
.sidebar {
|
|
|
|
|
display: none;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* ==========================================
|
|
|
|
|
MAIN CONTENT
|
|
|
|
|
========================================== */
|
|
|
|
|
.main-content {
|
|
|
|
|
background: var(--color-surface);
|
|
|
|
|
padding: 40px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.salon-header {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 16px;
|
|
|
|
|
margin-bottom: 36px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.salon-logo {
|
|
|
|
|
width: 52px;
|
|
|
|
|
height: 52px;
|
|
|
|
|
background: linear-gradient(135deg, var(--color-teal) 0%, #00695c 100%);
|
|
|
|
|
border-radius: 12px;
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
font-size: 16px;
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
color: white;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.salon-name {
|
|
|
|
|
font-size: 18px;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.salon-address {
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
color: var(--color-text-secondary);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* ==========================================
|
|
|
|
|
STEPS
|
|
|
|
|
========================================== */
|
|
|
|
|
.steps {
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
gap: 12px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.step {
|
|
|
|
|
background: var(--color-background);
|
|
|
|
|
border-radius: 12px;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.step.disabled {
|
|
|
|
|
opacity: 0.5;
|
|
|
|
|
pointer-events: none;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.step-header {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 14px;
|
|
|
|
|
padding: 18px 20px;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
user-select: none;
|
|
|
|
|
transition: background 150ms ease;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.step:not(.disabled) .step-header:hover {
|
|
|
|
|
background: rgba(0,0,0,0.02);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.step-icon {
|
|
|
|
|
font-size: 22px;
|
|
|
|
|
color: var(--color-text-muted);
|
|
|
|
|
transition: color var(--anim-duration) ease;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.step.active .step-icon,
|
|
|
|
|
.step.completed .step-icon {
|
|
|
|
|
color: var(--color-teal);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.step-info {
|
|
|
|
|
flex: 1;
|
|
|
|
|
min-width: 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.step-title {
|
|
|
|
|
font-size: 15px;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
margin-bottom: 2px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.step-summary {
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
color: var(--color-text-secondary);
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
text-overflow: ellipsis;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.step.completed .step-summary {
|
|
|
|
|
color: var(--color-teal);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.step-chevron {
|
|
|
|
|
font-size: 20px;
|
|
|
|
|
color: var(--color-text-muted);
|
|
|
|
|
transition: transform var(--anim-duration) ease;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.step.active .step-chevron {
|
|
|
|
|
transform: rotate(180deg);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.step-body {
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
height: 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.step-content {
|
|
|
|
|
padding: 4px 20px 24px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* ==========================================
|
|
|
|
|
SERVICES
|
|
|
|
|
========================================== */
|
|
|
|
|
.category {
|
|
|
|
|
margin-bottom: 20px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.category:last-child {
|
|
|
|
|
margin-bottom: 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.category-title {
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
color: var(--color-text-muted);
|
|
|
|
|
text-transform: uppercase;
|
|
|
|
|
letter-spacing: 0.5px;
|
|
|
|
|
margin-bottom: 10px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.service-list {
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
gap: 8px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.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;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.service-item:hover {
|
|
|
|
|
border-color: var(--color-border);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.service-item.selected {
|
|
|
|
|
border-color: var(--color-teal);
|
|
|
|
|
background: var(--color-teal-light);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.service-check {
|
|
|
|
|
width: 22px;
|
|
|
|
|
height: 22px;
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
border: 2px solid var(--color-border);
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
color: white;
|
|
|
|
|
transition: all 150ms ease;
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.service-item.selected .service-check {
|
|
|
|
|
background: var(--color-teal);
|
|
|
|
|
border-color: var(--color-teal);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.service-check i {
|
|
|
|
|
opacity: 0;
|
|
|
|
|
transition: opacity 150ms ease;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.service-item.selected .service-check i {
|
|
|
|
|
opacity: 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.service-info {
|
|
|
|
|
flex: 1;
|
|
|
|
|
min-width: 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.service-name {
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.service-duration {
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
color: var(--color-text-secondary);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.service-price {
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
font-family: var(--font-mono);
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* ==========================================
|
|
|
|
|
EMPLOYEES
|
|
|
|
|
========================================== */
|
|
|
|
|
.employee-grid {
|
|
|
|
|
display: grid;
|
|
|
|
|
grid-template-columns: repeat(2, 1fr);
|
|
|
|
|
gap: 12px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@media (max-width: 480px) {
|
|
|
|
|
.employee-grid {
|
|
|
|
|
grid-template-columns: 1fr;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.employee-card {
|
|
|
|
|
background: var(--color-surface);
|
|
|
|
|
border: 2px solid transparent;
|
|
|
|
|
border-radius: 12px;
|
|
|
|
|
padding: 20px 16px;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
text-align: center;
|
|
|
|
|
transition: all 150ms ease;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.employee-card:hover {
|
|
|
|
|
border-color: var(--color-border);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.employee-card.selected {
|
|
|
|
|
border-color: var(--color-teal);
|
|
|
|
|
background: var(--color-teal-light);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.employee-photo {
|
|
|
|
|
width: 80px;
|
|
|
|
|
height: 80px;
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
margin: 0 auto 12px;
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
font-size: 24px;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
color: white;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.employee-photo.neutral {
|
|
|
|
|
background: var(--color-border);
|
|
|
|
|
color: var(--color-text-secondary);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.employee-photo.neutral i {
|
|
|
|
|
font-size: 32px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.employee-name {
|
|
|
|
|
font-size: 15px;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
margin-bottom: 2px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.employee-role {
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
color: var(--color-text-secondary);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.employee-price {
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
font-family: var(--font-mono);
|
|
|
|
|
margin-top: 10px;
|
|
|
|
|
color: var(--color-text-secondary);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.employee-price.discount {
|
|
|
|
|
color: var(--color-green);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* ==========================================
|
|
|
|
|
CALENDAR & TIME
|
|
|
|
|
========================================== */
|
|
|
|
|
.datetime-grid {
|
|
|
|
|
display: grid;
|
|
|
|
|
grid-template-columns: 1fr 1fr;
|
|
|
|
|
gap: 20px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@media (max-width: 560px) {
|
|
|
|
|
.datetime-grid {
|
|
|
|
|
grid-template-columns: 1fr;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.calendar {
|
|
|
|
|
background: var(--color-surface);
|
|
|
|
|
border-radius: 10px;
|
|
|
|
|
padding: 16px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.calendar-header {
|
|
|
|
|
display: flex;
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
align-items: center;
|
|
|
|
|
margin-bottom: 14px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.calendar-month {
|
|
|
|
|
font-size: 15px;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.calendar-nav {
|
|
|
|
|
display: flex;
|
|
|
|
|
gap: 4px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.calendar-nav button {
|
|
|
|
|
width: 30px;
|
|
|
|
|
height: 30px;
|
|
|
|
|
border: none;
|
|
|
|
|
background: var(--color-background);
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
color: var(--color-text-secondary);
|
|
|
|
|
transition: all 150ms ease;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.calendar-nav button:hover {
|
|
|
|
|
background: var(--color-teal);
|
|
|
|
|
color: white;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.calendar-grid {
|
|
|
|
|
display: grid;
|
|
|
|
|
grid-template-columns: repeat(7, 1fr);
|
|
|
|
|
gap: 2px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.weekday {
|
|
|
|
|
text-align: center;
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
color: var(--color-text-muted);
|
|
|
|
|
padding: 6px 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.day {
|
|
|
|
|
aspect-ratio: 1;
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
transition: all 150ms ease;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.day:hover:not(.disabled):not(.selected) {
|
|
|
|
|
background: var(--color-surface);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.day.today {
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
color: var(--color-teal);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.day.selected {
|
|
|
|
|
background: var(--color-teal);
|
|
|
|
|
color: white;
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.day.disabled {
|
|
|
|
|
color: var(--color-border);
|
|
|
|
|
cursor: default;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.time-section-title {
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
color: var(--color-text-muted);
|
|
|
|
|
text-transform: uppercase;
|
|
|
|
|
letter-spacing: 0.5px;
|
|
|
|
|
margin-bottom: 10px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.time-grid {
|
|
|
|
|
display: grid;
|
|
|
|
|
grid-template-columns: repeat(3, 1fr);
|
|
|
|
|
gap: 6px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.time-slot {
|
|
|
|
|
padding: 10px 8px;
|
|
|
|
|
text-align: center;
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
font-family: var(--font-mono);
|
|
|
|
|
background: var(--color-surface);
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
transition: all 150ms ease;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.time-slot:hover:not(.disabled):not(.selected) {
|
|
|
|
|
background: var(--color-teal-light);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.time-slot.selected {
|
|
|
|
|
background: var(--color-teal);
|
|
|
|
|
color: white;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.time-slot.disabled {
|
|
|
|
|
color: var(--color-text-muted);
|
|
|
|
|
cursor: default;
|
|
|
|
|
text-decoration: line-through;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-02 21:19:10 +01:00
|
|
|
/* AI Recommended Slots */
|
|
|
|
|
.time-slot.recommended {
|
|
|
|
|
position: relative;
|
|
|
|
|
border: 2px solid var(--color-green);
|
|
|
|
|
background: color-mix(in srgb, var(--color-green) 8%, white);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.time-slot.recommended:hover:not(.disabled):not(.selected) {
|
|
|
|
|
background: color-mix(in srgb, var(--color-green) 15%, white);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.time-slot.recommended.selected {
|
|
|
|
|
background: var(--color-green);
|
|
|
|
|
border-color: var(--color-green);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.ai-badge {
|
|
|
|
|
position: absolute;
|
|
|
|
|
top: -8px;
|
|
|
|
|
right: -8px;
|
|
|
|
|
background: var(--color-green);
|
|
|
|
|
color: white;
|
|
|
|
|
font-size: 10px;
|
|
|
|
|
padding: 2px 5px;
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 2px;
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
font-family: var(--font-family);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.ai-badge i {
|
|
|
|
|
font-size: 10px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.ai-info {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 8px;
|
|
|
|
|
padding: 10px 14px;
|
|
|
|
|
background: color-mix(in srgb, var(--color-green) 10%, white);
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
margin-bottom: 12px;
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
color: var(--color-green);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.ai-info i {
|
|
|
|
|
font-size: 16px;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-02 07:39:53 +01:00
|
|
|
/* ==========================================
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-01 19:57:05 +01:00
|
|
|
/* ==========================================
|
|
|
|
|
FORM
|
|
|
|
|
========================================== */
|
|
|
|
|
.form-grid {
|
|
|
|
|
display: grid;
|
|
|
|
|
grid-template-columns: 1fr 1fr;
|
|
|
|
|
gap: 16px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@media (max-width: 480px) {
|
|
|
|
|
.form-grid {
|
|
|
|
|
grid-template-columns: 1fr;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.form-field {
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
gap: 6px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.form-field.full-width {
|
|
|
|
|
grid-column: 1 / -1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.form-label {
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
color: var(--color-text);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.form-label .optional {
|
|
|
|
|
font-weight: 400;
|
|
|
|
|
color: var(--color-text-muted);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.form-input {
|
|
|
|
|
width: 100%;
|
|
|
|
|
padding: 10px 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: 6px;
|
|
|
|
|
transition: border-color 150ms ease, box-shadow 150ms ease;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.form-input:focus {
|
|
|
|
|
outline: none;
|
|
|
|
|
border-color: var(--color-teal);
|
|
|
|
|
box-shadow: 0 0 0 3px rgba(0, 137, 123, 0.15);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.form-input::placeholder {
|
|
|
|
|
color: var(--color-text-muted);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
textarea.form-input {
|
|
|
|
|
resize: vertical;
|
|
|
|
|
min-height: 80px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* ==========================================
|
|
|
|
|
SIDEBAR
|
|
|
|
|
========================================== */
|
|
|
|
|
.sidebar {
|
|
|
|
|
background: var(--color-background);
|
|
|
|
|
border-left: 1px solid var(--color-border);
|
|
|
|
|
padding: 40px 28px;
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.sidebar-title {
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
color: var(--color-text-secondary);
|
|
|
|
|
text-transform: uppercase;
|
|
|
|
|
letter-spacing: 0.5px;
|
|
|
|
|
margin-bottom: 20px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.cart-empty {
|
|
|
|
|
text-align: center;
|
|
|
|
|
padding: 40px 20px;
|
|
|
|
|
color: var(--color-text-muted);
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.cart-empty i {
|
|
|
|
|
font-size: 48px;
|
|
|
|
|
margin-bottom: 12px;
|
|
|
|
|
display: block;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.cart-items {
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
gap: 10px;
|
|
|
|
|
margin-bottom: 20px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.cart-item {
|
|
|
|
|
display: flex;
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
align-items: flex-start;
|
|
|
|
|
padding: 12px 14px;
|
|
|
|
|
background: var(--color-surface);
|
|
|
|
|
border-radius: 10px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.cart-item-name {
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
margin-bottom: 2px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.cart-item-duration {
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
color: var(--color-text-secondary);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.cart-item-price {
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
font-family: var(--font-mono);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.cart-details {
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
gap: 10px;
|
|
|
|
|
padding: 16px 0;
|
|
|
|
|
border-top: 1px solid var(--color-border);
|
|
|
|
|
margin-bottom: 20px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.cart-detail {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 10px;
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.cart-detail i {
|
|
|
|
|
font-size: 18px;
|
|
|
|
|
color: var(--color-teal);
|
|
|
|
|
width: 20px;
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.cart-detail-label {
|
|
|
|
|
color: var(--color-text-secondary);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.cart-detail-value {
|
|
|
|
|
color: var(--color-text);
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.cart-total {
|
|
|
|
|
display: flex;
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
align-items: center;
|
|
|
|
|
padding: 18px;
|
|
|
|
|
background: var(--color-teal-light);
|
|
|
|
|
border-radius: 12px;
|
|
|
|
|
margin-bottom: 20px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.cart-total-label {
|
|
|
|
|
font-size: 15px;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.cart-total-price {
|
|
|
|
|
font-size: 22px;
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
font-family: var(--font-mono);
|
|
|
|
|
color: var(--color-teal);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.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: background 150ms ease;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.book-btn:hover:not(:disabled) {
|
|
|
|
|
background: #00695c;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.book-btn:disabled {
|
|
|
|
|
background: var(--color-border);
|
|
|
|
|
cursor: not-allowed;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.book-btn i {
|
|
|
|
|
font-size: 20px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* ==========================================
|
|
|
|
|
FLOATING BUTTON
|
|
|
|
|
========================================== */
|
|
|
|
|
.floating-btn {
|
|
|
|
|
position: fixed;
|
|
|
|
|
bottom: 24px;
|
|
|
|
|
left: 50%;
|
|
|
|
|
transform: translateX(-50%) translateY(80px);
|
|
|
|
|
opacity: 0;
|
|
|
|
|
pointer-events: none;
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 10px;
|
|
|
|
|
padding: 14px 28px;
|
|
|
|
|
background: var(--color-teal);
|
|
|
|
|
color: white;
|
|
|
|
|
font-size: 15px;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
font-family: var(--font-family);
|
|
|
|
|
border: none;
|
|
|
|
|
border-radius: 12px;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
box-shadow: 0 4px 20px rgba(0, 137, 123, 0.35);
|
|
|
|
|
transition: transform var(--anim-duration) ease, opacity var(--anim-duration) ease, background 150ms ease;
|
|
|
|
|
z-index: 100;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.floating-btn.visible {
|
|
|
|
|
transform: translateX(-50%) translateY(0);
|
|
|
|
|
opacity: 1;
|
|
|
|
|
pointer-events: auto;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.floating-btn:hover {
|
|
|
|
|
background: #00695c;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.floating-btn i {
|
|
|
|
|
font-size: 18px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* ==========================================
|
|
|
|
|
MOBILE RESPONSIVE
|
|
|
|
|
========================================== */
|
|
|
|
|
@media (max-width: 600px) {
|
|
|
|
|
.main-content {
|
|
|
|
|
padding: 20px 16px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.salon-header {
|
|
|
|
|
margin-bottom: 24px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.step-header {
|
|
|
|
|
padding: 16px 14px;
|
|
|
|
|
gap: 12px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.step-content {
|
|
|
|
|
padding: 4px 14px 20px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.service-item {
|
|
|
|
|
padding: 12px 14px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.calendar {
|
|
|
|
|
padding: 14px 12px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.employee-item {
|
|
|
|
|
padding: 10px 12px;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* ==========================================
|
|
|
|
|
SUCCESS OVERLAY
|
|
|
|
|
========================================== */
|
|
|
|
|
.success-overlay {
|
|
|
|
|
position: fixed;
|
|
|
|
|
inset: 0;
|
|
|
|
|
background: rgba(0,0,0,0.5);
|
|
|
|
|
display: none;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
z-index: 200;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.success-overlay.visible {
|
|
|
|
|
display: flex;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.success-modal {
|
|
|
|
|
background: var(--color-surface);
|
|
|
|
|
border-radius: 16px;
|
|
|
|
|
padding: 40px;
|
|
|
|
|
text-align: center;
|
|
|
|
|
max-width: 360px;
|
|
|
|
|
margin: 20px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.success-icon {
|
|
|
|
|
width: 72px;
|
|
|
|
|
height: 72px;
|
|
|
|
|
background: rgba(67, 160, 71, 0.12);
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
margin: 0 auto 20px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.success-icon i {
|
|
|
|
|
font-size: 36px;
|
|
|
|
|
color: var(--color-green);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.success-modal h2 {
|
|
|
|
|
font-size: 20px;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
margin-bottom: 8px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.success-modal p {
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
color: var(--color-text-secondary);
|
|
|
|
|
line-height: 1.5;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-02 07:39:53 +01:00
|
|
|
.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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-01 19:57:05 +01:00
|
|
|
/* ==========================================
|
|
|
|
|
LANDING VIEW
|
|
|
|
|
========================================== */
|
|
|
|
|
.landing-view {
|
|
|
|
|
min-height: 100vh;
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
background: var(--color-background);
|
|
|
|
|
padding: 20px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.landing-card {
|
|
|
|
|
background: var(--color-surface);
|
|
|
|
|
border-radius: 16px;
|
|
|
|
|
padding: 48px 40px;
|
|
|
|
|
max-width: 400px;
|
|
|
|
|
width: 100%;
|
|
|
|
|
text-align: center;
|
|
|
|
|
box-shadow: 0 4px 24px rgba(0,0,0,0.08);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.landing-card .salon-logo {
|
|
|
|
|
width: 64px;
|
|
|
|
|
height: 64px;
|
|
|
|
|
margin: 0 auto 16px;
|
|
|
|
|
font-size: 20px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.landing-card .salon-name {
|
|
|
|
|
font-size: 20px;
|
|
|
|
|
margin-bottom: 4px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.landing-card .salon-address {
|
|
|
|
|
margin-bottom: 32px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.landing-title {
|
|
|
|
|
font-size: 18px;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
margin-bottom: 24px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.landing-form {
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
gap: 12px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.landing-divider {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 16px;
|
|
|
|
|
color: var(--color-text-muted);
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
margin: 24px 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.landing-divider::before,
|
|
|
|
|
.landing-divider::after {
|
|
|
|
|
content: '';
|
|
|
|
|
flex: 1;
|
|
|
|
|
height: 1px;
|
|
|
|
|
background: var(--color-border);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.btn-primary {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
gap: 8px;
|
|
|
|
|
width: 100%;
|
|
|
|
|
padding: 14px 24px;
|
|
|
|
|
background: var(--color-teal);
|
|
|
|
|
color: white;
|
|
|
|
|
font-size: 15px;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
font-family: var(--font-family);
|
|
|
|
|
border: none;
|
|
|
|
|
border-radius: 10px;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
transition: background 150ms ease;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.btn-primary:hover {
|
|
|
|
|
background: #00695c;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.btn-secondary {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
gap: 8px;
|
|
|
|
|
width: 100%;
|
|
|
|
|
padding: 14px 24px;
|
|
|
|
|
background: var(--color-surface);
|
|
|
|
|
color: var(--color-text);
|
|
|
|
|
font-size: 15px;
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
font-family: var(--font-family);
|
|
|
|
|
border: 1px solid var(--color-border);
|
|
|
|
|
border-radius: 10px;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
transition: all 150ms ease;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.btn-secondary:hover {
|
|
|
|
|
background: var(--color-background);
|
|
|
|
|
border-color: var(--color-text-muted);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.landing-step {
|
|
|
|
|
width: 100%;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.landing-phone-display {
|
|
|
|
|
font-size: 18px;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
font-family: var(--font-mono);
|
|
|
|
|
color: var(--color-teal);
|
|
|
|
|
margin-bottom: 20px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.landing-back {
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 6px;
|
|
|
|
|
margin-top: 20px;
|
|
|
|
|
padding: 8px 12px;
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
font-family: var(--font-family);
|
|
|
|
|
color: var(--color-text-secondary);
|
|
|
|
|
background: transparent;
|
|
|
|
|
border: none;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
transition: color 150ms ease;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.landing-back:hover {
|
|
|
|
|
color: var(--color-text);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.landing-back i {
|
|
|
|
|
font-size: 16px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* ==========================================
|
|
|
|
|
MY PAGE VIEW
|
|
|
|
|
========================================== */
|
|
|
|
|
.mypage-view {
|
|
|
|
|
display: none;
|
|
|
|
|
min-height: 100vh;
|
|
|
|
|
background: var(--color-background);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.mypage-header {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
padding: 16px 24px;
|
|
|
|
|
background: var(--color-surface);
|
|
|
|
|
border-bottom: 1px solid var(--color-border);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.mypage-header-brand {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 12px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.mypage-header .salon-logo {
|
|
|
|
|
width: 40px;
|
|
|
|
|
height: 40px;
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.mypage-header .salon-name {
|
|
|
|
|
font-size: 16px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.btn-logout {
|
|
|
|
|
padding: 8px 16px;
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
color: var(--color-text-secondary);
|
|
|
|
|
background: transparent;
|
|
|
|
|
border: 1px solid var(--color-border);
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
transition: all 150ms ease;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.btn-logout:hover {
|
|
|
|
|
background: var(--color-background);
|
|
|
|
|
color: var(--color-text);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.mypage-content {
|
|
|
|
|
max-width: 600px;
|
|
|
|
|
margin: 0 auto;
|
|
|
|
|
padding: 32px 24px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.mypage-greeting {
|
|
|
|
|
font-size: 22px;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
margin-bottom: 24px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.mypage-section {
|
|
|
|
|
background: var(--color-surface);
|
|
|
|
|
border-radius: 12px;
|
|
|
|
|
margin-bottom: 20px;
|
|
|
|
|
box-shadow: 0 1px 3px rgba(0,0,0,0.06);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.mypage-section-header {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 10px;
|
|
|
|
|
padding: 16px 20px;
|
|
|
|
|
border-bottom: 1px solid var(--color-border);
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
color: var(--color-text);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.mypage-section-header i {
|
|
|
|
|
font-size: 20px;
|
|
|
|
|
color: var(--color-teal);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.mypage-section-content {
|
|
|
|
|
padding: 20px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.book-cta {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
padding: 20px;
|
|
|
|
|
background: linear-gradient(135deg, var(--color-teal) 0%, #00695c 100%);
|
|
|
|
|
color: white;
|
|
|
|
|
border-radius: 12px;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
transition: transform 150ms ease, box-shadow 150ms ease;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.book-cta:hover {
|
|
|
|
|
transform: translateY(-2px);
|
|
|
|
|
box-shadow: 0 4px 12px rgba(0, 137, 123, 0.3);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.book-cta-text {
|
|
|
|
|
font-size: 16px;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.book-cta-sub {
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
opacity: 0.9;
|
|
|
|
|
margin-top: 2px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.book-cta i {
|
|
|
|
|
font-size: 24px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.upcoming-booking {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: flex-start;
|
|
|
|
|
gap: 16px;
|
|
|
|
|
padding: 16px 0;
|
|
|
|
|
border-bottom: 1px solid var(--color-border);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.upcoming-booking:last-child {
|
|
|
|
|
border-bottom: none;
|
|
|
|
|
padding-bottom: 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.upcoming-booking:first-child {
|
|
|
|
|
padding-top: 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.upcoming-icon {
|
|
|
|
|
width: 44px;
|
|
|
|
|
height: 44px;
|
|
|
|
|
background: var(--color-teal-light);
|
|
|
|
|
border-radius: 10px;
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.upcoming-icon i {
|
|
|
|
|
font-size: 22px;
|
|
|
|
|
color: var(--color-teal);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.upcoming-info {
|
|
|
|
|
flex: 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.upcoming-service {
|
|
|
|
|
font-size: 15px;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
margin-bottom: 4px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.upcoming-datetime {
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
color: var(--color-text-secondary);
|
|
|
|
|
margin-bottom: 2px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.upcoming-details {
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
color: var(--color-text-muted);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.btn-cancel {
|
|
|
|
|
padding: 8px 14px;
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
color: var(--color-red);
|
|
|
|
|
background: transparent;
|
|
|
|
|
border: 1px solid var(--color-red);
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
transition: all 150ms ease;
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.btn-cancel:hover {
|
|
|
|
|
background: rgba(229, 57, 53, 0.08);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.mypage-form-grid {
|
|
|
|
|
display: grid;
|
|
|
|
|
grid-template-columns: 1fr 1fr;
|
|
|
|
|
gap: 16px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@media (max-width: 480px) {
|
|
|
|
|
.mypage-form-grid {
|
|
|
|
|
grid-template-columns: 1fr;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.mypage-form-field {
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
gap: 6px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.mypage-form-field.full-width {
|
|
|
|
|
grid-column: 1 / -1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.password-row {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 12px;
|
|
|
|
|
grid-column: 1 / -1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.password-row .mypage-form-field {
|
|
|
|
|
flex: 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.btn-change-password {
|
|
|
|
|
padding: 10px 16px;
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
color: var(--color-teal);
|
|
|
|
|
background: transparent;
|
|
|
|
|
border: 1px solid var(--color-teal);
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
transition: all 150ms ease;
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
margin-top: 20px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.btn-change-password:hover {
|
|
|
|
|
background: var(--color-teal-light);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.mypage-form-actions {
|
|
|
|
|
display: flex;
|
|
|
|
|
justify-content: flex-end;
|
|
|
|
|
margin-top: 20px;
|
|
|
|
|
padding-top: 16px;
|
|
|
|
|
border-top: 1px solid var(--color-border);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.btn-save {
|
|
|
|
|
padding: 10px 20px;
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
color: white;
|
|
|
|
|
background: var(--color-teal);
|
|
|
|
|
border: none;
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
transition: background 150ms ease;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.btn-save:hover {
|
|
|
|
|
background: #00695c;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.receipt-item {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
padding: 12px 0;
|
|
|
|
|
border-bottom: 1px solid var(--color-border);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.receipt-item:last-child {
|
|
|
|
|
border-bottom: none;
|
|
|
|
|
padding-bottom: 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.receipt-item:first-child {
|
|
|
|
|
padding-top: 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.receipt-date {
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
color: var(--color-text-muted);
|
|
|
|
|
width: 100px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.receipt-service {
|
|
|
|
|
flex: 1;
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.receipt-amount {
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
font-family: var(--font-mono);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.empty-state {
|
|
|
|
|
text-align: center;
|
|
|
|
|
padding: 24px;
|
|
|
|
|
color: var(--color-text-muted);
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.empty-state i {
|
|
|
|
|
font-size: 32px;
|
|
|
|
|
margin-bottom: 8px;
|
|
|
|
|
display: block;
|
|
|
|
|
}
|
|
|
|
|
</style>
|
|
|
|
|
</head>
|
|
|
|
|
<body>
|
|
|
|
|
|
|
|
|
|
<!-- Landing View -->
|
|
|
|
|
<div class="landing-view" id="landingView">
|
|
|
|
|
<div class="landing-card">
|
|
|
|
|
<div class="salon-logo">KK</div>
|
|
|
|
|
<div class="salon-name">KARINA KNUDSEN®</div>
|
2026-01-02 07:39:53 +01:00
|
|
|
<div class="salon-address">Amager Strandvej 22f, 2300 Kbh S</div>
|
2026-01-01 19:57:05 +01:00
|
|
|
|
|
|
|
|
<!-- Step 1: Phone -->
|
|
|
|
|
<div class="landing-step" id="landingStep1">
|
|
|
|
|
<div class="landing-title">Book tid hos os</div>
|
|
|
|
|
<div class="landing-form">
|
|
|
|
|
<input type="tel" class="form-input" id="phoneInput" placeholder="Telefonnummer">
|
|
|
|
|
<button class="btn-primary" id="phoneNextBtn">
|
|
|
|
|
Fortsæt <i class="ph ph-arrow-right"></i>
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Step 2: PIN -->
|
|
|
|
|
<div class="landing-step" id="landingStep2" style="display: none;">
|
|
|
|
|
<div class="landing-title">Indtast PIN-kode</div>
|
|
|
|
|
<p class="landing-phone-display" id="phoneDisplay"></p>
|
|
|
|
|
<div class="landing-form">
|
|
|
|
|
<input type="password" class="form-input" id="pinInput" placeholder="PIN-kode" inputmode="numeric" maxlength="4">
|
|
|
|
|
<button class="btn-primary" id="loginBtn">
|
|
|
|
|
Log ind <i class="ph ph-arrow-right"></i>
|
|
|
|
|
</button>
|
|
|
|
|
<div class="landing-divider">eller</div>
|
|
|
|
|
<button class="btn-secondary" id="continueBtn">
|
|
|
|
|
Fortsæt uden login <i class="ph ph-arrow-right"></i>
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
<button class="landing-back" id="backToPhoneBtn">
|
|
|
|
|
<i class="ph ph-arrow-left"></i> Ret telefonnummer
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- My Page View -->
|
|
|
|
|
<div class="mypage-view" id="mypageView">
|
|
|
|
|
<div class="mypage-header">
|
|
|
|
|
<div class="mypage-header-brand">
|
|
|
|
|
<div class="salon-logo">KK</div>
|
|
|
|
|
<div class="salon-name">KARINA KNUDSEN®</div>
|
|
|
|
|
</div>
|
|
|
|
|
<button class="btn-logout" id="logoutBtn">Log ud</button>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="mypage-content">
|
|
|
|
|
<div class="mypage-greeting" id="mypageGreeting">Hej, Maria!</div>
|
|
|
|
|
|
|
|
|
|
<!-- Book CTA -->
|
|
|
|
|
<div class="book-cta" id="bookCtaBtn">
|
|
|
|
|
<div>
|
|
|
|
|
<div class="book-cta-text">Book ny tid</div>
|
|
|
|
|
<div class="book-cta-sub">Vælg ydelse, tidspunkt og medarbejder</div>
|
|
|
|
|
</div>
|
|
|
|
|
<i class="ph ph-arrow-right"></i>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Upcoming Bookings -->
|
|
|
|
|
<div class="mypage-section" style="margin-top: 24px;">
|
|
|
|
|
<div class="mypage-section-header">
|
|
|
|
|
<i class="ph ph-calendar-check"></i> Kommende tider
|
|
|
|
|
</div>
|
|
|
|
|
<div class="mypage-section-content" id="upcomingBookings">
|
|
|
|
|
<div class="upcoming-booking">
|
|
|
|
|
<div class="upcoming-icon">
|
|
|
|
|
<i class="ph ph-scissors"></i>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="upcoming-info">
|
|
|
|
|
<div class="upcoming-service">Dameklip</div>
|
|
|
|
|
<div class="upcoming-datetime">Fredag 24. januar kl. 14:00</div>
|
|
|
|
|
<div class="upcoming-details">Camilla · 725 kr.</div>
|
|
|
|
|
</div>
|
|
|
|
|
<button class="btn-cancel">Aflys</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Account Settings -->
|
|
|
|
|
<div class="mypage-section">
|
|
|
|
|
<div class="mypage-section-header">
|
|
|
|
|
<i class="ph ph-user-circle"></i> Mine oplysninger
|
|
|
|
|
</div>
|
|
|
|
|
<div class="mypage-section-content">
|
|
|
|
|
<div class="mypage-form-grid">
|
|
|
|
|
<div class="mypage-form-field">
|
|
|
|
|
<label class="form-label">Fornavn</label>
|
|
|
|
|
<input type="text" class="form-input" id="mypageFirstName" value="Maria">
|
|
|
|
|
</div>
|
|
|
|
|
<div class="mypage-form-field">
|
|
|
|
|
<label class="form-label">Efternavn</label>
|
|
|
|
|
<input type="text" class="form-input" id="mypageLastName" value="Hansen">
|
|
|
|
|
</div>
|
|
|
|
|
<div class="mypage-form-field">
|
|
|
|
|
<label class="form-label">Email</label>
|
|
|
|
|
<input type="email" class="form-input" id="mypageEmail" value="maria@email.dk">
|
|
|
|
|
</div>
|
|
|
|
|
<div class="mypage-form-field">
|
|
|
|
|
<label class="form-label">Telefon</label>
|
|
|
|
|
<input type="tel" class="form-input" id="mypagePhone" value="12 34 56 78">
|
|
|
|
|
</div>
|
|
|
|
|
<div class="password-row">
|
|
|
|
|
<div class="mypage-form-field">
|
|
|
|
|
<label class="form-label">Adgangskode</label>
|
|
|
|
|
<input type="password" class="form-input" value="••••••••" disabled>
|
|
|
|
|
</div>
|
|
|
|
|
<button class="btn-change-password">Skift</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="mypage-form-actions">
|
|
|
|
|
<button class="btn-save">Gem ændringer</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Receipts -->
|
|
|
|
|
<div class="mypage-section">
|
|
|
|
|
<div class="mypage-section-header">
|
|
|
|
|
<i class="ph ph-receipt"></i> Kvitteringer
|
|
|
|
|
</div>
|
|
|
|
|
<div class="mypage-section-content" id="receipts">
|
|
|
|
|
<div class="receipt-item">
|
|
|
|
|
<div class="receipt-date">12. dec 2024</div>
|
|
|
|
|
<div class="receipt-service">Dameklip + Olaplex</div>
|
|
|
|
|
<div class="receipt-amount">1.275 kr.</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="receipt-item">
|
|
|
|
|
<div class="receipt-date">15. nov 2024</div>
|
|
|
|
|
<div class="receipt-service">Dameklip</div>
|
|
|
|
|
<div class="receipt-amount">725 kr.</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="receipt-item">
|
|
|
|
|
<div class="receipt-date">18. okt 2024</div>
|
|
|
|
|
<div class="receipt-service">Farve + Klip</div>
|
|
|
|
|
<div class="receipt-amount">1.510 kr.</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Booking View -->
|
|
|
|
|
<div class="booking-page" id="bookingView" style="display: none;">
|
|
|
|
|
<!-- Main Content -->
|
|
|
|
|
<div class="main-content">
|
|
|
|
|
<div class="salon-header">
|
|
|
|
|
<div class="salon-logo">KK</div>
|
|
|
|
|
<div>
|
|
|
|
|
<div class="salon-name">KARINA KNUDSEN®</div>
|
2026-01-02 07:39:53 +01:00
|
|
|
<div class="salon-address">Amager Strandvej 22f, 2300 Kbh S</div>
|
2026-01-01 19:57:05 +01:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="steps">
|
|
|
|
|
<!-- Step 1: Services -->
|
|
|
|
|
<div class="step active" data-step="1">
|
|
|
|
|
<div class="step-header">
|
|
|
|
|
<i class="ph ph-scissors step-icon"></i>
|
|
|
|
|
<div class="step-info">
|
|
|
|
|
<div class="step-title">Vælg ydelse</div>
|
|
|
|
|
<div class="step-summary" data-summary>Hvad vil du have lavet?</div>
|
|
|
|
|
</div>
|
|
|
|
|
<i class="ph ph-caret-down step-chevron"></i>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="step-body">
|
|
|
|
|
<div class="step-content" id="servicesContainer"></div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Step 2: Add-ons -->
|
|
|
|
|
<div class="step disabled" data-step="2">
|
|
|
|
|
<div class="step-header">
|
|
|
|
|
<i class="ph ph-plus-circle step-icon"></i>
|
|
|
|
|
<div class="step-info">
|
|
|
|
|
<div class="step-title">Tilvalg</div>
|
|
|
|
|
<div class="step-summary" data-summary>Valgfrit</div>
|
|
|
|
|
</div>
|
|
|
|
|
<i class="ph ph-caret-down step-chevron"></i>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="step-body">
|
|
|
|
|
<div class="step-content" id="addonsContainer"></div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Step 3: Employee -->
|
|
|
|
|
<div class="step disabled" data-step="3">
|
|
|
|
|
<div class="step-header">
|
|
|
|
|
<i class="ph ph-user-circle step-icon"></i>
|
|
|
|
|
<div class="step-info">
|
|
|
|
|
<div class="step-title">Vælg medarbejder</div>
|
|
|
|
|
<div class="step-summary" data-summary>Valgfrit</div>
|
|
|
|
|
</div>
|
|
|
|
|
<i class="ph ph-caret-down step-chevron"></i>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="step-body">
|
|
|
|
|
<div class="step-content" id="employeesContainer"></div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Step 4: Date & Time -->
|
|
|
|
|
<div class="step disabled" data-step="4">
|
|
|
|
|
<div class="step-header">
|
|
|
|
|
<i class="ph ph-calendar step-icon"></i>
|
|
|
|
|
<div class="step-info">
|
|
|
|
|
<div class="step-title">Vælg dato og tid</div>
|
|
|
|
|
<div class="step-summary" data-summary>Find en ledig tid</div>
|
|
|
|
|
</div>
|
|
|
|
|
<i class="ph ph-caret-down step-chevron"></i>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="step-body">
|
|
|
|
|
<div class="step-content" id="datetimeContainer"></div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Step 5: Contact -->
|
|
|
|
|
<div class="step disabled" data-step="5">
|
|
|
|
|
<div class="step-header">
|
|
|
|
|
<i class="ph ph-identification-card step-icon"></i>
|
|
|
|
|
<div class="step-info">
|
|
|
|
|
<div class="step-title">Dine oplysninger</div>
|
|
|
|
|
<div class="step-summary" data-summary>Kontaktinformation</div>
|
|
|
|
|
</div>
|
|
|
|
|
<i class="ph ph-caret-down step-chevron"></i>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="step-body">
|
|
|
|
|
<div class="step-content">
|
|
|
|
|
<div class="form-grid">
|
|
|
|
|
<div class="form-field">
|
|
|
|
|
<label class="form-label" for="firstName">Fornavn</label>
|
|
|
|
|
<input type="text" class="form-input" id="firstName" placeholder="Dit fornavn">
|
|
|
|
|
</div>
|
|
|
|
|
<div class="form-field">
|
|
|
|
|
<label class="form-label" for="lastName">Efternavn</label>
|
|
|
|
|
<input type="text" class="form-input" id="lastName" placeholder="Dit efternavn">
|
|
|
|
|
</div>
|
|
|
|
|
<div class="form-field">
|
|
|
|
|
<label class="form-label" for="phone">Telefon</label>
|
|
|
|
|
<input type="tel" class="form-input" id="phone" placeholder="12 34 56 78">
|
|
|
|
|
</div>
|
|
|
|
|
<div class="form-field">
|
|
|
|
|
<label class="form-label" for="email">Email</label>
|
|
|
|
|
<input type="email" class="form-input" id="email" placeholder="din@email.dk">
|
|
|
|
|
</div>
|
|
|
|
|
<div class="form-field full-width">
|
|
|
|
|
<label class="form-label" for="notes">Noter <span class="optional">(valgfrit)</span></label>
|
|
|
|
|
<textarea class="form-input" id="notes" placeholder="Særlige ønsker..."></textarea>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Sidebar -->
|
|
|
|
|
<div class="sidebar">
|
|
|
|
|
<div class="sidebar-title">Din booking</div>
|
|
|
|
|
|
|
|
|
|
<div class="cart-empty" id="cartEmpty">
|
|
|
|
|
<i class="ph ph-shopping-cart"></i>
|
|
|
|
|
Vælg en ydelse for at starte
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="cart-items" id="cartItems" style="display: none;"></div>
|
|
|
|
|
|
|
|
|
|
<div class="cart-details" id="cartDetails" style="display: none;">
|
|
|
|
|
<div class="cart-detail">
|
|
|
|
|
<i class="ph ph-user-circle"></i>
|
|
|
|
|
<span class="cart-detail-label">Medarbejder:</span>
|
|
|
|
|
<span class="cart-detail-value" id="detailEmployee">Ikke valgt</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="cart-detail">
|
|
|
|
|
<i class="ph ph-calendar"></i>
|
|
|
|
|
<span class="cart-detail-label">Dato:</span>
|
|
|
|
|
<span class="cart-detail-value" id="detailDate">Ikke valgt</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="cart-detail">
|
|
|
|
|
<i class="ph ph-clock"></i>
|
|
|
|
|
<span class="cart-detail-label">Tid:</span>
|
|
|
|
|
<span class="cart-detail-value" id="detailTime">Ikke valgt</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="cart-total" id="cartTotal" style="display: none;">
|
|
|
|
|
<span class="cart-total-label">Total</span>
|
|
|
|
|
<span class="cart-total-price" id="totalPrice">0 kr.</span>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<button class="book-btn" id="bookBtn" disabled>
|
|
|
|
|
<i class="ph ph-check-circle"></i>
|
|
|
|
|
Book tid
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Floating "Videre" button -->
|
|
|
|
|
<button class="floating-btn" id="floatingBtn">
|
|
|
|
|
Videre <i class="ph ph-arrow-right"></i>
|
|
|
|
|
</button>
|
|
|
|
|
|
|
|
|
|
<!-- Success overlay -->
|
|
|
|
|
<div class="success-overlay" id="successOverlay">
|
|
|
|
|
<div class="success-modal">
|
|
|
|
|
<div class="success-icon">
|
|
|
|
|
<i class="ph ph-check"></i>
|
|
|
|
|
</div>
|
|
|
|
|
<h2>Booking bekræftet!</h2>
|
|
|
|
|
<p>Du modtager en bekræftelse på email og SMS. Vi glæder os til at se dig!</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-01-02 07:39:53 +01:00
|
|
|
<!-- 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>
|
|
|
|
|
|
2026-01-01 19:57:05 +01:00
|
|
|
<script>
|
|
|
|
|
// ==========================================
|
|
|
|
|
// CONFIG
|
|
|
|
|
// ==========================================
|
|
|
|
|
const ANIM_DURATION = 250;
|
|
|
|
|
|
|
|
|
|
// ==========================================
|
|
|
|
|
// STATE
|
|
|
|
|
// ==========================================
|
|
|
|
|
const state = {
|
|
|
|
|
services: [],
|
|
|
|
|
addons: [],
|
|
|
|
|
employee: null,
|
|
|
|
|
date: null,
|
|
|
|
|
time: null,
|
|
|
|
|
customer: { firstName: '', lastName: '', phone: '', email: '', notes: '' }
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// ==========================================
|
|
|
|
|
// 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 addonsData = [
|
|
|
|
|
{ id: "a1", name: "Hårkur", duration: 15, price: 150 },
|
|
|
|
|
{ id: "a2", name: "Hovedbundsmassage", duration: 10, price: 95 },
|
|
|
|
|
{ id: "a3", name: "Styling produkt", duration: 0, price: 175 },
|
|
|
|
|
{ id: "a4", name: "Varmebeskyttelse", duration: 5, price: 85 }
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
const employeesData = [
|
|
|
|
|
{ id: null, name: "Ingen præference", role: "Første ledige", color: null, priceModifier: 0 },
|
|
|
|
|
{ id: "EMP001", name: "Camilla", role: "Master Stylist", color: "#9c27b0", priceModifier: 0 },
|
|
|
|
|
{ id: "EMP002", name: "Isabella", role: "Master Stylist", color: "#e91e63", priceModifier: 0 },
|
|
|
|
|
{ id: "EMP003", name: "Alexander", role: "Frisør", color: "#3f51b5", priceModifier: -50 },
|
|
|
|
|
{ id: "EMP004", name: "Viktor", role: "Junior Stylist", color: "#009688", priceModifier: -100 }
|
|
|
|
|
];
|
|
|
|
|
|
2026-01-02 21:19:10 +01:00
|
|
|
// ==========================================
|
|
|
|
|
// EKSISTERENDE BOOKINGER (Mock data til AI-optimering)
|
|
|
|
|
// ==========================================
|
|
|
|
|
const existingBookings = {
|
|
|
|
|
'EMP001': {
|
|
|
|
|
'2026-01-06': [
|
|
|
|
|
{ start: '10:00', end: '11:00', service: 'Dameklip' },
|
|
|
|
|
{ start: '13:30', end: '14:30', service: 'Herreklip' }
|
|
|
|
|
],
|
|
|
|
|
'2026-01-07': [
|
|
|
|
|
{ start: '09:00', end: '10:30', service: 'Bundfarve' },
|
|
|
|
|
{ start: '11:00', end: '12:00', service: 'Dameklip' },
|
|
|
|
|
{ start: '14:00', end: '15:00', service: 'Dameklip' }
|
|
|
|
|
]
|
|
|
|
|
},
|
|
|
|
|
'EMP002': {
|
|
|
|
|
'2026-01-06': [
|
|
|
|
|
{ start: '09:00', end: '10:00', service: 'Herreklip' },
|
|
|
|
|
{ start: '11:00', end: '12:00', service: 'Dameklip' },
|
|
|
|
|
{ start: '15:00', end: '16:30', service: 'Striber' }
|
|
|
|
|
],
|
|
|
|
|
'2026-01-07': [
|
|
|
|
|
{ start: '10:00', end: '11:00', service: 'Dameklip' },
|
|
|
|
|
{ start: '13:00', end: '14:00', service: 'Herreklip' }
|
|
|
|
|
]
|
|
|
|
|
},
|
|
|
|
|
'EMP003': {
|
|
|
|
|
'2026-01-06': [
|
|
|
|
|
{ start: '08:30', end: '09:30', service: 'Herreklip' },
|
|
|
|
|
{ start: '12:00', end: '13:00', service: 'Dameklip' }
|
|
|
|
|
]
|
|
|
|
|
},
|
|
|
|
|
'EMP004': {
|
|
|
|
|
'2026-01-06': [
|
|
|
|
|
{ start: '09:00', end: '10:00', service: 'Herreklip' },
|
|
|
|
|
{ start: '14:00', end: '15:30', service: 'Bundfarve' }
|
|
|
|
|
]
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const SALON_OPEN = '08:00';
|
|
|
|
|
const SALON_CLOSE = '17:00';
|
|
|
|
|
|
|
|
|
|
// ==========================================
|
|
|
|
|
// AI SLOT OPTIMERING
|
|
|
|
|
// ==========================================
|
|
|
|
|
function timeToMinutes(time) {
|
|
|
|
|
const [h, m] = time.split(':').map(Number);
|
|
|
|
|
return h * 60 + m;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function minutesToTime(mins) {
|
|
|
|
|
const h = Math.floor(mins / 60);
|
|
|
|
|
const m = mins % 60;
|
|
|
|
|
return `${h.toString().padStart(2, '0')}:${m.toString().padStart(2, '0')}`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getBookingsForSlot(date, employeeId) {
|
|
|
|
|
if (!employeeId) {
|
|
|
|
|
let bestEmployee = 'EMP001';
|
|
|
|
|
let minBookings = Infinity;
|
|
|
|
|
for (const empId of Object.keys(existingBookings)) {
|
|
|
|
|
const bookings = existingBookings[empId]?.[date] || [];
|
|
|
|
|
if (bookings.length < minBookings) {
|
|
|
|
|
minBookings = bookings.length;
|
|
|
|
|
bestEmployee = empId;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return existingBookings[bestEmployee]?.[date] || [];
|
|
|
|
|
}
|
|
|
|
|
return existingBookings[employeeId]?.[date] || [];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function calculateOptimalSlots(serviceDuration, date, employeeId) {
|
|
|
|
|
const bookings = getBookingsForSlot(date, employeeId);
|
|
|
|
|
const openMins = timeToMinutes(SALON_OPEN);
|
|
|
|
|
const closeMins = timeToMinutes(SALON_CLOSE);
|
|
|
|
|
|
|
|
|
|
const slots = [];
|
|
|
|
|
for (let mins = openMins; mins <= closeMins - serviceDuration; mins += 30) {
|
|
|
|
|
const slotStart = mins;
|
|
|
|
|
const slotEnd = mins + serviceDuration;
|
|
|
|
|
const time = minutesToTime(mins);
|
|
|
|
|
|
|
|
|
|
let isTaken = false;
|
|
|
|
|
for (const booking of bookings) {
|
|
|
|
|
const bookStart = timeToMinutes(booking.start);
|
|
|
|
|
const bookEnd = timeToMinutes(booking.end);
|
|
|
|
|
if (slotStart < bookEnd && slotEnd > bookStart) {
|
|
|
|
|
isTaken = true;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let score = 0;
|
|
|
|
|
if (!isTaken) {
|
|
|
|
|
if (slotStart === openMins) score += 3;
|
|
|
|
|
for (const booking of bookings) {
|
|
|
|
|
if (slotEnd === timeToMinutes(booking.start)) { score += 3; break; }
|
|
|
|
|
}
|
|
|
|
|
for (const booking of bookings) {
|
|
|
|
|
if (slotStart === timeToMinutes(booking.end)) { score += 2; break; }
|
|
|
|
|
}
|
|
|
|
|
for (const booking of bookings) {
|
|
|
|
|
const gap = timeToMinutes(booking.start) - slotEnd;
|
|
|
|
|
if (gap > 0 && gap < 30) { score -= 2; break; }
|
|
|
|
|
}
|
|
|
|
|
for (const booking of bookings) {
|
|
|
|
|
const gap = slotStart - timeToMinutes(booking.end);
|
|
|
|
|
if (gap > 0 && gap < 30) { score -= 2; break; }
|
|
|
|
|
}
|
|
|
|
|
if (slotEnd >= closeMins - 60) score += 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
slots.push({ time, taken: isTaken, score, recommended: false });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const availableSlots = slots.filter(s => !s.taken);
|
|
|
|
|
availableSlots.sort((a, b) => b.score - a.score);
|
|
|
|
|
const topSlots = availableSlots.slice(0, 3);
|
|
|
|
|
for (const slot of topSlots) {
|
|
|
|
|
if (slot.score > 0) slot.recommended = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return slots;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-01 19:57:05 +01:00
|
|
|
// ==========================================
|
|
|
|
|
// STEP ANIMATION
|
|
|
|
|
// ==========================================
|
|
|
|
|
function animateStep(stepEl, open) {
|
|
|
|
|
const body = stepEl.querySelector('.step-body');
|
|
|
|
|
|
|
|
|
|
if (open) {
|
|
|
|
|
body.style.height = 'auto';
|
|
|
|
|
const targetHeight = body.scrollHeight;
|
|
|
|
|
body.style.height = '0px';
|
|
|
|
|
body.offsetHeight; // Force reflow
|
|
|
|
|
|
|
|
|
|
const anim = body.animate(
|
|
|
|
|
[{ height: '0px' }, { height: targetHeight + 'px' }],
|
|
|
|
|
{ duration: ANIM_DURATION, easing: 'ease-out' }
|
|
|
|
|
);
|
|
|
|
|
anim.onfinish = () => { body.style.height = 'auto'; };
|
|
|
|
|
} else {
|
|
|
|
|
const currentHeight = body.scrollHeight;
|
|
|
|
|
const anim = body.animate(
|
|
|
|
|
[{ height: currentHeight + 'px' }, { height: '0px' }],
|
|
|
|
|
{ duration: ANIM_DURATION, easing: 'ease-out' }
|
|
|
|
|
);
|
|
|
|
|
anim.onfinish = () => { body.style.height = '0px'; };
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ==========================================
|
|
|
|
|
// STEP NAVIGATION
|
|
|
|
|
// ==========================================
|
|
|
|
|
function getActiveStep() {
|
|
|
|
|
const active = document.querySelector('.step.active');
|
|
|
|
|
return active ? parseInt(active.dataset.step) : null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function openStep(stepNum) {
|
|
|
|
|
const targetStep = document.querySelector(`.step[data-step="${stepNum}"]`);
|
|
|
|
|
if (!targetStep || targetStep.classList.contains('disabled')) return;
|
|
|
|
|
|
|
|
|
|
const currentActive = document.querySelector('.step.active');
|
|
|
|
|
if (currentActive) {
|
|
|
|
|
currentActive.classList.remove('active');
|
|
|
|
|
animateStep(currentActive, false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
targetStep.classList.add('active');
|
|
|
|
|
animateStep(targetStep, true);
|
|
|
|
|
updateFloatingBtn();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function toggleStep(stepEl) {
|
|
|
|
|
if (stepEl.classList.contains('disabled')) return;
|
|
|
|
|
|
|
|
|
|
const isActive = stepEl.classList.contains('active');
|
|
|
|
|
const stepNum = parseInt(stepEl.dataset.step);
|
|
|
|
|
|
|
|
|
|
if (isActive) {
|
|
|
|
|
stepEl.classList.remove('active');
|
|
|
|
|
animateStep(stepEl, false);
|
|
|
|
|
updateFloatingBtn();
|
|
|
|
|
} else {
|
|
|
|
|
openStep(stepNum);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ==========================================
|
|
|
|
|
// FLOATING BUTTON
|
|
|
|
|
// ==========================================
|
|
|
|
|
function updateFloatingBtn() {
|
|
|
|
|
const floatingBtn = document.getElementById('floatingBtn');
|
|
|
|
|
const activeStepNum = getActiveStep();
|
|
|
|
|
|
|
|
|
|
// Show floating button on step 1 (when services selected) and step 2 (always)
|
|
|
|
|
let showBtn = false;
|
|
|
|
|
if (activeStepNum === 1) showBtn = state.services.length > 0;
|
|
|
|
|
else if (activeStepNum === 2) showBtn = true;
|
|
|
|
|
|
|
|
|
|
floatingBtn.classList.toggle('visible', showBtn);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function onFloatingBtnClick() {
|
|
|
|
|
const activeStepNum = getActiveStep();
|
|
|
|
|
if (activeStepNum && activeStepNum < 5) {
|
|
|
|
|
openStep(activeStepNum + 1);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ==========================================
|
|
|
|
|
// RENDER: SERVICES
|
|
|
|
|
// ==========================================
|
|
|
|
|
function renderServices() {
|
|
|
|
|
const container = document.getElementById('servicesContainer');
|
|
|
|
|
container.innerHTML = Object.entries(servicesData).map(([category, services]) => `
|
|
|
|
|
<div class="category">
|
|
|
|
|
<div class="category-title">${category}</div>
|
|
|
|
|
<div class="service-list">
|
|
|
|
|
${services.map(s => `
|
|
|
|
|
<div class="service-item" data-id="${s.id}">
|
|
|
|
|
<div class="service-check"><i class="ph ph-check"></i></div>
|
|
|
|
|
<div class="service-info">
|
|
|
|
|
<div class="service-name">${s.name}</div>
|
|
|
|
|
<div class="service-duration">${s.duration} min</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="service-price">${s.price} kr.</div>
|
|
|
|
|
</div>
|
|
|
|
|
`).join('')}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
`).join('');
|
|
|
|
|
|
|
|
|
|
container.querySelectorAll('.service-item').forEach(item => {
|
|
|
|
|
item.addEventListener('click', () => {
|
|
|
|
|
const id = item.dataset.id;
|
|
|
|
|
const service = Object.values(servicesData).flat().find(s => s.id === 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(service);
|
|
|
|
|
item.classList.add('selected');
|
|
|
|
|
}
|
|
|
|
|
updateUI();
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ==========================================
|
|
|
|
|
// RENDER: ADDONS
|
|
|
|
|
// ==========================================
|
|
|
|
|
function renderAddons() {
|
|
|
|
|
const container = document.getElementById('addonsContainer');
|
|
|
|
|
container.innerHTML = `
|
|
|
|
|
<div class="service-list">
|
|
|
|
|
${addonsData.map(a => `
|
|
|
|
|
<div class="service-item" data-id="${a.id}">
|
|
|
|
|
<div class="service-check"><i class="ph ph-check"></i></div>
|
|
|
|
|
<div class="service-info">
|
|
|
|
|
<div class="service-name">${a.name}</div>
|
|
|
|
|
<div class="service-duration">${a.duration > 0 ? a.duration + ' min' : ''}</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="service-price">${a.price} kr.</div>
|
|
|
|
|
</div>
|
|
|
|
|
`).join('')}
|
|
|
|
|
</div>
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
container.querySelectorAll('.service-item').forEach(item => {
|
|
|
|
|
item.addEventListener('click', () => {
|
|
|
|
|
const id = item.dataset.id;
|
|
|
|
|
const addon = addonsData.find(a => a.id === id);
|
|
|
|
|
const idx = state.addons.findIndex(a => a.id === id);
|
|
|
|
|
|
|
|
|
|
if (idx > -1) {
|
|
|
|
|
state.addons.splice(idx, 1);
|
|
|
|
|
item.classList.remove('selected');
|
|
|
|
|
} else {
|
|
|
|
|
state.addons.push(addon);
|
|
|
|
|
item.classList.add('selected');
|
|
|
|
|
}
|
|
|
|
|
updateUI();
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ==========================================
|
|
|
|
|
// UPDATE EMPLOYEE PRICES
|
|
|
|
|
// ==========================================
|
|
|
|
|
function updateEmployeePrices() {
|
|
|
|
|
const servicesTotal = state.services.reduce((sum, s) => sum + s.price, 0);
|
|
|
|
|
const addonsTotal = state.addons.reduce((sum, a) => sum + a.price, 0);
|
|
|
|
|
const baseTotal = servicesTotal + addonsTotal;
|
|
|
|
|
const container = document.getElementById('employeesContainer');
|
|
|
|
|
|
|
|
|
|
container.querySelectorAll('.employee-card').forEach(card => {
|
|
|
|
|
const empId = card.dataset.id || null;
|
|
|
|
|
const emp = employeesData.find(e => (e.id || '') === empId);
|
|
|
|
|
if (!emp || emp.id === null) return;
|
|
|
|
|
|
|
|
|
|
const priceEl = card.querySelector('.employee-price');
|
|
|
|
|
const employeePrice = baseTotal + (emp.priceModifier || 0);
|
|
|
|
|
|
|
|
|
|
if (servicesTotal > 0) {
|
|
|
|
|
if (priceEl) {
|
|
|
|
|
priceEl.textContent = `${employeePrice} kr.`;
|
|
|
|
|
} else {
|
|
|
|
|
const newPriceEl = document.createElement('div');
|
|
|
|
|
newPriceEl.className = `employee-price ${emp.priceModifier < 0 ? 'discount' : ''}`;
|
|
|
|
|
newPriceEl.textContent = `${employeePrice} kr.`;
|
|
|
|
|
card.appendChild(newPriceEl);
|
|
|
|
|
}
|
|
|
|
|
} else if (priceEl) {
|
|
|
|
|
priceEl.remove();
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ==========================================
|
|
|
|
|
// RENDER: EMPLOYEES
|
|
|
|
|
// ==========================================
|
|
|
|
|
function renderEmployees() {
|
|
|
|
|
const container = document.getElementById('employeesContainer');
|
|
|
|
|
const servicesTotal = state.services.reduce((sum, s) => sum + s.price, 0);
|
|
|
|
|
const addonsTotal = state.addons.reduce((sum, a) => sum + a.price, 0);
|
|
|
|
|
const baseTotal = servicesTotal + addonsTotal;
|
|
|
|
|
|
|
|
|
|
container.innerHTML = `
|
|
|
|
|
<div class="employee-grid">
|
|
|
|
|
${employeesData.map(e => {
|
|
|
|
|
const employeePrice = baseTotal + (e.priceModifier || 0);
|
|
|
|
|
const isDiscount = e.priceModifier < 0;
|
|
|
|
|
const isSelected = (e.id === null && state.employee === null) || e.id === state.employee;
|
|
|
|
|
|
|
|
|
|
return `
|
|
|
|
|
<div class="employee-card ${isSelected ? 'selected' : ''}" data-id="${e.id || ''}">
|
|
|
|
|
<div class="employee-photo ${e.id === null ? 'neutral' : ''}" ${e.color ? `style="background:${e.color}"` : ''}>
|
|
|
|
|
${e.id === null ? '<i class="ph ph-shuffle"></i>' : e.name[0]}
|
|
|
|
|
</div>
|
|
|
|
|
<div class="employee-name">${e.name}</div>
|
|
|
|
|
<div class="employee-role">${e.role}</div>
|
|
|
|
|
${e.id !== null && servicesTotal > 0 ? `<div class="employee-price ${isDiscount ? 'discount' : ''}">${employeePrice} kr.</div>` : ''}
|
|
|
|
|
</div>
|
|
|
|
|
`;
|
|
|
|
|
}).join('')}
|
|
|
|
|
</div>
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
container.querySelectorAll('.employee-card').forEach(item => {
|
|
|
|
|
item.addEventListener('click', () => {
|
|
|
|
|
container.querySelectorAll('.employee-card').forEach(i => i.classList.remove('selected'));
|
|
|
|
|
item.classList.add('selected');
|
|
|
|
|
state.employee = item.dataset.id || null;
|
|
|
|
|
updateUI();
|
|
|
|
|
// Auto-advance to step 4
|
|
|
|
|
setTimeout(() => openStep(4), 150);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ==========================================
|
|
|
|
|
// RENDER: CALENDAR
|
|
|
|
|
// ==========================================
|
|
|
|
|
let currentMonth = new Date();
|
|
|
|
|
|
|
|
|
|
function renderCalendar() {
|
|
|
|
|
const container = document.getElementById('datetimeContainer');
|
|
|
|
|
const monthNames = ['Januar', 'Februar', 'Marts', 'April', 'Maj', 'Juni', 'Juli', 'August', 'September', 'Oktober', 'November', 'December'];
|
|
|
|
|
const today = new Date();
|
|
|
|
|
today.setHours(0,0,0,0);
|
|
|
|
|
|
|
|
|
|
const year = currentMonth.getFullYear();
|
|
|
|
|
const month = currentMonth.getMonth();
|
|
|
|
|
const firstDay = new Date(year, month, 1);
|
|
|
|
|
const lastDay = new Date(year, month + 1, 0);
|
|
|
|
|
|
|
|
|
|
let startOffset = firstDay.getDay() - 1;
|
|
|
|
|
if (startOffset < 0) startOffset = 6;
|
|
|
|
|
|
|
|
|
|
let daysHtml = '';
|
|
|
|
|
for (let i = 0; i < startOffset; i++) {
|
|
|
|
|
daysHtml += '<div class="day disabled"></div>';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (let d = 1; d <= lastDay.getDate(); d++) {
|
|
|
|
|
const date = new Date(year, month, d);
|
|
|
|
|
const dateStr = date.toISOString().split('T')[0];
|
|
|
|
|
const isToday = date.getTime() === today.getTime();
|
|
|
|
|
const isPast = date < today;
|
|
|
|
|
const isSelected = state.date === dateStr;
|
|
|
|
|
|
|
|
|
|
let cls = 'day';
|
|
|
|
|
if (isToday) cls += ' today';
|
|
|
|
|
if (isPast && !isToday) cls += ' disabled';
|
|
|
|
|
if (isSelected) cls += ' selected';
|
|
|
|
|
|
|
|
|
|
daysHtml += `<div class="${cls}" data-date="${dateStr}">${d}</div>`;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-02 21:19:10 +01:00
|
|
|
// Beregn total varighed for valgte ydelser
|
|
|
|
|
const serviceDuration = state.services.reduce((sum, s) => sum + s.duration, 0) || 60;
|
|
|
|
|
|
|
|
|
|
// Brug AI-algoritme til at beregne optimale slots
|
|
|
|
|
const selectedDate = state.date || new Date().toISOString().split('T')[0];
|
|
|
|
|
const slots = calculateOptimalSlots(serviceDuration, selectedDate, state.employee);
|
|
|
|
|
|
|
|
|
|
// Filtrer til åbningstider (08:00-17:00)
|
|
|
|
|
const displaySlots = slots.filter(s => {
|
|
|
|
|
const mins = timeToMinutes(s.time);
|
|
|
|
|
return mins >= timeToMinutes('08:00') && mins <= timeToMinutes('16:30');
|
|
|
|
|
});
|
2026-01-01 19:57:05 +01:00
|
|
|
|
|
|
|
|
container.innerHTML = `
|
|
|
|
|
<div class="datetime-grid">
|
|
|
|
|
<div class="calendar">
|
|
|
|
|
<div class="calendar-header">
|
|
|
|
|
<div class="calendar-month">${monthNames[month]} ${year}</div>
|
|
|
|
|
<div class="calendar-nav">
|
|
|
|
|
<button id="prevMonth"><i class="ph ph-caret-left"></i></button>
|
|
|
|
|
<button id="nextMonth"><i class="ph ph-caret-right"></i></button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="calendar-grid">
|
|
|
|
|
<div class="weekday">Ma</div>
|
|
|
|
|
<div class="weekday">Ti</div>
|
|
|
|
|
<div class="weekday">On</div>
|
|
|
|
|
<div class="weekday">To</div>
|
|
|
|
|
<div class="weekday">Fr</div>
|
|
|
|
|
<div class="weekday">Lø</div>
|
|
|
|
|
<div class="weekday">Sø</div>
|
|
|
|
|
${daysHtml}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="time-section">
|
|
|
|
|
<div class="time-section-title">Ledige tider</div>
|
2026-01-02 21:19:10 +01:00
|
|
|
<div class="ai-info">
|
|
|
|
|
<i class="ph ph-sparkle"></i>
|
|
|
|
|
<span>Anbefalede tider passer bedst i vores kalender</span>
|
|
|
|
|
</div>
|
2026-01-01 19:57:05 +01:00
|
|
|
<div class="time-grid">
|
2026-01-02 21:19:10 +01:00
|
|
|
${displaySlots.map(slot => {
|
2026-01-01 19:57:05 +01:00
|
|
|
let cls = 'time-slot';
|
2026-01-02 21:19:10 +01:00
|
|
|
if (slot.taken) cls += ' disabled';
|
|
|
|
|
if (slot.recommended && !slot.taken) cls += ' recommended';
|
|
|
|
|
if (state.time === slot.time) cls += ' selected';
|
|
|
|
|
return `<div class="${cls}" data-time="${slot.time}">
|
|
|
|
|
${slot.time}
|
|
|
|
|
${slot.recommended && !slot.taken ? '<span class="ai-badge"><i class="ph ph-sparkle"></i></span>' : ''}
|
|
|
|
|
</div>`;
|
2026-01-01 19:57:05 +01:00
|
|
|
}).join('')}
|
|
|
|
|
</div>
|
2026-01-02 07:39:53 +01:00
|
|
|
|
|
|
|
|
<!-- 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>
|
2026-01-01 19:57:05 +01:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
document.getElementById('prevMonth').addEventListener('click', () => {
|
|
|
|
|
currentMonth.setMonth(currentMonth.getMonth() - 1);
|
|
|
|
|
renderCalendar();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
document.getElementById('nextMonth').addEventListener('click', () => {
|
|
|
|
|
currentMonth.setMonth(currentMonth.getMonth() + 1);
|
|
|
|
|
renderCalendar();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
container.querySelectorAll('.day:not(.disabled)').forEach(day => {
|
|
|
|
|
day.addEventListener('click', () => {
|
|
|
|
|
container.querySelectorAll('.day').forEach(d => d.classList.remove('selected'));
|
|
|
|
|
day.classList.add('selected');
|
|
|
|
|
state.date = day.dataset.date;
|
|
|
|
|
updateUI();
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
container.querySelectorAll('.time-slot:not(.disabled)').forEach(slot => {
|
|
|
|
|
slot.addEventListener('click', () => {
|
|
|
|
|
container.querySelectorAll('.time-slot').forEach(s => s.classList.remove('selected'));
|
|
|
|
|
slot.classList.add('selected');
|
|
|
|
|
state.time = slot.dataset.time;
|
|
|
|
|
updateUI();
|
|
|
|
|
// Auto-advance to step 5 when both date and time are selected
|
|
|
|
|
if (state.date && state.time) {
|
|
|
|
|
setTimeout(() => openStep(5), 150);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
});
|
2026-01-02 07:39:53 +01:00
|
|
|
|
|
|
|
|
// 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 = '';
|
2026-01-01 19:57:05 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ==========================================
|
|
|
|
|
// UI UPDATE
|
|
|
|
|
// ==========================================
|
|
|
|
|
function updateUI() {
|
|
|
|
|
const step1 = document.querySelector('.step[data-step="1"]');
|
|
|
|
|
const step2 = document.querySelector('.step[data-step="2"]');
|
|
|
|
|
const step3 = document.querySelector('.step[data-step="3"]');
|
|
|
|
|
const step4 = document.querySelector('.step[data-step="4"]');
|
|
|
|
|
const step5 = document.querySelector('.step[data-step="5"]');
|
|
|
|
|
|
|
|
|
|
// Step 1: Services
|
|
|
|
|
if (state.services.length > 0) {
|
|
|
|
|
step1.classList.add('completed');
|
|
|
|
|
step1.querySelector('[data-summary]').textContent = state.services.map(s => s.name).join(', ');
|
|
|
|
|
step2.classList.remove('disabled');
|
|
|
|
|
step3.classList.remove('disabled');
|
|
|
|
|
step4.classList.remove('disabled');
|
|
|
|
|
} else {
|
|
|
|
|
step1.classList.remove('completed');
|
|
|
|
|
step1.querySelector('[data-summary]').textContent = 'Hvad vil du have lavet?';
|
|
|
|
|
step2.classList.add('disabled');
|
|
|
|
|
step3.classList.add('disabled');
|
|
|
|
|
step4.classList.add('disabled');
|
|
|
|
|
step5.classList.add('disabled');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Step 2: Addons
|
|
|
|
|
if (state.addons.length > 0) {
|
|
|
|
|
step2.classList.add('completed');
|
|
|
|
|
step2.querySelector('[data-summary]').textContent = state.addons.map(a => a.name).join(', ');
|
|
|
|
|
} else {
|
|
|
|
|
step2.classList.remove('completed');
|
|
|
|
|
step2.querySelector('[data-summary]').textContent = 'Valgfrit';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Update employee prices based on selected services + addons
|
|
|
|
|
updateEmployeePrices();
|
|
|
|
|
|
|
|
|
|
// Step 3: Employee
|
|
|
|
|
const selectedEmployee = state.employee ? employeesData.find(e => e.id === state.employee) : null;
|
|
|
|
|
const empName = selectedEmployee?.name || 'Ingen præference';
|
|
|
|
|
const empPriceModifier = selectedEmployee?.priceModifier || 0;
|
|
|
|
|
step3.classList.add('completed');
|
|
|
|
|
step3.querySelector('[data-summary]').textContent = empName;
|
|
|
|
|
|
|
|
|
|
// Step 4: Date & Time
|
|
|
|
|
if (state.date && state.time) {
|
|
|
|
|
step4.classList.add('completed');
|
|
|
|
|
const d = new Date(state.date);
|
|
|
|
|
step4.querySelector('[data-summary]').textContent = `${d.toLocaleDateString('da-DK', { weekday: 'short', day: 'numeric', month: 'short' })} kl. ${state.time}`;
|
|
|
|
|
step5.classList.remove('disabled');
|
|
|
|
|
} else {
|
|
|
|
|
step4.classList.remove('completed');
|
|
|
|
|
step4.querySelector('[data-summary]').textContent = 'Find en ledig tid';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Step 5: Contact
|
|
|
|
|
const { firstName, lastName, phone } = state.customer;
|
|
|
|
|
if (firstName && phone) {
|
|
|
|
|
step5.classList.add('completed');
|
|
|
|
|
step5.querySelector('[data-summary]').textContent = `${firstName} ${lastName || ''} · ${phone}`;
|
|
|
|
|
} else {
|
|
|
|
|
step5.classList.remove('completed');
|
|
|
|
|
step5.querySelector('[data-summary]').textContent = 'Kontaktinformation';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Sidebar
|
|
|
|
|
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';
|
|
|
|
|
|
|
|
|
|
let cartHtml = state.services.map(s => `
|
|
|
|
|
<div class="cart-item">
|
|
|
|
|
<div>
|
|
|
|
|
<div class="cart-item-name">${s.name}</div>
|
|
|
|
|
<div class="cart-item-duration">${s.duration} min</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="cart-item-price">${s.price} kr.</div>
|
|
|
|
|
</div>
|
|
|
|
|
`).join('');
|
|
|
|
|
|
|
|
|
|
if (state.addons.length > 0) {
|
|
|
|
|
cartHtml += state.addons.map(a => `
|
|
|
|
|
<div class="cart-item">
|
|
|
|
|
<div>
|
|
|
|
|
<div class="cart-item-name">${a.name}</div>
|
|
|
|
|
<div class="cart-item-duration">${a.duration > 0 ? a.duration + ' min' : 'Tilvalg'}</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="cart-item-price">${a.price} kr.</div>
|
|
|
|
|
</div>
|
|
|
|
|
`).join('');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
cartItems.innerHTML = cartHtml;
|
|
|
|
|
|
|
|
|
|
const servicesTotal = state.services.reduce((sum, s) => sum + s.price, 0);
|
|
|
|
|
const addonsTotal = state.addons.reduce((sum, a) => sum + a.price, 0);
|
|
|
|
|
const total = servicesTotal + addonsTotal + empPriceModifier;
|
|
|
|
|
document.getElementById('totalPrice').textContent = total + ' kr.';
|
|
|
|
|
|
|
|
|
|
document.getElementById('detailEmployee').textContent = empName;
|
|
|
|
|
|
|
|
|
|
if (state.date) {
|
|
|
|
|
const d = new Date(state.date);
|
|
|
|
|
document.getElementById('detailDate').textContent = d.toLocaleDateString('da-DK', { weekday: 'long', day: 'numeric', month: 'long' });
|
|
|
|
|
} else {
|
|
|
|
|
document.getElementById('detailDate').textContent = 'Ikke valgt';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
document.getElementById('detailTime').textContent = state.time || 'Ikke valgt';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Book button
|
|
|
|
|
const canBook = state.services.length > 0 && state.date && state.time && firstName && phone;
|
|
|
|
|
document.getElementById('bookBtn').disabled = !canBook;
|
|
|
|
|
|
|
|
|
|
updateFloatingBtn();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ==========================================
|
|
|
|
|
// FORM LISTENERS
|
|
|
|
|
// ==========================================
|
|
|
|
|
function setupFormListeners() {
|
|
|
|
|
['firstName', 'lastName', 'phone', 'email', 'notes'].forEach(field => {
|
|
|
|
|
const el = document.getElementById(field);
|
|
|
|
|
if (el) {
|
|
|
|
|
el.addEventListener('input', () => {
|
|
|
|
|
state.customer[field] = el.value;
|
|
|
|
|
updateUI();
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ==========================================
|
|
|
|
|
// BOOKING
|
|
|
|
|
// ==========================================
|
|
|
|
|
function onBookClick() {
|
|
|
|
|
const { firstName, phone } = state.customer;
|
|
|
|
|
if (!firstName || !phone) {
|
|
|
|
|
alert('Udfyld venligst fornavn og telefon.');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
document.getElementById('successOverlay').classList.add('visible');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ==========================================
|
|
|
|
|
// VIEW SWITCHING
|
|
|
|
|
// ==========================================
|
|
|
|
|
let isLoggedIn = false;
|
|
|
|
|
let loggedInUser = null;
|
|
|
|
|
let enteredPhone = '';
|
|
|
|
|
|
|
|
|
|
const views = {
|
|
|
|
|
landing: document.getElementById('landingView'),
|
|
|
|
|
mypage: document.getElementById('mypageView'),
|
|
|
|
|
booking: document.getElementById('bookingView')
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
function showView(viewName) {
|
|
|
|
|
Object.entries(views).forEach(([name, el]) => {
|
|
|
|
|
if (name === viewName) {
|
|
|
|
|
el.style.display = name === 'booking' ? 'grid' : (name === 'mypage' ? 'block' : 'flex');
|
|
|
|
|
} else {
|
|
|
|
|
el.style.display = 'none';
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Reset landing to step 1 when showing
|
|
|
|
|
if (viewName === 'landing') {
|
|
|
|
|
showLandingStep(1);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function showLandingStep(step) {
|
|
|
|
|
const step1 = document.getElementById('landingStep1');
|
|
|
|
|
const step2 = document.getElementById('landingStep2');
|
|
|
|
|
|
|
|
|
|
if (step === 1) {
|
|
|
|
|
step1.style.display = 'block';
|
|
|
|
|
step2.style.display = 'none';
|
|
|
|
|
} else {
|
|
|
|
|
step1.style.display = 'none';
|
|
|
|
|
step2.style.display = 'block';
|
|
|
|
|
document.getElementById('pinInput').value = '';
|
|
|
|
|
document.getElementById('pinInput').focus();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function handlePhoneNext() {
|
|
|
|
|
const phone = document.getElementById('phoneInput').value.trim();
|
|
|
|
|
|
|
|
|
|
if (!phone) {
|
|
|
|
|
alert('Indtast telefonnummer');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
enteredPhone = phone;
|
|
|
|
|
document.getElementById('phoneDisplay').textContent = phone;
|
|
|
|
|
showLandingStep(2);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function handleBackToPhone() {
|
|
|
|
|
showLandingStep(1);
|
|
|
|
|
document.getElementById('phoneInput').focus();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function handleLogin() {
|
|
|
|
|
const pin = document.getElementById('pinInput').value.trim();
|
|
|
|
|
|
|
|
|
|
if (!pin) {
|
|
|
|
|
alert('Indtast PIN-kode');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Mock login - in real app, validate phone + PIN against backend
|
|
|
|
|
isLoggedIn = true;
|
|
|
|
|
loggedInUser = {
|
|
|
|
|
firstName: 'Maria',
|
|
|
|
|
lastName: 'Hansen',
|
|
|
|
|
email: 'maria@email.dk',
|
|
|
|
|
phone: enteredPhone
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Update mypage form fields with user data
|
|
|
|
|
document.getElementById('mypageFirstName').value = loggedInUser.firstName;
|
|
|
|
|
document.getElementById('mypageLastName').value = loggedInUser.lastName;
|
|
|
|
|
document.getElementById('mypageEmail').value = loggedInUser.email;
|
|
|
|
|
document.getElementById('mypagePhone').value = loggedInUser.phone;
|
|
|
|
|
|
|
|
|
|
document.getElementById('mypageGreeting').textContent = `Hej, ${loggedInUser.firstName}!`;
|
|
|
|
|
showView('mypage');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function handleLogout() {
|
|
|
|
|
isLoggedIn = false;
|
|
|
|
|
loggedInUser = null;
|
|
|
|
|
enteredPhone = '';
|
|
|
|
|
document.getElementById('phoneInput').value = '';
|
|
|
|
|
document.getElementById('pinInput').value = '';
|
|
|
|
|
showView('landing');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function handleContinueWithoutLogin() {
|
|
|
|
|
// Not logged in, but pre-fill phone in booking
|
|
|
|
|
isLoggedIn = false;
|
|
|
|
|
loggedInUser = null;
|
|
|
|
|
|
|
|
|
|
document.getElementById('phone').value = enteredPhone;
|
|
|
|
|
state.customer.phone = enteredPhone;
|
|
|
|
|
updateUI();
|
|
|
|
|
|
|
|
|
|
showView('booking');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function handleBookFromMyPage() {
|
|
|
|
|
// Pre-fill customer info from logged in user
|
|
|
|
|
if (loggedInUser) {
|
|
|
|
|
document.getElementById('firstName').value = loggedInUser.firstName;
|
|
|
|
|
document.getElementById('lastName').value = loggedInUser.lastName;
|
|
|
|
|
document.getElementById('email').value = loggedInUser.email;
|
|
|
|
|
document.getElementById('phone').value = loggedInUser.phone;
|
|
|
|
|
|
|
|
|
|
state.customer = {
|
|
|
|
|
firstName: loggedInUser.firstName,
|
|
|
|
|
lastName: loggedInUser.lastName,
|
|
|
|
|
email: loggedInUser.email,
|
|
|
|
|
phone: loggedInUser.phone,
|
|
|
|
|
notes: ''
|
|
|
|
|
};
|
|
|
|
|
updateUI();
|
|
|
|
|
}
|
|
|
|
|
showView('booking');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ==========================================
|
|
|
|
|
// INIT
|
|
|
|
|
// ==========================================
|
|
|
|
|
function init() {
|
|
|
|
|
renderServices();
|
|
|
|
|
renderAddons();
|
|
|
|
|
renderEmployees();
|
|
|
|
|
renderCalendar();
|
|
|
|
|
|
|
|
|
|
// Open step 1
|
|
|
|
|
document.querySelector('.step[data-step="1"] .step-body').style.height = 'auto';
|
|
|
|
|
|
|
|
|
|
// Step headers
|
|
|
|
|
document.querySelectorAll('.step-header').forEach(header => {
|
|
|
|
|
header.addEventListener('click', () => toggleStep(header.closest('.step')));
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Floating button
|
|
|
|
|
document.getElementById('floatingBtn').addEventListener('click', onFloatingBtnClick);
|
|
|
|
|
|
|
|
|
|
// Book button
|
|
|
|
|
document.getElementById('bookBtn').addEventListener('click', onBookClick);
|
|
|
|
|
|
|
|
|
|
// Form
|
|
|
|
|
setupFormListeners();
|
|
|
|
|
|
|
|
|
|
// Success overlay close
|
|
|
|
|
document.getElementById('successOverlay').addEventListener('click', (e) => {
|
|
|
|
|
if (e.target.id === 'successOverlay') location.reload();
|
|
|
|
|
});
|
|
|
|
|
|
2026-01-02 07:39:53 +01:00
|
|
|
// Waitlist success overlay close
|
|
|
|
|
document.getElementById('waitlistSuccessClose').addEventListener('click', closeWaitlistSuccess);
|
|
|
|
|
document.getElementById('waitlistSuccessOverlay').addEventListener('click', (e) => {
|
|
|
|
|
if (e.target.id === 'waitlistSuccessOverlay') closeWaitlistSuccess();
|
|
|
|
|
});
|
|
|
|
|
|
2026-01-01 19:57:05 +01:00
|
|
|
// View switching - Landing
|
|
|
|
|
document.getElementById('phoneNextBtn').addEventListener('click', handlePhoneNext);
|
|
|
|
|
document.getElementById('backToPhoneBtn').addEventListener('click', handleBackToPhone);
|
|
|
|
|
document.getElementById('loginBtn').addEventListener('click', handleLogin);
|
|
|
|
|
document.getElementById('continueBtn').addEventListener('click', handleContinueWithoutLogin);
|
|
|
|
|
|
|
|
|
|
// Enter key support
|
|
|
|
|
document.getElementById('phoneInput').addEventListener('keydown', (e) => {
|
|
|
|
|
if (e.key === 'Enter') handlePhoneNext();
|
|
|
|
|
});
|
|
|
|
|
document.getElementById('pinInput').addEventListener('keydown', (e) => {
|
|
|
|
|
if (e.key === 'Enter') handleLogin();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// View switching - MyPage
|
|
|
|
|
document.getElementById('logoutBtn').addEventListener('click', handleLogout);
|
|
|
|
|
document.getElementById('bookCtaBtn').addEventListener('click', handleBookFromMyPage);
|
|
|
|
|
|
|
|
|
|
// Start with landing view
|
|
|
|
|
showView('landing');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
init();
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
</body>
|
|
|
|
|
</html>
|