From 07224ba1e7fc72f67a0278eb3daed76993d4d108 Mon Sep 17 00:00:00 2001 From: jhodgkin Date: Tue, 7 Jul 2026 23:00:39 -0600 Subject: [PATCH] Implement config-driven session type and length steps (issue #4) - site.json: add booking section with sessionTypes, pricing, venmoUsername - Step 2: session type cards rendered from siteConfig.booking.sessionTypes; "Other" option reveals a free-text field; validated before advancing - Step 3: two large length cards with duration badge + price from config; falls back to $75/$150 defaults if config missing - selectOption shows/hides "Other" field; stores sessionTypeOther in state - populateSummary uses config labels and shows price in length summary row - submitBooking sends "other: " for custom session types Closes #4 Co-Authored-By: Claude Sonnet 4.6 --- config/site.json | 14 +++++ src/index.html | 155 +++++++++++++++++++++++++++++++++-------------- 2 files changed, 124 insertions(+), 45 deletions(-) diff --git a/config/site.json b/config/site.json index 20d8669..ea9722c 100644 --- a/config/site.json +++ b/config/site.json @@ -57,6 +57,20 @@ "description": "Enter your gallery code to view and download your photos", "placeholder": "Enter your gallery code" }, + "booking": { + "venmoUsername": "LisiLouPhoto", + "sessionTypes": [ + { "id": "senior", "label": "Senior Portrait", "description": "Celebrate your milestone" }, + { "id": "couple", "label": "Couple", "description": "Engagement & couples" }, + { "id": "family", "label": "Family", "description": "Family portraits" }, + { "id": "individual", "label": "Individual", "description": "Personal portrait session" }, + { "id": "newborn", "label": "Newborn", "description": "Newborn & maternity" } + ], + "pricing": { + "mini": 75, + "full": 150 + } + }, "theme": { "primaryColor": "#8B7355", "accentColor": "#D4C5B5", diff --git a/src/index.html b/src/index.html index d95252f..6392ade 100644 --- a/src/index.html +++ b/src/index.html @@ -816,6 +816,36 @@ .option-card-desc { font-size: 0.75rem; color: var(--color-text-light); } + /* Length cards — larger variant with price */ + .length-grid { grid-template-columns: 1fr 1fr; } + + .length-card { padding: 2rem 1.25rem; } + + .length-card-badge { + font-size: 0.62rem; + letter-spacing: 0.2em; + text-transform: uppercase; + color: var(--color-primary); + margin-bottom: 0.6rem; + } + + .length-card-price { + font-family: var(--font-display); + font-size: 2.2rem; + font-weight: 300; + color: var(--color-text); + line-height: 1; + margin: 0.4rem 0 0.6rem; + } + + .length-card .option-card-desc { line-height: 1.5; margin-top: 0.5rem; } + + /* "Other" free-text field in session type step */ + .other-field { + margin-top: 1rem; + display: none; + } + /* Contract step */ .contract-placeholder { background: var(--color-bg-warm); @@ -1107,46 +1137,22 @@ - +

Session Type

What kind of session are you looking for?

-
-
-
Individual
-
Portrait session
-
-
-
Couple
-
Engagement & couples
-
-
-
Family
-
Family portraits
-
-
-
Newborn
-
Newborn & maternity
-
+
+
+ +
-
- +

Session Length

How much time would you like?

-
-
-
Mini Session
-
30 minutes
-
-
-
Full Session
-
60 minutes
-
-
- +
@@ -1505,26 +1511,59 @@ const BOOKING_STEPS = 7; const bookingState = { - sessionDate: '', sessionType: '', sessionLength: '', + sessionDate: '', sessionType: '', sessionTypeOther: '', sessionLength: '', sessionLocation: '', clientName: '', clientEmail: '', clientPhone: '', }; let currentStep = 0; + function renderSessionTypeCards() { + const types = (window.siteConfig?.booking?.sessionTypes || [ + { id: 'individual', label: 'Individual Portrait', description: 'Portrait session' }, + { id: 'couple', label: 'Couple', description: 'Engagement & couples' }, + { id: 'family', label: 'Family', description: 'Family portraits' }, + { id: 'newborn', label: 'Newborn', description: 'Newborn & maternity' }, + ]).concat([{ id: 'other', label: 'Other', description: 'Something else entirely' }]); + + document.getElementById('session-type-options').innerHTML = types.map(t => ` +
+
${t.label}
+
${t.description || ''}
+
`).join(''); + } + + function renderSessionLengthCards() { + const pricing = window.siteConfig?.booking?.pricing || { mini: 75, full: 150 }; + document.getElementById('session-length-options').innerHTML = ` +
+
30 minutes
+
Mini Session
+
$${pricing.mini}
+
A focused session — great for individuals or a single look
+
+
+
60 minutes
+
Full Session
+
$${pricing.full}
+
A relaxed session with time for multiple looks and locations
+
`; + } + function openBooking() { document.getElementById('booking-overlay').classList.add('active'); document.body.style.overflow = 'hidden'; - // Reset state Object.keys(bookingState).forEach(k => bookingState[k] = ''); - document.querySelectorAll('.option-card').forEach(c => c.classList.remove('selected')); document.getElementById('contract-agree').checked = false; document.getElementById('payment-sent').checked = false; document.getElementById('client-name').value = ''; document.getElementById('client-email').value = ''; document.getElementById('client-phone').value = ''; - // Set min date to today + document.getElementById('other-type-text').value = ''; + document.getElementById('other-type-field').style.display = 'none'; document.getElementById('session-date').min = new Date().toISOString().split('T')[0]; document.getElementById('session-date').value = ''; + renderSessionTypeCards(); + renderSessionLengthCards(); goToStep(1); } @@ -1568,6 +1607,11 @@ card.closest('.option-grid').querySelectorAll('.option-card').forEach(c => c.classList.remove('selected')); card.classList.add('selected'); bookingState[field] = card.dataset.value; + if (field === 'sessionType') { + const otherField = document.getElementById('other-type-field'); + otherField.style.display = card.dataset.value === 'other' ? 'block' : 'none'; + if (card.dataset.value !== 'other') bookingState.sessionTypeOther = ''; + } if (field === 'sessionLength') updatePaymentAmount(); } @@ -1578,23 +1622,42 @@ } function populateSummary() { - const labels = { - individual: 'Individual Portrait', couple: 'Couple', family: 'Family', newborn: 'Newborn', - mini: 'Mini Session (30 min)', full: 'Full Session (60 min)', - 'outdoor-park': 'Outdoor Park', studio: 'Studio', urban: 'Urban', 'client-choice': 'Your Choice', + const types = window.siteConfig?.booking?.sessionTypes || []; + const pricing = window.siteConfig?.booking?.pricing || { mini: 75, full: 150 }; + + const typeLabel = types.find(t => t.id === bookingState.sessionType)?.label + || bookingState.sessionType || '—'; + const displayType = bookingState.sessionType === 'other' && bookingState.sessionTypeOther + ? `Other — ${bookingState.sessionTypeOther}` + : typeLabel; + + const lengthLabels = { + mini: `Mini Session — 30 min ($${pricing.mini})`, + full: `Full Session — 60 min ($${pricing.full})`, }; + document.getElementById('summary-date').textContent = bookingState.sessionDate || '—'; - document.getElementById('summary-type').textContent = labels[bookingState.sessionType] || '—'; - document.getElementById('summary-length').textContent = labels[bookingState.sessionLength] || '—'; - document.getElementById('summary-location').textContent = labels[bookingState.sessionLocation] || '—'; + document.getElementById('summary-type').textContent = displayType; + document.getElementById('summary-length').textContent = lengthLabels[bookingState.sessionLength] || '—'; + document.getElementById('summary-location').textContent = bookingState.sessionLocation || '—'; } function validateStep(step) { switch (step) { case 1: return !!document.getElementById('session-date').value; - case 2: - return !!bookingState.sessionType; + case 2: { + if (!bookingState.sessionType) return false; + if (bookingState.sessionType === 'other') { + const txt = document.getElementById('other-type-text').value.trim(); + if (!txt) { + document.getElementById('other-type-text').focus(); + return false; + } + bookingState.sessionTypeOther = txt; + } + return true; + } case 3: return !!bookingState.sessionLength; case 4: @@ -1653,7 +1716,9 @@ client_email: bookingState.clientEmail, client_phone: bookingState.clientPhone, session_date: bookingState.sessionDate, - session_type: bookingState.sessionType, + session_type: bookingState.sessionType === 'other' + ? 'other: ' + bookingState.sessionTypeOther + : bookingState.sessionType, session_length: bookingState.sessionLength, location: bookingState.sessionLocation, }),