diff --git a/package.json b/package.json index c47da4d..fa6b414 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "ttrpg-player-landing", "private": true, - "version": "1.0.1", + "version": "1.0.2", "description": "Marketing landing for TTRPG Player (НРИ Плеер)", "type": "module", "scripts": { diff --git a/src/components/sections/Platforms.tsx b/src/components/sections/Platforms.tsx index 3f39924..bfd73a1 100644 --- a/src/components/sections/Platforms.tsx +++ b/src/components/sections/Platforms.tsx @@ -7,6 +7,7 @@ import { resolveDownloadHref, resolvePlatformDownloadHref, } from "../../lib/detectOs"; +import { trackDownload } from "../../lib/trackDownload"; import { Section } from "../layout/Section"; import { Button } from "../ui/Button"; import { PlatformLogo } from "../ui/PlatformLogo"; @@ -28,11 +29,24 @@ export function Platforms() { const hints = useMemo(() => readNavigatorHints(), []); - const osDownloadHref = useMemo(() => { + const { osDownloadHref, detectedOs } = useMemo(() => { const os = detectOs(hints.userAgent); - return resolveDownloadHref(os, DOWNLOAD_LINKS, hints); + return { + osDownloadHref: resolveDownloadHref(os, DOWNLOAD_LINKS, hints), + detectedOs: os, + }; }, [hints]); + const trackOsDownload = () => { + const platform = + detectedOs === "windows" || + detectedOs === "macos" || + detectedOs === "linux" + ? detectedOs + : "windows"; + trackDownload(platform); + }; + const platformDownloadHrefs = useMemo( () => Object.fromEntries( @@ -58,6 +72,9 @@ export function Platforms() { href={platformDownloadHrefs[id]} variant="ghost" className={styles.downloadBtn} + onClick={() => { + trackDownload(id); + }} > {copy.cta.download} @@ -72,7 +89,9 @@ export function Platforms() {

- +
); diff --git a/src/components/ui/Button.tsx b/src/components/ui/Button.tsx index aceada3..2114aa3 100644 --- a/src/components/ui/Button.tsx +++ b/src/components/ui/Button.tsx @@ -34,6 +34,7 @@ export function Button({ { + afterEach(() => { + mock.restoreAll(); + }); + + it("builds default tracking URL", () => { + assert.equal( + resolveTrackDownloadUrl(DEFAULT_LICENSE_TRACKING_BASE), + "https://license.ttrpgplayer.ru/v1/track/download", + ); + }); + + it("normalizes base URL without trailing slash", () => { + assert.equal( + resolveTrackDownloadUrl("https://license.example.test"), + "https://license.example.test/v1/track/download", + ); + }); + + it("POSTs platform with keepalive and swallows errors", async () => { + const fetchMock = mock.fn(() => Promise.reject(new Error("network down"))); + globalThis.fetch = fetchMock; + + trackDownload("macos"); + + await new Promise((resolve) => setTimeout(resolve, 0)); + + assert.equal(fetchMock.mock.calls.length, 1); + const call = fetchMock.mock.calls[0]; + assert.ok(call); + const [url, init] = call.arguments as unknown as [string, RequestInit]; + assert.equal(url, "https://license.ttrpgplayer.ru/v1/track/download"); + assert.equal(init.method, "POST"); + assert.equal(init.keepalive, true); + assert.deepEqual(init.headers, { "Content-Type": "application/json" }); + assert.equal(init.body, JSON.stringify({ platform: "macos" })); + }); +}); diff --git a/src/lib/trackDownload.ts b/src/lib/trackDownload.ts new file mode 100644 index 0000000..6ffcbca --- /dev/null +++ b/src/lib/trackDownload.ts @@ -0,0 +1,35 @@ +import type { PlatformId } from "../data/siteContent"; + +export const DEFAULT_LICENSE_TRACKING_BASE = "https://license.ttrpgplayer.ru/"; + +function normalizeBaseUrl(base: string): string { + return base.endsWith("/") ? base : `${base}/`; +} + +export function resolveLicenseTrackingBase(): string { + const fromEnv = + typeof import.meta.env !== "undefined" + ? import.meta.env.VITE_LICENSE_TRACKING_URL + : undefined; + if (typeof fromEnv === "string" && fromEnv.trim()) { + return normalizeBaseUrl(fromEnv.trim()); + } + return DEFAULT_LICENSE_TRACKING_BASE; +} + +export function resolveTrackDownloadUrl(base?: string): string { + return `${normalizeBaseUrl(base ?? resolveLicenseTrackingBase())}v1/track/download`; +} + +export function trackDownload(platform: PlatformId): void { + if (typeof fetch === "undefined") return; + + void fetch(resolveTrackDownloadUrl(), { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ platform }), + keepalive: true, + }).catch(() => { + // fire-and-forget: ignore network errors + }); +} diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts index 11f02fe..ae3a532 100644 --- a/src/vite-env.d.ts +++ b/src/vite-env.d.ts @@ -1 +1,9 @@ /// + +interface ImportMetaEnv { + readonly VITE_LICENSE_TRACKING_URL?: string; +} + +interface ImportMeta { + readonly env: ImportMetaEnv; +}