-
Notifications
You must be signed in to change notification settings - Fork 0
Rotate coin-face images from folder, enforce ≥2 headshots, add pre-deploy validation #16
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
510633a
a9b73a1
383ba3f
644dcc3
6dd8fb1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,18 +1,43 @@ | ||
| {{ $frontImg := .Site.Params.profileImage | relURL }} | ||
| {{ $frontWebp := $frontImg | replaceRE `\.(png|jpg|jpeg)$` ".webp" }} | ||
| {{/* Discover coin-face images from the coin-faces folder */}} | ||
| {{ $coinFaces := slice }} | ||
| {{ range readDir "static/img/coin-faces" }} | ||
| {{ if not .IsDir }} | ||
| {{ $name := .Name }} | ||
| {{ $lower := $name | lower }} | ||
| {{ if or (strings.HasSuffix $lower ".png") (strings.HasSuffix $lower ".jpg") (strings.HasSuffix $lower ".jpeg") }} | ||
| {{ $coinFaces = $coinFaces | append (printf "/img/coin-faces/%s" $name) }} | ||
| {{ end }} | ||
| {{ end }} | ||
| {{ end }} | ||
| {{ $backImg := "" }} | ||
| {{ if gt (len $coinFaces) 0 }} | ||
| {{ $backImg = index $coinFaces 0 }} | ||
| {{ end }} | ||
| {{ $backWebp := $backImg | replaceRE `\.(png|jpg|jpeg)$` ".webp" }} | ||
| {{ $isJpeg := findRE `(?i)\.(jpg|jpeg)$` $backImg }} | ||
| <nav class="navbar navbar-expand-lg navbar-dark bg-primary fixed-top" id="sideNav"> | ||
| <a class="navbar-brand js-scroll-trigger" href="{{ "#about" | relURL }}"> | ||
| <div class="d-block d-lg-none mobile-brand"> | ||
| <div id="mobileCoin" class="mobile-brand__coin"> | ||
| <div id="mobileCoin" class="mobile-brand__coin" data-coin-images='{{ $coinFaces | jsonify }}'> | ||
| <div class="coin-side coin-front" style="background-image: image-set(url('{{ $frontWebp }}') type('image/webp'), url('{{ $frontImg }}') type('image/png'))"></div> | ||
| <div class="coin-side coin-back" style="background-image: image-set(url('/img/me-photo.webp') type('image/webp'), url('/img/me-photo.png') type('image/png'))"></div> | ||
| {{ if $isJpeg }} | ||
| <div class="coin-side coin-back" style="background-image: image-set(url('{{ $backWebp }}') type('image/webp'), url('{{ $backImg }}') type('image/jpeg'))"></div> | ||
| {{ else }} | ||
| <div class="coin-side coin-back" style="background-image: image-set(url('{{ $backWebp }}') type('image/webp'), url('{{ $backImg }}') type('image/png'))"></div> | ||
|
||
| {{ end }} | ||
| </div> | ||
| <span class="mobile-brand__name">{{ .Site.Params.firstName }} {{ .Site.Params.lastName }}</span> | ||
| </div> | ||
| <div class="d-none d-lg-block"> | ||
| <div id="profileCoin" class="mx-auto mb-2"> | ||
| <div id="profileCoin" class="mx-auto mb-2" data-coin-images='{{ $coinFaces | jsonify }}'> | ||
| <div class="coin-side coin-front" style="background-image: image-set(url('{{ $frontWebp }}') type('image/webp'), url('{{ $frontImg }}') type('image/png'))"></div> | ||
| <div class="coin-side coin-back" style="background-image: image-set(url('/img/me-photo.webp') type('image/webp'), url('/img/me-photo.png') type('image/png'))"></div> | ||
| {{ if $isJpeg }} | ||
| <div class="coin-side coin-back" style="background-image: image-set(url('{{ $backWebp }}') type('image/webp'), url('{{ $backImg }}') type('image/jpeg'))"></div> | ||
| {{ else }} | ||
| <div class="coin-side coin-back" style="background-image: image-set(url('{{ $backWebp }}') type('image/webp'), url('{{ $backImg }}') type('image/png'))"></div> | ||
|
||
| {{ end }} | ||
| </div> | ||
| </div> | ||
| </a> | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| #!/usr/bin/env bash | ||
| # ─────────────────────────────────────────────────────────── | ||
| # Validate coin-faces: ensure at least 2 headshot images | ||
| # exist so the coin flip never shows the same photo twice. | ||
| # | ||
| # Usage: | ||
| # ./scripts/validate-coin-faces.sh # exits 1 on failure | ||
| # git hook (pre-push / pre-commit) # link or call this script | ||
| # CI step # runs before hugo build | ||
| # ─────────────────────────────────────────────────────────── | ||
|
|
||
| set -euo pipefail | ||
|
|
||
| COIN_DIR="static/img/coin-faces" | ||
| MIN_REQUIRED=2 | ||
|
|
||
| # Count only source images (png/jpg/jpeg) — not webp companions. | ||
| count=$(find "$COIN_DIR" -maxdepth 1 -type f \( -iname '*.png' -o -iname '*.jpg' -o -iname '*.jpeg' \) | wc -l) | ||
| count=$((count + 0)) # trim whitespace from wc | ||
|
|
||
| if (( count < MIN_REQUIRED )); then | ||
| echo "❌ coin-faces validation failed" | ||
| echo " Found $count image(s) in $COIN_DIR (need at least $MIN_REQUIRED)." | ||
| echo " Add more headshot .png/.jpg files so the coin never repeats." | ||
| exit 1 | ||
| fi | ||
|
|
||
| echo "✅ coin-faces OK — $count image(s) found (≥ $MIN_REQUIRED required)" |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -5,14 +5,55 @@ const coin = document.getElementById("profileCoin"); | |||||
| const mobileCoin = document.getElementById("mobileCoin"); | ||||||
| const frontFace = coin?.querySelector(".coin-front"); | ||||||
| const backFace = coin?.querySelector(".coin-back"); | ||||||
| const mobileBackFace = mobileCoin?.querySelector(".coin-back"); | ||||||
|
|
||||||
| let flipping = false; | ||||||
| let showingReal = true; // front = real, back = generative | ||||||
|
|
||||||
| // Coin face image rotation — images discovered at build time by Hugo | ||||||
| const coinImages = JSON.parse(coin?.dataset?.coinImages || '[]'); | ||||||
| let currentBackImage = coinImages.length > 0 ? coinImages[0] : null; | ||||||
|
|
||||||
|
Comment on lines
+13
to
+16
|
||||||
| if (coinImages.length < 2) { | ||||||
| console.warn('[coin-flip] fewer than 2 coin-face images — back-to-back repeats are unavoidable.'); | ||||||
| } | ||||||
|
|
||||||
| function randomRange(min, max) { | ||||||
| return Math.random() * (max - min) + min; | ||||||
| } | ||||||
|
|
||||||
| /** | ||||||
| * Pick a random image from the pool, different from the current one if possible. | ||||||
| */ | ||||||
| function pickRandomImage(excludePath) { | ||||||
| if (coinImages.length === 0) return null; | ||||||
| if (coinImages.length === 1) return coinImages[0]; | ||||||
| let pick; | ||||||
| do { | ||||||
| pick = coinImages[Math.floor(Math.random() * coinImages.length)]; | ||||||
| } while (pick === excludePath); | ||||||
| return pick; | ||||||
| } | ||||||
|
|
||||||
| /** | ||||||
| * Update a coin face's background-image with WebP + fallback via image-set(). | ||||||
| */ | ||||||
| function setFaceImage(face, imgPath) { | ||||||
| if (!face || !imgPath) return; | ||||||
| const webpPath = imgPath.replace(/\.(png|jpg|jpeg)$/, '.webp'); | ||||||
|
||||||
| const webpPath = imgPath.replace(/\.(png|jpg|jpeg)$/, '.webp'); | |
| const webpPath = imgPath.replace(/\.(png|jpg|jpeg)$/i, '.webp'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When
static/img/coin-faces/is empty,$backImgstays""and$backWebpis also empty. The generated inlineimage-set(url('')...)can resolve to the current document URL and trigger an unintended request / broken background. Consider conditionally emitting thebackground-imageonly when a back image exists (or providing a safe placeholder).