Add a manage page with tabs

This commit is contained in:
2025-08-23 17:39:54 +01:00
parent 8806f72f2a
commit 42caeb8834
38 changed files with 798 additions and 110 deletions

View File

@@ -3,7 +3,6 @@
"yaml.schemas": {
"https://json.schemastore.org/github-workflow.json": "file:///workspace/next-portfolio/.gitea/workflows/deploy.yaml"
},
"biome.lsp": {
"configurationPath": "./frontend/biome.json"
}
"editor.formatOnSave": true,
"typescript.format.enable": true
}

155
bun.lock
View File

@@ -7,6 +7,7 @@
"@auth/drizzle-adapter": "^1.10.0",
"@aws-sdk/client-s3": "^3.839.0",
"@heroicons/react": "^2.2.0",
"@hookform/resolvers": "^5.2.1",
"@libsql/client": "^0.15.9",
"@mdx-js/loader": "^3.1.0",
"@mdx-js/react": "^3.1.0",
@@ -14,6 +15,11 @@
"@t3-oss/env-nextjs": "^0.13.8",
"@tailwindcss/typography": "^0.5.16",
"@tanstack/react-query": "^5.81.5",
"@tiptap/extension-typography": "^3.2.2",
"@tiptap/pm": "^3.2.2",
"@tiptap/react": "^3.2.2",
"@tiptap/starter-kit": "^3.2.2",
"@total-typescript/ts-reset": "^0.6.1",
"@trpc/client": "^11.4.3",
"@trpc/react-query": "^11.4.3",
"@trpc/server": "^11.4.3",
@@ -30,6 +36,7 @@
"radash": "^12.1.1",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-hook-form": "^7.62.0",
"react-zoom-pan-pinch": "^3.7.0",
"server-only": "^0.0.1",
"sharp": "^0.34.2",
@@ -216,8 +223,16 @@
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.5", "", { "os": "win32", "cpu": "x64" }, "sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g=="],
"@floating-ui/core": ["@floating-ui/core@1.7.3", "", { "dependencies": { "@floating-ui/utils": "^0.2.10" } }, "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w=="],
"@floating-ui/dom": ["@floating-ui/dom@1.7.4", "", { "dependencies": { "@floating-ui/core": "^1.7.3", "@floating-ui/utils": "^0.2.10" } }, "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA=="],
"@floating-ui/utils": ["@floating-ui/utils@0.2.10", "", {}, "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ=="],
"@heroicons/react": ["@heroicons/react@2.2.0", "", { "peerDependencies": { "react": ">= 16 || ^19.0.0-rc" } }, "sha512-LMcepvRaS9LYHJGsF0zzmgKCUim/X3N/DQKc4jepAXJ7l8QxJ1PmxJzqplF2Z3FE4PqBAIGyJAQ/w4B5dsqbtQ=="],
"@hookform/resolvers": ["@hookform/resolvers@5.2.1", "", { "dependencies": { "@standard-schema/utils": "^0.3.0" }, "peerDependencies": { "react-hook-form": "^7.55.0" } }, "sha512-u0+6X58gkjMcxur1wRWokA7XsiiBJ6aK17aPZxhkoYiK5J+HcTx0Vhu9ovXe6H+dVpO6cjrn2FkJTryXEMlryQ=="],
"@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.34.2", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.1.0" }, "os": "darwin", "cpu": "arm64" }, "sha512-OfXHZPppddivUJnqyKoi5YVeHRkkNE2zUFT2gbpKxp/JZCFYEYubnMg+gOp6lWfasPrTS+KPosKqdI+ELYVDtg=="],
"@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.34.2", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.1.0" }, "os": "darwin", "cpu": "x64" }, "sha512-dYvWqmjU9VxqXmjEtjmvHnGqF8GrVjM2Epj9rJ6BUIXvk8slvNDJbhGFvIoXzkDhrJC2jUxNLz/GUjjvSzfw+g=="],
@@ -336,6 +351,8 @@
"@panva/hkdf": ["@panva/hkdf@1.2.1", "", {}, "sha512-6oclG6Y3PiDFcoyk8srjLfVKyMfVCKJ27JwNPViuXziFpmdz+MZnZN/aKY0JGXgYuO/VghU0jcOAZgWXZ1Dmrw=="],
"@remirror/core-constants": ["@remirror/core-constants@3.0.0", "", {}, "sha512-42aWfPrimMfDKDi4YegyS7x+/0tlzaqwPQCULLanv3DMIlu96KTJR0fM5isWX2UViOqlGnX6YFgqWepcX+XMNg=="],
"@smithy/abort-controller": ["@smithy/abort-controller@4.0.4", "", { "dependencies": { "@smithy/types": "^4.3.1", "tslib": "^2.6.2" } }, "sha512-gJnEjZMvigPDQWHrW3oPrFhQtkrgqBkyjj3pCIdF3A5M6vsZODG93KNlfJprv6bp4245bdT32fsHK4kkH3KYDA=="],
"@smithy/chunked-blob-reader": ["@smithy/chunked-blob-reader@5.0.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-+sKqDBQqb036hh4NPaUiEkYFkTUGYzRsn3EuFhyfQfMy6oGHEUJDurLP9Ufb5dasr/XiAmPNMr6wa9afjQB+Gw=="],
@@ -436,6 +453,8 @@
"@smithy/util-waiter": ["@smithy/util-waiter@4.0.6", "", { "dependencies": { "@smithy/abort-controller": "^4.0.4", "@smithy/types": "^4.3.1", "tslib": "^2.6.2" } }, "sha512-slcr1wdRbX7NFphXZOxtxRNA7hXAAtJAXJDE/wdoMAos27SIquVCKiSqfB6/28YzQ8FCsB5NKkhdM5gMADbqxg=="],
"@standard-schema/utils": ["@standard-schema/utils@0.3.0", "", {}, "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g=="],
"@swc/counter": ["@swc/counter@0.1.3", "", {}, "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ=="],
"@swc/helpers": ["@swc/helpers@0.5.15", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g=="],
@@ -480,6 +499,66 @@
"@tanstack/react-query": ["@tanstack/react-query@5.81.5", "", { "dependencies": { "@tanstack/query-core": "5.81.5" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-lOf2KqRRiYWpQT86eeeftAGnjuTR35myTP8MXyvHa81VlomoAWNEd8x5vkcAfQefu0qtYCvyqLropFZqgI2EQw=="],
"@tiptap/core": ["@tiptap/core@3.2.2", "", { "peerDependencies": { "@tiptap/pm": "^3.2.2" } }, "sha512-+9cSesDsImbHzhESjP/0imD2V/s6kcFUeay6nd5P2pDv6yMWipor9aAgzu+eJ5wxrEHx8WxreFws2WdSGEspkQ=="],
"@tiptap/extension-blockquote": ["@tiptap/extension-blockquote@3.2.2", "", { "peerDependencies": { "@tiptap/core": "^3.2.2" } }, "sha512-0X1LtESVeS1Z4ga4JYqf2WboHelPGY2kV0R7JJ0rcFQJ7XRtUGPHu4GtoGF/TlpXx2aJldgbn3YpEY1lmSZz9A=="],
"@tiptap/extension-bold": ["@tiptap/extension-bold@3.2.2", "", { "peerDependencies": { "@tiptap/core": "^3.2.2" } }, "sha512-Oj/ThrTwFq/xgi+u2Lb9TuSZC645mPeedOhM8mqo4pPmqnBjFvHEj82+Ddg5LAielMqp2pQnjAvKCRhokQdZqQ=="],
"@tiptap/extension-bubble-menu": ["@tiptap/extension-bubble-menu@3.2.2", "", { "dependencies": { "@floating-ui/dom": "^1.0.0" }, "peerDependencies": { "@tiptap/core": "^3.2.2", "@tiptap/pm": "^3.2.2" } }, "sha512-iP/b/lZScZa4lclDnF9116p5tzHOVshg6CFq+9I8gzYPBIUyJwIShSL/Gowl8dvIaoufkzu/SI7PL4PF+vMIcw=="],
"@tiptap/extension-bullet-list": ["@tiptap/extension-bullet-list@3.2.2", "", { "peerDependencies": { "@tiptap/extension-list": "^3.2.2" } }, "sha512-ylaluF8uoZpXq2RedGVnx4B4E/+7pKQb8skgv7hQQf17tdztl26weZDv3Hjr+uoitkNmVINghOtrdubAkOBSrw=="],
"@tiptap/extension-code": ["@tiptap/extension-code@3.2.2", "", { "peerDependencies": { "@tiptap/core": "^3.2.2" } }, "sha512-PasOGcow/tMLFbqgOt9syu8BASlNj78cxw3VcCMMbXyaCBFgdUK++Rsk4ad7jTquf48hx9nIg8OVIX4aQSOZ2A=="],
"@tiptap/extension-code-block": ["@tiptap/extension-code-block@3.2.2", "", { "peerDependencies": { "@tiptap/core": "^3.2.2", "@tiptap/pm": "^3.2.2" } }, "sha512-LOIChoX6rIqugZYLROyMwwuYfdWY+W517OR4jWCBVmEepUrXehVQIk0U8mnMpI9q/HuK9MbC3xQn6pf7LVVg2A=="],
"@tiptap/extension-document": ["@tiptap/extension-document@3.2.2", "", { "peerDependencies": { "@tiptap/core": "^3.2.2" } }, "sha512-aUPWcABB/e/wUIvNlZaTHqGE0egSXbLQn4cjrWp1fYdmfB3PcAgg2JuEAHv1I4R3MiftCfKD01+XWAtELQpBGQ=="],
"@tiptap/extension-dropcursor": ["@tiptap/extension-dropcursor@3.2.2", "", { "peerDependencies": { "@tiptap/extensions": "^3.2.2" } }, "sha512-/lFKAfYtxz73gBBvPXGlIzQ5gCoGhBK2PtfhCJgEtVCcRx7L1nOyTbZIAZ2W33nu9MNvaHi1xPU8jKz6arDBqQ=="],
"@tiptap/extension-floating-menu": ["@tiptap/extension-floating-menu@3.2.2", "", { "peerDependencies": { "@floating-ui/dom": "^1.0.0", "@tiptap/core": "^3.2.2", "@tiptap/pm": "^3.2.2" } }, "sha512-f5Y+yu4upTTmSFA7zcTtKaEkJi8iYE9VsyPbEO7VMUTP4FALt5CSx77ypO1sVGrMMw1xVQCw0IBEndJu766JZQ=="],
"@tiptap/extension-gapcursor": ["@tiptap/extension-gapcursor@3.2.2", "", { "peerDependencies": { "@tiptap/extensions": "^3.2.2" } }, "sha512-VRbxiZ4o74/womZIvcbLMrbYzfQvAvzmCLKtiTtOTtwejicd6snZ4ZSiU7CStDKruQqWfPDU9UjihF/S+gG01A=="],
"@tiptap/extension-hard-break": ["@tiptap/extension-hard-break@3.2.2", "", { "peerDependencies": { "@tiptap/core": "^3.2.2" } }, "sha512-VpK9IEQtDkxtmcA5nU/QSfVZ7azlMBYY8oDA1hz8YU1npwCCWULlhyluiJow+wIeb/RVhrig8aVtyB5Co26z9A=="],
"@tiptap/extension-heading": ["@tiptap/extension-heading@3.2.2", "", { "peerDependencies": { "@tiptap/core": "^3.2.2" } }, "sha512-r+hronPZPtehuNEigY+ZC+mECuenQC/OAN5BWXh3qgYpFtSEzpNf9iVeudfigDSNZTN/jQSYZg94KXfmmh9CEg=="],
"@tiptap/extension-horizontal-rule": ["@tiptap/extension-horizontal-rule@3.2.2", "", { "peerDependencies": { "@tiptap/core": "^3.2.2", "@tiptap/pm": "^3.2.2" } }, "sha512-zrV2wZ02Zkko6d1RA+3QrW5CdbpxBRZthfbMc7p75rdOqxA8hh7NXZxC9toHAYVqIxlkfe3SiXCYqA9hlCpO6A=="],
"@tiptap/extension-italic": ["@tiptap/extension-italic@3.2.2", "", { "peerDependencies": { "@tiptap/core": "^3.2.2" } }, "sha512-Xn59ZMXiYamo2LwTAAeC3XvliGKykAAmLKk2TQTiR6uscIBIxJrGsTvXHW3oZgXvCDJ40mig9j4AuYOesEH22w=="],
"@tiptap/extension-link": ["@tiptap/extension-link@3.2.2", "", { "dependencies": { "linkifyjs": "^4.3.2" }, "peerDependencies": { "@tiptap/core": "^3.2.2", "@tiptap/pm": "^3.2.2" } }, "sha512-rw4cq/oa0XyqjqoiY7pGCw9js2D3JNqI1SOrZHHmgaTTuachIV9NvYUH/SSk9bkhLfshZiMCml6VNOTRJSDofQ=="],
"@tiptap/extension-list": ["@tiptap/extension-list@3.2.2", "", { "peerDependencies": { "@tiptap/core": "^3.2.2", "@tiptap/pm": "^3.2.2" } }, "sha512-o7HFScNCyxuET0nw7OLhfVYvd2Jqk6kqWYMC6ynEmqXVRhSkYUdEVTridI1aFDsbbiL55b6PUdCbQtpKnINWPw=="],
"@tiptap/extension-list-item": ["@tiptap/extension-list-item@3.2.2", "", { "peerDependencies": { "@tiptap/extension-list": "^3.2.2" } }, "sha512-5qttMztGk93Y4tTELR4gP8z8KyBNnNEWc3VvXUFVirAOuM/u3EYmnuHZsxfsLeDRw77DyIXGw4TXaNNQPprIGg=="],
"@tiptap/extension-list-keymap": ["@tiptap/extension-list-keymap@3.2.2", "", { "peerDependencies": { "@tiptap/extension-list": "^3.2.2" } }, "sha512-HzMRGyRoE96g4UViG+tmDJ5YfxePhKV74Lx1/o8UA9dRPfANeiVVfMUjBaB9MU4yWOxIlDcT4uIzcX9QzGyaJA=="],
"@tiptap/extension-ordered-list": ["@tiptap/extension-ordered-list@3.2.2", "", { "peerDependencies": { "@tiptap/extension-list": "^3.2.2" } }, "sha512-epuUBL+v+aFFc25TqSAGnaKqCJc8zDuzl74/Z9gO2r6gq6licqYayFB24faateA/Q46m4+1MMMRldSkQwcPHwQ=="],
"@tiptap/extension-paragraph": ["@tiptap/extension-paragraph@3.2.2", "", { "peerDependencies": { "@tiptap/core": "^3.2.2" } }, "sha512-XM2WZi8pB8sbM20pizpJsWVCvKxYZDaoH4zM+7HiIiGa0lMbEwXcdUvnOmqbyc9fViYBF0w1i+iMOjGgZFfvbw=="],
"@tiptap/extension-strike": ["@tiptap/extension-strike@3.2.2", "", { "peerDependencies": { "@tiptap/core": "^3.2.2" } }, "sha512-rIFar/RDw+h107hn9HcSaFlZYrb7xuSxPW4HOghjYJFYSC+NkjlN4p0dZBeiMqejKTJYGn/Qs4bDF/xjonePqQ=="],
"@tiptap/extension-text": ["@tiptap/extension-text@3.2.2", "", { "peerDependencies": { "@tiptap/core": "^3.2.2" } }, "sha512-6mY7mNBJz0MWYonUaIy09LCKcNV7fCYVN86Oj6PiwlLcIIx33TUwVgCa2xt1nSGY5InHYU6laN3Yre5Rzxdytw=="],
"@tiptap/extension-typography": ["@tiptap/extension-typography@3.2.2", "", { "peerDependencies": { "@tiptap/core": "^3.2.2" } }, "sha512-i2HdWq6PXe+5nu7OIdMX32ZSW1xZ7UbL4sv2qUbkX00ap00ozcjkYKuFqGUHuhEYPQ1rRgFThI7939kUKEo7aQ=="],
"@tiptap/extension-underline": ["@tiptap/extension-underline@3.2.2", "", { "peerDependencies": { "@tiptap/core": "^3.2.2" } }, "sha512-lYhmN9BkcikMWt79PoiQd+o03UQhKyz2e3v2miMcuKjiHojzqrtNQ6vWgLQ6SoZmDZGDQQWgqbJ/wL6CdeI4cw=="],
"@tiptap/extensions": ["@tiptap/extensions@3.2.2", "", { "peerDependencies": { "@tiptap/core": "^3.2.2", "@tiptap/pm": "^3.2.2" } }, "sha512-AMA9Jsr1X1S5dq0PZubi0avw8HTlG/tqFMWePeHN3WNiBo3jreIZt3NoxtYNNrbJ6nwhFXNhQpEh915JBx/rPw=="],
"@tiptap/pm": ["@tiptap/pm@3.2.2", "", { "dependencies": { "prosemirror-changeset": "^2.3.0", "prosemirror-collab": "^1.3.1", "prosemirror-commands": "^1.6.2", "prosemirror-dropcursor": "^1.8.1", "prosemirror-gapcursor": "^1.3.2", "prosemirror-history": "^1.4.1", "prosemirror-inputrules": "^1.4.0", "prosemirror-keymap": "^1.2.2", "prosemirror-markdown": "^1.13.1", "prosemirror-menu": "^1.2.4", "prosemirror-model": "^1.24.1", "prosemirror-schema-basic": "^1.2.3", "prosemirror-schema-list": "^1.5.0", "prosemirror-state": "^1.4.3", "prosemirror-tables": "^1.6.4", "prosemirror-trailing-node": "^3.0.0", "prosemirror-transform": "^1.10.2", "prosemirror-view": "^1.38.1" } }, "sha512-Eju6vLpNMXubjZ3gjiTKIQIyhnUk4BLjU76t67EBFJkbQr/VvRNzn3BD/4XEmj6/LT0TgB12m50gaIb/BTAGng=="],
"@tiptap/react": ["@tiptap/react@3.2.2", "", { "dependencies": { "@types/use-sync-external-store": "^0.0.6", "fast-deep-equal": "^3.1.3", "use-sync-external-store": "^1.4.0" }, "optionalDependencies": { "@tiptap/extension-bubble-menu": "^3.2.2", "@tiptap/extension-floating-menu": "^3.2.2" }, "peerDependencies": { "@tiptap/core": "^3.2.2", "@tiptap/pm": "^3.2.2", "react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-eRogcwFokkcmX+RRoFrXrURhSOFxXrADcZaJmor0Y7QRcuqtbYZu9o8ZkBl9FMJ9BPBgDqqmvTsXb4rqnlsbyw=="],
"@tiptap/starter-kit": ["@tiptap/starter-kit@3.2.2", "", { "dependencies": { "@tiptap/core": "^3.2.2", "@tiptap/extension-blockquote": "^3.2.2", "@tiptap/extension-bold": "^3.2.2", "@tiptap/extension-bullet-list": "^3.2.2", "@tiptap/extension-code": "^3.2.2", "@tiptap/extension-code-block": "^3.2.2", "@tiptap/extension-document": "^3.2.2", "@tiptap/extension-dropcursor": "^3.2.2", "@tiptap/extension-gapcursor": "^3.2.2", "@tiptap/extension-hard-break": "^3.2.2", "@tiptap/extension-heading": "^3.2.2", "@tiptap/extension-horizontal-rule": "^3.2.2", "@tiptap/extension-italic": "^3.2.2", "@tiptap/extension-link": "^3.2.2", "@tiptap/extension-list": "^3.2.2", "@tiptap/extension-list-item": "^3.2.2", "@tiptap/extension-list-keymap": "^3.2.2", "@tiptap/extension-ordered-list": "^3.2.2", "@tiptap/extension-paragraph": "^3.2.2", "@tiptap/extension-strike": "^3.2.2", "@tiptap/extension-text": "^3.2.2", "@tiptap/extension-underline": "^3.2.2", "@tiptap/extensions": "^3.2.2", "@tiptap/pm": "^3.2.2" } }, "sha512-65Wy+W/xDRIBySuSMrjI2CAuJ2eayzyspB9P83RFptXWAikSz97K9V9hLjeMz2Ptleg0ekNejo467abP92/2lA=="],
"@total-typescript/ts-reset": ["@total-typescript/ts-reset@0.6.1", "", {}, "sha512-cka47fVSo6lfQDIATYqb/vO1nvFfbPw7uWLayIXIhGETj0wcOOlrlkobOMDNQOFr9QOafegUPq13V2+6vtD7yg=="],
"@trpc/client": ["@trpc/client@11.4.3", "", { "peerDependencies": { "@trpc/server": "11.4.3", "typescript": ">=5.7.2" } }, "sha512-i2suttUCfColktXT8bqex5kHW5jpT15nwUh0hGSDiW1keN621kSUQKcLJ095blqQAUgB+lsmgSqSMmB4L9shQQ=="],
"@trpc/react-query": ["@trpc/react-query@11.4.3", "", { "peerDependencies": { "@tanstack/react-query": "^5.80.3", "@trpc/client": "11.4.3", "@trpc/server": "11.4.3", "react": ">=18.2.0", "react-dom": ">=18.2.0", "typescript": ">=5.7.2" } }, "sha512-z+jhAiOBD22NNhHtvF0iFp9hO36YFA7M8AiUu/XtNmMxyLd3Y9/d1SMjMwlTdnGqxEGPo41VEWBrdhDUGtUuHg=="],
@@ -496,8 +575,14 @@
"@types/hast": ["@types/hast@3.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ=="],
"@types/linkify-it": ["@types/linkify-it@5.0.0", "", {}, "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q=="],
"@types/markdown-it": ["@types/markdown-it@14.1.2", "", { "dependencies": { "@types/linkify-it": "^5", "@types/mdurl": "^2" } }, "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog=="],
"@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="],
"@types/mdurl": ["@types/mdurl@2.0.0", "", {}, "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg=="],
"@types/mdx": ["@types/mdx@2.0.13", "", {}, "sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw=="],
"@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="],
@@ -510,6 +595,8 @@
"@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="],
"@types/use-sync-external-store": ["@types/use-sync-external-store@0.0.6", "", {}, "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg=="],
"@types/uuid": ["@types/uuid@9.0.8", "", {}, "sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA=="],
"@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="],
@@ -524,6 +611,8 @@
"ansi-styles": ["ansi-styles@6.2.1", "", {}, "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug=="],
"argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
"astring": ["astring@1.9.0", "", { "bin": { "astring": "bin/astring" } }, "sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg=="],
"babel-plugin-react-compiler": ["babel-plugin-react-compiler@19.1.0-rc.2", "", { "dependencies": { "@babel/types": "^7.26.0" } }, "sha512-kSNA//p5fMO6ypG8EkEVPIqAjwIXm5tMjfD1XRPL/sRjYSbJ6UsvORfaeolNWnZ9n310aM0xJP7peW26BuCVzA=="],
@@ -568,6 +657,8 @@
"copy-anything": ["copy-anything@3.0.5", "", { "dependencies": { "is-what": "^4.1.8" } }, "sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w=="],
"crelt": ["crelt@1.0.6", "", {}, "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g=="],
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
"cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="],
@@ -598,6 +689,8 @@
"enhanced-resolve": ["enhanced-resolve@5.18.2", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-6Jw4sE1maoRJo3q8MsSIn2onJFbLTOjY9hlx4DZXmOKvLRd1Ok2kXmAGXaafL2+ijsJZ1ClYbl/pmqr9+k4iUQ=="],
"entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="],
"esast-util-from-estree": ["esast-util-from-estree@2.0.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "devlop": "^1.0.0", "estree-util-visit": "^2.0.0", "unist-util-position-from-estree": "^2.0.0" } }, "sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ=="],
"esast-util-from-js": ["esast-util-from-js@2.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "acorn": "^8.0.0", "esast-util-from-estree": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-8Ja+rNJ0Lt56Pcf3TAmpBZjmx8ZcK5Ts4cAzIOjsjevg9oSXJnl6SUQ2EevU8tv3h6ZLWmoKL5H4fgWvdvfETw=="],
@@ -606,6 +699,8 @@
"esbuild-register": ["esbuild-register@3.6.0", "", { "dependencies": { "debug": "^4.3.4" }, "peerDependencies": { "esbuild": ">=0.12 <1" } }, "sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg=="],
"escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
"estree-util-attach-comments": ["estree-util-attach-comments@3.0.0", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-cKUwm/HUcTDsYh/9FgnuFqpfquUbwIqwKM26BVCGDPVgvaCl/nDCCjUfiLlx6lsEZ3Z4RFxNbOQ60pkaEwFxGw=="],
"estree-util-build-jsx": ["estree-util-build-jsx@3.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "estree-walker": "^3.0.0" } }, "sha512-8U5eiL6BTrPxp/CHbs2yMgP8ftMhR5ww1eIKoWRMlqvltHF8fZn5LRDvTKuxD3DUn+shRbLGqXemcP51oFCsGQ=="],
@@ -624,6 +719,8 @@
"extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="],
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
"fast-xml-parser": ["fast-xml-parser@4.4.1", "", { "dependencies": { "strnum": "^1.0.5" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-xkjOecfnKGkSsOwtZ5Pz7Us/T6mrbPQrq0nh+aCO5V9nk5NLWmasAHumTKjiPJPWANe+kAZ84Jc8ooJkzZ88Sw=="],
"fetch-blob": ["fetch-blob@3.2.0", "", { "dependencies": { "node-domexception": "^1.0.0", "web-streams-polyfill": "^3.0.3" } }, "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ=="],
@@ -698,6 +795,10 @@
"lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.30.1", "", { "os": "win32", "cpu": "x64" }, "sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg=="],
"linkify-it": ["linkify-it@5.0.0", "", { "dependencies": { "uc.micro": "^2.0.0" } }, "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ=="],
"linkifyjs": ["linkifyjs@4.3.2", "", {}, "sha512-NT1CJtq3hHIreOianA8aSXn6Cw0JzYOuDQbOrSPe7gqFnCpKP++MQe3ODgO3oh2GJFORkAAdqredOa60z63GbA=="],
"lodash.castarray": ["lodash.castarray@4.4.0", "", {}, "sha512-aVx8ztPv7/2ULbArGJ2Y42bG1mEQ5mGjpdvrbJcJFU3TbYybe+QlLS4pst9zV52ymy2in1KpFPiZnAOATxD4+Q=="],
"lodash.isplainobject": ["lodash.isplainobject@4.0.6", "", {}, "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA=="],
@@ -712,6 +813,8 @@
"markdown-extensions": ["markdown-extensions@2.0.0", "", {}, "sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q=="],
"markdown-it": ["markdown-it@14.1.0", "", { "dependencies": { "argparse": "^2.0.1", "entities": "^4.4.0", "linkify-it": "^5.0.0", "mdurl": "^2.0.0", "punycode.js": "^2.3.1", "uc.micro": "^2.1.0" }, "bin": { "markdown-it": "bin/markdown-it.mjs" } }, "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg=="],
"mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA=="],
"mdast-util-mdx": ["mdast-util-mdx@3.0.0", "", { "dependencies": { "mdast-util-from-markdown": "^2.0.0", "mdast-util-mdx-expression": "^2.0.0", "mdast-util-mdx-jsx": "^3.0.0", "mdast-util-mdxjs-esm": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-JfbYLAW7XnYTTbUsmpu0kdBUVe+yKVJZBItEjwyYJiDJuZ9w4eeaqks4HQO+R7objWgS2ymV60GYpI14Ug554w=="],
@@ -730,6 +833,8 @@
"mdast-util-to-string": ["mdast-util-to-string@4.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0" } }, "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg=="],
"mdurl": ["mdurl@2.0.0", "", {}, "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w=="],
"micromark": ["micromark@4.0.2", "", { "dependencies": { "@types/debug": "^4.0.0", "debug": "^4.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA=="],
"micromark-core-commonmark": ["micromark-core-commonmark@2.0.3", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-destination": "^2.0.0", "micromark-factory-label": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-factory-title": "^2.0.0", "micromark-factory-whitespace": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-html-tag-name": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg=="],
@@ -812,6 +917,8 @@
"oauth4webapi": ["oauth4webapi@3.5.3", "", {}, "sha512-2bnHosmBLAQpXNBLOvaJMyMkr4Yya5ohE5Q9jqyxiN+aa7GFCzvDN1RRRMrp0NkfqRR2MTaQNkcSUCCjILD9oQ=="],
"orderedmap": ["orderedmap@2.1.1", "", {}, "sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g=="],
"package-json-from-dist": ["package-json-from-dist@1.0.1", "", {}, "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="],
"parse-entities": ["parse-entities@4.0.2", "", { "dependencies": { "@types/unist": "^2.0.0", "character-entities-legacy": "^3.0.0", "character-reference-invalid": "^2.0.0", "decode-named-character-reference": "^1.0.0", "is-alphanumerical": "^2.0.0", "is-decimal": "^2.0.0", "is-hexadecimal": "^2.0.0" } }, "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw=="],
@@ -834,12 +941,52 @@
"property-information": ["property-information@7.1.0", "", {}, "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ=="],
"prosemirror-changeset": ["prosemirror-changeset@2.3.1", "", { "dependencies": { "prosemirror-transform": "^1.0.0" } }, "sha512-j0kORIBm8ayJNl3zQvD1TTPHJX3g042et6y/KQhZhnPrruO8exkTgG8X+NRpj7kIyMMEx74Xb3DyMIBtO0IKkQ=="],
"prosemirror-collab": ["prosemirror-collab@1.3.1", "", { "dependencies": { "prosemirror-state": "^1.0.0" } }, "sha512-4SnynYR9TTYaQVXd/ieUvsVV4PDMBzrq2xPUWutHivDuOshZXqQ5rGbZM84HEaXKbLdItse7weMGOUdDVcLKEQ=="],
"prosemirror-commands": ["prosemirror-commands@1.7.1", "", { "dependencies": { "prosemirror-model": "^1.0.0", "prosemirror-state": "^1.0.0", "prosemirror-transform": "^1.10.2" } }, "sha512-rT7qZnQtx5c0/y/KlYaGvtG411S97UaL6gdp6RIZ23DLHanMYLyfGBV5DtSnZdthQql7W+lEVbpSfwtO8T+L2w=="],
"prosemirror-dropcursor": ["prosemirror-dropcursor@1.8.2", "", { "dependencies": { "prosemirror-state": "^1.0.0", "prosemirror-transform": "^1.1.0", "prosemirror-view": "^1.1.0" } }, "sha512-CCk6Gyx9+Tt2sbYk5NK0nB1ukHi2ryaRgadV/LvyNuO3ena1payM2z6Cg0vO1ebK8cxbzo41ku2DE5Axj1Zuiw=="],
"prosemirror-gapcursor": ["prosemirror-gapcursor@1.3.2", "", { "dependencies": { "prosemirror-keymap": "^1.0.0", "prosemirror-model": "^1.0.0", "prosemirror-state": "^1.0.0", "prosemirror-view": "^1.0.0" } }, "sha512-wtjswVBd2vaQRrnYZaBCbyDqr232Ed4p2QPtRIUK5FuqHYKGWkEwl08oQM4Tw7DOR0FsasARV5uJFvMZWxdNxQ=="],
"prosemirror-history": ["prosemirror-history@1.4.1", "", { "dependencies": { "prosemirror-state": "^1.2.2", "prosemirror-transform": "^1.0.0", "prosemirror-view": "^1.31.0", "rope-sequence": "^1.3.0" } }, "sha512-2JZD8z2JviJrboD9cPuX/Sv/1ChFng+xh2tChQ2X4bB2HeK+rra/bmJ3xGntCcjhOqIzSDG6Id7e8RJ9QPXLEQ=="],
"prosemirror-inputrules": ["prosemirror-inputrules@1.5.0", "", { "dependencies": { "prosemirror-state": "^1.0.0", "prosemirror-transform": "^1.0.0" } }, "sha512-K0xJRCmt+uSw7xesnHmcn72yBGTbY45vm8gXI4LZXbx2Z0jwh5aF9xrGQgrVPu0WbyFVFF3E/o9VhJYz6SQWnA=="],
"prosemirror-keymap": ["prosemirror-keymap@1.2.3", "", { "dependencies": { "prosemirror-state": "^1.0.0", "w3c-keyname": "^2.2.0" } }, "sha512-4HucRlpiLd1IPQQXNqeo81BGtkY8Ai5smHhKW9jjPKRc2wQIxksg7Hl1tTI2IfT2B/LgX6bfYvXxEpJl7aKYKw=="],
"prosemirror-markdown": ["prosemirror-markdown@1.13.2", "", { "dependencies": { "@types/markdown-it": "^14.0.0", "markdown-it": "^14.0.0", "prosemirror-model": "^1.25.0" } }, "sha512-FPD9rHPdA9fqzNmIIDhhnYQ6WgNoSWX9StUZ8LEKapaXU9i6XgykaHKhp6XMyXlOWetmaFgGDS/nu/w9/vUc5g=="],
"prosemirror-menu": ["prosemirror-menu@1.2.5", "", { "dependencies": { "crelt": "^1.0.0", "prosemirror-commands": "^1.0.0", "prosemirror-history": "^1.0.0", "prosemirror-state": "^1.0.0" } }, "sha512-qwXzynnpBIeg1D7BAtjOusR+81xCp53j7iWu/IargiRZqRjGIlQuu1f3jFi+ehrHhWMLoyOQTSRx/IWZJqOYtQ=="],
"prosemirror-model": ["prosemirror-model@1.25.3", "", { "dependencies": { "orderedmap": "^2.0.0" } }, "sha512-dY2HdaNXlARknJbrManZ1WyUtos+AP97AmvqdOQtWtrrC5g4mohVX5DTi9rXNFSk09eczLq9GuNTtq3EfMeMGA=="],
"prosemirror-schema-basic": ["prosemirror-schema-basic@1.2.4", "", { "dependencies": { "prosemirror-model": "^1.25.0" } }, "sha512-ELxP4TlX3yr2v5rM7Sb70SqStq5NvI15c0j9j/gjsrO5vaw+fnnpovCLEGIcpeGfifkuqJwl4fon6b+KdrODYQ=="],
"prosemirror-schema-list": ["prosemirror-schema-list@1.5.1", "", { "dependencies": { "prosemirror-model": "^1.0.0", "prosemirror-state": "^1.0.0", "prosemirror-transform": "^1.7.3" } }, "sha512-927lFx/uwyQaGwJxLWCZRkjXG0p48KpMj6ueoYiu4JX05GGuGcgzAy62dfiV8eFZftgyBUvLx76RsMe20fJl+Q=="],
"prosemirror-state": ["prosemirror-state@1.4.3", "", { "dependencies": { "prosemirror-model": "^1.0.0", "prosemirror-transform": "^1.0.0", "prosemirror-view": "^1.27.0" } }, "sha512-goFKORVbvPuAQaXhpbemJFRKJ2aixr+AZMGiquiqKxaucC6hlpHNZHWgz5R7dS4roHiwq9vDctE//CZ++o0W1Q=="],
"prosemirror-tables": ["prosemirror-tables@1.7.1", "", { "dependencies": { "prosemirror-keymap": "^1.2.2", "prosemirror-model": "^1.25.0", "prosemirror-state": "^1.4.3", "prosemirror-transform": "^1.10.3", "prosemirror-view": "^1.39.1" } }, "sha512-eRQ97Bf+i9Eby99QbyAiyov43iOKgWa7QCGly+lrDt7efZ1v8NWolhXiB43hSDGIXT1UXgbs4KJN3a06FGpr1Q=="],
"prosemirror-trailing-node": ["prosemirror-trailing-node@3.0.0", "", { "dependencies": { "@remirror/core-constants": "3.0.0", "escape-string-regexp": "^4.0.0" }, "peerDependencies": { "prosemirror-model": "^1.22.1", "prosemirror-state": "^1.4.2", "prosemirror-view": "^1.33.8" } }, "sha512-xiun5/3q0w5eRnGYfNlW1uU9W6x5MoFKWwq/0TIRgt09lv7Hcser2QYV8t4muXbEr+Fwo0geYn79Xs4GKywrRQ=="],
"prosemirror-transform": ["prosemirror-transform@1.10.4", "", { "dependencies": { "prosemirror-model": "^1.21.0" } }, "sha512-pwDy22nAnGqNR1feOQKHxoFkkUtepoFAd3r2hbEDsnf4wp57kKA36hXsB3njA9FtONBEwSDnDeCiJe+ItD+ykw=="],
"prosemirror-view": ["prosemirror-view@1.40.1", "", { "dependencies": { "prosemirror-model": "^1.20.0", "prosemirror-state": "^1.0.0", "prosemirror-transform": "^1.1.0" } }, "sha512-pbwUjt3G7TlsQQHDiYSupWBhJswpLVB09xXm1YiJPdkjkh9Pe7Y51XdLh5VWIZmROLY8UpUpG03lkdhm9lzIBA=="],
"punycode.js": ["punycode.js@2.3.1", "", {}, "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA=="],
"radash": ["radash@12.1.1", "", {}, "sha512-h36JMxKRqrAxVD8201FrCpyeNuUY9Y5zZwujr20fFO77tpUtGa6EZzfKw/3WaiBX95fq7+MpsuMLNdSnORAwSA=="],
"react": ["react@19.1.0", "", {}, "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg=="],
"react-dom": ["react-dom@19.1.0", "", { "dependencies": { "scheduler": "^0.26.0" }, "peerDependencies": { "react": "^19.1.0" } }, "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g=="],
"react-hook-form": ["react-hook-form@7.62.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17 || ^18 || ^19" } }, "sha512-7KWFejc98xqG/F4bAxpL41NB3o1nnvQO1RWZT3TqRZYL8RryQETGfEdVnJN2fy1crCiBLLjkRBVK05j24FxJGA=="],
"react-zoom-pan-pinch": ["react-zoom-pan-pinch@3.7.0", "", { "peerDependencies": { "react": "*", "react-dom": "*" } }, "sha512-UmReVZ0TxlKzxSbYiAj+LeGRW8s8LraAFTXRAxzMYnNRgGPsxCudwZKVkjvGmjtx7SW/hZamt69NUmGf4xrkXA=="],
"recma-build-jsx": ["recma-build-jsx@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-util-build-jsx": "^3.0.0", "vfile": "^6.0.0" } }, "sha512-8GtdyqaBcDfva+GUKDr3nev3VpKAhup1+RvkMvUxURHpW7QyIvk9F5wz7Vzo06CEMSilw6uArgRqhpiUcWp8ew=="],
@@ -860,6 +1007,8 @@
"resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="],
"rope-sequence": ["rope-sequence@1.3.4", "", {}, "sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ=="],
"scheduler": ["scheduler@0.26.0", "", {}, "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA=="],
"semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
@@ -920,6 +1069,8 @@
"typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="],
"uc.micro": ["uc.micro@2.1.0", "", {}, "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A=="],
"undici-types": ["undici-types@7.8.0", "", {}, "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw=="],
"unified": ["unified@11.0.5", "", { "dependencies": { "@types/unist": "^3.0.0", "bail": "^2.0.0", "devlop": "^1.0.0", "extend": "^3.0.0", "is-plain-obj": "^4.0.0", "trough": "^2.0.0", "vfile": "^6.0.0" } }, "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA=="],
@@ -936,6 +1087,8 @@
"unist-util-visit-parents": ["unist-util-visit-parents@6.0.1", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw=="],
"use-sync-external-store": ["use-sync-external-store@1.5.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A=="],
"util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
"uuid": ["uuid@9.0.1", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA=="],
@@ -944,6 +1097,8 @@
"vfile-message": ["vfile-message@4.0.2", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw=="],
"w3c-keyname": ["w3c-keyname@2.2.8", "", {}, "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ=="],
"web-streams-polyfill": ["web-streams-polyfill@3.3.3", "", {}, "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="],
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],

View File

@@ -21,6 +21,7 @@
"@auth/drizzle-adapter": "^1.10.0",
"@aws-sdk/client-s3": "^3.839.0",
"@heroicons/react": "^2.2.0",
"@hookform/resolvers": "^5.2.1",
"@libsql/client": "^0.15.9",
"@mdx-js/loader": "^3.1.0",
"@mdx-js/react": "^3.1.0",
@@ -28,6 +29,11 @@
"@t3-oss/env-nextjs": "^0.13.8",
"@tailwindcss/typography": "^0.5.16",
"@tanstack/react-query": "^5.81.5",
"@tiptap/extension-typography": "^3.2.2",
"@tiptap/pm": "^3.2.2",
"@tiptap/react": "^3.2.2",
"@tiptap/starter-kit": "^3.2.2",
"@total-typescript/ts-reset": "^0.6.1",
"@trpc/client": "^11.4.3",
"@trpc/react-query": "^11.4.3",
"@trpc/server": "^11.4.3",
@@ -44,6 +50,7 @@
"radash": "^12.1.1",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-hook-form": "^7.62.0",
"react-zoom-pan-pinch": "^3.7.0",
"server-only": "^0.0.1",
"sharp": "^0.34.2",

1
reset.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
import "@total-typescript/ts-reset";

View File

@@ -1,27 +0,0 @@
import type React from "react";
import { signIn } from "@/lib/auth";
export default function Auth(props: {
searchParams: Promise<{ callbackUrl: string | undefined }>;
}): React.JSX.Element {
return (
<form
className="mx-auto w-40"
action={async () => {
"use server";
await signIn("authelia", {
redirectTo: (await props.searchParams)?.callbackUrl ?? "",
});
}}
>
<button
type="submit"
className={
"rounded-lg border-transparent px-2 py-2 font-normal transition-colors duration-100"
}
>
<span>Sign in with Authelia</span>
</button>
</form>
);
}

View File

@@ -9,7 +9,7 @@ export default function RootLayout({
return (
<>
<NavBar />
<main className="mx-auto w-full flex-1 px-6 py-8 align-middle lg:max-w-5xl">
<main className="mx-auto w-full flex-1 px-6 pt-8 pb-12 align-middle lg:max-w-5xl">
{children}
</main>
<Footer />

View File

@@ -0,0 +1,19 @@
export default function DirSvg(): React.JSX.Element {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth="1.5"
stroke="currentColor"
className="h-4 w-4"
>
<title>Directory</title>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M2.25 12.75V12A2.25 2.25 0 014.5 9.75h15A2.25 2.25 0 0121.75 12v.75m-8.69-6.44l-2.12-2.12a1.5 1.5 0 00-1.061-.44H4.5A2.25 2.25 0 002.25 6v12a2.25 2.25 0 002.25 2.25h15A2.25 2.25 0 0021.75 18V9a2.25 2.25 0 00-2.25-2.25h-5.379a1.5 1.5 0 01-1.06-.44z"
/>
</svg>
);
}

View File

@@ -0,0 +1,19 @@
export default function ImageSvg(): React.JSX.Element {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth="1.5"
stroke="currentColor"
className="h-4 w-4"
>
<title>Item</title>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M2.25 15.75l5.159-5.159a2.25 2.25 0 013.182 0l5.159 5.159m-1.5-1.5l1.409-1.409a2.25 2.25 0 013.182 0l2.909 2.909m-18 3.75h16.5a1.5 1.5 0 001.5-1.5V6a1.5 1.5 0 00-1.5-1.5H3.75A1.5 1.5 0 002.25 6v12a1.5 1.5 0 001.5 1.5zm10.5-11.25h.008v.008h-.008V8.25zm.375 0a.375.375 0 11-.75 0 .375.375 0 01.75 0z"
/>
</svg>
);
}

View File

@@ -0,0 +1,61 @@
"use client";
import { useEditor, EditorContent, useEditorState } from "@tiptap/react";
import Document from "@tiptap/extension-document";
import Paragraph from "@tiptap/extension-paragraph";
import Text from "@tiptap/extension-text";
import Bold from "@tiptap/extension-bold";
import Italic from "@tiptap/extension-italic";
import Typography from "@tiptap/extension-typography";
import { UndoRedo, Placeholder } from "@tiptap/extensions";
import { useEffect } from "react";
export default function Tiptap({
onChange,
}: {
onChange: (args: unknown) => void;
}) {
const editor = useEditor({
extensions: [
Text,
Document,
Paragraph,
Bold,
Italic,
UndoRedo,
Typography,
Placeholder.configure({
placeholder: "Add a photo description",
}),
],
// Don't render immediately on the server to avoid SSR issues
immediatelyRender: false,
editorProps: {
attributes: {
class: "py-1 px-2",
},
},
});
const editorState = useEditorState({
editor,
// the selector function is used to select the state you want to react to
selector: ({ editor }) => {
if (!editor) {
return {
currentContent: null,
};
}
return {
currentContent: editor.getJSON(),
};
},
});
useEffect(() => {
console.log(editorState?.currentContent);
onChange(editorState?.currentContent);
}, [editorState?.currentContent, onChange]);
return <EditorContent className="border border-base-300" editor={editor} />;
}

View File

@@ -0,0 +1,278 @@
"use client";
import type { PhotoData } from "@/server/api/routers/photos/list";
import { api } from "@/trpc/react";
import Image from "next/image";
import type React from "react";
import { useState } from "react";
import ImageSvg from "./file-svg";
import DirSvg from "./dir-svg";
import { Controller, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import z from "zod";
import Tiptap from "./photo-editor";
const FormSchema = z.object({
title: z
.string()
.min(3, "Title should be over 3 characters")
.max(128, "Title cannot be over 128 characters"),
description: z.object({
type: z.string(),
content: z.array(z.unknown()),
}),
});
type IFormInput = z.infer<typeof FormSchema>;
interface DirectoryTree {
[key: string]: DirectoryTree;
}
function buildDirectoryTree(filePaths: string[]): DirectoryTree {
const root: DirectoryTree = {};
filePaths.forEach((path) => {
const parts = path.split("/").filter((p) => p.length > 0);
let current = root;
// Traverse or create nodes for each part of the path
for (let i = 0; i < parts.length; i++) {
const part = parts[i];
if (part) {
if (!current[part]) {
current[part] = {};
}
current = current[part];
}
}
});
return root;
}
type Item = {
type: "directory" | "file";
name: string;
fullPath: string;
children?: Item[];
};
function renderTree(node: DirectoryTree, pathSoFar = ""): Item[] {
const entries = Object.entries(node);
const items: Item[] = [];
for (const [name, children] of entries) {
const fullPath = pathSoFar ? `${pathSoFar}/${name}` : name;
const isLeaf = Object.keys(children).length === 0;
if (isLeaf) {
// It's a file
items.push({ type: "file", name, fullPath });
} else {
// It's a directory
items.push({
type: "directory",
name,
fullPath,
children: renderTree(children, fullPath),
});
}
}
return items;
}
function RenderLeaf(leaf: Item[], selectImageTab: (path: string) => void) {
return leaf.map((leaf) => {
if (leaf.children?.length) {
return (
<li>
<details open>
<summary>
<DirSvg />
{leaf.name}
</summary>
<ul>{RenderLeaf(leaf.children, selectImageTab)}</ul>
</details>
</li>
);
}
return (
<li key={leaf.fullPath}>
<button type="button" onClick={() => selectImageTab(leaf.fullPath)}>
<ImageSvg />
{leaf.name}
</button>
</li>
);
});
}
export function PhotoTab(): React.JSX.Element {
const [selectedImage, setSelectedImage] = useState<PhotoData>();
const query = api.photos.list.useQuery(undefined, {
refetchOnWindowFocus: false,
});
const {
register,
handleSubmit,
control,
formState: { errors },
} = useForm<IFormInput>({
resolver: zodResolver(FormSchema),
mode: "onSubmit",
});
if (query.isLoading) {
return <p>Loading</p>;
}
if (query.error) {
return <p>{query.error.message}</p>;
}
const images = query.data?.data;
if (!images || images?.length === 0) {
return <p>No Images</p>;
}
const selectImageTab = (path: string) => {
const img = images.find(
(img) =>
img.src === `https://fly.storage.tigris.dev/joemonk-photos/${path}`,
);
setSelectedImage(img);
};
const tree = buildDirectoryTree(
images.map((img) =>
img.src.substring(
"https://fly.storage.tigris.dev/joemonk-photos/".length,
),
),
);
const renderedTree = renderTree(tree);
const onSubmit = (data: IFormInput) => {
console.log(data);
};
return (
<div className="flex w-full gap-2">
<ul className="menu menu-xs bg-base-200 box w-1/4">
{RenderLeaf(renderedTree, selectImageTab)}
</ul>
<div className="w-3/4 box border border-base-300 p-2">
{selectedImage?.src ? (
<form onSubmit={handleSubmit(onSubmit)}>
<label
className={`floating-label input text-lg mb-2 w-full ${errors.title ? "input-error" : null}`}
>
<span>{`Title ${errors.title ? " - " + errors.title.message : ""}`}</span>
<input
{...register("title")}
type="text"
placeholder="Title"
defaultValue={selectedImage?.title}
/>
</label>
<Image
src={selectedImage.src}
title={selectedImage?.title}
alt={selectedImage?.title ?? "Image to modify data for"}
width={selectedImage.width}
height={selectedImage.height}
blurDataURL={selectedImage.blur}
placeholder="blur"
/>
<div className="mt-2 grid grid-cols-3">
{[
{
title: "F-Stop",
value: selectedImage.exif.fNumber?.toString(),
},
{
title: "ISO",
value: selectedImage.exif.isoSpeedRatings?.toString(),
},
{
title: "Exposure",
value: selectedImage.exif.exposureBiasValue?.toString(),
},
].map((setting) => {
return (
<div key={setting.title} className="w-full border">
<span className="px-2 w-20 inline-block">
{setting.title}
</span>
<span className="px-2">{setting.value}</span>
</div>
);
})}
</div>
<div className="mt-2 grid grid-cols-3">
{[
{
title: "Taken",
value: selectedImage.exif.takenAt?.toLocaleDateString(),
},
{
title: "Lens",
value: selectedImage.exif.LensModel,
},
{
title: "Camera",
value: selectedImage.camera,
},
].map((setting) => {
return (
<div key={setting.title} className="w-full border">
<span className="px-2 w-20 inline-block">
{setting.title}
</span>
<span className="px-2">{setting.value}</span>
</div>
);
})}
</div>
<div className="mt-2 grid grid-cols-2">
{[
{
title: "Height",
value: selectedImage.height.toString(),
},
{
title: "Width",
value: selectedImage.width.toString(),
},
].map((setting) => {
return (
<div key={setting.title} className="w-full border">
<span className="px-2 w-20 inline-block">
{setting.title}
</span>
<span className="px-2">{setting.value}</span>
</div>
);
})}
</div>
<div className="mt-2 px-2 pb-2 border">
<span>Description</span>
<Controller
control={control}
name="description"
render={({ field: { onChange } }) => (
<Tiptap onChange={onChange} />
)}
/>
</div>
<button className="button" type="submit">
Submit
</button>
</form>
) : null}
</div>
</div>
);
}

View File

@@ -0,0 +1,28 @@
import { PhotoTab } from "./_components/photo-tab";
export default async function Photos(): Promise<React.JSX.Element> {
return (
<div className="mx-auto">
<div className="tabs tabs-lift">
<input
type="radio"
name="admin_tabs"
className="tab"
aria-label="Posts"
/>
<div className="tab-content bg-base-100 border-base-300 p-4"></div>
<input
type="radio"
name="admin_tabs"
className="tab"
aria-label="Photos"
defaultChecked
/>
<div className="tab-content bg-base-100 border-base-300 p-4">
<PhotoTab />
</div>
</div>
</div>
);
}

View File

@@ -7,7 +7,7 @@ export default async function Photos(): Promise<React.JSX.Element> {
return (
<div className="mx-auto">
<FilteredLightbox imageData={images}>
<FilteredLightbox photoData={images}>
{images.map((image) => (
<Image
key={image.src}

View File

@@ -48,8 +48,8 @@ export default async function Posts(): Promise<React.JSX.Element> {
<div className="flex flex-wrap sm:grid-cols-2">
{postDetails.map((post) => {
return (
<div key={post.link} className="sm:max-w-1/2 grow">
<div className="card card-border m-2 bg-neutral text-neutral-content shadow-md">
<div key={post.link} className="sm:max-w-1/2 grow p-2">
<div className="card card-border bg-base-300 shadow-md w-full h-full">
<div className="card-body">
<h1 className="card-title">{post.metadata.title}</h1>
<time dateTime={post.metadata.date}>{post.metadata.date}</time>
@@ -64,7 +64,10 @@ export default async function Posts(): Promise<React.JSX.Element> {
</div>
<p>{post.metadata.blurb}</p>
<div className="card-actions justify-end pt-2">
<Link className="btn btn-primary bg-primary text-primary-content" href={post.link}>
<Link
className="btn btn-primary hover:bg-primary/10"
href={post.link}
>
Read
</Link>
</div>

View File

@@ -1,6 +1,6 @@
import type React from "react";
import Cv from "@/components/cv";
import Cv from "@/app/_components/cv";
export default function CvPrint(): React.JSX.Element {
return <Cv />;

View File

@@ -21,7 +21,7 @@ export default async function LogIn(): Promise<React.JSX.Element | undefined> {
>
<button
type="submit"
className="btn btn-circle hover:bg-primary/25 group border-2 border-primary/75 p-1 transition-colors duration-100"
className="btn btn-outline btn-circle hover:bg-primary/25 group border-2 border-primary/75 p-1 transition-colors duration-100"
>
<UserIcon
className={`h-8 w-auto transition-colors ${

View File

@@ -162,8 +162,7 @@ export default function Cv(): React.JSX.Element {
development, has driven my proficiency with many languages and
tools. This allows me to be flexible when tackling problems. Over
the last few years I have enjoyed expanding my role to include
management of multiple teams, and have picked up new tech stacks
while moving between roles.
management of multiple teams, large scale architecture.
</p>
</div>
<div className="flex flex-row align-middle gap-2 px-2 py-1">

View File

@@ -1,8 +1,8 @@
// TODO
export default function NavBar(): React.JSX.Element {
return (
<footer className="border-t-2 border-accent bg-base-300">
<div className="mx-auto max-w-7xl px-4">
<footer className="border-t-2 border-accent bg-base-200">
<div className="mx-auto max-w-5xl px-4 py-2">
<div className="relative flex h-12 flex-row-reverse items-center justify-between">
<span className="select-none">© Joe Monk 2025</span>
</div>

View File

@@ -17,7 +17,7 @@ import "yet-another-react-lightbox/plugins/thumbnails.css";
import "yet-another-react-lightbox/plugins/captions.css";
import { api, type RouterOutputs } from "@/trpc/react";
type ImageData = RouterOutputs["photos"]["list"]["data"][number];
type PhotoData = RouterOutputs["photos"]["list"]["data"][number];
function NextJsImage({
slide,
@@ -25,7 +25,7 @@ function NextJsImage({
rect,
unoptimized = false,
}: {
slide: ImageData;
slide: PhotoData;
offset: number;
rect: { width: number; height: number };
unoptimized: boolean;
@@ -78,10 +78,10 @@ function NextJsImage({
}
export function Lightbox({
imageData,
photoData: photoData,
children,
}: {
imageData: ImageData[];
photoData: PhotoData[];
children: React.JSX.Element[];
}): React.JSX.Element {
const [active, setActive] = useState<number | null>(null);
@@ -107,7 +107,7 @@ export function Lightbox({
open={typeof active === "number"}
close={() => setActive(null)}
index={active ?? undefined}
slides={imageData}
slides={photoData}
render={{
// @ts-expect-error - Todo
slide: (args) => NextJsImage({ ...args, unoptimized: true }),
@@ -129,11 +129,11 @@ interface UsernameFormElement extends HTMLFormElement {
// TODO
export default function FilteredLightbox(props: {
imageData: ImageData[];
photoData: PhotoData[];
children: React.JSX.Element[];
}): React.JSX.Element {
//const [imageData, setImageData] = useState(props.imageData);
const [imageData] = useState(props.imageData);
//const [photoData, setImageData] = useState(props.photoData);
const [photoData] = useState(props.photoData);
const photoQuery = api.photos.list.useInfiniteQuery(
{
limit: 1,
@@ -142,8 +142,8 @@ export default function FilteredLightbox(props: {
initialData: {
pages: [
{
data: props.imageData,
next: props.imageData.length,
data: props.photoData,
next: props.photoData.length,
},
],
pageParams: [0],
@@ -159,9 +159,9 @@ export default function FilteredLightbox(props: {
function handleSubmit(event: React.FormEvent<UsernameFormElement>): void {
event.preventDefault();
// const imageData = props.imageData;
// const photoData = props.photoData;
// setImageData(
// imageData.filter(
// photoData.filter(
// (data) => data.src === event.currentTarget.elements.src.value
// )
// );
@@ -186,6 +186,7 @@ export default function FilteredLightbox(props: {
))
.filter((data) => !!data);
refreshQuery.error ? console.log(refreshQuery.error) : null;
return (
<>
<form onSubmit={handleSubmit}>
@@ -198,14 +199,15 @@ export default function FilteredLightbox(props: {
<button
type="button"
onClick={() => {
console.log("refetch");
void refreshQuery.refetch();
}}
>
Refresh
</button>
{refreshQuery.data ? JSON.stringify(refreshQuery.data) : "\nNot"}
{refreshQuery.error ? JSON.stringify(refreshQuery.error) : "\nNo Error"}
<Lightbox imageData={imageData}>{...children}</Lightbox>
{refreshQuery.data ? JSON.stringify(refreshQuery.data) : "No data"}
{refreshQuery.error ? JSON.stringify(refreshQuery.error) : "No Error"}
<Lightbox photoData={photoData}>{...children}</Lightbox>
</>
);
}

View File

@@ -50,7 +50,7 @@ export default function NavBarClient({
<div className="flex">
<button
type="button"
className="btn rounded-sm
className="btn btn-outline rounded-sm
border-2 border-primary/75 p-1 transition-colors duration-100 sm:hidden"
onClick={() => setOpen(!open)}
>
@@ -61,7 +61,7 @@ export default function NavBarClient({
)}
</button>
<Link
className="btn hidden items-center rounded border-2 border-primary/75 p-1 transition-colors hover:bg-primary/25 sm:flex"
className="btn btn-outline hidden items-center rounded border-2 border-primary/75 p-1 transition-colors hover:bg-primary/25 sm:flex"
href="/"
>
<HomeModernIcon className="h-8 w-auto rounded-sm" />
@@ -71,7 +71,7 @@ export default function NavBarClient({
<Link
key={item.name}
href={item.href}
className={`btn min-w-20 rounded-lg rounded-b-none border-transparent border-b-2 px-2 py-1 pt-1.5 text-center text-lg font-medium hover:border-primary hover:bg-primary/25 ${
className={`btn btn-outline min-w-20 rounded-lg rounded-b-none border-transparent border-b-2 px-2 py-1 pt-1.5 text-center text-lg font-medium hover:border-primary hover:bg-primary/25 ${
item.current ? "border-b-accent/75" : ""
}`}
aria-current={item.current ? "page" : undefined}
@@ -101,7 +101,7 @@ export default function NavBarClient({
<Link
key={item.name}
href={item.href}
className={`btn border-transparent border-l-4 px-4 py-2 transition-colors duration-100 hover:border-primary hover:bg-primary/25 ${
className={`btn btn-outline border-transparent border-l-4 px-4 py-2 transition-colors duration-100 hover:border-primary hover:bg-primary/25 ${
item.current ? "" : "border-primary"
}`}
aria-current={item.current ? "page" : undefined}

View File

@@ -13,9 +13,10 @@ const defaultNavigation = [
const authedNavigation = [{ name: "Manage", href: "/manage", current: false }];
export default async function NavBar(): Promise<React.JSX.Element> {
const session = await auth();
let nav = structuredClone(defaultNavigation);
const session = await auth();
console.log(session);
if (session?.user) {
nav = nav.concat(structuredClone(authedNavigation));
}

View File

@@ -3,7 +3,6 @@ type postMetadata = {
date: string;
coverImage: string;
blurb: string;
shortBlurb: string;
tags: string[];
};
@@ -17,9 +16,11 @@ export default function PostHeader({
}: PostHeaderProps): React.JSX.Element {
return (
<>
<h1>{metadata.title}</h1>
<time dateTime={metadata.date}>{metadata.date}</time>
<div className="mb-6 flex gap-2">
<h1 className="mb-2">{metadata.title}</h1>
<div className="mb-4 text-primary-content/80">
<time dateTime={metadata.date}>{metadata.date}</time>
</div>
<div className="mb-2 flex gap-2">
{metadata.tags.map((tag) => {
return (
<div key={`${metadata.title}_tag_${tag}`}>
@@ -28,6 +29,7 @@ export default function PostHeader({
);
})}
</div>
<div className="divider divider-accent" />
</>
);
}

View File

@@ -6,19 +6,19 @@ import type React from "react";
export default function ThemeSwitcher(): React.JSX.Element {
const toggleTheme = (): void => {
const currentTheme = document.documentElement.getAttribute("data-theme");
if (currentTheme === "nord") {
if (currentTheme === "alucard") {
localStorage.theme = "dracula-soft";
document.documentElement.setAttribute("data-theme", "dracula-soft");
} else {
localStorage.theme = "nord";
document.documentElement.setAttribute("data-theme", "nord");
localStorage.theme = "alucard";
document.documentElement.setAttribute("data-theme", "alucard");
}
};
return (
<button
type="button"
className="btn w-9 h-9 btn-circle border-2 hover:bg-primary/25 border-primary/75 p-1 transition-colors duration-100"
className="btn btn-outline w-9 h-9 btn-circle border-2 hover:bg-primary/25 border-primary/75 p-1 transition-colors duration-100"
onClick={toggleTheme}
>
<MoonIcon className="block dark:hidden" />

View File

@@ -1,3 +1,22 @@
import { handlers } from "@/server/auth";
import { NextRequest } from "next/server";
export const { GET, POST } = handlers;
const reqWithTrustedOrigin = (req: NextRequest): NextRequest => {
const proto = req.headers.get("x-forwarded-proto");
const host = req.headers.get("x-forwarded-host");
if (!proto || !host) {
console.warn("Missing x-forwarded-proto or x-forwarded-host headers.");
return req;
}
const envOrigin = `${proto}://${host}`;
const { href, origin } = req.nextUrl;
return new NextRequest(href.replace(origin, envOrigin), req);
};
export const GET = (req: NextRequest): Promise<Response> => {
return handlers.GET(reqWithTrustedOrigin(req));
};
export const POST = (req: NextRequest): Promise<Response> => {
return handlers.POST(reqWithTrustedOrigin(req));
};

View File

@@ -1,7 +1,6 @@
import { fetchRequestHandler } from "@trpc/server/adapters/fetch";
import type { NextRequest } from "next/server";
import { env } from "@/env";
import { appRouter } from "@/server/api/root";
import { createTRPCContext } from "@/server/api/trpc";
@@ -22,7 +21,7 @@ const handler = (req: NextRequest) =>
router: appRouter,
createContext: () => createContext(req),
onError:
env.NODE_ENV === "development"
process.env.NODE_ENV === "development"
? ({ path, error }) => {
console.error(
`❌ tRPC failed on ${path ?? "<no-path>"}: ${error.message}`,

View File

@@ -22,7 +22,11 @@ export default function RootLayout({
children,
}: Readonly<{ children: React.ReactNode }>) {
return (
<html lang="en" className={`${inter.variable}`} suppressHydrationWarning>
<html
lang="en"
className={`${inter.variable} w-screen overflow-x-hidden`}
suppressHydrationWarning
>
<head>
<script
id="SetTheme"

View File

@@ -1,6 +1,7 @@
import { env } from "@/env";
export function getBaseUrl() {
if (typeof window !== "undefined") return window.location.origin;
return `http://localhost:${env.PORT ?? 3000}`;
export function getBaseUrl(): string {
if (process.env.NODE_ENV === "production") {
return "https://joemonk.co.uk";
} else {
return "https://3000.vscode.home.joemonk.co.uk";
}
}

View File

@@ -4,7 +4,6 @@ export const metadata = {
title: "Being a Developer",
date: "2020-05-12",
blurb: "My thoughts on being a \"developer\", being a \"programmer\" and the differences between them.",
shortBlurb: "My thoughts on being a developer vs being a programmer.",
tags: ["Blog", "Development"]
}

View File

@@ -5,7 +5,6 @@ export const metadata = {
date: "2020-12-31",
path: "/posts/learning-Kubernetes",
blurb: "Learning how to use Kubenetes in an environment between \"Local testing\" and \"Full server deployments\".",
shortBlurb: "Finally getting around to \"learning\" Kubernetes.",
tags: ["Blog", "Development"],
}

View File

@@ -4,7 +4,6 @@ export const metadata = {
title: "Managing a Team Remotely",
date: "2020-10-05",
blurb: "With working remotely being a necessity at the moment, my thoughts on managing a team of developers with no physicality.",
shortBlurb: "My thoughts managing a team of developers with no physicality.",
tags: ["Blog", "Development"]
}

View File

@@ -0,0 +1,80 @@
import PostHeader from '@/app/_components/post-header';
export const metadata = {
title: "Setting Up Local Copilot",
date: "2025-07-24",
path: "/posts/setting-up-local-copilot",
blurb: "Setting up a locally running version of Copilot to experiment and learn some basic AI usage.",
tags: ["Blog", "Development"],
}
<PostHeader metadata={metadata} />
## Why
Copilot and other AI tools are generally a bit divisive. Lots of people harp on about it changing everything, and others say it's completely useless. I'm somewhere in the middle, and see it as another tool to help me do my job better. Intellisense on steroids, with some extra features. It's not going away anytime soon either so I think it's worth learning how to use.
It is however, difficult. You don't want to spend a lot on subscriptions and other services to just mess around with. 10 dollars a month isn't much, but it can quickly add up if you try out multiple of the "latest" tools. So instead, let's set up an equivalent, locally, for free.
## Tools
There are loads and loads of options out there. I was mostly after the code completion features and I wanted to be able to at least connect to it from [code-server](https://github.com/coder/code-server) instance, if not run it from that server.
I first wanted to figure out how to run my models. [Ollama](https://ollama.com/) makes it really really easy to get set up and running with loads of models. [LM Studio](https://lmstudio.ai/) seemed like another good option with a decent looking ui so I left both options open.
After playing around with a few different tools like KiloCode, Roo Code, continue.dev connecting to either Ollama or LM Studio, I didn't really get the results I wanted. The code completion either wasn't supported by the extension or it was difficult to use. I then found [Twinny](https://github.com/twinnydotdev/twinny) which seemed to be almost exactly what I wanted.
## Setup
### Ollama
One of the reasons I liked Ollama over other options is because it's so stupidly easy to run.
- Install [Docker](https://www.docker.com/) if for some reason you don't already have it.
- Ensure [CUDA drivers](https://developer.nvidia.com/cuda-toolkit) are installed if looking to utilise a Nvidia GPU.
- Run `docker run -d -v ./ollama:/root/.ollama -p 11434:11434 --gpus=all --name ollama ollama/ollama:0.10.1` to start the container, mapping an `ollama` directory in your cwd to keep downloaded models locally, attaching your GPU and exposing the port.
- You can then pull pull models with `docker exec -it ollama ollama pull bge-m3:567m`.
### Twinny
Just install Twinny from the VS Code [marketplace](https://open-vsx.org/extension/rjmacarthy/twinny) (I use code-server, so Open-vsx is my marketplace).
Configure the chat, fim and embedding providers to point at your Ollama instance.
#### Chat
Type: chat
Provider: ollama
Proto: http
Model Name: Pick your basic model here, options will change over time but any models you pull should be available and I used qwen3:8b
Hostname: localhost (or ip of Ollama server, I run it on a separate machine)
Port: If following above, 11434. Otherwise the port you exposed when running Ollama.
API Path: /v1
#### FIM Provider
Type: fim
FIM Template: Pick the one that relates to your model, i.e. codeqwen
Provider: ollama
Proto: http
Model Name: Pick your code model here, they're set up specifically for FIM, I used qwen2.5-coder:7b-base
Hostname: localhost (or ip of Ollama server, I run it on a separate machine)
Port: If following above, 11434. Otherwise the port you exposed when running Ollama.
API Path: /api/generate
#### FIM Provider
Type: embedding
Provider: ollama
Proto: http
Model Name: Pick your embedding model here, I used bge-m3:567m
Hostname: localhost (or ip of Ollama server, I run it on a separate machine)
Port: If following above, 11434. Otherwise the port you exposed when running Ollama.
API Path: /api/embed
## Additional models
Adding or switching models is as simple as running `docker exec -it ollama ollama pull <model>`, models can be found on the [Ollama site](https://ollama.com/search)
## Conclusion
This has been super simple to set up once the tools were picked, and the output is quick enough running on a remote (but on the same network) mobile RTX 4070. Being able to turn it on when doing actual coding tasks and set all up for free with no limits has been really nice. I'll probably mess around with other extensions and agentic tools next, and I'd like to be able to add docs for use like some of the other extensions.

View File

@@ -8,7 +8,11 @@ export function useMDXComponents(components: MDXComponents): MDXComponents {
}: {
children: React.JSX.Element[];
}): React.JSX.Element => {
return <article className="prose mx-auto">{children}</article>;
return (
<article className="prose mx-auto first:prose-h2:mt-8">
{children}
</article>
);
},
...components,
};

View File

@@ -2,7 +2,7 @@ import { shake } from "radash";
import { db } from "@/server/db";
import { photos } from "@/server/db/schema";
export type ImageData = {
export type PhotoData = {
width: number;
height: number;
blur: `data:image/${string}`;
@@ -25,7 +25,7 @@ export type ListOptions = {
limit: number;
};
export async function list(options: ListOptions): Promise<ImageData[]> {
export async function list(options: ListOptions): Promise<PhotoData[]> {
const currentSources = await db
.select()
.from(photos)

View File

@@ -13,7 +13,7 @@ export const photosRouter = createTRPCRouter({
.input(
z
.object({
limit: z.number().nonnegative().default(1),
limit: z.number().nonnegative().default(2),
cursor: z.number().nonnegative().default(0),
})
.optional()
@@ -36,5 +36,5 @@ export const photosRouter = createTRPCRouter({
next,
};
}),
update: protectedProcedure.query(update),
update: publicProcedure.query(update),
});

View File

@@ -46,7 +46,7 @@ export async function update(): Promise<string[]> {
return [];
}
const imageData = newPhotos.map(async (fileName: string) => {
const photoData = newPhotos.map(async (fileName: string) => {
const getImageCmd = new GetObjectCommand({
Bucket: "joemonk-photos",
Key: fileName.replace(
@@ -81,7 +81,7 @@ export async function update(): Promise<string[]> {
return photo;
});
const images = await Promise.all(imageData);
const images = await Promise.all(photoData);
await db.insert(photos).values(images);

View File

@@ -1,5 +1,4 @@
import type { DefaultSession, NextAuthConfig } from "next-auth";
import { env } from "@/env";
import { getBaseUrl } from "@/lib/base-url";
/**
@@ -35,19 +34,13 @@ export const authConfig = {
name: "Authelia",
type: "oidc",
issuer: "https://auth.home.joemonk.co.uk",
clientId: env.AUTH_CLIENT_ID,
clientSecret: env.AUTH_CLIENT_SECRET,
clientId: process.env.AUTH_CLIENT_ID,
clientSecret: process.env.AUTH_CLIENT_SECRET,
wellKnown:
"https://auth.home.joemonk.co.uk/.well-known/openid-configuration",
idToken: true,
},
],
trustHost: true,
redirectProxyUrl: `${getBaseUrl()}/api/auth`,
callbacks: {
session: ({ session, user }) => ({
...session,
user: {
...session.user,
id: user.id,
},
}),
},
} satisfies NextAuthConfig;

View File

@@ -14,6 +14,6 @@ const globalForDb = globalThis as unknown as {
export const client =
globalForDb.client ?? createClient({ url: env.DATABASE_URL });
if (env.NODE_ENV !== "production") globalForDb.client = client;
if (process.env.NODE_ENV !== "production") globalForDb.client = client;
export const db = drizzle(client, { schema });

View File

@@ -2,10 +2,50 @@
@import "tailwindcss";
@plugin "@tailwindcss/typography";
@plugin "daisyui" {
themes: nord --default;
@plugin "daisyui";
@plugin "daisyui/theme" {
name: "alucard";
default: true;
prefersdark: false;
color-scheme: "light";
--color-base-100: oklch(97.02% 0.000 0);
--color-base-200: oklch(95.20% 0.007 268.55);
--color-base-300: oklch(88.75% 0.015 264.49);
--color-base-content: oklch(23.93% 0.000 0);
--color-primary: oklch(64.21% 0.086 228.32);
--color-primary-content: oklch(100% 0 0);
--color-secondary: oklch(67.53% 0.129 27.41);
--color-secondary-content: oklch(23.93% 0.000 0);
--color-accent: oklch(50.93% 0.091 287.46);
--color-accent-content: oklch(100% 0 0);
--color-neutral: oklch(45.68% 0.000 0);
--color-neutral-content: oklch(100% 0 0);
--color-info: oklch(49.47% 0.122 243.83);
--color-info-content: oklch(100% 0 0);
--color-success: oklch(63.12% 0.124 141.91);
--color-success-content: oklch(0% 0 0);
--color-warning: oklch(76.96% 0.156 99.76);
--color-warning-content: oklch(0% 0 0);
--color-error: oklch(52.56% 0.199 5.45);
--color-error-content: oklch(100% 0 0);
--radius-selector: 1rem;
--radius-field: 0.5rem;
--radius-box: 1rem;
--size-selector: 0.25rem;
--size-field: 0.25rem;
--border: 1px;
--depth: 1;
--noise: 0;
}
@plugin "daisyui/theme" {
/* Nicked from the vscode soft theme https://github.com/dracula/visual-studio-code/blob/master/src/dracula.yml */
name: "dracula-soft";
@@ -20,8 +60,7 @@
--color-base-content: oklch(91% 0.02 278);
--color-primary: oklch(88.263% 0.093 212.846);
--color-primary-content: oklch(88.263% 0.093 212.846);
/* --color-primary-content: oklch(17.652% 0.018 212.846); */
--color-primary-content: oklch(16.678% 0.024 66.558);
--color-secondary: oklch(83.392% 0.124 66.558);
--color-secondary-content: oklch(16.678% 0.024 66.558);
--color-accent: oklch(74.202% 0.148 301.883);
@@ -59,7 +98,7 @@
@custom-variant dark (&:where([data-theme=dracula-soft], [data-theme=dracula-soft] *));
@utility btn {
@apply shadow-none bg-transparent;
@apply shadow-none;
}
:root .prose {

View File

@@ -7,12 +7,11 @@ import type { inferRouterInputs, inferRouterOutputs } from "@trpc/server";
import { useState } from "react";
import SuperJSON from "superjson";
import { env } from "@/env";
import { getBaseUrl } from "@/lib/base-url";
import type { AppRouter } from "@/server/api/root";
import { createQueryClient } from "./query-client";
import { env } from "@/env";
let clientQueryClientSingleton: QueryClient | undefined;
let clientQueryClientSingleton: QueryClient | undefined = undefined;
const getQueryClient = () => {
if (typeof window === "undefined") {
// Server: always make a new query client
@@ -48,7 +47,7 @@ export function TRPCReactProvider(props: { children: React.ReactNode }) {
links: [
loggerLink({
enabled: (op) =>
env.NODE_ENV === "development" ||
process.env.NODE_ENV === "development" ||
(op.direction === "down" && op.result instanceof Error),
}),
httpBatchStreamLink({
@@ -72,3 +71,9 @@ export function TRPCReactProvider(props: { children: React.ReactNode }) {
</QueryClientProvider>
);
}
function getBaseUrl() {
if (typeof window !== "undefined") return window.location.origin;
if (process.env.VERCEL_URL) return `https://${process.env.VERCEL_URL}`;
return "https://3000.vscode.home.joemonk.co.uk";
}