From 36642da376b7333713c4d5f4054c23c734276529 Mon Sep 17 00:00:00 2001 From: Script Raccoon Date: Fri, 10 Apr 2026 00:26:55 +0200 Subject: [PATCH 01/15] create endpoint to create issues via GitHub API --- .env.example | 3 +- package.json | 1 + pnpm-lock.yaml | 248 ++++++++++++++++++++++++++++++++ src/lib/server/ratelimit.ts | 45 ++++++ src/routes/api/issue/+server.ts | 60 ++++++++ src/routes/api/issue/config.ts | 4 + 6 files changed, 360 insertions(+), 1 deletion(-) create mode 100644 src/lib/server/ratelimit.ts create mode 100644 src/routes/api/issue/+server.ts create mode 100644 src/routes/api/issue/config.ts diff --git a/.env.example b/.env.example index c63a11d0..e72b34a4 100644 --- a/.env.example +++ b/.env.example @@ -1,4 +1,5 @@ DB_URL = file:database/local.db # location of local database DB_AUTH_TOKEN = # you can leave this empty for local development DB_VISITS_AUTH_TOKEN = # you can leave this empty for local development -DB_VISITS_URL = file:database/visits.db # location of local database with site visits \ No newline at end of file +DB_VISITS_URL = file:database/visits.db # location of local database with site visits +GITHUB_PRIVATE_KEY = # you can leave this empty for local development \ No newline at end of file diff --git a/package.json b/package.json index 7e2819b1..4324b631 100644 --- a/package.json +++ b/package.json @@ -47,6 +47,7 @@ "@fortawesome/free-regular-svg-icons": "^7.2.0", "@fortawesome/free-solid-svg-icons": "^7.2.0", "@libsql/client": "^0.17.2", + "@octokit/app": "^16.1.2", "chart.js": "^4.5.1", "es6-crawler-detect": "^4.0.2", "katex": "^0.16.44", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c1fca712..55c9fc61 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -21,6 +21,9 @@ importers: '@libsql/client': specifier: ^0.17.2 version: 0.17.2 + '@octokit/app': + specifier: ^16.1.2 + version: 16.1.2 chart.js: specifier: ^4.5.1 version: 4.5.1 @@ -509,6 +512,89 @@ packages: '@neon-rs/load@0.0.4': resolution: {integrity: sha512-kTPhdZyTQxB+2wpiRcFWrDcejc4JI6tkPuS7UZCG4l6Zvc5kU/gGQ/ozvHTh1XR5tS+UlfAfGuPajjzQjCiHCw==} + '@octokit/app@16.1.2': + resolution: {integrity: sha512-8j7sEpUYVj18dxvh0KWj6W/l6uAiVRBl1JBDVRqH1VHKAO/G5eRVl4yEoYACjakWers1DjUkcCHyJNQK47JqyQ==} + engines: {node: '>= 20'} + + '@octokit/auth-app@8.2.0': + resolution: {integrity: sha512-vVjdtQQwomrZ4V46B9LaCsxsySxGoHsyw6IYBov/TqJVROrlYdyNgw5q6tQbB7KZt53v1l1W53RiqTvpzL907g==} + engines: {node: '>= 20'} + + '@octokit/auth-oauth-app@9.0.3': + resolution: {integrity: sha512-+yoFQquaF8OxJSxTb7rnytBIC2ZLbLqA/yb71I4ZXT9+Slw4TziV9j/kyGhUFRRTF2+7WlnIWsePZCWHs+OGjg==} + engines: {node: '>= 20'} + + '@octokit/auth-oauth-device@8.0.3': + resolution: {integrity: sha512-zh2W0mKKMh/VWZhSqlaCzY7qFyrgd9oTWmTmHaXnHNeQRCZr/CXy2jCgHo4e4dJVTiuxP5dLa0YM5p5QVhJHbw==} + engines: {node: '>= 20'} + + '@octokit/auth-oauth-user@6.0.2': + resolution: {integrity: sha512-qLoPPc6E6GJoz3XeDG/pnDhJpTkODTGG4kY0/Py154i/I003O9NazkrwJwRuzgCalhzyIeWQ+6MDvkUmKXjg/A==} + engines: {node: '>= 20'} + + '@octokit/auth-token@6.0.0': + resolution: {integrity: sha512-P4YJBPdPSpWTQ1NU4XYdvHvXJJDxM6YwpS0FZHRgP7YFkdVxsWcpWGy/NVqlAA7PcPCnMacXlRm1y2PFZRWL/w==} + engines: {node: '>= 20'} + + '@octokit/auth-unauthenticated@7.0.3': + resolution: {integrity: sha512-8Jb1mtUdmBHL7lGmop9mU9ArMRUTRhg8vp0T1VtZ4yd9vEm3zcLwmjQkhNEduKawOOORie61xhtYIhTDN+ZQ3g==} + engines: {node: '>= 20'} + + '@octokit/core@7.0.6': + resolution: {integrity: sha512-DhGl4xMVFGVIyMwswXeyzdL4uXD5OGILGX5N8Y+f6W7LhC1Ze2poSNrkF/fedpVDHEEZ+PHFW0vL14I+mm8K3Q==} + engines: {node: '>= 20'} + + '@octokit/endpoint@11.0.3': + resolution: {integrity: sha512-FWFlNxghg4HrXkD3ifYbS/IdL/mDHjh9QcsNyhQjN8dplUoZbejsdpmuqdA76nxj2xoWPs7p8uX2SNr9rYu0Ag==} + engines: {node: '>= 20'} + + '@octokit/graphql@9.0.3': + resolution: {integrity: sha512-grAEuupr/C1rALFnXTv6ZQhFuL1D8G5y8CN04RgrO4FIPMrtm+mcZzFG7dcBm+nq+1ppNixu+Jd78aeJOYxlGA==} + engines: {node: '>= 20'} + + '@octokit/oauth-app@8.0.3': + resolution: {integrity: sha512-jnAjvTsPepyUaMu9e69hYBuozEPgYqP4Z3UnpmvoIzHDpf8EXDGvTY1l1jK0RsZ194oRd+k6Hm13oRU8EoDFwg==} + engines: {node: '>= 20'} + + '@octokit/oauth-authorization-url@8.0.0': + resolution: {integrity: sha512-7QoLPRh/ssEA/HuHBHdVdSgF8xNLz/Bc5m9fZkArJE5bb6NmVkDm3anKxXPmN1zh6b5WKZPRr3697xKT/yM3qQ==} + engines: {node: '>= 20'} + + '@octokit/oauth-methods@6.0.2': + resolution: {integrity: sha512-HiNOO3MqLxlt5Da5bZbLV8Zarnphi4y9XehrbaFMkcoJ+FL7sMxH/UlUsCVxpddVu4qvNDrBdaTVE2o4ITK8ng==} + engines: {node: '>= 20'} + + '@octokit/openapi-types@27.0.0': + resolution: {integrity: sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA==} + + '@octokit/openapi-webhooks-types@12.1.0': + resolution: {integrity: sha512-WiuzhOsiOvb7W3Pvmhf8d2C6qaLHXrWiLBP4nJ/4kydu+wpagV5Fkz9RfQwV2afYzv3PB+3xYgp4mAdNGjDprA==} + + '@octokit/plugin-paginate-rest@14.0.0': + resolution: {integrity: sha512-fNVRE7ufJiAA3XUrha2omTA39M6IXIc6GIZLvlbsm8QOQCYvpq/LkMNGyFlB1d8hTDzsAXa3OKtybdMAYsV/fw==} + engines: {node: '>= 20'} + peerDependencies: + '@octokit/core': '>=6' + + '@octokit/request-error@7.1.0': + resolution: {integrity: sha512-KMQIfq5sOPpkQYajXHwnhjCC0slzCNScLHs9JafXc4RAJI+9f+jNDlBNaIMTvazOPLgb4BnlhGJOTbnN0wIjPw==} + engines: {node: '>= 20'} + + '@octokit/request@10.0.8': + resolution: {integrity: sha512-SJZNwY9pur9Agf7l87ywFi14W+Hd9Jg6Ifivsd33+/bGUQIjNujdFiXII2/qSlN2ybqUHfp5xpekMEjIBTjlSw==} + engines: {node: '>= 20'} + + '@octokit/types@16.0.0': + resolution: {integrity: sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg==} + + '@octokit/webhooks-methods@6.0.0': + resolution: {integrity: sha512-MFlzzoDJVw/GcbfzVC1RLR36QqkTLUf79vLVO3D+xn7r0QgxnFoLZgtrzxiQErAjFUOdH6fas2KeQJ1yr/qaXQ==} + engines: {node: '>= 20'} + + '@octokit/webhooks@14.2.0': + resolution: {integrity: sha512-da6KbdNCV5sr1/txD896V+6W0iamFWrvVl8cHkBSPT+YlvmT3DwXa4jxZnQc+gnuTEqSWbBeoSZYTayXH9wXcw==} + engines: {node: '>= 20'} + '@opentelemetry/api@1.9.0': resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==} engines: {node: '>=8.0.0'} @@ -656,6 +742,9 @@ packages: '@tybys/wasm-util@0.10.1': resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} + '@types/aws-lambda@8.10.161': + resolution: {integrity: sha512-rUYdp+MQwSFocxIOcSsYSF3YYYC/uUpMbCY/mbO21vGqfrEYvNSoPyKYDj6RhXXpPfS0KstW9RwG3qXh9sL7FQ==} + '@types/cookie@0.6.0': resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==} @@ -691,6 +780,9 @@ packages: resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} engines: {node: '>= 0.4'} + before-after-hook@4.0.0: + resolution: {integrity: sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ==} + chart.js@4.5.1: resolution: {integrity: sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==} engines: {pnpm: '>=8'} @@ -760,6 +852,9 @@ packages: esrap@2.2.4: resolution: {integrity: sha512-suICpxAmZ9A8bzJjEl/+rLJiDKC0X4gYWUxT6URAWBLvlXmtbZd5ySMu/N2ZGEtMCAmflUDPSehrP9BQcsGcSg==} + fast-content-type-parse@3.0.0: + resolution: {integrity: sha512-ZvLdcY8P+N8mGQJahJV5G4U88CSvT1rP8ApL6uETe88MBXrBHAkZlSEySdUlyztF7ccb+Znos3TFqaepHxdhBg==} + fdir@6.5.0: resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} engines: {node: '>=12.0.0'} @@ -800,6 +895,9 @@ packages: js-base64@3.7.8: resolution: {integrity: sha512-hNngCeKxIUQiEUN3GPJOkz4wF/YvdUdbNL9hsBcMQTkKzboD7T/q3OYOuuPZLUE6dBxSGpwhk5mwuDud7JVAow==} + json-with-bigint@3.5.8: + resolution: {integrity: sha512-eq/4KP6K34kwa7TcFdtvnftvHCD9KvHOGGICWwMFc4dOOKF5t4iYqnfLK8otCRCRv06FXOzGGyqE8h8ElMvvdw==} + katex@0.16.44: resolution: {integrity: sha512-EkxoDTk8ufHqHlf9QxGwcxeLkWRR3iOuYfRpfORgYfqc8s13bgb+YtRY59NK5ZpRaCwq1kqA6a5lpX8C/eLphQ==} hasBin: true @@ -1014,6 +1112,10 @@ packages: resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} engines: {node: '>=12.0.0'} + toad-cache@3.7.0: + resolution: {integrity: sha512-/m8M+2BJUpoJdgAHoG+baCwBT+tf2VraSfkBgl0Y00qIWt41DJ8R5B8nsEw0I58YwF5IZH6z24/2TobDKnqSWw==} + engines: {node: '>=12'} + totalist@3.0.1: resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} engines: {node: '>=6'} @@ -1037,6 +1139,12 @@ packages: undici-types@7.18.2: resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==} + universal-github-app-jwt@2.2.2: + resolution: {integrity: sha512-dcmbeSrOdTnsjGjUfAlqNDJrhxXizjAz94ija9Qw8YkZ1uu0d+GoZzyH+Jb9tIIqvGsadUfwg+22k5aDqqwzbw==} + + universal-user-agent@7.0.3: + resolution: {integrity: sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A==} + vite@8.0.5: resolution: {integrity: sha512-nmu43Qvq9UopTRfMx2jOYW5l16pb3iDC1JH6yMuPkpVbzK0k+L7dfsEDH4jRgYFmsg0sTAqkojoZgzLMlwHsCQ==} engines: {node: ^20.19.0 || >=22.12.0} @@ -1396,6 +1504,132 @@ snapshots: '@neon-rs/load@0.0.4': {} + '@octokit/app@16.1.2': + dependencies: + '@octokit/auth-app': 8.2.0 + '@octokit/auth-unauthenticated': 7.0.3 + '@octokit/core': 7.0.6 + '@octokit/oauth-app': 8.0.3 + '@octokit/plugin-paginate-rest': 14.0.0(@octokit/core@7.0.6) + '@octokit/types': 16.0.0 + '@octokit/webhooks': 14.2.0 + + '@octokit/auth-app@8.2.0': + dependencies: + '@octokit/auth-oauth-app': 9.0.3 + '@octokit/auth-oauth-user': 6.0.2 + '@octokit/request': 10.0.8 + '@octokit/request-error': 7.1.0 + '@octokit/types': 16.0.0 + toad-cache: 3.7.0 + universal-github-app-jwt: 2.2.2 + universal-user-agent: 7.0.3 + + '@octokit/auth-oauth-app@9.0.3': + dependencies: + '@octokit/auth-oauth-device': 8.0.3 + '@octokit/auth-oauth-user': 6.0.2 + '@octokit/request': 10.0.8 + '@octokit/types': 16.0.0 + universal-user-agent: 7.0.3 + + '@octokit/auth-oauth-device@8.0.3': + dependencies: + '@octokit/oauth-methods': 6.0.2 + '@octokit/request': 10.0.8 + '@octokit/types': 16.0.0 + universal-user-agent: 7.0.3 + + '@octokit/auth-oauth-user@6.0.2': + dependencies: + '@octokit/auth-oauth-device': 8.0.3 + '@octokit/oauth-methods': 6.0.2 + '@octokit/request': 10.0.8 + '@octokit/types': 16.0.0 + universal-user-agent: 7.0.3 + + '@octokit/auth-token@6.0.0': {} + + '@octokit/auth-unauthenticated@7.0.3': + dependencies: + '@octokit/request-error': 7.1.0 + '@octokit/types': 16.0.0 + + '@octokit/core@7.0.6': + dependencies: + '@octokit/auth-token': 6.0.0 + '@octokit/graphql': 9.0.3 + '@octokit/request': 10.0.8 + '@octokit/request-error': 7.1.0 + '@octokit/types': 16.0.0 + before-after-hook: 4.0.0 + universal-user-agent: 7.0.3 + + '@octokit/endpoint@11.0.3': + dependencies: + '@octokit/types': 16.0.0 + universal-user-agent: 7.0.3 + + '@octokit/graphql@9.0.3': + dependencies: + '@octokit/request': 10.0.8 + '@octokit/types': 16.0.0 + universal-user-agent: 7.0.3 + + '@octokit/oauth-app@8.0.3': + dependencies: + '@octokit/auth-oauth-app': 9.0.3 + '@octokit/auth-oauth-user': 6.0.2 + '@octokit/auth-unauthenticated': 7.0.3 + '@octokit/core': 7.0.6 + '@octokit/oauth-authorization-url': 8.0.0 + '@octokit/oauth-methods': 6.0.2 + '@types/aws-lambda': 8.10.161 + universal-user-agent: 7.0.3 + + '@octokit/oauth-authorization-url@8.0.0': {} + + '@octokit/oauth-methods@6.0.2': + dependencies: + '@octokit/oauth-authorization-url': 8.0.0 + '@octokit/request': 10.0.8 + '@octokit/request-error': 7.1.0 + '@octokit/types': 16.0.0 + + '@octokit/openapi-types@27.0.0': {} + + '@octokit/openapi-webhooks-types@12.1.0': {} + + '@octokit/plugin-paginate-rest@14.0.0(@octokit/core@7.0.6)': + dependencies: + '@octokit/core': 7.0.6 + '@octokit/types': 16.0.0 + + '@octokit/request-error@7.1.0': + dependencies: + '@octokit/types': 16.0.0 + + '@octokit/request@10.0.8': + dependencies: + '@octokit/endpoint': 11.0.3 + '@octokit/request-error': 7.1.0 + '@octokit/types': 16.0.0 + fast-content-type-parse: 3.0.0 + json-with-bigint: 3.5.8 + universal-user-agent: 7.0.3 + + '@octokit/types@16.0.0': + dependencies: + '@octokit/openapi-types': 27.0.0 + + '@octokit/webhooks-methods@6.0.0': {} + + '@octokit/webhooks@14.2.0': + dependencies: + '@octokit/openapi-webhooks-types': 12.1.0 + '@octokit/request-error': 7.1.0 + '@octokit/webhooks-methods': 6.0.0 + '@opentelemetry/api@1.9.0': optional: true @@ -1502,6 +1736,8 @@ snapshots: tslib: 2.8.1 optional: true + '@types/aws-lambda@8.10.161': {} + '@types/cookie@0.6.0': {} '@types/estree@1.0.8': {} @@ -1526,6 +1762,8 @@ snapshots: axobject-query@4.1.0: {} + before-after-hook@4.0.0: {} + chart.js@4.5.1: dependencies: '@kurkle/color': 0.3.4 @@ -1629,6 +1867,8 @@ snapshots: '@jridgewell/sourcemap-codec': 1.5.5 '@typescript-eslint/types': 8.58.0 + fast-content-type-parse@3.0.0: {} + fdir@6.5.0(picomatch@4.0.4): optionalDependencies: picomatch: 4.0.4 @@ -1660,6 +1900,8 @@ snapshots: js-base64@3.7.8: {} + json-with-bigint@3.5.8: {} + katex@0.16.44: dependencies: commander: 8.3.0 @@ -1866,6 +2108,8 @@ snapshots: fdir: 6.5.0(picomatch@4.0.4) picomatch: 4.0.4 + toad-cache@3.7.0: {} + totalist@3.0.1: {} tr46@0.0.3: {} @@ -1884,6 +2128,10 @@ snapshots: undici-types@7.18.2: {} + universal-github-app-jwt@2.2.2: {} + + universal-user-agent@7.0.3: {} + vite@8.0.5(@emnapi/core@1.9.0)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3): dependencies: lightningcss: 1.32.0 diff --git a/src/lib/server/ratelimit.ts b/src/lib/server/ratelimit.ts new file mode 100644 index 00000000..461942f1 --- /dev/null +++ b/src/lib/server/ratelimit.ts @@ -0,0 +1,45 @@ +const minute_limit = 2 +const hour_limit = 10 + +type Bucket = { + minute_count: number + hour_count: number + minute_start: number + hour_start: number +} + +const ip_store = new Map() + +export function rate_limit(ip: string): boolean { + const now = Date.now() + let entry = ip_store.get(ip) + + if (!entry) { + entry = { + minute_count: 0, + hour_count: 0, + minute_start: now, + hour_start: now, + } + ip_store.set(ip, entry) + } + + if (now - entry.minute_start > 60 * 1000) { + entry.minute_start = now + entry.minute_count = 0 + } + + if (now - entry.hour_start > 60 * 60 * 1000) { + entry.hour_start = now + entry.hour_count = 0 + } + + if (entry.minute_count >= minute_limit || entry.hour_count >= hour_limit) { + return false + } + + entry.minute_count += 1 + entry.hour_count += 1 + + return true +} diff --git a/src/routes/api/issue/+server.ts b/src/routes/api/issue/+server.ts new file mode 100644 index 00000000..de05022b --- /dev/null +++ b/src/routes/api/issue/+server.ts @@ -0,0 +1,60 @@ +import { json } from '@sveltejs/kit' +import { App } from '@octokit/app' +import { GITHUB_PRIVATE_KEY } from '$env/static/private' +import { rate_limit } from '$lib/server/ratelimit' +import { + GITHUB_APP_ID, + GITHUB_INSTALLATION_ID, + GITHUB_OWNER, + GITHUB_REPO, +} from './config' + +const app = new App({ + appId: GITHUB_APP_ID, + privateKey: GITHUB_PRIVATE_KEY, +}) + +export const POST = async (event) => { + const ip = event.getClientAddress() + + if (!rate_limit(ip)) { + return json( + { error: 'Too many requests. Please try again later.' }, + { status: 429 }, + ) + } + + const data = await get_data(event.request) + + if (!data) return json({ error: 'Invalid request body' }, { status: 400 }) + + const { title, body } = data + + try { + const octokit = await app.getInstallationOctokit(Number(GITHUB_INSTALLATION_ID)) + + const issue = await octokit.request('POST /repos/{owner}/{repo}/issues', { + owner: GITHUB_OWNER, + repo: GITHUB_REPO, + title, + body, + }) + + return json({ url: issue.data.html_url }) + } catch (err) { + console.error(err) + return json({ error: 'Issue could not be created' }, { status: 502 }) + } +} + +async function get_data(request: Request) { + try { + const data = await request.json() + const { title, body } = data + if (!title || typeof title !== 'string') return null + if (!body || typeof body !== 'string') return null + return { title, body } + } catch (_) { + return null + } +} diff --git a/src/routes/api/issue/config.ts b/src/routes/api/issue/config.ts new file mode 100644 index 00000000..23bd792a --- /dev/null +++ b/src/routes/api/issue/config.ts @@ -0,0 +1,4 @@ +export const GITHUB_APP_ID = '3330448' +export const GITHUB_INSTALLATION_ID = '122747163' +export const GITHUB_OWNER = 'ScriptRaccoon' +export const GITHUB_REPO = 'CatDat' From abea360eb8178c1dbc7aaab8357f0bc22c9fbc19 Mon Sep 17 00:00:00 2001 From: Script Raccoon Date: Fri, 10 Apr 2026 01:05:39 +0200 Subject: [PATCH 02/15] add suggestion form --- src/components/SuggestionForm.svelte | 92 ++++++++++++++++++++++++++++ src/lib/client/utils.ts | 5 ++ src/routes/app.css | 14 ++++- 3 files changed, 110 insertions(+), 1 deletion(-) create mode 100644 src/components/SuggestionForm.svelte diff --git a/src/components/SuggestionForm.svelte b/src/components/SuggestionForm.svelte new file mode 100644 index 00000000..6d5b1790 --- /dev/null +++ b/src/components/SuggestionForm.svelte @@ -0,0 +1,92 @@ + + +
+

