Add About / Support page (roadmap #4)

- New 'about' view wired into sidebar nav (♥ support) and App.tsx
- Sections: in-app zap button (reuses ZapModal + NWC infra), Lightning
  address QR + copy, Bitcoin address QR + copy, Ko-fi link, GitHub link,
  version/tech credits footer
- QR codes rendered as SVG via react-qr-code (no canvas dependency)
- lightning: URI and bitcoin: URI formats for wallet-scannable QR codes
- Tasteful and non-nagging — lives in the nav, never pops up unprompted

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Jure
2026-03-10 18:16:54 +01:00
parent e3ba3dbcee
commit 926a7cbdae
6 changed files with 207 additions and 4 deletions

63
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "wrystr",
"version": "0.1.0",
"version": "0.1.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "wrystr",
"version": "0.1.0",
"version": "0.1.1",
"dependencies": {
"@nostr-dev-kit/ndk": "^3.0.3",
"@tailwindcss/vite": "^4.2.1",
@@ -15,6 +15,7 @@
"marked": "^17.0.4",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-qr-code": "^2.0.18",
"tailwindcss": "^4.2.1",
"zustand": "^5.0.11"
},
@@ -2418,7 +2419,6 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
"dev": true,
"license": "MIT"
},
"node_modules/jsesc": {
@@ -2717,6 +2717,18 @@
"url": "https://opencollective.com/parcel"
}
},
"node_modules/loose-envify": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
"license": "MIT",
"dependencies": {
"js-tokens": "^3.0.0 || ^4.0.0"
},
"bin": {
"loose-envify": "cli.js"
}
},
"node_modules/lru-cache": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
@@ -2968,6 +2980,15 @@
"license": "MIT",
"peer": true
},
"node_modules/object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/oniguruma-parser": {
"version": "0.12.1",
"resolved": "https://registry.npmjs.org/oniguruma-parser/-/oniguruma-parser-0.12.1.tgz",
@@ -3037,6 +3058,17 @@
"node": "^10 || ^12 || >=14"
}
},
"node_modules/prop-types": {
"version": "15.8.1",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
"license": "MIT",
"dependencies": {
"loose-envify": "^1.4.0",
"object-assign": "^4.1.1",
"react-is": "^16.13.1"
}
},
"node_modules/property-information": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz",
@@ -3047,6 +3079,12 @@
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/qr.js": {
"version": "0.0.0",
"resolved": "https://registry.npmjs.org/qr.js/-/qr.js-0.0.0.tgz",
"integrity": "sha512-c4iYnWb+k2E+vYpRimHqSu575b1/wKl4XFeJGpFmrJQz5I88v9aY2czh7s0w36srfCM1sXgC/xpoJz5dJfq+OQ==",
"license": "MIT"
},
"node_modules/react": {
"version": "19.2.4",
"resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz",
@@ -3068,6 +3106,25 @@
"react": "^19.2.4"
}
},
"node_modules/react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
"license": "MIT"
},
"node_modules/react-qr-code": {
"version": "2.0.18",
"resolved": "https://registry.npmjs.org/react-qr-code/-/react-qr-code-2.0.18.tgz",
"integrity": "sha512-v1Jqz7urLMhkO6jkgJuBYhnqvXagzceg3qJUWayuCK/c6LTIonpWbwxR1f1APGd4xrW/QcQEovNrAojbUz65Tg==",
"license": "MIT",
"dependencies": {
"prop-types": "^15.8.1",
"qr.js": "0.0.0"
},
"peerDependencies": {
"react": "*"
}
},
"node_modules/react-refresh": {
"version": "0.17.0",
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",

View File

@@ -17,6 +17,7 @@
"marked": "^17.0.4",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-qr-code": "^2.0.18",
"tailwindcss": "^4.2.1",
"zustand": "^5.0.11"
},

View File

