diff --git a/gatsby-node.js b/gatsby-node.js index a024c806f..94f5ba14d 100644 --- a/gatsby-node.js +++ b/gatsby-node.js @@ -1,4 +1,5 @@ const path = require(`path`); +const fs = require("fs"); // eslint-disable-next-line import/no-extraneous-dependencies const webpack = require("webpack"); const pagesConfig = require("./src/config"); @@ -236,6 +237,11 @@ exports.createResolvers = ({ createResolvers }) => { exports.onCreateWebpackConfig = ({ actions }) => { actions.setWebpackConfig({ + resolve: { + fallback: { + fs: false, + }, + }, plugins: [ /** * See line 203 of: @@ -277,3 +283,52 @@ exports.onCreateWebpackConfig = ({ actions }) => { ], }); }; + +exports.sourceNodes = ({ actions, createNodeId, createContentDigest }) => { + const { createNode } = actions; + + // Read all files in the sub-directories of the patterns folder + function getFilePaths(dirPath) { + let filePaths = []; + const files = fs.readdirSync(dirPath); + + files.forEach((file) => { + const absolutePath = path.join(dirPath, file); + if (fs.statSync(absolutePath).isDirectory()) { + filePaths = filePaths.concat(getFilePaths(absolutePath)); + } else { + filePaths.push(absolutePath); + } + }); + + return filePaths; + } + + // Read all files in the patterns folder + const patternsDir = path.resolve( + __dirname, + "src/content/structured/patterns/templates" + ); + const filePaths = getFilePaths(patternsDir); + + // For each file in the patterns folder, read the contents of the file and create a data node + filePaths.forEach((filePath) => { + const content = fs.readFileSync(filePath, "utf8"); + const regex = /(\/patterns\/templates\/.*)$/; + const regexMatchedFilePath = filePath.match(regex)[0]; + + const nodeMeta = { + id: createNodeId(`${regexMatchedFilePath}`), + parent: null, + children: [], + internal: { + type: `PatternsTemplates`, + contentFilePath: regexMatchedFilePath, + content, + contentDigest: createContentDigest(content), + }, + }; + + createNode(nodeMeta); + }); +}; diff --git a/package-lock.json b/package-lock.json index 338fa52c8..ec8792bb8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "@mdi/react": "^1.5.0", "@mdx-js/mdx": "^1.6.22", "@mdx-js/react": "^1.6.22", + "@stackblitz/sdk": "^1.9.0", "@ukic/docs": "^2.9.2", "@ukic/fonts": "^2.6.0", "@ukic/react": "^2.11.0", @@ -47,6 +48,8 @@ "gatsby-transformer-remark": "^6.12.0", "gatsby-transformer-sharp": "^4.2.0", "github-slugger": "^1.4.0", + "lodash.kebabcase": "^4.1.1", + "lodash.startcase": "^4.4.0", "performant-array-to-tree": "^1.9.1", "prism-react-renderer": "^1.3.1", "prismjs": "^1.25.0", @@ -5962,6 +5965,11 @@ "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz", "integrity": "sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==" }, + "node_modules/@stackblitz/sdk": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@stackblitz/sdk/-/sdk-1.9.0.tgz", + "integrity": "sha512-3m6C7f8pnR5KXys/Hqx2x6ylnpqOak6HtnZI6T5keEO0yT+E4Spkw37VEbdwuC+2oxmjdgq6YZEgiKX7hM1GmQ==" + }, "node_modules/@stencil/core": { "version": "4.12.1", "resolved": "https://registry.npmjs.org/@stencil/core/-/core-4.12.1.tgz", @@ -20059,6 +20067,11 @@ "resolved": "https://registry.npmjs.org/lodash.has/-/lodash.has-4.5.2.tgz", "integrity": "sha512-rnYUdIo6xRCJnQmbVFEwcxF144erlD+M3YcJUVesflU9paQaE8p+fJDcIQrlMYbxoANFL+AB9hZrzSBBk5PL+g==" }, + "node_modules/lodash.kebabcase": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.kebabcase/-/lodash.kebabcase-4.1.1.tgz", + "integrity": "sha512-N8XRTIMMqqDgSy4VLKPnJ/+hpGZN+PHQiJnSenYqPaVV/NCqEogTnAdZLQiGKhxX+JCs8waWq2t1XHWKOmlY8g==" + }, "node_modules/lodash.map": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/lodash.map/-/lodash.map-4.6.0.tgz", @@ -20099,6 +20112,11 @@ "resolved": "https://registry.npmjs.org/lodash.some/-/lodash.some-4.6.0.tgz", "integrity": "sha512-j7MJE+TuT51q9ggt4fSgVqro163BEFjAt3u97IqU+JA2DkWl80nFTrowzLpZ/BnpN7rrl0JA/593NAdd8p/scQ==" }, + "node_modules/lodash.startcase": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.startcase/-/lodash.startcase-4.4.0.tgz", + "integrity": "sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==" + }, "node_modules/lodash.template": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.template/-/lodash.template-4.5.0.tgz", diff --git a/package.json b/package.json index bbb2f5eb1..ce466f45b 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "@mdi/react": "^1.5.0", "@mdx-js/mdx": "^1.6.22", "@mdx-js/react": "^1.6.22", + "@stackblitz/sdk": "^1.9.0", "@ukic/docs": "^2.9.2", "@ukic/fonts": "^2.6.0", "@ukic/react": "^2.11.0", @@ -42,6 +43,8 @@ "gatsby-transformer-remark": "^6.12.0", "gatsby-transformer-sharp": "^4.2.0", "github-slugger": "^1.4.0", + "lodash.kebabcase": "^4.1.1", + "lodash.startcase": "^4.4.0", "performant-array-to-tree": "^1.9.1", "prism-react-renderer": "^1.3.1", "prismjs": "^1.25.0", diff --git a/src/assets/svg/index.ts b/src/assets/svg/index.ts index a1f43ba82..1486ed922 100644 --- a/src/assets/svg/index.ts +++ b/src/assets/svg/index.ts @@ -3,3 +3,4 @@ export { default as GCHQLogo } from "./gchq-logo.svg"; export { default as MI5Logo } from "./mi5-logo.svg"; export { default as SISLogo } from "./sis-logo.svg"; export { default as ICDSLogo } from "./icds-logo.svg"; +export { default as StackblitzLogo } from "./stackblitz-logo.svg"; diff --git a/src/assets/svg/stackblitz-logo.svg b/src/assets/svg/stackblitz-logo.svg new file mode 100644 index 000000000..d3feff844 --- /dev/null +++ b/src/assets/svg/stackblitz-logo.svg @@ -0,0 +1 @@ +StackBlitz \ No newline at end of file diff --git a/src/components/CodePreview/index.tsx b/src/components/CodePreview/index.tsx index 537dd8576..6a7e4cdbb 100644 --- a/src/components/CodePreview/index.tsx +++ b/src/components/CodePreview/index.tsx @@ -1,5 +1,5 @@ import Highlight, { defaultProps } from "prism-react-renderer"; -import React, { CSSProperties, ReactNode } from "react"; +import React, { CSSProperties, ReactNode, useState, useEffect } from "react"; import { mdiCheckboxMarkedCircle, @@ -11,13 +11,17 @@ import clsx from "clsx"; import { SlottedSVG } from "@ukic/react"; import "./index.css"; +import StackblitzButton, { + StackblitzProps, +} from "../../content/structured/patterns/components/StackblitzButton"; +import { debounce } from "../../utils/helpers"; interface Snippet { language: string; snippet: string; } -interface ComponentPreviewProps { +interface ComponentPreviewProps extends Partial { snippets?: Snippet[]; left?: boolean; noPadding?: boolean; @@ -25,42 +29,91 @@ interface ComponentPreviewProps { children: ReactNode; style: CSSProperties; state: "none" | "good" | "bad"; + showStackblitzBtn?: boolean; } -const CodeSnippet: React.FC<{ code: string }> = ({ code }) => ( - <> - - {({ className, style, tokens, getLineProps, getTokenProps }) => ( -
-          
-            {tokens.map((line, i) => (
-              
- {line.map((token, key) => ( - - ))} -
- ))} -
-
- )} -
-
- { - navigator.clipboard.writeText(code); - document - .querySelector("#copy-to-clipboard-toast") - ?.setVisible(); - }} - > - Copy code - - -
- -); +interface CodeSnippetProps extends Partial { + code: string; + showStackblitzBtn?: boolean; +} + +const CodeSnippet: React.FC = ({ + code, + isWebComponents, + showStackblitzBtn, + projectTitle, + projectDescription, +}) => { + let defaultViewportWidth = 0; + + if (typeof window !== "undefined") { + defaultViewportWidth = window.innerWidth; + } + + const [viewportWidth, setViewportWidth] = + useState(defaultViewportWidth); + + const isLargeViewport: boolean = viewportWidth > 992; + + useEffect(() => { + const handleResize = debounce(() => setViewportWidth(window.innerWidth)); + + window.addEventListener("resize", handleResize); + + // Cleanup function + return () => { + window.removeEventListener("resize", handleResize); + }; + }, []); + + return ( + <> + + {({ className, style, tokens, getLineProps, getTokenProps }) => ( +
+            
+              {tokens.map((line, i) => (
+                
+ {line.map((token, key) => ( + + ))} +
+ ))} +
+
+ )} +
+
+ {showStackblitzBtn && projectTitle !== undefined && ( + + )} + { + navigator.clipboard.writeText(code); + document + .querySelector("#copy-to-clipboard-toast") + ?.setVisible(); + }} + > + + {isLargeViewport && "Copy code"} + +
+ + ); +}; const ComponentPreview: React.FC = ({ snippets, @@ -70,6 +123,9 @@ const ComponentPreview: React.FC = ({ centered = true, style, state = "none", + showStackblitzBtn, + projectTitle, + projectDescription, }) => (

Interactive example

@@ -111,7 +167,17 @@ const ComponentPreview: React.FC = ({
{snippets.map((snippet, index) => ( - + ))} diff --git a/src/content/static/components/CookiesData/index.tsx b/src/content/static/components/CookiesData/index.tsx index e0bc43306..345597f28 100644 --- a/src/content/static/components/CookiesData/index.tsx +++ b/src/content/static/components/CookiesData/index.tsx @@ -1,4 +1,4 @@ -import React, { useState } from "react"; +import React, { useState, useEffect } from "react"; import { debounce } from "../../../../utils/helpers"; import CookiesCards from "./CookiesCards"; @@ -31,14 +31,18 @@ const CookiesData = ({ headers, data, caption }: CookiesDataProps) => { const [viewportWidth, setViewportWidth] = useState(defaultViewportWidth); - React.useEffect(() => { - window.addEventListener( - "resize", - debounce(() => setViewportWidth(window.innerWidth)) - ); + useEffect(() => { + const handleResize = debounce(() => setViewportWidth(window.innerWidth)); + + window.addEventListener("resize", handleResize); + + // Cleanup function + return () => { + window.removeEventListener("resize", handleResize); + }; }, []); - if (viewportWidth > 991) { + if (viewportWidth > 992) { return ; } diff --git a/src/content/structured/patterns/components/StackblitzButton/configFiles.ts b/src/content/structured/patterns/components/StackblitzButton/configFiles.ts new file mode 100644 index 000000000..1361f8e97 --- /dev/null +++ b/src/content/structured/patterns/components/StackblitzButton/configFiles.ts @@ -0,0 +1,113 @@ +// eslint-disable-next-line import/no-extraneous-dependencies +import kebabCase from "lodash/kebabCase"; +const designSystemPackageJson = require("../../../../../../package.json"); + +export const createIndexTsx = ( + componentName: string +) => `import { StrictMode } from 'react'; + import { createRoot } from 'react-dom/client'; + import { BrowserRouter } from 'react-router-dom'; + + import ${componentName} from './app'; + + const root = createRoot(document.getElementById('root')); + + root.render( + + + <${componentName} /> + + + );`; + +export const createReactHTML = (ext: string) => ` + + + + + Vite + React + TS + + +
+ + + `; + +export const packageJson = (projectTitle: string, isWebComponents: boolean) => { + const dependencies = isWebComponents + ? {} + : { + "@mdi/js": "^7.4.47", + "@ukic/fonts": `${designSystemPackageJson.dependencies["@ukic/fonts"]}`, + "@ukic/react": `${designSystemPackageJson.dependencies["@ukic/react"]}`, + react: "^18.2.0", + "react-dom": "^18.2.0", + "react-router-dom": "^6.22.0", + }; + + const reactDevDependencies = { + "@types/react": "^18.2.48", + "@types/react-dom": "^18.2.18", + "@vitejs/plugin-react": "^4.2.1", + }; + const devDependencies = { vite: "^5.0.12" }; + + return { + name: `icds-${kebabCase(projectTitle)}`, + version: "0.0.0", + private: true, + scripts: { + dev: "vite", + build: "vite build", + preview: "vite preview", + }, + dependencies, + devDependencies: isWebComponents + ? { ...devDependencies } + : { ...devDependencies, ...reactDevDependencies }, + }; +}; + +export const tsConfig = `{ + "compilerOptions": { + "jsx": "react-jsx", + "lib": ["DOM", "ES2022"], + "moduleResolution": "node", + "target": "ES2022" + } + }`; + +export const viteConfig = `import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [react()], +}) +`; + +export const createWebComponentsIndexHTML = ( + codeSnippet: string +) => ` + + + Home + + + + + + + + ${codeSnippet} + + + + `; diff --git a/src/content/structured/patterns/components/StackblitzButton/index.tsx b/src/content/structured/patterns/components/StackblitzButton/index.tsx new file mode 100644 index 000000000..174494d21 --- /dev/null +++ b/src/content/structured/patterns/components/StackblitzButton/index.tsx @@ -0,0 +1,174 @@ +import React, { useEffect, useState } from "react"; +import sdk from "@stackblitz/sdk"; +// eslint-disable-next-line import/no-extraneous-dependencies +import startCase from "lodash/startCase"; +import { graphql, useStaticQuery } from "gatsby"; +import { + createIndexTsx, + createReactHTML, + createWebComponentsIndexHTML, + packageJson, + tsConfig, + viteConfig, +} from "./configFiles"; +import { StackblitzLogo } from "../../../../../assets/svg"; +import { debounce } from "../../../../../utils/helpers"; + +export type StackblitzProps = { + codeSnippet?: string; + filePaths?: string[]; + isWebComponents?: boolean; + projectTitle: string; + projectDescription?: string; +}; + +const StackblitzButton: React.FC = ({ + codeSnippet, + filePaths, + isWebComponents, + projectTitle, + projectDescription, +}) => { + let defaultViewportWidth = 0; + + if (typeof window !== "undefined") { + defaultViewportWidth = window.innerWidth; + } + + const [viewportWidth, setViewportWidth] = + useState(defaultViewportWidth); + + const isLargeViewport: boolean = viewportWidth > 992; + + useEffect(() => { + const handleResize = debounce(() => setViewportWidth(window.innerWidth)); + + window.addEventListener("resize", handleResize); + + // Cleanup function + return () => { + window.removeEventListener("resize", handleResize); + }; + }, []); + + const data = useStaticQuery(graphql` + query { + allPatternsTemplates { + nodes { + internal { + content + contentFilePath + } + } + } + } + `); + + const createStackblitzProject = (paths?: string[]) => { + const files: { [key: string]: string } = {}; + let isWebComponentsInternal: boolean = + isWebComponents !== undefined ? isWebComponents : true; + let isJSX: boolean = true; + let fileName: string | undefined; + + if (paths && paths.length > 0) { + paths.forEach((path) => { + data.allPatternsTemplates.nodes + .filter((node: { internal: { contentFilePath: string } }) => + node.internal.contentFilePath.includes(path) + ) + .forEach( + (node: { + internal: { contentFilePath: string; content: string }; + }) => { + // Check if path ends in HTML, CSS, TSX or JSX + const extension = path.match(/\.([a-z]+)$/i); + if ( + extension && + ["html", "css", "tsx", "jsx"].includes(extension[1]) + ) { + if (extension[1] !== "css") { + if (extension[1] !== "html") { + isWebComponentsInternal = false; + } + if (extension[1] !== "jsx") { + isJSX = false; + } + fileName = node.internal.contentFilePath.split("/").pop(); + } + files[ + `${ + isWebComponentsInternal + ? `index.${extension[1]}` + : `src/index.${extension[1]}` + }` + ] = node.internal.content; + } + } + ); + }); + } else if (codeSnippet !== undefined && codeSnippet !== "") { + if (isWebComponents) { + files[`index.html`] = createWebComponentsIndexHTML(codeSnippet); + } + } + + const ext = isJSX ? "jsx" : "tsx"; + const componentName = startCase(fileName?.split(".")[0]).replace(/ /g, ""); + + // Define the index.tsx content for a React app + const indexTsx = createIndexTsx(componentName); + + // Change file structure for React code examples + if ( + (isWebComponents !== undefined && !isWebComponents) || + !isWebComponentsInternal + ) { + files[`src/app.${ext}`] = files[`src/index.${ext}`]; + files[`src/index.${ext}`] = indexTsx; + files[`index.html`] = createReactHTML(ext); + files["vite.config.js"] = viteConfig; + if (ext === "tsx") { + files["tsconfig.json"] = tsConfig; + } + } + + files["package.json"] = JSON.stringify( + packageJson(projectTitle, isWebComponentsInternal), + null, + 2 + ); + + const description = + projectDescription === undefined || projectDescription === "" + ? "" + : projectDescription; + + sdk.openProject({ + title: `ICDS ${projectTitle}`, + description, + files, + template: "node", + }); + }; + + return ( + createStackblitzProject(filePaths)} + > + {isLargeViewport ? ( + + + + ) : ( + + )} + {isLargeViewport && "Stackblitz"} + + ); +}; + +export default StackblitzButton;