feat(footer): link the build sha to its commit page

The sha now opens that commit on GitHub in a new tab, and a copy button beside it takes over copying the full value.
This commit is contained in:
tiennm99 committed 2026-09-04 11:59:48 +07:00
1 parent 7862bcec9d
commit 6251ea5f4b
1 file changed
+48 -19
+48 -19
View File
@@ -2,14 +2,18 @@
import { useEffect, useRef, useState } from 'react';
// Where the sha links to. Hard-coded rather than derived from a git remote:
// the remote is a build-machine detail, the public repo is a product fact.
const REPO_URL = 'https://github.com/tiennm99/vngeoguessr';
// Footer credit plus build stamp, rendered on every page: who made this, and
// which commit is this deployment?
//
// An in-flow strip at the bottom of the body's sticky-footer column, not a
// fixed overlay: it owns its own row (and the home-indicator safe area), so
// it never overlaps the game's panorama, map, or action bar, and no layer
// needs a z-index to win over it. Clicking the sha copies the FULL value
// (the label shows the short form).
// needs a z-index to win over it. The sha label opens that commit on GitHub;
// the button beside it copies the FULL sha (the label shows the short form).
//
// Height is --footer-h (36px) rather than the 44px touch floor: this strip is
// a band taken out of the game's non-scrolling surface, where in landscape
@@ -17,6 +21,7 @@ import { useEffect, useRef, useState } from 'react';
// minimum, and both targets stay wide.
export default function DebugFooter() {
const sha = process.env.NEXT_PUBLIC_COMMIT_SHA || 'unknown';
const hasSha = sha !== 'unknown';
const [copied, setCopied] = useState(false);
const resetTimerRef = useRef(null);
@@ -35,34 +40,58 @@ export default function DebugFooter() {
}
};
// Every footer target shares this: --footer-h tall so the tap area comes
// from padding rather than the deliberately tiny label. Horizontal padding
// is per-target, so the sha and its copy button can sit tight together
// while each still spans the full strip height.
const targetClass =
'flex min-h-(--footer-h) items-center justify-center rounded-md text-[11px] leading-none text-muted-foreground transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-[3px] focus-visible:ring-ring';
return (
<footer className="flex shrink-0 items-center justify-center gap-2 border-t border-border bg-card pb-[env(safe-area-inset-bottom)]">
<footer className="flex shrink-0 items-center justify-center gap-1 border-t border-border bg-card pb-[env(safe-area-inset-bottom)]">
<a
href="https://miti99.com"
target="_blank"
rel="noopener noreferrer"
className="flex min-h-(--footer-h) items-center rounded-md px-3 text-[11px] leading-none text-muted-foreground transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-[3px] focus-visible:ring-ring"
className={`${targetClass} px-3`}
>
Made by <span className="underline underline-offset-2 mx-1">miti99</span> with{' '}
<span aria-hidden="true" className="ml-1">❤️</span>
<span className="sr-only">love</span>
</a>
<span className="text-[11px] leading-none text-muted-foreground" aria-hidden="true">·</span>
{/* The visual is deliberately tiny, so the tap target comes from
padding: --footer-h tall, well past the label's own box. min-w in ch
keeps the width stable when the 7-char sha swaps for the 7-char
'copied!', so the click never shifts the thing being clicked.
aria-live announces that swap to screen readers. */}
<button
type="button"
onClick={handleCopy}
aria-label="Copy build commit"
aria-live="polite"
title={sha}
className="flex min-h-(--footer-h) min-w-[9ch] cursor-pointer select-none items-center justify-center rounded-md px-3 font-mono text-[11px] leading-none text-muted-foreground underline underline-offset-2 transition-colors hover:text-foreground active:text-foreground focus-visible:outline-none focus-visible:ring-[3px] focus-visible:ring-ring"
>
{copied ? 'copied!' : sha.slice(0, 7)}
</button>
{/* The sha and its copy button read as one control, so they sit in a
gapless pair rather than as two separate items in the footer row. */}
<span className="flex items-center">
{hasSha ? (
<a
href={`${REPO_URL}/commit/${sha}`}
target="_blank"
rel="noopener noreferrer"
title={sha}
aria-label={`View build commit ${sha} on GitHub`}
className={`${targetClass} pl-2 pr-1 font-mono underline underline-offset-2`}
>
{sha.slice(0, 7)}
</a>
) : (
<span className={`${targetClass} px-2 font-mono`}>{sha}</span>
)}
{/* aria-live announces the ✅ swap; the emoji itself is decorative, so
the label carries the meaning either way. */}
{hasSha && (
<button
type="button"
onClick={handleCopy}
aria-label={copied ? 'Build commit copied' : 'Copy build commit'}
aria-live="polite"
title="Copy full commit sha"
className={`${targetClass} cursor-pointer select-none pl-1 pr-2 active:text-foreground`}
>
<span aria-hidden="true">{copied ? '✅' : '🗐'}</span>
</button>
)}
</span>
</footer>
);
}