@@ -8,6 +8,7 @@ import { ProfileView } from "./components/profile/ProfileView";
import { ThreadView } from "./components/thread/ThreadView";
import { ArticleEditor } from "./components/article/ArticleEditor";
import { OnboardingFlow } from "./components/onboarding/OnboardingFlow";
import { AboutView } from "./components/shared/AboutView";
import { useUIStore } from "./stores/ui";
function App() {
@@ -31,6 +32,7 @@ function App() {
{currentView === "profile" && <ProfileView />}
{currentView === "thread" && <ThreadView />}
{currentView === "article-editor" && <ArticleEditor />}
{currentView === "about" && <AboutView />}
</main>
</div>
);

View File

@@ -0,0 +1,142 @@
import { useState } from "react";
import QRCode from "react-qr-code";
import { ZapModal } from "../zap/ZapModal";
const DEV_NPUB = "npub1ezt7xcq87ljj65jkjsuagwll4yp75tacgkuyjdhkw6mza8j3azfq2vrvl6";
const DEV_PUBKEY = "c897e36007f7e52d52569439d43bffa903ea2fb845b84936f676b62e9e51e892";
const LIGHTNING_ADDRESS = "harpos@getalby.com";
const BITCOIN_ADDRESS = "bc1qcgaupf80j28ca537xjlcs9dm9s03khezjs7crp";
const KOFI_URL = "https://ko-fi.com/jure";
const GITHUB_URL = "https://github.com/hoornet/wrystr";
function CopyButton({ text }: { text: string }) {
const [copied, setCopied] = useState(false);
const handle = () => {
navigator.clipboard.writeText(text);
setCopied(true);
setTimeout(() => setCopied(false), 1500);
};
return (
<button
onClick={handle}
className="text-text-dim hover:text-accent text-[10px] transition-colors ml-2 shrink-0"
>
{copied ? "copied ✓" : "copy"}
</button>
);
}
function QRBlock({ value, label }: { value: string; label: string }) {
return (
<div className="flex flex-col items-center gap-2">
<div className="bg-white p-2.5 inline-block">
<QRCode value={value} size={120} />
</div>
<div className="flex items-center gap-1 max-w-[160px]">
<span className="text-text-dim text-[10px] font-mono truncate">{label}</span>
<CopyButton text={label} />
</div>
</div>
);
}
export function AboutView() {
const [showZap, setShowZap] = useState(false);
return (
<div className="h-full overflow-y-auto">
<div className="max-w-xl mx-auto px-6 py-8">
{/* Header */}
<div className="mb-8">
<h1 className="text-text text-lg font-medium tracking-tight mb-2">Support Wrystr</h1>
<p className="text-text-muted text-[13px] leading-relaxed">
Wrystr is free, open-source, and built by one person. If it's useful to you,
any support a zap, a share, a star on GitHub genuinely helps.
</p>
</div>
{/* Zap */}
<section className="mb-8">
<h2 className="text-text-dim text-[10px] uppercase tracking-widest mb-3"> Zap the developer</h2>
<p className="text-text-muted text-[12px] mb-3">
Send sats directly from Wrystr using your connected Lightning wallet.
</p>
<button
onClick={() => setShowZap(true)}
className="px-4 py-2 text-[12px] font-medium bg-zap hover:bg-zap/90 text-white transition-colors"
>
Zap hoornet
</button>
<p className="text-text-dim text-[10px] mt-2 font-mono break-all">{DEV_NPUB}</p>
</section>
{/* QR codes */}
<section className="mb-8">
<h2 className="text-text-dim text-[10px] uppercase tracking-widest mb-4">Scan to send</h2>
<div className="flex flex-wrap gap-8">
<div>
<div className="text-text-muted text-[11px] mb-2">Lightning</div>
<QRBlock value={`lightning:${LIGHTNING_ADDRESS}`} label={LIGHTNING_ADDRESS} />
</div>
<div>
<div className="text-text-muted text-[11px] mb-2">Bitcoin</div>
<QRBlock value={`bitcoin:${BITCOIN_ADDRESS}`} label={BITCOIN_ADDRESS} />
</div>
</div>
</section>
{/* Links */}
<section className="mb-8">
<h2 className="text-text-dim text-[10px] uppercase tracking-widest mb-3">Other ways to help</h2>
<div className="space-y-2">
<div className="flex items-center gap-3">
<span className="text-text-muted text-[12px] w-16 shrink-0">Ko-fi</span>
<a
href={KOFI_URL}
target="_blank"
rel="noopener noreferrer"
className="text-accent text-[12px] hover:text-accent-hover transition-colors"
>
{KOFI_URL}
</a>
</div>
<div className="flex items-center gap-3">
<span className="text-text-muted text-[12px] w-16 shrink-0">GitHub</span>
<a
href={GITHUB_URL}
target="_blank"
rel="noopener noreferrer"
className="text-accent text-[12px] hover:text-accent-hover transition-colors"
>
{GITHUB_URL}
</a>
</div>
</div>
</section>
{/* Version / About */}
<section className="border-t border-border pt-6">
<div className="text-text-dim text-[11px] space-y-1">
<div>Wrystr v0.1.1 MIT license</div>
<div>
Built with{" "}
<a href="https://tauri.app" target="_blank" rel="noopener noreferrer" className="text-accent hover:text-accent-hover transition-colors">Tauri</a>
{" · "}
<a href="https://github.com/nostr-dev-kit/ndk" target="_blank" rel="noopener noreferrer" className="text-accent hover:text-accent-hover transition-colors">NDK</a>
{" · "}
<a href="https://nostr.com" target="_blank" rel="noopener noreferrer" className="text-accent hover:text-accent-hover transition-colors">Nostr</a>
</div>
</div>
</section>
</div>
{showZap && (
<ZapModal
target={{ type: "profile", pubkey: DEV_PUBKEY }}
recipientName="hoornet"
onClose={() => setShowZap(false)}
/>
)}
</div>
);
}

View File

@@ -9,6 +9,7 @@ const NAV_ITEMS = [
{ id: "search" as const, label: "search", icon: "⌕" },
{ id: "relays" as const, label: "relays", icon: "⟐" },
{ id: "settings" as const, label: "settings", icon: "⚙" },
{ id: "about" as const, label: "support", icon: "♥" },
] as const;
export function Sidebar() {

View File

@@ -2,7 +2,7 @@ import { create } from "zustand";
import { NDKEvent } from "@nostr-dev-kit/ndk";
type View = "feed" | "search" | "relays" | "settings" | "profile" | "thread" | "article-editor";
type View = "feed" | "search" | "relays" | "settings" | "profile" | "thread" | "article-editor" | "about";
interface UIState {
currentView: View;