Add a manage page with tabs
This commit is contained in:
5
.vscode/settings.json
vendored
5
.vscode/settings.json
vendored
@@ -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
155
bun.lock
@@ -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=="],
|
||||
|
||||
@@ -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
1
reset.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
import "@total-typescript/ts-reset";
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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 />
|
||||
|
||||
19
src/app/(root)/manage/_components/dir-svg.tsx
Normal file
19
src/app/(root)/manage/_components/dir-svg.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
19
src/app/(root)/manage/_components/file-svg.tsx
Normal file
19
src/app/(root)/manage/_components/file-svg.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
61
src/app/(root)/manage/_components/photo-editor.tsx
Normal file
61
src/app/(root)/manage/_components/photo-editor.tsx
Normal 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} />;
|
||||
}
|
||||
278
src/app/(root)/manage/_components/photo-tab.tsx
Normal file
278
src/app/(root)/manage/_components/photo-tab.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
28
src/app/(root)/manage/page.tsx
Normal file
28
src/app/(root)/manage/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 />;
|
||||
|
||||
@@ -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 ${
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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" />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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));
|
||||
};
|
||||
|
||||
@@ -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}`,
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
|
||||
@@ -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"],
|
||||
}
|
||||
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
|
||||
80
src/markdown/posts/[...slug]/setting-up-local-copilot.mdx
Normal file
80
src/markdown/posts/[...slug]/setting-up-local-copilot.mdx
Normal 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.
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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),
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user