feat: add a Keep it free sponsor button to the top nav (#427)

Adds a prominent Keep it free sponsor button to the top nav, linking to https://github.com/sponsors/snapotter-hq. Solid orange pill on desktop (left of the avatar), orange heart icon on mobile. Opens in a new tab with rel=noopener noreferrer, so no referrer or user data leaks, and it adds no passive network activity (offline-mode compatible). Fires an opt-in, property-less sponsor_clicked analytics event. Adds sidebar.sponsor and a11y.sponsorLink across all 21 locales.

Claude-Session: https://claude.ai/code/session_01DnYLLA5z4Uf1GDeEPENVgr
This commit is contained in:
SnapOtter
2026-07-04 08:48:16 +00:00
committed by GitHub
parent 3ae48cc76a
commit 7b04317ed2
25 changed files with 115 additions and 2 deletions
@@ -1,8 +1,10 @@
import { ANALYTICS_EVENTS } from "@snapotter/shared";
import {
ChevronLeft,
ChevronRight,
FolderOpen,
Globe,
Heart,
HelpCircle,
LayoutGrid,
MessageSquare,
@@ -128,6 +130,8 @@ export function TopNav({
</button>
)}
<SponsorButton isDark={isDark} compact />
<button
type="button"
onClick={onHelpClick}
@@ -280,12 +284,58 @@ export function TopNav({
<HelpCircle className="h-4 w-4" />
</button>
<SponsorButton isDark={isDark} />
{!isMobile && <AvatarDropdown onSettingsClick={onSettingsClick} variant={variant} />}
</div>
</header>
);
}
const SPONSOR_URL = "https://github.com/sponsors/snapotter-hq";
function SponsorButton({ isDark, compact = false }: { isDark: boolean; compact?: boolean }) {
const { t } = useTranslation();
const handleClick = () => {
import("@/lib/analytics").then(({ track }) => {
track(ANALYTICS_EVENTS.SPONSOR_CLICKED);
});
};
if (compact) {
return (
<a
href={SPONSOR_URL}
target="_blank"
rel="noopener noreferrer"
onClick={handleClick}
aria-label={t.a11y.sponsorLink}
title={t.a11y.sponsorLink}
className={cn(
"p-1.5 rounded-md transition-colors text-primary",
isDark ? "hover:bg-[#333]" : "hover:bg-muted",
)}
>
<Heart className="h-4 w-4 fill-current" />
</a>
);
}
return (
<a
href={SPONSOR_URL}
target="_blank"
rel="noopener noreferrer"
onClick={handleClick}
className="ms-1 inline-flex items-center gap-1.5 rounded-md bg-primary px-3 py-1.5 text-sm font-medium text-[#1a1814] transition-opacity hover:opacity-90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-1"
>
<Heart className="h-3.5 w-3.5 fill-current" aria-hidden="true" />
{t.sidebar.sponsor}
</a>
);
}
function ThemeToggle({ isDark }: { isDark: boolean }) {
const { resolvedTheme, toggleTheme } = useTheme();
return (