mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
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:
@@ -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 (
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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: "إغلاق الشريط الجانبي",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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: "साइडबार बंद करें",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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: "サイドバーを閉じる",
|
||||
|
||||
@@ -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: "사이드바 닫기",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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: "Закрыть боковую панель",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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: "ปิดแถบด้านข้าง",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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: "Закрити бічну панель",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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: "关闭侧边栏",
|
||||
|
||||
@@ -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: "關閉側邊欄",
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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", () => {
|
||||
|
||||
Reference in New Issue
Block a user