mirror of
https://github.com/okieselbach/AutopilotMonitor.git
synced 2026-10-24 12:42:38 +00:00
Landing shows the GitHub star count and a customer quote
The navbar gets a "Star" pill with the repository's star count from sm up; phones keep the bare GitHub mark. A new section before the closing CTA shows one customer quote with a star ask underneath. The footer's "Star us on GitHub" line is gone, so the page asks twice, not three times. The count is read once per web deploy (deploy-web.yml, own step, job token only there) and inlined by next build as NEXT_PUBLIC_GITHUB_STARS, so visitors' browsers never contact GitHub and the CSP stays as it is. Without a value the pill renders without a counter (D-303). The desktop links, bell and sign-in now start at lg instead of md: the bar already overflowed between 768 and ~1000 px and the pill widened it further. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -78,6 +78,23 @@ jobs:
|
||||
working-directory: ${{ env.WEB_DIR }}
|
||||
run: npm ci
|
||||
|
||||
# Star counter for the GitHub star link (utils/githubStars.ts). Baked into the static HTML
|
||||
# so visitors' browsers never call GitHub; stale until the next web deploy. The token stays
|
||||
# in this step, never in the build. Fail-soft: without a number the link has no counter.
|
||||
- name: Read GitHub star count (fail-soft)
|
||||
id: stars
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
REPO: ${{ github.repository }}
|
||||
run: |
|
||||
count=$(gh api "repos/$REPO" --jq .stargazers_count 2>/dev/null || true)
|
||||
if [[ "$count" =~ ^[0-9]+$ ]]; then
|
||||
echo "count=$count" >> "$GITHUB_OUTPUT"
|
||||
echo "GitHub stars: $count"
|
||||
else
|
||||
echo "::warning::GitHub star count unavailable - the star link renders without a counter"
|
||||
fi
|
||||
|
||||
# next.config output:'export' -- `next build` writes the static site to out/
|
||||
# (prebuild: sync-error-codes, generate-lastmod, generate-version, whats-new).
|
||||
- name: Build
|
||||
@@ -98,6 +115,8 @@ jobs:
|
||||
GITHUB_SHA: ${{ github.sha }}
|
||||
# Docs checkout for public/whats-new.json; the prebuild fails in CI without it.
|
||||
WHATS_NEW_DOCS_DIR: ${{ github.workspace }}/whats-new-docs
|
||||
# Star counter from the step above; empty when the lookup failed.
|
||||
NEXT_PUBLIC_GITHUB_STARS: ${{ steps.stars.outputs.count }}
|
||||
run: |
|
||||
npm run build
|
||||
test -f out/version.json || { echo "out/version.json missing - export did not run"; exit 1; }
|
||||
|
||||
@@ -49,6 +49,8 @@ html {
|
||||
--lp-warn: #d97706;
|
||||
--lp-warn-soft: rgba(217, 119, 6, 0.1);
|
||||
--lp-danger: #dc2626;
|
||||
/* GitHub star glyph only (GitHubStarLink) — never text or a fill */
|
||||
--lp-star: #eab308;
|
||||
/* Terminal is dark in both themes */
|
||||
--lp-term-bg: #0d1424;
|
||||
--lp-term-line: #22304a;
|
||||
@@ -73,6 +75,7 @@ html {
|
||||
--lp-warn: #f5a623;
|
||||
--lp-warn-soft: rgba(245, 166, 35, 0.12);
|
||||
--lp-danger: #f87171;
|
||||
--lp-star: #facc15;
|
||||
}
|
||||
|
||||
body {
|
||||
@@ -762,6 +765,7 @@ html.auth-pending .landing-v2::after {
|
||||
--lp-warn: #d97706;
|
||||
--lp-warn-soft: rgba(217, 119, 6, 0.1);
|
||||
--lp-danger: #dc2626;
|
||||
--lp-star: #eab308;
|
||||
}
|
||||
|
||||
/* Neutralize the global .dark utility overrides inside the light-only
|
||||
|
||||
@@ -7,6 +7,7 @@ import { Story } from "../components/landing/Story";
|
||||
import { CapabilitiesStrip } from "../components/landing/CapabilitiesStrip";
|
||||
import { Comparison } from "../components/landing/Comparison";
|
||||
import { HowItWorks } from "../components/landing/HowItWorks";
|
||||
import { Testimonials } from "../components/landing/Testimonials";
|
||||
import { FinalCta } from "../components/landing/FinalCta";
|
||||
import { SiteFooter } from "../components/SiteFooter";
|
||||
|
||||
@@ -29,6 +30,7 @@ export default function LandingPage() {
|
||||
<CapabilitiesStrip />
|
||||
<Comparison />
|
||||
<HowItWorks />
|
||||
<Testimonials />
|
||||
<FinalCta />
|
||||
<SiteFooter />
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
/** The GitHub mark (Octicon), inherits the text color. */
|
||||
export function GitHubIcon({ className = "w-4 h-4" }: { className?: string }) {
|
||||
return (
|
||||
<svg className={className} viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
|
||||
<path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0024 12c0-6.63-5.37-12-12-12z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { GITHUB_REPO_URL, GITHUB_STARS, formatStarCount } from "@/utils/githubStars";
|
||||
import { GitHubIcon } from "./GitHubIcon";
|
||||
|
||||
/**
|
||||
* "Star us" pill for the public surface: [GitHub mark] label │ ★ count.
|
||||
* The counter segment only renders when the deploy baked a star count in (utils/githubStars.ts).
|
||||
* No hooks, so server components (Testimonials) and client components (LandingNavbar) share it.
|
||||
*/
|
||||
export function GitHubStarLink({ label }: { label: string }) {
|
||||
return (
|
||||
<a
|
||||
href={GITHUB_REPO_URL}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label={
|
||||
GITHUB_STARS !== null
|
||||
? `Star Autopilot Monitor on GitHub (${GITHUB_STARS} stars)`
|
||||
: "Star Autopilot Monitor on GitHub"
|
||||
}
|
||||
className="inline-flex items-stretch h-8 rounded-lg border border-[var(--lp-line)] bg-[var(--lp-surface)] text-[13px] font-medium text-[var(--lp-ink-soft)] hover:text-[var(--lp-ink)] hover:border-[var(--lp-ink-faint)] transition-colors overflow-hidden whitespace-nowrap"
|
||||
>
|
||||
<span className="inline-flex items-center gap-1.5 px-2.5">
|
||||
<GitHubIcon className="w-[15px] h-[15px]" />
|
||||
{label}
|
||||
</span>
|
||||
{GITHUB_STARS !== null && (
|
||||
<span className="inline-flex items-center gap-1 px-2.5 border-l border-[var(--lp-line)] bg-[var(--lp-surface-2)] font-semibold text-[var(--lp-ink)]">
|
||||
<svg className="w-3 h-3 text-[var(--lp-star)]" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
|
||||
<path d="M12 2.5l2.94 5.96 6.56.95-4.75 4.63 1.12 6.54L12 17.5l-5.87 3.08 1.12-6.54L2.5 9.41l6.56-.95z" />
|
||||
</svg>
|
||||
{formatStarCount(GITHUB_STARS)}
|
||||
</span>
|
||||
)}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
import { DOCS_URL } from "@/utils/config";
|
||||
import { GITHUB_REPO_URL } from "@/utils/githubStars";
|
||||
import { BrandMark } from "./BrandMark";
|
||||
import { GitHubIcon } from "./GitHubIcon";
|
||||
|
||||
const LINK_COLUMNS = [
|
||||
{
|
||||
@@ -17,7 +19,7 @@ const LINK_COLUMNS = [
|
||||
{ label: "Documentation", href: DOCS_URL, external: true },
|
||||
{ label: "Help & Support", href: "/help" },
|
||||
{ label: "Feedback", href: "https://github.com/okieselbach/AutopilotMonitor/issues", external: true },
|
||||
{ label: "GitHub", href: "https://github.com/okieselbach/AutopilotMonitor", external: true },
|
||||
{ label: "GitHub", href: GITHUB_REPO_URL, external: true },
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -71,15 +73,13 @@ export function SiteFooter() {
|
||||
</svg>
|
||||
</a>
|
||||
<a
|
||||
href="https://github.com/okieselbach/AutopilotMonitor"
|
||||
href={GITHUB_REPO_URL}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-[var(--lp-ink-faint)] hover:text-[var(--lp-ink)] transition-colors"
|
||||
title="GitHub"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0024 12c0-6.63-5.37-12-12-12z" />
|
||||
</svg>
|
||||
<GitHubIcon className="w-4 h-4" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -110,22 +110,11 @@ export function SiteFooter() {
|
||||
</div>
|
||||
|
||||
{/* Bottom bar */}
|
||||
<div className="mt-10 pt-5 border-t border-[var(--lp-line-soft)] flex flex-col sm:flex-row items-center justify-between gap-2">
|
||||
<div className="mt-10 pt-5 border-t border-[var(--lp-line-soft)] text-center sm:text-left">
|
||||
<p className="text-[11px] text-[var(--lp-ink-faint)]">
|
||||
© 2026 Autopilot Monitor. Built with ❤️ by Oliver Kieselbach.{" "}
|
||||
<span className="inline-block">Hosted on Azure by glueckkanja AG.</span>
|
||||
</p>
|
||||
<p className="text-[11px] text-[var(--lp-ink-faint)]">
|
||||
Open source. Star us on{" "}
|
||||
<a
|
||||
href="https://github.com/okieselbach/AutopilotMonitor"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-[var(--lp-ink-soft)] hover:text-[var(--lp-accent-ink)] transition-colors"
|
||||
>
|
||||
GitHub
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
@@ -6,6 +6,9 @@ import { useAuth } from "../../contexts/AuthContext";
|
||||
import { getPortalLoginUrl, shouldCrossOriginToPortal } from "../../lib/hostRouting";
|
||||
import { DOCS_URL } from "@/utils/config";
|
||||
import { BrandMark } from "../BrandMark";
|
||||
import { GitHubIcon } from "../GitHubIcon";
|
||||
import { GitHubStarLink } from "../GitHubStarLink";
|
||||
import { GITHUB_REPO_URL } from "@/utils/githubStars";
|
||||
import { useWhatsNew } from "@/hooks/useWhatsNew";
|
||||
|
||||
// Root-anchored (/#…) so the links also work from subpages
|
||||
@@ -18,14 +21,6 @@ const NAV_LINKS = [
|
||||
{ href: DOCS_URL, label: "Docs", external: true },
|
||||
];
|
||||
|
||||
function GitHubIcon({ className }: { className?: string }) {
|
||||
return (
|
||||
<svg className={className} viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0024 12c0-6.63-5.37-12-12-12z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function BellIcon({ className }: { className?: string }) {
|
||||
// Ringing bell: the public site announces "there is news" without claiming a count —
|
||||
// anonymous visitors have no seen mark, so a counter would read 9+ for everyone.
|
||||
@@ -78,16 +73,18 @@ export function LandingNavbar() {
|
||||
// see components/NavLink.tsx.)
|
||||
return (
|
||||
<nav className="sticky top-0 z-40 bg-[var(--lp-nav)] backdrop-blur-xl border-b border-[var(--lp-line-soft)]">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 h-16 flex items-center gap-8">
|
||||
{/* Measured widths: the full desktop bar (links, star link, bell, sign-in, CTA) needs
|
||||
~1000px, so it starts at lg; below that the burger menu carries links and sign-in. */}
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 h-16 flex items-center gap-4 lg:gap-8">
|
||||
<Link href="/" prefetch={false} className="flex items-center gap-2.5 shrink-0">
|
||||
<BrandMark className="w-6 h-6" />
|
||||
{/* Wordmark needs ~360px alongside CTA + burger; mark alone below that */}
|
||||
<span className="hidden min-[360px]:block text-[15px] font-bold tracking-tight text-[var(--lp-ink)] whitespace-nowrap">
|
||||
{/* Wordmark needs ~390px alongside GitHub mark + CTA + burger; mark alone below that */}
|
||||
<span className="hidden min-[390px]:block text-[15px] font-bold tracking-tight text-[var(--lp-ink)] whitespace-nowrap">
|
||||
Autopilot Monitor
|
||||
</span>
|
||||
</Link>
|
||||
|
||||
<div className="hidden md:flex items-center gap-1">
|
||||
<div className="hidden lg:flex items-center gap-1">
|
||||
{NAV_LINKS.map(link =>
|
||||
link.external ? (
|
||||
<a
|
||||
@@ -111,20 +108,25 @@ export function LandingNavbar() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="ml-auto flex items-center gap-1.5 sm:gap-2 shrink-0">
|
||||
<div className="ml-auto flex items-center gap-1 sm:gap-2 shrink-0">
|
||||
{/* Phones get the bare GitHub mark; the star pill with its counter starts at sm */}
|
||||
<a
|
||||
href="https://github.com/okieselbach/AutopilotMonitor"
|
||||
href={GITHUB_REPO_URL}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="hidden md:block p-2 rounded-lg text-[var(--lp-ink-faint)] hover:text-[var(--lp-ink)] hover:bg-[var(--lp-surface-2)] transition-colors"
|
||||
className="sm:hidden p-1.5 rounded-lg text-[var(--lp-ink-faint)] hover:text-[var(--lp-ink)] hover:bg-[var(--lp-surface-2)] transition-colors"
|
||||
title="GitHub"
|
||||
aria-label="GitHub"
|
||||
>
|
||||
<GitHubIcon className="w-4 h-4" />
|
||||
</a>
|
||||
<div className="hidden sm:flex">
|
||||
<GitHubStarLink label="Star" />
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => whatsNew.open("landing")}
|
||||
className="hidden md:block p-2 rounded-lg text-[var(--lp-ink-faint)] hover:text-[var(--lp-ink)] hover:bg-[var(--lp-surface-2)] transition-colors"
|
||||
className="hidden lg:block p-2 rounded-lg text-[var(--lp-ink-faint)] hover:text-[var(--lp-ink)] hover:bg-[var(--lp-surface-2)] transition-colors"
|
||||
title="What's new"
|
||||
aria-label="What's new"
|
||||
>
|
||||
@@ -132,7 +134,7 @@ export function LandingNavbar() {
|
||||
</button>
|
||||
<button
|
||||
onClick={handleSignIn}
|
||||
className="hidden md:block px-3 py-2 text-sm font-semibold text-[var(--lp-ink)] hover:text-[var(--lp-accent-ink)] transition-colors"
|
||||
className="hidden lg:block px-3 py-2 text-sm font-semibold text-[var(--lp-ink)] hover:text-[var(--lp-accent-ink)] transition-colors"
|
||||
>
|
||||
Sign in
|
||||
</button>
|
||||
@@ -145,7 +147,7 @@ export function LandingNavbar() {
|
||||
</Link>
|
||||
<button
|
||||
onClick={() => setMenuOpen(open => !open)}
|
||||
className="md:hidden p-2 -mr-2 rounded-lg text-[var(--lp-ink)] hover:bg-[var(--lp-surface-2)] transition-colors"
|
||||
className="lg:hidden p-2 -mr-2 rounded-lg text-[var(--lp-ink)] hover:bg-[var(--lp-surface-2)] transition-colors"
|
||||
aria-expanded={menuOpen}
|
||||
aria-controls="landing-mobile-menu"
|
||||
aria-label={menuOpen ? "Close menu" : "Open menu"}
|
||||
@@ -168,13 +170,13 @@ export function LandingNavbar() {
|
||||
{/* nav's backdrop-blur makes it the containing block, so this stays
|
||||
absolute (not fixed) and stretches a viewport height below the bar */}
|
||||
<div
|
||||
className="md:hidden absolute top-full left-0 right-0 h-screen bg-black/20"
|
||||
className="lg:hidden absolute top-full left-0 right-0 h-screen bg-black/20"
|
||||
onClick={() => setMenuOpen(false)}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<div
|
||||
id="landing-mobile-menu"
|
||||
className="md:hidden absolute top-full left-0 right-0 bg-[var(--lp-surface)] border-b border-[var(--lp-line-soft)] shadow-lg"
|
||||
className="lg:hidden absolute top-full left-0 right-0 bg-[var(--lp-surface)] border-b border-[var(--lp-line-soft)] shadow-lg"
|
||||
>
|
||||
<div className="px-6 py-4 flex flex-col gap-1">
|
||||
{NAV_LINKS.map(link => (
|
||||
@@ -189,7 +191,7 @@ export function LandingNavbar() {
|
||||
</a>
|
||||
))}
|
||||
<a
|
||||
href="https://github.com/okieselbach/AutopilotMonitor"
|
||||
href={GITHUB_REPO_URL}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
onClick={() => setMenuOpen(false)}
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import { GitHubStarLink } from "../GitHubStarLink";
|
||||
import { Reveal } from "./Reveal";
|
||||
|
||||
/**
|
||||
* Customer voice before the closing CTA: one quote, shared with permission and attributed by
|
||||
* role only. More voices need a different layout (a grid of bubbles), not copies of this one.
|
||||
*/
|
||||
export function Testimonials() {
|
||||
return (
|
||||
<section className="py-20 sm:py-24 px-6 border-t border-[var(--lp-line-soft)] bg-[var(--lp-surface)]">
|
||||
<div className="max-w-7xl mx-auto grid lg:grid-cols-12 gap-10 lg:gap-6">
|
||||
<Reveal className="lg:col-span-4">
|
||||
<p className="text-xs font-semibold uppercase tracking-[0.22em] text-[var(--lp-accent-ink)]">From the field</p>
|
||||
<h2 className="mt-3 text-3xl sm:text-4xl font-bold tracking-tight text-[var(--lp-ink)] text-balance">
|
||||
What endpoint teams see on day one.
|
||||
</h2>
|
||||
</Reveal>
|
||||
|
||||
<Reveal className="lg:col-span-8" delayMs={100}>
|
||||
<figure>
|
||||
<div className="relative rounded-2xl border border-[var(--lp-line)] bg-[var(--lp-bg)] px-6 pt-7 pb-8 sm:px-10 sm:pt-9 sm:pb-10">
|
||||
<span aria-hidden="true" className="block h-6 sm:h-8 font-serif text-6xl sm:text-7xl leading-none text-[var(--lp-accent)]">
|
||||
“
|
||||
</span>
|
||||
<blockquote className="mt-3 text-2xl sm:text-3xl font-semibold tracking-tight leading-snug text-[var(--lp-ink)] text-balance">
|
||||
Wow — for the first time I can actually see all of our enrollments running out there.
|
||||
</blockquote>
|
||||
{/* Speech-bubble tail: the upper half of the rotated square covers the bubble border */}
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="absolute left-10 -bottom-[9px] w-4 h-4 rotate-45 bg-[var(--lp-bg)] border-r border-b border-[var(--lp-line)]"
|
||||
/>
|
||||
</div>
|
||||
<figcaption className="mt-5 flex items-center gap-3.5 pl-6 sm:pl-8">
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="inline-flex w-11 h-11 shrink-0 items-center justify-center rounded-full bg-[var(--lp-accent-soft)] text-[var(--lp-accent-ink)]"
|
||||
>
|
||||
<svg className="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2" />
|
||||
<circle cx="12" cy="7" r="4" />
|
||||
</svg>
|
||||
</span>
|
||||
<span className="text-[15px] font-semibold text-[var(--lp-ink)]">Head of Endpoint Management</span>
|
||||
</figcaption>
|
||||
</figure>
|
||||
|
||||
<div className="mt-8 flex flex-wrap items-center gap-x-4 gap-y-3">
|
||||
<p className="text-[15px] text-[var(--lp-ink-soft)]">Seeing the same in your tenant? Leave a star.</p>
|
||||
<GitHubStarLink label="Star on GitHub" />
|
||||
</div>
|
||||
</Reveal>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { formatStarCount, parseStarCount } from "../githubStars";
|
||||
|
||||
describe("parseStarCount", () => {
|
||||
it("parses a plain integer", () => {
|
||||
expect(parseStarCount("45")).toBe(45);
|
||||
});
|
||||
|
||||
it("accepts zero", () => {
|
||||
expect(parseStarCount("0")).toBe(0);
|
||||
});
|
||||
|
||||
it("trims surrounding whitespace", () => {
|
||||
expect(parseStarCount(" 276\n")).toBe(276);
|
||||
});
|
||||
|
||||
it("returns null when the env value is missing or empty", () => {
|
||||
expect(parseStarCount(undefined)).toBeNull();
|
||||
expect(parseStarCount("")).toBeNull();
|
||||
});
|
||||
|
||||
it("rejects anything that is not a plain non-negative integer", () => {
|
||||
expect(parseStarCount("null")).toBeNull();
|
||||
expect(parseStarCount("-1")).toBeNull();
|
||||
expect(parseStarCount("4.5")).toBeNull();
|
||||
expect(parseStarCount("1e3")).toBeNull();
|
||||
expect(parseStarCount("45 stars")).toBeNull();
|
||||
});
|
||||
|
||||
it("rejects values beyond the safe integer range", () => {
|
||||
expect(parseStarCount("99999999999999999999")).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("formatStarCount", () => {
|
||||
it("shows counts below 1000 verbatim", () => {
|
||||
expect(formatStarCount(0)).toBe("0");
|
||||
expect(formatStarCount(45)).toBe("45");
|
||||
expect(formatStarCount(999)).toBe("999");
|
||||
});
|
||||
|
||||
it("abbreviates thousands with one decimal", () => {
|
||||
expect(formatStarCount(1000)).toBe("1k");
|
||||
expect(formatStarCount(1234)).toBe("1.2k");
|
||||
expect(formatStarCount(12000)).toBe("12k");
|
||||
expect(formatStarCount(12345)).toBe("12.3k");
|
||||
});
|
||||
|
||||
it("truncates rather than rounding up", () => {
|
||||
expect(formatStarCount(1999)).toBe("1.9k");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* GitHub star count for the public surface (navbar, testimonials, footer).
|
||||
*
|
||||
* The count is baked in at deploy time: deploy-web.yml reads it with the job token and passes
|
||||
* it to `next build` as NEXT_PUBLIC_GITHUB_STARS, which Next inlines into the static HTML.
|
||||
* Visitors' browsers never call GitHub — the CSP would block it, and it would be a third-party
|
||||
* connection the privacy page does not list. CI, local builds and a failed lookup have no
|
||||
* value; the star link then renders without the counter.
|
||||
*/
|
||||
|
||||
export const GITHUB_REPO_URL = "https://github.com/okieselbach/AutopilotMonitor";
|
||||
|
||||
/** Parses the build-time env value. Anything but a plain non-negative integer yields null. */
|
||||
export function parseStarCount(raw: string | undefined): number | null {
|
||||
const value = raw?.trim();
|
||||
if (!value || !/^\d+$/.test(value)) return null;
|
||||
const count = Number(value);
|
||||
return Number.isSafeInteger(count) ? count : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compact display: 45 → "45", 1234 → "1.2k", 12000 → "12k".
|
||||
* Truncates instead of rounding so the label never overstates the count.
|
||||
*/
|
||||
export function formatStarCount(count: number): string {
|
||||
if (count < 1000) return String(count);
|
||||
const thousands = Math.floor(count / 100) / 10;
|
||||
return `${thousands}k`;
|
||||
}
|
||||
|
||||
export const GITHUB_STARS = parseStarCount(process.env.NEXT_PUBLIC_GITHUB_STARS);
|
||||
Reference in New Issue
Block a user