diff --git a/.gitignore b/.gitignore index e9667de..926fca6 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,11 @@ dist/ .env reference/ *.local + +# TypeScript build cache — regenerated on every build, not source tsconfig.tsbuildinfo +*.tsbuildinfo + +# IDE / agent local config +.vscode/ +.kiro/ diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md new file mode 100644 index 0000000..bdf2ab5 --- /dev/null +++ b/docs/CONTRIBUTING.md @@ -0,0 +1,115 @@ +# Contributing to usewraith.xyz + +## Analytics + +### Provider: Plausible Analytics + +We use [Plausible Analytics](https://plausible.io) — **not** Google Analytics or any other +cookie-based tracker. + +**Why Plausible?** + +| Requirement | Plausible | +|---|---| +| No cookies | ✅ Daily-rotating hash, never persisted | +| No personal data | ✅ IP never stored; no fingerprinting | +| GDPR / ePrivacy compliant without a consent banner | ✅ [Confirmed by Plausible](https://plausible.io/privacy-focused-web-analytics) | +| EU-hosted | ✅ Hetzner Germany/Finland | +| Open source | ✅ [AGPL-3.0](https://github.com/plausible/analytics) | +| Script bundle ≤ 2 KB gzipped | ✅ ~1 KB (verified via Network tab) | + +### How the script is loaded + +`index.html` loads the combined Plausible extension script: + +```html + + +``` + +- `script.scroll` — automatically tracks scroll depth at every percentage. + No setup needed; Plausible shows scroll depth in the dashboard. +- `script.tagged-events` — enables the `window.plausible()` JS function for + custom goal events (see below). +- The queue shim lets goal events fire before the script fully loads. + +**Note on `integrity` attribute:** Plausible does not currently publish SRI +hashes for their CDN script because they ship frequent minor updates. If your +CSP requires SRI, proxy the script through your own infrastructure (see +[Plausible proxy docs](https://plausible.io/docs/proxy/introduction)). + +### Custom goals + +All goal tracking goes through the typed helper in `src/analytics.ts`: + +```ts +import { trackEvent } from '../analytics'; + +trackEvent('Read the Docs'); +trackEvent('Code Tab Change', { props: { tab: 'scan.ts' } }); +``` + +#### Goals currently configured + +| Goal name | Where it fires | Notes | +|---|---|---| +| `Read the Docs` | Hero CTA, CtaStrip secondary button | Fires on click | +| `Try the Demo` | Hero secondary CTA | Fires on click | +| `Get API Key` | CtaStrip primary button | Fires on click | +| `Code Tab Change` | Hero code snippet tabs | Includes `tab` prop (`send.ts` / `scan.ts` / `withdraw.ts`) | +| Scroll depth | Automatic — all pages | Provided by `script.scroll` extension, no code needed | + +To add a new goal: +1. Call `trackEvent('Your Goal Name')` where appropriate. +2. Go to **usewraith.xyz → Plausible dashboard → Goals → Add Goal** and add + a matching Custom Event entry. + +### Privacy page + +`src/pages/Privacy.tsx` is the canonical "What we collect" page linked from +the footer. It documents Plausible's data practices in plain language and +explains the no-cookie guarantee. Keep it up to date whenever the analytics +setup changes. + +Route: `/privacy` (served by React Router, no server-side config needed for +Vite SPA — just ensure your hosting platform redirects all paths to +`index.html`). + +### No consent banner + +Because Plausible sets no cookies and stores no personal data, **no cookie +consent banner is required** under GDPR, PECR, or the ePrivacy Directive. Do +not add one. See [Plausible's data policy](https://plausible.io/data-policy) +for the legal basis. + +--- + +## Development setup + +```bash +pnpm install +pnpm dev # starts Vite dev server +pnpm build # TypeScript check + Vite production build +pnpm format # Prettier +``` + +## Commit conventions + +Commits follow [Conventional Commits](https://www.conventionalcommits.org/) +enforced via `commitlint` + `husky`. Examples: + +``` +feat: add privacy page +fix: correct plausible script extension url +docs: update contributing analytics section +``` diff --git a/index.html b/index.html index f66e2ee..ef4c7f3 100644 --- a/index.html +++ b/index.html @@ -29,6 +29,28 @@ + + + + + =18'} hasBin: true - convert-source-map@2.0.0: - resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + cookie@1.1.1: + resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} + engines: {node: '>=18'} cosmiconfig-typescript-loader@6.3.0: resolution: {integrity: sha512-Akr82WH1Wfqatyiqpj8HDkO2o2KmJRu1FhKfSNJP3K4IdXwHfEyL7MOb62i1AGQVLtIQM+iCE9CGOtrfhR+mmA==} @@ -1011,8 +1015,22 @@ packages: peerDependencies: react: ^19.2.5 - react-is@17.0.2: - resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} + react-router-dom@7.18.0: + resolution: {integrity: sha512-Fi0yY6kgtKae/Th2xibdWK0KSdYZ4B53Gyf6wRtomOKWgpNm7H7+DyfDhncdz9FKbpS+1jmDhg3F4WoGJ+yFOA==} + engines: {node: '>=20.0.0'} + peerDependencies: + react: '>=18' + react-dom: '>=18' + + react-router@7.18.0: + resolution: {integrity: sha512-pTTGt8J+ji1NOmYnjzT+bAJy/1zD+Jp4ziO6cL7T3ZLvXKtusO7BpFqlRXitqpcPVqllsIXFHRMt+2/k3Xn6HQ==} + engines: {node: '>=20.0.0'} + peerDependencies: + react: '>=18' + react-dom: '>=18' + peerDependenciesMeta: + react-dom: + optional: true react@19.2.5: resolution: {integrity: sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==} @@ -1055,8 +1073,8 @@ packages: engines: {node: '>=10'} hasBin: true - siginfo@2.0.0: - resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + set-cookie-parser@2.7.2: + resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==} source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} @@ -1804,7 +1822,7 @@ snapshots: '@simple-libs/stream-utils': 1.2.0 meow: 13.2.0 - convert-source-map@2.0.0: {} + cookie@1.1.1: {} cosmiconfig-typescript-loader@6.3.0(@types/node@25.6.0)(cosmiconfig@9.0.1(typescript@6.0.2))(typescript@6.0.2): dependencies: @@ -2098,7 +2116,19 @@ snapshots: react: 19.2.5 scheduler: 0.27.0 - react-is@17.0.2: {} + react-router-dom@7.18.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5): + dependencies: + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + react-router: 7.18.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + + react-router@7.18.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5): + dependencies: + cookie: 1.1.1 + react: 19.2.5 + set-cookie-parser: 2.7.2 + optionalDependencies: + react-dom: 19.2.5(react@19.2.5) react@19.2.5: {} @@ -2144,7 +2174,7 @@ snapshots: semver@7.7.4: {} - siginfo@2.0.0: {} + set-cookie-parser@2.7.2: {} source-map-js@1.2.1: {} diff --git a/src/App.tsx b/src/App.tsx index 5b69467..2279733 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,3 +1,4 @@ +import { BrowserRouter, Routes, Route } from 'react-router-dom'; import Header from './components/Header'; import Hero from './components/Hero'; import Features from './components/Features'; @@ -8,13 +9,9 @@ import Compare from './components/Compare'; import Showcase from './components/Showcase'; import CtaStrip from './components/CtaStrip'; import Footer from './components/Footer'; -import Press from './pages/Press'; - -const path = window.location.pathname.replace(/\/$/, ''); - -export default function App() { - if (path === '/press') return ; +import Privacy from './pages/Privacy'; +function Home() { return (
@@ -35,3 +32,14 @@ export default function App() {
); } + +export default function App() { + return ( + + + } /> + } /> + + + ); +} diff --git a/src/analytics.ts b/src/analytics.ts new file mode 100644 index 0000000..cff1aff --- /dev/null +++ b/src/analytics.ts @@ -0,0 +1,31 @@ +/** + * Thin wrapper around window.plausible() so the rest of the app stays + * decoupled from the analytics provider. + * + * Plausible sets no cookies and collects no personal data, so no consent + * banner is required under GDPR / ePrivacy. + * + * Usage: + * import { trackEvent } from '../analytics'; + * trackEvent('Read the Docs'); + * trackEvent('Code Tab Change', { props: { tab: 'scan.ts' } }); + */ + +declare global { + interface Window { + plausible?: ( + event: string, + options?: { props?: Record }, + ) => void; + } +} + +/** Fires a Plausible custom event. Safe to call even before the script loads. */ +export function trackEvent( + event: string, + options?: { props?: Record }, +): void { + if (typeof window.plausible === 'function') { + window.plausible(event, options); + } +} diff --git a/src/components/CtaStrip.tsx b/src/components/CtaStrip.tsx index 234355c..42491df 100644 --- a/src/components/CtaStrip.tsx +++ b/src/components/CtaStrip.tsx @@ -1,3 +1,5 @@ +import { trackEvent } from '../analytics'; + export default function CtaStrip() { return (
@@ -14,14 +16,16 @@ export default function CtaStrip() { href="https://console.usewraith.xyz" target="_blank" rel="noopener noreferrer" + onClick={() => trackEvent('Get API Key')} className="flex h-12 items-center justify-center bg-primary px-7 font-heading text-[13px] font-semibold uppercase tracking-[1.5px] text-surface transition-[filter] duration-150 hover:brightness-110" > - Get API Key + Get API Keys trackEvent('Read the Docs')} className="flex h-12 items-center justify-center border border-outline-variant px-7 font-heading text-[13px] font-semibold uppercase tracking-[1.5px] text-primary transition-colors duration-150 hover:bg-surface-bright" > Read the Docs diff --git a/src/components/Footer.tsx b/src/components/Footer.tsx index 4f4352b..491c25c 100644 --- a/src/components/Footer.tsx +++ b/src/components/Footer.tsx @@ -1,3 +1,5 @@ +import { Link } from 'react-router-dom'; + const columns = [ { title: 'PRODUCT', @@ -108,12 +110,12 @@ export default function Footer() { BUILT ON HORIZEN · ERC-5564 · OPEN SOURCE
- Privacy - + ('send.ts'); const [copyStatus, setCopyStatus] = useState<'idle' | 'copied' | 'failed'>('idle'); + const handleTabChange = (tab: Tab) => { + setActiveTab(tab); + trackEvent('Code Tab Change', { props: { tab } }); + }; + const lines = codeByTab[activeTab]; const activeTabIndex = tabs.indexOf(activeTab); @@ -174,6 +179,7 @@ export default function Hero() { href="https://docs.usewraith.xyz" target="_blank" rel="noopener noreferrer" + onClick={() => trackEvent('Read the Docs')} className="flex h-12 items-center justify-center bg-primary px-7 font-heading text-[13px] font-semibold uppercase tracking-[1.5px] text-surface transition-[filter] duration-150 hover:brightness-110" > Read the Docs @@ -182,6 +188,7 @@ export default function Hero() { href="https://demo.usewraith.xyz" target="_blank" rel="noopener noreferrer" + onClick={() => trackEvent('Try the Demo')} className="flex h-12 items-center justify-center border border-outline-variant px-7 font-heading text-[13px] font-semibold uppercase tracking-[1.5px] text-primary transition-colors duration-150 hover:bg-surface-bright" > Try the Demo @@ -216,14 +223,7 @@ export default function Hero() { {tabs.map((tab) => (