From e0e697356f194075a5be13b684f99331ed79e10a Mon Sep 17 00:00:00 2001 From: GCHQ-Developer-112 <113986285+GCHQ-Developer-112@users.noreply.github.com> Date: Tue, 30 Jan 2024 13:43:08 +0000 Subject: [PATCH] feat(root): add stackblitz button and add new variant of "copy code" button for small screen sizes Add new optional Stackblitz button to ComponentPreview & CodePreview components, connect Stackblitz SDK to onClick of StackblitzButton component, add cleanup function to useEffect in CookieData component . #751 --- gatsby-node.js | 5 + package-lock.json | 12 ++ package.json | 2 + src/assets/svg/index.ts | 1 + src/assets/svg/stackblitz-logo.svg | 1 + src/components/CodePreview/index.tsx | 139 +++++++++++----- .../static/components/CookiesData/index.tsx | 18 ++- .../components/StackblitzButton/index.tsx | 152 ++++++++++++++++++ 8 files changed, 286 insertions(+), 44 deletions(-) create mode 100644 src/assets/svg/stackblitz-logo.svg create mode 100644 src/content/structured/patterns/components/StackblitzButton/index.tsx diff --git a/gatsby-node.js b/gatsby-node.js index a024c806f..46f7dcd4b 100644 --- a/gatsby-node.js +++ b/gatsby-node.js @@ -236,6 +236,11 @@ exports.createResolvers = ({ createResolvers }) => { exports.onCreateWebpackConfig = ({ actions }) => { actions.setWebpackConfig({ + resolve: { + fallback: { + fs: false, + }, + }, plugins: [ /** * See line 203 of: diff --git a/package-lock.json b/package-lock.json index 26d0616d2..a111eaf0b 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.1", "@ukic/fonts": "^2.6.0", "@ukic/react": "^2.10.0", @@ -47,6 +48,7 @@ "gatsby-transformer-remark": "^6.12.0", "gatsby-transformer-sharp": "^4.2.0", "github-slugger": "^1.4.0", + "lodash.kebabcase": "^4.1.1", "performant-array-to-tree": "^1.9.1", "prism-react-renderer": "^1.3.1", "prismjs": "^1.25.0", @@ -5962,6 +5964,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.11.0", "resolved": "https://registry.npmjs.org/@stencil/core/-/core-4.11.0.tgz", @@ -20059,6 +20066,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", diff --git a/package.json b/package.json index 7ad52c107..fbede5964 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.1", "@ukic/fonts": "^2.6.0", "@ukic/react": "^2.10.0", @@ -42,6 +43,7 @@ "gatsby-transformer-remark": "^6.12.0", "gatsby-transformer-sharp": "^4.2.0", "github-slugger": "^1.4.0", + "lodash.kebabcase": "^4.1.1", "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..56e30141f 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,94 @@ 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, + showStackblitzBtn, + filePaths, + projectTitle, + projectDescription, +}) => { + let defaultViewportWidth = 0; + + if (typeof window !== "undefined") { + defaultViewportWidth = window.innerWidth; + } + + const [viewportWidth, setViewportWidth] = + useState(defaultViewportWidth); + + let 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 && + filePaths !== undefined && + filePaths?.length > 0 && + projectTitle !== undefined && + projectDescription !== undefined && ( + + )} + { + navigator.clipboard.writeText(code); + document + .querySelector("#copy-to-clipboard-toast") + ?.setVisible(); + }} + > + + {isLargeViewport && "Copy code"} + +
+ + ); +}; const ComponentPreview: React.FC = ({ snippets, @@ -70,6 +126,7 @@ const ComponentPreview: React.FC = ({ centered = true, style, state = "none", + showStackblitzBtn, }) => (

Interactive example

@@ -111,7 +168,15 @@ 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/index.tsx b/src/content/structured/patterns/components/StackblitzButton/index.tsx new file mode 100644 index 000000000..c1c2723cb --- /dev/null +++ b/src/content/structured/patterns/components/StackblitzButton/index.tsx @@ -0,0 +1,152 @@ +import React, { useEffect, useState } from "react"; +import { readFile } from "fs"; +import sdk from "@stackblitz/sdk"; +import kebabCase from "lodash/kebabCase"; +import { StackblitzLogo } from "../../../../../assets/svg"; +import { debounce } from "../../../../../utils/helpers"; + +export type StackblitzProps = { + filePaths: string[]; + projectTitle: string; + projectDescription: string; +}; + +const StackblitzButton: React.FC = ({ + filePaths, + projectTitle, + projectDescription, +}) => { + let defaultViewportWidth = 0; + + if (typeof window !== "undefined") { + defaultViewportWidth = window.innerWidth; + } + + const [viewportWidth, setViewportWidth] = + useState(defaultViewportWidth); + + let isLargeViewport: boolean = viewportWidth > 992; + + useEffect(() => { + const handleResize = debounce(() => setViewportWidth(window.innerWidth)); + + window.addEventListener("resize", handleResize); + + // Cleanup function + return () => { + window.removeEventListener("resize", handleResize); + }; + }, []); + + // Define the package.json content for a React app + const packageJson = { + name: `icds-${kebabCase(projectTitle)}`, + version: "0.0.0", + private: true, + dependencies: { + "@mdi/js": "^7.4.47", + "@types/react": "18.2.48", + "@types/react-dom": "18.2.18", + "@ukic/fonts": "^2.6.0", + "@ukic/react": "^2.10.0", + // prettier-ignore + "react": "18.2.0", + "react-dom": "18.2.0", + "react-router-dom": "^6.21.3", + }, + scripts: { + start: "react-scripts start", + build: "react-scripts build", + test: "react-scripts test --env=jsdom", + eject: "react-scripts eject", + }, + devDependencies: { + "react-scripts": "latest", + }, + }; + + // Define the index.tsx content for a React app + const indexTsx = `import { StrictMode } from 'react'; + import { createRoot } from 'react-dom/client'; + import { BrowserRouter } from 'react-router-dom'; + + import App from './App'; + + const root = createRoot(document.getElementById('app')); + + root.render( + + + + + + );`; + + const createStackblitzProject = (paths: string[]) => { + const files: { [key: string]: string } = {}; + let isWebComponents = false; + let isJSX = false; + let ext = isJSX ? "jsx" : "tsx"; + if (paths.length > 0) { + for (const path of paths) { + const extension = path.match(/\.([a-z]+)$/i); + // Check if path ends in HTML, CSS, TSX or JSX + if (extension && ["html", "css", "tsx", "jsx"].includes(extension[1])) { + if (extension[1] === "html") { + isWebComponents = true; + } + if (extension[1] === "jsx") { + isJSX = true; + } + // Read the contents of the passed file + readFile(path, "utf8", (err, data) => { + if (err) { + console.error( + "Error reading file passed into Stackblitz SDK", + err + ); + } + files[`index.${extension}`] = data; + }); + } else { + console.error(`File type not supported for path: ${path}`); + } + } + } + + // Change file structure for React code examples + if (!isWebComponents) { + files["package.json"] = JSON.stringify(packageJson, null, 2); + files[`app.${ext}`] = files[`index.${ext}`]; + files[`index.${ext}`] = indexTsx; + } + + sdk.embedProject("app", { + title: `ICDS ${projectTitle}`, + description: `${projectDescription}`, + files, + template: isWebComponents ? "html" : "create-react-app", + tags: ["stackblitz", "sdk"], + }); + }; + + return ( + createStackblitzProject(filePaths)} + > + {isLargeViewport ? ( + + + + ) : ( + + )} + {isLargeViewport && "Stackblitz"} + + ); +}; + +export default StackblitzButton;