├── .prettierignore ├── .eslintrc.json ├── public ├── shoe.glb ├── favicon.ico └── vercel.svg ├── constants └── colors.ts ├── .gitignore ├── pages ├── _app.tsx └── [[...page]].tsx ├── next-env.d.ts ├── README.md ├── styles └── globals.css ├── next.config.js ├── lib └── resolve-builder-content.ts ├── components ├── GoToLink.tsx ├── FlexGapExplorer.tsx ├── CodeBlock.tsx ├── BorderImageExplorer.tsx └── MixBlendMode.tsx ├── tsconfig.json ├── functions └── copy-to-clipboard.ts └── package.json /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .next 3 | public -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /public/shoe.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BuilderIO/builder-fiddle-demos/main/public/shoe.glb -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BuilderIO/builder-fiddle-demos/main/public/favicon.ico -------------------------------------------------------------------------------- /constants/colors.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | primary: "rgba(28, 151, 204, 1)", 3 | primaryLight: "rgb(7, 178, 215)", 4 | }; 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /nbproject/ 2 | /.idea/* 3 | *.tmlanguage.cache 4 | *.tmPreferences.cache 5 | *.stTheme.cache 6 | *.sublime-workspace 7 | *.sublime-project 8 | -------------------------------------------------------------------------------- /pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import "../styles/globals.css"; 2 | 3 | import type { AppProps } from "next/app"; 4 | 5 | export default function MyApp({ Component, pageProps }: AppProps) { 6 | return ; 7 | } 8 | -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Builder.io Fiddle Demos 2 | 3 | Fun examples of using Builder in different ways for Builder [fiddles](https://builder.io/fiddle). 4 | 5 | To run locally: 6 | 7 | ``` 8 | git clone https://github.com/BuilderIO/builder-fiddle-demos 9 | cd builder-fiddle-demos 10 | npm install 11 | npm run dev 12 | ``` 13 | -------------------------------------------------------------------------------- /styles/globals.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | padding: 0; 4 | margin: 0; 5 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, 6 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; 7 | } 8 | 9 | a { 10 | color: inherit; 11 | text-decoration: none; 12 | } 13 | 14 | * { 15 | box-sizing: border-box; 16 | } 17 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | module.exports = { 3 | reactStrictMode: true, 4 | images: { 5 | domains: ["cdn.builder.io"], 6 | }, 7 | async headers() { 8 | return [ 9 | { 10 | source: "/:path*", 11 | headers: [ 12 | // this will allow site to be framed under builder.io for wysiwyg editing 13 | { 14 | key: "Content-Security-Policy", 15 | value: 16 | "frame-ancestors https://*.builder.io https://builder.io http://localhost:1234", 17 | }, 18 | ], 19 | }, 20 | ]; 21 | }, 22 | }; 23 | -------------------------------------------------------------------------------- /lib/resolve-builder-content.ts: -------------------------------------------------------------------------------- 1 | import { builder, Builder } from "@builder.io/react"; 2 | 3 | Builder.isStatic = true; 4 | 5 | export async function resolveBuilderContent( 6 | modelName: string, 7 | targetingAttributes: any, 8 | cachebust?: boolean 9 | ) { 10 | const cacheOpts = cachebust 11 | ? { 12 | cachebust: true, 13 | noCache: true, 14 | } 15 | : { 16 | staleCacheSeconds: 140, 17 | }; 18 | const page = await builder 19 | .get(modelName, { 20 | userAttributes: targetingAttributes, 21 | ...cacheOpts, 22 | }) 23 | .toPromise(); 24 | 25 | return page || null; 26 | } 27 | -------------------------------------------------------------------------------- /components/GoToLink.tsx: -------------------------------------------------------------------------------- 1 | import colors from "../constants/colors"; 2 | 3 | /** 4 | * Link component that will open in a new tab even when clickin in Builder's visual editor 5 | */ 6 | export function GoToLink( 7 | props: React.DetailedHTMLProps< 8 | React.AnchorHTMLAttributes, 9 | HTMLAnchorElement 10 | > 11 | ) { 12 | return ( 13 | { 15 | if (!(e.metaKey || e.shiftKey || e.altKey || e.ctrlKey)) { 16 | open(props.href, "_blank", "noopener"); 17 | } 18 | }} 19 | rel="noopener" 20 | target="_blank" 21 | css={{ 22 | color: colors.primary, 23 | }} 24 | {...props} 25 | > 26 | {props.children} 27 | 28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2019", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "strict": true, 12 | "forceConsistentCasingInFileNames": true, 13 | "noEmit": true, 14 | "esModuleInterop": true, 15 | "module": "esnext", 16 | "moduleResolution": "node", 17 | "resolveJsonModule": true, 18 | "isolatedModules": true, 19 | "incremental": true, 20 | "jsx": "preserve", 21 | "jsxImportSource": "@emotion/react" 22 | }, 23 | "include": [ 24 | "next-env.d.ts", 25 | "**/*.ts", 26 | "**/*.tsx", 27 | "**/*.js" 28 | ], 29 | "exclude": [ 30 | "node_modules" 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /functions/copy-to-clipboard.ts: -------------------------------------------------------------------------------- 1 | // Adopted from https://stackoverflow.com/a/30810322/1959717 2 | function fallbackCopyTextToClipboard(text: string) { 3 | const textarea = document.createElement("textarea"); 4 | textarea.textContent = text; 5 | document.body.appendChild(textarea); 6 | 7 | const selection = document.getSelection()!; 8 | const range = document.createRange(); 9 | range.selectNode(textarea); 10 | selection.removeAllRanges(); 11 | selection.addRange(range); 12 | 13 | let successful = false; 14 | try { 15 | successful = document.execCommand("copy"); 16 | if (!successful) { 17 | console.warn("Uncable to copy to clipboard"); 18 | } 19 | } catch (err) { 20 | console.warn("Uncable to copy to clipboard", err); 21 | } 22 | 23 | selection.removeAllRanges(); 24 | document.body.removeChild(textarea); 25 | return successful; 26 | } 27 | 28 | export async function copyToClipboard(text: string) { 29 | try { 30 | await navigator.clipboard.writeText(text); 31 | } catch (e) { 32 | fallbackCopyTextToClipboard(text); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "builder-fiddle-demos", 3 | "private": true, 4 | "scripts": { 5 | "dev": "next dev", 6 | "build": "next build", 7 | "start": "next dev", 8 | "serve": "next start", 9 | "lint": "next lint", 10 | "prettier": "prettier --write ." 11 | }, 12 | "dependencies": { 13 | "@builder.io/react": "^1.1.47", 14 | "@emotion/react": "^11.8.2", 15 | "@emotion/styled": "^11.8.1", 16 | "@mui/icons-material": "^5.5.1", 17 | "@mui/material": "^5.5.3", 18 | "@react-three/drei": "8.11.0", 19 | "@react-three/fiber": "7.0.26", 20 | "@vercel/fetch": "^6.1.0", 21 | "dedent": "^0.7.0", 22 | "next": "^12.1.0", 23 | "react": "^17.0.2", 24 | "react-dom": "^17.0.2", 25 | "react-syntax-highlighter": "^15.5.0", 26 | "three": "0.137.5" 27 | }, 28 | "devDependencies": { 29 | "@emotion/babel-plugin": "^11.7.2", 30 | "@types/dedent": "^0.7.0", 31 | "@types/node": "16.11.9", 32 | "@types/react": "17.0.36", 33 | "@types/react-syntax-highlighter": "^13.5.2", 34 | "@types/three": "^0.137.0", 35 | "eslint": "7", 36 | "eslint-config-next": "12.0.4", 37 | "prettier": "^2.4.1", 38 | "typescript": "4.5.2" 39 | }, 40 | "license": "MIT" 41 | } 42 | -------------------------------------------------------------------------------- /pages/[[...page]].tsx: -------------------------------------------------------------------------------- 1 | import type { GetStaticPropsContext, InferGetStaticPropsType } from "next"; 2 | import { BuilderComponent, Builder, builder } from "@builder.io/react"; 3 | import DefaultErrorPage from "next/error"; 4 | import Head from "next/head"; 5 | import "../components/FlexGapExplorer"; 6 | import "../components/BorderImageExplorer"; 7 | import "../components/MixBlendMode"; 8 | 9 | builder.init("63f829e0e7a44824a11461f3037b38ed"); 10 | 11 | export async function getStaticProps({ 12 | params, 13 | }: GetStaticPropsContext<{ page: string[] }>) { 14 | const page = await builder 15 | .get("page", { 16 | userAttributes: { 17 | urlPath: "/" + (params?.page?.join("/") || ""), 18 | }, 19 | }) 20 | .toPromise(); 21 | 22 | return { 23 | props: { 24 | page: page || null, 25 | }, 26 | revalidate: 5, 27 | }; 28 | } 29 | 30 | export async function getStaticPaths() { 31 | const pages = await builder.getAll("page", { 32 | options: { noTargeting: true }, 33 | omit: "data.blocks", 34 | }); 35 | 36 | return { 37 | paths: pages.map((page) => `${page.data?.url}`), 38 | fallback: "blocking", 39 | }; 40 | } 41 | 42 | export default function Page({ 43 | page, 44 | }: InferGetStaticPropsType) { 45 | const isLive = !Builder.isEditing && !Builder.isPreviewing; 46 | if (!page && isLive) { 47 | return ( 48 | <> 49 | 50 | 51 | 52 | 53 |
54 | 55 |
56 | 57 | ); 58 | } 59 | 60 | return ( 61 |
62 | 63 | 64 | 65 | 66 |
67 | ); 68 | } 69 | -------------------------------------------------------------------------------- /components/FlexGapExplorer.tsx: -------------------------------------------------------------------------------- 1 | import { Builder } from "@builder.io/react"; 2 | import { CodeBlockComponent } from "./CodeBlock"; 3 | import { GoToLink } from "./GoToLink"; 4 | import dedent from "dedent"; 5 | 6 | interface FlexGapExplorerProps { 7 | flexGap?: number; 8 | boxes?: number; 9 | boxColor?: string; 10 | } 11 | 12 | const defaultBoxColor = "#7F76AC"; 13 | const defaultBoxNumber = 20; 14 | const defaultGapSize = 20; 15 | 16 | export function FlexGapExplorer(props: FlexGapExplorerProps) { 17 | const range = Array(props.boxes || defaultBoxNumber).fill(0); 18 | return ( 19 |
20 |

Flex Gap Explorer

21 |

22 | This is a React component to visualize Flex gap. We have connected it to{" "} 23 | 24 | Builder.io 25 | {" "} 26 | for visual editing. View the source of this component{" "} 27 | 28 | here 29 | 30 |

31 | 32 | 43 | 44 |
52 | {range.map((_, index) => ( 53 |
61 | ))} 62 |
63 |
64 | ); 65 | } 66 | 67 | // Add for visual editing in Builder.io 68 | // https://www.builder.io/blog/drag-drop-react 69 | Builder.registerComponent(FlexGapExplorer, { 70 | name: "Flex Gap Explorer", 71 | inputs: [ 72 | { 73 | name: "flexGap", 74 | type: "number", 75 | defaultValue: defaultGapSize, 76 | helperText: 'Edit this to change the "gap" value', 77 | }, 78 | { 79 | name: "boxes", 80 | type: "number", 81 | defaultValue: defaultBoxNumber, 82 | helperText: "Number of boxes to render", 83 | }, 84 | { 85 | name: "boxColor", 86 | type: "color", 87 | defaultValue: defaultBoxColor, 88 | helperText: "Color of the boxes", 89 | }, 90 | ], 91 | }); 92 | -------------------------------------------------------------------------------- /components/CodeBlock.tsx: -------------------------------------------------------------------------------- 1 | import Assignment from "@mui/icons-material/Assignment"; 2 | import IconButton from "@mui/material/IconButton"; 3 | import Tooltip from "@mui/material/Tooltip"; 4 | import { useState } from "react"; 5 | import javascript from "react-syntax-highlighter/dist/cjs/languages/hljs/javascript"; 6 | import html from "react-syntax-highlighter/dist/cjs/languages/hljs/xml"; 7 | import SyntaxHighlighter from "react-syntax-highlighter/dist/cjs/light-async"; 8 | import oneDark from "react-syntax-highlighter/dist/cjs/styles/hljs/atom-one-dark"; 9 | import githubGist from "react-syntax-highlighter/dist/cjs/styles/hljs/github-gist"; 10 | import { copyToClipboard } from "../functions/copy-to-clipboard"; 11 | 12 | SyntaxHighlighter.registerLanguage("html", html); 13 | SyntaxHighlighter.registerLanguage("xml", html); 14 | SyntaxHighlighter.registerLanguage("javascript", javascript); 15 | SyntaxHighlighter.registerLanguage("js", javascript); 16 | SyntaxHighlighter.registerLanguage("jsx", javascript); 17 | 18 | // Adapted from https://github.com/dpeek/highlightjs-graphql/blob/master/graphql.js#L10 19 | SyntaxHighlighter.registerLanguage("graphql", (hljs: any) => ({ 20 | aliases: ["gql"], 21 | keywords: { 22 | keyword: 23 | "query mutation subscription|10 type interface union scalar fragment|10 enum on ...", 24 | literal: "true false null", 25 | }, 26 | contains: [ 27 | hljs.HASH_COMMENT_MODE, 28 | hljs.QUOTE_STRING_MODE, 29 | hljs.NUMBER_MODE, 30 | { 31 | className: "type", 32 | begin: "[^\\w][A-Z][a-z]", 33 | end: "\\W", 34 | excludeEnd: true, 35 | }, 36 | { 37 | className: "literal", 38 | begin: "[^\\w][A-Z][A-Z]", 39 | end: "\\W", 40 | excludeEnd: true, 41 | }, 42 | { className: "variable", begin: "\\$", end: "\\W", excludeEnd: true }, 43 | { 44 | className: "keyword", 45 | begin: "[.]{2}", 46 | end: "\\.", 47 | }, 48 | { 49 | className: "meta", 50 | begin: "@", 51 | end: "\\W", 52 | excludeEnd: true, 53 | }, 54 | ], 55 | illegal: /([;<']|BEGIN)/, 56 | })); 57 | 58 | const defaultCopyButtonTooltipText = "Copy code to clipboard"; 59 | 60 | export function CodeBlockComponent( 61 | { language, code, dark, fontSize }: any /* TODO: types */ 62 | ) { 63 | const [copyButtonTooltipText, setCopyButtonTooltipText] = useState( 64 | defaultCopyButtonTooltipText 65 | ); 66 | 67 | return ( 68 |
80 | 81 | { 91 | setCopyButtonTooltipText(defaultCopyButtonTooltipText); 92 | }} 93 | onClick={() => { 94 | copyToClipboard(code); 95 | setCopyButtonTooltipText("Copied!"); 96 | }} 97 | > 98 | 104 | 105 | 106 | 117 | {code} 118 | 119 |
120 | ); 121 | } 122 | -------------------------------------------------------------------------------- /components/BorderImageExplorer.tsx: -------------------------------------------------------------------------------- 1 | import { Builder } from "@builder.io/react"; 2 | import { CodeBlockComponent } from "./CodeBlock"; 3 | import { GoToLink } from "./GoToLink"; 4 | import dedent from "dedent"; 5 | 6 | interface BorderImageExplorerProps { 7 | borderImageWidth?: number; 8 | borderImageOutset?: number; 9 | borderImageSlice?: string; 10 | borderImageRepeat?: string; 11 | image?: string; 12 | } 13 | 14 | const defaultImage = 15 | "https://cdn.builder.io/api/v1/image/assets%2F63f829e0e7a44824a11461f3037b38ed%2F5f6b0952ca554ddaaba4d131f91327e4?quality=60&width=800"; 16 | const defaultborderImageWidth = 20; 17 | const defaultBorderImageSlice = "30"; 18 | const defaultBorderImageRepeat = "repeat"; 19 | const defaultBorderImageOutset = 0; 20 | 21 | export function BorderImageExplorer(props: BorderImageExplorerProps) { 22 | return ( 23 |
24 |

Border Image Explorer

25 |

26 | This is a React component to visualize mix-blend-mode in 27 | CSS. We have connected it to{" "} 28 | 29 | Builder.io 30 | {" "} 31 | for visual editing. View the full source code of this component{" "} 32 | 33 | here 34 | 35 |

36 | 37 | 56 | 57 |
65 |
85 | Hello world! 86 |
87 |
88 |
89 | ); 90 | } 91 | 92 | // Add for visual editing in Builder.io 93 | // https://www.builder.io/blog/drag-drop-react 94 | Builder.registerComponent(BorderImageExplorer, { 95 | name: "Border Image Explorer", 96 | inputs: [ 97 | { 98 | name: "image", 99 | type: "file", 100 | allowedFileTypes: ["jpg", "png", "svg"], 101 | defaultValue: defaultImage, 102 | }, 103 | { 104 | name: "borderImageWidth", 105 | type: "number", 106 | defaultValue: defaultborderImageWidth, 107 | }, 108 | { 109 | name: "borderImageSlice", 110 | type: "string", 111 | defaultValue: defaultBorderImageSlice, 112 | helperText: 113 | "https://developer.mozilla.org/en-US/docs/Web/CSS/border-image-slice", 114 | }, 115 | { 116 | name: "borderImageRepeat", 117 | type: "string", 118 | enum: ["stretch", "repeat", "round", "space"], 119 | defaultValue: defaultBorderImageRepeat, 120 | helperText: 121 | "https://developer.mozilla.org/en-US/docs/Web/CSS/border-image-repeat", 122 | }, 123 | // { 124 | // name: "borderImageOutset", 125 | // type: "number", 126 | // defaultValue: defaultBorderImageOutset, 127 | // helperText: 128 | // "https://developer.mozilla.org/en-US/docs/Web/CSS/border-image-outset", 129 | // }, 130 | ], 131 | }); 132 | -------------------------------------------------------------------------------- /components/MixBlendMode.tsx: -------------------------------------------------------------------------------- 1 | import { Builder } from "@builder.io/react"; 2 | import { CodeBlockComponent } from "./CodeBlock"; 3 | import { GoToLink } from "./GoToLink"; 4 | import dedent from "dedent"; 5 | import { useState } from "react"; 6 | 7 | interface MixBlendModeExplorerProps { 8 | text?: string; 9 | fontSize?: string; 10 | video?: string; 11 | image?: string; 12 | } 13 | 14 | const oceanVideo = 15 | "https://cdn.builder.io/o/assets%2F63f829e0e7a44824a11461f3037b38ed%2F321eda8b91344a349a2fe52c1400c348%2Fcompressed?apiKey=63f829e0e7a44824a11461f3037b38ed&token=321eda8b91344a349a2fe52c1400c348&alt=media&optimized=true"; 16 | const spaceVideo = 17 | "https://cdn.builder.io/o/assets%2F63f829e0e7a44824a11461f3037b38ed%2Feec9bda8d36c455ab48e875a7701a171%2Fcompressed?apiKey=63f829e0e7a44824a11461f3037b38ed&token=eec9bda8d36c455ab48e875a7701a171&alt=media&optimized=true"; 18 | const rainforestVideo = 19 | "https://cdn.builder.io/o/assets%2F63f829e0e7a44824a11461f3037b38ed%2F6ed2e518224c469c95a709338243fb2a%2Fcompressed?apiKey=63f829e0e7a44824a11461f3037b38ed&token=6ed2e518224c469c95a709338243fb2a&alt=media&optimized=true"; 20 | 21 | const defaultImage = ""; 22 | const defaultFontSize = "150px"; 23 | const defaultVideo = oceanVideo; 24 | const defaultText = "Hello world!"; 25 | 26 | export function MixBlendModeExplorer(props: MixBlendModeExplorerProps) { 27 | const [expandCodeExample, setExpandCodeExample] = useState(false); 28 | 29 | const video = props.video ?? defaultVideo; 30 | const text = props.text ?? defaultText; 31 | const image = props.image ?? defaultImage; 32 | const fontSize = props.fontSize ?? defaultFontSize; 33 | 34 | return ( 35 |
36 |

