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 (
+1
View File
@@ -12,6 +12,7 @@ export const ANALYTICS_EVENTS = {
AI_BUNDLE_PROMPTED: "ai_bundle_prompted",
BATCH_PROCESSED: "batch_processed",
FEEDBACK_SUBMITTED: "feedback_submitted",
SPONSOR_CLICKED: "sponsor_clicked",
} as const;
export type AnalyticsEvent = (typeof ANALYTICS_EVENTS)[keyof typeof ANALYTICS_EVENTS];
+2
View File
@@ -3819,6 +3819,7 @@ export const ar: TranslationKeys = {
connectionError: "خطأ في الاتصال",
},
sidebar: {
sponsor: "أبقِه مجانيًا",
tools: "الأدوات",
grid: "شبكة",
automate: "أتمتة",
@@ -3838,6 +3839,7 @@ export const ar: TranslationKeys = {
mobileNavSettings: "الإعدادات",
},
a11y: {
sponsorLink: "ادعم SnapOtter على GitHub Sponsors",
skipToContent: "تخطي إلى المحتوى الرئيسي",
openSidebar: "فتح الشريط الجانبي",
closeSidebar: "إغلاق الشريط الجانبي",
+2
View File
@@ -3866,6 +3866,7 @@ export const de: TranslationKeys = {
connectionError: "Verbindungsfehler",
},
sidebar: {
sponsor: "Kostenlos halten",
tools: "Werkzeuge",
grid: "Raster",
automate: "Automatisieren",
@@ -3885,6 +3886,7 @@ export const de: TranslationKeys = {
mobileNavSettings: "Einstellungen",
},
a11y: {
sponsorLink: "SnapOtter auf GitHub Sponsors unterstützen",
skipToContent: "Zum Hauptinhalt springen",
openSidebar: "Seitenleiste öffnen",
closeSidebar: "Seitenleiste schließen",
+2
View File
@@ -3795,6 +3795,7 @@ export const en = {
automate: "Automate",
editor: "Editor",
files: "Files",
sponsor: "Keep it free",
help: "Help",
settings: "Settings",
},
@@ -3870,6 +3871,7 @@ export const en = {
helpDialog: "Help",
navigationMenu: "Navigation",
homeLink: "SnapOtter home",
sponsorLink: "Support SnapOtter on GitHub Sponsors",
},
pipelineTemplates: {
"web-optimize": {
+2
View File
@@ -3844,6 +3844,7 @@ export const es: TranslationKeys = {
connectionError: "Error de conexión",
},
sidebar: {
sponsor: "Mantenlo gratis",
tools: "Herramientas",
grid: "Cuadrícula",
automate: "Automatizar",
@@ -3863,6 +3864,7 @@ export const es: TranslationKeys = {
mobileNavSettings: "Configuración",
},
a11y: {
sponsorLink: "Apoya a SnapOtter en GitHub Sponsors",
skipToContent: "Ir al contenido principal",
openSidebar: "Abrir barra lateral",
closeSidebar: "Cerrar barra lateral",
+2
View File
@@ -3868,6 +3868,7 @@ export const fr: TranslationKeys = {
connectionError: "Erreur de connexion",
},
sidebar: {
sponsor: "Gardons-le gratuit",
tools: "Outils",
grid: "Grille",
automate: "Automatiser",
@@ -3887,6 +3888,7 @@ export const fr: TranslationKeys = {
mobileNavSettings: "Paramètres",
},
a11y: {
sponsorLink: "Soutenir SnapOtter sur GitHub Sponsors",
skipToContent: "Aller au contenu principal",
openSidebar: "Ouvrir le panneau latéral",
closeSidebar: "Fermer le panneau latéral",
+2
View File
@@ -3649,6 +3649,7 @@ export const hi: TranslationKeys = {
connectionError: "कनेक्शन त्रुटि",
},
sidebar: {
sponsor: "इसे मुफ़्त रखें",
tools: "टूल्स",
grid: "ग्रिड",
automate: "ऑटोमेट",
@@ -3668,6 +3669,7 @@ export const hi: TranslationKeys = {
mobileNavSettings: "सेटिंग्स",
},
a11y: {
sponsorLink: "GitHub Sponsors पर SnapOtter का समर्थन करें",
skipToContent: "मुख्य सामग्री पर जाएं",
openSidebar: "साइडबार खोलें",
closeSidebar: "साइडबार बंद करें",
+2
View File
@@ -3844,6 +3844,7 @@ export const id: TranslationKeys = {
connectionError: "Kesalahan koneksi",
},
sidebar: {
sponsor: "Jaga tetap gratis",
tools: "Alat",
grid: "Kisi",
automate: "Otomasi",
@@ -3863,6 +3864,7 @@ export const id: TranslationKeys = {
mobileNavSettings: "Pengaturan",
},
a11y: {
sponsorLink: "Dukung SnapOtter di GitHub Sponsors",
skipToContent: "Lewati ke konten utama",
openSidebar: "Buka bilah sisi",
closeSidebar: "Tutup bilah sisi",
+2
View File
@@ -3859,6 +3859,7 @@ export const it: TranslationKeys = {
connectionError: "Errore di connessione",
},
sidebar: {
sponsor: "Mantienilo gratis",
tools: "Strumenti",
grid: "Griglia",
automate: "Automatizza",
@@ -3878,6 +3879,7 @@ export const it: TranslationKeys = {
mobileNavSettings: "Impostazioni",
},
a11y: {
sponsorLink: "Sostieni SnapOtter su GitHub Sponsors",
skipToContent: "Vai al contenuto principale",
openSidebar: "Apri barra laterale",
closeSidebar: "Chiudi barra laterale",
+2
View File
@@ -3795,6 +3795,7 @@ export const ja: TranslationKeys = {
connectionError: "接続エラー",
},
sidebar: {
sponsor: "無料を守る",
tools: "ツール",
grid: "グリッド",
automate: "自動化",
@@ -3814,6 +3815,7 @@ export const ja: TranslationKeys = {
mobileNavSettings: "設定",
},
a11y: {
sponsorLink: "GitHub Sponsors で SnapOtter を支援する",
skipToContent: "メインコンテンツへスキップ",
openSidebar: "サイドバーを開く",
closeSidebar: "サイドバーを閉じる",
+2
View File
@@ -3776,6 +3776,7 @@ export const ko: TranslationKeys = {
connectionError: "연결 오류",
},
sidebar: {
sponsor: "무료로 유지하기",
tools: "도구",
grid: "그리드",
automate: "자동화",
@@ -3795,6 +3796,7 @@ export const ko: TranslationKeys = {
mobileNavSettings: "설정",
},
a11y: {
sponsorLink: "GitHub Sponsors에서 SnapOtter 후원하기",
skipToContent: "본문으로 건너뛰기",
openSidebar: "사이드바 열기",
closeSidebar: "사이드바 닫기",
+2
View File
@@ -3853,6 +3853,7 @@ export const nl: TranslationKeys = {
connectionError: "Verbindingsfout",
},
sidebar: {
sponsor: "Houd het gratis",
tools: "Tools",
grid: "Raster",
automate: "Automatiseren",
@@ -3872,6 +3873,7 @@ export const nl: TranslationKeys = {
mobileNavSettings: "Instellingen",
},
a11y: {
sponsorLink: "Steun SnapOtter op GitHub Sponsors",
skipToContent: "Ga naar hoofdinhoud",
openSidebar: "Zijbalk openen",
closeSidebar: "Zijbalk sluiten",
+2
View File
@@ -3859,6 +3859,7 @@ export const pl: TranslationKeys = {
connectionError: "Błąd połączenia",
},
sidebar: {
sponsor: "Zachowaj darmowość",
tools: "Narzędzia",
grid: "Siatka",
automate: "Automatyzacja",
@@ -3878,6 +3879,7 @@ export const pl: TranslationKeys = {
mobileNavSettings: "Ustawienia",
},
a11y: {
sponsorLink: "Wesprzyj SnapOtter na GitHub Sponsors",
skipToContent: "Przejdź do treści",
openSidebar: "Otwórz panel boczny",
closeSidebar: "Zamknij panel boczny",
+2
View File
@@ -3851,6 +3851,7 @@ export const ptBR: TranslationKeys = {
connectionError: "Erro de conexão",
},
sidebar: {
sponsor: "Mantenha gratuito",
tools: "Ferramentas",
grid: "Grade",
automate: "Automatizar",
@@ -3870,6 +3871,7 @@ export const ptBR: TranslationKeys = {
mobileNavSettings: "Configurações",
},
a11y: {
sponsorLink: "Apoie o SnapOtter no GitHub Sponsors",
skipToContent: "Pular para o conteúdo principal",
openSidebar: "Abrir barra lateral",
closeSidebar: "Fechar barra lateral",
+2
View File
@@ -3847,6 +3847,7 @@ export const ru: TranslationKeys = {
connectionError: "Ошибка подключения",
},
sidebar: {
sponsor: "Пусть будет бесплатным",
tools: "Инструменты",
grid: "Сетка",
automate: "Автоматизация",
@@ -3866,6 +3867,7 @@ export const ru: TranslationKeys = {
mobileNavSettings: "Настройки",
},
a11y: {
sponsorLink: "Поддержать SnapOtter на GitHub Sponsors",
skipToContent: "Перейти к основному содержимому",
openSidebar: "Открыть боковую панель",
closeSidebar: "Закрыть боковую панель",
+2
View File
@@ -3840,6 +3840,7 @@ export const sv: TranslationKeys = {
connectionError: "Anslutningsfel",
},
sidebar: {
sponsor: "Håll det gratis",
tools: "Verktyg",
grid: "Rutnät",
automate: "Automatisera",
@@ -3859,6 +3860,7 @@ export const sv: TranslationKeys = {
mobileNavSettings: "Inställningar",
},
a11y: {
sponsorLink: "Stöd SnapOtter på GitHub Sponsors",
skipToContent: "Hoppa till huvudinnehåll",
openSidebar: "Öppna sidopanel",
closeSidebar: "Stäng sidopanel",
+2
View File
@@ -3799,6 +3799,7 @@ export const th: TranslationKeys = {
connectionError: "ข้อผิดพลาดในการเชื่อมต่อ",
},
sidebar: {
sponsor: "ให้ใช้ฟรีต่อไป",
tools: "เครื่องมือ",
grid: "กริด",
automate: "อัตโนมัติ",
@@ -3818,6 +3819,7 @@ export const th: TranslationKeys = {
mobileNavSettings: "ตั้งค่า",
},
a11y: {
sponsorLink: "สนับสนุน SnapOtter บน GitHub Sponsors",
skipToContent: "ข้ามไปยังเนื้อหาหลัก",
openSidebar: "เปิดแถบด้านข้าง",
closeSidebar: "ปิดแถบด้านข้าง",
+2
View File
@@ -3851,6 +3851,7 @@ export const tr: TranslationKeys = {
connectionError: "Bağlantı hatası",
},
sidebar: {
sponsor: "Ücretsiz kalsın",
tools: "Araçlar",
grid: "Izgara",
automate: "Otomatikleştir",
@@ -3870,6 +3871,7 @@ export const tr: TranslationKeys = {
mobileNavSettings: "Ayarlar",
},
a11y: {
sponsorLink: "SnapOtter'ı GitHub Sponsors'ta destekleyin",
skipToContent: "Ana içeriğe geç",
openSidebar: "Kenar çubuğunu aç",
closeSidebar: "Kenar çubuğunu kapat",
+2
View File
@@ -3848,6 +3848,7 @@ export const uk: TranslationKeys = {
connectionError: "Помилка з'єднання",
},
sidebar: {
sponsor: "Нехай буде безкоштовним",
tools: "Інструменти",
grid: "Сітка",
automate: "Автоматизація",
@@ -3867,6 +3868,7 @@ export const uk: TranslationKeys = {
mobileNavSettings: "Налаштування",
},
a11y: {
sponsorLink: "Підтримати SnapOtter на GitHub Sponsors",
skipToContent: "Перейти до основного вмісту",
openSidebar: "Відкрити бічну панель",
closeSidebar: "Закрити бічну панель",
+2
View File
@@ -3840,6 +3840,7 @@ export const vi: TranslationKeys = {
connectionError: "Lỗi kết nối",
},
sidebar: {
sponsor: "Giữ nó miễn phí",
tools: "Công cụ",
grid: "Lưới",
automate: "Tự động hóa",
@@ -3859,6 +3860,7 @@ export const vi: TranslationKeys = {
mobileNavSettings: "Cài đặt",
},
a11y: {
sponsorLink: "Ủng hộ SnapOtter trên GitHub Sponsors",
skipToContent: "Chuyển đến nội dung chính",
openSidebar: "Mở thanh bên",
closeSidebar: "Đóng thanh bên",
+2
View File
@@ -3583,6 +3583,7 @@ export const zhCN: TranslationKeys = {
connectionError: "连接错误",
},
sidebar: {
sponsor: "保持免费",
tools: "工具",
grid: "网格",
automate: "自动化",
@@ -3602,6 +3603,7 @@ export const zhCN: TranslationKeys = {
mobileNavSettings: "设置",
},
a11y: {
sponsorLink: "在 GitHub Sponsors 上支持 SnapOtter",
skipToContent: "跳到主要内容",
openSidebar: "打开侧边栏",
closeSidebar: "关闭侧边栏",
+2
View File
@@ -3583,6 +3583,7 @@ export const zhTW: TranslationKeys = {
connectionError: "連線錯誤",
},
sidebar: {
sponsor: "保持免費",
tools: "工具",
grid: "格線",
automate: "自動化",
@@ -3602,6 +3603,7 @@ export const zhTW: TranslationKeys = {
mobileNavSettings: "設定",
},
a11y: {
sponsorLink: "在 GitHub Sponsors 上支持 SnapOtter",
skipToContent: "跳至主要內容",
openSidebar: "開啟側邊欄",
closeSidebar: "關閉側邊欄",
+19
View File
@@ -0,0 +1,19 @@
import { expect, test } from "./helpers";
const SPONSOR_URL = "https://github.com/sponsors/snapotter-hq";
test.describe("Sponsor button", () => {
// Pin a desktop viewport so the full pill (with visible text) renders,
// not the mobile icon-only form.
test.use({ viewport: { width: 1280, height: 720 } });
test("top nav links to GitHub Sponsors with safe rel", async ({ page }) => {
await page.goto("/");
const link = page.locator(`a[href="${SPONSOR_URL}"]`);
await expect(link).toBeVisible();
await expect(link).toHaveAttribute("target", "_blank");
await expect(link).toHaveAttribute("rel", "noopener noreferrer");
await expect(link).toContainText(/keep it free/i);
});
});
+3 -2
View File
@@ -2,8 +2,8 @@ import { ANALYTICS_EVENTS } from "@snapotter/shared";
import { describe, expect, it } from "vitest";
describe("ANALYTICS_EVENTS", () => {
it("has exactly 13 event keys", () => {
expect(Object.keys(ANALYTICS_EVENTS)).toHaveLength(13);
it("has exactly 14 event keys", () => {
expect(Object.keys(ANALYTICS_EVENTS)).toHaveLength(14);
});
it("contains the expected keys", () => {
@@ -20,6 +20,7 @@ describe("ANALYTICS_EVENTS", () => {
expect(ANALYTICS_EVENTS).toHaveProperty("AI_BUNDLE_PROMPTED");
expect(ANALYTICS_EVENTS).toHaveProperty("BATCH_PROCESSED");
expect(ANALYTICS_EVENTS).toHaveProperty("FEEDBACK_SUBMITTED");
expect(ANALYTICS_EVENTS).toHaveProperty("SPONSOR_CLICKED");
});
it("all event values are strings", () => {