From 9dcc73ecfe4e6c14bcd2ad94bb5f3f6c9089d19f Mon Sep 17 00:00:00 2001 From: Jerod Hodgkin Date: Thu, 16 Jul 2026 00:39:36 -0600 Subject: [PATCH] Design pass: fix dead site config, add hero image support, polish cards MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix applyConfig self-recursion (const _origApply captured the hoisted wrapper itself): site.json config — theme, portfolio, socials, bio — was never applied; site silently fell back to hardcoded brown defaults - Strengthen theme test to assert the configured primary color exactly - Portfolio covers: styled placeholder on missing image (like locations), restore img when Immich carousel kicks in; overlay always visible on touch - Optional hero photo via site.heroImage with soft scrim (activates only when the image loads); site.json points at /images/hero.jpg - Option cards: equal height, centered content - /#book deep link also works via hashchange Co-Authored-By: Claude Fable 5 --- config/site.json | 3 +- src/index.html | 82 ++++++++++++++++++++++++++++++++++++----- tests/portfolio.spec.js | 22 +++++------ 3 files changed, 84 insertions(+), 23 deletions(-) diff --git a/config/site.json b/config/site.json index eb25286..27325c8 100644 --- a/config/site.json +++ b/config/site.json @@ -4,7 +4,8 @@ "tagline": "Capturing life's beautiful moments", "description": "Senior portraits, engagement sessions, and milestone photography", "logo": "/images/logo.png", - "favicon": "/images/favicon.ico" + "favicon": "/images/favicon.ico", + "heroImage": "/images/hero.jpg" }, "photographer": { "name": "Elysse Hodgkin", diff --git a/src/index.html b/src/index.html index 129a931..44528c8 100644 --- a/src/index.html +++ b/src/index.html @@ -198,6 +198,19 @@ opacity: 0.5; } + /* Optional hero photo (config: site.heroImage) with a soft scrim for text legibility */ + .hero-bg.hero-bg--image { + background-size: cover; + background-position: center; + opacity: 1; + } + .hero-bg.hero-bg--image::after { + content: ''; + position: absolute; + inset: 0; + background: linear-gradient(to bottom, rgba(255,255,255,0.5) 0%, rgba(255,255,255,0.72) 100%); + } + .hero-pattern { position: absolute; inset: 0; @@ -413,6 +426,25 @@ letter-spacing: 0.1em; } + /* Styled stand-in when a category cover image is missing (same idea as location cards) */ + .portfolio-card-fallback { + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + background: linear-gradient(135deg, var(--color-bg-warm) 0%, var(--color-accent) 100%); + font-family: var(--font-display); + font-style: italic; + font-size: 1.3rem; + color: var(--color-primary); + } + + /* Touch devices have no hover — keep category titles visible */ + @media (hover: none) { + .portfolio-card-overlay { opacity: 1; } + } + /* Client Access Section */ #client-access { background: var(--color-bg); @@ -867,6 +899,10 @@ text-align: center; transition: all 0.3s var(--transition-smooth); user-select: none; + display: flex; + flex-direction: column; + justify-content: center; + min-height: 6.5rem; } .option-card:hover { border-color: var(--color-primary); } @@ -1402,7 +1438,7 @@
-
+

Photography

@@ -1699,6 +1735,14 @@ return []; } + // Swap a missing category cover for a styled placeholder (keeps the card elegant) + function portfolioCoverFallback(img) { + const fallback = document.createElement('div'); + fallback.className = 'portfolio-card-fallback'; + fallback.textContent = img.alt || 'Coming soon'; + img.replaceWith(fallback); + } + // Start carousel for a portfolio card function startCarousel(cardElement, images) { if (images.length <= 1) return; @@ -1729,8 +1773,9 @@ } async function applyConfig(config) { - // Store immich config globally first (needed for thumbnail fetching) + // Store config globally: immich for thumbnails, siteConfig for the booking form window.immichConfig = config.immich || {}; + window.siteConfig = config; // Apply theme colors if (config.theme) { @@ -1752,6 +1797,16 @@ if (config.site.tagline) { document.getElementById('hero-tagline').textContent = config.site.tagline; } + if (config.site.heroImage) { + // Only activate the photo treatment once the image actually loads + const probe = new Image(); + probe.onload = () => { + const heroBg = document.getElementById('hero-bg'); + heroBg.style.backgroundImage = `url('${config.site.heroImage}')`; + heroBg.classList.add('hero-bg--image'); + }; + probe.src = config.site.heroImage; + } } // Apply portfolio categories @@ -1763,7 +1818,7 @@ // First render the cards with placeholder/fallback images grid.innerHTML = config.portfolio.categories.map(cat => `
- ${cat.name} + ${cat.name}

${cat.name}

${cat.description || ''}

@@ -1778,7 +1833,15 @@ const images = await getImmichAlbumImages(baseUrl, prefix, cat.immichAlbumId); if (images.length > 0) { const card = grid.children[index]; - const img = card.querySelector('img'); + let img = card.querySelector('img'); + + // The cover may have been swapped for the styled fallback — restore an + if (!img) { + const fallback = card.querySelector('.portfolio-card-fallback'); + img = document.createElement('img'); + img.alt = cat.name; + if (fallback) fallback.replaceWith(img); else card.prepend(img); + } // Set the first image img.src = images[0]; @@ -1889,12 +1952,7 @@ reveals.forEach(el => observer.observe(el)); } - // Store config globally for the booking form to use - const _origApply = applyConfig; - async function applyConfig(config) { - window.siteConfig = config; - return _origApply(config); - } + // (window.siteConfig is set at the top of applyConfig for the booking form) // ── Date Picker ─────────────────────────────────────── @@ -2613,6 +2671,10 @@ A complete service agreement and model release will be provided at your session setTimeout(openBooking, 400); } }); + + window.addEventListener('hashchange', function() { + if (window.location.hash === '#book') openBooking(); + }); diff --git a/tests/portfolio.spec.js b/tests/portfolio.spec.js index ba7ec93..9136a36 100644 --- a/tests/portfolio.spec.js +++ b/tests/portfolio.spec.js @@ -45,19 +45,17 @@ test.describe('Portfolio site', () => { expect(json.ok).toBe(true); }); - test('pink theme is applied (primary color is rose/pink)', async ({ page }) => { + test('configured theme is applied (primary color matches site.json)', async ({ page, request }) => { + const config = await (await request.get('/config/site.json')).json(); + const expected = (config.theme?.primaryColor || '').toLowerCase(); + expect(expected).toMatch(/^#[a-f0-9]{6}$/); + await page.goto('/'); - const primary = await page.evaluate(() => - getComputedStyle(document.documentElement).getPropertyValue('--color-primary').trim() + // Config is fetched on window load; wait until the var flips from the CSS default + await page.waitForFunction( + exp => getComputedStyle(document.documentElement).getPropertyValue('--color-primary').trim().toLowerCase() === exp, + expected, + { timeout: 8_000 } ); - // Should be a pink/rose hex — starts with #B or #b (our dusty rose #B06A7A) - expect(primary.toLowerCase()).toMatch(/^#[a-f0-9]{6}$/i); - // Hue should be in the red-pink range: R > G and R > B - const hex = primary.replace('#', ''); - const r = parseInt(hex.slice(0, 2), 16); - const g = parseInt(hex.slice(2, 4), 16); - const b = parseInt(hex.slice(4, 6), 16); - expect(r).toBeGreaterThan(g); - expect(r).toBeGreaterThan(b); }); });