Mix Blend Mode Explorer

37 |

38 | This is a React component to visualize border images. We have connected 39 | it to{" "} 40 | 41 | Builder.io 42 | {" "} 43 | for visual editing. View the source of this component{" "} 44 | 45 | here 46 | 47 |

48 | 49 |
55 | 68 | 80 |
81 | 82 |
89 | 106 |
118 | {text} 119 |
120 |
121 |
122 | ); 123 | } 124 | 125 | // Add for visual editing in Builder.io 126 | // https://www.builder.io/blog/drag-drop-react 127 | Builder.registerComponent(MixBlendModeExplorer, { 128 | name: "Mix Blend Mode Explorer", 129 | inputs: [ 130 | { 131 | name: "text", 132 | type: "text", 133 | defaultValue: defaultText, 134 | }, 135 | { 136 | name: "video", 137 | type: "string", 138 | enum: [ 139 | { 140 | label: "Ocean", 141 | value: oceanVideo, 142 | }, 143 | { 144 | label: "Rainforest", 145 | value: rainforestVideo, 146 | }, 147 | { 148 | label: "Space", 149 | value: spaceVideo, 150 | }, 151 | ], 152 | defaultValue: defaultVideo, 153 | }, 154 | { 155 | name: "fontSize", 156 | type: "text", 157 | defaultValue: defaultFontSize, 158 | }, 159 | { 160 | name: "image", 161 | type: "file", 162 | allowedFileTypes: ["jpg", "png", "svg"], 163 | defaultValue: defaultImage, 164 | helperText: `If you choose an image, it'll display instead of the video`, 165 | }, 166 | ], 167 | }); 168 | --------------------------------------------------------------------------------