diff --git a/static/.dockerignore b/static/.dockerignore new file mode 100644 index 00000000..93090764 --- /dev/null +++ b/static/.dockerignore @@ -0,0 +1,3 @@ +node_modules +public +.cache diff --git a/static/.editorconfig b/static/.editorconfig new file mode 100644 index 00000000..1923d410 --- /dev/null +++ b/static/.editorconfig @@ -0,0 +1,8 @@ +root = true + +[*] +indent_style = space +indent_size = 2 +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true diff --git a/static/.eslintrc.json b/static/.eslintrc.json new file mode 100755 index 00000000..7bb6769f --- /dev/null +++ b/static/.eslintrc.json @@ -0,0 +1,48 @@ +{ + "extends": [ + "eslint:recommended", + "plugin:import/errors", + "plugin:react/recommended", + "plugin:jsx-a11y/recommended", + "prettier", + "prettier/react" + ], + "plugins": ["react", "import", "jsx-a11y"], + "settings": { + "react": { + "version": "detect" + } + }, + "rules": { + "react/prop-types": 0, + "react/react-in-jsx-scope": "off", + "lines-between-class-members": ["error", "always"], + "padding-line-between-statements": [ + "error", + { "blankLine": "always", "prev": ["const", "let", "var"], "next": "*" }, + { + "blankLine": "always", + "prev": ["const", "let", "var"], + "next": ["const", "let", "var"] + }, + { "blankLine": "always", "prev": "directive", "next": "*" }, + { "blankLine": "any", "prev": "directive", "next": "directive" } + ] + }, + "parser": "babel-eslint", + "parserOptions": { + "ecmaVersion": 10, + "sourceType": "module", + "ecmaFeatures": { + "jsx": true + } + }, + "env": { + "es6": true, + "browser": true, + "node": true + }, + "globals": { + "graphql": false + } +} diff --git a/static/.gitignore b/static/.gitignore new file mode 100755 index 00000000..577571c4 --- /dev/null +++ b/static/.gitignore @@ -0,0 +1,7 @@ +public +.cache +node_modules +*DS_Store +*.env + +.idea/ diff --git a/static/.prettierrc b/static/.prettierrc new file mode 100644 index 00000000..48bb2e78 --- /dev/null +++ b/static/.prettierrc @@ -0,0 +1,17 @@ +{ + "printWidth": 100, + "jsxBracketSameLine": false, + "singleQuote": true, + "tabWidth": 2, + "trailingComma": "es5", + "semi": false, + "overrides": [ + { + "files": ["*.md", "*.mdx"], + "options": { + "printWidth": 80, + "proseWrap": "always" + } + } + ] +} diff --git a/static/Dockerfile b/static/Dockerfile new file mode 100644 index 00000000..a6595a7d --- /dev/null +++ b/static/Dockerfile @@ -0,0 +1,22 @@ +FROM node:buster + +# Create app directory +WORKDIR /app + +# Install app dependencies +# RUN npm -g install serve +RUN npm -g install gatsby-cli + +COPY package*.json ./ + +RUN npm ci + +# Bundle app source +COPY . . + +# Build static files +RUN npm run build + +# serve on port 8080 +# CMD ["serve", "-l", "tcp://0.0.0.0:8080", "public"] +CMD ["gatsby", "serve", "--verbose", "--prefix-paths", "-p", "8080", "--host", "0.0.0.0"] diff --git a/static/LICENSE b/static/LICENSE new file mode 100644 index 00000000..dcd6c260 --- /dev/null +++ b/static/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 Hasura + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/static/README.md b/static/README.md new file mode 100755 index 00000000..4a75cde4 --- /dev/null +++ b/static/README.md @@ -0,0 +1,146 @@ +# gatsby-gitbook-starter + +Kick off your project with this starter to create a powerful/flexible docs/tutorial web apps. + +![gatsby-gitbook-starter](https://graphql-engine-cdn.hasura.io/learn-hasura/gatsby-gitbook-starter/assets/documentation_app_blog.png) + +## Motivation + +We wanted to create a [GraphQL tutorial](https://learn.hasura.io) series. The content would be written by developers for various languages/frameworks and what better than writing it in Markdown! And since this is a tutorial series we also needed rich embeds, syntax highlighting and more customisations. + +We also wanted to serve these tutorials in sub paths of [learn.hasura.io](https://learn.hasura.io). To serve all these requirements, we decided to use Gatsby + MDX (Markdown + JSX) to extend markdown and used a neat consistent theme like the one at [GitBook](https://www.gitbook.com) and deployed as docker containers. + +## 馃敟 Features +- Write using Markdown / [MDX](https://github.com/mdx-js/mdx) +- GitBook style theme +- Syntax Highlighting using Prism [`Bonus`: Code diff highlighting] +- Search Integration with Algolia +- Progressive Web App, Works Offline +- Google Analytics Integration +- Automatically generated sidebar navigation, table of contents, previous/next +- Dark Mode toggle +- Edit on Github +- Fully customisable +- Rich embeds and live code editor using MDX +- Easy deployment: Deploy on Netlify / Now.sh / Docker + +## 馃敆 Live Demo + +Here's a [live demo](https://learn.hasura.io/graphql/react) + +## 馃殌 Quickstart + +Get started by running the following commands: + +``` +$ git clone git@github.com:hasura/gatsby-gitbook-starter.git +$ cd gatsby-gitbook-starter +$ npm install +$ npm start +``` + +Visit `http://localhost:8000/` to view the app. + +## 馃敡 Configure + +Write markdown files in `content` folder. + +Open `config.js` for templating variables. Broadly configuration is available for `gatsby`, `header`, `sidebar` and `siteMetadata`. + +- `gatsby` config for global configuration like + - `pathPrefix` - Gatsby Path Prefix + - `siteUrl` - Gatsby Site URL + - `gaTrackingId` - Google Analytics Tracking ID + +- `header` config for site header configuration like + - `title` - The title that appears on the top left + - `githubUrl` - The Github URL for the docs website + - `helpUrl` - Help URL for pointing to resources + - `tweetText` - Tweet text + - `links` - Links on the top right + - `search` - Enable search and [configure Algolia](https://www.gatsbyjs.org/docs/adding-search-with-algolia/) + +- `sidebar` config for navigation links configuration + - `forcedNavOrder` for left sidebar navigation order. It should be in the format "/\" + - `frontLine` - whether to show a front line at the beginning of a nested menu.(Collapsing capability would be turned of if this option is set to true) + - `links` - Links on the bottom left of the sidebar + - `ignoreIndex` - Set this to true if the index.md file shouldn't appear on the left sidebar navigation. Typically this can be used for landing pages. + +- `siteMetadata` config for website related configuration + - `title` - Title of the website + - `description` - Description of the website + - `ogImage` - Social Media share og:image tag + - `docsLocation` - The Github URL for Edit on Github + +- For sub nesting in left sidebar, create a folder with the same name as the top level `.md` filename and the sub navigation is auto-generated. The sub navigation is alphabetically ordered. + +### Algolia Configuration + +To setup Algolia, go to `config.js` and update the `search` object to look like the one below: + +```..., + "search": { + "enabled": true, + "indexName": "MY_INDEX_NAME", + "algoliaAppId": process.env.GATSBY_ALGOLIA_APP_ID, + "algoliaSearchKey": process.env.GATSBY_ALGOLIA_SEARCH_KEY, + "algoliaAdminKey": process.env.ALGOLIA_ADMIN_KEY + }, +``` + +Values for Algolia App ID, Search Key, and Admin Key can be obtained from Algolia Dashboard with the right set of permissions. Replace `MY_INDEX_NAME` with the Algolia Index name of your choice. To build the Algolia index, you need to run `npm run build` which will do a gatsby build along with content indexing in Algolia. + +### Progressive Web App, Offline + +To enable PWA, go to `config.js` and update the `pwa` object to look like the one below: + +``` + "pwa": { + "enabled": false, // disabling this will also remove the existing service worker. + "manifest": { + "name": "Gatsby Gitbook Starter", + "short_name": "GitbookStarter", + "start_url": "/", + "background_color": "#6b37bf", + "theme_color": "#6b37bf", + "display": "standalone", + "crossOrigin": "use-credentials", + icons: [ + { + src: "src/pwa-512.png", + sizes: `512x512`, + type: `image/png`, + }, + ], + }, + } +``` + +## Live Code Editor + +To render react components for live editing, add the `react-live=true` to the code section. For example: + +```javascript react-live=true + +``` + +In the above code, just add `javascript react-live=true` after the triple quote ``` to start rendering react components that can be edited by users. + +## 馃 SEO friendly + +This is a static site and comes with all the SEO benefits. Configure meta tags like title and description for each markdown file using MDX Frontmatter + +```markdown +--- +title: "Title of the page" +metaTitle: "Meta Title Tag for this page" +metaDescription: "Meta Description Tag for this page" +--- +``` + +Canonical URLs are generated automatically. + +## 鈽侊笍 Deploy + +[![Deploy to Netlify](https://www.netlify.com/img/deploy/button.svg)](https://app.netlify.com/start/deploy?repository=https://github.com/hasura/gatsby-gitbook-starter) + diff --git a/static/codesandbox-template/.codesandbox/workspace.json b/static/codesandbox-template/.codesandbox/workspace.json new file mode 100644 index 00000000..e7d06a2e --- /dev/null +++ b/static/codesandbox-template/.codesandbox/workspace.json @@ -0,0 +1,20 @@ +{ + "responsive-preview": { + "Mobile": [ + 320, + 675 + ], + "Tablet": [ + 1024, + 765 + ], + "Desktop": [ + 1400, + 800 + ], + "Desktop HD": [ + 1920, + 1080 + ] + } +} \ No newline at end of file diff --git a/static/codesandbox-template/.prettierrc b/static/codesandbox-template/.prettierrc new file mode 100644 index 00000000..e2142eb8 --- /dev/null +++ b/static/codesandbox-template/.prettierrc @@ -0,0 +1,11 @@ +{ + "printWidth": 80, + "tabWidth": 2, + "useTabs": false, + "semi": false, + "singleQuote": true, + "trailingComma": "none", + "bracketSpacing": true, + "jsxBracketSameLine": false, + "fluid": false +} diff --git a/static/codesandbox-template/package.json b/static/codesandbox-template/package.json new file mode 100644 index 00000000..33ea1880 --- /dev/null +++ b/static/codesandbox-template/package.json @@ -0,0 +1,36 @@ +{ + "name": "react-babylonjs", + "version": "1.0.0", + "description": "", + "keywords": [], + "main": "src/index.tsx", + "dependencies": { + "@babylonjs/core": "4.2.1", + "@babylonjs/gui": "4.2.1", + "babylonjs-hook": "0.1.0", + "react": "17.0.2", + "react-babylonjs": "3.0.31", + "react-dom": "17.0.2", + "react-scripts": "4.0.3", + "type-fest": "2.9.0" + }, + "devDependencies": { + "prettier": "2.5.1", + "prettier-plugin-organize-imports": "2.3.4", + "@types/react": "17.0.20", + "@types/react-dom": "17.0.9", + "typescript": "4.4.2" + }, + "scripts": { + "start": "react-scripts start", + "build": "react-scripts build", + "test": "react-scripts test --env=jsdom", + "eject": "react-scripts eject" + }, + "browserslist": [ + ">0.2%", + "not dead", + "not ie <= 11", + "not op_mini all" + ] +} diff --git a/static/codesandbox-template/src/App.tsx b/static/codesandbox-template/src/App.tsx new file mode 100644 index 00000000..dbc5c594 --- /dev/null +++ b/static/codesandbox-template/src/App.tsx @@ -0,0 +1,10 @@ +import "./styles.css"; + +export default function App() { + return ( +
+

Hello CodeSandbox

+

Start editing to see some magic happen!

