From 5a0b2798dbcaead54f997a0c1445a98f4075a005 Mon Sep 17 00:00:00 2001 From: Danilo Woznica Date: Tue, 23 Nov 2021 15:39:47 +0100 Subject: [PATCH] feat(landing): add examples section (#160) --- .../src/components/CodeEditor/CodeMirror.tsx | 11 +- .../src/components/CodeEditor/index.tsx | 83 +++--- .../src/components/CodeViewer/index.tsx | 3 +- .../src/presets/CustomSandpack.stories.tsx | 1 - website/docs/src/CustomSandpack.tsx | 1 - website/landing/components/Intro/Examples.tsx | 176 +++++++++++++ website/landing/components/Intro/Header.tsx | 50 ++++ website/landing/components/Intro/Intro.tsx | 158 ++--------- .../components/Intro/SandpackExample.tsx | 47 ++++ .../components/Intro/Sections/Custom.tsx | 167 ++++++++++++ .../components/Intro/Sections/Editor.tsx | 158 +++++++++++ .../components/Intro/Sections/Layout.tsx | 119 +++++++++ .../Intro/Sections/LayoutContext.tsx | 61 +++++ .../components/Intro/Sections/Template.tsx | 123 +++++++++ .../components/Intro/Sections/Theme.tsx | 125 +++++++++ .../components/Intro/Sections/common.tsx | 249 ++++++++++++++++++ .../landing/components/common/Clipboard.tsx | 7 +- .../landing/components/common/CodeBlock.tsx | 2 +- .../landing/components/common/Resources.tsx | 2 +- .../components/common/SandpackPreview.tsx | 22 +- .../components/common/useBreakpoint.ts | 29 ++ website/landing/stitches.config.ts | 12 +- website/landing/styles/globalStyles.ts | 1 + website/landing/website.config.json | 26 +- 24 files changed, 1407 insertions(+), 226 deletions(-) create mode 100644 website/landing/components/Intro/Examples.tsx create mode 100644 website/landing/components/Intro/Header.tsx create mode 100644 website/landing/components/Intro/SandpackExample.tsx create mode 100644 website/landing/components/Intro/Sections/Custom.tsx create mode 100644 website/landing/components/Intro/Sections/Editor.tsx create mode 100644 website/landing/components/Intro/Sections/Layout.tsx create mode 100644 website/landing/components/Intro/Sections/LayoutContext.tsx create mode 100644 website/landing/components/Intro/Sections/Template.tsx create mode 100644 website/landing/components/Intro/Sections/Theme.tsx create mode 100644 website/landing/components/Intro/Sections/common.tsx create mode 100644 website/landing/components/common/useBreakpoint.ts diff --git a/sandpack-react/src/components/CodeEditor/CodeMirror.tsx b/sandpack-react/src/components/CodeEditor/CodeMirror.tsx index 872c5fe46..43bb1f281 100644 --- a/sandpack-react/src/components/CodeEditor/CodeMirror.tsx +++ b/sandpack-react/src/components/CodeEditor/CodeMirror.tsx @@ -65,7 +65,11 @@ interface CodeMirrorProps { decorators?: Decorators; } -export const CodeMirror = React.forwardRef( +export interface CodeMirrorRef { + getCodemirror: () => EditorView | undefined; +} + +export const CodeMirror = React.forwardRef( ( { code, @@ -90,6 +94,10 @@ export const CodeMirror = React.forwardRef( const { listen } = useSandpack(); const ariaId = React.useRef(generateRandomId()); + React.useImperativeHandle(ref, () => ({ + getCodemirror: () => cmView.current, + })); + React.useEffect(() => { if (!wrapper.current) { return () => { @@ -200,6 +208,7 @@ export const CodeMirror = React.forwardRef( "aria-describedby", `exit-instructions-${ariaId.current}` ); + view.contentDOM.setAttribute("data-gramm", "false"); } cmView.current = view; diff --git a/sandpack-react/src/components/CodeEditor/index.tsx b/sandpack-react/src/components/CodeEditor/index.tsx index b7c01feab..7e29b5a21 100644 --- a/sandpack-react/src/components/CodeEditor/index.tsx +++ b/sandpack-react/src/components/CodeEditor/index.tsx @@ -8,7 +8,9 @@ import { useSandpack } from "../../hooks/useSandpack"; import { FileTabs } from "../FileTabs"; import { CodeMirror } from "./CodeMirror"; +import type { CodeMirrorRef } from "./CodeMirror"; +export type CodeEditorRef = CodeMirrorRef; export interface CodeEditorProps { customStyle?: React.CSSProperties; showTabs?: boolean; @@ -21,44 +23,53 @@ export interface CodeEditorProps { export { CodeMirror as CodeEditor }; -export const SandpackCodeEditor: React.FC = ({ - customStyle, - showTabs, - showLineNumbers = false, - showInlineErrors = false, - showRunButton = true, - wrapContent = false, - closableTabs = false, -}) => { - const { sandpack } = useSandpack(); - const { code, updateCode } = useActiveCode(); - const { activePath, status, editorState } = sandpack; - const shouldShowTabs = showTabs ?? sandpack.openPaths.length > 1; +export const SandpackCodeEditor = React.forwardRef< + CodeMirrorRef, + CodeEditorProps +>( + ( + { + customStyle, + showTabs, + showLineNumbers = false, + showInlineErrors = false, + showRunButton = true, + wrapContent = false, + closableTabs = false, + }, + ref + ) => { + const { sandpack } = useSandpack(); + const { code, updateCode } = useActiveCode(); + const { activePath, status, editorState } = sandpack; + const shouldShowTabs = showTabs ?? sandpack.openPaths.length > 1; - const c = useClasser("sp"); + const c = useClasser("sp"); - const handleCodeUpdate = (newCode: string) => { - updateCode(newCode); - }; + const handleCodeUpdate = (newCode: string) => { + updateCode(newCode); + }; - return ( - - {shouldShowTabs ? : null} + return ( + + {shouldShowTabs ? : null} -
- +
+ - {showRunButton && status === "idle" ? : null} -
- - ); -}; + {showRunButton && status === "idle" ? : null} +
+
+ ); + } +); diff --git a/sandpack-react/src/components/CodeViewer/index.tsx b/sandpack-react/src/components/CodeViewer/index.tsx index 752a103e6..9bc511c0d 100644 --- a/sandpack-react/src/components/CodeViewer/index.tsx +++ b/sandpack-react/src/components/CodeViewer/index.tsx @@ -6,6 +6,7 @@ import { SandpackStack } from "../../common/Stack"; import { useActiveCode } from "../../hooks/useActiveCode"; import { useSandpack } from "../../hooks/useSandpack"; import { CodeEditor } from "../CodeEditor"; +import type { CodeEditorRef } from "../CodeEditor"; import type { Decorators } from "../CodeEditor/CodeMirror"; import { FileTabs } from "../FileTabs"; @@ -16,7 +17,7 @@ export interface CodeViewerProps { code?: string; } -export const SandpackCodeViewer = forwardRef( +export const SandpackCodeViewer = forwardRef( ({ showTabs, showLineNumbers, decorators, code: propCode }, ref) => { const { sandpack } = useSandpack(); const { code } = useActiveCode(); diff --git a/sandpack-react/src/presets/CustomSandpack.stories.tsx b/sandpack-react/src/presets/CustomSandpack.stories.tsx index 65454e997..23a5b7755 100644 --- a/sandpack-react/src/presets/CustomSandpack.stories.tsx +++ b/sandpack-react/src/presets/CustomSandpack.stories.tsx @@ -18,7 +18,6 @@ import { useSandpackNavigation, SandpackStack, } from "../index"; -import { getThemeStyleSheet } from "../themes"; export default { title: "presets/Custom Sandpack", diff --git a/website/docs/src/CustomSandpack.tsx b/website/docs/src/CustomSandpack.tsx index 303b593d8..b75426c89 100644 --- a/website/docs/src/CustomSandpack.tsx +++ b/website/docs/src/CustomSandpack.tsx @@ -1,4 +1,3 @@ -import type { SandpackTheme } from "@codesandbox/sandpack-react"; import { Sandpack as SandpackDefault, SandpackLayout as SandpackLayoutDefault, diff --git a/website/landing/components/Intro/Examples.tsx b/website/landing/components/Intro/Examples.tsx new file mode 100644 index 000000000..eeff6d054 --- /dev/null +++ b/website/landing/components/Intro/Examples.tsx @@ -0,0 +1,176 @@ +import { + ClasserProvider, + SandpackLayout, + SandpackPreview, + SandpackProvider, + SandpackThemeProvider, +} from "@codesandbox/sandpack-react"; +import { motion, useTransform, useViewportScroll } from "framer-motion"; +import { useLayoutEffect, useRef, useState } from "react"; + +import { Box, List } from "../common"; +import { useBreakpoint } from "../common/useBreakpoint"; + +import { SandpackExample } from "./SandpackExample"; +import { CustomExample } from "./Sections/Custom"; +import { EditorExample } from "./Sections/Editor"; +import { LayoutExample } from "./Sections/Layout"; +import { useLayoutExampleContext } from "./Sections/LayoutContext"; +import { TemplateExample } from "./Sections/Template"; +import { ThemeExample } from "./Sections/Theme"; + +export const Examples: React.FC = () => { + const { layoutFiles, visibility } = useLayoutExampleContext(); + + const sandpackSection = useRef(null); + const { scrollY } = useViewportScroll(); + const isLarge = useBreakpoint("2260"); + + const [sandpackSectionTop, setSandpackSectionTop] = useState(0); + const [sandpackSectionHeight, setSandpackSectionHeight] = useState(0); + + useLayoutEffect(() => { + if (!sandpackSection.current) return; + + const onResize = () => { + if (!sandpackSection.current) return; + + setSandpackSectionTop(sandpackSection.current?.offsetTop); + setSandpackSectionHeight(sandpackSection.current?.offsetHeight); + }; + + onResize(); + window.addEventListener("resize", onResize); + + return () => window.removeEventListener("resize", onResize); + }, [sandpackSection]); + + const scrollRangeX = [ + sandpackSectionTop * 0.7, + (sandpackSectionTop + sandpackSectionHeight) * 0.8, + ]; + + const progressRangeX = ["0vw", isLarge ? "0vw" : "30vw"]; + const x = useTransform(scrollY, scrollRangeX, progressRangeX); + + return ( + <> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +}; diff --git a/website/landing/components/Intro/Header.tsx b/website/landing/components/Intro/Header.tsx new file mode 100644 index 000000000..1f7411b0c --- /dev/null +++ b/website/landing/components/Intro/Header.tsx @@ -0,0 +1,50 @@ +import content from "../../website.config.json"; +import { Box, CodeBlock, Text } from "../common"; + +export const Header: React.FC = () => { + const { commands, intro } = content; + + return ( + + + + {commands.import} + + + ); +}; diff --git a/website/landing/components/Intro/Intro.tsx b/website/landing/components/Intro/Intro.tsx index 2f436a5e8..4944e1f8c 100644 --- a/website/landing/components/Intro/Intro.tsx +++ b/website/landing/components/Intro/Intro.tsx @@ -1,156 +1,28 @@ -import content from "../../website.config.json"; -import { Box, CodeBlock, List, SandpackPreview, Text } from "../common"; +import { Box } from "../common"; -export const Intro: React.FC = () => { - const { intro, commands } = content; +import { Examples } from "./Examples"; +import { Header } from "./Header"; +import { SandpackExampleProvider } from "./SandpackExample"; +import { LayoutExampleProvider } from "./Sections/LayoutContext"; +export const Intro: React.FC = () => { return ( - + - - - {commands.import} - +
+ + + - - - - - {intro.highlights.map((h, index) => ( - - - - {h.title} - - - {h.snippet && {h.snippet}} - - - - ))} - - + ); }; diff --git a/website/landing/components/Intro/SandpackExample.tsx b/website/landing/components/Intro/SandpackExample.tsx new file mode 100644 index 000000000..5b23be9de --- /dev/null +++ b/website/landing/components/Intro/SandpackExample.tsx @@ -0,0 +1,47 @@ +import { createContext, useContext, useState } from "react"; + +import { SandpackPreview } from "../common"; + +interface Context { + setOptions: (opts: Record) => void; + options: Record; +} + +const SandpackExampleContext = createContext({ + setOptions: () => { + return; + }, + options: {}, +}); + +export const SandpackExampleProvider: React.FC = ({ children }) => { + const [options, setOptions] = useState({ + options: { + showNavigator: true, + showLineNumbers: true, + showTabs: true, + closableTabs: true, + }, + }); + + return ( + + setOptions((prev) => ({ ...prev, ...payload })), + options, + }} + > + {children} + + ); +}; + +export const useSandpackExample = (): Context => + useContext(SandpackExampleContext); + +export const SandpackExample: React.FC = () => { + const { options } = useSandpackExample(); + + return ; +}; diff --git a/website/landing/components/Intro/Sections/Custom.tsx b/website/landing/components/Intro/Sections/Custom.tsx new file mode 100644 index 000000000..ef2de794b --- /dev/null +++ b/website/landing/components/Intro/Sections/Custom.tsx @@ -0,0 +1,167 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import type { CodeEditorRef } from "@codesandbox/sandpack-react"; +import { + useSandpack, + useActiveCode, + SandpackThemeProvider, + SandpackCodeEditor, +} from "@codesandbox/sandpack-react"; +import { useEffect, useRef } from "react"; +import { useInView } from "react-intersection-observer"; + +import { Box, SandpackPreview } from "../../common"; +import { useBreakpoint } from "../../common/useBreakpoint"; +import { useSandpackExample } from "../SandpackExample"; + +import { + Wrapper, + Title, + Description, + Container, + SeeMoreLink, + RefreshButton, + SandpackContainerPlaceholder, + SandpackContainerMobile, + FadeAnimation, +} from "./common"; + +const ORIGINAL_CODE = `# Hello, *world*! +}\` + } + }} +/>; +`; + +export const CustomExample: React.FC = () => { + const { ref, inView } = useInView({ threshold: 0.5 }); + const { setOptions } = useSandpackExample(); + const codeEditorRef = useRef(null); + const higherMobile = useBreakpoint("bp1"); + + const { sandpack } = useSandpack(); + const { code } = useActiveCode(); + + useEffect( + function componentOnView() { + const position = 225; + const cmInstance = codeEditorRef.current?.getCodemirror(); + + if (higherMobile && inView && cmInstance && code.length > position) { + cmInstance.focus(); + const newState = cmInstance.state.update({ + selection: { anchor: position }, + }); + + if (newState) { + cmInstance.update([newState]); + } + } + }, + // Ignore code.length + // eslint-disable-next-line react-hooks/exhaustive-deps + [higherMobile, inView] + ); + + useEffect(function componentMount() { + sandpack.updateCurrentFile(ORIGINAL_CODE); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + useEffect(() => { + if (inView) { + try { + setOptions({ customSetup: parseFromSandpackToJson(code) }); + } catch (err) { + console.error(err); + } + } else { + setOptions({ customSetup: {} }); + } + }, [code, inView]); + + return ( + + + + Easily customise the project to run + + Use the customSetup prop to add dependencies or change + the file structure. + + + + + + + { + sandpack.updateCurrentFile(ORIGINAL_CODE); + }} + > + + + + + + + + + See more + + + + + + + + + + + ); +}; + +const parseFromSandpackToJson = (code: string) => { + const customSetup = code.match(/customSetup={{([\s\S]*?)}}/)?.[1]; + if (!customSetup) return; + + const fixString = `{${customSetup}}` + .replace(/(\w+:)|(\w+ :)/g, (matchedStr) => { + return '"' + matchedStr.substring(0, matchedStr.length - 1) + '":'; + }) + .replace(/`([\s\S]*?)`/gm, (matchedStr) => { + return `"${matchedStr.replace(/`/gm, "").replace(/\n/gm, "\\n")}"`; + }); + + return JSON.parse(fixString); +}; diff --git a/website/landing/components/Intro/Sections/Editor.tsx b/website/landing/components/Intro/Sections/Editor.tsx new file mode 100644 index 000000000..5566a3810 --- /dev/null +++ b/website/landing/components/Intro/Sections/Editor.tsx @@ -0,0 +1,158 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import type { CodeEditorRef } from "@codesandbox/sandpack-react"; +import { + useSandpack, + useActiveCode, + SandpackThemeProvider, + SandpackCodeEditor, +} from "@codesandbox/sandpack-react"; +import { useEffect, useState, useRef } from "react"; +import { useInView } from "react-intersection-observer"; + +import { Box, SandpackPreview } from "../../common"; +import { useBreakpoint } from "../../common/useBreakpoint"; +import { useSandpackExample } from "../SandpackExample"; + +import { + Wrapper, + Title, + Description, + Container, + SeeMoreLink, + RefreshButton, + SandpackContainerPlaceholder, + SandpackContainerMobile, + FadeAnimation, +} from "./common"; + +const ORIGINAL_CODE = ``; + +const ORIGINAL_CUSTOM = { + showNavigator: true, + showLineNumbers: true, + showTabs: true, + closableTabs: true, +}; + +export const EditorExample: React.FC = () => { + const { ref, inView } = useInView({ threshold: 0.5 }); + const { setOptions } = useSandpackExample(); + const [custom, setCustom] = useState(ORIGINAL_CUSTOM); + const codeEditorRef = useRef(null); + const higherMobile = useBreakpoint("bp1"); + + const { sandpack } = useSandpack(); + const { code } = useActiveCode(); + + useEffect(() => { + setOptions({ options: custom }); + }, [custom]); + + useEffect( + function componentOnView() { + const position = 43; + const cmInstance = codeEditorRef.current?.getCodemirror(); + + if (higherMobile && inView && cmInstance && code.length > position) { + cmInstance.focus(); + const newState = cmInstance.state.update({ + selection: { anchor: position }, + }); + + if (newState) { + cmInstance.update([newState]); + } + } + }, + // Ignore code.length + // eslint-disable-next-line react-hooks/exhaustive-deps + [inView, higherMobile] + ); + + useEffect(function componentMount() { + sandpack.updateCurrentFile(ORIGINAL_CODE); + }, []); + + useEffect( + function listener() { + const newCustomOptions: Record = {}; + + Object.keys(custom).map((key) => { + const value = code.match(new RegExp(`${key}:(.+)`)); + + newCustomOptions[key] = value?.[1]?.includes("true") ?? false; + }); + + setCustom((prev) => ({ ...prev, ...(newCustomOptions as any) })); + }, + [code] + ); + + return ( + + + + Configure your editor + + The options prop allows you to toggle on/off different + features of the code editor. + + + + + + + { + sandpack.updateCurrentFile(ORIGINAL_CODE); + setOptions(ORIGINAL_CUSTOM); + }} + > + + + + + + + + + See more + + + + + + + + + + + ); +}; diff --git a/website/landing/components/Intro/Sections/Layout.tsx b/website/landing/components/Intro/Sections/Layout.tsx new file mode 100644 index 000000000..35785e60d --- /dev/null +++ b/website/landing/components/Intro/Sections/Layout.tsx @@ -0,0 +1,119 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { + SandpackCodeEditor, + SandpackThemeProvider, + useActiveCode, + SandpackProvider, + ClasserProvider, + SandpackPreview, + SandpackLayout, + useSandpack, +} from "@codesandbox/sandpack-react"; +import { useEffect } from "react"; +import { useInView } from "react-intersection-observer"; + +import { Box } from "../../common"; + +import { useLayoutExampleContext } from "./LayoutContext"; +import { + Wrapper, + Title, + Description, + Container, + SeeMoreLink, + SandpackContainerPlaceholder, + SandpackContainerMobile, + FadeAnimation, +} from "./common"; + +export const LayoutExample: React.FC = () => { + const { ref, inView } = useInView({ threshold: 0.5 }); + + const { sandpack } = useSandpack(); + const { code } = useActiveCode(); + const { layoutFiles, setLayoutFiles, setVisibility } = + useLayoutExampleContext(); + + useEffect(() => { + setLayoutFiles(sandpack.activePath, code); + }, [code]); + + useEffect(() => { + Object.entries(layoutFiles).map(([filename, fileCode]) => { + sandpack.updateFile(filename, fileCode); + }); + }, []); + + useEffect(() => { + setVisibility(inView); + }, [inView]); + + return ( + + + + Build your own Sandpack + + If you want to fully customise the experience, you can build the UI + yourself. The library exports a set of composable components and + hooks that allow you to tailor the editing experience to your own + needs. + + + + + + + + + + See more + + + + + + + + + + + + + + + + + + ); +}; diff --git a/website/landing/components/Intro/Sections/LayoutContext.tsx b/website/landing/components/Intro/Sections/LayoutContext.tsx new file mode 100644 index 000000000..72eb20214 --- /dev/null +++ b/website/landing/components/Intro/Sections/LayoutContext.tsx @@ -0,0 +1,61 @@ +import { createContext, useContext, useState } from "react"; + +interface Context { + layoutFiles: Record; + setLayoutFiles: (filename: string, code: string) => void; + visibility: boolean; + setVisibility: (value: boolean) => void; +} + +const LayoutContext = createContext({ + layoutFiles: {}, + setLayoutFiles: () => { + return; + }, + visibility: false, + setVisibility: () => { + return; + }, +}); + +export const useLayoutExampleContext = (): Context => useContext(LayoutContext); + +const ORIGINAL_CODE = { + "/App.js": `import { + SandpackProvider, + SandpackThemeProvider, + SandpackCodeEditor, + SandpackTranspiledCode, +} from "@codesandbox/sandpack-react"; +import "@codesandbox/sandpack-react/dist/index.css"; + +export default () => ( + + + + + + +); + +// ✨ BTW, all examples are built in Sandpack`, +}; + +export const LayoutExampleProvider: React.FC = ({ children }) => { + const [layoutFiles, setLayoutFiles] = useState(ORIGINAL_CODE); + const [visibility, setVisibility] = useState(false); + + return ( + + setLayoutFiles((prev) => ({ ...prev, [filename]: code })), + visibility, + setVisibility, + }} + > + {children} + + ); +}; diff --git a/website/landing/components/Intro/Sections/Template.tsx b/website/landing/components/Intro/Sections/Template.tsx new file mode 100644 index 000000000..3d3b55419 --- /dev/null +++ b/website/landing/components/Intro/Sections/Template.tsx @@ -0,0 +1,123 @@ +import type { SandpackPredefinedTemplate } from "@codesandbox/sandpack-react"; +import { AnimatePresence } from "framer-motion"; +import { useEffect, useState, useRef } from "react"; +import { useInView } from "react-intersection-observer"; + +import { CodeBlock, SandpackPreview } from "../../common"; +import { useBreakpoint } from "../../common/useBreakpoint"; +import { useSandpackExample } from "../SandpackExample"; + +import { + Wrapper, + Title, + Description, + Container, + SeeMoreLink, + getRelativeCoordinates, + ToolTip, + SnippetButton, + SandpackContainerPlaceholder, + SandpackContainerMobile, + FadeAnimation, +} from "./common"; + +const frameworkOptions: SandpackPredefinedTemplate[] = [ + "react", + "react-ts", + "vue", + "vanilla", + "angular", +]; + +export const TemplateExample: React.FC = () => { + const { ref, inView } = useInView({ threshold: 0.5 }); + const { setOptions } = useSandpackExample(); + const [toolTipVisibility, setToolTipVisibility] = useState(false); + const [mousePosition, setMousePosition] = useState>( + {} + ); + const boxRef = useRef(null); + const [template, setTemplate] = useState(frameworkOptions[0]); + const higherMobile = useBreakpoint("bp1"); + + useEffect(() => { + setOptions({ template }); + }, [template]); + + useEffect(() => { + if (!higherMobile) { + setTemplate(frameworkOptions[2]); + + return; + } + + if (inView) { + setTemplate(frameworkOptions[2]); + } else { + setTemplate(frameworkOptions[0]); + } + }, [higherMobile, inView]); + + const shuffleTemplate = () => { + const currentIndex = frameworkOptions.indexOf(template); + + setTemplate(frameworkOptions[(currentIndex + 1) % frameworkOptions.length]); + }; + + return ( + + + + Get started in a few lines + + Set the template prop to get started with Sandpack in a + few lines of code. + + setToolTipVisibility(true)} + onMouseLeave={() => setToolTipVisibility(false)} + onMouseMove={(event) => + setMousePosition(getRelativeCoordinates(event, boxRef.current)) + } + > + {``} + + +
+ {toolTipVisibility && higherMobile && ( + + + click to change + + + )} +
+ + + See more + +
+ + + + + + +
+
+ ); +}; diff --git a/website/landing/components/Intro/Sections/Theme.tsx b/website/landing/components/Intro/Sections/Theme.tsx new file mode 100644 index 000000000..93f36beb1 --- /dev/null +++ b/website/landing/components/Intro/Sections/Theme.tsx @@ -0,0 +1,125 @@ +import type { SandpackPredefinedTheme } from "@codesandbox/sandpack-react"; +import { AnimatePresence } from "framer-motion"; +import { useEffect, useState, useRef } from "react"; +import { useInView } from "react-intersection-observer"; + +import { CodeBlock, SandpackPreview } from "../../common"; +import { useBreakpoint } from "../../common/useBreakpoint"; +import { useSandpackExample } from "../SandpackExample"; + +import { + Wrapper, + Title, + Description, + Container, + SeeMoreLink, + getRelativeCoordinates, + ToolTip, + SnippetButton, + SandpackContainerPlaceholder, + SandpackContainerMobile, + FadeAnimation, +} from "./common"; + +const themeOptions: SandpackPredefinedTheme[] = [ + "dark", + "light", + "sandpack-dark", + "github-light", + "monokai-pro", + "night-owl", + "aqua-blue", +]; + +export const ThemeExample: React.FC = () => { + const { ref, inView } = useInView({ threshold: 0.5 }); + const { setOptions } = useSandpackExample(); + const [toolTipVisibility, setToolTipVisibility] = useState(false); + const [mousePosition, setMousePosition] = useState>( + {} + ); + const boxRef = useRef(null); + const [theme, setTheme] = useState(themeOptions[0]); + const higherMobile = useBreakpoint("bp1"); + + useEffect(() => { + setOptions({ theme }); + }, [theme]); + + useEffect(() => { + if (!higherMobile) { + setTheme("light"); + + return; + } + + if (inView) { + setTheme("light"); + } else { + setTheme("sandpack-dark"); + } + }, [inView]); + + const shuffleTheme = () => { + const currentIndex = themeOptions.indexOf(theme); + + setTheme(themeOptions[(currentIndex + 1) % themeOptions.length]); + }; + + return ( + + + + Apply a theme + + Use the theme prop to set a predefined option made by + CodeSandbox. Try below, just click to change code and see all the + predefined options + + setToolTipVisibility(true)} + onMouseLeave={() => setToolTipVisibility(false)} + onMouseMove={(event) => + setMousePosition(getRelativeCoordinates(event, boxRef.current)) + } + > + {``} + + +
+ {toolTipVisibility && higherMobile && ( + + + click to change + + + )} +
+ + + See more + +
+ + + + + +
+
+ ); +}; diff --git a/website/landing/components/Intro/Sections/common.tsx b/website/landing/components/Intro/Sections/common.tsx new file mode 100644 index 000000000..fbba9e812 --- /dev/null +++ b/website/landing/components/Intro/Sections/common.tsx @@ -0,0 +1,249 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ +import { motion, useTransform, useViewportScroll } from "framer-motion"; +import { useState } from "react"; +import { useRef } from "react"; +import { useLayoutEffect } from "react"; +import { forwardRef } from "react"; + +import { styled } from "../../../stitches.config"; +import { Box, Text } from "../../common"; +import { useBreakpoint } from "../../common/useBreakpoint"; + +// eslint-disable-next-line react/display-name +export const Wrapper = forwardRef( + ({ children }, ref) => { + return ( + + {children} + + ); + } +); + +export const SandpackContainerPlaceholder = styled("div", { width: "50vw" }); + +export const SandpackContainerMobile = styled("div", { + "@bp2": { + display: "none", + }, +}); + +export const Container: React.FC = ({ children }) => { + return ( + + {children} + + ); +}; + +export const Title: React.FC = ({ children }) => { + return ( + + {children} + + ); +}; + +export const Description: React.FC = ({ children }) => { + return ( + + {children} + + ); +}; + +export const SeeMoreLink = styled("a", { + fontSize: "18px", + span: { + color: "$primary", + textDecoration: "underline", + "&:hover": { + textDecoration: "none", + }, + }, +}); + +export const getRelativeCoordinates = (event: any, referenceElement: any) => { + const position = { + x: event.pageX, + y: event.pageY, + }; + + const offset = { + left: referenceElement.offsetLeft, + top: referenceElement.offsetTop, + }; + + let reference = referenceElement.offsetParent; + + while (reference) { + offset.left += reference.offsetLeft; + offset.top += reference.offsetTop; + reference = reference.offsetParent; + } + + return { + x: position.x - offset.left + 15, + y: position.y - offset.top - 35, + }; +}; + +export const ToolTip = styled(motion.div, { + alignItems: "center", + background: "$primary", + borderRadius: "24px", + color: "$lightTextPrimary", + fontWeight: "$normal", + fontSize: "inherit", + lineHeight: "inherit", + letterSpacing: "inherit", + padding: "4px 12px", + display: "inline-block", +}); + +export const SnippetButton = styled("button", { + background: "none", + border: "none", + + ".sp-wrapper": { + cursor: "pointer", + userSelect: "none", + }, +}); + +export const RefreshButton = styled("button", { + background: "rgba(136, 136, 136, 0.2)", + border: "none", + color: "rgba(255,255,255, .5)", + borderRadius: "100%", + width: "24px", + height: "24px", + display: "flex", + padding: 0, + cursor: "pointer", + + position: "absolute", + bottom: "12px", + right: "10px", + + transition: ".2s ease all", + + "&:hover": { + color: "rgba(255,255,255, 1)", + }, + + svg: { + padding: "1px", + margin: "auto", + }, +}); + +export const FadeAnimation: React.FC = ({ children }) => { + const sectionRef = useRef(null); + const [sectionTop, setSectionTop] = useState(0); + const [sectionHeight, setSectionHeight] = useState(0); + + const isDesktop = useBreakpoint("bp2"); + + useLayoutEffect(() => { + const sectionEl = sectionRef.current; + if (!sectionEl) return; + + const onResize = () => { + setSectionTop(sectionEl.offsetTop); + setSectionHeight(sectionEl.offsetHeight); + }; + + onResize(); + window.addEventListener("resize", onResize); + return () => window.removeEventListener("resize", onResize); + }, [sectionRef]); + + const { scrollY } = useViewportScroll(); + const opacity = useTransform( + scrollY, + [ + sectionTop - sectionHeight / 4, + sectionTop - sectionHeight / 6, + sectionTop + sectionHeight / 6, + sectionTop + sectionHeight / 4, + ], + [0, 1, 1, 0] + ); + + return ( + + {children} + + ); +}; diff --git a/website/landing/components/common/Clipboard.tsx b/website/landing/components/common/Clipboard.tsx index 8c4bb5dc8..74843b2f8 100644 --- a/website/landing/components/common/Clipboard.tsx +++ b/website/landing/components/common/Clipboard.tsx @@ -1,8 +1,9 @@ +import { useCallback, useEffect, useState } from "react"; + +import { styled } from "../../stitches.config"; import content from "../../website.config.json"; import { Box, Button, Text } from "."; -import { styled } from "../../stitches.config"; -import { useCallback, useEffect, useState } from "react"; const command = content.commands.install; @@ -75,7 +76,7 @@ export const Clipboard: React.FC = () => { }, }} > - + = ({ files: { "index.ts": (children as string)?.trim() }, }} > - + diff --git a/website/landing/components/common/Resources.tsx b/website/landing/components/common/Resources.tsx index 89d9338b7..a32c85106 100644 --- a/website/landing/components/common/Resources.tsx +++ b/website/landing/components/common/Resources.tsx @@ -1,7 +1,7 @@ +import { styled } from "../../stitches.config"; import content from "../../website.config.json"; import { List, Text } from "."; -import { styled } from "../../stitches.config"; const ResourceLink = styled("a", { color: "inherit", diff --git a/website/landing/components/common/SandpackPreview.tsx b/website/landing/components/common/SandpackPreview.tsx index 49c56a2d3..0c09788b0 100644 --- a/website/landing/components/common/SandpackPreview.tsx +++ b/website/landing/components/common/SandpackPreview.tsx @@ -1,8 +1,11 @@ +import type { SandpackProps } from "@codesandbox/sandpack-react"; import { Sandpack } from "@codesandbox/sandpack-react"; import { Box } from "./Box"; -export const SandpackPreview: React.FC = () => { +export const SandpackPreview: React.FC<{ options?: SandpackProps }> = ({ + options, +}) => { return ( { overflow: "hidden", width: "100%", + ".custom-wrapper": { + "--sp-border-radius": "10px", + }, + ".custom-layout": { width: "342px", height: "512px", + border: 0, "@bp1": { width: "384px", @@ -24,11 +32,6 @@ export const SandpackPreview: React.FC = () => { height: "448px", width: "996px", }, - - "@bp3": { - height: "664px", - width: "1328px", - }, }, ".custom-stack": { @@ -40,14 +43,17 @@ export const SandpackPreview: React.FC = () => { }} > ); diff --git a/website/landing/components/common/useBreakpoint.ts b/website/landing/components/common/useBreakpoint.ts new file mode 100644 index 000000000..2662422b4 --- /dev/null +++ b/website/landing/components/common/useBreakpoint.ts @@ -0,0 +1,29 @@ +import { useCallback, useEffect, useState } from "react"; + +import { media } from "../../stitches.config"; + +export const useBreakpoint = ( + breakpoint: keyof typeof media | string +): boolean => { + const [value, setValue] = useState(true); + + const checkBreakpoint = useCallback(() => { + const query = + breakpoint in media + ? media[breakpoint as keyof typeof media] + : `(min-width: ${breakpoint}px)`; + + setValue(window.matchMedia(query).matches); + }, [breakpoint]); + + useEffect(() => { + checkBreakpoint(); + + window.addEventListener("resize", checkBreakpoint); + return () => { + window.removeEventListener("resize", checkBreakpoint); + }; + }, [checkBreakpoint]); + + return value; +}; diff --git a/website/landing/stitches.config.ts b/website/landing/stitches.config.ts index f74b99e63..130a622e5 100644 --- a/website/landing/stitches.config.ts +++ b/website/landing/stitches.config.ts @@ -10,12 +10,14 @@ const SCREEN_SIZES = { xl: 1920, }; +export const media = { + bp1: `(min-width: ${SCREEN_SIZES.md}px)`, + bp2: `(min-width: ${SCREEN_SIZES.lg}px)`, + bp3: `(min-width: ${SCREEN_SIZES.xl}px)`, +}; + export const { globalCss, getCssText, styled } = createStitches({ - media: { - bp1: `(min-width: ${SCREEN_SIZES.md}px)`, - bp2: `(min-width: ${SCREEN_SIZES.lg}px)`, - bp3: `(min-width: ${SCREEN_SIZES.xl}px)`, - }, + media, theme: { colors: palette, fontWeights, diff --git a/website/landing/styles/globalStyles.ts b/website/landing/styles/globalStyles.ts index 5f5ae446b..9bb3f07a4 100644 --- a/website/landing/styles/globalStyles.ts +++ b/website/landing/styles/globalStyles.ts @@ -33,6 +33,7 @@ export const globalStyles = globalCss({ fontSize: "10px", fontFamily: "$base", margin: 0, + overflowX: "hidden", }, "a, a:visited": { diff --git a/website/landing/website.config.json b/website/landing/website.config.json index a992abe72..a2920e3c6 100644 --- a/website/landing/website.config.json +++ b/website/landing/website.config.json @@ -113,31 +113,7 @@ ] }, "intro": { - "title": "Live coding in
the browser.
", - "highlights": [ - { - "title": "Start with preset templates", - "description": "Use the template prop to choose an option made by CodeSandbox: react, angular, vanilla." - }, - { - "title": "Customize your setup", - "description": "If you want to add dependencies or you need a different file structure, you can use the customSetup prop." - }, - { - "title": "Configure your editor", - "description": "The options prop allows you to toggle on/off different features of the code editor.", - "snippet": "" - }, - { - "title": "Customize your setup", - "description": "Use the theme prop to set a predefined option made by CodeSandbox. Try below, just click to change code and see all the predefined options. (mention the user can create its own theme)." - }, - { - "title": "Build your own Sandpack", - "description": "The library exports a set of composable components and hooks that allow you to tailor the editing experience to your own needs.", - "snippet": "\n \n \n \n \n" - } - ] + "title": "Live coding in
the browser.
" }, "showCase": { "title": "Showcase",