Design pass: fix dead site config, add hero image support, polish cards
Deploy to Dev / Deploy & Smoke Test (push) Successful in 21s
Deploy to Dev / Deploy & Smoke Test (push) Successful in 21s
- 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 <noreply@anthropic.com>
This commit is contained in:
+2
-1
@@ -4,7 +4,8 @@
|
|||||||
"tagline": "Capturing life's beautiful moments",
|
"tagline": "Capturing life's beautiful moments",
|
||||||
"description": "Senior portraits, engagement sessions, and milestone photography",
|
"description": "Senior portraits, engagement sessions, and milestone photography",
|
||||||
"logo": "/images/logo.png",
|
"logo": "/images/logo.png",
|
||||||
"favicon": "/images/favicon.ico"
|
"favicon": "/images/favicon.ico",
|
||||||
|
"heroImage": "/images/hero.jpg"
|
||||||
},
|
},
|
||||||
"photographer": {
|
"photographer": {
|
||||||
"name": "Elysse Hodgkin",
|
"name": "Elysse Hodgkin",
|
||||||
|
|||||||
+72
-10
@@ -198,6 +198,19 @@
|
|||||||
opacity: 0.5;
|
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 {
|
.hero-pattern {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
@@ -413,6 +426,25 @@
|
|||||||
letter-spacing: 0.1em;
|
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 Section */
|
||||||
#client-access {
|
#client-access {
|
||||||
background: var(--color-bg);
|
background: var(--color-bg);
|
||||||
@@ -867,6 +899,10 @@
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
transition: all 0.3s var(--transition-smooth);
|
transition: all 0.3s var(--transition-smooth);
|
||||||
user-select: none;
|
user-select: none;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
min-height: 6.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.option-card:hover { border-color: var(--color-primary); }
|
.option-card:hover { border-color: var(--color-primary); }
|
||||||
@@ -1402,7 +1438,7 @@
|
|||||||
|
|
||||||
<!-- Hero Section -->
|
<!-- Hero Section -->
|
||||||
<section class="hero">
|
<section class="hero">
|
||||||
<div class="hero-bg"></div>
|
<div class="hero-bg" id="hero-bg"></div>
|
||||||
<div class="hero-pattern"></div>
|
<div class="hero-pattern"></div>
|
||||||
<div class="hero-content">
|
<div class="hero-content">
|
||||||
<p class="hero-eyebrow" id="hero-eyebrow">Photography</p>
|
<p class="hero-eyebrow" id="hero-eyebrow">Photography</p>
|
||||||
@@ -1699,6 +1735,14 @@
|
|||||||
return [];
|
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
|
// Start carousel for a portfolio card
|
||||||
function startCarousel(cardElement, images) {
|
function startCarousel(cardElement, images) {
|
||||||
if (images.length <= 1) return;
|
if (images.length <= 1) return;
|
||||||
@@ -1729,8 +1773,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function applyConfig(config) {
|
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.immichConfig = config.immich || {};
|
||||||
|
window.siteConfig = config;
|
||||||
|
|
||||||
// Apply theme colors
|
// Apply theme colors
|
||||||
if (config.theme) {
|
if (config.theme) {
|
||||||
@@ -1752,6 +1797,16 @@
|
|||||||
if (config.site.tagline) {
|
if (config.site.tagline) {
|
||||||
document.getElementById('hero-tagline').textContent = 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
|
// Apply portfolio categories
|
||||||
@@ -1763,7 +1818,7 @@
|
|||||||
// First render the cards with placeholder/fallback images
|
// First render the cards with placeholder/fallback images
|
||||||
grid.innerHTML = config.portfolio.categories.map(cat => `
|
grid.innerHTML = config.portfolio.categories.map(cat => `
|
||||||
<div class="portfolio-card reveal" data-album-id="${cat.immichAlbumId || ''}" onclick="openPortfolio('${cat.id}', '${cat.immichAlbumId || ''}')">
|
<div class="portfolio-card reveal" data-album-id="${cat.immichAlbumId || ''}" onclick="openPortfolio('${cat.id}', '${cat.immichAlbumId || ''}')">
|
||||||
<img src="${cat.coverImage || '/images/placeholder.jpg'}" alt="${cat.name}" loading="lazy">
|
<img src="${cat.coverImage || '/images/placeholder.jpg'}" alt="${cat.name}" loading="lazy" onerror="portfolioCoverFallback(this)">
|
||||||
<div class="portfolio-card-overlay">
|
<div class="portfolio-card-overlay">
|
||||||
<h3 class="portfolio-card-title">${cat.name}</h3>
|
<h3 class="portfolio-card-title">${cat.name}</h3>
|
||||||
<p class="portfolio-card-desc">${cat.description || ''}</p>
|
<p class="portfolio-card-desc">${cat.description || ''}</p>
|
||||||
@@ -1778,7 +1833,15 @@
|
|||||||
const images = await getImmichAlbumImages(baseUrl, prefix, cat.immichAlbumId);
|
const images = await getImmichAlbumImages(baseUrl, prefix, cat.immichAlbumId);
|
||||||
if (images.length > 0) {
|
if (images.length > 0) {
|
||||||
const card = grid.children[index];
|
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 <img>
|
||||||
|
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
|
// Set the first image
|
||||||
img.src = images[0];
|
img.src = images[0];
|
||||||
@@ -1889,12 +1952,7 @@
|
|||||||
reveals.forEach(el => observer.observe(el));
|
reveals.forEach(el => observer.observe(el));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store config globally for the booking form to use
|
// (window.siteConfig is set at the top of applyConfig for the booking form)
|
||||||
const _origApply = applyConfig;
|
|
||||||
async function applyConfig(config) {
|
|
||||||
window.siteConfig = config;
|
|
||||||
return _origApply(config);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Date Picker ───────────────────────────────────────
|
// ── Date Picker ───────────────────────────────────────
|
||||||
|
|
||||||
@@ -2613,6 +2671,10 @@ A complete service agreement and model release will be provided at your session
|
|||||||
setTimeout(openBooking, 400);
|
setTimeout(openBooking, 400);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
window.addEventListener('hashchange', function() {
|
||||||
|
if (window.location.hash === '#book') openBooking();
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js" crossorigin="anonymous"></script>
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js" crossorigin="anonymous"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
+10
-12
@@ -45,19 +45,17 @@ test.describe('Portfolio site', () => {
|
|||||||
expect(json.ok).toBe(true);
|
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('/');
|
await page.goto('/');
|
||||||
const primary = await page.evaluate(() =>
|
// Config is fetched on window load; wait until the var flips from the CSS default
|
||||||
getComputedStyle(document.documentElement).getPropertyValue('--color-primary').trim()
|
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);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user