From d44db3563ddfda9a8a07a020124daf01dc959926 Mon Sep 17 00:00:00 2001 From: Chroma <110534939+uziff@users.noreply.github.com> Date: Fri, 1 May 2026 00:07:21 +0200 Subject: [PATCH] commit new code --- bun.lock | 4 +- functions/api/donors.js | 51 ++++++++++++++++++++++ functions/api/kofi-webhook.js | 79 +++++++++++++++++++++++++++++++++++ index.html | 9 ++++ js/app.js | 33 +++++++++++++++ styles.css | 13 ++++++ 6 files changed, 187 insertions(+), 2 deletions(-) create mode 100644 functions/api/donors.js create mode 100644 functions/api/kofi-webhook.js diff --git a/bun.lock b/bun.lock index 1e31d77ec..e16de85cd 100644 --- a/bun.lock +++ b/bun.lock @@ -19,7 +19,7 @@ "@svta/common-media-library": "^0.18.1", "@types/wicg-file-system-access": "^2023.10.7", "@typescript-eslint/eslint-plugin": "^8.57.2", - "@uimaxbai/am-lyrics": "^1.2.8", + "@uimaxbai/am-lyrics": "^1.2.9", "@vitest/web-worker": "^4.1.2", "appwrite": "^23.0.0", "butterchurn": "^2.6.7", @@ -678,7 +678,7 @@ "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.58.0", "", { "dependencies": { "@typescript-eslint/types": "8.58.0", "eslint-visitor-keys": "^5.0.0" } }, "sha512-XJ9UD9+bbDo4a4epraTwG3TsNPeiB9aShrUneAVXy8q4LuwowN+qu89/6ByLMINqvIMeI9H9hOHQtg/ijrYXzQ=="], - "@uimaxbai/am-lyrics": ["@uimaxbai/am-lyrics@1.2.8", "", { "dependencies": { "@babel/runtime": "^7.27.6", "lit": "^3.1.4" }, "peerDependencies": { "@lit/react": "^1.0.0", "react": ">=17.0.0" }, "optionalPeers": ["@lit/react", "react"] }, "sha512-aR8kxqIYcVlsMCH6bbH8ANG+bN/2OAw66ZFjYD1a25hkMTyxtULWgWwAZlUfreP9V47bFvNgXIKvOqhO5JFpeg=="], + "@uimaxbai/am-lyrics": ["@uimaxbai/am-lyrics@1.3.0", "", { "dependencies": { "@babel/runtime": "^7.27.6", "lit": "^3.1.4" }, "peerDependencies": { "@lit/react": "^1.0.0", "react": ">=17.0.0" }, "optionalPeers": ["@lit/react", "react"] }, "sha512-0IpvmCY6384cIKntIap/N9Rq/JuacSjL7Dq+QXPqRAjewZVVGcxEgka7tXtm1BnhwqIZWYWT/2Uzz6aA71RSmQ=="], "@vitest/browser": ["@vitest/browser@4.1.2", "", { "dependencies": { "@blazediff/core": "1.9.1", "@vitest/mocker": "4.1.2", "@vitest/utils": "4.1.2", "magic-string": "^0.30.21", "pngjs": "^7.0.0", "sirv": "^3.0.2", "tinyrainbow": "^3.1.0", "ws": "^8.19.0" }, "peerDependencies": { "vitest": "4.1.2" } }, "sha512-CwdIf90LNf1Zitgqy63ciMAzmyb4oIGs8WZ40VGYrWkssQKeEKr32EzO8MKUrDPPcPVHFI9oQ5ni2Hp24NaNRQ=="], diff --git a/functions/api/donors.js b/functions/api/donors.js new file mode 100644 index 000000000..1a27c9aad --- /dev/null +++ b/functions/api/donors.js @@ -0,0 +1,51 @@ + +// placeholders - for now anyway - when real data loads, these going to disappear +// Monthly donors appear first; within each group sorted by recency + +const PLACEHOLDERS_DONORS = [ + /* so many to test if: + - It works + - How it behaves if there are 5< users + - If code breaks when it runs out of space (this is unlikely since there is so many lines) [auto suggested by VsCode auto completion yay] + - if monthly users are correctly placed before one-time donors + - to write useless memes + */ + { name: 'Samidy', type: 'monthly', timestamp: '2026-04-29T10:00:00Z' }, + { name: 'Binimum', type: 'monthly', timestamp: '2026-04-20T09:00:00Z' }, + { name: 'John Monochrome', type: 'once', timestamp: '2026-04-27T15:00:00Z' }, + { name: 'Chroma', type: 'monthly', timestamp: '2026-04-25T12:00:00Z' }, + { name: 'Israel', type: 'once', timestamp: '2026-04-18T08:00:00Z' }, + { name: 'Tidal', type: 'once', timestamp: '2026-04-18T08:00:00Z' }, + { name: 'Kasane Teto (i think thats how you write her name)', type: 'monthly', timestamp: '2026-04-18T08:00:00Z' }, +]; + +const CORS_HEADERS = { + 'Access-Control-Allow-Origin': '*', + 'Content-Type': 'application/json', +}; + +export async function onRequestOptions() { + return new Response(null, { status: 204, headers: CORS_HEADERS }); +} + +export async function onRequestGet(context) { + const { env } = context; + + let donors = PLACEHOLDERS_DONORS; + + if (env.DONORS_KV) { + const stored = await env.DONORS_KV.get('donors').catch(() => null); + if (stored) donors = JSON.parse(stored); + } + + donors.sort((a, b) => { + if (a.type === 'monthly' && b.type !== 'monthly') return -1; + if (a.type !== 'monthly' && b.type === 'monthly') return 1; + return new Date(b.timestamp) - new Date(a.timestamp); + }); + + return new Response(JSON.stringify(donors), { + status: 200, + headers: { ...CORS_HEADERS, 'Cache-Control': 'public, max-age=60' }, + }); +} diff --git a/functions/api/kofi-webhook.js b/functions/api/kofi-webhook.js new file mode 100644 index 000000000..f48005dd8 --- /dev/null +++ b/functions/api/kofi-webhook.js @@ -0,0 +1,79 @@ +// Ko-fi webhook handler | by uzif (God i fucking love claude) + + +// how to run this stupid shit: +// Configure the webhook URL in your Ko-fi settings: https://ko-fi.com/manage/webhooks +// Set the URL to: https://monochrome.tf/api/kofi-webhook +// Set KOFI_VERIFICATION_TOKEN in your Cloudflare Pages environment variables +// Bind a KV namespace named DONORS_KV in your Cloudflare Pages settings + + +const CORS_HEADERS = { + 'Access-Control-Allow-Origin': '*', + 'Content-Type': 'application/json', +}; + +export async function onRequestOptions() { + return new Response(null, { status: 204, headers: CORS_HEADERS }); +} + +export async function onRequestPost(context) { + const { request, env } = context; + + try { + const formData = await request.formData(); + const raw = formData.get('data'); + if (!raw) { + return new Response(JSON.stringify({ error: 'Missing data' }), { + status: 400, + headers: CORS_HEADERS, + }); + } + + const donation = JSON.parse(raw); + + if (env.KOFI_VERIFICATION_TOKEN && donation.verification_token !== env.KOFI_VERIFICATION_TOKEN) { + return new Response(JSON.stringify({ error: 'Invalid verification token' }), { + status: 401, + headers: CORS_HEADERS, + }); + } + + if (!donation.is_public) { + return new Response(JSON.stringify({ ok: true, skipped: 'private' }), { + status: 200, + headers: CORS_HEADERS, + }); + } + + const donor = { + name: donation.from_name || 'Anonymous', + type: donation.is_subscription_payment ? 'monthly' : 'once', + timestamp: donation.timestamp || new Date().toISOString(), + }; + + if (!env.DONORS_KV) { + return new Response(JSON.stringify({ ok: true, stored: false, reason: 'KV not configured' }), { + status: 200, + headers: CORS_HEADERS, + }); + } + + const existing = JSON.parse((await env.DONORS_KV.get('donors').catch(() => null)) || '[]'); + + const idx = existing.findIndex((d) => d.name === donor.name); + if (idx >= 0) { + if (donor.type === 'monthly') existing[idx].type = 'monthly'; + existing[idx].timestamp = donor.timestamp; + } else { + existing.unshift(donor); + } + + //modify the values to show more or less ocntributors (0, 100) + await env.DONORS_KV.put('donors', JSON.stringify(existing.slice(0, 100))); + + return new Response(JSON.stringify({ ok: true }), { status: 200, headers: CORS_HEADERS }); + } catch (e) { + return new Response(JSON.stringify({ error: e.message }), { status: 500, headers: CORS_HEADERS }); + } +} diff --git a/index.html b/index.html index 5f7f196f5..91895b4fc 100644 --- a/index.html +++ b/index.html @@ -5994,6 +5994,15 @@