+
+ ); +} diff --git a/static/codesandbox-template/src/index.tsx b/static/codesandbox-template/src/index.tsx new file mode 100644 index 00000000..cf0d8f99 --- /dev/null +++ b/static/codesandbox-template/src/index.tsx @@ -0,0 +1,7 @@ +import { render } from "react-dom"; +import "./styles.css"; + +import App from "./App"; + +const rootElement = document.getElementById("root"); +render(, rootElement); diff --git a/static/codesandbox-template/src/styles.css b/static/codesandbox-template/src/styles.css new file mode 100644 index 00000000..80ab75f8 --- /dev/null +++ b/static/codesandbox-template/src/styles.css @@ -0,0 +1,11 @@ +canvas { + display: flex; + flex: 1; + width: 100%; + height: 100%; +} + +.App { + font-family: sans-serif; + text-align: center; +} diff --git a/static/codesandbox-template/tsconfig.json b/static/codesandbox-template/tsconfig.json new file mode 100644 index 00000000..73b7bdb5 --- /dev/null +++ b/static/codesandbox-template/tsconfig.json @@ -0,0 +1,14 @@ +{ + "include": [ + "./src/**/*" + ], + "compilerOptions": { + "strict": true, + "esModuleInterop": true, + "lib": [ + "dom", + "es2015" + ], + "jsx": "react-jsx" + } +} \ No newline at end of file diff --git a/static/config.js b/static/config.js new file mode 100644 index 00000000..5b5cb8d8 --- /dev/null +++ b/static/config.js @@ -0,0 +1,66 @@ +const config = { + gatsby: { + pathPrefix: '/', + siteUrl: 'https://brianzinn.github.io/react-babylonjs/', + gaTrackingId: null, + trailingSlash: false, + }, + header: { + logo: '', + logoLink: 'https://brianzinn.github.io/react-babylonjs/', + title: "react-babylonjs", + githubUrl: 'https://github.com/brianzinn/react-babylonjs', + helpUrl: '', + tweetText: '', + social: null, + links: [{ text: '', link: '' }], + search: { + enabled: false, + indexName: '', + algoliaAppId: process.env.GATSBY_ALGOLIA_APP_ID, + algoliaSearchKey: process.env.GATSBY_ALGOLIA_SEARCH_KEY, + algoliaAdminKey: process.env.ALGOLIA_ADMIN_KEY, + }, + }, + sidebar: { + forcedNavOrder: [ + '/introduction', // add trailing slash if enabled above + '/codeblock', + ], + collapsedNav: [ + '/codeblock', // add trailing slash if enabled above + ], + links: [{ text: 'react-babylonjs', link: 'https://github.com/brianzinn/react-babylonjs' }], + frontline: false, + ignoreIndex: true, + title: 'React for Babylon 3D engine', + }, + siteMetadata: { + title: 'Documentation | react-babylonjs', + description: 'React for Babylon 3D engine', + ogImage: null, + docsLocation: 'https://brianzinn.github.io/react-babylonjs/', + favicon: '', + }, + pwa: { + enabled: false, // disabling this will also remove the existing service worker. + manifest: { + name: 'react-babylonjs', + short_name: 'react-babylonjs', + start_url: '/', + background_color: '#6b37bf', + theme_color: '#6b37bf', + display: 'standalone', + crossOrigin: 'use-credentials', + icons: [ + { + src: 'src/pwa-512.png', + sizes: `512x512`, + type: `image/png`, + }, + ], + }, + }, +}; + +module.exports = config; diff --git a/static/content/1-introduction.md b/static/content/1-introduction.md new file mode 100644 index 00000000..505c50d5 --- /dev/null +++ b/static/content/1-introduction.md @@ -0,0 +1,37 @@ +--- +title: "Introduction" +metaTitle: "This is the title tag of this page" +metaDescription: "This is the meta description" +--- + +Some introduction text. Lists out all the headings from h1 to h6. Markdown link handling for relative and absolute URLs. Easy to customise. + +# Heading H1 +Heading 1 text + +## Heading H2 +Heading 2 text + +### Heading H3 +Heading 3 text + +#### Heading H4 +Heading 4 text + +##### Heading H5 +Heading 5 text + +###### Heading H6 +Heading 6 text + +## Lists +- Item 1 +- Item 2 +- Item 3 +- Item 4 +- Item 5 + +## Links + +* Relative: [Codeblock](/codeblock) +* Absolute: [Demo](https://learn.hasura.io/graphql/react) \ No newline at end of file diff --git a/static/content/2-hello-react-babylonjs.md b/static/content/2-hello-react-babylonjs.md new file mode 100644 index 00000000..973954b5 --- /dev/null +++ b/static/content/2-hello-react-babylonjs.md @@ -0,0 +1,7 @@ +--- +title: 'Hello react-babylonjs' +slug: 'hello-react-babylonjs' +--- + +These basic examples will get you started in react-babylonjs. If you are not +familiar with React or Babylon.js, this is a great place to begin. diff --git a/static/content/3-hello-react-babylonjs/1-react-boiler-plate.mdx b/static/content/3-hello-react-babylonjs/1-react-boiler-plate.mdx new file mode 100644 index 00000000..afd5cb72 --- /dev/null +++ b/static/content/3-hello-react-babylonjs/1-react-boiler-plate.mdx @@ -0,0 +1,29 @@ +--- +title: 'Creating a Boilerplate React Project' +slug: 'react-boiler-plate' +--- + +If you already know how to create a React project, you can skip this section. +Otherwise, let's go! + +```bash +npx create-react-app my-app +``` + +or with yarn: + +```bash +yarn create react-app my-app +``` + +Once you have a project, you'll find a setup that looks a little like this: + +```tsx codesandbox=rbjs?entry=./src/App.tsx +import { FC } from 'react' + +const App: FC = () => ( +
Hello, React!
+) + +export default App +``` diff --git a/static/content/3-hello-react-babylonjs/2-react-with-imperitive-babylonjs.mdx b/static/content/3-hello-react-babylonjs/2-react-with-imperitive-babylonjs.mdx new file mode 100644 index 00000000..f3b21649 --- /dev/null +++ b/static/content/3-hello-react-babylonjs/2-react-with-imperitive-babylonjs.mdx @@ -0,0 +1,232 @@ +--- +title: 'Plain Old React + Babylon.js' +slug: 'react-with-imperitive-babylonjs' +--- + +Once you have a React project set up, you can program Babylon.js imperitively +just like they tell you in the +[official Babylon.js + React guide](https://doc.babylonjs.com/extensions/Babylon.js+ExternalLibraries/BabylonJS_and_ReactJS). + +Wow, it takes **almost 150 lines of code** to make a box show up on a screen! +But do not be scared. We are showing you how much coding it actually takes so +you'll fully appreciate the next section. + +```tsx codesandbox=rbjs?entry=./src/App.tsx +import { + Engine, + EngineOptions, + FreeCamera, + HemisphericLight, + Mesh, + MeshBuilder, + Scene, + SceneOptions, + Vector3, +} from '@babylonjs/core' +import React, { FC, useEffect, useRef } from 'react' +import { SetRequired } from 'type-fest' +type OnSceneReadyHandler = (scene: Scene) => void + +type OnRenderHandler = (scene: Scene) => void + +// import SceneComponent from 'babylonjs-hook'; // if you install 'babylonjs-hook' NPM. + +type SceneComponentProps = { + canvasId: string + antialias?: boolean + engineOptions?: EngineOptions + adaptToDeviceRatio?: boolean + sceneOptions?: SceneOptions + onRender: OnRenderHandler + onSceneReady: OnSceneReadyHandler +} + +type SceneComponentPropsIn = SetRequired< + Partial, + 'onRender' | 'onSceneReady' +> + +const SceneComponent: FC = (props) => { + const reactCanvas = useRef(null) + const _props: SceneComponentProps = { + canvasId: 'babylonjs-canvas', + ...props, + } + + const { + canvasId, + antialias, + engineOptions, + adaptToDeviceRatio, + sceneOptions, + onRender, + onSceneReady, + ...rest + } = _props + + useEffect(() => { + if (!reactCanvas.current) return + const engine = new Engine( + reactCanvas.current, + antialias, + engineOptions, + adaptToDeviceRatio + ) + const scene = new Scene(engine, sceneOptions) + if (scene.isReady()) { + onSceneReady(scene) + } else { + scene.onReadyObservable.addOnce(onSceneReady) + } + + engine.runRenderLoop(() => { + onRender(scene) + scene.render() + }) + + const resize = () => { + scene.getEngine().resize() + } + + if (window) { + window.addEventListener('resize', resize) + } + + return () => { + scene.getEngine().dispose() + + if (window) { + window.removeEventListener('resize', resize) + } + } + }, [ + antialias, + engineOptions, + adaptToDeviceRatio, + sceneOptions, + onRender, + onSceneReady, + ]) + + return +} + +let box: Mesh + +const onSceneReady: OnSceneReadyHandler = (scene) => { + // This creates and positions a free camera (non-mesh) + var camera = new FreeCamera('camera1', new Vector3(0, 5, -10), scene) + + // This targets the camera to scene origin + camera.setTarget(Vector3.Zero()) + + const canvas = scene.getEngine().getRenderingCanvas() + + // This attaches the camera to the canvas + camera.attachControl(canvas, true) + + // This creates a light, aiming 0,1,0 - to the sky (non-mesh) + var light = new HemisphericLight('light', new Vector3(0, 1, 0), scene) + + // Default intensity is 1. Let's dim the light a small amount + light.intensity = 0.7 + + // Our built-in 'box' shape. + box = MeshBuilder.CreateBox('box', { size: 2 }, scene) + + // Move the box upward 1/2 its height + box.position.y = 1 + + // Our built-in 'ground' shape. + MeshBuilder.CreateGround('ground', { width: 6, height: 6 }, scene) +} + +/** + * Will run on every frame render. We are spinning the box on y-axis. + */ +const onRender: OnRenderHandler = (scene) => { + if (box !== undefined) { + var deltaTimeInMillis = scene.getEngine().getDeltaTime() + + const rpm = 10 + box.rotation.y += (rpm / 60) * Math.PI * 2 * (deltaTimeInMillis / 1000) + } +} + +const App: FC = () => ( +
+ +
+) + +export default App +``` + +Now to be completely fair, Babylon.js has created a helpful +[babylonjs-hook](https://www.npmjs.com/package/babylonjs-hook) package which +replaces `SceneComponent`. If you use this, you can get down to about **60 lines +of code**. But next, I'll show you how to do it in **under 30**. + +```tsx codesandbox=rbjs?entry=./src/App.tsx +import { + FreeCamera, + HemisphericLight, + Mesh, + MeshBuilder, + Scene, + Vector3, +} from '@babylonjs/core' +import SceneComponent from 'babylonjs-hook' + +type OnSceneReadyHandler = (scene: Scene) => void + +type OnRenderHandler = (scene: Scene) => void + +let box: Mesh + +const onSceneReady: OnSceneReadyHandler = (scene) => { + // This creates and positions a free camera (non-mesh) + var camera = new FreeCamera('camera1', new Vector3(0, 5, -10), scene) + + // This targets the camera to scene origin + camera.setTarget(Vector3.Zero()) + + const canvas = scene.getEngine().getRenderingCanvas() + + // This attaches the camera to the canvas + camera.attachControl(canvas, true) + + // This creates a light, aiming 0,1,0 - to the sky (non-mesh) + var light = new HemisphericLight('light', new Vector3(0, 1, 0), scene) + + // Default intensity is 1. Let's dim the light a small amount + light.intensity = 0.7 + + // Our built-in 'box' shape. + box = MeshBuilder.CreateBox('box', { size: 2 }, scene) + + // Move the box upward 1/2 its height + box.position.y = 1 + + // Our built-in 'ground' shape. + MeshBuilder.CreateGround('ground', { width: 6, height: 6 }, scene) +} + +/** + * Will run on every frame render. We are spinning the box on y-axis. + */ +const onRender: OnRenderHandler = (scene) => { + if (box !== undefined) { + var deltaTimeInMillis = scene.getEngine().getDeltaTime() + + const rpm = 10 + box.rotation.y += (rpm / 60) * Math.PI * 2 * (deltaTimeInMillis / 1000) + } +} + +export default () => ( +
+ +
+) +``` diff --git a/static/content/3-hello-react-babylonjs/3-react-with-declarative-babylonjs.mdx b/static/content/3-hello-react-babylonjs/3-react-with-declarative-babylonjs.mdx new file mode 100644 index 00000000..a0ccf54c --- /dev/null +++ b/static/content/3-hello-react-babylonjs/3-react-with-declarative-babylonjs.mdx @@ -0,0 +1,69 @@ +--- +title: 'Under 50 Lines of Code: react-babylonjs' +slug: 'react-with-declarative-babylonjs' +--- + +In previous sections, we saw how to: + +- Create a boilerplate React app +- Instantiate a Babylon.js scene imperitively +- Shoehorn the above into React in about 150 lines of code, still not "reactive" + or declarative +- Reduce 150 lines of code to 60 by moving some of it into a 3rd party package, + but still not reactive or declarative + +But now, we'll see how to do the exact same thing in **just under 50 lines of +code using a true reactive/declarative approach**. + +We also learn how to make custom Babylon React components, and how to use some +helpful hooks like `useScene`, `useBeforeRender`. + +```tsx codesandbox=rbjs?entry=./src/App.tsx +import { Vector3 } from '@babylonjs/core' +import { FC, useRef } from 'react' +import { Engine, Scene, useBeforeRender, useScene } from 'react-babylonjs' + +const RPM = 5 + +const MovingBox: FC = (props) => { + // access Babylon Scene + const scene = useScene() + // access refs to Babylon objects in scene like DOM nodes + const boxRef = useRef(null) + + useBeforeRender(() => { + if (!boxRef.current?.rotation) return + if (!scene) return + const deltaTimeInMillis = scene.getEngine().getDeltaTime() + boxRef.current.rotation.y += + (RPM / 60) * Math.PI * 2 * (deltaTimeInMillis / 1000) + }) + + return ( + + ) +} + +const App: FC = () => ( +
+ + + + + + + + +
+) + +export default App +``` diff --git a/static/content/3-hello-react-babylonjs/4-adding-animation-and-color.mdx b/static/content/3-hello-react-babylonjs/4-adding-animation-and-color.mdx new file mode 100644 index 00000000..d0b85d38 --- /dev/null +++ b/static/content/3-hello-react-babylonjs/4-adding-animation-and-color.mdx @@ -0,0 +1,76 @@ +--- +title: 'Adding More Animation and Color' +slug: 'react-babylonjs-with-animation' +--- + +Let's take a look at how to spice up this 3D scene a bit. + +```tsx codesandbox=rbjs?entry=./src/App.tsx +import { Color3, Vector3 } from '@babylonjs/core' +import { FC, useRef } from 'react' +import { Engine, Scene, useBeforeRender, useScene } from 'react-babylonjs' + +const RPM = 5 + +type MovingBoxProps = { + rotationAxis: 'x' | 'y' | 'z' + position: Vector3 + color: Color3 +} + +const MovingBox: FC = (props) => { + // access Babylon Scene + const scene = useScene() + // access refs to Babylon objects in scene like DOM nodes + const boxRef = useRef(null) + + useBeforeRender(() => { + if (!boxRef.current?.rotation) return + if (!scene) return + const deltaTimeInMillis = scene.getEngine().getDeltaTime() + boxRef.current.rotation[props.rotationAxis] += + (RPM / 60) * Math.PI * 2 * (deltaTimeInMillis / 1000) + }) + + return ( + + + + ) +} + +const App: FC = () => ( +
+ + + + + + + + +
+) + +export default App +``` diff --git a/static/content/3-hello-react-babylonjs/5-todo.md b/static/content/3-hello-react-babylonjs/5-todo.md new file mode 100644 index 00000000..d664b915 --- /dev/null +++ b/static/content/3-hello-react-babylonjs/5-todo.md @@ -0,0 +1,5 @@ +--- +title: 'TODO' +--- + +XR demos diff --git a/static/content/index.mdx b/static/content/index.mdx new file mode 100644 index 00000000..310148c5 --- /dev/null +++ b/static/content/index.mdx @@ -0,0 +1,26 @@ +--- +title: 'react-babylonjs' +--- + +**react-babylonjs** integrates the Babylon.js real time 3D engine with React. + +`react-babylonjs` lets you build 3D and XR scenes and games using reactive programming, reusable components, and hooks. + +The Babylon.js API is mostly covered declaratively thanks to code generation and even custom props allow you to declaratively add shadows, physics, 3D models, attach 2D/3D UI to meshes, etc. + +Fully supports hooks. Full support for TypeScript with auto-completion on elements and compile time checks. Context API and hooks provide easy access to Scene/Engine/Canvas. + +[![NPM version](http://img.shields.io/npm/v/react-babylonjs.svg?style=flat-square)](https://www.npmjs.com/package/react-babylonjs) +[![NPM downloads](http://img.shields.io/npm/dm/react-babylonjs.svg?style=flat-square)](https://www.npmjs.com/package/react-babylonjs) + +# Quickstart + +``` +npx create-react-babylon-js-app MyFirstApp +``` + +or + +``` +yarn create react-babylon-js-app MyFirstApp +``` diff --git a/static/gatsby-browser.js b/static/gatsby-browser.js new file mode 100644 index 00000000..da1df678 --- /dev/null +++ b/static/gatsby-browser.js @@ -0,0 +1,9 @@ +export const onServiceWorkerUpdateReady = () => { + const answer = window.confirm( + `This tutorial has been updated. ` + + `Reload to display the latest version?` + ) + if (answer === true) { + window.location.reload() + } +} \ No newline at end of file diff --git a/static/gatsby-config.js b/static/gatsby-config.js new file mode 100755 index 00000000..708ccfdd --- /dev/null +++ b/static/gatsby-config.js @@ -0,0 +1,130 @@ +require('dotenv').config() +const queries = require('./src/utils/algolia') +const config = require('./config') +const plugins = [ + 'gatsby-plugin-sitemap', + 'gatsby-plugin-sharp', + { + resolve: `gatsby-plugin-layout`, + options: { + component: require.resolve(`./src/templates/docs.js`), + }, + }, + 'gatsby-plugin-emotion', + 'gatsby-plugin-react-helmet', + { + resolve: 'gatsby-source-filesystem', + options: { + name: 'docs', + path: `${__dirname}/content/`, + }, + }, + { + resolve: 'gatsby-plugin-mdx', + options: { + gatsbyRemarkPlugins: [ + { + resolve: 'gatsby-remark-images', + options: { + maxWidth: 1035, + sizeByPixelDensity: true, + }, + }, + { + resolve: 'gatsby-remark-copy-linked-files', + }, + { + resolve: 'remark-codesandbox/gatsby', + options: { + mode: 'iframe', + customTemplates: { + rbjs: { + extends: 'file:./codesandbox-template', + entry: './src/App.tsx', + }, + }, + }, + }, + ], + extensions: ['.mdx', '.md'], + }, + }, + { + resolve: `gatsby-plugin-gtag`, + options: { + // your google analytics tracking id + trackingId: config.gatsby.gaTrackingId, + // Puts tracking script in the head instead of the body + head: true, + // enable ip anonymization + anonymize: false, + }, + }, +] +// check and add algolia +if ( + config.header.search && + config.header.search.enabled && + config.header.search.algoliaAppId && + config.header.search.algoliaAdminKey +) { + plugins.push({ + resolve: `gatsby-plugin-algolia`, + options: { + appId: config.header.search.algoliaAppId, // algolia application id + apiKey: config.header.search.algoliaAdminKey, // algolia admin key to index + queries, + chunkSize: 10000, // default: 1000 + }, + }) +} +// check and add pwa functionality +if (config.pwa && config.pwa.enabled && config.pwa.manifest) { + plugins.push({ + resolve: `gatsby-plugin-manifest`, + options: { ...config.pwa.manifest }, + }) + plugins.push({ + resolve: 'gatsby-plugin-offline', + options: { + appendScript: require.resolve(`./src/custom-sw-code.js`), + }, + }) +} else { + plugins.push('gatsby-plugin-remove-serviceworker') +} + +// check and remove trailing slash +if (config.gatsby && !config.gatsby.trailingSlash) { + plugins.push('gatsby-plugin-remove-trailing-slashes') +} + +module.exports = { + pathPrefix: config.gatsby.pathPrefix, + siteMetadata: { + title: config.siteMetadata.title, + description: config.siteMetadata.description, + docsLocation: config.siteMetadata.docsLocation, + ogImage: config.siteMetadata.ogImage, + favicon: config.siteMetadata.favicon, + logo: { + link: config.header.logoLink ? config.header.logoLink : '/', + image: config.header.logo, + }, // backwards compatible + headerTitle: config.header.title, + githubUrl: config.header.githubUrl, + helpUrl: config.header.helpUrl, + tweetText: config.header.tweetText, + headerLinks: config.header.links, + siteUrl: config.gatsby.siteUrl, + }, + plugins: plugins, + flags: { + DEV_SSR: false, + FAST_DEV: false, // Enable all experiments aimed at improving develop server start time + PRESERVE_WEBPACK_CACHE: false, // (Umbrella Issue (https://gatsby.dev/cache-clearing-feedback)) 路 Use webpack's persistent caching and don't delete webpack's cache when changing gatsby-node.js & gatsby-config.js files. + PRESERVE_FILE_DOWNLOAD_CACHE: false, // (Umbrella Issue (https://gatsby.dev/cache-clearing-feedback)) 路 Don't delete the downloaded files cache when changing gatsby-node.js & gatsby-config.js files. + PARALLEL_SOURCING: false, // EXPERIMENTAL 路 (Umbrella Issue (https://gatsby.dev/parallel-sourcing-feedback)) 路 Run all source plugins at the same time instead of serially. For sites with multiple source plugins, this can speedup sourcing and transforming considerably. + FUNCTIONS: false, // EXPERIMENTAL 路 (Umbrella Issue (https://gatsby.dev/functions-feedback)) 路 Compile Serverless functions in your Gatsby project and write them to disk, ready to deploy to Gatsby Cloud + }, +} diff --git a/static/gatsby-node.js b/static/gatsby-node.js new file mode 100644 index 00000000..73834141 --- /dev/null +++ b/static/gatsby-node.js @@ -0,0 +1,109 @@ +const componentWithMDXScope = require('gatsby-plugin-mdx/component-with-mdx-scope'); + +const path = require('path'); + +const startCase = require('lodash.startcase'); + +const config = require('./config'); + +exports.createPages = ({ graphql, actions }) => { + const { createPage } = actions; + + return new Promise((resolve, reject) => { + resolve( + graphql( + ` + { + allMdx { + edges { + node { + fields { + id + } + tableOfContents + fields { + slug + } + } + } + } + } + ` + ).then(result => { + if (result.errors) { + console.log(result.errors); // eslint-disable-line no-console + reject(result.errors); + } + + // Create blog posts pages. + result.data.allMdx.edges.forEach(({ node }) => { + createPage({ + path: node.fields.slug ? node.fields.slug : '/', + component: path.resolve('./src/templates/docs.js'), + context: { + id: node.fields.id, + }, + }); + }); + }) + ); + }); +}; + +exports.onCreateWebpackConfig = ({ actions }) => { + actions.setWebpackConfig({ + resolve: { + modules: [path.resolve(__dirname, 'src'), 'node_modules'], + alias: { + $components: path.resolve(__dirname, 'src/components'), + buble: '@philpl/buble', // to reduce bundle size + }, + }, + }); +}; + +exports.onCreateBabelConfig = ({ actions }) => { + actions.setBabelPlugin({ + name: '@babel/plugin-proposal-export-default-from', + }); +}; + +exports.onCreateNode = ({ node, getNode, actions }) => { + const { createNodeField } = actions; + + if (node.internal.type === `Mdx`) { + const parent = getNode(node.parent); + + let value = parent.relativePath.replace(parent.ext, ''); + + if (value === 'index') { + value = ''; + } + + if (config.gatsby && config.gatsby.trailingSlash) { + createNodeField({ + name: `slug`, + node, + value: value === '' ? `/` : `/${value}/`, + }); + } else { + createNodeField({ + name: `slug`, + node, + value: `/${value}`, + }); + } + + createNodeField({ + name: 'id', + node, + value: node.id, + }); + + createNodeField({ + name: 'title', + node, + value: node.frontmatter.title || startCase(parent.name), + }); + } +}; diff --git a/static/package.json b/static/package.json new file mode 100755 index 00000000..62f6b39e --- /dev/null +++ b/static/package.json @@ -0,0 +1,84 @@ +{ + "name": "gatsby-gitbook-boilerplate", + "private": true, + "description": "Documentation, built with mdx", + "author": "Praveen (@praveenweb)", + "version": "0.0.1", + "dependencies": { + "@babel/plugin-proposal-export-default-from": "^7.12.13", + "@emotion/react": "^11.1.5", + "@emotion/styled": "^11.3.0", + "@emotion/styled-base": "^11.0.0", + "@mdx-js/loader": "^1.6.22", + "@mdx-js/mdx": "^1.6.22", + "@mdx-js/react": "^1.6.22", + "@philpl/buble": "^0.19.7", + "@playlyfe/gql": "^2.6.2", + "@styled-icons/fa-brands": "^10.26.0", + "@styled-icons/fa-solid": "^10.32.0", + "algoliasearch": "^4.9.1", + "babylonjs-hook": "^0.1.0", + "dotenv": "^8.5.1", + "emotion": "^11.0.0", + "emotion-server": "^11.0.0", + "gatsby": "^4.0.0", + "gatsby-link": "^4.0.0", + "gatsby-plugin-algolia": "^0.19.0", + "gatsby-plugin-emotion": "^7.0.0", + "gatsby-plugin-gtag": "^1.0.13", + "gatsby-plugin-layout": "^3.0.0", + "gatsby-plugin-manifest": "^4.0.0", + "gatsby-plugin-mdx": "^3.0.0", + "gatsby-plugin-offline": "^5.0.0", + "gatsby-plugin-react-helmet": "^5.0.0", + "gatsby-plugin-remove-serviceworker": "^1.0.0", + "gatsby-plugin-sharp": "^4.0.0", + "gatsby-plugin-sitemap": "^5.0.0", + "gatsby-remark-copy-linked-files": "^5.0.0", + "gatsby-remark-images": "^6.0.0", + "gatsby-source-filesystem": "^4.0.0", + "gatsby-transformer-remark": "^5.0.0", + "graphql": "^15.5.0", + "is-absolute-url": "^3.0.3", + "lodash.flatten": "^4.4.0", + "lodash.startcase": "^4.4.0", + "prismjs": "^1.23.0", + "react": "^17.0.2", + "react-babylonjs": "^3.0.31", + "react-dom": "^17.0.2", + "react-feather": "^2.0.9", + "react-github-btn": "^1.2.0", + "react-helmet": "^6.1.0", + "react-id-generator": "^3.0.1", + "react-instantsearch-dom": "^6.11.0", + "react-live": "^2.2.3", + "react-loadable": "^5.5.0", + "styled-components": "^5.3.0" + }, + "license": "MIT", + "main": "n/a", + "scripts": { + "start": "gatsby develop", + "build": "gatsby build --prefix-paths", + "format": "prettier --write \"src/**/*.{js,jsx,css,json}\"", + "lint": "eslint --fix \"src/**/*.{js,jsx}\"", + "develop": "gatsby develop", + "serve": "gatsby serve", + "clean": "gatsby clean", + "test": "echo \"Write tests! -> https://gatsby.dev/unit-testing\" && exit 1" + }, + "devDependencies": { + "babel-eslint": "^10.1.0", + "eslint": "^7.25.0", + "eslint-config-prettier": "^8.3.0", + "eslint-plugin-import": "^2.22.1", + "eslint-plugin-jsx-a11y": "^6.4.1", + "eslint-plugin-prettier": "^3.4.0", + "eslint-plugin-react": "^7.23.2", + "gatsby-plugin-remove-trailing-slashes": "^3.4.0", + "prettier": "^2.2.1", + "prism-react-renderer": "^1.2.0", + "remark-codesandbox": "^0.10.0", + "prettier-plugin-organize-imports": "^2.3.4" + } +} diff --git a/static/src/CommunityAuthor.js b/static/src/CommunityAuthor.js new file mode 100644 index 00000000..2f1622a4 --- /dev/null +++ b/static/src/CommunityAuthor.js @@ -0,0 +1,40 @@ +import * as React from 'react'; + +const CommunityAuthor = ({ name, imageUrl, twitterUrl, githubUrl, description }) => { + return ( + <> +

About the community author

+
+
+ {name} +
+
+
+ {name} + {twitterUrl ? ( + + Twitter Icon + + ) : null} + {githubUrl ? ( + + Github Icon + + ) : null} +
+
{description}
+
+
+ + ); +}; + +export default CommunityAuthor; diff --git a/static/src/GithubLink.js b/static/src/GithubLink.js new file mode 100644 index 00000000..760819dd --- /dev/null +++ b/static/src/GithubLink.js @@ -0,0 +1,13 @@ +import * as React from 'react'; +const githubIcon = require('./components/images/github.svg').default; + +const GithubLink = ({ link, text }) => { + return ( + + github + {text} + + ); +}; + +export default GithubLink; diff --git a/static/src/YoutubeEmbed.js b/static/src/YoutubeEmbed.js new file mode 100644 index 00000000..bcbeea5c --- /dev/null +++ b/static/src/YoutubeEmbed.js @@ -0,0 +1,19 @@ +import * as React from 'react'; + +const YoutubeEmbed = ({ link }) => { + return ( +
+ +
+ ); +}; + +export default YoutubeEmbed; diff --git a/static/src/components/DarkModeSwitch.js b/static/src/components/DarkModeSwitch.js new file mode 100644 index 00000000..ce646e13 --- /dev/null +++ b/static/src/components/DarkModeSwitch.js @@ -0,0 +1,94 @@ +import * as React from 'react'; +import styled from '@emotion/styled'; + +import NightImage from './images/night.png'; +import DayImage from './images/day.png'; + +const StyledSwitch = styled('div')` + display: flex; + justify-content: flex-end; + width: 100%; + padding: 0 20px 0 25px; + + /* The switch - the box around the slider */ + .switch { + position: relative; + display: inline-block; + width: 50px; + height: 20px; + } + + /* Hide default HTML checkbox */ + .switch input { + opacity: 0; + width: 0; + height: 0; + } + + /* The slider */ + .slider { + position: absolute; + cursor: pointer; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: #ccc; + -webkit-transition: 0.4s; + transition: 0.4s; + } + + .slider:before { + position: absolute; + content: ''; + height: 30px; + width: 30px; + left: 0px; + bottom: 4px; + top: 0; + bottom: 0; + margin: auto 0; + -webkit-transition: 0.4s; + transition: 0.4s; + box-shadow: 0 0px 15px #2020203d; + background: white url(${NightImage}); + background-repeat: no-repeat; + background-position: center; + } + + input:checked + .slider { + background: linear-gradient(to right, #fefb72, #f0bb31); + } + + input:checked + .slider:before { + -webkit-transform: translateX(24px); + -ms-transform: translateX(24px); + transform: translateX(24px); + background: white url(${DayImage}); + background-repeat: no-repeat; + background-position: center; + } + + /* Rounded sliders */ + .slider.round { + border-radius: 34px; + } + + .slider.round:before { + border-radius: 50%; + } +`; + +export const DarkModeSwitch = ({ isDarkThemeActive, toggleActiveTheme }) => ( + + + +); diff --git a/static/src/components/Header.js b/static/src/components/Header.js new file mode 100644 index 00000000..7671edf1 --- /dev/null +++ b/static/src/components/Header.js @@ -0,0 +1,219 @@ +import * as React from 'react'; +import styled from '@emotion/styled'; +import { StaticQuery, graphql } from 'gatsby'; +import GitHubButton from 'react-github-btn'; +import Link from './link'; +import Loadable from 'react-loadable'; + +import config from '../../config.js'; +import LoadingProvider from './mdxComponents/loading'; +import { DarkModeSwitch } from './DarkModeSwitch'; + +const help = require('./images/help.svg'); + +const isSearchEnabled = config.header.search && config.header.search.enabled ? true : false; + +let searchIndices = []; + +if (isSearchEnabled && config.header.search.indexName) { + searchIndices.push({ + name: `${config.header.search.indexName}`, + title: `Results`, + hitComp: `PageHit`, + }); +} + +import Sidebar from './sidebar'; + +const LoadableComponent = Loadable({ + loader: () => import('./search/index'), + loading: LoadingProvider, +}); + +function myFunction() { + var x = document.getElementById('navbar'); + + if (x.className === 'topnav') { + x.className += ' responsive'; + } else { + x.className = 'topnav'; + } +} + +const StyledBgDiv = styled('div')` + height: 60px; + box-shadow: 0 3px 6px 0 rgba(0, 0, 0, 0.16); + background-color: #f8f8f8; + position: relative; + display: none; + background: ${(props) => (props.isDarkThemeActive ? '#001932' : undefined)}; + + @media (max-width: 767px) { + display: block; + } +`; + +const Header = ({ location, isDarkThemeActive, toggleActiveTheme }) => ( + { + const logoImg = require('./images/logo.svg'); + + const twitter = require('./images/twitter.svg'); + + const discordBrandsBlock = require('./images/discord-brands-block.svg'); + + const twitterBrandsBlock = require('./images/twitter-brands-block.svg'); + + const { + site: { + siteMetadata: { headerTitle, githubUrl, helpUrl, tweetText, logo, headerLinks }, + }, + } = data; + + const finalLogoLink = logo.link !== '' ? logo.link : 'https://hasura.io/'; + + return ( +
+ + +
+ + + + + +
+ {isSearchEnabled ? ( +
+ +
+ ) : null} +
+
+ ); + }} + /> +); + +export default Header; diff --git a/static/src/components/NextPrevious.js b/static/src/components/NextPrevious.js new file mode 100644 index 00000000..facb1276 --- /dev/null +++ b/static/src/components/NextPrevious.js @@ -0,0 +1,120 @@ +import * as React from 'react'; +import Link from './link'; + +import { StyledNextPrevious } from './styles/PageNavigationButtons'; + +const NextPrevious = ({ mdx, nav }) => { + let currentIndex; + + const currentPaginationInfo = nav.map((el, index) => { + if (el && el.url === mdx.fields.slug) { + currentIndex = index; + } + }); + + const nextInfo = {}; + + const previousInfo = {}; + + if (currentIndex === undefined) { + // index + if (nav[0]) { + nextInfo.url = nav[0].url; + nextInfo.title = nav[0].title; + } + previousInfo.url = null; + previousInfo.title = null; + currentIndex = -1; + } else if (currentIndex === 0) { + // first page + nextInfo.url = nav[currentIndex + 1] ? nav[currentIndex + 1].url : null; + nextInfo.title = nav[currentIndex + 1] ? nav[currentIndex + 1].title : null; + previousInfo.url = null; + previousInfo.title = null; + } else if (currentIndex === nav.length - 1) { + // last page + nextInfo.url = null; + nextInfo.title = null; + previousInfo.url = nav[currentIndex - 1] ? nav[currentIndex - 1].url : null; + previousInfo.title = nav[currentIndex - 1] ? nav[currentIndex - 1].title : null; + } else if (currentIndex) { + // any other page + nextInfo.url = nav[currentIndex + 1].url; + nextInfo.title = nav[currentIndex + 1].title; + if (nav[currentIndex - 1]) { + previousInfo.url = nav[currentIndex - 1].url; + previousInfo.title = nav[currentIndex - 1].title; + } + } + + return ( + + {previousInfo.url && currentIndex >= 0 ? ( + +
+ + + + + + +
+
+
+ Previous +
+
+ {nav[currentIndex - 1].title} +
+
+ + ) : null} + {nextInfo.url && currentIndex >= 0 ? ( + +
+
+ Next +
+
+ {nav[currentIndex + 1] && nav[currentIndex + 1].title} +
+
+
+ + + + + + +
+ + ) : null} +
+ ); +}; + +export default NextPrevious; diff --git a/static/src/components/images/closed.js b/static/src/components/images/closed.js new file mode 100644 index 00000000..f5f456f1 --- /dev/null +++ b/static/src/components/images/closed.js @@ -0,0 +1,9 @@ +import * as React from 'react'; + +const ClosedSvg = () => ( + + + +); + +export default ClosedSvg; diff --git a/static/src/components/images/day.png b/static/src/components/images/day.png new file mode 100644 index 00000000..89eabae6 Binary files /dev/null and b/static/src/components/images/day.png differ diff --git a/static/src/components/images/discord-brands-block.svg b/static/src/components/images/discord-brands-block.svg new file mode 100644 index 00000000..e2f8380e --- /dev/null +++ b/static/src/components/images/discord-brands-block.svg @@ -0,0 +1,8 @@ + + + + + + diff --git a/static/src/components/images/github.svg b/static/src/components/images/github.svg new file mode 100644 index 00000000..a9a1bb96 --- /dev/null +++ b/static/src/components/images/github.svg @@ -0,0 +1,12 @@ + + + + + diff --git a/static/src/components/images/help.svg b/static/src/components/images/help.svg new file mode 100644 index 00000000..8d0ee1f1 --- /dev/null +++ b/static/src/components/images/help.svg @@ -0,0 +1,9 @@ + + + + + + + diff --git a/static/src/components/images/logo.svg b/static/src/components/images/logo.svg new file mode 100644 index 00000000..403d4121 --- /dev/null +++ b/static/src/components/images/logo.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + diff --git a/static/src/components/images/night.png b/static/src/components/images/night.png new file mode 100644 index 00000000..dc378383 Binary files /dev/null and b/static/src/components/images/night.png differ diff --git a/static/src/components/images/opened.js b/static/src/components/images/opened.js new file mode 100644 index 00000000..29dd7dc8 --- /dev/null +++ b/static/src/components/images/opened.js @@ -0,0 +1,9 @@ +import * as React from 'react'; + +const OpenedSvg = () => ( + + + +); + +export default OpenedSvg; diff --git a/static/src/components/images/twitter-brands-block.svg b/static/src/components/images/twitter-brands-block.svg new file mode 100644 index 00000000..f66a76b0 --- /dev/null +++ b/static/src/components/images/twitter-brands-block.svg @@ -0,0 +1,8 @@ + + + + + + diff --git a/static/src/components/images/twitter.svg b/static/src/components/images/twitter.svg new file mode 100644 index 00000000..54f369cd --- /dev/null +++ b/static/src/components/images/twitter.svg @@ -0,0 +1,14 @@ + + + + + + diff --git a/static/src/components/index.js b/static/src/components/index.js new file mode 100644 index 00000000..c531d5c1 --- /dev/null +++ b/static/src/components/index.js @@ -0,0 +1,7 @@ +export * from './theme'; +import mdxComponents from './mdxComponents'; +import ThemeProvider from './theme/themeProvider'; +import Layout from './layout'; +import Link from './link'; + +export {mdxComponents, ThemeProvider, Layout, Link} diff --git a/static/src/components/layout.js b/static/src/components/layout.js new file mode 100644 index 00000000..03117cb9 --- /dev/null +++ b/static/src/components/layout.js @@ -0,0 +1,89 @@ +import * as React from 'react'; +import styled from '@emotion/styled'; +import { MDXProvider } from '@mdx-js/react'; + +import ThemeProvider from './theme/themeProvider'; +import mdxComponents from './mdxComponents'; +import Sidebar from './sidebar'; +import RightSidebar from './rightSidebar'; +import config from '../../config.js'; + +const Wrapper = styled('div')` + display: flex; + justify-content: space-between; + background: ${({ theme }) => theme.colors.background}; + + .sideBarUL li a { + color: ${({ theme }) => theme.colors.text}; + } + + .sideBarUL .item > a:hover { + background-color: #1ed3c6; + color: #fff !important; + + /* background: #F8F8F8 */ + } + + @media only screen and (max-width: 767px) { + display: block; + } +`; + +const Content = styled('main')` + display: flex; + flex-grow: 1; + margin: 0px 88px; + padding-top: 3rem; + background: ${({ theme }) => theme.colors.background}; + + table tr { + background: ${({ theme }) => theme.colors.background}; + } + + @media only screen and (max-width: 1023px) { + padding-left: 0; + margin: 0 10px; + padding-top: 3rem; + } +`; + +const MaxWidth = styled('div')` + @media only screen and (max-width: 50rem) { + width: 100%; + position: relative; + } +`; + +const LeftSideBarWidth = styled('div')` + width: 298px; +`; + +const RightSideBarWidth = styled('div')` + width: 224px; +`; + +const Layout = ({ children, location }) => ( + + + + + + + {config.sidebar.title ? ( +
+ ) : null} + + {children} + + {/* + + */} + + + +); + +export default Layout; diff --git a/static/src/components/link.js b/static/src/components/link.js new file mode 100644 index 00000000..51e4f5b4 --- /dev/null +++ b/static/src/components/link.js @@ -0,0 +1,14 @@ +import * as React from 'react'; +import { Link as GatsbyLink } from 'gatsby'; +import isAbsoluteUrl from 'is-absolute-url'; + +const Link = ({ to, ...props }) => + isAbsoluteUrl(to) ? ( + + {props.children} + + ) : ( + + ); + +export default Link; diff --git a/static/src/components/mdxComponents/LiveProvider.js b/static/src/components/mdxComponents/LiveProvider.js new file mode 100644 index 00000000..bf99ff8a --- /dev/null +++ b/static/src/components/mdxComponents/LiveProvider.js @@ -0,0 +1,14 @@ +import * as React from 'react'; +import { LiveProvider, LiveEditor, LiveError, LivePreview } from 'react-live'; + +const ReactLiveProvider = ({ code }) => { + return ( + + + + + + ); +}; + +export default ReactLiveProvider; diff --git a/static/src/components/mdxComponents/ReactDemo.js b/static/src/components/mdxComponents/ReactDemo.js new file mode 100644 index 00000000..a043fc51 --- /dev/null +++ b/static/src/components/mdxComponents/ReactDemo.js @@ -0,0 +1,20 @@ +import React, { FC } from 'react' + +const ReactDemo = (props) => { + const { children } = props + return ( +
+ {children} +
+ ) +} + +export default ReactDemo diff --git a/static/src/components/mdxComponents/Sandbox.js b/static/src/components/mdxComponents/Sandbox.js new file mode 100644 index 00000000..30822929 --- /dev/null +++ b/static/src/components/mdxComponents/Sandbox.js @@ -0,0 +1,22 @@ +import React, { FC } from 'react' + +const Sandbox = (props) => { + const { name } = props + return ( + + ) +} + +export default Sandbox diff --git a/static/src/components/mdxComponents/Stats.tsx b/static/src/components/mdxComponents/Stats.tsx new file mode 100644 index 00000000..e7501509 --- /dev/null +++ b/static/src/components/mdxComponents/Stats.tsx @@ -0,0 +1,25 @@ +import React, { FC, useEffect, useState } from 'react' +import { useBeforeRender, useCanvas, useEngine } from 'react-babylonjs' + +const Stats = (props) => { + const engine = useEngine() + + useEffect(() => { + const update = () => { + console.log('callback', engine.getFps()) + const ctx = engine.getRenderingCanvas().getContext('2d') + if (ctx) { + ctx.font = '48px serif' + ctx.fillText('Hello world', 0, 0) + } + requestAnimationFrame(update) + } + requestAnimationFrame(update) + }) + + console.log('render') + + return null +} + +export default Stats diff --git a/static/src/components/mdxComponents/anchor.js b/static/src/components/mdxComponents/anchor.js new file mode 100644 index 00000000..0e0dd847 --- /dev/null +++ b/static/src/components/mdxComponents/anchor.js @@ -0,0 +1,25 @@ +import * as React from 'react'; +import { Link as GatsbyLink } from 'gatsby'; +import isAbsoluteUrl from 'is-absolute-url'; + +const AnchorTag = ({ children: link, ...props }) => { + if (link) { + if (isAbsoluteUrl(props.href)) { + return ( + + {link} + + ); + } else { + return ( + + {link} + + ); + } + } else { + return null; + } +}; + +export default AnchorTag; diff --git a/static/src/components/mdxComponents/codeBlock.js b/static/src/components/mdxComponents/codeBlock.js new file mode 100644 index 00000000..0c39ff44 --- /dev/null +++ b/static/src/components/mdxComponents/codeBlock.js @@ -0,0 +1,123 @@ +import * as React from 'react'; +import Highlight, { defaultProps, Prism } from 'prism-react-renderer'; +import { applyLanguages, getTheme } from '../../custom/config/codeBlockLanguages'; +import Loadable from 'react-loadable'; +import LoadingProvider from './loading'; + +const theme = getTheme(); + +/** Removes the last token from a code example if it's empty. */ +function cleanTokens(tokens) { + const tokensLength = tokens.length; + + if (tokensLength === 0) { + return tokens; + } + const lastToken = tokens[tokensLength - 1]; + + if (lastToken.length === 1 && lastToken[0].empty) { + return tokens.slice(0, tokensLength - 1); + } + return tokens; +} + +const LoadableComponent = Loadable({ + loader: () => import('./LiveProvider'), + loading: LoadingProvider, +}); + +/* eslint-disable react/jsx-key */ +const CodeBlock = ({ children: exampleCode, ...props }) => { + const [_, updateView] = React.useState(0); + + React.useEffect(() => { + var windowPrism = window.Prism; + window.Prism = Prism; + applyLanguages(Prism); + window.Prism = windowPrism; + updateView({ + data: Date.now() + }); + }, []); + + if (props['react-live']) { + return ; + } else { + return ( + + {({ className, style, tokens, getLineProps, getTokenProps }) => ( +
+            {cleanTokens(tokens).map((line, i) => {
+              let lineClass = {};
+
+              let isDiff = false;
+
+              if (line[0] && line[0].content.length && line[0].content[0] === '+') {
+                lineClass = { backgroundColor: 'rgba(76, 175, 80, 0.2)' };
+                isDiff = true;
+              } else if (line[0] && line[0].content.length && line[0].content[0] === '-') {
+                lineClass = { backgroundColor: 'rgba(244, 67, 54, 0.2)' };
+                isDiff = true;
+              } else if (line[0] && line[0].content === '' && line[1] && line[1].content === '+') {
+                lineClass = { backgroundColor: 'rgba(76, 175, 80, 0.2)' };
+                isDiff = true;
+              } else if (line[0] && line[0].content === '' && line[1] && line[1].content === '-') {
+                lineClass = { backgroundColor: 'rgba(244, 67, 54, 0.2)' };
+                isDiff = true;
+              }
+              const lineProps = getLineProps({ line, key: i });
+
+              lineProps.style = lineClass;
+              const diffStyle = {
+                userSelect: 'none',
+                MozUserSelect: '-moz-none',
+                WebkitUserSelect: 'none',
+              };
+
+              let splitToken;
+
+              return (
+                
+ {line.map((token, key) => { + if (isDiff) { + if ( + (key === 0 || key === 1) & + (token.content.charAt(0) === '+' || token.content.charAt(0) === '-') + ) { + if (token.content.length > 1) { + splitToken = { + types: ['template-string', 'string'], + content: token.content.slice(1), + }; + const firstChar = { + types: ['operator'], + content: token.content.charAt(0), + }; + + return ( + + + + + ); + } else { + return ; + } + } + } + return ; + })} +
+ ); + })} +
+ )} +
+ ); + } +}; + +export default CodeBlock; diff --git a/static/src/components/mdxComponents/index.js b/static/src/components/mdxComponents/index.js new file mode 100644 index 00000000..015b784b --- /dev/null +++ b/static/src/components/mdxComponents/index.js @@ -0,0 +1,90 @@ +import * as React from 'react' +import styled from '@emotion/styled' + +import CodeBlock from './codeBlock' +import AnchorTag from './anchor' +import Sandbox from './Sandbox' +import ReactDemo from './ReactDemo' +import Stats from './Stats' + +const StyledPre = styled('pre')` + padding: 16px; + background: ${(props) => props.theme.colors.preFormattedText}; +` + +const appendString = (children) => { + if (Array.isArray(children)) { + return children.reduce((acc, current) => { + if (typeof current === 'string') { + return acc.concat(current) + } else if (typeof current === 'object') { + return acc.concat(current.props.children) + } else { + return acc + } + }, '') + } else { + return children + } +} + +export default { + h1: (props) => ( +

+ ), + h2: (props) => ( +

+ ), + h3: (props) => ( +

+ ), + h4: (props) => ( +

+ ), + h5: (props) => ( +

+ ), + h6: (props) => ( +
+ ), + p: (props) =>

, + pre: (props) => ( + +

+    
+  ),
+  code: CodeBlock,
+  a: AnchorTag,
+  Sandbox,
+  ReactDemo,
+  Stats,
+  // TODO add `img`
+  // TODO add `blockquote`
+  // TODO add `ul`
+  // TODO add `li`
+  // TODO add `table`
+}
diff --git a/static/src/components/mdxComponents/loading.js b/static/src/components/mdxComponents/loading.js
new file mode 100644
index 00000000..60355190
--- /dev/null
+++ b/static/src/components/mdxComponents/loading.js
@@ -0,0 +1,7 @@
+import * as React from 'react';
+
+const LoadingProvider = ({ ...props }) => {
+  return 
; +}; + +export default LoadingProvider; diff --git a/static/src/components/rightSidebar.js b/static/src/components/rightSidebar.js new file mode 100644 index 00000000..06f28701 --- /dev/null +++ b/static/src/components/rightSidebar.js @@ -0,0 +1,79 @@ +import * as React from 'react'; +import { StaticQuery, graphql } from 'gatsby'; + +// import Link from './link'; +import config from '../../config'; +import { Sidebar, ListItem } from './styles/Sidebar'; + +const SidebarLayout = ({ location }) => ( + { + let navItems = []; + + let finalNavItems; + + if (allMdx.edges !== undefined && allMdx.edges.length > 0) { + const navItems = allMdx.edges.map((item, index) => { + let innerItems; + + if (item !== undefined) { + if ( + item.node.fields.slug === location.pathname || + config.gatsby.pathPrefix + item.node.fields.slug === location.pathname + ) { + if (item.node.tableOfContents.items) { + innerItems = item.node.tableOfContents.items.map((innerItem, index) => { + const itemId = innerItem.title + ? innerItem.title.replace(/\s+/g, '').toLowerCase() + : '#'; + + return ( + + {innerItem.title} + + ); + }); + } + } + } + if (innerItems) { + finalNavItems = innerItems; + } + }); + } + + if (finalNavItems && finalNavItems.length) { + return ( + +
    +
  • CONTENTS
  • + {finalNavItems} +
