Calendar/wwwroot/poc-booking-v2.html
Janus C. H. Knudsen f73133b51c Adds waitlist feature for booking system
Introduces new waitlist functionality to allow customers to register for unavailable time slots

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

Updates salon address in multiple files
2026-01-02 07:39:53 +01:00

2796 lines
78 KiB
HTML

<!DOCTYPE html>
<html lang="da">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Book tid - KARINA KNUDSEN®</title>
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<style>
@font-face { font-family: 'Poppins'; src: url('fonts/Poppins-Regular.woff') format('woff'); font-weight: 400; }
@font-face { font-family: 'Poppins'; src: url('fonts/Poppins-Medium.woff') format('woff'); font-weight: 500; }
@font-face { font-family: 'Poppins'; src: url('fonts/Poppins-SemiBold.woff') format('woff'); font-weight: 600; }
@font-face { font-family: 'Poppins'; src: url('fonts/Poppins-Bold.woff') format('woff'); font-weight: 700; }
</style>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@phosphor-icons/web@2.1.2/src/regular/style.css">
<style>
:root {
--color-teal: #00897b;
--color-teal-light: #e0f2f1;
--color-surface: #ffffff;
--color-background: #f5f5f5;
--color-border: #e0e0e0;
--color-text: #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;
}
/* ==========================================
WAITLIST
========================================== */
.waitlist-trigger {
display: flex;
align-items: center;
gap: 8px;
margin-top: 20px;
padding: 12px 16px;
width: 100%;
background: transparent;
border: 1px dashed var(--color-border);
border-radius: 8px;
font-size: 13px;
font-family: var(--font-family);
color: var(--color-text-secondary);
cursor: pointer;
transition: all 150ms ease;
}
.waitlist-trigger:hover {
border-color: var(--color-teal);
color: var(--color-teal);
background: var(--color-teal-light);
}
.waitlist-trigger i {
font-size: 18px;
}
.waitlist-section {
overflow: hidden;
height: 0;
}
.waitlist-content {
padding-top: 20px;
}
.waitlist-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 16px;
}
.waitlist-title {
font-size: 14px;
font-weight: 600;
color: var(--color-text);
}
.waitlist-close {
display: flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
background: transparent;
border: none;
border-radius: 6px;
color: var(--color-text-secondary);
cursor: pointer;
transition: all 150ms ease;
}
.waitlist-close:hover {
background: var(--color-background);
color: var(--color-text);
}
.waitlist-close i {
font-size: 18px;
}
.waitlist-desc {
font-size: 13px;
color: var(--color-text-secondary);
margin-bottom: 20px;
line-height: 1.5;
}
.waitlist-field {
margin-bottom: 16px;
}
.waitlist-field:last-child {
margin-bottom: 0;
}
.waitlist-label {
display: block;
font-size: 13px;
font-weight: 500;
color: var(--color-text);
margin-bottom: 6px;
}
.waitlist-periods {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 8px;
}
.period-option {
display: flex;
align-items: center;
gap: 10px;
padding: 12px 14px;
background: var(--color-surface);
border: 2px solid transparent;
border-radius: 8px;
cursor: pointer;
transition: all 150ms ease;
}
.period-option:hover {
border-color: var(--color-border);
}
.period-option.selected {
border-color: var(--color-teal);
background: var(--color-teal-light);
}
.period-check {
width: 20px;
height: 20px;
border-radius: 4px;
border: 2px solid var(--color-border);
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
color: white;
transition: all 150ms ease;
flex-shrink: 0;
}
.period-option.selected .period-check {
background: var(--color-teal);
border-color: var(--color-teal);
}
.period-check i {
opacity: 0;
transition: opacity 150ms ease;
}
.period-option.selected .period-check i {
opacity: 1;
}
.period-info {
flex: 1;
}
.period-name {
font-size: 13px;
font-weight: 500;
}
.period-time {
font-size: 11px;
color: var(--color-text-secondary);
}
.waitlist-submit {
margin-top: 20px;
width: 100%;
padding: 12px 20px;
background: var(--color-teal);
color: white;
font-size: 14px;
font-weight: 500;
font-family: var(--font-family);
border: none;
border-radius: 8px;
cursor: pointer;
transition: background 150ms ease;
}
.waitlist-submit:hover {
background: #00695c;
}
/* ==========================================
FORM
========================================== */
.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;
}
.success-icon.waitlist {
background: rgba(0, 137, 123, 0.12);
}
.success-icon.waitlist i {
color: var(--color-teal);
}
.success-btn {
display: inline-flex;
align-items: center;
gap: 6px;
margin-top: 20px;
padding: 12px 24px;
background: var(--color-teal);
color: white;
font-size: 14px;
font-weight: 500;
font-family: var(--font-family);
border: none;
border-radius: 8px;
cursor: pointer;
transition: background 150ms ease;
}
.success-btn:hover {
background: #00695c;
}
.success-btn i {
font-size: 16px;
}
/* ==========================================
LANDING VIEW
========================================== */
.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>
<div class="salon-address">Amager Strandvej 22f, 2300 Kbh S</div>
<!-- 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>
<div class="salon-address">Amager Strandvej 22f, 2300 Kbh S</div>
</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>
<!-- Waitlist success overlay -->
<div class="success-overlay" id="waitlistSuccessOverlay">
<div class="success-modal">
<div class="success-icon waitlist">
<i class="ph ph-clock-countdown"></i>
</div>
<h2>Du er på ventelisten!</h2>
<p>Tak for din tilmelding. Vi kontakter dig hurtigst muligt, hvis der bliver en ledig tid.</p>
<button class="success-btn" id="waitlistSuccessClose">
Luk <i class="ph ph-x"></i>
</button>
</div>
</div>
<script>
// ==========================================
// CONFIG
// ==========================================
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 }
];
// ==========================================
// 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>`;
}
const times = ['09:00', '09:30', '10:00', '10:30', '11:00', '11:30', '12:00', '13:00', '13:30', '14:00', '14:30', '15:00', '15:30', '16:00', '16:30'];
const taken = ['10:30', '14:00'];
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>
<div class="time-grid">
${times.map(t => {
let cls = 'time-slot';
if (taken.includes(t)) cls += ' disabled';
if (state.time === t) cls += ' selected';
return `<div class="${cls}" data-time="${t}">${t}</div>`;
}).join('')}
</div>
<!-- Waitlist -->
<button class="waitlist-trigger" id="waitlistTrigger">
<i class="ph ph-clock-countdown"></i>
Ingen tid der passer? Skriv dig på venteliste
</button>
<div class="waitlist-section" id="waitlistSection">
<div class="waitlist-content">
<div class="waitlist-header">
<div class="waitlist-title">Skriv dig på venteliste</div>
<button class="waitlist-close" id="waitlistClose">
<i class="ph ph-x"></i>
</button>
</div>
<p class="waitlist-desc">
Hvis der bliver en ledig tid, kontakter vi dig hurtigst muligt.
Angiv hvornår du senest ønsker en tid, og hvilke tidspunkter der passer dig.
</p>
<div class="waitlist-field">
<label class="waitlist-label">Senest dato for aftale</label>
<input type="date" class="form-input" id="waitlistDate">
</div>
<div class="waitlist-field">
<label class="waitlist-label">Hvilke tidspunkter passer dig?</label>
<div class="waitlist-periods">
<div class="period-option" data-period="morning">
<div class="period-check"><i class="ph ph-check"></i></div>
<div class="period-info">
<div class="period-name">Morgen</div>
<div class="period-time">Åbning - 10:00</div>
</div>
</div>
<div class="period-option" data-period="lateMorning">
<div class="period-check"><i class="ph ph-check"></i></div>
<div class="period-info">
<div class="period-name">Formiddag</div>
<div class="period-time">10:00 - 12:00</div>
</div>
</div>
<div class="period-option" data-period="afternoon">
<div class="period-check"><i class="ph ph-check"></i></div>
<div class="period-info">
<div class="period-name">Eftermiddag</div>
<div class="period-time">12:00 - 17:00</div>
</div>
</div>
<div class="period-option" data-period="evening">
<div class="period-check"><i class="ph ph-check"></i></div>
<div class="period-info">
<div class="period-name">Aften</div>
<div class="period-time">17:00 - Luk</div>
</div>
</div>
</div>
</div>
<div class="waitlist-field">
<label class="waitlist-label">Besked til salonen <span class="optional">(valgfrit)</span></label>
<textarea class="form-input" id="waitlistMessage" placeholder="Evt. særlige ønsker eller kommentarer..." rows="3"></textarea>
</div>
<button class="waitlist-submit" id="waitlistSubmit">
<i class="ph ph-check"></i> Skriv mig på venteliste
</button>
</div>
</div>
</div>
</div>
`;
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);
}
});
});
// 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 = '';
}
// ==========================================
// 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();
});
// Waitlist success overlay close
document.getElementById('waitlistSuccessClose').addEventListener('click', closeWaitlistSuccess);
document.getElementById('waitlistSuccessOverlay').addEventListener('click', (e) => {
if (e.target.id === 'waitlistSuccessOverlay') closeWaitlistSuccess();
});
// View switching - Landing
document.getElementById('phoneNextBtn').addEventListener('click', handlePhoneNext);
document.getElementById('backToPhoneBtn').addEventListener('click', handleBackToPhone);
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>