Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 64 additions & 1 deletion docs/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ Sinister Dexter band website - A modular, data-driven static site generator for
## Common Tasks

### 📅 Update Shows
1. Edit `public/data/shows.json`
1. Edit `template/data/shows.json`
2. Add show details in this format:
```json
{
Expand All @@ -97,6 +97,69 @@ Sinister Dexter band website - A modular, data-driven static site generator for
3. Run `npm run build:html`
4. Deploy: `./deploy.sh staging` then `./deploy.sh prod`

### 📸 Add Media to Past Shows
Past shows can include optional media (pictures, vertical videos, horizontal videos):
1. Edit `template/data/shows.json`
2. Add a `media` array to any past show:
```json
{
"date": "2025-11-14",
"venue": "Club Fox",
"city": "Redwood City, CA",
"time": "7:00 PM - 11:00 PM",
"link": "https://clubfoxrwc.com/",
"media": [
{
"type": "image",
"url": "https://cdn.sinister-dexter.com/photos/show-photo.jpg",
"alt": "Band performing at Club Fox"
},
{
"type": "video-vertical",
"platform": "tiktok",
"videoId": "7573064485842242871",
"username": "@sindex_band",
"caption": "From our Nov 14, 2025 show at Club Fox!"
},
{
"type": "video-horizontal",
"platform": "youtube",
"videoId": "xyz789"
}
]
}
```
**Media Types:**
- `image` - Photos from the show (displayed as square thumbnails)
- `video-vertical` - TikTok videos (uses official TikTok embed)
- `video-horizontal` - YouTube videos (iframe embed)

**Required Fields by Type:**

**Images:**
- `type`: "image"
- `url`: Direct image URL
- `alt`: Alt text for accessibility

**TikTok Videos:**
- `type`: "video-vertical"
- `videoId`: TikTok video ID (the numeric ID from the URL)
- `username`: TikTok username (optional, defaults to @sindex_band)
- `caption`: Caption text (optional)
- `url`: Full TikTok URL (optional, auto-generated from videoId)

**YouTube Videos:**
- `type`: "video-horizontal"
- `videoId`: YouTube video ID (the part after `v=` or in shorts URL)

3. Run `npm run build:html`
4. Deploy changes

**Note:** The TikTok embed component is located at `template/partials/components/tiktok-embed.hbs` and can be reused anywhere in templates with:
```handlebars
{{> components/tiktok-embed videoId="7573064485842242871" username="@sindex_band" caption="Optional caption"}}
```

### 👥 Update Band Members
1. Edit `public/data/members.json`
2. Add member data:
Expand Down
7 changes: 7 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

30 changes: 12 additions & 18 deletions public/index.html

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion public/styles.css

Large diffs are not rendered by default.

17 changes: 17 additions & 0 deletions scripts/build.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,19 @@ Handlebars.registerHelper('slice', (array, start, end) => {

Handlebars.registerHelper('gt', (a, b) => a > b);

Handlebars.registerHelper('eq', (a, b) => a === b);

Handlebars.registerHelper('startsWith', (str, prefix) => {
return str && typeof str === 'string' && str.startsWith(prefix);
});

Handlebars.registerHelper('isDifferentYear', (dateStr) => {
if (!dateStr) return false;
const eventYear = new Date(dateStr).getFullYear();
const currentYear = new Date().getFullYear();
return eventYear !== currentYear;
});

// Helper to lookup nested array values
Handlebars.registerHelper('lookup', (array, index, property) => {
if (!array || !array[index]) return '';
Expand Down Expand Up @@ -493,13 +506,17 @@ function prepareTemplateData(data) {
console.log(` 🎵 ${tracks.length} tracks`);
console.log(` 🖼️ ${data.images.thumbnail ? data.images.thumbnail.length : 0} gallery images`);

// Filter out private events for structured data (SEO)
const publicShows = upcomingShows.filter(show => !show.private);

return {
site,
band,
navigation,
hero,
members,
upcomingShows,
publicShows,
pastShows,
videos,
images: data.images,
Expand Down
39 changes: 33 additions & 6 deletions template/data/shows.json
Original file line number Diff line number Diff line change
@@ -1,18 +1,45 @@
{
"upcomingShows": [
{
"date": "2025-12-05",
"name": "Corporate Holiday Party",
"venue": "The Fillmore",
"address": "1805 Geary Blvd, San Francisco, CA 94115",
"time": "",
"link": "mailto:booking@sinisterdexter.net",
"description": "Book us for your own!",
"private": true
},
{
"date": "2026-09-02",
"name": "City of Albany's Concert in the Park",
"venue": "Albany Community Center",
"address": "1249 Marin Ave, Albany, CA 94706",
"time": "",
"link": "https://www.albanyca.gov/City-Calendar/2025/Aug-13-Concert-in-the-Park",
"description": "Free outdoor summer concert series in Albany"
}
],
"pastShows": [
{
"date": "2025-11-14",
"name": "Friday Night Funk",
"venue": "Club Fox",
"address": "2209 Broadway, Redwood City, CA 94063",
"city": "Redwood City, CA",
"time": "7:00 PM - 11:00 PM",
"link": "https://clubfoxrwc.com/",
"tickets": "https://www.eventbrite.com/e/sinister-dexter-funk-soul-horns-tickets-1568339939979?aff=ebdssbdestsearch",
"description": "Friday night funk at the legendary Club Fox venue",
"admission": "$20"
}
],
"pastShows": [
"media": [
{
"type": "video-vertical",
"platform": "tiktok",
"url": "https://www.tiktok.com/@sindex_band/video/7573064485842242871",
"videoId": "7573064485842242871",
"username": "@sindex_band",
"caption": "From our Nov 14, 2025 show at Club Fox!"
}
]
},
{
"date": "2025-10-19",
"venue": "Blue Oak Brewing Company",
Expand Down
42 changes: 42 additions & 0 deletions template/partials/components/scripts.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -143,4 +143,46 @@ const tracks = {{{json tracks}}};
loadTrack(0);
}
})();
</script>

<!-- TikTok Embed Script -->
<script async src="https://www.tiktok.com/embed.js"></script>

<!-- Hero Video Background Handler -->
<script>
(function() {
const heroVideo = document.querySelector('#hero video');
if (!heroVideo) return;

// Handle responsive video source switching
function updateVideoSource() {
const isMobile = window.innerWidth <= 768;
const currentSrc = heroVideo.querySelector('source[src*="hero-mobile.mp4"]');
const desktopSrc = heroVideo.querySelector('source[src*="hero-desktop.mp4"]');

// Browsers will automatically select the appropriate source based on media queries
// But we can force reload if needed on resize
heroVideo.load();
}

// Ensure video plays (some browsers block autoplay)
heroVideo.play().catch(function(error) {
console.log('Video autoplay prevented:', error);
});

// Pause video when tab is not visible (save bandwidth/battery)
document.addEventListener('visibilitychange', function() {
if (document.hidden) {
heroVideo.pause();
} else {
heroVideo.play().catch(function() {});
}
});

// Reduce motion for users who prefer it
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
heroVideo.pause();
heroVideo.style.display = 'none';
}
})();
</script>
27 changes: 27 additions & 0 deletions template/partials/components/tiktok-embed.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{{!--
TikTok Video Embed Component

Usage:
{{> components/tiktok-embed videoId="7573064485842242871" username="@sindex_band" caption="Optional caption text"}}

Parameters:
- videoId (required): The TikTok video ID
- username (optional): TikTok username (defaults to @sindex_band)
- caption (optional): Caption text to display
- url (optional): Full TikTok URL (auto-generated if not provided)
--}}
<div class="flex justify-center">
<blockquote class="tiktok-embed"
cite="{{#if url}}{{url}}{{else}}https://www.tiktok.com/{{#if username}}{{username}}{{else}}@sindex_band{{/if}}/video/{{videoId}}{{/if}}"
data-video-id="{{videoId}}"
style="max-width: 605px; min-width: 325px;">
<section>
<a target="_blank"
title="{{#if username}}{{username}}{{else}}@sindex_band{{/if}}"
href="https://www.tiktok.com/{{#if username}}{{username}}{{else}}@sindex_band{{/if}}?refer=embed">
{{#if username}}{{username}}{{else}}@sindex_band{{/if}}
</a>
{{#if caption}}<p>{{caption}}</p>{{/if}}
</section>
</blockquote>
</div>
4 changes: 2 additions & 2 deletions template/partials/head/structured-data.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -66,9 +66,9 @@
"@type": "ContactPoint",
"contactType": "Booking",
"email": "{{site.bookingEmail}}"
}{{#if upcomingShows}},
}{{#if publicShows}},
"event": [
{{#each upcomingShows}}
{{#each publicShows}}
{
"@type": "MusicEvent",
"@id": "{{../site.url}}#event-{{this.id}}",
Expand Down
43 changes: 32 additions & 11 deletions template/partials/sections/hero.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,42 @@
class="relative h-screen flex items-center justify-center overflow-hidden"
>

<!-- Background Image with WebP fallback -->
<picture class="absolute inset-0">
<source srcset="images/large/{{hero.image}}.webp" type="image/webp" />
<img
src="images/large/{{hero.image}}.jpg"
alt="{{site.title}} full band"
class="w-full h-full object-cover object-center"
<!-- Background Video with Image fallback -->
<video
autoplay
muted
loop
playsinline
class="absolute inset-0 w-full h-full object-cover object-center"
poster="images/large/{{hero.image}}.jpg"
>
<!-- Responsive video sources -->
<source
src="https://cdn.sinister-dexter.com/video/hero-mobile.mp4"
type="video/mp4"
media="(max-width: 768px)"
/>
<source
src="https://cdn.sinister-dexter.com/video/hero-desktop.mp4"
type="video/mp4"
/>
</picture>

<!-- Dark Overlay for text contrast -->
<div class="absolute inset-0 bg-black/60"></div>
<!-- Fallback for browsers that don't support video -->
<picture class="absolute inset-0">
<source srcset="images/large/{{hero.image}}.webp" type="image/webp" />
<img
src="images/large/{{hero.image}}.jpg"
alt="{{site.title}} full band"
class="w-full h-full object-cover object-center"
/>
</picture>
</video>

<!-- Dark Overlay for text contrast (reduced since video is already darkened) -->
<div class="absolute inset-0 bg-black/40"></div>
<!-- Gradient Overlay for color tint -->
<div
class="absolute inset-0 bg-gradient-to-br from-purple-900/40 to-amber-900/40"
class="absolute inset-0 bg-gradient-to-br from-purple-900/30 to-amber-900/30"
></div>
<div class="hero-overlay absolute inset-0"></div>

Expand Down
Loading