Support Monochrome

+
{ + const div = document.createElement('div'); + const icon = donor.type === 'monthly' ? '♥' : '☕'; + const label = donor.type === 'monthly' ? 'Monthly Supporter' : 'One-time Donor'; + div.innerHTML = ` + + ${icon} + ${donor.name} + ${label} + + `; + con.appendChild(div); + }); + } catch (e) { + const con = document.querySelector('.donate-donors-failed'); + if (!con) return; + const div = document.createElement('div'); + div.innerHTML = `

Failed to Fetch Donors List

`; + con.appendChild(div); + } +} + async function fetchcontributors() { try { const response = await fetch('https://api.samidy.com/api/contributors'); @@ -482,6 +514,7 @@ document.addEventListener('DOMContentLoaded', async () => { initTracker().catch(console.error); await fetchcontributors(); + fetchDonors(); const castBtn = document.getElementById('cast-btn'); initializeCasting(audioPlayer, castBtn); diff --git a/styles.css b/styles.css index 0a7055f5e..fc348120d 100644 --- a/styles.css +++ b/styles.css @@ -10562,6 +10562,19 @@ body:has(#side-panel.active) #close-fullscreen-cover-btn { } } +.donate-donors-section { + max-width: 800px; + margin: 2rem auto 0; + padding: 2rem 0; + border-top: 1px solid var(--border); +} + +.donor-icon { + font-size: 28px; + display: block; + margin-bottom: 4px; +} + /* Fullscreen layout rebuild on PR 378 base */ #fullscreen-cover-overlay .fullscreen-shell { width: 100%;