From 12778bb386d3c16f0cf5078f4d9b9938f6c502d2 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 30 Sep 2026 22:27:18 +0000 Subject: [PATCH 1/3] Vercel Web Analytics on the public site Mounts @vercel/analytics in the root layout through a small client wrapper. The Controller is left out twice: the component renders nothing on /controller, and a beforeSend filter drops any event for that path, so the secret address never reaches the dashboard. The privacy policy now says the website (not the app) counts page views with Vercel Web Analytics, what it records, and that it sets no cookies and stores no IP address. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01GVCvBYmceuqXQkc8AFCw8h --- package-lock.json | 44 ++++++++++++++++++++++++++++++++ package.json | 1 + src/app/layout.tsx | 6 ++++- src/app/privacy/page.tsx | 18 ++++++++++--- src/components/SiteAnalytics.tsx | 21 +++++++++++++++ 5 files changed, 85 insertions(+), 5 deletions(-) create mode 100644 src/components/SiteAnalytics.tsx diff --git a/package-lock.json b/package-lock.json index 3abe7f4..3fc88a2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,7 +7,9 @@ "": { "name": "atomic-community-base", "version": "0.1.0", + "license": "UNLICENSED", "dependencies": { + "@vercel/analytics": "^2.0.1", "gray-matter": "^4.0.3", "marked": "^18.0.11", "next": "15.5.24", @@ -861,6 +863,48 @@ "@types/react": "^19.3.0" } }, + "node_modules/@vercel/analytics": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@vercel/analytics/-/analytics-2.0.1.tgz", + "integrity": "sha512-MTQG6V9qQrt1tsDeF+2Uoo5aPjqbVPys1xvnIftXSJYG2SrwXRHnqEvVoYID7BTruDz4lCd2Z7rM1BdkUehk2g==", + "license": "MIT", + "peerDependencies": { + "@remix-run/react": "^2", + "@sveltejs/kit": "^1 || ^2", + "next": ">= 13", + "nuxt": ">= 3", + "react": "^18 || ^19 || ^19.0.0-rc", + "svelte": ">= 4", + "vue": "^3", + "vue-router": "^4" + }, + "peerDependenciesMeta": { + "@remix-run/react": { + "optional": true + }, + "@sveltejs/kit": { + "optional": true + }, + "next": { + "optional": true + }, + "nuxt": { + "optional": true + }, + "react": { + "optional": true + }, + "svelte": { + "optional": true + }, + "vue": { + "optional": true + }, + "vue-router": { + "optional": true + } + } + }, "node_modules/any-promise": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", diff --git a/package.json b/package.json index 6fc3dc8..4481090 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "test:smoke": "node --test tests/runtime.test.mjs" }, "dependencies": { + "@vercel/analytics": "^2.0.1", "gray-matter": "^4.0.3", "marked": "^18.0.11", "next": "15.5.24", diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 9c5fb1b..93ffa0b 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -2,6 +2,7 @@ import type { Metadata, Viewport } from "next"; import { Bebas_Neue, Hanken_Grotesk, JetBrains_Mono } from "next/font/google"; import "./globals.css"; import { SITE_URL } from "@/lib/site"; +import { SiteAnalytics } from "@/components/SiteAnalytics"; const bebas = Bebas_Neue({ weight: "400", @@ -72,7 +73,10 @@ export default function RootLayout({ }) { return ( - {children} + + {children} + + ); } diff --git a/src/app/privacy/page.tsx b/src/app/privacy/page.tsx index 9116535..ec77874 100644 --- a/src/app/privacy/page.tsx +++ b/src/app/privacy/page.tsx @@ -13,7 +13,7 @@ const CONTACT = "https://github.com/DevBehindYou"; export default function PrivacyPage() { return ( - +

This policy explains how the Atomic Notes Android app, its sync server and this website handle your information. Atomic Notes is built and operated by Ashutosh Sharma, who publishes as DevBehindYou (“we”, @@ -42,11 +42,21 @@ export default function PrivacyPage() {

  • Which in-app announcements you have read or dismissed.
  • - We do not collect your note content, contacts, location, advertising IDs, analytics or crash reports. Our hosting - provider processes standard request data, such as IP addresses, to deliver the app's server and this + The app does not collect your note content, contacts, location, advertising IDs, analytics or crash reports. Our + hosting provider processes standard request data, such as IP addresses, to deliver the app's server and this website. This website sets no tracking or advertising cookies.

    +

    Website analytics

    +

    + This website (not the app) uses Vercel Web Analytics to count page views. For each page you open it records the + page address, the referring site, your country, and your browser, operating system and device type. It sets no + cookies and stores no IP address. Visits are told apart by a hash of the request that Vercel resets every day, + so a visitor cannot be followed across days or across other websites. We only see these totals in Vercel's + dashboard, and we never combine them with your Atomic Notes account. A content blocker stops it without breaking + the site. +

    +

    How we use Google user data

    Atomic Notes asks Google for these permissions, and uses each one only as described:

      @@ -121,7 +131,7 @@ export default function PrivacyPage() {

      Service providers

      • Google: sign-in and Google Drive storage.
      • -
      • Vercel: hosting for the server and this website.
      • +
      • Vercel: hosting for the server and this website, and this website's page-view analytics.
      • MongoDB Atlas: the database for account data.

      diff --git a/src/components/SiteAnalytics.tsx b/src/components/SiteAnalytics.tsx new file mode 100644 index 0000000..ffc3223 --- /dev/null +++ b/src/components/SiteAnalytics.tsx @@ -0,0 +1,21 @@ +"use client"; + +import { usePathname } from "next/navigation"; +import { Analytics, type BeforeSendEvent } from "@vercel/analytics/next"; + +// Vercel Web Analytics for the public pages. The secret Controller is never reported: its address +// stays out of the analytics dashboard, the same way robots.ts keeps it out of robots.txt. +function isController(pathname: string): boolean { + return pathname === "/controller" || pathname.startsWith("/controller/"); +} + +// A script loaded on a public page keeps running after client-side navigation, so events are +// filtered as well as the component being left off the Controller. +function dropController(event: BeforeSendEvent): BeforeSendEvent | null { + return isController(new URL(event.url).pathname) ? null : event; +} + +export function SiteAnalytics() { + if (isController(usePathname() ?? "")) return null; + return ; +} From 6da80c16b11d4098b95b4b8e85caf0316b4b72c0 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 2 Oct 2026 19:22:35 +0000 Subject: [PATCH 2/3] Community Discord popup, bottom-left A small framed card invites visitors to the new Atomic Notes Community Discord server. It appears 5 seconds after every full page load or refresh; closing it (button or Escape) lasts until the next load, and nothing is stored. Client-side navigation does not bring it back. It never renders on the Controller. Site-matched styling: ink frame with a signal glow, paper card, 4px corners and a hard signal offset shadow. It is inert and hidden from assistive tech until shown, and respects reduced motion. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01GVCvBYmceuqXQkc8AFCw8h --- src/app/globals.css | 184 ++++++++++++++++++++++++++++++ src/app/layout.tsx | 2 + src/components/CommunityPopup.tsx | 89 +++++++++++++++ src/lib/site.ts | 3 + 4 files changed, 278 insertions(+) create mode 100644 src/components/CommunityPopup.tsx diff --git a/src/app/globals.css b/src/app/globals.css index 4cbe882..164f263 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -1779,3 +1779,187 @@ section h2 { .legal .prose li { margin-bottom: 6px; } + +/* ---------- community Discord popup ---------- */ +.community-pop { + position: fixed; + left: 20px; + bottom: 20px; + z-index: 60; + width: 320px; + max-width: calc(100vw - 32px); + padding: 12px; + background: + radial-gradient(320px circle at 0% 0%, rgba(58, 47, 240, 0.38), transparent 65%), + var(--ink); + border-radius: 4px; + box-shadow: 5px 5px 0 var(--signal); + opacity: 0; + transform: translateY(16px); + visibility: hidden; + transition: + opacity 0.35s ease, + transform 0.35s ease, + visibility 0s linear 0.35s; +} +.community-pop.is-visible { + opacity: 1; + transform: none; + visibility: visible; + transition: + opacity 0.35s ease, + transform 0.35s ease, + visibility 0s; +} +.community-pop-close { + position: absolute; + top: 8px; + right: 8px; + width: 28px; + height: 28px; + display: grid; + place-items: center; + border: none; + border-radius: 4px; + background: transparent; + color: #cfd0d3; + font-size: 18px; + line-height: 1; + cursor: pointer; +} +.community-pop-close:hover { + background: rgba(244, 245, 241, 0.1); + color: #fff; +} +.community-pop-head { + display: flex; + align-items: center; + gap: 10px; + padding: 2px 34px 12px 4px; +} +.community-pop-mark { + width: 30px; + height: 30px; + flex-shrink: 0; +} +.community-pop-brand { + display: flex; + flex-direction: column; + gap: 2px; + min-width: 0; +} +.community-pop-brand b { + font-family: var(--display); + font-weight: 400; + font-size: 1.15rem; + letter-spacing: 0.5px; + line-height: 1; + color: var(--paper); +} +.community-pop-live { + display: inline-flex; + align-items: center; + gap: 6px; + font-family: var(--mono); + font-size: 0.62rem; + letter-spacing: 1.6px; + text-transform: uppercase; + color: #8f88ff; +} +.community-pop-live::before { + content: ""; + width: 6px; + height: 6px; + border-radius: 50%; + background: #3ddc84; + box-shadow: 0 0 0 3px rgba(61, 220, 132, 0.18); +} +.community-pop-card { + background: var(--paper); + border-radius: 3px; + padding: 14px 14px 0; + overflow: hidden; +} +.community-pop-title { + font-family: var(--display); + font-size: 1.55rem; + line-height: 0.98; + letter-spacing: 0.3px; + text-transform: uppercase; + margin: 0 0 8px; +} +.community-pop-copy { + font-size: 0.86rem; + line-height: 1.45; + color: var(--slate); + margin: 0 0 12px; +} +.community-pop-copy strong { + color: var(--ink); +} +.community-pop-cta { + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; + margin: 0 -14px; + padding: 11px 14px; + border-top: 3px solid var(--ink); + background: var(--signal); + color: #fff; + text-decoration: none; + transition: background 0.15s ease; +} +.community-pop-cta:hover { + background: #2a20c9; +} +.community-pop-cta-text { + display: flex; + flex-direction: column; + gap: 3px; +} +.community-pop-label { + font-family: var(--mono); + font-size: 0.62rem; + letter-spacing: 1.6px; + text-transform: uppercase; + opacity: 0.85; +} +.community-pop-go { + font-family: var(--display); + font-size: 1.25rem; + letter-spacing: 0.6px; + line-height: 1; +} +.community-pop-arrow { + font-size: 1.2rem; + transition: transform 0.15s ease; +} +.community-pop-cta:hover .community-pop-arrow { + transform: translateX(3px); +} +.community-pop-close:focus-visible, +.community-pop-cta:focus-visible { + outline: 2px solid #8f88ff; + outline-offset: 2px; +} +@media (max-width: 420px) { + .community-pop { + left: 16px; + bottom: 16px; + } +} +@media (prefers-reduced-motion: reduce) { + .community-pop, + .community-pop.is-visible { + transform: none; + transition: + opacity 0.2s ease, + visibility 0s; + } + .community-pop-arrow, + .community-pop-cta:hover .community-pop-arrow { + transition: none; + transform: none; + } +} diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 93ffa0b..6a8fb64 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -3,6 +3,7 @@ import { Bebas_Neue, Hanken_Grotesk, JetBrains_Mono } from "next/font/google"; import "./globals.css"; import { SITE_URL } from "@/lib/site"; import { SiteAnalytics } from "@/components/SiteAnalytics"; +import { CommunityPopup } from "@/components/CommunityPopup"; const bebas = Bebas_Neue({ weight: "400", @@ -75,6 +76,7 @@ export default function RootLayout({ {children} + diff --git a/src/components/CommunityPopup.tsx b/src/components/CommunityPopup.tsx new file mode 100644 index 0000000..1e3949a --- /dev/null +++ b/src/components/CommunityPopup.tsx @@ -0,0 +1,89 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { usePathname } from "next/navigation"; +import { DISCORD_URL } from "@/lib/site"; + +const DELAY_MS = 5000; + +// Bottom-left invite to the community Discord. It lives in the root layout, so it mounts once per +// full page load: it appears 5 s after every load or refresh, and closing it lasts until the next one. +// Nothing is stored. Never shown on the secret Controller. +export function CommunityPopup() { + const pathname = usePathname() ?? ""; + const onController = pathname === "/controller" || pathname.startsWith("/controller/"); + const [visible, setVisible] = useState(false); + + useEffect(() => { + if (onController) return; + const timer = window.setTimeout(() => setVisible(true), DELAY_MS); + return () => window.clearTimeout(timer); + }, [onController]); + + useEffect(() => { + if (!visible) return; + const onKey = (e: KeyboardEvent) => { + if (e.key === "Escape") setVisible(false); + }; + document.addEventListener("keydown", onKey); + return () => document.removeEventListener("keydown", onKey); + }, [visible]); + + if (onController) return null; + + return ( +

      + ); +} + +function DiscordMark() { + return ( + + ); +} diff --git a/src/lib/site.ts b/src/lib/site.ts index 6e46351..f47f803 100644 --- a/src/lib/site.ts +++ b/src/lib/site.ts @@ -17,3 +17,6 @@ export const SITE_URL = originOf(process.env.NEXT_PUBLIC_SITE_URL, DEFAULT_SITE_ /** Where the download buttons point. */ export const APK_URL = process.env.NEXT_PUBLIC_APK_URL || DEFAULT_APK_URL; + +/** Invite to the Atomic Notes Community Discord server. */ +export const DISCORD_URL = "https://discord.gg/T4Vs7P3Qs"; From 8a0a0f5adefe2aaf18453ff9caadc0a9464f00db Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 2 Oct 2026 22:33:31 +0000 Subject: [PATCH 3/3] Support page: Ko-fi alongside Patreon Adds Ko-fi (ko-fi.com/devbehindyou) as a second way to support the project and earn early-supporter Atomic Coins. A new "Two ways to support" section compares the platforms, the four steps explain how to send the account email on each (a Patreon message, or a private Ko-fi message so the email stays off the public page), and every Patreon-only call to action now offers both. The home page Energy note, the footer link and llms.txt name both platforms too. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01GVCvBYmceuqXQkc8AFCw8h --- public/llms.txt | 4 +- src/app/globals.css | 24 +++++++- src/app/page.tsx | 2 +- src/app/support-atomic-notes/page.tsx | 88 +++++++++++++++++++++++---- src/components/SiteFooter.tsx | 2 +- src/lib/content.ts | 1 + 6 files changed, 104 insertions(+), 17 deletions(-) diff --git a/public/llms.txt b/public/llms.txt index e96bbdd..9cc8cfa 100644 --- a/public/llms.txt +++ b/public/llms.txt @@ -6,7 +6,7 @@ Key facts: - Storage: on-device first (Hive). Sync writes one .atomic file per note to the user's Google Drive, using the drive.file scope (the app sees only files it created). - Server: stores metadata only (note ids, pinned and deleted flags, timestamps, Drive file ids). It never stores note titles or text. With the vault off, note text passes through it in transit to Drive. -- Price: free. Every account holds 30 notes. Cloud sync uses Atomic Energy: +20 every 24 hours, cap 120, standard sync 5 (at most once an hour), instant sync 10, syncs with nothing to upload are free. 1 Atomic Coin = 40 energy. New accounts get 5 coins. Coins are not sold in the app yet. Early supporters on Patreon (https://www.patreon.com/cw/DevBehindYou) who send their account email get Atomic Coins from the developer by hand. +- Price: free. Every account holds 30 notes. Cloud sync uses Atomic Energy: +20 every 24 hours, cap 120, standard sync 5 (at most once an hour), instant sync 10, syncs with nothing to upload are free. 1 Atomic Coin = 40 energy. New accounts get 5 coins. Coins are not sold in the app yet. Early supporters on Patreon (https://www.patreon.com/cw/DevBehindYou) or Ko-fi (https://ko-fi.com/devbehindyou) who send their account email get Atomic Coins from the developer by hand. - Capacity tiers: Tachyon 30 notes (free), Antimatter 40 (10 coins), Monopole 50 (20 coins), Strangelet 100 (30 coins). - Device security: biometric lock, TOTP two-step verification, blocked screenshots (FLAG_SECURE), Android secure storage. - Distribution: signed APKs on GitHub Releases. Certificate SHA-256 cc24ae5ce1dca50fcd5e5c4c252d69e4965c55a975bd0e4739e8938fad9bebeb. Not on Google Play. No iOS build yet. @@ -16,7 +16,7 @@ Key facts: - [Home](https://atomic-notes-community.vercel.app/): features, how sync works, what each party can see, Atomic Energy, FAQ - [Updates](https://atomic-notes-community.vercel.app/updates): live release notes and service status - [Blog](https://atomic-notes-community.vercel.app/blog): development updates and technical write-ups -- [Support](https://atomic-notes-community.vercel.app/support-atomic-notes): support on Patreon and get Atomic Coins early +- [Support](https://atomic-notes-community.vercel.app/support-atomic-notes): support on Patreon or Ko-fi and get Atomic Coins early ## Source and downloads diff --git a/src/app/globals.css b/src/app/globals.css index 164f263..6f8ff8e 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -1744,6 +1744,27 @@ section h2 { line-height: 1.55; color: #2b2e34; } +.support-platforms { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 24px; + margin-top: 30px; +} +.support-platform { + display: flex; + flex-direction: column; +} +.support-platform h3 { + font-size: 2rem; + margin-top: 6px; +} +.support-platform .support-list { + flex: 1; + margin-bottom: 20px; +} +.support-platform .btn-signal { + align-self: flex-start; +} .support-facts { margin: 22px 0 26px; max-width: 640px; @@ -1752,7 +1773,8 @@ section h2 { color: var(--signal); } @media (max-width: 820px) { - .support-grid { + .support-grid, + .support-platforms { grid-template-columns: 1fr; } } diff --git a/src/app/page.tsx b/src/app/page.tsx index 8b6c7f1..d384e86 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -573,7 +573,7 @@ export default async function Home() {

      New accounts get 5 Atomic Coins. If a sync fails and no note gets through, its energy is refunded. Coins can't be bought in the app yet, but early supporters get them:{" "} - support on Patreon. + support on Patreon or Ko-fi.

      diff --git a/src/app/support-atomic-notes/page.tsx b/src/app/support-atomic-notes/page.tsx index f4c26fb..d8cc53a 100644 --- a/src/app/support-atomic-notes/page.tsx +++ b/src/app/support-atomic-notes/page.tsx @@ -1,12 +1,12 @@ import type { Metadata } from "next"; import { SiteNav } from "@/components/SiteNav"; import { SiteFooter } from "@/components/SiteFooter"; -import { PATREON_URL } from "@/lib/content"; +import { KOFI_URL, PATREON_URL } from "@/lib/content"; export const metadata: Metadata = { title: "Support Atomic Notes, get Atomic Coins early", description: - "Support Atomic Notes on Patreon at the amount you choose. Send your Atomic Notes account email, and the developer sends you Atomic Coins as an early-supporter reward.", + "Support Atomic Notes on Patreon or Ko-fi at the amount you choose. Send your Atomic Notes account email, and the developer sends you Atomic Coins as an early-supporter reward.", alternates: { canonical: "/support-atomic-notes" }, }; @@ -21,15 +21,20 @@ const STEPS: { t: string; d: React.ReactNode }[] = [ ), }, { - t: "Support on Patreon", - d: <>Open the DevBehindYou page on Patreon and support the project at the amount you choose., + t: "Pick a platform", + d: ( + <> + Support the DevBehindYou page on Patreon or Ko-fi, at the amount you choose. Both reach the same + developer and earn the same reward. + + ), }, { t: "Send your email", d: ( <> - Send the developer a Patreon message with your Atomic Notes account email. Check it letter by letter: the coins - go to exactly that account. + On Patreon, send the developer a message. On Ko-fi, write it in the message box when you pay and mark the + message private. Check it letter by letter: the coins go to exactly that account. ), }, @@ -44,6 +49,31 @@ const STEPS: { t: string; d: React.ReactNode }[] = [ }, ]; +const PLATFORMS: { name: string; tag: string; url: string; label: string; points: string[] }[] = [ + { + name: "Patreon", + tag: "MEMBERSHIP", + url: PATREON_URL, + label: "Support on Patreon", + points: [ + "Usually a monthly membership", + "Send your account email in a Patreon message", + "Cancel on Patreon at any time", + ], + }, + { + name: "Ko-fi", + tag: "ONE-TIME OR MONTHLY", + url: KOFI_URL, + label: "Support on Ko-fi", + points: [ + "Good for a single payment", + "Put your account email in the message and mark it private", + "Monthly support is also available on Ko-fi", + ], + }, +]; + export default function SupportPage() { return ( <> @@ -57,13 +87,16 @@ export default function SupportPage() {

      Atomic Notes is built by one developer, with no ads and no trackers. Coins aren't sold in the app - yet. Support the project on Patreon, and the developer sends Atomic Coins to your account as an + yet. Support the project on Patreon or Ko-fi, and the developer sends Atomic Coins to your account as an early-supporter reward.

      Support on Patreon + + Support on Ko-fi + How it works @@ -71,6 +104,31 @@ export default function SupportPage() {
      +
      +
      +

      TWO WAYS TO SUPPORT

      +

      + Pick the one that suits you. +

      +
      + {PLATFORMS.map((p) => ( +
      +

      {p.tag}

      +

      {p.name}

      +
        + {p.points.map((pt) => ( +
      • {pt}
      • + ))} +
      + + {p.label} + +
      + ))} +
      +
      +
      +

      FOUR STEPS

      @@ -92,16 +150,19 @@ export default function SupportPage() {

      IMPORTANT

      Use the exact account email

      - The reward is sent to the email you write, not to your Patreon name. A wrong or misspelled email means - the coins can't reach you. If you signed in with more than one Google account, use the one that - holds your notes. + The reward is sent to the email you write, not to your Patreon or Ko-fi name. A wrong or misspelled + email means the coins can't reach you. If you signed in with more than one Google account, use the + one that holds your notes. On Ko-fi, mark the message private so your email isn't shown on the + public page.

      GOOD TO KNOW

        -
      • Patreon handles the payment. Atomic Notes never sees your card or bank details.
      • -
      • Patreon support is usually a monthly membership. You can cancel on Patreon at any time.
      • +
      • + Patreon or Ko-fi handles the payment. Atomic Notes never sees your card, PayPal or bank details. +
      • +
      • Each platform charges its own fees, under its own terms.
      • The developer sends rewards by hand, so they can take a little time to arrive.
      • Writing notes stays free. Coins only buy sync energy and room for more notes.
      @@ -138,6 +199,9 @@ export default function SupportPage() { Support on Patreon + + Support on Ko-fi +
      diff --git a/src/components/SiteFooter.tsx b/src/components/SiteFooter.tsx index bff0829..85a27dd 100644 --- a/src/components/SiteFooter.tsx +++ b/src/components/SiteFooter.tsx @@ -22,7 +22,7 @@ export function SiteFooter() { Features How it works FAQ - Support on Patreon + Support the project