Donate stranka: PAY by square QR s vyberom sumy

Linka Na kavu v hlavicke lobby vedie na /donate. QR sa generuje
dynamicky (balicek bysquare) pre presety 1/2/5 EUR alebo vlastnu sumu;
IBAN sa da skopirovat do schranky. Navstevy loguje existujuci
pageview beacon.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
tim
2026-07-04 16:54:49 +02:00
co-authored by Claude Fable 5
parent aed0cd5d7f
commit 53274607b1
5 changed files with 154 additions and 0 deletions
+35
View File
@@ -8,6 +8,7 @@
"name": "bridzik-frontend", "name": "bridzik-frontend",
"version": "0.1.0", "version": "0.1.0",
"dependencies": { "dependencies": {
"bysquare": "^4.0.0",
"qrcode.react": "^4.1.0", "qrcode.react": "^4.1.0",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
@@ -3152,6 +3153,22 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/bysquare": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/bysquare/-/bysquare-4.0.0.tgz",
"integrity": "sha512-V1gGJFn39TY9XnvQDbzMTgGau3wCatsH5+XNSC2wXGgYijIVUD0gkZGalNIub28R+XhM/zUvge+8ccMPJ2+bZw==",
"license": "Apache-2.0",
"dependencies": {
"lzma1": "0.2.0",
"validator": "^13.15.26"
},
"bin": {
"bysquare": "lib/cli.js"
},
"funding": {
"url": "https://github.com/sponsors/xseman"
}
},
"node_modules/call-bind": { "node_modules/call-bind": {
"version": "1.0.9", "version": "1.0.9",
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz",
@@ -5096,6 +5113,15 @@
"yallist": "^3.0.2" "yallist": "^3.0.2"
} }
}, },
"node_modules/lzma1": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/lzma1/-/lzma1-0.2.0.tgz",
"integrity": "sha512-JkPtHHKANh2je4Gp2kTjtobUXf7+zzfGHYm9PPRnx+Mr4KhAqoyb8dwtCExGaxkJl5upwWStdkIJWpn28XWiHw==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/xseman"
}
},
"node_modules/magic-string": { "node_modules/magic-string": {
"version": "0.30.21", "version": "0.30.21",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
@@ -6952,6 +6978,15 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/validator": {
"version": "13.15.35",
"resolved": "https://registry.npmjs.org/validator/-/validator-13.15.35.tgz",
"integrity": "sha512-TQ5pAGhd5whStmqWvYF4OjQROlmv9SMFVt37qoCBdqRffuuklWYQlCNnEs2ZaIBD1kZRNnikiZOS1eqgkar0iw==",
"license": "MIT",
"engines": {
"node": ">= 0.10"
}
},
"node_modules/victory-vendor": { "node_modules/victory-vendor": {
"version": "36.9.2", "version": "36.9.2",
"resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.2.tgz", "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.2.tgz",
+1
View File
@@ -9,6 +9,7 @@
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"bysquare": "^4.0.0",
"qrcode.react": "^4.1.0", "qrcode.react": "^4.1.0",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
+2
View File
@@ -9,6 +9,7 @@ import Lobby from './pages/Lobby';
import GameTable from './pages/GameTable'; import GameTable from './pages/GameTable';
import Auth from './pages/Auth'; import Auth from './pages/Auth';
import History from './pages/History'; import History from './pages/History';
import Donate from './pages/Donate';
// Admin pulls in recharts — lazy-load it so players never download that chunk. // Admin pulls in recharts — lazy-load it so players never download that chunk.
const AdminLayout = lazy(() => import('./pages/admin/AdminLayout')); const AdminLayout = lazy(() => import('./pages/admin/AdminLayout'));
@@ -107,6 +108,7 @@ function AppInner() {
<Route path="/auth" element={<Auth />} /> <Route path="/auth" element={<Auth />} />
<Route path="/" element={<GameList />} /> <Route path="/" element={<GameList />} />
<Route path="/history" element={<History />} /> <Route path="/history" element={<History />} />
<Route path="/donate" element={<Donate />} />
<Route path="/lobby/:gid" element={<Lobby />} /> <Route path="/lobby/:gid" element={<Lobby />} />
<Route path="/game/:gid" element={<GameTable />} /> <Route path="/game/:gid" element={<GameTable />} />
<Route path="/admin" element={<AdminLayout />}> <Route path="/admin" element={<AdminLayout />}>
+113
View File
@@ -0,0 +1,113 @@
import { useMemo, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { QRCodeSVG } from 'qrcode.react';
import { encode, PaymentOptions, CurrencyCode } from 'bysquare/pay';
const IBAN = 'SK2583300000002803542822';
const AMOUNTS = [1, 2, 5];
const PAYMENT_NOTE = 'Bridzik';
const BENEFICIARY = 'Liptak Timotej';
export default function Donate() {
const navigate = useNavigate();
const [amount, setAmount] = useState(2);
const [custom, setCustom] = useState('');
const [copied, setCopied] = useState(false);
// Vlastna suma ma prednost pred presetom; akceptuje ciarku aj bodku.
const customParsed = parseFloat(custom.replace(',', '.'));
const customValid = custom !== '' && !isNaN(customParsed) && customParsed > 0;
const effectiveAmount = customValid ? Math.round(customParsed * 100) / 100 : amount;
const qrValue = useMemo(
() =>
encode({
payments: [
{
type: PaymentOptions.PaymentOrder,
amount: effectiveAmount,
currencyCode: CurrencyCode.EUR,
paymentNote: PAYMENT_NOTE,
beneficiary: { name: BENEFICIARY },
bankAccounts: [{ iban: IBAN }],
},
],
}),
[effectiveAmount],
);
const copyIban = async () => {
await navigator.clipboard.writeText(IBAN);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
};
return (
<div className="max-w-md mx-auto p-4 pt-8 min-h-screen">
<div className="flex items-center justify-between mb-6">
<h1 className="font-serif text-2xl text-gold">Podpor Bridžik </h1>
<button onClick={() => navigate('/')} className="text-sm text-green-dim hover:text-gold">
Späť
</button>
</div>
<div className="bg-header border border-[#142018] rounded-2xl p-6 text-sm text-green-score">
<p className="text-green-dim mb-4">
Bridžik je zadarmo. Ak sa ti hra páči, dobrovoľný príspevok pomôže s prevádzkou servera a rozvojom nových funkcionalít.
</p>
<div className="flex gap-2 mb-2">
{AMOUNTS.map((a) => (
<button
key={a}
onClick={() => {
setAmount(a);
setCustom('');
}}
className={`flex-1 py-1.5 rounded-lg font-serif font-semibold transition-colors ${
!customValid && amount === a
? 'bg-gold text-table'
: 'border border-gold/40 text-gold hover:bg-gold/10'
}`}
>
{a}
</button>
))}
</div>
<div
className={`flex items-center gap-2 rounded-lg border px-3 py-1.5 mb-4 transition-colors ${
customValid ? 'border-gold bg-gold/10' : 'border-gold/40'
}`}
>
<input
type="text"
inputMode="decimal"
placeholder="Vlastná suma"
value={custom}
onChange={(e) => setCustom(e.target.value)}
className="flex-1 bg-transparent outline-none text-gold placeholder-green-dim font-serif font-semibold"
/>
<span className="text-gold font-serif font-semibold"></span>
</div>
<div className="bg-white rounded-xl p-4 flex justify-center mb-2">
<QRCodeSVG value={qrValue} size={176} />
</div>
<p className="text-center text-xs text-green-dim mb-4">
Naskenuj QR kód bankovou aplikáciou (PAY by square)
</p>
<div className="flex items-center justify-between gap-2 border border-gold/20 rounded-lg px-3 py-2">
<span className="font-mono text-xs break-all">{IBAN}</span>
<button
onClick={copyIban}
className="shrink-0 text-gold hover:text-gold-bright text-xs font-semibold"
>
{copied ? 'Skopírované ✓' : 'Kopírovať'}
</button>
</div>
</div>
</div>
);
}
+3
View File
@@ -38,6 +38,9 @@ export default function GameList() {
<button onClick={() => navigate('/history')} className="text-gold hover:text-gold-bright"> <button onClick={() => navigate('/history')} className="text-gold hover:text-gold-bright">
História História
</button> </button>
<button onClick={() => navigate('/donate')} className="text-gold hover:text-gold-bright">
Na kávu
</button>
<button onClick={handleLogout} className="text-green-dim hover:text-gold"> <button onClick={handleLogout} className="text-green-dim hover:text-gold">
Odhlásiť Odhlásiť
</button> </button>