From 0d90ed7939df790ce61535f85b0d1a421baf360b Mon Sep 17 00:00:00 2001 From: Jared Schoeny Date: Mon, 15 Dec 2025 21:38:29 -1000 Subject: [PATCH 01/23] Add email confirmation supabase template --- supabase/templates/confirmation.html | 224 +++++++++++++++++++++++++++ 1 file changed, 224 insertions(+) create mode 100644 supabase/templates/confirmation.html diff --git a/supabase/templates/confirmation.html b/supabase/templates/confirmation.html new file mode 100644 index 0000000..0e57ffa --- /dev/null +++ b/supabase/templates/confirmation.html @@ -0,0 +1,224 @@ + + + + + + + + + Confirm your email + + + + +
+ + + + +
+ + + + + + + + + + + +
+
+ + + From 93d8b83e7c7e5654d7ed8d2e8746d96e139a5b9e Mon Sep 17 00:00:00 2001 From: jschoeny Date: Wed, 17 Dec 2025 01:12:02 -1000 Subject: [PATCH 02/23] Add basic anonymous analytics --- package-lock.json | 155 +++++++++++++++++++++------------------------ package.json | 1 + src/app/layout.tsx | 2 + 3 files changed, 74 insertions(+), 84 deletions(-) diff --git a/package-lock.json b/package-lock.json index 9fc67df..0b491f5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ "@supabase/ssr": "^0.7.0", "@supabase/supabase-js": "^2.74.0", "@types/serialize-javascript": "^5.0.4", + "@vercel/analytics": "^1.6.1", "@vercel/edge-config": "^1.4.3", "chart.js": "^4.5.1", "embla-carousel-react": "8.6.0", @@ -836,6 +837,7 @@ "resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz", "integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==", "license": "MIT", + "peer": true, "dependencies": { "@dnd-kit/accessibility": "^3.1.1", "@dnd-kit/utilities": "^3.2.2", @@ -1421,7 +1423,6 @@ "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25" @@ -2379,6 +2380,7 @@ "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.74.0.tgz", "integrity": "sha512-IEMM/V6gKdP+N/X31KDIczVzghDpiPWFGLNjS8Rus71KvV6y6ueLrrE/JGCHDrU+9pq5copF3iCa0YQh+9Lq9Q==", "license": "MIT", + "peer": true, "dependencies": { "@supabase/auth-js": "2.74.0", "@supabase/functions-js": "2.74.0", @@ -2723,7 +2725,6 @@ "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/estree": "*", "@types/json-schema": "*" @@ -2735,7 +2736,6 @@ "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/eslint": "*", "@types/estree": "*" @@ -2818,6 +2818,7 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.0.tgz", "integrity": "sha512-1LOH8xovvsKsCBq1wnT4ntDUdCJKmnEakhsuoUSy6ExlHCkGP2hqnatagYTgFk6oeL0VU31u7SNjunPN+GchtA==", "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -2859,6 +2860,44 @@ "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", "license": "ISC" }, + "node_modules/@vercel/analytics": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@vercel/analytics/-/analytics-1.6.1.tgz", + "integrity": "sha512-oH9He/bEM+6oKlv3chWuOOcp8Y6fo6/PSro8hEkgCW3pu9/OiCXiUpRUogDh3Fs3LH2sosDrx8CxeOLBEE+afg==", + "license": "MPL-2.0", + "peerDependencies": { + "@remix-run/react": "^2", + "@sveltejs/kit": "^1 || ^2", + "next": ">= 13", + "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 + }, + "react": { + "optional": true + }, + "svelte": { + "optional": true + }, + "vue": { + "optional": true + }, + "vue-router": { + "optional": true + } + } + }, "node_modules/@vercel/edge-config": { "version": "1.4.3", "resolved": "https://registry.npmjs.org/@vercel/edge-config/-/edge-config-1.4.3.tgz", @@ -2895,7 +2934,6 @@ "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@webassemblyjs/helper-numbers": "1.13.2", "@webassemblyjs/helper-wasm-bytecode": "1.13.2" @@ -2906,24 +2944,21 @@ "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@webassemblyjs/helper-api-error": { "version": "1.13.2", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@webassemblyjs/helper-buffer": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@webassemblyjs/helper-numbers": { "version": "1.13.2", @@ -2931,7 +2966,6 @@ "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@webassemblyjs/floating-point-hex-parser": "1.13.2", "@webassemblyjs/helper-api-error": "1.13.2", @@ -2943,8 +2977,7 @@ "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@webassemblyjs/helper-wasm-section": { "version": "1.14.1", @@ -2952,7 +2985,6 @@ "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-buffer": "1.14.1", @@ -2966,7 +2998,6 @@ "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@xtuc/ieee754": "^1.2.0" } @@ -2977,7 +3008,6 @@ "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", "dev": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "@xtuc/long": "4.2.2" } @@ -2987,8 +3017,7 @@ "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@webassemblyjs/wasm-edit": { "version": "1.14.1", @@ -2996,7 +3025,6 @@ "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-buffer": "1.14.1", @@ -3014,7 +3042,6 @@ "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-wasm-bytecode": "1.13.2", @@ -3029,7 +3056,6 @@ "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-buffer": "1.14.1", @@ -3043,7 +3069,6 @@ "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-api-error": "1.13.2", @@ -3059,7 +3084,6 @@ "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@webassemblyjs/ast": "1.14.1", "@xtuc/long": "4.2.2" @@ -3070,16 +3094,14 @@ "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", "dev": true, - "license": "BSD-3-Clause", - "peer": true + "license": "BSD-3-Clause" }, "node_modules/@xtuc/long": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", "dev": true, - "license": "Apache-2.0", - "peer": true + "license": "Apache-2.0" }, "node_modules/@yarnpkg/lockfile": { "version": "1.1.0", @@ -3115,7 +3137,6 @@ "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=10.13.0" }, @@ -3129,6 +3150,7 @@ "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -3146,7 +3168,6 @@ "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "ajv": "^8.0.0" }, @@ -3165,7 +3186,6 @@ "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -3182,8 +3202,7 @@ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/ajv-keywords": { "version": "3.5.2", @@ -3247,7 +3266,6 @@ "integrity": "sha512-JU0h5APyQNsHOlAM7HnQnPToSDQoEBZqzu/YBlqDnEeymPnZDREeXJA3KBMQee+dKteAxZ2AtvQEvVYdZf241Q==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "baseline-browser-mapping": "dist/cli.js" } @@ -3346,8 +3364,7 @@ "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/call-bind": { "version": "1.0.8", @@ -3487,6 +3504,7 @@ "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.1.tgz", "integrity": "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==", "license": "MIT", + "peer": true, "dependencies": { "@kurkle/color": "^0.3.0" }, @@ -3500,7 +3518,6 @@ "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=6.0" } @@ -3733,14 +3750,14 @@ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.244.tgz", "integrity": "sha512-OszpBN7xZX4vWMPJwB9illkN/znA8M36GQqQxi6MNy9axWxhOfJyZZJtSLQCpEFLHP2xK33BiWx9aIuIEXVCcw==", "dev": true, - "license": "ISC", - "peer": true + "license": "ISC" }, "node_modules/embla-carousel": { "version": "8.6.0", "resolved": "https://registry.npmjs.org/embla-carousel/-/embla-carousel-8.6.0.tgz", "integrity": "sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/embla-carousel-react": { "version": "8.6.0", @@ -3811,8 +3828,7 @@ "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/es-object-atoms": { "version": "1.1.1", @@ -3832,7 +3848,6 @@ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=6" } @@ -3855,7 +3870,6 @@ "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", "dev": true, "license": "BSD-2-Clause", - "peer": true, "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^4.1.1" @@ -3870,7 +3884,6 @@ "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", "dev": true, "license": "BSD-2-Clause", - "peer": true, "dependencies": { "estraverse": "^5.2.0" }, @@ -3884,7 +3897,6 @@ "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", "dev": true, "license": "BSD-2-Clause", - "peer": true, "engines": { "node": ">=4.0" } @@ -3895,7 +3907,6 @@ "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", "dev": true, "license": "BSD-2-Clause", - "peer": true, "engines": { "node": ">=4.0" } @@ -3922,7 +3933,6 @@ "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=0.8.x" } @@ -3962,8 +3972,7 @@ "url": "https://opencollective.com/fastify" } ], - "license": "BSD-3-Clause", - "peer": true + "license": "BSD-3-Clause" }, "node_modules/fast-xml-parser": { "version": "4.5.3", @@ -4111,8 +4120,7 @@ "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", "dev": true, - "license": "BSD-2-Clause", - "peer": true + "license": "BSD-2-Clause" }, "node_modules/gopd": { "version": "1.2.0", @@ -4485,7 +4493,6 @@ "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/node": "*", "merge-stream": "^2.0.0", @@ -4501,7 +4508,6 @@ "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "has-flag": "^4.0.0" }, @@ -4527,8 +4533,7 @@ "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/json-schema-traverse": { "version": "0.4.1", @@ -4870,7 +4875,6 @@ "integrity": "sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=6.11.5" }, @@ -5214,8 +5218,7 @@ "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/micromark": { "version": "4.0.2", @@ -5879,14 +5882,14 @@ "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/next": { "version": "15.5.9", "resolved": "https://registry.npmjs.org/next/-/next-15.5.9.tgz", "integrity": "sha512-agNLK89seZEtC5zUHwtut0+tNrc0Xw4FT/Dg+B/VLEo9pAcS9rtTKpek3V6kVcVwsB2YlqMaHdfZL4eLEVYuCg==", "license": "MIT", + "peer": true, "dependencies": { "@next/env": "15.5.9", "@swc/helpers": "0.5.15", @@ -5978,8 +5981,7 @@ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/nodemailer": { "version": "7.0.11", @@ -6197,7 +6199,6 @@ "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "safe-buffer": "^5.1.0" } @@ -6228,6 +6229,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.1.2.tgz", "integrity": "sha512-MdWVitvLbQULD+4DP8GYjZUrepGW7d+GQkNVqJEzNxE+e9WIa4egVFE/RDfVb1u9u/Jw7dNMmPB4IqxzbFYJ0w==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -6247,6 +6249,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.2.tgz", "integrity": "sha512-dEoydsCp50i7kS1xHOmPXq4zQYoGWedUsvqv9H6zdif2r7yLHygyfP9qou71TulRN0d6ng9EbRVsQhSqfUc19g==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.26.0" }, @@ -6393,7 +6396,6 @@ "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -6606,7 +6608,6 @@ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true, "license": "BSD-3-Clause", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -6626,7 +6627,6 @@ "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" @@ -6774,7 +6774,8 @@ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.17.tgz", "integrity": "sha512-j9Ee2YjuQqYT9bbRTfTZht9W/ytp5H+jJpZKiYdP/bpnXARAuELt9ofP0lPnmHjbga7SNQIxdTAXCmtKVYjN+Q==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/tapable": { "version": "2.3.0", @@ -6796,7 +6797,6 @@ "integrity": "sha512-nIVck8DK+GM/0Frwd+nIhZ84pR/BX7rmXMfYwyg+Sri5oGVE99/E3KvXqpC2xHFxyqXyGHTKBSioxxplrO4I4w==", "dev": true, "license": "BSD-2-Clause", - "peer": true, "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.15.0", @@ -6816,7 +6816,6 @@ "integrity": "sha512-vkZjpUjb6OMS7dhV+tILUW6BhpDR7P2L/aQSAv+Uwk+m8KATX9EccViHTJR2qDtACKPIYndLGCyl3FMo+r2LMw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "jest-worker": "^27.4.5", @@ -6870,7 +6869,6 @@ "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3" }, @@ -6883,8 +6881,7 @@ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/terser-webpack-plugin/node_modules/schema-utils": { "version": "4.3.3", @@ -6892,7 +6889,6 @@ "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/json-schema": "^7.0.9", "ajv": "^8.9.0", @@ -6913,7 +6909,6 @@ "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", "dev": true, "license": "BSD-3-Clause", - "peer": true, "dependencies": { "randombytes": "^2.1.0" } @@ -6923,8 +6918,7 @@ "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/through2": { "version": "4.0.2", @@ -7127,7 +7121,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" @@ -7224,7 +7217,6 @@ "integrity": "sha512-c5EGNOiyxxV5qmTtAB7rbiXxi1ooX1pQKMLX/MIabJjRA0SJBQOjKF+KSVfHkr9U1cADPon0mRiVe/riyaiDUA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.1.2" @@ -7257,7 +7249,6 @@ "integrity": "sha512-7h/weGm9d/ywQ6qzJ+Xy+r9n/3qgp/thalBbpOi5i223dPXKi04IBtqPN9nTd+jBc7QKfvDbaBnFipYp4sJAUQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.8", @@ -7307,7 +7298,6 @@ "integrity": "sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=10.13.0" } @@ -7336,7 +7326,6 @@ "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3" }, @@ -7349,8 +7338,7 @@ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/webpack/node_modules/schema-utils": { "version": "4.3.3", @@ -7358,7 +7346,6 @@ "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/json-schema": "^7.0.9", "ajv": "^8.9.0", diff --git a/package.json b/package.json index 8e89d42..4b116e0 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "@supabase/ssr": "^0.7.0", "@supabase/supabase-js": "^2.74.0", "@types/serialize-javascript": "^5.0.4", + "@vercel/analytics": "^1.6.1", "@vercel/edge-config": "^1.4.3", "chart.js": "^4.5.1", "embla-carousel-react": "8.6.0", diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 2b05c0d..1316556 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,5 +1,6 @@ import type { Metadata } from "next"; import { Geist, Geist_Mono } from "next/font/google"; +import { Analytics } from "@vercel/analytics/react"; import "./globals.css"; import Header from "@/components/Header"; import Footer from "@/components/Footer"; @@ -49,6 +50,7 @@ export default function RootLayout({ + ); From 3a5c22bae6fc001b631391dca89f7f880d4e1bb3 Mon Sep 17 00:00:00 2001 From: jschoeny Date: Wed, 17 Dec 2025 08:41:06 -1000 Subject: [PATCH 03/23] Fix author name rendering --- src/app/hack/[slug]/page.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/app/hack/[slug]/page.tsx b/src/app/hack/[slug]/page.tsx index 8db968f..f8b7a0f 100644 --- a/src/app/hack/[slug]/page.tsx +++ b/src/app/hack/[slug]/page.tsx @@ -58,7 +58,7 @@ export async function generateMetadata({ params }: HackDetailProps): Promise )} -
- {!isArchive && ( +
+ {!hack.original_author && ( )} -

By {isArchive ? (hack.original_author || "Unknown") : author}

+

By {author}

{hack.summary}

From 2b8322d585a0c3445016e7092c4662671ea9ee33 Mon Sep 17 00:00:00 2001 From: Jared Schoeny Date: Wed, 17 Dec 2025 11:45:13 -1000 Subject: [PATCH 04/23] Change "Newest" sorting --- src/components/Discover/DiscoverBrowser.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/Discover/DiscoverBrowser.tsx b/src/components/Discover/DiscoverBrowser.tsx index adc9c92..4d02244 100644 --- a/src/components/Discover/DiscoverBrowser.tsx +++ b/src/components/Discover/DiscoverBrowser.tsx @@ -56,7 +56,7 @@ export default function DiscoverBrowser() { setLoadingTags(true); let query = supabase .from("hacks") - .select("slug,title,summary,description,base_rom,downloads,created_by,updated_at,current_patch,original_author"); + .select("slug,title,summary,description,base_rom,downloads,created_by,updated_at,current_patch,original_author,approved_at"); if (sort === "popular") { // When sorting by popularity, always show non-archive hacks first. @@ -68,7 +68,7 @@ export default function DiscoverBrowser() { } else if (sort === "updated") { query = query.order("updated_at", { ascending: false }); } else { - query = query.order("created_at", { ascending: false }); + query = query.order("approved_at", { ascending: false }); } const { data: rows } = await query; From bd22894475f3e0308c4c492937b9868adde43f69 Mon Sep 17 00:00:00 2001 From: Jared Schoeny Date: Wed, 17 Dec 2025 20:43:21 -1000 Subject: [PATCH 05/23] Differentiate pending hacks missing patch --- src/app/dashboard/page.tsx | 29 ++++++++++++++++++++++++----- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/src/app/dashboard/page.tsx b/src/app/dashboard/page.tsx index a513eda..7c2cb17 100644 --- a/src/app/dashboard/page.tsx +++ b/src/app/dashboard/page.tsx @@ -1,7 +1,7 @@ import { createClient } from "@/utils/supabase/server"; import Link from "next/link"; import { redirect } from "next/navigation"; -import { FiExternalLink } from "react-icons/fi"; +import { FiAlertTriangle, FiExternalLink } from "react-icons/fi"; import DashboardClient from "@/components/Dashboard/DashboardClient"; import ArchiverManagement from "@/components/Dashboard/ArchiverManagement"; import { getDownloadsSeriesAll } from "./actions"; @@ -85,7 +85,6 @@ export default async function DashboardPage() {
{pendingHacks.map((h) => { - console.log(h.created_at); const createdDate = h.created_at ? new Date(h.created_at).toLocaleTimeString(undefined, { year: "numeric", @@ -96,20 +95,32 @@ export default async function DashboardPage() { }) : "Unknown"; const creator = h.creator_username ? `@${h.creator_username}` : "Unknown"; + const hasMissingPatch = h.current_patch === null; + const bgClasses = hasMissingPatch + ? "bg-amber-50/5 dark:bg-amber-900/5 hover:bg-amber-400/10 dark:hover:bg-amber-600/10" + : "bg-amber-500/5 hover:bg-amber-500/10"; return ( {/* Desktop row */}
-
{h.title}
+
+
{h.title}
+ {hasMissingPatch && ( +
+ + Missing Patch +
+ )} +
/{h.slug}
@@ -126,7 +137,15 @@ export default async function DashboardPage() {
-
{h.title}
+
+
{h.title}
+ {hasMissingPatch && ( +
+ + Missing Patch +
+ )} +
/{h.slug}
{creator} From b3a146d35c88cc5b10a83352118ba7adc026daf6 Mon Sep 17 00:00:00 2001 From: Jared Schoeny Date: Wed, 17 Dec 2025 20:50:18 -1000 Subject: [PATCH 06/23] Add num pending hacks badge --- src/app/dashboard/page.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/app/dashboard/page.tsx b/src/app/dashboard/page.tsx index 7c2cb17..1975ae1 100644 --- a/src/app/dashboard/page.tsx +++ b/src/app/dashboard/page.tsx @@ -75,7 +75,12 @@ export default async function DashboardPage() { {pendingHacks.length > 0 && (
-

Pending hacks

+
+

Pending hacks

+ + {pendingHacks.length} + +
{/* Header row (desktop only) */}
From c5597e48f1b834cd1f08b79eea18497e56c19c5e Mon Sep 17 00:00:00 2001 From: jschoeny Date: Wed, 17 Dec 2025 22:48:09 -1000 Subject: [PATCH 07/23] Add language to hack page under details --- src/app/hack/[slug]/page.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/app/hack/[slug]/page.tsx b/src/app/hack/[slug]/page.tsx index f8b7a0f..69e113e 100644 --- a/src/app/hack/[slug]/page.tsx +++ b/src/app/hack/[slug]/page.tsx @@ -118,7 +118,7 @@ export default async function HackDetail({ params }: HackDetailProps) { const supabase = await createClient(); const { data: hack, error } = await supabase .from("hacks") - .select("slug,title,summary,description,base_rom,created_at,updated_at,downloads,current_patch,box_art,social_links,created_by,approved,original_author,permission_from") + .select("slug,title,summary,description,base_rom,created_at,updated_at,downloads,current_patch,box_art,social_links,created_by,approved,original_author,permission_from,language") .eq("slug", slug) .maybeSingle(); if (error || !hack) return notFound(); @@ -419,6 +419,7 @@ export default async function HackDetail({ params }: HackDetailProps) {

Details

    +
  • Language: {hack.language || "Unknown"}
  • Base ROM: {baseRom?.name || "Unknown"}
  • Created: {new Date(hack.created_at).toLocaleDateString()}
  • {lastUpdated &&
  • Last updated: {lastUpdated}
  • } From acff75da0cb05417aca15dcbb3a1cc5bb84107ea Mon Sep 17 00:00:00 2001 From: Jared Schoeny Date: Thu, 18 Dec 2025 00:53:18 -1000 Subject: [PATCH 08/23] Add user's name to admin pending hacks list --- src/app/dashboard/page.tsx | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/app/dashboard/page.tsx b/src/app/dashboard/page.tsx index 1975ae1..45fe4f1 100644 --- a/src/app/dashboard/page.tsx +++ b/src/app/dashboard/page.tsx @@ -14,7 +14,7 @@ export default async function DashboardPage() { if (!user) redirect("/login"); const { data: isAdmin } = await supa.rpc("is_admin"); - let pendingHacks: (HackRow & { created_by: string; creator_username: string | null })[] = []; + let pendingHacks: (HackRow & { created_by: string; creator_username: string | null; creator_full_name: string | null })[] = []; if (isAdmin) { const { data: pendingHacksData } = await supa .from("hacks") @@ -27,15 +27,20 @@ export default async function DashboardPage() { const creatorIds = [...new Set(pendingHacksData.map(h => h.created_by as string))]; const { data: profiles } = await supa .from("profiles") - .select("id,username") + .select("id,username,full_name") .in("id", creatorIds); const usernameById = new Map(); - (profiles || []).forEach((p) => usernameById.set(p.id, p.username)); + const fullNameById = new Map(); + (profiles || []).forEach((p) => { + usernameById.set(p.id, p.username); + fullNameById.set(p.id, p.full_name); + }); pendingHacks = pendingHacksData.map((h) => ({ ...h, creator_username: usernameById.get(h.created_by as string) || null, + creator_full_name: fullNameById.get(h.created_by as string) || null, })); } } @@ -130,7 +135,10 @@ export default async function DashboardPage() {
-
{creator}
+
+ {h.creator_full_name &&
{h.creator_full_name}
} +
{creator}
+
{createdDate}
From cf87e1d1ff77fd7fe27b7a2f17d5d2e6cd47f99e Mon Sep 17 00:00:00 2001 From: Jared Schoeny Date: Thu, 18 Dec 2025 01:20:38 -1000 Subject: [PATCH 09/23] Add alphabetical sorting to `DiscoverBrowser` --- src/components/Discover/DiscoverBrowser.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/components/Discover/DiscoverBrowser.tsx b/src/components/Discover/DiscoverBrowser.tsx index 4d02244..1da2553 100644 --- a/src/components/Discover/DiscoverBrowser.tsx +++ b/src/components/Discover/DiscoverBrowser.tsx @@ -67,6 +67,8 @@ export default function DiscoverBrowser() { .order("current_patch", { ascending: false, nullsFirst: false }); } else if (sort === "updated") { query = query.order("updated_at", { ascending: false }); + } else if (sort === "alphabetical") { + query = query.order("title", { ascending: true }); } else { query = query.order("approved_at", { ascending: false }); } @@ -305,6 +307,7 @@ export default function DiscoverBrowser() { +
From 0579c6aedd559eed9cb0d8a45ca1648ceda43457 Mon Sep 17 00:00:00 2001 From: Jared Schoeny Date: Thu, 18 Dec 2025 01:34:06 -1000 Subject: [PATCH 10/23] Add roadmap to footer --- src/components/Footer.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/components/Footer.tsx b/src/components/Footer.tsx index a7742c3..870f4ea 100644 --- a/src/components/Footer.tsx +++ b/src/components/Footer.tsx @@ -55,6 +55,9 @@ export default function Footer() {
  • GitHub
  • +
  • + Roadmap +
  • From 41d0a1e5733df0925e6001f2783e73690d96e635 Mon Sep 17 00:00:00 2001 From: Jared Schoeny Date: Thu, 18 Dec 2025 01:50:53 -1000 Subject: [PATCH 11/23] Make `HackCard` only float after click --- src/app/globals.css | 2 +- src/components/HackCard.tsx | 10 +++++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/app/globals.css b/src/app/globals.css index e2f1b3d..16a5fc0 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -154,7 +154,7 @@ html { scroll-padding-top: var(--anchor-offset); } 100% { transform: scale(1); } } -.anim-float:hover { animation: floatY 2.4s ease-in-out infinite; } +.anim-float { animation: floatY 2.4s ease-in-out infinite; } .anim-pulse { animation: pulseSoft 2.4s ease-in-out infinite; } .anim-pop { animation: popIn 220ms cubic-bezier(0.16, 1, 0.3, 1); } diff --git a/src/components/HackCard.tsx b/src/components/HackCard.tsx index 3e04677..a114f40 100644 --- a/src/components/HackCard.tsx +++ b/src/components/HackCard.tsx @@ -44,6 +44,7 @@ export default function HackCard({ hack, clickable = true, className = "" }: { h const [emblaRef, emblaApi] = useEmblaCarousel({ loop: true }); const [selectedIndex, setSelectedIndex] = useState(0); + const [isClicked, setIsClicked] = useState(false); const dragStartRef = useRef<{ x: number; y: number } | null>(null); const didDragRef = useRef(false); @@ -56,8 +57,14 @@ export default function HackCard({ hack, clickable = true, className = "" }: { h emblaApi.off("select", onSelect); }; }, [emblaApi]); + + useEffect(() => { + // Reset isClicked state when component mounts + setIsClicked(false); + }, []); + const cardClass = `rounded-[12px] overflow-hidden h-full ${ - clickable ? "transition-transform duration-300 hover:-translate-y-0.5 hover:shadow-xl anim-float" : "" + clickable ? `transition-transform duration-300 hover:-translate-y-0.5 hover:shadow-xl ${isClicked ? "anim-float" : ""}` : "" } ring-1 ${ready ? "ring-emerald-400/50 bg-emerald-500/10" : "card ring-[var(--border)]"}`; const gradientBgClass = `bg-gradient-to-b ${ready ? 'from-emerald-300/5 to-emerald-400/30 dark:from-emerald-950/10 dark:to-emerald-600/40' : 'from-black/30 to-black/10 dark:from-black/80 dark:to-black/40'}`; const shadowClass = `shadow-xl ${ready ? "shadow-emerald-700/40 dark:shadow-emerald-200/40" : "shadow-slate-500/40 dark:shadow-slate-300/40"}`; @@ -219,6 +226,7 @@ export default function HackCard({ hack, clickable = true, className = "" }: { h e.stopPropagation(); didDragRef.current = false; } + setIsClicked(true); }} > {content} From 72ec1be6d3bd5bab67b8f6e236b0fcf37ec5aacd Mon Sep 17 00:00:00 2001 From: Jared Schoeny Date: Thu, 18 Dec 2025 02:19:56 -1000 Subject: [PATCH 12/23] Link `DiscoverBrowser` sort with query params --- src/app/discover/page.tsx | 15 +++++++++++-- src/components/Discover/DiscoverBrowser.tsx | 24 ++++++++++++++++++--- 2 files changed, 34 insertions(+), 5 deletions(-) diff --git a/src/app/discover/page.tsx b/src/app/discover/page.tsx index 8a8f9db..4d9483e 100644 --- a/src/app/discover/page.tsx +++ b/src/app/discover/page.tsx @@ -8,7 +8,18 @@ export const metadata: Metadata = { }, }; -export default function DiscoverPage() { +interface DiscoverPageProps { + searchParams: Promise>; +}; + +export default async function DiscoverPage(props: DiscoverPageProps) { + const searchParams = await props.searchParams; + const sortParam = searchParams.sort; + const sort = + typeof sortParam === "string" && ["popular", "new", "updated", "alphabetical"].includes(sortParam) + ? sortParam + : "popular"; // Default to popular if no sort param is provided + return (
    @@ -20,7 +31,7 @@ export default function DiscoverPage() {
    - +
    ); diff --git a/src/components/Discover/DiscoverBrowser.tsx b/src/components/Discover/DiscoverBrowser.tsx index 1da2553..63f4bb7 100644 --- a/src/components/Discover/DiscoverBrowser.tsx +++ b/src/components/Discover/DiscoverBrowser.tsx @@ -13,15 +13,24 @@ import { CATEGORY_ICONS } from "@/components/Icons/tagCategories"; import { useBaseRoms } from "@/contexts/BaseRomContext"; import { sortOrderedTags, OrderedTag, getCoverUrls } from "@/utils/format"; import { HackCardAttributes } from "@/components/HackCard"; +import { usePathname, useRouter, useSearchParams } from "next/navigation"; const HACKS_PER_PAGE = 9; -export default function DiscoverBrowser() { +interface DiscoverBrowserProps { + initialSort?: string; +} + +export default function DiscoverBrowser({ initialSort = "popular" }: DiscoverBrowserProps) { const supabase = createClient(); + const router = useRouter(); + const pathname = usePathname(); + const searchParams = useSearchParams(); + const [query, setQuery] = React.useState(""); const [selectedTags, setSelectedTags] = React.useState([]); const [selectedBaseRoms, setSelectedBaseRoms] = React.useState([]); - const [sort, setSort] = React.useState("popular"); + const [sort, setSort] = React.useState(initialSort); const [hacks, setHacks] = React.useState([]); const [tagGroups, setTagGroups] = React.useState>({}); const [ungroupedTags, setUngroupedTags] = React.useState([]); @@ -301,7 +310,16 @@ export default function DiscoverBrowser() {
    { - const nextSort = e.target.value; + const nextSort = e.target.value as DiscoverSortOption; setSort(nextSort); // Keep URL query param in sync so refresh/back preserves sort const current = searchParams ? new URLSearchParams(searchParams.toString()) : new URLSearchParams(); @@ -322,6 +195,7 @@ export default function DiscoverBrowser({ initialSort = "popular" }: DiscoverBro }} className="h-11 rounded-md bg-[var(--surface-2)] px-3 text-sm ring-1 ring-inset ring-[var(--border)] focus:outline-none focus:ring-2 focus:ring-[var(--ring)]" > + diff --git a/src/types/discover.ts b/src/types/discover.ts new file mode 100644 index 0000000..ec793e5 --- /dev/null +++ b/src/types/discover.ts @@ -0,0 +1,3 @@ +export type DiscoverSortOption = "trending" | "popular" | "new" | "updated" | "alphabetical"; + + From b360b5f62fd56ee40adf27638ba964f2846445f9 Mon Sep 17 00:00:00 2001 From: Jared Schoeny Date: Thu, 18 Dec 2025 23:53:48 -1000 Subject: [PATCH 15/23] Update discover page subtitle messaging --- src/app/discover/page.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/discover/page.tsx b/src/app/discover/page.tsx index 63bec60..d8e9b0e 100644 --- a/src/app/discover/page.tsx +++ b/src/app/discover/page.tsx @@ -27,8 +27,8 @@ export default async function DiscoverPage(props: DiscoverPageProps) {

    Discover ROM hacks

    -

    - Browse curated patches. See what others are downloading. +

    + Hackdex supports developers by only hosting hacks that have been uploaded by the person or team that created them. By using this site, you are supporting the original creators and their labors of love.

    From 42a5928e4132275904a67a13e8c6785ee463d7d1 Mon Sep 17 00:00:00 2001 From: Jared Schoeny Date: Fri, 19 Dec 2025 00:03:33 -1000 Subject: [PATCH 16/23] Add icons to sort dropdown in DiscoverBrowser --- src/components/Discover/DiscoverBrowser.tsx | 65 ++++++++++++++------- 1 file changed, 45 insertions(+), 20 deletions(-) diff --git a/src/components/Discover/DiscoverBrowser.tsx b/src/components/Discover/DiscoverBrowser.tsx index e15d99a..fcdcefd 100644 --- a/src/components/Discover/DiscoverBrowser.tsx +++ b/src/components/Discover/DiscoverBrowser.tsx @@ -6,7 +6,14 @@ import { baseRoms } from "@/data/baseRoms"; import { Listbox, ListboxButton, ListboxOption, ListboxOptions, Transition } from "@headlessui/react"; import { useFloating, offset, flip, shift, size, autoUpdate } from "@floating-ui/react"; import { IconType } from "react-icons"; -import { MdTune } from "react-icons/md"; +import { + MdTune, + MdWhatshot, + MdTrendingUp, + MdNewReleases, + MdUpdate, + MdSortByAlpha, +} from "react-icons/md"; import { BsSdCardFill } from "react-icons/bs"; import { CATEGORY_ICONS } from "@/components/Icons/tagCategories"; import { useBaseRoms } from "@/contexts/BaseRomContext"; @@ -15,6 +22,14 @@ import { usePathname, useRouter, useSearchParams } from "next/navigation"; import { getDiscoverData } from "@/app/discover/actions"; import type { DiscoverSortOption } from "@/types/discover"; +const SORT_ICON_MAP: Record = { + trending: MdWhatshot, + popular: MdTrendingUp, + new: MdNewReleases, + updated: MdUpdate, + alphabetical: MdSortByAlpha, +}; + const HACKS_PER_PAGE = 9; interface DiscoverBrowserProps { @@ -170,6 +185,11 @@ export default function DiscoverBrowser({ initialSort = "trending" }: DiscoverBr setSelectedBaseRoms([]); } + const sortIcon = React.useMemo(() => { + const SortIcon = SORT_ICON_MAP[sort]; + return SortIcon ?
    {/* Unified filter section: Base ROM dropdown first, category dropdowns next, ungrouped tags last */} From 345c8a2c0691c19188afa636e407dbf572dc81ff Mon Sep 17 00:00:00 2001 From: Jared Schoeny Date: Fri, 19 Dec 2025 00:18:28 -1000 Subject: [PATCH 17/23] Change `updated` sorting to use `current_patch.published_at` --- src/app/discover/actions.ts | 37 ++++++++++++++++++++++++++++++++++--- 1 file changed, 34 insertions(+), 3 deletions(-) diff --git a/src/app/discover/actions.ts b/src/app/discover/actions.ts index e934227..cd389b3 100644 --- a/src/app/discover/actions.ts +++ b/src/app/discover/actions.ts @@ -52,7 +52,7 @@ export async function getDiscoverData(sort: DiscoverSortOption): Promise(); + const publishedAtByPatchId = new Map(); if (patchIds.length > 0) { const { data: patchRows, error: patchesError } = await supabase .from("patches") - .select("id,version") + .select("id,version,published_at") .in("id", patchIds); if (patchesError) throw patchesError; (patchRows || []).forEach((p: any) => { if (typeof p.id === "number") { versionsByPatchId.set(p.id, p.version || "Pre-release"); + publishedAtByPatchId.set(p.id, p.published_at ?? null); } }); } @@ -186,14 +188,19 @@ export async function getDiscoverData(sort: DiscoverSortOption): Promise(); + const publishedAtBySlug = new Map(); (rows || []).forEach((r: any) => { if (typeof r.current_patch === "number") { const version = versionsByPatchId.get(r.current_patch) || "Pre-release"; mappedVersions.set(r.slug, version); + + const publishedAt = publishedAtByPatchId.get(r.current_patch) ?? null; + publishedAtBySlug.set(r.slug, publishedAt); } else { mappedVersions.set(r.slug, r.original_author ? "Archive" : "Pre-release"); + publishedAtBySlug.set(r.slug, null); } }); @@ -227,6 +234,30 @@ export async function getDiscoverData(sort: DiscoverSortOption): Promise { + const aPub = publishedAtBySlug.get(a.slug); + const bPub = publishedAtBySlug.get(b.slug); + + // Nulls (no published_at) go last + if (!aPub && !bPub) return 0; + if (!aPub) return 1; + if (!bPub) return -1; + + const aTime = new Date(aPub).getTime(); + const bTime = new Date(bPub).getTime(); + + // Secondary sort: when times are equal, push archives to end + if (aTime === bTime) { + if (a.isArchive && !b.isArchive) return 1; + if (!a.isArchive && b.isArchive) return -1; + } + + return bTime - aTime; // Descending order (newest first) + }); + } + // Sort by trending score if needed if (sort === "trending" && trendingScores) { mapped = [...mapped].sort((a, b) => { From ddddfdc1c3e7b70ea03a14d77840fbc0622edd07 Mon Sep 17 00:00:00 2001 From: Jared Schoeny Date: Fri, 19 Dec 2025 12:30:23 -1000 Subject: [PATCH 18/23] Update wording of StickyActionBar patching --- src/components/Hack/StickyActionBar.tsx | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/src/components/Hack/StickyActionBar.tsx b/src/components/Hack/StickyActionBar.tsx index f3ce6fe..bc5b74b 100644 --- a/src/components/Hack/StickyActionBar.tsx +++ b/src/components/Hack/StickyActionBar.tsx @@ -90,9 +90,15 @@ export default function StickyActionBar({
    {!termsAgreed || status === "downloading" ? ( -

    - By downloading this patch, you agree to the Terms of Service. -

    + !romReady ? ( +

    + To download the patch file, you must select a clean ROM for the patcher to use. +

    + ) : ( +

    + By downloading this patch, you agree to the Terms of Service. +

    + ) ) : ( uploadInputRef.current?.click()} - className="w-5/6 mx-auto md:w-auto md:mx-0 rounded-md border border-[var(--border)] bg-[var(--surface-2)] px-3 py-2 text-foreground text-sm md:text-xs cursor-pointer hover:bg-[var(--surface-3)] hover:text-foreground/80 disabled:cursor-not-allowed disabled:opacity-60" + className="shine-wrap btn-premium h-11 md:h-9 w-5/6 mx-auto md:w-auto md:mx-0 md:min-w-34 text-base md:text-sm font-semibold cursor-pointer disabled:cursor-not-allowed disabled:opacity-70" > {baseRomName ? ( Select {baseRomName} ROM @@ -135,15 +141,15 @@ export default function StickyActionBar({
    From d01881896bbd23f3a8b84fa8855c70290d5414bf Mon Sep 17 00:00:00 2001 From: Jared Schoeny Date: Fri, 19 Dec 2025 13:13:27 -1000 Subject: [PATCH 19/23] Fix missing tags bug by working around Supabase 1000 row limit --- src/app/discover/actions.ts | 51 ++++++++++++++++++++++++++----------- 1 file changed, 36 insertions(+), 15 deletions(-) diff --git a/src/app/discover/actions.ts b/src/app/discover/actions.ts index cd389b3..415ac77 100644 --- a/src/app/discover/actions.ts +++ b/src/app/discover/actions.ts @@ -92,22 +92,43 @@ export async function getDiscoverData(sort: DiscoverSortOption): Promise(); + const BATCH_SIZE = 1000; + let offset = 0; + let hasMore = true; - const tagsBySlug = new Map(); - (tagRows || []).forEach((r: any) => { - const arr = tagsBySlug.get(r.hack_slug) || []; - arr.push({ - name: r.tags.name, - order: r.order, - }); - tagsBySlug.set(r.hack_slug, arr); - }); + while (hasMore) { + const { data: tagRows, error: tagsError } = await supabase + .from("hack_tags") + .select("hack_slug,order,tags(name,category)") + .in("hack_slug", slugs) + .range(offset, offset + BATCH_SIZE - 1) + .order("hack_slug", { ascending: true }); + + if (tagsError) throw tagsError; + + if (!tagRows || tagRows.length === 0) { + hasMore = false; + } else { + tagRows.forEach((r: any) => { + if (!r.tags?.name) return; + const arr = tagsBySlug.get(r.hack_slug) || []; + arr.push({ + name: r.tags.name, + order: r.order, + }); + tagsBySlug.set(r.hack_slug, arr); + }); + + // If we got fewer rows than the batch size, we've reached the end + if (tagRows.length < BATCH_SIZE) { + hasMore = false; + } else { + offset += BATCH_SIZE; + } + } + } // Fetch patches for version mapping const patchIds = Array.from( From ded389362011024b8c7803a085148b7219653780 Mon Sep 17 00:00:00 2001 From: Jared Schoeny Date: Fri, 19 Dec 2025 15:33:02 -1000 Subject: [PATCH 20/23] Fix trending sort being limited by 1000 patch_downloads rows --- src/app/discover/actions.ts | 44 ++++++++++++++++++++----------------- 1 file changed, 24 insertions(+), 20 deletions(-) diff --git a/src/app/discover/actions.ts b/src/app/discover/actions.ts index 415ac77..6d88210 100644 --- a/src/app/discover/actions.ts +++ b/src/app/discover/actions.ts @@ -159,19 +159,20 @@ export async function getDiscoverData(sort: DiscoverSortOption): Promise | null = null; if (sort === "trending") { - // Get all patches for all hacks + // Get all patches for all hacks, grouped by slug const { data: allPatches, error: allPatchesError } = await supabase .from("patches") .select("id,parent_hack") .in("parent_hack", slugs); if (allPatchesError) throw allPatchesError; - const patchIdToSlug = new Map(); - const allPatchIds: number[] = []; + // Group patch IDs by parent_hack (slug) + const patchIdsBySlug = new Map(); (allPatches || []).forEach((p: any) => { if (typeof p.id === "number" && p.parent_hack) { - patchIdToSlug.set(p.id, p.parent_hack); - allPatchIds.push(p.id); + const arr = patchIdsBySlug.get(p.parent_hack) || []; + arr.push(p.id); + patchIdsBySlug.set(p.parent_hack, arr); } }); @@ -181,22 +182,25 @@ export async function getDiscoverData(sort: DiscoverSortOption): Promise(); - if (allPatchIds.length > 0) { - const { data: recentDownloads, error: downloadsError } = await supabase - .from("patch_downloads") - .select("patch,created_at") - .in("patch", allPatchIds) - .gte("created_at", sinceISO); - if (downloadsError) throw downloadsError; - (recentDownloads || []).forEach((dl: any) => { - const pid = dl.patch as number | null; - if (!pid) return; - const slug = patchIdToSlug.get(pid); - if (!slug) return; - recentDownloadsBySlug.set(slug, (recentDownloadsBySlug.get(slug) || 0) + 1); - }); - } + // Query download counts per slug using head: true with count: 'exact' + // This avoids fetching all download rows and just gets counts + // One query per slug instead of one per patch + const downloadCountPromises = Array.from(patchIdsBySlug.entries()).map(async ([slug, patchIds]) => { + const { count, error } = await supabase + .from("patch_downloads") + .select("*", { count: "exact", head: true }) + .in("patch", patchIds) + .gte("created_at", sinceISO); + + if (error) throw error; + return { slug, count: count || 0 }; + }); + + const downloadCounts = await Promise.all(downloadCountPromises); + downloadCounts.forEach(({ slug, count }) => { + recentDownloadsBySlug.set(slug, count); + }); // Calculate trending scores: recent_downloads_window + (8 * log(downloads + 1)) // Give small boost to longer lived popular hacks From 5b57b12e0e483f864fa98b94e8e5de34deddcb31 Mon Sep 17 00:00:00 2001 From: Jared Schoeny Date: Fri, 19 Dec 2025 15:48:11 -1000 Subject: [PATCH 21/23] Add simple "More from @username" card to hack page --- src/app/hack/[slug]/page.tsx | 41 +++++++++++++++++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/src/app/hack/[slug]/page.tsx b/src/app/hack/[slug]/page.tsx index 69e113e..6a70fd7 100644 --- a/src/app/hack/[slug]/page.tsx +++ b/src/app/hack/[slug]/page.tsx @@ -1,5 +1,6 @@ import { baseRoms, PLATFORM_NAMES } from "@/data/baseRoms"; import type { Metadata } from "next"; +import Link from "next/link"; import { notFound } from "next/navigation"; import Gallery from "@/components/Hack/Gallery"; import HackActions from "@/components/Hack/HackActions"; @@ -7,7 +8,7 @@ import ReactMarkdown from "react-markdown"; import remarkGfm from "remark-gfm"; import rehypeSlug from "rehype-slug"; import Image from "next/image"; -import { FaDiscord, FaTwitter, FaTriangleExclamation } from "react-icons/fa6"; +import { FaDiscord, FaTwitter, FaTriangleExclamation, FaArrowUpRightFromSquare } from "react-icons/fa6"; import PokeCommunityIcon from "@/components/Icons/PokeCommunityIcon"; import { createClient, createServiceClient } from "@/utils/supabase/server"; import HackOptionsMenu from "@/components/Hack/HackOptionsMenu"; @@ -154,6 +155,16 @@ export default async function HackDetail({ params }: HackDetailProps) { .maybeSingle(); const author = hack.original_author ? hack.original_author : (profile?.username ? `@${profile.username}` : "Unknown"); + // Get other approved hacks by the same author + const { data: otherHacks } = await supabase + .from("hacks") + .select("slug,title,summary") + .eq("created_by", hack.created_by) + .eq("approved", true) + .neq("slug", hack.slug) + .order("downloads", { ascending: false }) + .limit(10); + const { data: { user }, } = await supabase.auth.getUser(); @@ -463,6 +474,34 @@ export default async function HackDetail({ params }: HackDetailProps) {
    )} + {otherHacks && otherHacks.length > 0 && ( +
    +

    More from {author}

    +
      + {otherHacks.map((otherHack) => ( +
    • + +
      + + {otherHack.title} + + +
      + {otherHack.summary && ( +

      + {otherHack.summary} +

      + )} + +
    • + ))} +
    +
    + )} {isInformationalArchive ? (

    From c079bf3ab92c02e3a6aefbea12e6c88325aec9e0 Mon Sep 17 00:00:00 2001 From: Jared Schoeny Date: Fri, 19 Dec 2025 20:14:15 -1000 Subject: [PATCH 22/23] Fix too many pagiantion buttons --- src/components/Discover/DiscoverBrowser.tsx | 123 +++++++++++++++----- 1 file changed, 92 insertions(+), 31 deletions(-) diff --git a/src/components/Discover/DiscoverBrowser.tsx b/src/components/Discover/DiscoverBrowser.tsx index fcdcefd..dae633a 100644 --- a/src/components/Discover/DiscoverBrowser.tsx +++ b/src/components/Discover/DiscoverBrowser.tsx @@ -13,7 +13,10 @@ import { MdNewReleases, MdUpdate, MdSortByAlpha, + MdChevronLeft, + MdChevronRight, } from "react-icons/md"; +import { IoEllipsisHorizontal } from "react-icons/io5"; import { BsSdCardFill } from "react-icons/bs"; import { CATEGORY_ICONS } from "@/components/Icons/tagCategories"; import { useBaseRoms } from "@/contexts/BaseRomContext"; @@ -169,6 +172,43 @@ export default function DiscoverBrowser({ initialSort = "trending" }: DiscoverBr [scrollToListTopOnMobile, totalPages] ); + const getPageNumbers = React.useCallback((current: number, total: number): (number | string)[] => { + // If 7 or fewer pages, show all + if (total <= 6) { + return Array.from({ length: total }, (_, i) => i + 1); + } + + const pages: (number | string)[] = []; + + // Always show first page + pages.push(1); + + if (current <= 3) { + // Near the start: show 1, 2, 3, 4, ..., last + for (let i = 2; i <= 4; i++) { + pages.push(i); + } + pages.push("ellipsis"); + pages.push(total); + } else if (current >= total - 2) { + // Near the end: show 1, ..., total-3, total-2, total-1, total + pages.push("ellipsis"); + for (let i = total - 3; i <= total; i++) { + pages.push(i); + } + } else { + // In the middle: show 1, ..., current-1, current, current+1, ..., last + pages.push("ellipsis"); + pages.push(current - 1); + pages.push(current); + pages.push(current + 1); + pages.push("ellipsis"); + pages.push(total); + } + + return pages; + }, []); + function toggleTag(name: string) { setSelectedTags((prev) => (prev.includes(name) ? prev.filter((t) => t !== name) : [...prev, name])); } @@ -383,32 +423,65 @@ export default function DiscoverBrowser({ initialSort = "trending" }: DiscoverBr

    Showing {paginationRange.startIndex + 1}-{paginationRange.endIndex} of {filtered.length} hacks
    -
    - -
    - {Array.from({ length: totalPages }).map((_, i) => { - const page = i + 1; +
    + {/* Mobile: Previous/Next buttons row, Desktop: unwraps with contents */} +
    + + +
    + {/* Page numbers - order-2 on mobile (below buttons), order-2 on desktop (between Previous and Next) */} +
    + {getPageNumbers(currentPage, totalPages).map((item, i) => { + if (item === "ellipsis") { + return ( + + ); + } + const page = item as number; const isActive = page === currentPage; return (
    -
    )} From a013565c6f956417aba89c59ccb0e0bd598a71f0 Mon Sep 17 00:00:00 2001 From: Jared Schoeny Date: Fri, 19 Dec 2025 20:16:25 -1000 Subject: [PATCH 23/23] Fix discover sort selector on mobile --- src/components/Discover/DiscoverBrowser.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Discover/DiscoverBrowser.tsx b/src/components/Discover/DiscoverBrowser.tsx index dae633a..b4093f7 100644 --- a/src/components/Discover/DiscoverBrowser.tsx +++ b/src/components/Discover/DiscoverBrowser.tsx @@ -257,7 +257,7 @@ export default function DiscoverBrowser({ initialSort = "trending" }: DiscoverBr const url = queryString ? `${pathname}?${queryString}` : pathname; router.replace(url); }} - className="h-full bg-transparent pl-1 pr-0 text-sm text-foreground focus:outline-none focus:ring-0" + className="w-full h-full bg-transparent pl-1 pr-0 text-sm text-foreground focus:outline-none focus:ring-0" >