Suggestion Form

+ +

+ Use the form below to contribute missing data, report an issue, or make a + suggestion. +

+ +
+
+ + +
+ +
+ + +
+ + +
+ + {#if error} +

+ + Error: {error} +

+ {/if} + + {#if url} +

+ + Your suggestion has been created as a + GitHub issue. We will have a look at it! +

+ {/if} +
+ + diff --git a/src/lib/client/utils.ts b/src/lib/client/utils.ts index febf73e8..c8f587f9 100644 --- a/src/lib/client/utils.ts +++ b/src/lib/client/utils.ts @@ -1,4 +1,5 @@ import { goto } from '$app/navigation' +import type { Attachment } from 'svelte/attachments' export function get_device_type() { const w = window.innerWidth @@ -25,3 +26,7 @@ export function string_to_color(str: string): string { const h = hash % 360 return `hsl(${h}, 80%, 50%)` } + +export const scroll_into_view: Attachment = (element) => { + element.scrollIntoView({ block: 'end', behavior: 'smooth' }) +} diff --git a/src/routes/app.css b/src/routes/app.css index 0b271098..872112a5 100644 --- a/src/routes/app.css +++ b/src/routes/app.css @@ -124,15 +124,21 @@ ul li::marker { button, input, -select { +select, +textarea { font: inherit; background: none; border: none; color: inherit; } +textarea { + resize: vertical; +} + input[type='text'], input[type='search'], +textarea, select { padding: 0.25rem 0.75rem; border-radius: 0.4rem; @@ -158,6 +164,12 @@ input[type='search']::-ms-clear { display: none; } +label { + display: block; + margin-bottom: 0.1rem; + font-size: 1rem; +} + button:not(:disabled) { cursor: pointer; } From 51804ce6a3b83ba4edc1117eb546155eb2901802 Mon Sep 17 00:00:00 2001 From: Script Raccoon Date: Fri, 10 Apr 2026 01:05:48 +0200 Subject: [PATCH 03/15] display suggestion form on relevant pages --- src/routes/categories/+page.svelte | 3 +++ src/routes/category-implication/[id]/+page.svelte | 3 +++ src/routes/category-implications/+page.svelte | 3 +++ src/routes/category-properties/+page.svelte | 3 +++ src/routes/category-property/[id]/+page.svelte | 3 +++ src/routes/category/[id]/+page.svelte | 3 +++ src/routes/contribute/+page.svelte | 3 +++ src/routes/functor-implication/[id]/+page.svelte | 3 +++ src/routes/functor-implications/+page.svelte | 3 +++ src/routes/functor-properties/+page.svelte | 3 +++ src/routes/functor-property/[id]/+page.svelte | 3 +++ src/routes/functor/[id]/+page.svelte | 3 +++ src/routes/functors/+page.svelte | 3 +++ src/routes/missing/+page.svelte | 3 +++ 14 files changed, 42 insertions(+) diff --git a/src/routes/categories/+page.svelte b/src/routes/categories/+page.svelte index 532bb3c5..01d01778 100644 --- a/src/routes/categories/+page.svelte +++ b/src/routes/categories/+page.svelte @@ -4,6 +4,7 @@ import ChipGroup from '$components/ChipGroup.svelte' import MetaData from '$components/MetaData.svelte' import SearchFilter from '$components/SearchFilter.svelte' + import SuggestionForm from '$components/SuggestionForm.svelte' import { filter_by_tag, pluralize } from '$lib/client/utils' let { data } = $props() @@ -46,6 +47,8 @@ + +