diff --git a/.storybook/preview.tsx b/.storybook/preview.tsx index 064dbc6..883e62d 100644 --- a/.storybook/preview.tsx +++ b/.storybook/preview.tsx @@ -1,14 +1,19 @@ -import React from "react"; -import type { Preview } from "@storybook/react"; -import CssBaseline from "@mui/material/CssBaseline"; -import { ThemeProvider } from "@mui/material/styles"; -import { AppRouterCacheProvider } from "@mui/material-nextjs/v14-appRouter"; +import React from 'react'; +import type { Preview } from '@storybook/react'; +import CssBaseline from '@mui/material/CssBaseline'; +import { ThemeProvider } from '@mui/material/styles'; +import { AppRouterCacheProvider } from '@mui/material-nextjs/v14-appRouter'; +import { initialize, mswLoader } from 'msw-storybook-addon'; // import { baseTheme } from '@/styles/baseTheme'; -import { baseTheme } from "../src/_styles/baseTheme"; +import { baseTheme } from '../src/_styles/baseTheme'; // import theme from './theme'; - +initialize(); const preview: Preview = { parameters: { + actions: { argTypesRegex: '^on.*|^handle*' }, + nextjs: { + appDirectory: true, + }, controls: { matchers: { color: /(background|color)$/i, @@ -16,6 +21,7 @@ const preview: Preview = { }, }, }, + loaders: [mswLoader], // 👈 Add the MSW loader to all stories decorators: [ (Story) => ( diff --git a/.vscode/settings.json b/.vscode/settings.json index 28f2413..e8d6718 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -45,5 +45,6 @@ "prettier.jsxSingleQuote": true, "eslint.codeActionsOnSave.rules": null, "prettier.singleQuote": true, - "prettier.trailingComma": "es5" + "prettier.trailingComma": "es5", + "githubPullRequests.ignoredPullRequestBranches": ["main"] } diff --git a/package-lock.json b/package-lock.json index d8eb31f..cd05c4d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -55,6 +55,7 @@ "remark-unwrap-images": "^4.0.0", "sharp": "^0.33.3", "ts-node": "^10.9.2", + "uuid": "^10.0.0", "zod": "^3.22.4" }, "devDependencies": { @@ -118,6 +119,8 @@ "jest-environment-jsdom": "^29.7.0", "jest-fail-on-console": "^3.1.2", "lint-staged": "^15.2.2", + "msw": "^2.3.1", + "msw-storybook-addon": "^2.0.2", "npm-run-all": "^4.1.5", "postcss": "^8.4.38", "prettier": "3.3.2", @@ -2363,6 +2366,33 @@ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "dev": true }, + "node_modules/@bundled-es-modules/cookie": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@bundled-es-modules/cookie/-/cookie-2.0.0.tgz", + "integrity": "sha512-Or6YHg/kamKHpxULAdSqhGqnWFneIXu1NKvvfBBzKGwpVsYuFIQ5aBPHDnnoR3ghW1nvSkALd+EF9iMtY7Vjxw==", + "dev": true, + "dependencies": { + "cookie": "^0.5.0" + } + }, + "node_modules/@bundled-es-modules/cookie/node_modules/cookie": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", + "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@bundled-es-modules/statuses": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@bundled-es-modules/statuses/-/statuses-1.0.1.tgz", + "integrity": "sha512-yn7BklA5acgcBr+7w064fGV+SGIFySjCKpqjcWgBAIfrAkY+4GQTJJHQMeT3V/sgz23VTEVV8TtOmkvJAhFVfg==", + "dev": true, + "dependencies": { + "statuses": "^2.0.1" + } + }, "node_modules/@chromatic-com/storybook": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@chromatic-com/storybook/-/storybook-1.5.0.tgz", @@ -4430,6 +4460,92 @@ "url": "https://opencollective.com/libvips" } }, + "node_modules/@inquirer/confirm": { + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-3.1.9.tgz", + "integrity": "sha512-UF09aejxCi4Xqm6N/jJAiFXArXfi9al52AFaSD+2uIHnhZGtd1d6lIGTRMPouVSJxbGEi+HkOWSYaiEY/+szUw==", + "dev": true, + "dependencies": { + "@inquirer/core": "^8.2.2", + "@inquirer/type": "^1.3.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/core": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-8.2.2.tgz", + "integrity": "sha512-K8SuNX45jEFlX3EBJpu9B+S2TISzMPGXZIuJ9ME924SqbdW6Pt6fIkKvXg7mOEOKJ4WxpQsxj0UTfcL/A434Ww==", + "dev": true, + "dependencies": { + "@inquirer/figures": "^1.0.3", + "@inquirer/type": "^1.3.3", + "@types/mute-stream": "^0.0.4", + "@types/node": "^20.12.13", + "@types/wrap-ansi": "^3.0.0", + "ansi-escapes": "^4.3.2", + "chalk": "^4.1.2", + "cli-spinners": "^2.9.2", + "cli-width": "^4.1.0", + "mute-stream": "^1.0.0", + "signal-exit": "^4.1.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^6.2.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/core/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@inquirer/core/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@inquirer/figures": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.3.tgz", + "integrity": "sha512-ErXXzENMH5pJt5/ssXV0DfWUZqly8nGzf0UcBV9xTnP+KyffE2mqyxIMBrZ8ijQck2nU0TQm40EQB53YreyWHw==", + "dev": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/type": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-1.3.3.tgz", + "integrity": "sha512-xTUt0NulylX27/zMx04ZYar/kr1raaiFTVvQ5feljQsiAgdm0WPj4S73/ye0fbslh+15QrIuDvfCXTek7pMY5A==", + "dev": true, + "engines": { + "node": ">=18" + } + }, "node_modules/@ioredis/commands": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.2.0.tgz", @@ -5604,6 +5720,18 @@ } } }, + "node_modules/@langchain/community/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/@langchain/core": { "version": "0.1.54", "resolved": "https://registry.npmjs.org/@langchain/core/-/core-0.1.54.tgz", @@ -5647,6 +5775,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@langchain/core/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/@langchain/openai": { "version": "0.0.26", "resolved": "https://registry.npmjs.org/@langchain/openai/-/openai-0.0.26.tgz", @@ -6290,6 +6430,38 @@ "node": ">= 10" } }, + "node_modules/@mswjs/cookies": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@mswjs/cookies/-/cookies-1.1.1.tgz", + "integrity": "sha512-W68qOHEjx1iD+4VjQudlx26CPIoxmIAtK4ZCexU0/UJBG6jYhcuyzKJx+Iw8uhBIGd9eba64XgWVgo20it1qwA==", + "dev": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@mswjs/interceptors": { + "version": "0.29.1", + "resolved": "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.29.1.tgz", + "integrity": "sha512-3rDakgJZ77+RiQUuSK69t1F0m8BQKA8Vh5DCS5V0DWvNY67zob2JhhQrhCO0AKLGINTRSFd1tBaHcJTkhefoSw==", + "dev": true, + "dependencies": { + "@open-draft/deferred-promise": "^2.2.0", + "@open-draft/logger": "^0.3.0", + "@open-draft/until": "^2.0.0", + "is-node-process": "^1.2.0", + "outvariant": "^1.2.1", + "strict-event-emitter": "^0.5.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@mswjs/interceptors/node_modules/strict-event-emitter": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/strict-event-emitter/-/strict-event-emitter-0.5.1.tgz", + "integrity": "sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==", + "dev": true + }, "node_modules/@mui/base": { "version": "5.0.0-beta.40", "resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-beta.40.tgz", @@ -7480,6 +7652,22 @@ "resolved": "https://registry.npmjs.org/@open-draft/deferred-promise/-/deferred-promise-2.2.0.tgz", "integrity": "sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==" }, + "node_modules/@open-draft/logger": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@open-draft/logger/-/logger-0.3.0.tgz", + "integrity": "sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ==", + "dev": true, + "dependencies": { + "is-node-process": "^1.2.0", + "outvariant": "^1.4.0" + } + }, + "node_modules/@open-draft/until": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@open-draft/until/-/until-2.1.0.tgz", + "integrity": "sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==", + "dev": true + }, "node_modules/@opensearch-project/opensearch": { "version": "2.6.0", "resolved": "https://registry.npmjs.org/@opensearch-project/opensearch/-/opensearch-2.6.0.tgz", @@ -9732,6 +9920,19 @@ "url": "https://opencollective.com/storybook" } }, + "node_modules/@storybook/addon-actions/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "dev": true, + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/@storybook/addon-backgrounds": { "version": "8.1.9", "resolved": "https://registry.npmjs.org/@storybook/addon-backgrounds/-/addon-backgrounds-8.1.9.tgz", @@ -12151,6 +12352,12 @@ "@types/node": "*" } }, + "node_modules/@types/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==", + "dev": true + }, "node_modules/@types/cross-spawn": { "version": "6.0.6", "resolved": "https://registry.npmjs.org/@types/cross-spawn/-/cross-spawn-6.0.6.tgz", @@ -12420,10 +12627,19 @@ "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.34.tgz", "integrity": "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==" }, + "node_modules/@types/mute-stream": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/@types/mute-stream/-/mute-stream-0.0.4.tgz", + "integrity": "sha512-CPM9nzrCPPJHQNA9keH9CVkVI+WR5kMa+7XEs5jcGQ0VoAGnLv242w8lIVgwAEfmE4oufJRaTc9PNLQl0ioAow==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/node": { - "version": "20.12.4", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.4.tgz", - "integrity": "sha512-E+Fa9z3wSQpzgYQdYmme5X3OTuejnnTx88A6p6vkkJosR3KBz+HpE3kqNm98VE6cfLFcISx7zW7MsJkH6KwbTw==", + "version": "20.14.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.3.tgz", + "integrity": "sha512-Nuzqa6WAxeGnve6SXqiPAM9rA++VQs+iLZ1DDd56y0gdvygSZlQvZuvdFPR3yLqkVxPu4WrO02iDEyH1g+wazw==", "dependencies": { "undici-types": "~5.26.4" } @@ -12555,6 +12771,12 @@ "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", "dev": true }, + "node_modules/@types/statuses": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/statuses/-/statuses-2.0.5.tgz", + "integrity": "sha512-jmIUGWrAiwu3dZpxntxieC+1n/5c3mjrImkmOSQ2NC5uP6cYO4aAZDdSmRcI5C1oiTmqlZGHC+/NmJrKogbP5A==", + "dev": true + }, "node_modules/@types/tough-cookie": { "version": "4.0.5", "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", @@ -12580,6 +12802,12 @@ "@types/node": "*" } }, + "node_modules/@types/wrap-ansi": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/wrap-ansi/-/wrap-ansi-3.0.0.tgz", + "integrity": "sha512-ltIpx+kM7g/MLRZfkbL7EsCEjfzCcScLpkg37eXEtx5kmrAKBkTJwd1GIAjDSL8wTpM6Hzn5YO4pSb91BEwu1g==", + "dev": true + }, "node_modules/@types/yargs": { "version": "17.0.32", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", @@ -15529,7 +15757,6 @@ "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", "dev": true, - "peer": true, "engines": { "node": ">= 12" } @@ -22226,6 +22453,15 @@ "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", "dev": true }, + "node_modules/graphql": { + "version": "16.8.2", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.8.2.tgz", + "integrity": "sha512-cvVIBILwuoSyD54U4cF/UXDh5yAobhNV/tPygI4lZhgOIJQE/WLWC4waBRb4I6bDVYb3OVx3lfHbaQOEoUD5sg==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" + } + }, "node_modules/gray-matter": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/gray-matter/-/gray-matter-4.0.3.tgz", @@ -22684,6 +22920,12 @@ "he": "bin/he" } }, + "node_modules/headers-polyfill": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/headers-polyfill/-/headers-polyfill-4.0.3.tgz", + "integrity": "sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ==", + "dev": true + }, "node_modules/heap": { "version": "0.2.7", "resolved": "https://registry.npmjs.org/heap/-/heap-0.2.7.tgz", @@ -23795,6 +24037,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-node-process": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-node-process/-/is-node-process-1.2.0.tgz", + "integrity": "sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==", + "dev": true + }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -26832,6 +27080,18 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/langchain/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/langchainhub": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/langchainhub/-/langchainhub-0.0.8.tgz", @@ -26857,6 +27117,18 @@ "node": ">=14" } }, + "node_modules/langsmith/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/language-subtag-registry": { "version": "0.3.22", "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.22.tgz", @@ -29294,12 +29566,121 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, + "node_modules/msw": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/msw/-/msw-2.3.1.tgz", + "integrity": "sha512-ocgvBCLn/5l3jpl1lssIb3cniuACJLoOfZu01e3n5dbJrpA5PeeWn28jCLgQDNt6d7QT8tF2fYRzm9JoEHtiig==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "@bundled-es-modules/cookie": "^2.0.0", + "@bundled-es-modules/statuses": "^1.0.1", + "@inquirer/confirm": "^3.0.0", + "@mswjs/cookies": "^1.1.0", + "@mswjs/interceptors": "^0.29.0", + "@open-draft/until": "^2.1.0", + "@types/cookie": "^0.6.0", + "@types/statuses": "^2.0.4", + "chalk": "^4.1.2", + "graphql": "^16.8.1", + "headers-polyfill": "^4.0.2", + "is-node-process": "^1.2.0", + "outvariant": "^1.4.2", + "path-to-regexp": "^6.2.0", + "strict-event-emitter": "^0.5.1", + "type-fest": "^4.9.0", + "yargs": "^17.7.2" + }, + "bin": { + "msw": "cli/index.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/mswjs" + }, + "peerDependencies": { + "typescript": ">= 4.7.x" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/msw-storybook-addon": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/msw-storybook-addon/-/msw-storybook-addon-2.0.2.tgz", + "integrity": "sha512-sdw++X+AoUbaG2ku493ViVqCA/LfqnybXsKXyPUrF3ZS/x8BqGBnkBLmT/0SHCC5zIO3Vfm5zlclAxnhqOOikQ==", + "dev": true, + "dependencies": { + "is-node-process": "^1.0.1" + }, + "peerDependencies": { + "msw": "^2.0.0" + } + }, + "node_modules/msw/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/msw/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/msw/node_modules/outvariant": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/outvariant/-/outvariant-1.4.2.tgz", + "integrity": "sha512-Ou3dJ6bA/UJ5GVHxah4LnqDwZRwAmWxrG3wtrHrbGnP4RnLCtA64A4F+ae7Y8ww660JaddSoArUR5HjipWSHAQ==", + "dev": true + }, + "node_modules/msw/node_modules/strict-event-emitter": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/strict-event-emitter/-/strict-event-emitter-0.5.1.tgz", + "integrity": "sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==", + "dev": true + }, + "node_modules/msw/node_modules/type-fest": { + "version": "4.20.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.20.1.tgz", + "integrity": "sha512-R6wDsVsoS9xYOpy8vgeBlqpdOyzJ12HNfQhC/aAKWM3YoCV9TtunJzh/QpkMgeDhkoynDcw5f1y+qF9yc/HHyg==", + "dev": true, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/mute-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-1.0.0.tgz", "integrity": "sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==", "dev": true, - "peer": true, "engines": { "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } @@ -40628,9 +41009,9 @@ } }, "node_modules/uuid": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", - "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", "funding": [ "https://github.com/sponsors/broofa", "https://github.com/sponsors/ctavan" diff --git a/package.json b/package.json index 39b13a6..30e4f6e 100644 --- a/package.json +++ b/package.json @@ -76,6 +76,7 @@ "remark-unwrap-images": "^4.0.0", "sharp": "^0.33.3", "ts-node": "^10.9.2", + "uuid": "^10.0.0", "zod": "^3.22.4" }, "devDependencies": { @@ -139,6 +140,8 @@ "jest-environment-jsdom": "^29.7.0", "jest-fail-on-console": "^3.1.2", "lint-staged": "^15.2.2", + "msw": "^2.3.1", + "msw-storybook-addon": "^2.0.2", "npm-run-all": "^4.1.5", "postcss": "^8.4.38", "prettier": "3.3.2", @@ -152,5 +155,10 @@ }, "engines": { "node": ">=18.14.0" + }, + "msw": { + "workerDirectory": [ + "public" + ] } } diff --git a/public/mockServiceWorker.js b/public/mockServiceWorker.js new file mode 100644 index 0000000..24fe3a2 --- /dev/null +++ b/public/mockServiceWorker.js @@ -0,0 +1,284 @@ +/* eslint-disable */ +/* tslint:disable */ + +/** + * Mock Service Worker. + * @see https://github.com/mswjs/msw + * - Please do NOT modify this file. + * - Please do NOT serve this file on production. + */ + +const PACKAGE_VERSION = '2.3.1' +const INTEGRITY_CHECKSUM = '26357c79639bfa20d64c0efca2a87423' +const IS_MOCKED_RESPONSE = Symbol('isMockedResponse') +const activeClientIds = new Set() + +self.addEventListener('install', function () { + self.skipWaiting() +}) + +self.addEventListener('activate', function (event) { + event.waitUntil(self.clients.claim()) +}) + +self.addEventListener('message', async function (event) { + const clientId = event.source.id + + if (!clientId || !self.clients) { + return + } + + const client = await self.clients.get(clientId) + + if (!client) { + return + } + + const allClients = await self.clients.matchAll({ + type: 'window', + }) + + switch (event.data) { + case 'KEEPALIVE_REQUEST': { + sendToClient(client, { + type: 'KEEPALIVE_RESPONSE', + }) + break + } + + case 'INTEGRITY_CHECK_REQUEST': { + sendToClient(client, { + type: 'INTEGRITY_CHECK_RESPONSE', + payload: { + packageVersion: PACKAGE_VERSION, + checksum: INTEGRITY_CHECKSUM, + }, + }) + break + } + + case 'MOCK_ACTIVATE': { + activeClientIds.add(clientId) + + sendToClient(client, { + type: 'MOCKING_ENABLED', + payload: true, + }) + break + } + + case 'MOCK_DEACTIVATE': { + activeClientIds.delete(clientId) + break + } + + case 'CLIENT_CLOSED': { + activeClientIds.delete(clientId) + + const remainingClients = allClients.filter((client) => { + return client.id !== clientId + }) + + // Unregister itself when there are no more clients + if (remainingClients.length === 0) { + self.registration.unregister() + } + + break + } + } +}) + +self.addEventListener('fetch', function (event) { + const { request } = event + + // Bypass navigation requests. + if (request.mode === 'navigate') { + return + } + + // Opening the DevTools triggers the "only-if-cached" request + // that cannot be handled by the worker. Bypass such requests. + if (request.cache === 'only-if-cached' && request.mode !== 'same-origin') { + return + } + + // Bypass all requests when there are no active clients. + // Prevents the self-unregistered worked from handling requests + // after it's been deleted (still remains active until the next reload). + if (activeClientIds.size === 0) { + return + } + + // Generate unique request ID. + const requestId = crypto.randomUUID() + event.respondWith(handleRequest(event, requestId)) +}) + +async function handleRequest(event, requestId) { + const client = await resolveMainClient(event) + const response = await getResponse(event, client, requestId) + + // Send back the response clone for the "response:*" life-cycle events. + // Ensure MSW is active and ready to handle the message, otherwise + // this message will pend indefinitely. + if (client && activeClientIds.has(client.id)) { + ;(async function () { + const responseClone = response.clone() + + sendToClient( + client, + { + type: 'RESPONSE', + payload: { + requestId, + isMockedResponse: IS_MOCKED_RESPONSE in response, + type: responseClone.type, + status: responseClone.status, + statusText: responseClone.statusText, + body: responseClone.body, + headers: Object.fromEntries(responseClone.headers.entries()), + }, + }, + [responseClone.body], + ) + })() + } + + return response +} + +// Resolve the main client for the given event. +// Client that issues a request doesn't necessarily equal the client +// that registered the worker. It's with the latter the worker should +// communicate with during the response resolving phase. +async function resolveMainClient(event) { + const client = await self.clients.get(event.clientId) + + if (client?.frameType === 'top-level') { + return client + } + + const allClients = await self.clients.matchAll({ + type: 'window', + }) + + return allClients + .filter((client) => { + // Get only those clients that are currently visible. + return client.visibilityState === 'visible' + }) + .find((client) => { + // Find the client ID that's recorded in the + // set of clients that have registered the worker. + return activeClientIds.has(client.id) + }) +} + +async function getResponse(event, client, requestId) { + const { request } = event + + // Clone the request because it might've been already used + // (i.e. its body has been read and sent to the client). + const requestClone = request.clone() + + function passthrough() { + const headers = Object.fromEntries(requestClone.headers.entries()) + + // Remove internal MSW request header so the passthrough request + // complies with any potential CORS preflight checks on the server. + // Some servers forbid unknown request headers. + delete headers['x-msw-intention'] + + return fetch(requestClone, { headers }) + } + + // Bypass mocking when the client is not active. + if (!client) { + return passthrough() + } + + // Bypass initial page load requests (i.e. static assets). + // The absence of the immediate/parent client in the map of the active clients + // means that MSW hasn't dispatched the "MOCK_ACTIVATE" event yet + // and is not ready to handle requests. + if (!activeClientIds.has(client.id)) { + return passthrough() + } + + // Notify the client that a request has been intercepted. + const requestBuffer = await request.arrayBuffer() + const clientMessage = await sendToClient( + client, + { + type: 'REQUEST', + payload: { + id: requestId, + url: request.url, + mode: request.mode, + method: request.method, + headers: Object.fromEntries(request.headers.entries()), + cache: request.cache, + credentials: request.credentials, + destination: request.destination, + integrity: request.integrity, + redirect: request.redirect, + referrer: request.referrer, + referrerPolicy: request.referrerPolicy, + body: requestBuffer, + keepalive: request.keepalive, + }, + }, + [requestBuffer], + ) + + switch (clientMessage.type) { + case 'MOCK_RESPONSE': { + return respondWithMock(clientMessage.data) + } + + case 'PASSTHROUGH': { + return passthrough() + } + } + + return passthrough() +} + +function sendToClient(client, message, transferrables = []) { + return new Promise((resolve, reject) => { + const channel = new MessageChannel() + + channel.port1.onmessage = (event) => { + if (event.data && event.data.error) { + return reject(event.data.error) + } + + resolve(event.data) + } + + client.postMessage( + message, + [channel.port2].concat(transferrables.filter(Boolean)), + ) + }) +} + +async function respondWithMock(response) { + // Setting response status code to 0 is a no-op. + // However, when responding with a "Response.error()", the produced Response + // instance will have status code set to 0. Since it's not possible to create + // a Response instance with status code 0, handle that use-case separately. + if (response.status === 0) { + return Response.error() + } + + const mockedResponse = new Response(response.body, response) + + Reflect.defineProperty(mockedResponse, IS_MOCKED_RESPONSE, { + value: true, + enumerable: true, + }) + + return mockedResponse +} diff --git a/src/_components/Cards/Image.tsx b/src/_components/Cards/Image.tsx index 5786e1e..32e9241 100644 --- a/src/_components/Cards/Image.tsx +++ b/src/_components/Cards/Image.tsx @@ -57,7 +57,7 @@ function getAPIUrl(src: string, baseContext: ContentItem) { newSrc = `${dir}/${newSrc}`; } // ignore base paths } - url = `/api/content/github?owner=${baseContext.linked.owner}&repo=${baseContext.linked.repo}&path=${newSrc}&branch=${baseContext.linked.branch}`; + url = `/api/github/content?owner=${baseContext.linked.owner}&repo=${baseContext.linked.repo}&path=${newSrc}&branch=${baseContext.linked.branch}`; } else { if (baseContext.file) { const dir = path.dirname(baseContext.file); @@ -66,7 +66,7 @@ function getAPIUrl(src: string, baseContext: ContentItem) { newSrc = `${dir}/${newSrc}`; } // ignore base paths } - url = `/api/content/github?owner=${baseContext.owner}&repo=${baseContext.repo}&path=${newSrc}&branch=${baseContext.branch}`; + url = `/api/github/content?owner=${baseContext.owner}&repo=${baseContext.repo}&path=${newSrc}&branch=${baseContext.branch}`; } // console.debug('mdxProvider:MdxImage:src: ', src) } else if (src.slice(0, 1) === '/') { diff --git a/src/_components/Editor/NewBranchDialog.stories.tsx b/src/_components/Editor/NewBranchDialog.stories.tsx new file mode 100644 index 0000000..861eeb4 --- /dev/null +++ b/src/_components/Editor/NewBranchDialog.stories.tsx @@ -0,0 +1,33 @@ +// import { toSnakeCase } from '@/lib/utils/stringUtils'; +import type { StoryObj } from '@storybook/react'; +import { fn } from '@storybook/test'; +import React from 'react'; + +import { NewBranchDialog } from './NewBranchDialog'; + +export default { + title: 'App Bar/NewBranchDialog', + component: NewBranchDialog, + parameters: {}, + tags: ['autodocs'], + args: { + handleDialog: fn(), + }, +}; + +type Story = StoryObj; + +const Template: Story = { + render: ({ ...args }) => { + return ( + + ); + }, +}; +export const Default = { + ...Template, +}; diff --git a/src/_components/Editor/NewBranchDialog.tsx b/src/_components/Editor/NewBranchDialog.tsx new file mode 100644 index 0000000..d926d3c --- /dev/null +++ b/src/_components/Editor/NewBranchDialog.tsx @@ -0,0 +1,119 @@ +import { + ButtonGroup, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + TextField, +} from '@mui/material'; +import Alert from '@mui/material/Alert'; +import Button from '@mui/material/Button'; +import CircularProgress from '@mui/material/CircularProgress'; +import Typography from '@mui/material/Typography'; +import * as React from 'react'; +import { useState } from 'react'; + +interface NewBranchDialogProps { + dialogOpen: boolean; + handleDialog: (value: { name?: string } | null) => Promise; +} + +export const NewBranchDialog: React.FC = ({ + dialogOpen = false, + handleDialog, +}) => { + const [title, setTitle] = useState(''); + const [branchType, setBranchType] = useState('feature'); + const branchTypes: string[] = ['feature', 'bugfix', 'support']; + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + + const handleCreateNew = async () => { + setIsLoading(true); + setError(null); + try { + if (title) { + const prName = `${branchType}/${title + .split(' ') + .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) + .join('-')}`; + await handleDialog({ name: prName }); + setTitle(''); // Reset title + } else { + setError('Title is required'); + } + } catch (err: any) { + setError(err.message); + } finally { + setIsLoading(false); + } + }; + + const handleBranchTypeChange = (x: string) => { + setBranchType(x); + }; + + return ( + handleDialog(null)} + fullWidth + maxWidth='md' + > + Create new branch + + + A branch is a collection of changes to be merged into the main content + repository. Make a note of the branch name, you will need it to create + a pull request (a proposal to merge your changes into the main content + repository). + + + + Change Type + + + {branchTypes.map((branchTypeItem) => ( + + ))} + + + + Title + + setTitle(e.target.value)} + /> + + + + + + + {error && {error}} + + ); +}; diff --git a/src/_components/Editor/NewContentDialog.stories.tsx b/src/_components/Editor/NewContentDialog.stories.tsx new file mode 100644 index 0000000..c70844d --- /dev/null +++ b/src/_components/Editor/NewContentDialog.stories.tsx @@ -0,0 +1,97 @@ +import type { StoryObj } from '@storybook/react'; +import { fn } from '@storybook/test'; +import { delay, http, HttpResponse } from 'msw'; +import React from 'react'; + +import { NewContentDialog } from './NewContentDialog'; + +// More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export +export default { + title: 'App Bar/NewContentDialog', + component: NewContentDialog, + parameters: { + // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout + // layout: 'centered', + }, + // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs + tags: ['autodocs'], + // More on argTypes: https://storybook.js.org/docs/api/argtypes + argTypes: { + handleAdd: { action: 'clicked' }, + // backgroundColor: { control: 'color' }, + }, + args: { + handleDialog: fn(), + }, +}; + +type Story = StoryObj; + +const Template: Story = { + render: ({ ...args }) => { + return ( + + ); + }, +}; +export const Default = { + ...Template, +}; + +const TestData = { + docs: [ + { + file: '/path/to/document1.md', + frontmatter: { + title: 'Document Title 1', + }, + }, + { + file: '/path/to/document2.md', + frontmatter: { + title: 'Document Title 2', + }, + }, + { + file: '/path/to/document3.md', + frontmatter: { + title: 'Document Title 3', + }, + }, + // Add more documents as needed + ], +}; + +export const MockedSuccess = { + ...Template, + parameters: { + msw: { + handlers: [ + http.get('/api/structure', async () => { + await delay(800); + return HttpResponse.json(TestData); + }), + ], + }, + }, +}; + +export const MockedError = { + ...Template, + parameters: { + msw: { + handlers: [ + http.get('/api/structure', async () => { + await delay(800); + return new HttpResponse(null, { + status: 403, + }); + }), + ], + }, + }, +}; diff --git a/src/_components/Editor/NewContentDialog.tsx b/src/_components/Editor/NewContentDialog.tsx new file mode 100644 index 0000000..1438cfc --- /dev/null +++ b/src/_components/Editor/NewContentDialog.tsx @@ -0,0 +1,294 @@ +import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown'; +import { + ButtonGroup, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + FormControl, + MenuItem, + Select, + TextField, +} from '@mui/material'; +import Alert from '@mui/material/Alert'; +import Button from '@mui/material/Button'; +import CircularProgress from '@mui/material/CircularProgress'; +import Typography from '@mui/material/Typography'; +import { dirname } from 'path'; +import * as React from 'react'; +import { useState } from 'react'; + +// import * as matter from "gray-matter"; +import { siteConfig } from '@/config'; +import { getLogger } from '@/lib/Logger'; + +const logger = getLogger().child({ namespace: 'NewContentDialog' }); +logger.level = 'info'; + +// Custom Dropdown Indicator defined outside +const DropdownIndicator = ({ isLoadingItems }: { isLoadingItems: boolean }) => + isLoadingItems ? ( + + ) : ( + + ); + +export function NewContentDialog({ + dialogOpen = false, + handleDialog, + initialDropDownData = [], +}: { + dialogOpen: boolean; + handleDialog: (value: { frontmatter: any } | null) => Promise; + initialDropDownData?: any[]; +}) { + const [dropDownData, setDropDownData] = useState(initialDropDownData); + const [selectedDropDown, setSelectedDropDown] = useState(''); + const [docType, setDocType] = useState(''); + + const [parent, setParent] = useState('None'); + + const [availableParents, setAvailableParents] = useState(['None']); + const [title, setTitle] = useState(''); + const [isLoading, setIsLoading] = useState(false); + const [isLoadingItems, setIsLoadingItems] = useState(false); + + const [error, setError] = useState(null); + + const docTypes = Object.entries(siteConfig.content) + .map(([_, item]) => { + // Now TypeScript understands `item` is a `ContentItem` or undefined + if (item && item.reference && item.path) { + return { + label: item.reference, + prefix: item.path.split('/').pop(), // Extract the last segment of the path as the prefix + }; + } + return null; // Exclude the item from the list + }) + .filter( + (entry): entry is { label: string; prefix: string } => entry !== null + ); + + const getParentContentElements = (): { [key: string]: string[] } => { + const parentContentElements: { [key: string]: string[] } = {}; + + Object.entries(siteConfig.content).forEach(([contentType, content]) => { + content?.collections?.forEach((collection) => { + const entries = parentContentElements[collection] || ['None']; + entries.push(contentType); + parentContentElements[collection] = entries; + }); + }); + return parentContentElements; + }; + + const parentReference = () => { + for (const [key, content] of Object.entries(siteConfig.content)) { + if (key === parent) { + return content.reference; + } + } + return null; + }; + + const parentContentElements = getParentContentElements(); + + // console.log('parentContentElements: ', parentContentElements); + // // console.log(docTypes); + // const docTypes = [{ label: 'Solution', prefix: 'solutions' }, { label: 'Design', prefix: 'designs' }, { label: 'Service', prefix: 'services' }, { label: 'Provider', prefix: 'providers' }, { label: 'Knowledge', prefix: 'knowledge' }]; + + const handleCreateNew = async () => { + // console.log('create new pad: ', title, ' / ', selectedDropDown, ' / ', parent); + // const pad = uuidv4(); // Generate a unique padID + + const parentRef = parentReference(); + // console.log('parentRef: ', parentRef); + + let frontmatter; + if (!parentRef) { + // define the object for when parent === 'None' + frontmatter = { + type: docType, + title, + }; + } else { + // define the object for when parent !== 'None' + frontmatter = { + type: docType, + [parentRef.toLowerCase()]: dirname(selectedDropDown), + title, + }; + } + setIsLoading(true); + setError(null); + try { + await handleDialog({ frontmatter }); + } catch (err: any) { + setError(err.message); + } finally { + setIsLoading(false); + } + }; + + const handleParentChange = async (newParent: string) => { + setIsLoadingItems(true); + setParent(newParent); + setSelectedDropDown(''); + fetch(`/api/structure?collection=${newParent}`) + .then((res) => res.json()) + .then((data) => { + // const values = data.docs.map((item) => item.frontmatter.title) ?? []; + const values = data.docs.map( + ({ file, frontmatter }: { file: string; frontmatter: any }) => ({ + label: frontmatter.title, + url: file, + }) + ); + + // console.log('NewContentDialog:handleParentChange:values: ', values); + + setDropDownData( + values.sort((a: any, b: any) => { + // Assuming that some objects might not have a 'label' property + const labelA = a.label || ''; + const labelB = b.label || ''; + + if (labelA < labelB) { + return -1; // a comes first + } + if (labelA > labelB) { + return 1; // b comes first + } + return 0; // no change in order + }) + ); + setIsLoadingItems(false); + }) + .catch((err: any) => { + logger.error(err); + setIsLoadingItems(false); + }); + }; + + const handleDocTypeChange = async (x: string) => { + setAvailableParents(parentContentElements[x] ?? ['None']); + setDocType(x); + setSelectedDropDown(''); + }; + + const handleDropDownChange = (event: any) => { + logger.info('handleDropDownChange: ', event.target.value); + setSelectedDropDown(event.target.value); + }; + + return ( + handleDialog(null)} + fullWidth + maxWidth='md' + > + Create New + + {/* Title Input */} + + Title + + setTitle(e.target.value)} + /> + {/* Parent Buttons */} + + Document Type + + + {docTypes.map((docTypeItem) => ( + + ))} + + + {/* Document Type Buttons */} + + Select Parent + + + {availableParents.map((parentOption) => ( + + ))} + + + {/* Dropdown for selected parent */} + + Select Item + + + + + + + + + + + {error && {error}} + + ); +} diff --git a/src/_components/Editor/index.ts b/src/_components/Editor/index.ts index d1c9b8b..afa42ea 100644 --- a/src/_components/Editor/index.ts +++ b/src/_components/Editor/index.ts @@ -1,3 +1,5 @@ import { Editor } from './Editor'; +import { NewBranchDialog } from './NewBranchDialog'; +import { NewContentDialog } from './NewContentDialog'; -export { Editor }; +export { Editor, NewBranchDialog, NewContentDialog }; diff --git a/src/_components/Editor/lib/functions.ts b/src/_components/Editor/lib/functions.ts index e545154..526e86d 100644 --- a/src/_components/Editor/lib/functions.ts +++ b/src/_components/Editor/lib/functions.ts @@ -1,37 +1,65 @@ import path from 'path'; +import { getLogger } from '@/lib/Logger'; import type { ContentItem } from '@/lib/Types'; -export async function createFile( - owner: string, - repo: string, - branch: string, - file: string, - fileName: string, - content: string, - message: string -) { - // use in pages - - // const file = path.basename(path); - - // console.debug( - // 'Editor:createFile: ', - // owner, - // repo, - // branch, - // file, - // fileName, - // content, - // message, - // ); - const filePath = file.replace(/^\/|^\.\//, ''); +const logger = getLogger().child({ namespace: 'Editor/functions' }); +logger.level = 'info'; + +export async function createNewBranch({ + owner, + repo, + branch, + sourceBranch, +}: { + owner: string; + repo: string; + branch: string; + sourceBranch: string; +}) { + const response = await fetch('/api/github/branch', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + owner, + repo, + branch, + sourceBranch, + }), + }); + + if (!response.ok) { + const data = await response.json(); + logger.info('ControlBar:createNewBranch:response: ', data); + throw new Error(`${data.error}`); + } + + const data = await response.json(); + return data; +} - // return fileName; +export async function createFile({ + owner, + repo, + branch, + file, + content, + message, +}: { + owner: string; + repo: string; + branch: string; + file: string; + content: string; + message: string; +}) { + const filePath = file.replace(/^\/|^\.\//, ''); try { const response = await fetch( - `/api/content/github/${owner}/${repo}?branch=${branch}&path=${filePath}`, + `/api/github/content/${owner}/${repo}?branch=${branch}&path=${filePath}`, { method: 'POST', headers: { @@ -44,9 +72,9 @@ export async function createFile( if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } - await response.json(); + const result = await response.json(); // console.log('Editor:createFile:Commit successful:', data); - return fileName; + return result; } catch (e: any) { // console.error('Editor:createFile:Error committing file:', e.message); return null; @@ -69,7 +97,7 @@ export async function imagePreviewHandler( // console.log('Editor:imagePreviewHandler:filePath: ', filePath); const response = await fetch( - `/api/content/github/${context.owner}/${context.repo}?branch=${context.branch}&path=${filePath}` + `/api/github/content/${context.owner}/${context.repo}?branch=${context.branch}&path=${filePath}` ); // console.log('Editor:imagePreviewHandler:response: ', response); if (!response.ok) { @@ -95,7 +123,7 @@ export async function imageUploadHandler( const file = `${path.dirname(context.file || '')}/${image.name .replace(/[^a-zA-Z0-9.]/g, '') .toLowerCase()}`; - const fileName = image.name.replace(/[^a-zA-Z0-9.]/g, '').toLowerCase(); + // const fileName = image.name.replace(/[^a-zA-Z0-9.]/g, '').toLowerCase(); return new Promise((resolve, reject) => { const reader = new FileReader(); @@ -106,15 +134,14 @@ export async function imageUploadHandler( const imageData = typeof base64Image === 'string' ? base64Image.split(',')[1] : ''; try { - const url = await createFile( - context.owner, - context.repo, - context.branch, + const url = await createFile({ + owner: context.owner, + repo: context.repo, + branch: context.branch, file, - fileName, - imageData ?? '', - 'Image uploaded from Airview' - ); + content: imageData ?? '', + message: 'Image uploaded from Airview', + }); if (url) { resolve(url); } else { @@ -130,3 +157,40 @@ export async function imageUploadHandler( reader.readAsDataURL(image); }); } + +export const raisePR = async ({ + owner, + repo, + title, + message, + head, + base, +}: { + owner: string; + repo: string; + title: string; + message: string; + head: string; + base: string; +}) => { + try { + const response = await fetch('/api/repo/pr', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ owner, repo, title, message, head, base }), + }); + + const data = await response.json(); + // console.log('lib/github/raisePR:response: ', data) + if (!response.ok) { + throw Error(data.error || 'Network response was not ok'); + } + + return data; + } catch (error: any) { + // console.error('There has been a problem with your fetch operation:', error); + throw Error(error.message); + } +}; diff --git a/src/_components/Layouts/ControlBar.stories.tsx b/src/_components/Layouts/ControlBar.stories.tsx index 202beb0..af9e5b4 100644 --- a/src/_components/Layouts/ControlBar.stories.tsx +++ b/src/_components/Layouts/ControlBar.stories.tsx @@ -1,6 +1,6 @@ import type { Meta, StoryFn } from '@storybook/react'; import { fn } from '@storybook/test'; -import React, { useState } from 'react'; +import React from 'react'; import type { ContentItem } from '@/lib/Types'; @@ -29,7 +29,7 @@ export default { args: { handleRefresh: fn(), handlePrint: fn(), - handleAdd: fn(), + handleAddContent: fn(), handlePresentation: fn(), }, } as Meta; @@ -110,7 +110,7 @@ export const Simple = { handleEdit: fn(), handleRefresh: fn(), handlePrint: fn(), - handleAdd: fn(), + handleAddContent: fn(), onContextUpdate: fn(), handlePresentation: fn(), collection: dummyCollection, @@ -128,7 +128,7 @@ export const EditMode = { handleEdit: fn(), handleRefresh: fn(), handlePrint: fn(), - handleAdd: fn(), + handleAddContent: fn(), onContextUpdate: fn(), handlePresentation: fn(), collection: dummyCollection, @@ -160,23 +160,11 @@ export const DefaultBranch = { const Template: StoryFn = (args) => { // const [collection, setCollection] = useState(dummyCollection); - const [editMode, setEditMode] = useState(args.editMode || false); const context = { ...args.context, branch: 'main', }; - // function fn()() {} - - const handleEdit = (edit: boolean) => { - setEditMode(edit); - fn(); - }; - - // function onContextUpdate(newCollection: ExtendedContentItem) { - // setContext(newCollection); - // } - function dummyDelay() { fn(); // const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); @@ -195,17 +183,12 @@ const Template: StoryFn = (args) => { fn()} - handlePresentation={() => fn()} - handlePrint={() => fn()} handleNewBranch={() => fn()} // onContextUpdate={() => onContextUpdate} collection={args.collection} context={context} branches={args.branches} - editMode={editMode} - fetchBranches={fn()} + editMode handlePR={() => dummyDelay()} /> ); diff --git a/src/_components/Layouts/ControlBar.tsx b/src/_components/Layouts/ControlBar.tsx index 6c5a11f..f2ca22f 100644 --- a/src/_components/Layouts/ControlBar.tsx +++ b/src/_components/Layouts/ControlBar.tsx @@ -1,7 +1,5 @@ import AddCircleIcon from '@mui/icons-material/AddCircle'; import ApprovalIcon from '@mui/icons-material/Approval'; -import PrintIcon from '@mui/icons-material/Print'; -import SlideshowIcon from '@mui/icons-material/Slideshow'; import { AppBar, Autocomplete, @@ -26,17 +24,17 @@ logger.level = 'info'; export interface ControlBarProps { open: boolean; // height: number; - handleEdit?: (editMode: boolean) => void; - handlePrint?: () => void; - handleAdd?: () => void; - handlePresentation?: () => void; + // handleEdit?: (editMode: boolean) => void; + // handlePrint?: () => void; + handleAddContent?: () => void; + // handlePresentation?: () => void; collection: any; context: any; branches: any[]; top?: number; // onContextUpdate: (context: any) => void; editMode: boolean; - fetchBranches?: (collection: any) => void; + // fetchBranches?: (collection: any) => void; handleNewBranch?: () => void; handlePR?: () => void; } @@ -63,10 +61,10 @@ const BranchSelector: React.FC = ({ const router = useRouter(); const pathname = usePathname(); - const onBranchChange = (value: any) => { - logger.info('handleContextUpdate', value); + const onBranchChange = (event: any, value: string) => { + logger.info('handleContextUpdate', { value, event }); const pathnameArray = pathname.split('/'); - pathnameArray[3] = value; + pathnameArray[3] = encodeURIComponent(value); const newPathname = pathnameArray.join('/'); router.push(newPathname); }; @@ -95,17 +93,17 @@ const BranchSelector: React.FC = ({ export const ControlBar: React.FC = ({ open, // height, - handleEdit, - handlePrint, - handleAdd, - handlePresentation, + // handleEdit, + // handlePrint, + handleAddContent, + // handlePresentation, collection, context, branches, top = '64px', // onContextUpdate, editMode, - fetchBranches = () => {}, + // fetchBranches = () => {}, handleNewBranch = () => {}, handlePR = () => {}, }) => { @@ -114,7 +112,8 @@ export const ControlBar: React.FC = ({ const [showError, setShowError] = useState(''); const [showPRSuccess, setShowPRSuccess] = useState(false); const [branch, setBranch] = useState(context.branch); - + const router = useRouter(); + const pathname = usePathname(); useEffect(() => { if (context.branch !== collection.branch) { setBranch(context.branch); @@ -126,16 +125,18 @@ export const ControlBar: React.FC = ({ if (changeBranch) { // const newCollection = { ...collection }; setBranch(collection.branch); + const pathnameArray = pathname.split('/'); + pathnameArray[3] = encodeURIComponent(collection.branch); + const newPathname = pathnameArray.join('/'); + router.push(newPathname); // onContextUpdate(newCollection); - if (typeof window !== 'undefined') { - const url = new URL(window.location.href); - if (url.searchParams.has('branch')) { - url.searchParams.delete('branch'); - } - window.history.replaceState({}, document.title, url); - } - } else { - fetchBranches(collection); + // if (typeof window !== 'undefined') { + // const url = new URL(window.location.href); + // if (url.searchParams.has('branch')) { + // url.searchParams.delete('branch'); + // } + // window.history.replaceState({}, document.title, url); + // } } setChangeBranch(!changeBranch); if (state === 'open') { @@ -153,21 +154,21 @@ export const ControlBar: React.FC = ({ // } // }; - const handlePresentationClick = () => { - if (typeof handlePresentation === 'function') { - handlePresentation(); - } - }; + // const handlePresentationClick = () => { + // if (typeof handlePresentation === 'function') { + // handlePresentation(); + // } + // }; - const handlePrintClick = () => { - if (typeof handlePrint === 'function') { - handlePrint(); - } - }; + // const handlePrintClick = () => { + // if (typeof handlePrint === 'function') { + // handlePrint(); + // } + // }; const handleAddClick = () => { - if (typeof handleAdd === 'function') { - handleAdd(); + if (typeof handleAddContent === 'function') { + handleAddContent(); } }; @@ -192,13 +193,13 @@ export const ControlBar: React.FC = ({ }; const onEditClick = () => { - if (typeof handleEdit === 'function') { - handleEdit(!editMode); - if (!editMode) { - fetchBranches(collection); - setChangeBranch(true); - } - } + // if (typeof handleEdit === 'function') { + // handleEdit(!editMode); + // if (!editMode) { + // fetchBranches(collection); + // setChangeBranch(true); + // } + // } }; return ( @@ -316,34 +317,6 @@ export const ControlBar: React.FC = ({ label='Add Content' /> )} - {handlePrint && !editMode && ( - handlePrintClick()} - color='primary' - > - - - } - label='Print' - /> - )} - {handlePresentation && !editMode && ( - handlePresentationClick()} - color='primary' - > - - - } - label='View Presentation' - /> - )} diff --git a/src/_components/Layouts/IndexTiles.tsx b/src/_components/Layouts/IndexTiles.tsx index d2c0a6e..cbbd667 100644 --- a/src/_components/Layouts/IndexTiles.tsx +++ b/src/_components/Layouts/IndexTiles.tsx @@ -102,11 +102,11 @@ export default async function IndexTiles({ isHero={c?.frontmatter?.hero} image={ c?.frontmatter?.hero && c?.frontmatter?.image != null - ? `/api/content/github?owner=${initialContext.owner}&repo=${initialContext.repo}&path=${path.dirname(c.file.path)}/${c.frontmatter.image}&branch=${initialContext.branch}` + ? `/api/github/content?owner=${initialContext.owner}&repo=${initialContext.repo}&path=${path.dirname(c.file.path)}/${c.frontmatter.image}&branch=${initialContext.branch}` : c?.frontmatter?.hero ? '/generic-solution.png' : c?.frontmatter?.image - ? `/api/content/github?owner=${initialContext.owner}&repo=${initialContext.repo}&path=${path.dirname(c.file.path)}/${c.frontmatter.image}&branch=${initialContext.branch}` + ? `/api/github/content?owner=${initialContext.owner}&repo=${initialContext.repo}&path=${path.dirname(c.file.path)}/${c.frontmatter.image}&branch=${initialContext.branch}` : undefined } /> diff --git a/src/_features/Mdx/EditorWrapper.tsx b/src/_features/Mdx/EditorWrapper.tsx index 9c3fcba..95a88b4 100644 --- a/src/_features/Mdx/EditorWrapper.tsx +++ b/src/_features/Mdx/EditorWrapper.tsx @@ -3,11 +3,20 @@ import { type MDXEditorMethods } from '@mdxeditor/editor'; import { Box, LinearProgress } from '@mui/material'; import Container from '@mui/material/Container'; +import matter from 'gray-matter'; +import { usePathname, useRouter } from 'next/navigation'; import React, { useEffect, useRef, useState } from 'react'; -import { Editor } from '@/components/Editor'; +import { Editor, NewBranchDialog, NewContentDialog } from '@/components/Editor'; +import { + createFile, + createNewBranch, + raisePR, +} from '@/components/Editor/lib/functions'; import { ControlBar } from '@/components/Layouts/ControlBar'; +// import { raisePR } from '@/lib/Github'; import { getLogger } from '@/lib/Logger'; +import { toSnakeCase } from '@/lib/StringUtils'; import type { ContentItem } from '@/lib/Types'; const logger = getLogger().child({ namespace: 'EditorWrapper' }); @@ -20,17 +29,21 @@ interface EditorWrapperProps { } export default function EditorWrapper({ - defaultContext, + defaultContext = undefined, context, branches, }: EditorWrapperProps) { const editorRef = useRef(null); const searchParams = `owner=${context.owner}&repo=${context.repo}&path=${context.file}&branch=${context.branch}`; const [mdx, setMdx] = useState(''); + const [isAddOpen, setIsAddOpen] = useState(false); + const [isNewBranchOpen, setIsNewBranchOpen] = useState(false); + const router = useRouter(); + const pathname = usePathname(); useEffect(() => { const fetchData = async () => { - const response = await fetch(`/api/content/github?${searchParams}`); + const response = await fetch(`/api/github/content?${searchParams}`); const mdxResponse = await response.text(); setMdx(mdxResponse); }; @@ -38,15 +51,123 @@ export default function EditorWrapper({ fetchData(); }, [context, searchParams]); - // const router = useRouter(); - // const pathname = usePathname(); - // const handleContextUpdate = (newContext: any) => { - // logger.info('handleContextUpdate', newContext); - // const pathnameArray = pathname.split('/'); - // pathnameArray[2] = newContext.branch; - // const newPathname = pathnameArray.join('/'); - // router.push(newPathname); - // }; + const onNewBranchClicked = () => { + setIsNewBranchOpen(true); + }; + + const handleNewBranch = async (value: { name?: string } | null) => { + if (!value) { + // handle the cancel button + setIsNewBranchOpen(false); + return; + } + if (value && value.name) { + logger.debug('ControlBar:handleNewBranch: ', value); + try { + await createNewBranch({ + owner: context.owner, + repo: context.repo, + branch: value.name, + sourceBranch: defaultContext?.branch || 'main', + }); + setIsNewBranchOpen(false); + const pathnameArray = pathname.split('/'); + pathnameArray[3] = encodeURIComponent(value.name); + const newPathname = pathnameArray.join('/'); + router.push(newPathname); + + // if (typeof window !== 'undefined') { + // const url = new URL(window.location.href); + // const params = new URLSearchParams(url.search); + // params.set('branch', value.name); + // params.set('edit', 'true'); + // url.search = params.toString(); + // window.location.href = url.toString(); + // } + } catch (e: any) { + throw new Error(`Error creating branch: ${e.message}`); + } + } + }; + const onAddContentClicked = () => { + setIsAddOpen(true); + }; + const handleAdd = async (newFile: any) => { + if (!newFile) { + // handle the cancel button + setIsAddOpen(false); + return; + } + const newContent: { frontmatter: any; path: string } = { + frontmatter: undefined, + path: '', + }; + if ( + newFile.frontmatter && + newFile.frontmatter.title && + newFile.frontmatter.type + ) { + try { + newContent.frontmatter = newFile.frontmatter; + newContent.path = `${newFile.frontmatter.type}/${toSnakeCase( + newFile.frontmatter.title + )}/_index.mdx`; + createFile({ + owner: context.owner, + repo: context.repo, + branch: context.branch, + file: newContent.path, + content: matter.stringify('\n', newContent.frontmatter), + message: 'New file created from Airview', + }); + logger.debug( + 'ControlBar:handleAdd: ', + context.owner, + context.repo, + context.branch, + newContent.path, + matter.stringify('\n', newContent.frontmatter), + 'New file created from Airview' + ); + } catch (e: any) { + throw new Error(`Error creating file: ${e.message}`); + } + } + + setIsAddOpen(false); + }; + + const handlePR = async () => { + const prName = (branch: string) => { + const [branchType, ...titleParts] = branch.split('/'); + const title = titleParts.join(' ').replace(/([a-z])([A-Z])/g, '$1 $2'); + return `${ + (branchType ?? '').charAt(0).toUpperCase() + (branchType ?? '').slice(1) + }: ${title + .split('-') + .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) + .join(' ')}`; + }; + logger.debug( + context.owner, + context.repo, + prName(context.branch), + 'PR created from Airview', + context.branch + ); + try { + await raisePR({ + owner: context.owner, + repo: context.repo, + title: prName(context.branch), + message: 'PR created from Airview', + head: context.branch, + base: defaultContext?.branch || 'main', + }); + } catch (e: any) { + throw new Error(`Error creating PR: ${e.message}`); + } + }; return ( <> @@ -54,19 +175,23 @@ export default function EditorWrapper({ branches={branches} collection={defaultContext} context={context} - fetchBranches={() => {}} - handleAdd={() => {}} - handleEdit={() => {}} - handleNewBranch={() => {}} - handlePR={() => {}} - handlePresentation={() => {}} - handlePrint={() => {}} + handleAddContent={onAddContentClicked} + // handleEdit={() => {}} + handleNewBranch={onNewBranchClicked} + handlePR={handlePR} + // handlePresentation={() => {}} + // handlePrint={() => {}} // handleRefresh={() => {}} // onContextUpdate={handleContextUpdate} open editMode top={65} /> + + {mdx ? ( diff --git a/src/app/api/github/branch/route.ts b/src/app/api/github/branch/route.ts new file mode 100644 index 0000000..1d527c4 --- /dev/null +++ b/src/app/api/github/branch/route.ts @@ -0,0 +1,43 @@ +import type { NextRequest } from 'next/server'; +import { NextResponse } from 'next/server'; + +import { createBranch } from '@/lib/Github'; +import { getLogger } from '@/lib/Logger'; + +const logger = getLogger().child({ namespace: 'API:/api/github/content' }); +logger.level = 'error'; + +export async function POST(req: NextRequest): Promise { + try { + const body: Record = await req.json(); + + const { owner, repo, branch, sourceBranch } = body; + + if (!owner || !repo || !branch || !sourceBranch) { + return NextResponse.json( + { + error: + 'Missing required parameters: owner, repo, branch or sourceBranch', + }, + { status: 400 } + ); + } + + const response = await createBranch(owner, repo, branch, sourceBranch); + if (response) { + return NextResponse.json( + response, + { status: 201, statusText: 'OK' } // 201 status code for resource creation + ); + } + return NextResponse.json( + { error: 'no response from Github API' }, + { status: 400 } + ); + } catch (err) { + return NextResponse.json( + { error: `Error in API: ${err}` }, + { status: 500 } + ); + } +} diff --git a/src/app/api/content/github/route.ts b/src/app/api/github/content/route.ts similarity index 91% rename from src/app/api/content/github/route.ts rename to src/app/api/github/content/route.ts index 0237eda..cf67857 100644 --- a/src/app/api/content/github/route.ts +++ b/src/app/api/github/content/route.ts @@ -6,7 +6,7 @@ import path from 'path'; import { commitFileToBranch, getFileContent } from '@/lib/Github'; import { getLogger } from '@/lib/Logger'; -const logger = getLogger().child({ namespace: 'API:/api/content/github' }); +const logger = getLogger().child({ namespace: 'API:/api/github/content' }); logger.level = 'error'; // export const config = { // api: { @@ -16,7 +16,7 @@ logger.level = 'error'; export async function GET(req: NextRequest) { // logger.info( - // `[GET /api/content/github][query]: ${util.inspect(Object.fromEntries(req.nextUrl.searchParams))}`, + // `[GET /api/github/content][query]: ${util.inspect(Object.fromEntries(req.nextUrl.searchParams))}`, // ); try { const { @@ -26,7 +26,7 @@ export async function GET(req: NextRequest) { path: filepath, } = Object.fromEntries(req.nextUrl.searchParams); // logger.info( - // `[GET /api/content/github][query]: branch:${branch}, path:${filepath}, owner:${owner}, repo:${repo}`, + // `[GET /api/github/content][query]: branch:${branch}, path:${filepath}, owner:${owner}, repo:${repo}`, // ); if (!owner || !repo || typeof filepath !== 'string') { return NextResponse.json( @@ -45,7 +45,7 @@ export async function GET(req: NextRequest) { const data = await getFileContent({ owner, repo, branch, path: filepath }); const extension = path.extname(filepath); const contentType = mime.lookup(extension) || 'application/octet-stream'; - // logger.info(`[GET /api/content/github][data]: ${util.inspect(data)}`); + // logger.info(`[GET /api/github/content][data]: ${util.inspect(data)}`); const headers = new Headers(); headers.set('Content-Type', contentType); diff --git a/src/app/api/github/pr/route.ts b/src/app/api/github/pr/route.ts new file mode 100644 index 0000000..4dfdc60 --- /dev/null +++ b/src/app/api/github/pr/route.ts @@ -0,0 +1,43 @@ +import type { NextRequest } from 'next/server'; +import { NextResponse } from 'next/server'; + +import { createPR } from '@/lib/Github'; +import { getLogger } from '@/lib/Logger'; + +const logger = getLogger().child({ namespace: 'API:/api/github/pr' }); +logger.level = 'error'; + +export async function POST(req: NextRequest): Promise { + try { + const postBody: Record = await req.json(); + + const { owner, repo, title, body, head, base } = postBody; + + if (!owner || !repo || !title || !body || !head || !base) { + return NextResponse.json( + { + error: + 'Missing required parameters: owner, repo, title, body, head or base', + }, + { status: 400 } + ); + } + + const response = await createPR(owner, repo, title, body, head, base); + if (response) { + return NextResponse.json( + response, + { status: 201, statusText: 'OK' } // 201 status code for resource creation + ); + } + return NextResponse.json( + { error: 'no response from Github API' }, + { status: 400 } + ); + } catch (err) { + return NextResponse.json( + { error: `Error in API: ${err}` }, + { status: 500 } + ); + } +} diff --git a/src/app/docs/[mode]/[branch]/[[...path]]/@edit/page.tsx b/src/app/docs/[mode]/[branch]/[[...path]]/@edit/page.tsx index e93a9ae..a84221f 100644 --- a/src/app/docs/[mode]/[branch]/[[...path]]/@edit/page.tsx +++ b/src/app/docs/[mode]/[branch]/[[...path]]/@edit/page.tsx @@ -43,7 +43,7 @@ export default async function Page({ const branch = () => params.branch === 'default' ? siteConfig?.content?.[contentKey]?.branch - : params.branch; + : decodeURIComponent(params.branch); const contentConfig = { ...siteConfig?.content?.[contentKey], file: file, diff --git a/src/app/docs/[mode]/[branch]/[[...path]]/@index/page.tsx b/src/app/docs/[mode]/[branch]/[[...path]]/@index/page.tsx index 9e7e062..1b41b9a 100644 --- a/src/app/docs/[mode]/[branch]/[[...path]]/@index/page.tsx +++ b/src/app/docs/[mode]/[branch]/[[...path]]/@index/page.tsx @@ -1,85 +1,95 @@ - // import type { Metadata } from 'next' -import { Metadata } from 'next' +import { Metadata } from 'next'; -import React from "react"; -import { IndexTiles, MenuWrapper } from "@/components/Layouts"; +import React from 'react'; +import { IndexTiles, MenuWrapper } from '@/components/Layouts'; import { siteConfig } from '../../../../../../../site.config'; -import { notFound } from "next/navigation"; +import { notFound } from 'next/navigation'; import { getLogger } from '@/lib/Logger'; import type { ContentItem } from '@/lib/Types'; import { loadMenu, nestMenu } from '@/lib/Content/loadMenu'; const logger = getLogger().child({ namespace: 'docs/page' }); logger.level = 'error'; -export async function generateMetadata( - { params }: {params: { mode: 'view' | 'edit' | 'print', branch: string, path: string[] }; +export async function generateMetadata({ + params, +}: { + params: { mode: 'view' | 'edit' | 'print'; branch: string; path: string[] }; }): Promise { // read route params if ( params.path[0] && siteConfig.content[params.path[0] as keyof typeof siteConfig.content] - ) { - - const file = params.path.join("/") as string; + ) { + const file = params.path.join('/') as string; const contentKey = params.path[0] as keyof typeof siteConfig.content; const contentConfig = { ...siteConfig?.content?.[contentKey], file: file, } as ContentItem; return { - title: `${contentConfig?.path?.charAt(0).toUpperCase()}${contentConfig?.path?.slice(1)}` - } + title: `${contentConfig?.path?.charAt(0).toUpperCase()}${contentConfig?.path?.slice(1)}`, + }; } else { return { - title: 'Not Found' - } + title: 'Not Found', + }; } } export default async function Page({ params, }: { - params: { mode: 'view' | 'edit' | 'print', branch: string, path: string[] } + params: { mode: 'view' | 'edit' | 'print'; branch: string; path: string[] }; }) { if ( params.path[0] && siteConfig.content[params.path[0] as keyof typeof siteConfig.content] - ) { - - const file = params.path.join("/") as string; + ) { + const file = params.path.join('/') as string; let loading = false; const contentKey = params.path[0] as keyof typeof siteConfig.content; // if params.branch is default return the branch from siteConfig, else return the params.branch - const branch = () => params.branch === 'default' ? siteConfig?.content?.[contentKey]?.branch : params.branch; + const branch = () => + params.branch === 'default' + ? siteConfig?.content?.[contentKey]?.branch + : decodeURIComponent(params.branch); const contentConfig = { ...siteConfig?.content?.[contentKey], file: file, branch: branch(), } as ContentItem; - const menuConfig = (contentConfig : ContentItem) => { - if (contentConfig.menu && contentConfig.menu.collection ) { - return siteConfig?.content?.[contentConfig?.menu?.collection as keyof typeof siteConfig.content] || contentConfig as ContentItem; + const menuConfig = (contentConfig: ContentItem) => { + if (contentConfig.menu && contentConfig.menu.collection) { + return ( + siteConfig?.content?.[ + contentConfig?.menu?.collection as keyof typeof siteConfig.content + ] || (contentConfig as ContentItem) + ); } else { return contentConfig; } - } - - const content = await loadMenu(siteConfig, menuConfig(contentConfig)); - const { menu: menuStructure } = nestMenu(content, 'docs'); - logger.debug({ msg: 'menuStructure: ', menuStructure}); - return ( -
- - { contentConfig ? : <> } - -
- ) + }; + const content = await loadMenu(siteConfig, menuConfig(contentConfig)); + const { menu: menuStructure } = nestMenu(content, 'docs'); + logger.debug({ msg: 'menuStructure: ', menuStructure }); + return ( +
+ + {contentConfig ? ( + + ) : ( + <> + )} + +
+ ); } else { return notFound(); } diff --git a/src/app/docs/[mode]/[branch]/[[...path]]/@view/page.tsx b/src/app/docs/[mode]/[branch]/[[...path]]/@view/page.tsx index a65ef53..7faaa6a 100644 --- a/src/app/docs/[mode]/[branch]/[[...path]]/@view/page.tsx +++ b/src/app/docs/[mode]/[branch]/[[...path]]/@view/page.tsx @@ -92,7 +92,7 @@ export default async function Page({ const branch = () => params.branch === 'default' ? siteConfig?.content?.[contentKey]?.branch - : params.branch; + : decodeURIComponent(params.branch); const contentConfig = { ...siteConfig?.content?.[contentKey], file: file, diff --git a/src/lib/Github.ts b/src/lib/Github.ts index aef12a8..fc22f74 100644 --- a/src/lib/Github.ts +++ b/src/lib/Github.ts @@ -926,7 +926,7 @@ export async function commitFileChanges( // use in pages try { const response = await fetch( - `/api/content/github/${owner}/${repo}?branch=${branch}&path=${path}`, + `/api/github/content/${owner}/${repo}?branch=${branch}&path=${path}`, { method: 'POST', headers: { diff --git a/src/lib/StringUtils.ts b/src/lib/StringUtils.ts new file mode 100644 index 0000000..12aab0f --- /dev/null +++ b/src/lib/StringUtils.ts @@ -0,0 +1,14 @@ +// utils/stringUtils.ts + +/** + * Converts a string to snake_case. + * @param {string} str - The string to convert. + * @return {string} The converted snake_case string. + */ +export function toSnakeCase(str: string) { + return str + .replace(/([A-Z])/g, ' $1') // Insert space before capital letters + .trim() // Trim leading and trailing spaces + .toLowerCase() // Convert to lowercase + .replace(/\s+/g, '_'); // Replace spaces with underscores +}