+
+ ); + } else { + return ( + +
    +
    + ); + } + }} + /> +); + +export default SidebarLayout; diff --git a/static/src/components/search/hitComps.js b/static/src/components/search/hitComps.js new file mode 100644 index 00000000..4efaaad3 --- /dev/null +++ b/static/src/components/search/hitComps.js @@ -0,0 +1,14 @@ +import * as React from 'react'; +import { Highlight, Snippet } from 'react-instantsearch-dom'; +import { Link } from 'gatsby'; + +export const PageHit = (clickHandler) => ({ hit }) => ( +
    + +
    + +
    + + +
    +); diff --git a/static/src/components/search/index.js b/static/src/components/search/index.js new file mode 100644 index 00000000..6e84aa6a --- /dev/null +++ b/static/src/components/search/index.js @@ -0,0 +1,157 @@ +import React, { useState, useEffect, createRef } from 'react'; +import { + InstantSearch, + Index, + Hits, + Configure, + Pagination, + connectStateResults, +} from 'react-instantsearch-dom'; +import algoliasearch from 'algoliasearch/lite'; +import config from '../../../config.js'; + +import styled from '@emotion/styled'; +import { css } from '@emotion/react'; +import { PoweredBy } from './styles'; +import { Search } from '@styled-icons/fa-solid/Search'; +import Input from './input'; +import * as hitComps from './hitComps'; + +const SearchIcon = styled(Search)` + width: 1em; + pointer-events: none; +`; + +const HitsWrapper = styled.div` + display: ${props => (props.show ? `grid` : `none`)}; + max-height: 80vh; + overflow: scroll; + z-index: 2; + -webkit-overflow-scrolling: touch; + position: absolute; + right: 0; + top: calc(100% + 0.5em); + width: 80vw; + max-width: 30em; + box-shadow: 0 0 5px 0; + padding: 0.7em 1em 0.4em; + background: white; + @media only screen and (max-width: 991px) { + width: 400px; + max-width: 400px; + } + @media only screen and (max-width: 767px) { + width: 100%; + max-width: 500px; + } + border-radius: ${props => props.theme.smallBorderRadius}; + > * + * { + padding-top: 1em !important; + border-top: 2px solid ${props => props.theme.darkGray}; + } + li + li { + margin-top: 0.7em; + padding-top: 0.7em; + border-top: 1px solid ${props => props.theme.lightGray}; + } + * { + margin-top: 0; + padding: 0; + color: black !important; + } + ul { + list-style: none; + } + mark { + color: ${props => props.theme.lightBlue}; + background: ${props => props.theme.darkBlue}; + } + header { + display: flex; + justify-content: space-between; + margin-bottom: 0.3em; + h3 { + color: black; + background: ${props => props.theme.gray}; + padding: 0.1em 0.4em; + border-radius: ${props => props.theme.smallBorderRadius}; + } + } + h3 { + color: black; + margin: 0 0 0.5em; + } + h4 { + color: black; + margin-bottom: 0.3em; + } +`; + +const Root = styled.div` + position: relative; + display: grid; + grid-gap: 1em; + @media only screen and (max-width: 767px) { + width: 100%; + } +`; + +const Results = connectStateResults( + ({ searching, searchState: state, searchResults: res }) => + (searching && `Searching...`) || (res && res.nbHits === 0 && `No results for '${state.query}'`) +); + +const useClickOutside = (ref, handler, events) => { + if (!events) events = [`mousedown`, `touchstart`]; + const detectClickOutside = event => + ref && ref.current && !ref.current.contains(event.target) && handler(); + + useEffect(() => { + for (const event of events) document.addEventListener(event, detectClickOutside); + return () => { + for (const event of events) document.removeEventListener(event, detectClickOutside); + }; + }); +}; + +const searchClient = algoliasearch( + config.header.search.algoliaAppId, + config.header.search.algoliaSearchKey +); + +export default function SearchComponent({ indices, collapse, hitsAsGrid }) { + const ref = createRef(); + + const [query, setQuery] = useState(``); + + const [focus, setFocus] = useState(false); + + useClickOutside(ref, () => setFocus(false)); + const displayResult = query.length > 0 && focus ? 'showResults' : 'hideResults'; + return ( + setQuery(query)} + root={{ Root, props: { ref } }} + > + setFocus(true)} {...{ collapse, focus }} /> + 0 && focus} + asGrid={hitsAsGrid} + > + {indices.map(({ name, title, hitComp, type }) => { + return ( + + + setFocus(false))} /> + + ); + })} + + + + + ); +} diff --git a/static/src/components/search/input.js b/static/src/components/search/input.js new file mode 100644 index 00000000..1985b6e6 --- /dev/null +++ b/static/src/components/search/input.js @@ -0,0 +1,94 @@ +import * as React from 'react'; +import { connectSearchBox } from 'react-instantsearch-dom'; + +import styled from '@emotion/styled'; +import { css } from '@emotion/react'; +import { Search } from '@styled-icons/fa-solid/Search'; + +const SearchIcon = styled(Search)` + width: 1em; + pointer-events: none; + margin-right: 10px; + position: absolute; + left: 15px; + color: #2fd2c5; +`; + +const focus = (props) => css` + background: white; + color: ${(props) => props.theme.darkBlue}; + cursor: text; + width: 5em; + + ${SearchIcon} { + color: ${(props) => props.theme.darkBlue}; + margin: 0.3em; + } +`; + +const collapse = (props) => css` + width: 0; + cursor: pointer; + color: ${(props) => props.theme.lightBlue}; + + ${SearchIcon} { + color: white; + } + ${(props) => props.focus && focus()} + margin-left: ${(props) => (props.focus ? `-1.6em` : `-1em`)}; + padding-left: ${(props) => (props.focus ? `1.6em` : `1em`)}; + ::placeholder { + color: ${(props) => props.theme.gray}; + } +`; + +const expand = (props) => css` + background: ${(props) => props.theme.veryLightGray}; + width: 6em; + margin-left: -1.6em; + padding-left: 1.6em; + + ${SearchIcon} { + margin: 0.3em; + } +`; + +const collapseExpand = (props) => css` + ${(props) => (props.collapse ? collapse() : expand())} +`; + +const Input = styled.input` + outline: none; + border: none; + font-size: 1em; + background: white; + transition: ${(props) => props.theme.shortTrans}; + border-radius: ${(props) => props.theme.smallBorderRadius}; + {collapseExpand} +`; + +const Form = styled.form` + display: flex; + align-items: center; + @media only screen and (max-width: 767px) { + width: 100%; + margin-left: 15px; + } +`; + +export default connectSearchBox(({ refine, ...rest }) => { + const preventSubmit = (e) => { + e.preventDefault(); + }; + + return ( +
    + + refine(e.target.value)} + {...rest} + /> + + ); +}); diff --git a/static/src/components/search/styles.js b/static/src/components/search/styles.js new file mode 100644 index 00000000..8dc73f7d --- /dev/null +++ b/static/src/components/search/styles.js @@ -0,0 +1,11 @@ +import * as React from 'react'; +import { Algolia } from '@styled-icons/fa-brands/Algolia'; + +export const PoweredBy = () => ( + + Powered by{` `} + + Algolia + + +); diff --git a/static/src/components/sidebar/index.js b/static/src/components/sidebar/index.js new file mode 100644 index 00000000..cc7a57a3 --- /dev/null +++ b/static/src/components/sidebar/index.js @@ -0,0 +1,138 @@ +import styled from '@emotion/styled' +import { graphql, StaticQuery } from 'gatsby' +import * as React from 'react' +import { ExternalLink } from 'react-feather' +import config from '../../../config' +import Tree from './tree' + +// eslint-disable-next-line no-unused-vars +const ListItem = styled(({ className, active, level, ...props }) => { + return ( +
  • + + {props.children} + +
  • + ) +})` + list-style: none; + + a { + color: #5c6975; + text-decoration: none; + font-weight: ${({ level }) => (level === 0 ? 700 : 400)}; + padding: 0.45rem 0 0.45rem ${(props) => 2 + (props.level || 0) * 1}rem; + display: block; + position: relative; + + &:hover { + color: #1ed3c6 !important; + } + + ${(props) => + props.active && + ` + // color: #663399; + border-color: rgb(230,236,241) !important; + border-style: solid none solid solid; + border-width: 1px 0px 1px 1px; + background-color: #fff; + `} // external link icon + svg { + float: right; + margin-right: 1rem; + } + } +` + +const Sidebar = styled('aside')` + width: 100%; + height: 100vh; + overflow: auto; + position: fixed; + padding-left: 0px; + position: -webkit-sticky; + position: -moz-sticky; + position: sticky; + top: 0; + padding-right: 0; + -webkit-box-shadow: -1px 0px 4px 1px rgba(175, 158, 232, 0.4); + + @media only screen and (max-width: 1023px) { + width: 100%; + /* position: relative; */ + height: 100vh; + } + + @media (min-width: 767px) and (max-width: 1023px) { + padding-left: 0; + } + + @media only screen and (max-width: 767px) { + padding-left: 0px; + height: auto; + } +` + +const Divider = styled((props) => ( +
  • +
    +
  • +))` + list-style: none; + padding: 0.5rem 0; + + hr { + margin: 0; + padding: 0; + border: 0; + border-bottom: 1px solid #ede7f3; + } +` + +const SidebarLayout = ({ location }) => ( + { + return ( + + {config.sidebar.title ? ( +
    + ) : null} +
      + + {config.sidebar.links && config.sidebar.links.length > 0 && } + {config.sidebar.links.map((link, key) => { + if (link.link !== '' && link.text !== '') { + return ( + + {link.text} + + + ) + } + })} +
    + + ) + }} + /> +) + +export default SidebarLayout diff --git a/static/src/components/sidebar/tree.js b/static/src/components/sidebar/tree.js new file mode 100644 index 00000000..bd3d23ae --- /dev/null +++ b/static/src/components/sidebar/tree.js @@ -0,0 +1,151 @@ +import React, { useState } from 'react'; +import config from '../../../config'; +import TreeNode from './treeNode'; + +const calculateTreeData = edges => { + const originalData = config.sidebar.ignoreIndex + ? edges.filter( + ({ + node: { + fields: { slug }, + }, + }) => slug !== '/' + ) + : edges; + + const tree = originalData.reduce( + ( + accu, + { + node: { + fields: { slug, title }, + }, + } + ) => { + const parts = slug.split('/'); + + let { items: prevItems } = accu; + + const slicedParts = + config.gatsby && config.gatsby.trailingSlash ? parts.slice(1, -2) : parts.slice(1, -1); + + for (const part of slicedParts) { + let tmp = prevItems && prevItems.find(({ label }) => label == part); + + if (tmp) { + if (!tmp.items) { + tmp.items = []; + } + } else { + tmp = { label: part, items: [] }; + prevItems.push(tmp); + } + prevItems = tmp.items; + } + const slicedLength = + config.gatsby && config.gatsby.trailingSlash ? parts.length - 2 : parts.length - 1; + + const existingItem = prevItems.find(({ label }) => label === parts[slicedLength]); + + if (existingItem) { + existingItem.url = slug; + existingItem.title = title; + } else { + prevItems.push({ + label: parts[slicedLength], + url: slug, + items: [], + title, + }); + } + return accu; + }, + { items: [] } + ); + + const { + sidebar: { forcedNavOrder = [] }, + } = config; + + const tmp = [...forcedNavOrder]; + + if (config.gatsby && config.gatsby.trailingSlash) { + } + tmp.reverse(); + return tmp.reduce((accu, slug) => { + const parts = slug.split('/'); + + let { items: prevItems } = accu; + + const slicedParts = + config.gatsby && config.gatsby.trailingSlash ? parts.slice(1, -2) : parts.slice(1, -1); + + for (const part of slicedParts) { + let tmp = prevItems.find(item => item && item.label == part); + + if (tmp) { + if (!tmp.items) { + tmp.items = []; + } + } else { + tmp = { label: part, items: [] }; + prevItems.push(tmp); + } + if (tmp && tmp.items) { + prevItems = tmp.items; + } + } + // sort items alphabetically. + prevItems.map(item => { + item.items = item.items.sort(function(a, b) { + if (a.label < b.label) return -1; + if (a.label > b.label) return 1; + return 0; + }); + }); + const slicedLength = + config.gatsby && config.gatsby.trailingSlash ? parts.length - 2 : parts.length - 1; + + const index = prevItems.findIndex(({ label }) => label === parts[slicedLength]); + + if (prevItems.length) { + accu.items.unshift(prevItems.splice(index, 1)[0]); + } + return accu; + }, tree); +}; + +const Tree = ({ edges }) => { + let [treeData] = useState(() => { + return calculateTreeData(edges); + }); + + const defaultCollapsed = {}; + + treeData.items.forEach(item => { + if (config.sidebar.collapsedNav && config.sidebar.collapsedNav.includes(item.url)) { + defaultCollapsed[item.url] = true; + } else { + defaultCollapsed[item.url] = false; + } + }); + const [collapsed, setCollapsed] = useState(defaultCollapsed); + + const toggle = url => { + setCollapsed({ + ...collapsed, + [url]: !collapsed[url], + }); + }; + + return ( + + ); +}; + +export default Tree; diff --git a/static/src/components/sidebar/treeNode.js b/static/src/components/sidebar/treeNode.js new file mode 100644 index 00000000..022fdee4 --- /dev/null +++ b/static/src/components/sidebar/treeNode.js @@ -0,0 +1,55 @@ +import * as React from 'react'; +import OpenedSvg from '../images/opened'; +import ClosedSvg from '../images/closed'; +import config from '../../../config'; +import Link from '../link'; + +const TreeNode = ({ className = '', setCollapsed, collapsed, url, title, items, ...rest }) => { + const isCollapsed = collapsed[url]; + + const collapse = () => { + setCollapsed(url); + }; + + const hasChildren = items.length !== 0; + + let location; + + if (typeof document != 'undefined') { + location = document.location; + } + const active = + location && (location.pathname === url || location.pathname === config.gatsby.pathPrefix + url); + + const calculatedClassName = `${className} item ${active ? 'active' : ''}`; + + return ( +
  • + {title && ( + + {title} + {!config.sidebar.frontLine && title && hasChildren ? ( + + ) : null} + + )} + + {!isCollapsed && hasChildren ? ( +
      + {items.map((item, index) => ( + + ))} +
    + ) : null} +
  • + ); +}; + +export default TreeNode; diff --git a/static/src/components/styles/Docs.js b/static/src/components/styles/Docs.js new file mode 100644 index 00000000..3dc7e454 --- /dev/null +++ b/static/src/components/styles/Docs.js @@ -0,0 +1,78 @@ +import styled from '@emotion/styled'; + +export const StyledHeading = styled('h1')` + font-size: 32px; + line-height: 1.5; + font-weight: 500; + /* border-left: 2px solid #1ed3c6; */ + padding: 0 16px; + flex: 1; + margin-top: 0; + padding-top: 0; + color: ${(props) => props.theme.colors.heading}; +`; + +export const Edit = styled('div')` + padding: 1rem 1.5rem; + text-align: right; + + a { + font-size: 14px; + font-weight: 500; + line-height: 1em; + text-decoration: none; + color: #555; + border: 1px solid rgb(211, 220, 228); + cursor: pointer; + border-radius: 3px; + transition: all 0.2s ease-out 0s; + text-decoration: none; + color: rgb(36, 42, 49); + background-color: rgb(255, 255, 255); + box-shadow: rgba(116, 129, 141, 0.1) 0px 1px 1px 0px; + height: 30px; + padding: 5px 16px; + &:hover { + background-color: rgb(245, 247, 249); + } + } +`; + +export const StyledMainWrapper = styled.div` + max-width: 750px; + color: ${(props) => props.theme.colors.text}; + + ul, + ol { + -webkit-padding-start: 40px; + -moz-padding-start: 40px; + -o-padding-start: 40px; + margin: 24px 0px; + padding: 0px 0px 0px 2em; + + li { + font-size: 16px; + line-height: 1.8; + font-weight: 400; + } + } + + a { + transition: color 0.15s; + color: ${(props) => props.theme.colors.link}; + } + + code { + /* border: 1px solid #ede7f3; */ + border-radius: 4px; + padding: 2px 6px; + font-size: 0.9375em; + + background: ${(props) => props.theme.colors.background}; + background-color: ${(props) => props.theme.colors.code}; + } + + @media (max-width: 767px) { + padding: 0 15px; + } +`; diff --git a/static/src/components/styles/GlobalStyles.js b/static/src/components/styles/GlobalStyles.js new file mode 100644 index 00000000..baa96094 --- /dev/null +++ b/static/src/components/styles/GlobalStyles.js @@ -0,0 +1,905 @@ +import { css } from '@emotion/react' + +export const baseStyles = css` + @import url('https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap'); + @import url('https://fonts.googleapis.com/css?family=Poppins:300,400,500,600&display=swap'); + * { + margin: 0; + padding: 0; + box-sizing: border-box; + font-display: swap; + } + ::-webkit-input-placeholder { + /* Edge */ + color: #c2c2c2; + } + + :-ms-input-placeholder { + /* Internet Explorer */ + color: #c2c2c2; + } + + ::placeholder { + color: #c2c2c2; + } + html, + body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Roboto Light', 'Oxygen', + 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif, + 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; + + font-size: 16px; + scroll-behavior: smooth; + } + + a { + transition: color 0.15s; + /* color: #663399; */ + } + + body { + font-family: 'Roboto'; + } + .visibleMobile { + display: none; + } + .visibleMobileView { + display: none !important; + } + .video-responsive { + position: relative; + padding-bottom: 56.2%; + } + a { + text-decoration: none; + } + a:hover { + text-decoration: none; + } + .displayInline { + display: inline-block; + } + .navBarToggle { + border: 0px solid #fff; + border-radius: 4px; + width: 36px; + height: 33px; + position: absolute; + right: 20px; + padding: 8px 5px; + display: none; + } + .navBarToggle .iconBar { + display: block; + width: 22px; + height: 2px; + border-radius: 1px; + margin: 0 auto; + margin-top: 4px; + background-color: #001934; + } + .navBarToggle .iconBar:first-child { + margin-top: 0px; + } + .video-responsive iframe { + position: absolute; + width: 100%; + height: 100%; + } + + .diffNewLine { + color: #22863a; + background-color: #f0fff4; + } + + .diffRemoveLine { + color: red; + background-color: #ffcccc; + } + .navBarParent { + width: 100%; + float: left; + display: flex; + align-items: center; + } + .divider { + height: 30px; + margin: 0 15px; + border-right: 1px solid rgba(255, 255, 255, 0.3); + } + .navBarULRight { + /* position: absolute; + right: 30px; */ + } + .githubIcon { + width: 15px; + margin-right: 5px; + } + + .githubSection { + display: flex; + align-items: center; + color: #000; + opacity: 0.7; + } + + .githubSection:hover { + text-decoration: none; + opacity: 1; + } + + .navbar-default .navbar-toggle .icon-bar { + background-color: #fff !important; + } + + .navbar-default .navbar-toggle:focus, + .navbar-default .navbar-toggle:hover { + background-color: #001933; + } + + .headerWrapper { + border-bottom: 1px solid rgb(212, 218, 223); + box-shadow: rgba(116, 129, 141, 0.1) 0px 1px 1px 0px; + display: flex; + align-items: center; + } + .formElement { + background-color: transparent; + padding: 4px; + border-radius: 5px; + position: relative; + } + .formElement:focus { + outline: none; + border: none; + } + .formElement svg path { + fill: #2fd2c5; + } + .searchInput { + width: 100%; + background-color: rgba(28, 211, 198, 0.12) !important; + border-width: 0 !important; + color: #c2c2c2; + padding: 10px; + border-radius: 5px; + color: #fff; + opacity: 0.6; + padding-left: 38px; + max-width: 600px; + } + .searchInput:focus, + .searchInput:visited, + .searchInput:hover, + .searchInput:focus-within { + outline: none; + border: 0; + } + .searchWrapper { + padding-left: 0px; + padding-right: 20px; + flex: 1; + position: relative; + } + .searchWrapper a { + font-weight: 500; + } + .hitWrapper { + background-color: #fff; + padding: 0.7em 1em 0.4em; + border-radius: 4px; + position: absolute; + width: 80vw; + max-width: 30em; + top: 40px; + border: 1px solid #ccc; + box-shadow: 0 1px 4px 0 rgba(0, 0, 0, 0.16); + height: auto; + max-height: 80vh; + overflow: scroll; + left: 0; + } + .hitWrapper ul li { + margin-top: 0.7em; + padding-top: 0.7em; + border-top: 1px solid; + list-style-type: none; + } + .hitWrapper ul li:first-child { + border-top: 0px; + margin-top: 0px; + color: black !important; + padding: 0px; + } + .showResults { + display: block; + } + .hideResults { + display: none; + } + .hitWrapper span { + color: black; + font-size: 14px; + } + .headerTitle { + height: auto; + font-size: 16px; + line-height: 1.5; + font-weight: 300; + color: #fff !important; + margin-top: 16px; + text-transform: uppercase; + } + .headerTitle a { + color: #fff; + } + + .headerTitle a:hover { + text-decoration: none; + opacity: 0.8; + } + .logoWrapper { + padding: 21px 0; + padding-left: 20px; + } + + .logoContent { + font-family: 'Roboto'; + margin-left: 16px; + font-size: 28px; + line-height: 1.5; + font-weight: 500; + padding-right: 10px; + } + + /* Header section starts here */ + .removePadd { + padding: 0 !important; + } + .navBarDefault { + background-color: #001934; + border-radius: 0; + border-top: 0; + margin-bottom: 0; + border: 0; + display: flex; + align-items: center; + box-shadow: -1px 0px 4px 1px rgba(175, 158, 232, 0.4); + -webkit-box-shadow: -1px 0px 4px 1px rgba(175, 158, 232, 0.4); + -moz-box-shadow: -1px 0px 4px 1px rgba(175, 158, 232, 0.8); + -o-box-shadow: -1px 0px 4px 1px rgba(175, 158, 232, 0.4); + z-index: 1; + padding: 15px; + position: relative; + height: 80px; + } + .navBarHeader { + min-width: 335px; + padding-right: 20px; + display: flex; + align-items: center; + } + .navBarBrand { + padding: 0px 0px; + display: flex; + align-items: center; + } + + .navBarBrand img { + width: 120px; + margin-right: 6px; + display: inline-block; + } + .navBarUL li { + list-style-type: none; + } + .navBarUL { + -webkit-overflow-scrolling: touch; + } + .navBarUL li a { + font-family: 'Roboto'; + color: #fff !important; + font-size: 16px; + font-weight: 500; + line-height: 1em; + opacity: 1; + padding: 10px 15px; + } + .navBarNav { + display: flex; + align-items: center; + } + .navBarUL li a img, + .navBarUL li a .shareIcon { + width: 20px; + } + .navBarUL li a:hover { + opacity: 0.7; + } + pre { + border: 0 !important; + background-color: rgb(245, 247, 249); /* !important; */ + } + + blockquote { + color: rgb(116, 129, 141); + margin: 0px 0px 24px; + padding: 0px 0px 0px 12px; + border-left: 4px solid rgb(230, 236, 241); + border-color: rgb(230, 236, 241); + } + .socialWrapper { + display: flex; + align-items: center; + } + .socialWrapper li { + display: inline-block; + } + .socialWrapper li a { + display: contents; + } + .discordBtn, + .twitterBtn { + border-radius: 4px; + border: solid 1px #d1d2d3; + background-color: #f1f5f8; + width: 20px; + height: 20px; + padding-top: 2px; + margin-left: 8px; + display: flex; + align-items: center; + justify-content: center; + opacity: 0.8; + cursor: pointer; + } + .twitterBtn img { + width: 12px !important; + } + .discordBtn img { + width: 10px !important; + } + .discordBtn:hover, + .twitterBtn:hover { + opacity: 1; + } + .discordBtn { + img { + width: 10px; + } + } + /* Header section ends here */ + .sidebarTitle { + /* box-shadow: 0 3px 6px 0 rgba(0, 0, 0, 0.16); */ + background-color: #f8f8f8; + padding: 18px 16px; + font-family: 'Poppins'; + font-size: 18px; + font-weight: 600; + color: #001934; + display: flex; + align-items: center; + } + + .sideBarShow { + display: none; + } + + .sidebarTitle a { + color: #001934; + } + + .greenCircle { + width: 8px; + height: 8px; + background-color: #1cd3c6; + border-radius: 50%; + margin: 0 12px; + } + + .headerNav { + font-family: 'Roboto'; + padding: 0px 24px; + color: #001933; + font-size: 16px; + font-weight: 500; + line-height: 1em; + } + + .headerNav a { + color: #001933; + text-decoration: none; + } + + .headerNav a:hover { + text-decoration: none; + } + + .logoWrapper img { + width: 40px; + } + + .sideBarUL { + margin-top: 32px; + } + + .sideBarUL li { + list-style-type: none; + width: auto; + } + + .sideBarUL li a { + /* color: #fff; */ + font-size: 14px; + font-weight: 500; + line-height: 1.5; + padding: 7px 24px 7px 16px; + padding-left: 10px; + padding-right: 25px; + border-style: solid none solid solid; + border-width: 1px 0px 1px 1px; + border-color: transparent currentcolor transparent transparent; + } + + .hideFrontLine .collapser { + background: transparent; + border: none; + outline: none; + position: absolute; + right: 20px; + z-index: 1; + cursor: pointer; + } + + .hideFrontLine .active > a { + background-color: #1ed3c6; + color: #fff !important; + } + .firstLevel ul li .collapser svg path { + fill: #fff !important; + } + .active .collapser > svg > path { + fill: #001933 !important; + } + + .firstLevel ul .item ul .item { + border-left: 1px solid #e6ecf1; + } + + .sideBarUL .item { + list-style: none; + padding: 0; + } + + .sideBarUL .item > a { + color: #1ed3c6; + text-decoration: none; + display: flex; + align-items: center; + position: relative; + width: 100%; + padding-right: 35px; + padding-left: 15px; + } + + .showFrontLine .item > a:hover { + background-color: #001933; + } + + .showFrontLine .active > a { + /* color: #fff; */ + background-color: #001933; + } + + .sideBarUL .item .item { + margin-left: 16px; + } + + .firstLevel > ul > .item { + margin-left: 0 !important; + } + + .showFrontLine .item .item { + border-left: 1px solid #e6ecf1; + border-left-color: rgb(230, 236, 241); + padding: 0; + width: calc(100% - 16px) !important; + } + + .showFrontLine .item .active > a { + border-color: rgb(230, 236, 241) !important; + border-style: solid none solid solid; + border-width: 1px 0px 1px 1px; + background-color: #1ed3c6 !important; + color: #fff; + } + + .titleWrapper { + display: flex; + align-items: center; + padding-bottom: 40px; + border-bottom: 1px solid rgb(230, 236, 241); + margin-bottom: 32px; + } + + .gitBtn { + height: 30px; + min-height: 30px; + display: flex; + align-items: center; + } + + .gitBtn img { + width: 15px; + display: inline-block; + margin-right: 5px; + } + + .addPaddTopBottom { + padding: 50px 0; + } + + .preRightWrapper { + display: block; + margin: 0px; + flex: 1 1 0%; + padding: 16px; + text-align: right; + } + + .smallContent { + display: block; + margin: 0px; + padding: 0px; + color: #6e6e6e; + } + + .smallContent span { + font-size: 12px; + line-height: 1.625; + font-weight: 400; + } + + /* **************************** */ + + .nextRightWrapper { + display: block; + margin: 0px; + padding: 16px; + flex: 1 1 0%; + } + + /* tables.css */ + table { + padding: 0; + } + + table tr { + border-top: 1px solid #cccccc; + margin: 0; + padding: 0; + } + + table tr:nth-child(2n) { + background-color: #f8f8f8; + } + + table tr th { + font-weight: bold; + border: 1px solid #cccccc; + text-align: left; + margin: 0; + padding: 6px 13px; + } + + table tr td { + border: 1px solid #cccccc; + text-align: left; + margin: 0; + padding: 6px 13px; + } + + table tr th :first-child, + table tr td :first-child { + margin-top: 0; + } + + table tr th :last-child, + table tr td :last-child { + margin-bottom: 0; + } + /* end - tables.css */ + + /* Image styling */ + img { + max-width: 100%; + } + /* end image */ + .githubBtn { + display: flex; + align-items: center; + font-size: 16px; + padding: 10px 0px; + padding-left: 15px; + max-height: 40px; + } + .githubBtn span span { + display: flex; + align-items: center; + } + + .communitySection { + font-size: 24px; + font-weight: 700; + } + .authorSection { + padding: 20px 0; + } + .authorSection, + .authorName { + display: flex; + align-items: center; + } + .authorImg img { + width: 75px; + height: 75px; + border-radius: 50%; + min-width: 75px; + max-width: 75px; + min-height: 75px; + max-height: 75px; + } + .authorDetails { + padding-left: 10px; + } + .authorDesc { + padding-top: 5px; + font-size: 14px; + } + .authorName img { + margin-left: 10px; + display: inline-block; + width: 20px; + } + .authorName img:hover { + opacity: 0.7; + } + + .heading1 { + font-size: 26px; + font-weight: 800; + line-height: 1.5; + margin-bottom: 16px; + margin-top: 32px; + } + + .heading2 { + font-size: 24px; + font-weight: 700; + line-height: 1.5; + margin-bottom: 16px; + margin-top: 32px; + } + + .heading3 { + font-size: 20px; + font-weight: 600; + line-height: 1.5; + margin-bottom: 16px; + margin-top: 32px; + } + + .heading4 { + font-size: 18px; + font-weight: 500; + line-height: 1.5; + margin-bottom: 16px; + margin-top: 32px; + } + + .heading5 { + font-size: 16px; + font-weight: 400; + line-height: 1.5; + margin-bottom: 16px; + margin-top: 32px; + } + + .heading6 { + font-size: 14px; + font-weight: 300; + line-height: 1.5; + margin-bottom: 16px; + margin-top: 32px; + } + + .paragraph { + margin: 16px 0px 32px; + line-height: 1.625; + } + + .pre { + font-size: 14px; + margin: 0px; + padding: 16px; + overflow: auto; + } + + .poweredBy { + font-size: 0.6em; + text-align: end; + padding: 0; + } + .topnav { + -webkit-transition: top 0.5s, bottom 0.5s; + } + + @media (max-width: 767px) { + .formElement svg path { + fill: #001934; + } + .visibleMobileView { + display: block !important; + } + .searchInput { + color: #001934; + } + .socialWrapper { + position: absolute; + right: 10px; + top: 29px; + } + .responsive { + margin-top: 15px; + position: relative; + padding-bottom: 20px; + border-top: 1px solid #fff; + } + .headerTitle { + padding-right: 50px; + font-size: 16px; + } + .navBarBrand { + min-height: 40px; + } + .navBarBrand img { + margin-right: 8px; + } + .topnav.responsive .visibleMobile { + display: block; + } + .topnav .navBarUL { + display: none; + } + .topnav.responsive .navBarUL { + display: block; + text-align: left; + } + .hiddenMobile { + display: none !important; + } + hr { + margin-top: 0; + margin-bottom: 0; + } + .navBarParent { + display: block; + } + .separator { + margin-top: 20px; + margin-bottom: 20px; + } + .navBarULRight { + position: static; + } + .navBarUL { + display: flex; + align-items: center; + margin: 7.5px 0px; + } + .navBarUL li { + height: 37px; + } + .navBarUL li a { + font-size: 14px; + padding: 10px 15px; + } + + .navBarDefault { + display: block; + height: auto; + } + + .navBarToggle { + margin-right: 0; + display: block; + position: absolute; + left: 11px; + top: 15px; + background: #fff; + } + + .navBarHeader { + display: flex; + min-width: auto; + padding-right: 0; + align-items: center; + } + + .navBarBrand { + font-size: 20px; + padding: 0 0; + padding-left: 0; + flex: initial; + padding-right: 15px; + } + + .titleWrapper { + padding: 0 15px; + display: block; + } + + .gitBtn { + display: inline-block; + } + + .mobileView { + text-align: left !important; + padding-left: 0 !important; + } + + .searchWrapper { + padding: 0px 0; + padding-top: 0px; + position: absolute; + bottom: 0px; + width: calc(100% - 70px); + position: absolute; + left: 40px; + top: 8px; + } + .hitWrapper { + width: 100%; + right: 0; + top: 35px; + max-height: fit-content; + position: static; + } + } + + @media (min-width: 768px) and (max-width: 991px) { + .navBarDefault { + padding: 10px; + } + .navBarBrand { + font-size: 22px; + } + .navBarHeader { + min-width: 240px; + flex: initial; + } + .githubBtn { + padding: 10px 10px; + } + .divider { + margin: 0 5px; + height: 20px; + } + .hitWrapper { + max-width: 500px; + } + .navBarUL li a { + padding: 10px 5px; + } + .searchWrapper { + padding-left: 0px; + } + } + + canvas { + display: flex; + flex: 1; + width: 100%; + height: 100%; + } +` diff --git a/static/src/components/styles/PageNavigationButtons.js b/static/src/components/styles/PageNavigationButtons.js new file mode 100644 index 00000000..f2cdaa4c --- /dev/null +++ b/static/src/components/styles/PageNavigationButtons.js @@ -0,0 +1,110 @@ +import styled from '@emotion/styled'; + +export const StyledNextPrevious = styled('div')` + margin: 0px; + padding: 0px; + width: auto; + display: grid; + grid-template-rows: auto; + column-gap: 24px; + grid-template-columns: calc(50% - 8px) calc(50% - 8px); + + .previousBtn { + cursor: pointer; + -moz-box-align: center; + -moz-box-direction: normal; + -moz-box-orient: horizontal; + margin: 0px; + padding: 0px; + position: relative; + display: flex; + flex-direction: row; + align-items: center; + place-self: stretch; + border-radius: 3px; + border: 1px solid rgb(230, 236, 241); + transition: border 200ms ease 0s; + box-shadow: rgba(116, 129, 141, 0.1) 0px 3px 8px 0px; + text-decoration: none; + + background-color: ${props => props.theme.colors.background}; + color: ${props => props.theme.colors.text}; + } + + .nextBtn { + cursor: pointer; + -moz-box-align: center; + -moz-box-direction: normal; + -moz-box-orient: horizontal; + margin: 0px; + padding: 0px; + position: relative; + display: flex; + flex-direction: row; + align-items: center; + place-self: stretch; + border-radius: 3px; + border: 1px solid rgb(230, 236, 241); + transition: border 200ms ease 0s; + box-shadow: rgba(116, 129, 141, 0.1) 0px 3px 8px 0px; + text-decoration: none; + + background-color: ${props => props.theme.colors.background}; + color: ${props => props.theme.colors.text}; + } + + .nextBtn:hover, + .previousBtn:hover { + text-decoration: none; + border: 1px solid #1ed3c6; + } + + .nextBtn:hover .rightArrow, + .previousBtn:hover .leftArrow { + color: #1ed3c6; + } + + .leftArrow { + display: flex; + margin: 0px; + color: rgb(157, 170, 182); + flex: 0 0 auto; + font-size: 24px; + transition: color 200ms ease 0s; + padding: 16px; + padding-right: 16px; + } + + .rightArrow { + display: flex; + flex: 0 0 auto; + font-size: 24px; + transition: color 200ms ease 0s; + padding: 16px; + padding-left: 16px; + margin: 0px; + color: rgb(157, 170, 182); + } + + .nextPreviousTitle { + display: block; + margin: 0px; + padding: 0px; + transition: color 200ms ease 0s; + } + + .nextPreviousTitle span { + font-size: 16px; + line-height: 1.5; + font-weight: 500; + } + + @media (max-width: 767px) { + display: block; + padding: 0 15px; + + .previousBtn { + margin-bottom: 20px; + } + } +`; diff --git a/static/src/components/styles/Sidebar.js b/static/src/components/styles/Sidebar.js new file mode 100644 index 00000000..7be389a9 --- /dev/null +++ b/static/src/components/styles/Sidebar.js @@ -0,0 +1,92 @@ +import styled from '@emotion/styled'; + +export const Sidebar = styled('aside')` + width: 100%; + border-right: 1px solid #ede7f3; + height: 100vh; + overflow: auto; + position: fixed; + padding-left: 24px; + position: -webkit-sticky; + position: -moz-sticky; + position: sticky; + top: 0; + + background: ${props => props.theme.colors.background}; + + .rightSideTitle { + font-size: 10px; + line-height: 1; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 1.2px; + padding: 7px 24px 7px 16px; + border-left: 1px solid #e6ecf1; + border-left-color: rgb(230, 236, 241); + + color: ${props => props.theme.colors.text}; + } + + .rightSideBarUL { + margin-top: 32px; + } + + .rightSideBarUL li { + list-style-type: none; + border-left: 1px solid #e6ecf1; + border-left-color: rgb(230, 236, 241); + } + + .rightSideBarUL li a { + font-size: 12px; + font-weight: 500; + line-height: 1.5; + padding: 7px 24px 7px 16px; + + color: ${props => props.theme.colors.text}; + } + + @media only screen and (max-width: 50rem) { + width: 100%; + position: relative; + } +`; + +export const ListItem = styled(({ className, active, level, ...props }) => { + return ( +
  • + + {props.children} + +
  • + ); +})` + list-style: none; + + a { + color: #5c6975; + text-decoration: none; + font-weight: ${({ level }) => (level === 0 ? 700 : 400)}; + padding: 0.45rem 0 0.45rem ${props => 2 + (props.level || 0) * 1}rem; + display: block; + position: relative; + + &:hover { + color: #1ed3c6 !important; + } + + ${props => + props.active && + ` + color: #1ED3C6; + border-color: rgb(230,236,241) !important; + border-style: solid none solid solid; + border-width: 1px 0px 1px 1px; + background-color: #fff; + `} // external link icon + svg { + float: right; + margin-right: 1rem; + } + } +`; diff --git a/static/src/components/theme.js b/static/src/components/theme.js new file mode 100644 index 00000000..7192cae1 --- /dev/null +++ b/static/src/components/theme.js @@ -0,0 +1,5 @@ +export default { + fonts: { + mono: '"SF Mono", "Roboto Mono", Menlo, monospace', + }, +}; diff --git a/static/src/components/theme/index.js b/static/src/components/theme/index.js new file mode 100644 index 00000000..c38a55c6 --- /dev/null +++ b/static/src/components/theme/index.js @@ -0,0 +1,31 @@ +const baseTheme = { + fonts: { + mono: '"SF Mono", "Roboto Mono", Menlo, monospace', + }, +} + +const lightTheme = { + ...baseTheme, + colors: { + background: '#fff', + code: '#bbbbbb', + heading: '#000', + text: '#3B454E', + preFormattedText: 'rgb(245, 247, 249)', + link: '#1000EE', + }, +} + +const darkTheme = { + ...baseTheme, + colors: { + background: '#001933', + code: '#323232', + heading: '#fff', + text: '#fff', + preFormattedText: '#000', + link: '#1ED3C6', + }, +} + +export { lightTheme, darkTheme } diff --git a/static/src/components/theme/themeProvider.js b/static/src/components/theme/themeProvider.js new file mode 100644 index 00000000..1a8478f3 --- /dev/null +++ b/static/src/components/theme/themeProvider.js @@ -0,0 +1,51 @@ +import * as React from 'react'; +import { ThemeProvider as EmotionThemeProvider, Global, css } from '@emotion/react'; + +import { lightTheme, darkTheme } from './index'; +import Header from '../Header'; +import { baseStyles } from '../styles/GlobalStyles'; +import { styles } from '../../custom/styles/styles'; + +class ThemeProvider extends React.Component { + state = { + isDarkThemeActive: false, + }; + + componentDidMount() { + this.retrieveActiveTheme(); + } + + retrieveActiveTheme = () => { + const isDarkThemeActive = JSON.parse(window.localStorage.getItem('isDarkThemeActive')); + + this.setState({ isDarkThemeActive }); + }; + + toggleActiveTheme = () => { + this.setState(prevState => ({ isDarkThemeActive: !prevState.isDarkThemeActive })); + + window.localStorage.setItem('isDarkThemeActive', JSON.stringify(!this.state.isDarkThemeActive)); + }; + + render() { + const { children, location } = this.props; + + const { isDarkThemeActive } = this.state; + + const currentActiveTheme = isDarkThemeActive ? darkTheme : lightTheme; + + return ( +
    + +
    + {children} +
    + ); + } +} + +export default ThemeProvider; diff --git a/static/src/components/themeProvider.js b/static/src/components/themeProvider.js new file mode 100644 index 00000000..d9cef9f8 --- /dev/null +++ b/static/src/components/themeProvider.js @@ -0,0 +1,13 @@ +import * as React from 'react'; +import { ThemeProvider as EmotionThemeProvider } from '@emotion/react'; +import { default as defaultTheme } from './theme'; +import Header from './Header'; + +export default function ThemeProvider({ children, theme = {}, location }) { + return ( +
    +
    + {children} +
    + ); +} diff --git a/static/src/custom-sw-code.js b/static/src/custom-sw-code.js new file mode 100644 index 00000000..9a765366 --- /dev/null +++ b/static/src/custom-sw-code.js @@ -0,0 +1,6 @@ +workbox.routing.registerRoute( + new RegExp('https:.*min.(css|js)'), + workbox.strategies.staleWhileRevalidate({ + cacheName: 'cdn-cache', + }) +); diff --git a/static/src/custom/config/codeBlockLanguages.js b/static/src/custom/config/codeBlockLanguages.js new file mode 100644 index 00000000..7cfa285f --- /dev/null +++ b/static/src/custom/config/codeBlockLanguages.js @@ -0,0 +1,26 @@ +export function applyLanguages(_Prism) { + /** + * Here you have the possibility to add other supported languages to Prism and thus to the code highlighting. + * + * We support the following languages automatically: + * markup, bash, clike, c, cpp, css, javascript, jsx, coffeescript, actionscript, css-extr, + * diff, git, go, graphql, handlebars, json, less, makefile, markdown, objectivec, ocaml, python, + * reason, sass, scss, sql, stylus, tsx, typescript, wasm, yaml + * + * If you need more languages you can add them yourself as follows: + * (https://github.com/PrismJS/prism/tree/master/components) + * + * ```js + * require("prismjs/components/prism-docker"); + * require("prismjs/components/prism-ada"); + * ``` + */ + +} + +export function getTheme(_Prism) { + /** + * Here you have the possibility to change the prism highlighting. + */ + return require('prism-react-renderer/themes/vsDark').default; +} diff --git a/static/src/custom/styles/styles.js b/static/src/custom/styles/styles.js new file mode 100644 index 00000000..3bb4429b --- /dev/null +++ b/static/src/custom/styles/styles.js @@ -0,0 +1,7 @@ +import { css } from '@emotion/react'; + +const customStyles = css` + +`; + +export const styles = [customStyles]; diff --git a/static/src/html.js b/static/src/html.js new file mode 100644 index 00000000..ff07421e --- /dev/null +++ b/static/src/html.js @@ -0,0 +1,58 @@ +import * as React from 'react'; +import PropTypes from 'prop-types'; +import config from '../config'; + +export default class HTML extends React.Component { + render() { + return ( + + + + + + {config.siteMetadata.ogImage ? ( + + ) : null} + + {config.siteMetadata.ogImage ? ( + + ) : null} + {config.siteMetadata.favicon ? ( + + ) : null} + + {this.props.headComponents} + + + {this.props.preBodyComponents} +
    + {this.props.postBodyComponents} +