├── .gitignore ├── .vscode └── settings.json ├── client ├── static │ ├── obs-trans.png │ ├── obsidianLogo2.png │ ├── obsidianLogo3.png │ ├── prism.css │ ├── github-icon.svg │ ├── Deno-Logo.svg │ ├── logo.svg │ ├── prism.js │ └── style.css ├── Components │ ├── SideBarContext │ │ ├── AboutContext.jsx │ │ ├── MainContext.tsx │ │ ├── DemoContext.tsx │ │ └── DocsContext.tsx │ ├── DocPages │ │ ├── Caching │ │ │ ├── Errors.tsx │ │ │ ├── Server.tsx │ │ │ ├── Client.tsx │ │ │ └── Strategies.tsx │ │ ├── Advanced │ │ │ └── Polling.tsx │ │ ├── Basics │ │ │ ├── Errors.tsx │ │ │ ├── Mutations.tsx │ │ │ ├── Queries.tsx │ │ │ ├── GettingStarted.tsx │ │ │ └── ServerSideRendering.tsx │ │ ├── Philosophy.tsx │ │ ├── Overview.tsx │ │ └── QuickStart.tsx │ ├── About.tsx │ ├── Main.tsx │ ├── MainContainer.tsx │ ├── NestedCache.tsx │ ├── Cache.tsx │ ├── Demo.tsx │ ├── TeamMember.jsx │ ├── Docs.tsx │ ├── SideBar.tsx │ ├── NavBar.tsx │ ├── ObsidianLogo.tsx │ └── Team.jsx ├── client.tsx └── app.tsx ├── serverDeps.ts ├── README.md ├── docker-compose.yml ├── Dockerfile ├── .travis.yml ├── deno.test.ts ├── deps.ts ├── staticFileMiddleware.ts ├── tsconfig.json └── server.tsx /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "deno.enable": true, 3 | "deno.lint": true, 4 | "deno.unstable": true 5 | } -------------------------------------------------------------------------------- /client/static/obs-trans.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lascaux-obsidian/obsidian-website/HEAD/client/static/obs-trans.png -------------------------------------------------------------------------------- /client/static/obsidianLogo2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lascaux-obsidian/obsidian-website/HEAD/client/static/obsidianLogo2.png -------------------------------------------------------------------------------- /client/static/obsidianLogo3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lascaux-obsidian/obsidian-website/HEAD/client/static/obsidianLogo3.png -------------------------------------------------------------------------------- /client/Components/SideBarContext/AboutContext.jsx: -------------------------------------------------------------------------------- 1 | import { React } from '../../../deps.ts'; 2 | 3 | const AboutContext = (props) =>
; 4 | 5 | export default AboutContext; 6 | -------------------------------------------------------------------------------- /serverDeps.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Application, 3 | Router, 4 | Context, 5 | send, 6 | } from 'https://deno.land/x/oak@v6.0.1/mod.ts'; 7 | 8 | export { Application, Router, Context, send }; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ObsidianWebsite 2 | 3 | Travis (.org) 4 | 5 | deno run --allow-net --allow-read --unstable server.tsx -c tsconfig.json 6 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.8" 2 | 3 | services: 4 | site: 5 | build: . 6 | restart: always 7 | volumes: 8 | - ./:/usr/app 9 | ports: 10 | - "80:3000" 11 | -------------------------------------------------------------------------------- /client/client.tsx: -------------------------------------------------------------------------------- 1 | import { React, ReactDom } from '../deps.ts'; 2 | import App from './app.tsx'; 3 | 4 | // Hydrate the app and reconnect React functionality 5 | (ReactDom as any).hydrate( 6 | , 7 | document.getElementById('root') 8 | ); 9 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM denoland/deno:1.11.3 2 | 3 | EXPOSE 3000 4 | 5 | WORKDIR /usr/app 6 | 7 | USER deno 8 | 9 | COPY . . 10 | 11 | CMD [ "run", "--allow-net", "--allow-env", "--allow-read", "--unstable", "server.tsx" ] 12 | 13 | 14 | # deno run --unstable --allow-net --allow-env --allow-read server.tsx -c tsconfig.json 15 | -------------------------------------------------------------------------------- /client/Components/SideBarContext/MainContext.tsx: -------------------------------------------------------------------------------- 1 | import { React } from '../../../deps.ts'; 2 | 3 | declare global { 4 | namespace JSX { 5 | interface IntrinsicElements { 6 | div: any; 7 | } 8 | } 9 | } 10 | 11 | const MainContext = (props: any) => { 12 | return
; 13 | }; 14 | 15 | export default MainContext; 16 | -------------------------------------------------------------------------------- /client/Components/DocPages/Caching/Errors.tsx: -------------------------------------------------------------------------------- 1 | import { React, CodeBlock, dracula } from '../../../../deps.ts'; 2 | 3 | const CachingErrors = (props: any) => { 4 | 5 | return ( 6 |
7 |

Errors

8 |

This page is a stub. Check back soon for more information about caching errors in obsidian.

9 |
10 | ) 11 | } 12 | 13 | export default CachingErrors; 14 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | 3 | services: 4 | - docker 5 | before_install: 6 | - docker build -t lascaux-obsidian/obsidian-website . 7 | script: 8 | - docker run lascaux-obsidian/obsidian-website test 9 | deploy: 10 | provider: elasticbeanstalk 11 | region: 'us-east-1' 12 | app: 'obs-website' 13 | env: 'obs-website-prod' 14 | bucket_name: 'elasticbeanstalk-us-east-1-416954177761' 15 | bucket_path: 'obs_website' 16 | access_key_id: '$AWS_ACCESS_KEY_ID' 17 | secret_access_key: '$AWS_SECRET_ACCESS_KEY' 18 | on: 19 | branch: master 20 | -------------------------------------------------------------------------------- /client/app.tsx: -------------------------------------------------------------------------------- 1 | import { React } from '../deps.ts'; 2 | import NavBar from './Components/NavBar.tsx'; 3 | import MainContainer from './Components/MainContainer.tsx'; 4 | 5 | declare global { 6 | namespace JSX { 7 | interface IntrinsicElements { 8 | div: any; 9 | } 10 | } 11 | } 12 | 13 | const App = () => { 14 | const [page, setPage] = (React as any).useState('home'); 15 | 16 | return ( 17 |
18 | 19 | 20 |
21 | ); 22 | }; 23 | 24 | export default App; -------------------------------------------------------------------------------- /client/Components/About.tsx: -------------------------------------------------------------------------------- 1 | import { React } from '../../deps.ts'; 2 | import Team from './Team.jsx'; 3 | import SideBar from './SideBar.tsx'; 4 | 5 | declare global { 6 | namespace JSX { 7 | interface IntrinsicElements { 8 | div: any; 9 | } 10 | } 11 | } 12 | 13 | 14 | 15 | 16 | 17 | const About = (props: any) => { 18 | return ( 19 | <> 20 |
21 | 22 | 23 |
24 | 25 |
26 |
27 | 28 | 29 | 30 | 31 | ); 32 | }; 33 | 34 | export default About; 35 | -------------------------------------------------------------------------------- /deno.test.ts: -------------------------------------------------------------------------------- 1 | // import { superoak } from 'https://deno.land/x/superoak@2.3.1/mod.ts'; 2 | // import { describe, it } from 'https://deno.land/x/superoak@2.3.1/test/utils.ts'; 3 | // import { expect } from 'https://deno.land/x/superoak@2.3.1/test/deps.ts'; 4 | // import { app } from './server.tsx'; 5 | 6 | // describe('GET request to root url', () => { 7 | // it('Sends 200 Status and Content Type text/html', async (done) => { 8 | // (await superoak(app)).get('/').end((err, res) => { 9 | // expect(res.status).toEqual(200); 10 | // expect(res.type).toEqual('text/html'); 11 | // done(); 12 | // }); 13 | // }); 14 | // }); 15 | 16 | // deno test -A --unstable 17 | -------------------------------------------------------------------------------- /deps.ts: -------------------------------------------------------------------------------- 1 | import React from 'https://dev.jspm.io/react'; 2 | import ReactDomServer from 'https://dev.jspm.io/react-dom/server'; 3 | import ReactDom from 'https://dev.jspm.io/react-dom'; 4 | 5 | import rsh from 'https://dev.jspm.io/react-syntax-highlighter'; 6 | import codeStyles from 'https://dev.jspm.io/npm:react-syntax-highlighter@15.3.1/dist/cjs/styles/prism'; 7 | 8 | 9 | const realRSH: any = rsh; 10 | const realCodeStyles: any = codeStyles; 11 | 12 | const CodeBlock = realRSH.Prism; 13 | const { dracula } = realCodeStyles; 14 | 15 | dracula['pre[class*="language-"]'].background = 'rgba(5, 5, 5, 0.93)'; 16 | 17 | export { 18 | React, 19 | ReactDomServer, 20 | ReactDom, 21 | CodeBlock, 22 | dracula 23 | }; 24 | -------------------------------------------------------------------------------- /staticFileMiddleware.ts: -------------------------------------------------------------------------------- 1 | import { Context, send } from './serverDeps.ts'; 2 | 3 | export const staticFileMiddleware = async (ctx: Context, next: Function) => { 4 | const path = `${Deno.cwd()}/client${ctx.request.url.pathname}`; 5 | if (await fileExists(path)) { 6 | await send(ctx, ctx.request.url.pathname, { 7 | root: `${Deno.cwd()}/client`, 8 | }); 9 | } else { 10 | await next(); 11 | } 12 | }; 13 | 14 | async function fileExists(path: string) { 15 | try { 16 | const stats = await Deno.lstat(path); 17 | return stats && stats.isFile; 18 | } catch (e) { 19 | if (e && e instanceof Deno.errors.NotFound) { 20 | return false; 21 | } else { 22 | throw e; 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /client/Components/Main.tsx: -------------------------------------------------------------------------------- 1 | import { React } from '../../deps.ts'; 2 | import ObsidianLogo from './ObsidianLogo.tsx'; 3 | import SideBar from './SideBar.tsx'; 4 | 5 | declare global { 6 | namespace JSX { 7 | interface IntrinsicElements { 8 | div: any; 9 | img: any; 10 | } 11 | } 12 | } 13 | 14 | const Main = (props: any) => { 15 | return ( 16 | <> 17 |
18 |
19 | {/* */} 20 | 21 |
22 |
23 | 24 | 25 | ); 26 | }; 27 | 28 | export default Main; 29 | -------------------------------------------------------------------------------- /client/Components/MainContainer.tsx: -------------------------------------------------------------------------------- 1 | import { React } from '../../deps.ts'; 2 | import Main from './Main.tsx'; 3 | import About from './About.tsx'; 4 | import Demo from './Demo.tsx'; 5 | import Docs from './Docs.tsx'; 6 | 7 | declare global { 8 | namespace JSX { 9 | interface IntrinsicElements { 10 | div: any; 11 | } 12 | } 13 | } 14 | 15 | const MainContainer = (props: any) => { 16 | const { page } = props; 17 | 18 | let curPage; 19 | if (page === 'home') curPage =
; 20 | if (page === 'about') curPage = ; 21 | if (page === 'demo') curPage = ; 22 | if (page === 'docs') curPage = ; 23 | 24 | return <>{curPage}; 25 | }; 26 | 27 | export default MainContainer; 28 | -------------------------------------------------------------------------------- /client/Components/NestedCache.tsx: -------------------------------------------------------------------------------- 1 | import { React } from '../../deps.ts'; 2 | 3 | declare global { 4 | namespace JSX { 5 | interface IntrinsicElements { 6 | div: any; 7 | p: any; 8 | } 9 | } 10 | } 11 | 12 | const NestedCache = (props: any) => { 13 | const { cache } = props; 14 | const cachedPair: any = []; 15 | 16 | Object.entries(cache).forEach((pair, i) => { 17 | if (pair[1] instanceof Object) { 18 | cachedPair.push(

{pair[0]} :

) 19 | } else { 20 | cachedPair.push(

{pair[0]} : {JSON.stringify(pair[1])}

) 21 | } 22 | }) 23 | 24 | return ( 25 |
26 | {'{'}{cachedPair}{'}'} 27 |
28 | ); 29 | }; 30 | 31 | export default NestedCache; 32 | -------------------------------------------------------------------------------- /client/Components/Cache.tsx: -------------------------------------------------------------------------------- 1 | import { React } from '../../deps.ts'; 2 | import NestedCache from './NestedCache.tsx'; 3 | 4 | declare global { 5 | namespace JSX { 6 | interface IntrinsicElements { 7 | div: any; 8 | p: any; 9 | } 10 | } 11 | } 12 | 13 | const Cache = (props: any) => { 14 | const { cache } = props; 15 | 16 | const cachedPair: any = []; 17 | 18 | Object.entries(cache).forEach((pair, i, arr) => { 19 | if(pair[0] === 'ROOT_MUTATION' || pair[0] === 'ROOT_QUERY') return; 20 | cachedPair.push( 21 |

22 | {pair[0]} : , 23 |

24 | ); 25 | }); 26 | 27 | return ( 28 | <> 29 | {'{'}{cachedPair}{'}'} 30 | 31 | ); 32 | }; 33 | 34 | export default Cache; -------------------------------------------------------------------------------- /client/Components/Demo.tsx: -------------------------------------------------------------------------------- 1 | import { React } from '../../deps.ts'; 2 | import SideBar from './SideBar.tsx'; 3 | 4 | declare global { 5 | namespace JSX { 6 | interface IntrinsicElements { 7 | div: any; 8 | br: any; 9 | pre: any; 10 | code: any; 11 | label: any; 12 | select: any; 13 | option: any; 14 | p: any; 15 | input: any; 16 | } 17 | } 18 | } 19 | 20 | const Demo = (props: any) => { 21 | return ( 22 | <> 23 |
24 | {/*
*/} 25 | 30 | {/*
*/} 31 |
32 | {/* */} 33 | 34 | ); 35 | }; 36 | 37 | export default Demo; 38 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "jsx": "react", 4 | "target": "es6" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */, 5 | "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */, 6 | "lib": [ 7 | "DOM", 8 | "ES2017", 9 | "deno.ns" 10 | ] /* Specify library files to be included in the compilation. */, 11 | "strict": true /* Enable all strict type-checking options. */, 12 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */, 13 | "skipLibCheck": true /* Skip type checking of declaration files. */, 14 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /client/Components/TeamMember.jsx: -------------------------------------------------------------------------------- 1 | import { React } from '../../deps.ts'; 2 | 3 | const TeamMember = ({ 4 | user: { firstName, lastName, image, info, linkedin, github }, 5 | }) => { 6 | return ( 7 |
8 |
9 |
17 |
{`${firstName} ${lastName}`}
18 | 27 |

{info}

28 |
29 |
30 | ); 31 | }; 32 | export default TeamMember; 33 | -------------------------------------------------------------------------------- /client/Components/SideBarContext/DemoContext.tsx: -------------------------------------------------------------------------------- 1 | import { React } from '../../../deps.ts'; 2 | 3 | declare global { 4 | namespace JSX { 5 | interface IntrinsicElements { 6 | div: any; 7 | h4: any; 8 | h6: any; 9 | p: any; 10 | } 11 | } 12 | } 13 | 14 | const DemoContext = (props: any) => { 15 | return ( 16 |
17 |
18 |

22 | Demo Guide 23 |

24 | 25 |
26 |
Execute A Query
27 |
28 |

33 | Utilize the input fields on the left to make a GraphQL query or mutation. 34 |

35 |
36 |
37 | 38 |
39 |
Response
40 |
41 |

45 | Moments after the query is excecuted, the raw response from the 46 | GraphQL API is displayed. 47 |

48 |
49 |
50 |
51 |
Cache
52 |
53 |

57 | Finally, we can see our destructured query and responses which are 58 | currently stored in a Redis cache. If you query for a specific 59 | property that is stored in the cache, the Obsidian algorithm will 60 | find and return it. Eliminating the need to query the database again. 61 |

62 |
63 |
64 |
65 |
66 | ); 67 | }; 68 | 69 | export default DemoContext; 70 | -------------------------------------------------------------------------------- /client/Components/DocPages/Advanced/Polling.tsx: -------------------------------------------------------------------------------- 1 | import { React, CodeBlock, dracula } from '../../../../deps.ts'; 2 | 3 | const Polling = (props: any) => { 4 | return ( 5 |
6 |

Polling

7 |

8 | Some applications require regular streams of GraphQL queries sent via 9 | HTTP requests. If you find yourself in this position, 10 | obsidian can help. The 11 | query method accepts an 12 | optional 'pollInterval' property in its options parameter, which 13 | provides near-real-time synchronization with your server by causing a 14 | query to execute periodically and will automatically update the cache 15 | consistently. To enable polling for a query, pass a 'pollInterval' 16 | configuration option to the{' '} 17 | query method with a specified 18 | interval in milliseconds: 19 |

20 | 21 | {`function actorsPhoto() { 22 | const interval = query(GET_ACTOR_PHOTO, { 23 | pollInterval: 2000, 24 | }); 25 | setInterval(interval); // react hook to add the interval in the state 26 | return ( 27 |
28 |

Actor Showcase

29 |

Check out our favorite actor photo by clicking the button below

30 | 37 |

Name: {actor.name}

38 | 39 |
40 | ); 41 | }`} 42 |
43 |
44 |

45 | This query will be sent every 2 seconds, updating the cache via the 46 | default normalized caching strategy if any changes are found. You can 47 | also stop polling dynamically with the{' '} 48 | stopPollInterval method on the 49 | cache class using the interval returned by the invocation of the{' '} 50 | obsidian query, here an example: 51 |

52 | 53 | {`const { cache } = useObsidian(); 54 | const { stopPollInterval } = cache; 55 | stopPollInterval(interval);`} 56 | 57 |
58 | ); 59 | }; 60 | 61 | export default Polling; 62 | -------------------------------------------------------------------------------- /client/Components/Docs.tsx: -------------------------------------------------------------------------------- 1 | import { React } from '../../deps.ts'; 2 | 3 | import Overview from './DocPages/Overview.tsx'; 4 | import Polling from './DocPages/Advanced/Polling.tsx'; 5 | 6 | import BasicsErrors from './DocPages/Basics/Errors.tsx'; 7 | import GettingStarted from './DocPages/Basics/GettingStarted.tsx'; 8 | 9 | import Mutations from './DocPages/Basics/Mutations.tsx'; 10 | import Queries from './DocPages/Basics/Queries.tsx'; 11 | import ServerSideRendering from './DocPages/Basics/ServerSideRendering.tsx'; 12 | 13 | import Client from './DocPages/Caching/Client.tsx'; 14 | import CachingErrors from './DocPages/Caching/Errors.tsx'; 15 | 16 | import Server from './DocPages/Caching/Server.tsx'; 17 | 18 | import Strategies from './DocPages/Caching/Strategies.tsx'; 19 | import Philosophy from './DocPages/Philosophy.tsx'; 20 | import QuickStart from './DocPages/QuickStart.tsx'; 21 | 22 | import SideBar from './SideBar.tsx'; 23 | 24 | declare global { 25 | namespace JSX { 26 | interface IntrinsicElements { 27 | [elemName: string]: any; 28 | } 29 | } 30 | } 31 | 32 | const Docs = (props: any) => { 33 | const [docsPage, setDocsPage] = (React as any).useState('QuickStart'); 34 | 35 | let curDocsPage; 36 | if (docsPage === 'QuickStart') curDocsPage = ; 37 | if (docsPage === 'Overview') curDocsPage = ; 38 | if (docsPage === 'Philosophy') curDocsPage = ; 39 | 40 | if (docsPage === 'Polling') curDocsPage = ; 41 | 42 | if (docsPage === 'BasicsErrors') 43 | curDocsPage = ; 44 | if (docsPage === 'GettingStarted') 45 | curDocsPage = ; 46 | if (docsPage === 'Mutations') 47 | curDocsPage = ; 48 | if (docsPage === 'Queries') 49 | curDocsPage = ; 50 | if (docsPage === 'ServerSideRendering') curDocsPage = ; 51 | 52 | if (docsPage === 'Client') curDocsPage = ; 53 | if (docsPage === 'CachingErrors') curDocsPage = ; 54 | if (docsPage === 'Server') curDocsPage = ; 55 | if (docsPage === 'Strategies') 56 | curDocsPage = ; 57 | 58 | return ( 59 | <> 60 |
61 |
{curDocsPage}
62 |
63 | 68 | 69 | ); 70 | }; 71 | 72 | export default Docs; 73 | -------------------------------------------------------------------------------- /client/Components/DocPages/Basics/Errors.tsx: -------------------------------------------------------------------------------- 1 | import { React, CodeBlock, dracula } from '../../../../deps.ts'; 2 | 3 | const BasicsErrors = (props: any) => { 4 | return ( 5 |
6 |

Errors

7 |

8 | This chapter will cover the most common errors related to{' '} 9 | obsidian implementation. 10 |

11 |

ObsidianRouter

12 |

Connection Refused

13 |

14 | This error is likely to arise if you have not disabled the server-side 15 | cache and/or do not have an active redis instance at port 6379. To learn 16 | more about redis and server-side caching, check out the{' '} 17 | props.setDocsPage('Server')}> 18 | Caching 19 | {' '} 20 | section. 21 |

22 |

ObsidianWrapper

23 |

Cache is returning undefined

24 |

25 | Check to see if your query and/or mutation is being sent with an id 26 | field, as not providing one prevents normalization of the response 27 | object 28 |

29 |

Your app doesn't work after wrapping it with Obsidian

30 |

31 | Check to make sure you've wrapped your app at the proper level. You 32 | should be wrapping your app at the top-level component, but not in the 33 | server, as this will require wrapping the app in the hydrate method as 34 | well. For example, if your top level component is App, App should be 35 | used in the renderToString{' '} 36 | method and in the hydrate{' '} 37 | methods, and ObsidianWrapper should be the first child of App. Check out 38 | the chapter on server-side rendering to see an example. 39 |

40 |

Recap & Next Up

41 |

42 | This section has walked through a simple implementation of{' '} 43 | obsidian with ObsidianRouter and 44 | ObsidianWrapper, and covered the most common use cases and errors. Next, 45 | we're going to dive into caching and{' '} 46 | obsidian's design philosophy and 47 | guiding development principles. Once we have a firm grasp on how{' '} 48 | obsidian approaches GraphQL 49 | caching, we'll examine the specifics of server-side and client-side 50 | caching in obsidian. 51 |

52 |
53 | ); 54 | }; 55 | 56 | export default BasicsErrors; 57 | -------------------------------------------------------------------------------- /client/static/prism.css: -------------------------------------------------------------------------------- 1 | /* PrismJS 1.21.0 2 | https://prismjs.com/download.html#themes=prism&languages=markup+css+clike+javascript */ 3 | /** 4 | * prism.js default theme for JavaScript, CSS and HTML 5 | * Based on dabblet (http://dabblet.com) 6 | * @author Lea Verou 7 | */ 8 | 9 | /* 10 | 11 | code[class*="language-"], 12 | pre[class*="language-"] { 13 | color: black; 14 | background: none; 15 | text-shadow: 0 1px white; 16 | font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace; 17 | font-size: 1em; 18 | text-align: left; 19 | white-space: pre; 20 | word-spacing: normal; 21 | word-break: normal; 22 | word-wrap: normal; 23 | line-height: 1.5; 24 | 25 | -moz-tab-size: 4; 26 | -o-tab-size: 4; 27 | tab-size: 4; 28 | 29 | -webkit-hyphens: none; 30 | -moz-hyphens: none; 31 | -ms-hyphens: none; 32 | hyphens: none; 33 | } 34 | 35 | pre[class*="language-"]::-moz-selection, pre[class*="language-"] ::-moz-selection, 36 | code[class*="language-"]::-moz-selection, code[class*="language-"] ::-moz-selection { 37 | text-shadow: none; 38 | background: #b3d4fc; 39 | } 40 | 41 | pre[class*="language-"]::selection, pre[class*="language-"] ::selection, 42 | code[class*="language-"]::selection, code[class*="language-"] ::selection { 43 | text-shadow: none; 44 | background: #b3d4fc; 45 | } 46 | 47 | @media print { 48 | code[class*="language-"], 49 | pre[class*="language-"] { 50 | text-shadow: none; 51 | } 52 | } 53 | 54 | /* Code blocks */ 55 | pre[class*="language-"] { 56 | padding: 1em; 57 | margin: .5em 0; 58 | overflow: auto; 59 | } 60 | 61 | :not(pre) > code[class*="language-"], 62 | pre[class*="language-"] { 63 | background: #f5f2f0; 64 | } 65 | 66 | /* Inline code */ 67 | :not(pre) > code[class*="language-"] { 68 | padding: .1em; 69 | border-radius: .3em; 70 | white-space: normal; 71 | } 72 | 73 | .token.comment, 74 | .token.prolog, 75 | .token.doctype, 76 | .token.cdata { 77 | color: slategray; 78 | } 79 | 80 | .token.punctuation { 81 | color: #999; 82 | } 83 | 84 | .token.namespace { 85 | opacity: .7; 86 | } 87 | 88 | .token.property, 89 | .token.tag, 90 | .token.boolean, 91 | .token.number, 92 | .token.constant, 93 | .token.symbol, 94 | .token.deleted { 95 | color: #905; 96 | } 97 | 98 | .token.selector, 99 | .token.attr-name, 100 | .token.string, 101 | .token.char, 102 | .token.builtin, 103 | .token.inserted { 104 | color: #690; 105 | } 106 | 107 | .token.operator, 108 | .token.entity, 109 | .token.url, 110 | .language-css .token.string, 111 | .style .token.string { 112 | color: #9a6e3a; 113 | /* This background color was intended by the author of this theme. */ 114 | background: hsla(0, 0%, 100%, .5); 115 | } 116 | 117 | .token.atrule, 118 | .token.attr-value, 119 | .token.keyword { 120 | color: #07a; 121 | } 122 | 123 | .token.function, 124 | .token.class-name { 125 | color: #DD4A68; 126 | } 127 | 128 | .token.regex, 129 | .token.important, 130 | .token.variable { 131 | color: #e90; 132 | } 133 | 134 | .token.important, 135 | .token.bold { 136 | font-weight: bold; 137 | } 138 | .token.italic { 139 | font-style: italic; 140 | } 141 | 142 | .token.entity { 143 | cursor: help; 144 | } 145 | 146 | */ 147 | -------------------------------------------------------------------------------- /client/Components/DocPages/Philosophy.tsx: -------------------------------------------------------------------------------- 1 | import { React, CodeBlock, dracula } from '../../../deps.ts'; 2 | 3 | const Philosophy = (props: any) => { 4 | return ( 5 |
6 |

Philosophy

7 |

8 | In this chapter we seek to lay bare our approach to GraphQL caching, 9 | providing context for our caching implementations in each part of{' '} 10 | obsidian. 11 |

12 |

Core Values

13 |

Fast

14 |

15 | Caching with obsidian should be nearly imperceptible - a client or 16 | server module should feel no slower with a caching layer 17 |

18 |

19 | While building obsidian's 20 | caching strategies, speed reigned supreme. We believe that a caching 21 | layer for your GraphQL service has to be fast and efficient. Your API 22 | should not take a large performance hit after incorporating a cache. 23 |

24 |

Flexible

25 |

26 | Caching with obsidian should be developer friendly without sacrificing 27 | performance{' '} 28 |

29 |

30 | obsidian's flexible API is 31 | designed to be performant, no matter how big or small your project is. 32 | Our caching solutions do not require strict conventions or a schema on 33 | the client side to be able to offer efficiency and expandability for the 34 | developer. 35 |

36 |

Consistent

37 |

38 | Caching with obsidian should prize consistency - discerning truth should 39 | be automatic and transparent 40 |

41 |

42 | Many GraphQL caching solutions give up on consistency and mutations, 43 | choosing to give the tools to the developer and letting go of the 44 | reigns. With obsidian, 45 | consistent truth is a priority and a responsibility, rather than an 46 | afterthought. 47 |

48 |

Integration

49 |

50 | obsidian was built for Deno, and 51 | as such it has been designed to integrate into React apps the way they 52 | are built in Deno, on both the frontend and backend. While{' '} 53 | obsidian is still in it's early 54 | stages, we are actively seeking to define what that means. For now, Oak 55 | has proven to be Deno's prevailing framework, and React apps are 56 | typically constructed with server-side rendering. 57 |

58 |

59 | We choose to view these restrictions as inspiration for innovation, and 60 | we have applied that philosophy to our caching implementations. By 61 | adhering to pre-defined scaffolding, opportunities to improve upon 62 | existing methodologies readily present themselves. Whenever possible, we 63 | chose to build in favor of the above core values, and we hope they can 64 | guide your implementation of{' '} 65 | obsidian. 66 |

67 |

Recap & Next Up

68 |

69 | Now that we have a grasp on the guiding principles behind the 70 | development of obsidian and it's 71 | integration into your application, we'll move onto a deeper dive into 72 | the caching capabilities of{' '} 73 | obsidian. 74 |

75 |
76 | ); 77 | }; 78 | 79 | export default Philosophy; 80 | -------------------------------------------------------------------------------- /client/static/github-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /client/Components/SideBar.tsx: -------------------------------------------------------------------------------- 1 | import { React } from '../../deps.ts'; 2 | import DocsContext from './SideBarContext/DocsContext.tsx'; 3 | import MainContext from './SideBarContext/MainContext.tsx'; 4 | import AboutContext from './SideBarContext/AboutContext.jsx'; 5 | import DemoContext from './SideBarContext/DemoContext.tsx'; 6 | 7 | declare global { 8 | namespace JSX { 9 | interface IntrinsicElements { 10 | div: any; 11 | img: any; 12 | h4: any; 13 | a: any; 14 | hr: any; 15 | button: any; 16 | footer: any; 17 | } 18 | } 19 | } 20 | 21 | const NavBar = (props: any) => { 22 | const { page, docsPage, setDocsPage } = props; 23 | 24 | let curContext; 25 | 26 | if (page === 'home') curContext = ; 27 | if (page === 'about') curContext = ; 28 | if (page === 'demo') curContext = ; 29 | if (page === 'docs') 30 | curContext = ; 31 | 32 | const [open, setOpen] = (React as any).useState(null); 33 | 34 | const createSidebarStyle = () => { 35 | let styleObj = { height: '94%', position: 'relative', top: '0%' }; 36 | if ((window as any).innerWidth < 600) { 37 | styleObj = open ? { height: '70%', position: 'absolute', top: '30%' } : { height: '20%', position: 'absolute', top: '75%' }; 38 | } 39 | const homeAbout = { backgroundColor: 'rgba(0,0,0,0)' }; 40 | 41 | return page === 'home'|| page ==='about' ? Object.assign(styleObj, homeAbout) : styleObj; 42 | }; 43 | 44 | const sidebarStyle = createSidebarStyle(); 45 | 46 | return ( 47 |
52 | 70 | 83 |
91 | {curContext} 92 |
93 |
94 | ); 95 | }; 96 | 97 | export default NavBar; 98 | -------------------------------------------------------------------------------- /client/Components/DocPages/Caching/Server.tsx: -------------------------------------------------------------------------------- 1 | import { React, CodeBlock, dracula } from '../../../../deps.ts'; 2 | 3 | const Server = (props: any) => { 4 | return ( 5 |
6 |

Server

7 |

8 | In this chapter, we'll dig into caching in ObsidianRouter, as well as 9 | redis implementation. 10 |

11 |

Server-Side Caching with Obsidian

12 |

13 | Obsidian utilizes a local redis server to store our server-side cache, 14 | keeping our cache fast and responsive while maintaining ACID compliance. 15 | To learn how to setup redis on your machine, check out the{' '} 16 | quick start{' '} 17 | documentation. 18 |

19 |

20 | By default, your redis server should be running at port 6379. If you 21 | need to change your port, you may do so inside the ObsidianRouter 22 | options. 23 |

24 |

ObsidianRouter

25 |

26 | ObsidianRouter setup requires an options object, which we demonstrated 27 | in the Getting Started chapter. The options object accepts the following 28 | properties, only three of which are required: 29 |

30 |
    31 |
  • 32 | Router - (required) You must provide the Oak Router 33 | class 34 |
  • 35 |
  • 36 | path - (optional, default:{' '} 37 | '/graphql') Your GraphQL 38 | service will be available at this endpoint 39 |
  • 40 |
  • 41 | typeDefs - (required) Your GraphQL schema goes here 42 |
  • 43 |
  • 44 | resolvers - (required) Your resolvers go here 45 |
  • 46 |
  • 47 | context - (optional) A function to alter your Oak 48 | server's context object upon entering the router 49 |
  • 50 |
  • 51 | usePlayground - (optional, default:{' '} 52 | true) Set to{' '} 53 | false to disable the GraphQL 54 | Playground. Recommended when deploying your application in production 55 |
  • 56 |
  • 57 | useCache - (optional, default:{' '} 58 | true) Set to{' '} 59 | false to disable server-side 60 | caching. This negates the caching layer, and institutes a cacheless 61 | caching strategy in ObsidianRouter 62 |
  • 63 |
  • 64 | redisPort - (optional, default:{' '} 65 | 6379) If your redis server is 66 | running at a different port, specify it here 67 |
  • 68 |
  • 69 | maxQueryDepth - (optional, default:{' '} 70 | 0) If set to integer of 1 or 71 | more, returns error in response to request object with nesting depth 72 | greater than the maxQueryDepth and does not route request to GraphQL 73 | server. Feature is disabled by default. 74 |
  • 75 |
76 |

An example of ObsidianRouter with optional parameters:

77 | 78 | {`const GraphQLRouter = await ObsidianRouter({ 79 | Router, 80 | path = '/graphql', 81 | typeDefs, 82 | resolvers, 83 | context, 84 | usePlayground = false, 85 | useCache = true, 86 | redisPort = 6379, 87 | maxQueryDepth = 3, 88 | });`} 89 | 90 |
91 |

Recap

92 |

93 | This section has walked through caching in{' '} 94 | obsidian, including our approach 95 | to GraphQL caching and the various caching strategies{' '} 96 | obsidian offers. 97 |

98 |
99 | ); 100 | }; 101 | 102 | export default Server; 103 | -------------------------------------------------------------------------------- /server.tsx: -------------------------------------------------------------------------------- 1 | import { Application, Router } from './serverDeps.ts'; 2 | import { React, ReactDomServer } from './deps.ts'; 3 | import App from './client/app.tsx'; 4 | import { staticFileMiddleware } from './staticFileMiddleware.ts'; 5 | 6 | const PORT = 3000; 7 | 8 | // Create a new server 9 | const app = new Application(); 10 | 11 | // Track response time in headers of responses 12 | app.use(async (ctx, next) => { 13 | await next(); 14 | const rt = ctx.response.headers.get('X-Response-Time'); 15 | console.log(`${ctx.request.method} ${ctx.request.url} - ${rt}`); 16 | }); 17 | 18 | app.use(async (ctx, next) => { 19 | const start = Date.now(); 20 | await next(); 21 | const ms = Date.now() - start; 22 | ctx.response.headers.set('X-Response-Time', `${ms}ms`); 23 | }); 24 | const initialState = { 25 | obsidianSchema: { 26 | returnTypes: { 27 | Country: { kind: 'NamedType', type: 'Country' }, 28 | }, 29 | argTypes: { 30 | Country: { _id: 'ID' }, 31 | }, 32 | obsidianTypeSchema: { 33 | Country: { 34 | _id: { type: 'ID', scalar: true }, 35 | name: { type: 'String', scalar: true }, 36 | capital: { type: 'String', scalar: true }, 37 | population: { type: 'Int', scalar: true }, 38 | flag: { type: 'Flag', scalar: false }, 39 | borders: { type: 'Country', scalar: false }, 40 | }, 41 | Flag: { 42 | _id: { type: 'ID', scalar: true }, 43 | emoji: { type: 'String', scalar: true }, 44 | }, 45 | }, 46 | }, 47 | }; 48 | 49 | // Router for base path 50 | const router = new Router(); 51 | 52 | router.get('/', handlePage); 53 | 54 | // Bundle the client-side code 55 | // const [_, clientJS] = await Deno.bundle('./client/client.tsx'); 56 | 57 | const {files, diagnostics } = await Deno.emit('./client/client.tsx', {bundle: 'module'}); 58 | 59 | // Router for bundle 60 | const serverrouter = new Router(); 61 | serverrouter.get('/static/client.js', (context) => { 62 | context.response.headers.set('Content-Type', 'text/html'); 63 | // context.response.body = clientJS; 64 | context.response.body = files['deno:///bundle.js']; 65 | }); 66 | 67 | // Implement the routes on the server 68 | app.use(staticFileMiddleware); 69 | app.use(router.routes()); 70 | app.use(serverrouter.routes()); 71 | app.use(router.allowedMethods()); 72 | 73 | app.addEventListener('listen', () => { 74 | console.log(`Listening at http://localhost:${PORT}`); 75 | }); 76 | 77 | if (import.meta.main) { 78 | await app.listen({ port: PORT }); 79 | } 80 | 81 | export { app }; 82 | 83 | 84 | 85 | 86 | function handlePage(ctx: any) { 87 | try { 88 | const body = (ReactDomServer as any).renderToString(); 89 | ctx.response.body = ` 90 | 91 | 92 | 93 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 110 | 111 | 112 | Obsidian 113 | 116 | 117 | 118 |
${body}
119 | 120 | 121 | 122 | `; 123 | } catch (error) { 124 | console.error(error); 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /client/Components/DocPages/Caching/Client.tsx: -------------------------------------------------------------------------------- 1 | import { React, CodeBlock, dracula } from '../../../../deps.ts'; 2 | 3 | const Client = (props: any) => { 4 | return ( 5 |
6 |

Client

7 |

8 | In this chapter, we'll cover how to specify your caching strategy in 9 | ObsidianWrapper. 10 |

11 |

ObsidianWrapper

12 |

13 | While working with ObsidianWrapper, you have the opportunity to 14 | configure caching strategies for specific queries and mutations. 15 |

16 |

query

17 |

18 | Obsidian queries can be configured in five ways through an options 19 | parameter that takes in a configuration object: 20 |

21 | 22 | {`const { 23 | endpoint = '/graphql', 24 | cacheRead = true, 25 | cacheWrite = true, 26 | pollInterval = null, 27 | wholeQuery = false, 28 | } = options; 29 | query(\`query { getMovie { id title releaseYear } }\`, options) 30 | .then(resp => console.log(resp))`} 31 | 32 |
    33 |
  • 34 | endpoint - (default:{' '} 35 | '/graphql') The endpoint where{' '} 36 | obsidian should send all 37 | requests by default 38 |
  • 39 |
  • 40 | cacheRead - (default:{' '} 41 | true) When this option is 42 | enabled, Obsidian will check the cache to see if the relevant data is 43 | available before querying the server 44 |
  • 45 |
  • 46 | cacheWrite - (default:{' '} 47 | true) When this option is 48 | enabled, Obsidian will write responses to the cache 49 |
  • 50 |
  • 51 | pollInterval - (default:{' '} 52 | null) Turns query polling on 53 | and specifies how often Obsidian should send a GraphQL query to the 54 | server 55 |
  • 56 |
  • 57 | wholeQuery - (default:{' '} 58 | false) Enables whole query 59 | caching 60 |
  • 61 |
62 |
63 |

mutate

64 |

65 | Obsidian mutations can be configured in four ways through an options 66 | parameter that takes in a configuration object: 67 |

68 | 69 | {`const { 70 | endpoint = '/graphql', 71 | cache = true, 72 | toDelete = true, 73 | update = null, 74 | } = options; 75 | mutate(\`mutation addFavoriteMovie{favoriteMovie(id:4){ __typename id isFavorite}}\`, options) 76 | .then(resp => console.log(resp))`} 77 | 78 |
    79 |
  • 80 | endpoint - (default:{' '} 81 | '/graphql') The endpoint where{' '} 82 | obsidian should send this 83 | request 84 |
  • 85 |
  • 86 | cache - (default:{' '} 87 | true) When this option is 88 | enabled, obsidian will 89 | automatically update the cache after a mutation. 90 |
  • 91 |
  • 92 | toDelete - (default:{' '} 93 | false) When this option is set 94 | to true, it will indicate to 95 | obsidian that a delete 96 | mutation is being sent and to update the value of that hash in the 97 | cache with 'DELETED'. 98 |
  • 99 |
  • 100 | update - (default:{' '} 101 | null) Optional update function 102 | to customize cache updating behavior. 103 |
  • 104 |
105 |
106 |

Recap & Next Up

107 |

108 | In this chapter we learned how to control our caching strategy in the{' '} 109 | obsidian client, 110 | ObsidianWrapper. Next, we'll dive into caching options in 111 | ObsidianRouter. 112 |

113 |
114 | ); 115 | }; 116 | 117 | export default Client; 118 | -------------------------------------------------------------------------------- /client/Components/DocPages/Overview.tsx: -------------------------------------------------------------------------------- 1 | import { React, CodeBlock, dracula } from '../../../deps.ts'; 2 | 3 | const Overview = (props: any) => { 4 | return ( 5 |
6 |

Overview

7 |

8 | obsidian is a{' '} 9 | Deno{' '} 10 | GraphQL server module and a GraphQL 11 | client, built to optimize application performance via caching 12 | strategies. obsidian's server 13 | module may be used independently to quickly build out a GraphQL API 14 | while enabling the full suite of caching strategies that{' '} 15 | obsidian offers. 16 |

17 |

18 | obsidian simplifies GraphQL 19 | implementations in Deno by offering a solution integrated across the 20 | stack, ensuring your API stays lean and performant even as it's scope 21 | grows. By utilizing obsidian and 22 | the power of server-side rendering, we can maintain rapid page-load 23 | times. 24 |

25 |

The Module

26 |

27 | obsidian is a Deno module, 28 | published at deno.land. 29 | There are two distinct parts to{' '} 30 | obsidian: ObsidianRouter, a 31 | caching GraphQL router built upon Deno's{' '} 32 | Oak server framework, and 33 | ObsidianWrapper, a React component 34 | that functions as a GraphQL client for your application, providing 35 | global access to fetching and caching capabilities. 36 |

37 |

38 | It is important to note that ObsidianRouter can be implemented as a 39 | standalone GraphQL router for your server, and third-party client 40 | implementations may be used in your client-side code. ObsidianWrapper 41 | can also be used independently of ObsidianRouter if normalized caching 42 | capabilities are not needed or desired. However, ObsidianWrapper's 43 | normalized caching strategy is only available when used in conjunction 44 | with ObsidianRouter- this choice was made to preserve the destructuring 45 | and normalizing algorithm that is at the heart of{' '} 46 | obsidian's caching strategy. 47 |

48 |

49 | This two-pronged approach enables{' '} 50 | obsidian to guide your GraphQL 51 | implementation every step of the way, and brings clarity and speed to 52 | every part of your app. 53 |

54 |

55 | NOTE - To keep our server-side cache efficient and ACID 56 | compliant, obsidian uses redis 57 | to store cached data. If you are not utilizing the caching features of 58 | ObsidianRouter, you do not need to have a running redis server. 59 |

60 |

The Documentation

61 |

We've split our documentation into four distinct sections.

62 |
    63 |
  • 64 | Basics guides us through a simple implementation of 65 | full stack obsidian, detailing 66 | the setup of ObsidianRouter, ObsidianWrapper, usage patterns in React, 67 | and how to harness server-side rendering to improve your client 68 | caching strategy. 69 |
  • 70 |
  • 71 | Philosophy provides a high-level overview of caching 72 | in obsidian, revealing our 73 | vision of obsidian integration 74 | and guiding principles for development. 75 |
  • 76 |
  • 77 | Caching covers the different caching strategies{' '} 78 | obsidian offers and draws upon 79 | our Philosophy to illustrate their implementations in ObsidianRouter 80 | and ObsidianWrapper. 81 |
  • 82 |
  • 83 | Advanced contains guides for more specialized options 84 | and use-cases that we won't need while first exploring{' '} 85 | obsidian. 86 |
  • 87 |
88 |

89 | We hope you enjoy working with{' '} 90 | obsidian! 91 |

92 |
93 | ); 94 | }; 95 | 96 | export default Overview; 97 | -------------------------------------------------------------------------------- /client/Components/SideBarContext/DocsContext.tsx: -------------------------------------------------------------------------------- 1 | import { React } from '../../../deps.ts'; 2 | 3 | const DocsContext = (props: any) => { 4 | const { setDocsPage } = props; 5 | 6 | return ( 7 |
8 |
9 | 19 | 29 |
30 |
Basics
31 |
32 | 42 | 52 | 62 | 72 | 82 |
83 |
84 | 94 |
95 |
Caching
96 |
97 | 107 | 117 | 127 | {/**/} 133 |
134 |
135 |
136 |
Advanced
137 |
138 | 148 |
149 |
150 |
151 |
152 | ); 153 | }; 154 | 155 | export default DocsContext; 156 | -------------------------------------------------------------------------------- /client/Components/DocPages/QuickStart.tsx: -------------------------------------------------------------------------------- 1 | import { React, CodeBlock, dracula } from '../../../deps.ts'; 2 | 3 | const QuickStart = (props: any) => { 4 | return ( 5 |
6 |

Quick Start

7 |

8 | obsidian is Deno's first native 9 | GraphQL caching client and server module. Boasting lightning-fast 10 | caching and fetching capabilities alongside headlining normalization and 11 | destructuring strategies,{' '} 12 | obsidian is equipped to support 13 | scalable, highly performant applications. 14 |

15 |

16 | Optimized for use in server-side rendered React apps built with Deno, 17 | full stack integration of{' '} 18 | obsidian enables many of its 19 | most powerful features, including optimized caching exchanges between 20 | client and server and extremely lightweight client-side caching. 21 |

22 |

Installation

23 |

In the server:

24 | 25 | { 26 | "import { ObsidianRouter } from 'https://deno.land/x/obsidian/mod.ts';" 27 | } 28 | 29 |
30 |

In the app:

31 | 32 | { 33 | "import { ObsidianWrapper } from 'https://deno.land/x/obsidian/clientMod.ts';" 34 | } 35 | 36 |
37 |

Creating the Router

38 | 39 | {`import { Application, Router } from 'https://deno.land/x/oak@v6.0.1/mod.ts'; 40 | import { ObsidianRouter, gql } from 'https://deno.land/x/obsidian/mod.ts'; 41 | 42 | const PORT = 8000; 43 | 44 | const app = new Application(); 45 | 46 | const types = (gql as any)\` 47 | // Type definitions 48 | \`; 49 | 50 | const resolvers = { 51 | // Resolvers 52 | } 53 | 54 | interface ObsRouter extends Router { 55 | obsidianSchema?: any; 56 | } 57 | 58 | const GraphQLRouter = await ObsidianRouter({ 59 | Router, 60 | typeDefs: types, 61 | resolvers: resolvers, 62 | redisPort: 6379, 63 | }); 64 | 65 | const router = new Router(); 66 | router.get('/', handlePage); 67 | 68 | function handlePage(ctx: any) { 69 | try { 70 | const body = (ReactDomServer as any).renderToString(); 71 | ctx.response.body = \` 72 | 73 | 74 | 75 | SSR React App 76 | 77 | 78 |
\${body}
79 | 80 | 81 | \`; 82 | } catch (error) { 83 | console.error(error); 84 | 85 | app.use(GraphQLRouter.routes(), GraphQLRouter.allowedMethods()); 86 | 87 | await app.listen({ port: PORT });`} 88 |
89 |
90 |

Creating the Wrapper

91 | 92 | {`import { ObsidianWrapper } from 'https://deno.land/x/obsidian/clientMod.ts'; 93 | 94 | const App = () => { 95 | return ( 96 | 97 | 98 | 99 | ); 100 | };`} 101 | 102 |
103 |

Making a Query

104 | 105 | {`import { useObsidian, BrowserCache } from 'https://deno.land/x/obsidian/clientMod.ts'; 106 | 107 | const MovieApp = () => { 108 | const { query, cache, setCache } = useObsidian(); 109 | const [movies, setMovies] = (React as any).useState(''); 110 | 111 | const queryStr = \`query { 112 | movies { 113 | id 114 | title 115 | releaseYear 116 | genre 117 | } 118 | } 119 | \` 120 | 121 | return ( 122 |

{movies}

123 | 130 | ); 131 | };`} 132 |
133 |
134 |

Making a Mutation

135 | 136 | {`import { useObsidian, BrowserCache } from 'https://deno.land/x/obsidian/clientMod.ts'; 137 | 138 | const MovieApp = () => { 139 | const { mutate, cache, setCache } = useObsidian(); 140 | const [movies, setMovies] = (React as any).useState(''); 141 | 142 | const queryStr = \`mutation { 143 | addMovie(input: {title: "Cruel Intentions", releaseYear: 1999, genre: "DRAMA" }) { 144 | id 145 | title 146 | releaseYear 147 | genre 148 | } 149 | } 150 | \`; 151 | 152 | return ( 153 |

{movies}

154 | 161 | ); 162 | };`} 163 |
164 |
165 | ); 166 | }; 167 | 168 | export default QuickStart; 169 | -------------------------------------------------------------------------------- /client/Components/NavBar.tsx: -------------------------------------------------------------------------------- 1 | import { React } from '../../deps.ts'; 2 | 3 | declare global { 4 | namespace JSX { 5 | interface IntrinsicElements { 6 | div: any; 7 | a: any; 8 | h5: any; 9 | button: any; 10 | svg: any; 11 | path: any; 12 | h3: any; 13 | } 14 | } 15 | } 16 | 17 | const NavBar = (props: any) => { 18 | const { setPage } = props; 19 | 20 | // (React as any).useEffect(() => { 21 | // let touchEvent = 'ontouchstart' in window ? 'touchstart' : 'click'; 22 | 23 | // window.addEventListener(touchEvent, (e:any) => { 24 | // console.log(e.target) 25 | // console.log('the id???',e.target.id) 26 | // if (e.target.id === 'ontouchie') { 27 | // console.log('found ya!') 28 | // setPage('about'); 29 | // } 30 | // }) 31 | // }, []); 32 | 33 | return ( 34 |
35 | {/* DOCS LETS GO jsx */} 36 | {/* setPage('docs')}>normal react */} 37 | {/*
Touch me
*/} 38 | {/* string func 39 | {e.preventDefault(); setPage('about');}}>onclick 40 | {e.preventDefault(); setPage('about');}}>touchend */} 41 | 69 | 93 | 115 | 141 |
142 | 143 | GitHub Logo 148 | 149 |
150 |
151 | 152 | Deno Logo 153 | 154 |
155 |
156 | ); 157 | }; 158 | 159 | export default NavBar; 160 | -------------------------------------------------------------------------------- /client/static/Deno-Logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | Created by potrace 1.15, written by Peter Selinger 2001-2017 9 | 10 | 12 | 87 | 89 | 91 | 92 | 93 | -------------------------------------------------------------------------------- /client/Components/DocPages/Basics/Mutations.tsx: -------------------------------------------------------------------------------- 1 | import { React, CodeBlock, dracula } from '../../../../deps.ts'; 2 | 3 | const Mutations = (props: any) => { 4 | return ( 5 |
6 |

Mutations

7 |

8 | In this chapter, we'll learn how to send GraphQL mutations using{' '} 9 | mutate to ensure our cache 10 | behaves as expected. 11 |

12 |

mutate

13 |

14 | To maintain and ascertain the truth of our cache, GraphQL mutations must 15 | be handled differently than queries. By allowing the user to send a 16 | configurations options object with 17 | mutate, the cache can be altered 18 | to their liking. To learn more about mutations, caching, and{' '} 19 | obsidian's caching philosophy, 20 | see the{' '} 21 | props.setDocsPage('Philosophy')}> 22 | Caching 23 | {' '} 24 | section. 25 |

26 |

27 | Just as with query,{' '} 28 | mutate is made available via{' '} 29 | useObsidian. To send a mutation, 30 | first destructure the hook: 31 |

32 | 33 | {`// DeleteButton.tsx 34 | import { useObsidian } from 'https://deno.land/x/obsidian/clientMod.ts'; 35 | 36 | const DeleteButton = () => { 37 | const { mutate } = useObsidian(); 38 | 39 | // jsx below 40 | };`} 41 | 42 |
43 |

44 | mutate has two parameters: 45 |

46 |
    47 |
  1. 48 | mutation - Your GraphQL mutation string 49 |
  2. 50 |
  3. 51 | options - (optional) An object with further 52 | parameters 53 |
      54 |
    • 55 | endpoint - (default:{' '} 56 | '/graphql') The endpoint 57 | where obsidian should send 58 | this request 59 |
    • 60 |
    • 61 | cache - (default:{' '} 62 | true) Set to{' '} 63 | true to enable the cache 64 | to automatically update after a mutation request. 65 |
    • 66 |
    • 67 | delete - (default:{' '} 68 | false) Set to{' '} 69 | true to indicate a delete 70 | mutation. 71 |
    • 72 |
    • 73 | update - (default:{' '} 74 | null) Optional update 75 | function to customize cache updating behavior. 76 |
    • 77 |
    78 |
  4. 79 |
80 |

81 | Send mutations with or without the configurations options parameter 82 | depending on your mutation request. 83 |

84 |

A simple mutation request to update a field:

85 | 86 | {`// UpdateFavoriteCharacter.tsx 87 | 88 | const ADD_FAVORITE_MOVIE =\` 89 | mutation AddFavoriteMovie { 90 | favoriteMovie(id: 4) { 91 | __typename 92 | id 93 | isFavorite 94 | } 95 | } 96 | \`; 97 | 98 | return ( 99 |
100 | 106 |
107 | );`} 108 |
109 |

110 | We don’t need an options object since we are using the default settings. 111 |

112 |
113 |

114 | To send a delete mutation request, provide an options object with delete 115 | set to true. This will let 116 | obsidian know to update value in 117 | the cache to ‘DELETED’ 118 |

119 | 120 | {`// DeleteButton.tsx 121 | 122 | const DELETE_MOVIE = \` 123 | mutation DeleteMovie { 124 | deleteMovie(id: 4) { 125 | __typename 126 | id 127 | } 128 | } 129 | \`; 130 | 131 | return ( 132 |
133 | 139 |
140 | );`} 141 |
142 |
143 | 144 |

145 | When sending a mutation request to create a new element, provide an 146 | update function to let obsidian 147 | know how to store the newly created element into the existing cache. 148 |

149 | 150 | {`// DeleteButton.tsx 151 | 152 | const ADD_MOVIE = \` 153 | mutation AddMovie { 154 | addMovie(input: {title: 'The Fugitive', releaseYear: 1993, genre: ACTION }) { 155 | __typename 156 | id 157 | title 158 | releaseYear 159 | genre 160 | isFavorite 161 | } 162 | } 163 | \`; 164 | 165 | function movieUpdate(cache, respObj) { 166 | const result = cache.read(ALL_MOVIES_BY_RELEASE_DATE); 167 | const { movies } = result.data; 168 | const newMovie = respObj.data.addMovie; 169 | const updatedMovieArr = movies.push(newMovie).sort((movie1, movie2) => { 170 | return movie1.releaseYear - movie2.releaseYear; 171 | }); 172 | const updatedRespObj = { data: { movies: updatedMovieArr } }; 173 | cache.write(ALL_MOVIES_BY_RELEASE_DATE, updatedRespObj); 174 | } 175 | 176 | return ( 177 |
178 | 184 |
185 | );`} 186 |
187 |
188 |

Recap & Next Up

189 |

190 | In this chapter we covered how to send mutations using{' '} 191 | mutate. To round out the Basics 192 | section, we'll examine some common errors you might find when using{' '} 193 | obsidian. 194 |

195 |
196 | ); 197 | }; 198 | 199 | export default Mutations; 200 | -------------------------------------------------------------------------------- /client/static/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /client/Components/DocPages/Caching/Strategies.tsx: -------------------------------------------------------------------------------- 1 | import { React, CodeBlock, dracula } from '../../../../deps.ts'; 2 | 3 | const Strategies = (props: any) => { 4 | return ( 5 |
6 |

Strategies

7 |

8 | In this section, we cover the details of caching in{' '} 9 | obsidian. In this chapter, we'll 10 | learn more about the different caching strategies{' '} 11 | obsidian makes available to us, 12 | and their pros and cons. 13 |

14 |

Caching in Obsidian

15 |

16 | What do we mean when we talk about caching strategies? Throughout 17 | this documentation, we use the term to refer to the methods by which you 18 | cache and store your GraphQL data. These methods each come with their 19 | own strengths and weaknesses- just like in a game of chess, where a 20 | development strategy may yield long-term benefits but come at the cost 21 | of short-term disadvantages.{' '} 22 | obsidian's caching strategies 23 | offer control over how ObsidianRouter and ObsidianWrapper store and 24 | reconstruct your data, so that you can pick the strategy that best suits 25 | your GraphQL needs. 26 |

27 |

28 | Let's examine each caching strategy in{' '} 29 | obsidian. 30 |

31 |

Cacheless

32 |

33 | At first blush the cacheless caching strategy sounds like an oxymoron- 34 | and it is. However, we feel it is important to denote cacheless as it's 35 | own caching strategy, as{' '} 36 | obsidian does not demand a 37 | caching layer. If you would prefer to forgo another caching strategy,{' '} 38 | obsidian can be configured to 39 | bypass the caching layer and serve as a basic GraphQL router or client. 40 |

41 |

42 | The cacheless strategy is recommended when your GraphQL needs are light. 43 | If you make a very small number of content calls to a GraphQL service, 44 | caching can be an unnecessary complexity. 45 |

46 |

Whole-Query Caching

47 |

48 | Whole-query caching is a simple and powerful caching strategy. 49 | Whole-query caching stores each query and response in the caching layer. 50 | If an identical query enters{' '} 51 | obsidian, the stored response is 52 | returned in its entirety. This approach to caching is extremely fast, as 53 | long as queries are absolutely identical. However, even minor 54 | differences in queries will store new key-value pairs in the cache, 55 | often resulting in large amounts of redundant information. 56 |

57 |

58 | Whole-query caching is recommended if your app makes many identical 59 | GraphQL queries throughout a typical user-session, such as flipping 60 | through pages of content. 61 |

62 |

Destructuring & Normalization

63 |

64 | Normalized caching is obsidian's 65 | headlining caching strategy. As responses from your GraphQL queries exit{' '} 66 | obsidian, they are{' '} 67 | normalized and stored in the cache at an object level. Then, by 68 | parsing incoming queries and destructuring their contents,{' '} 69 | obsidian is able to reconstruct 70 | a response object from the cache. This removes replication of data in 71 | the cache, improves memory management, and opens the door to mutation 72 | consistency in the cache layer. 73 |

74 |

75 | To facilitate the normalized caching strategy,{' '} 76 | obsidian requires your responses 77 | to always return IDs coupled with each GraphQL object type. What exactly 78 | does that mean? We can learn more by looking at an example: 79 |

80 | 81 | {`type Movie { 82 | id: ID! 83 | title: String! 84 | releaseYear: Int 85 | genre: String 86 | }`} 87 | 88 |
89 |

90 | This is a GraphQL object type- it's comprised of many fields, each of 91 | which can be assigned a Scalar value (Int, Float, String, Boolean, or 92 | ID) or another object type. For example, our Movie type could also have 93 | a field comprised of an array of actors that appear in the film: 94 |

95 | 96 | {`type Movie { 97 | id: ID! 98 | title: String! 99 | releaseYear: Int 100 | genre: String 101 | actors: [Actor] 102 | } 103 | 104 | type Actor { 105 | id: ID! 106 | firstName: String 107 | lastName: String 108 | nickname: String 109 | movies: [Movie!] 110 | }`} 111 | 112 |
113 |

114 | Note that for both object types, we have an id field. To utilize the 115 | normalized caching strategy in{' '} 116 | obsidian, you must request the 117 | id field of each object type you receive in every query. For example, if 118 | you'd like to query for just the actors in a particular movie, your 119 | query should look like this: 120 |

121 | 122 | {`query { 123 | getMovie(id: "1") { 124 | id 125 | actors { 126 | id 127 | firstName 128 | lastName 129 | } 130 | } 131 | }`} 132 | 133 |
134 |

135 | We requested the id of both the Movie object type and the Actor object 136 | type. Conversely, if you don't need any information about the actors in 137 | a movie, there's no need to ask for their id as you won't be accessing 138 | any info found on the Actor object type: 139 |

140 | 141 | {`query { 142 | getMovie(id: "1") { 143 | id 144 | title 145 | } 146 | }`} 147 | 148 |
149 |

150 | Normalized caching is incredibly robust, enabling{' '} 151 | obsidian to maintain consistency 152 | with greater precision, and thus ships as the default option when 153 | utilizing ObsidianRouter and ObsidianWrapper. 154 |

155 |

156 | Normalized caching is recommended for more complex and robust GraphQL 157 | applications. If you app is often making queries for subsets of 158 | information on object types or making many similar but unique API calls, 159 | normalized caching can drastically improve load times and help maintain 160 | global consistency. 161 |

162 |

Recap & Next Up

163 |

164 | We've learned about the different caching strategies{' '} 165 | obsidian offers, and highlighted 166 | the best use-case for each. Next, we will walk through the 167 | implementations of each of these caching strategies in both 168 | ObsidianWrapper and ObsidianRouter. 169 |

170 |
171 | ); 172 | }; 173 | 174 | export default Strategies; 175 | -------------------------------------------------------------------------------- /client/Components/DocPages/Basics/Queries.tsx: -------------------------------------------------------------------------------- 1 | import { React, CodeBlock, dracula } from '../../../../deps.ts'; 2 | 3 | const Queries = (props: any) => { 4 | return ( 5 |
6 |

Queries

7 |

8 | In this chapter, we'll cover how to fetch GraphQL data in React using 9 | obsidian query, and discuss the 10 | options available to you when querying. 11 |

12 |

useObsidian

13 |

14 | After setting up ObsidianWrapper, 15 | obsidian exposes a custom React 16 | hook, useObsidian, that can be 17 | used to globally access fetching and caching capabilities. Destructure 18 | the hook inside your React components to access 19 | obsidian functionality: 20 |

21 | 22 | {`// MainContainer.tsx 23 | import { useObsidian } from 'https://deno.land/x/obsidian/clientMod.ts'; 24 | 25 | const MainContainer = () => { 26 | const { query } = useObsidian(); 27 | 28 | // jsx below 29 | };`} 30 | 31 |
32 |

Executing a query

33 |

34 | To run a simple query within a React component, call 35 | obsidian query and pass it a 36 | GraphQL query string as a required first parameter. When your component 37 | renders, the obsidian query 38 | returns a response object from Obsidian Client that contains data you 39 | can use to render your UI. Let's look at an example: 40 |

41 | 42 | {`// MainContainer.tsx 43 | 44 | const allMoviesQuery = \`query{ 45 | movies{ 46 | id 47 | title 48 | } 49 | }\` 50 | async function getAllMovies() { 51 | const data = await query(allMoviesQuery); 52 | return ( 53 | 60 | ); 61 | }`} 62 | 63 |
64 |

65 | Whenever Obsidian Client fetches query results from your server, it 66 | automatically caches those results locally. This makes subsequent 67 | executions of the same query extremely fast. To utilize the caching 68 | capabilities of obsidian and to have more control over how the data is 69 | fetched, we’re providing some configuration options as a second and 70 | optional parameter to the 71 | obsidian query in addition to 72 | the query string: 73 |

74 |
    75 |
  1. 76 | query - Your GraphQL query string. 77 |
  2. 78 |
  3. 79 | options - (optional) An object with further 80 | parameters. 81 |
      82 |
    • 83 | endpoint - (default: 84 | '/graphql') The endpoint 85 | where obsidian should send 86 | this request. 87 |
    • 88 |
    • 89 | cacheRead - (default: 90 | true) Determines whether 91 | the cache should be checked before making a server request. 92 |
    • 93 |
    • 94 | cacheWrite - (default: 95 | true) Determines whether 96 | the response from a server request should be written into the 97 | cache. See the{' '} 98 | props.setDocsPage('Client')}> 99 | Caching 100 | {' '} 101 | section for more details. 102 |
    • 103 |
    • 104 | pollInterval - (default: 105 | null) How often 106 | obsidian should execute 107 | this query, in ms. Learn more in the 108 | props.setDocsPage('Polling')}> 109 | {' '} 110 | Advanced{' '} 111 | 112 | section. 113 |
    • 114 |
    • 115 | wholeQuery - (default: 116 | false) Set to 117 | true to conduct 118 | wholeQuery writes or retrieval from the cache. See the 119 | props.setDocsPage('Client')}> 120 | {' '} 121 | Caching{' '} 122 | 123 | section for more details. 124 |
    • 125 |
    126 |
  4. 127 |
128 |

129 | As you can see, invoking{' '} 130 | obsidian query with a query 131 | string as its only argument will make a request to your '/graphql' 132 | endpoint only once, utilizing the destructure caching strategy and 133 | storing the cached data in global memory. 134 |

135 |

136 | We'll explore caching in more detail in the 137 | props.setDocsPage('Strategies')}> 138 | {' '} 139 | Caching 140 | {' '} 141 | section. For now, let's use 142 | obsidian query to showcase a 143 | simple GraphQL request that responds just like the fetch API, returning 144 | a promise that can be consumed with a .then(): 145 |

146 | 147 | {`// MainContainer.tsx 148 | import React from 'https://dev.jspm.io/react'; 149 | import { useObsidian } from 'https://deno.land/x/obsidian/clientMod.ts'; 150 | 151 | const MainContainer = () => { 152 | const { query } = useObsidian(); 153 | const [movie, setMovie] = (React as any).useState({}); 154 | 155 | return ( 156 |
157 |

Obsidian Film Showcase

158 |

Check out our favorite movie by clicking the button below

159 | 165 |

Title: {movie.title}

166 |

Release Year: {movie.releaseYear}

167 |
168 | ); 169 | }; 170 | 171 | export default MainContainer;`} 172 |
173 |
174 |

175 | That's it! Subsequent queries will utilize the cache to reconstruct the 176 | result without querying the GraphQL endpoint. 177 |

178 |

Recap & Next Up

179 |

180 | In this chapter we covered{' '} 181 | useObsidian and the basic use 182 | cases of the query method. Next 183 | up, we'll cover mutations and the 184 | mutate method 185 | obsidian supplies. 186 |

187 |
188 | ); 189 | }; 190 | 191 | export default Queries; 192 | -------------------------------------------------------------------------------- /client/Components/DocPages/Basics/GettingStarted.tsx: -------------------------------------------------------------------------------- 1 | import { React, CodeBlock, dracula } from '../../../../deps.ts'; 2 | 3 | const GettingStarted = (props: any) => { 4 | return ( 5 |
6 |

Getting Started

7 |

8 | In this section, we'll learn{' '} 9 | obsidian by walking through the 10 | setup of a simple full stack server-side rendered React app in Deno. 11 |

12 |

ObsidianRouter

13 |

14 | We're going to build the backend of our app with{' '} 15 | 16 | Oak 17 | 18 | , a middleware framework for your Deno server. ObsidianRouter is an{' '} 19 | Oak router, so we must build our server with Oak in order to use{' '} 20 | obsidian. 21 |

22 |

Installation

23 |

24 | Thanks to Deno's ECMAScript package importing, installation of Oak and{' '} 25 | obsidian is incredibly simple. 26 | Just import the pieces of the modules you need at the top of your 27 | server, like so: 28 |

29 | 30 | {`// server.tsx 31 | import { Application, Router } from 'https://deno.land/x/oak@v6.0.1/mod.ts'; 32 | import { ObsidianRouter, gql } from 'https://deno.land/x/obsidian/mod.ts';`} 33 | 34 |
35 |

36 | NOTE - Throughout these guides, we will be illustrating imports 37 | directly from a url. It is common practice for Deno apps to utilize a 38 | dependencies file, usually called{' '} 39 | deps.ts, where packages are 40 | imported from their urls and then referenced with local imports 41 | throughout the app. We recommend this approach, with the key caveat that 42 | your Oak import statements not be accidentally bundled with your 43 | client-side code, as the browser is unable to interpret any references 44 | to Deno. You can easily accomplish this by creating two separate 45 | dependency files for your server and client code. 46 |

47 |

Oak

48 |

49 | Now that we've imported our modules, let's begin by setting up our Oak 50 | server: 51 |

52 | 53 | {`// server.tsx 54 | const PORT = 8000; 55 | 56 | const app = new Application(); 57 | 58 | app.addEventListener('listen', () => { 59 | console.log(\`Listening at http://localhost:\${PORT}\`); 60 | }); 61 | 62 | await app.listen({ port: PORT });`} 63 | 64 |
65 |

Schema

66 |

67 | Next we'll add our GraphQL endpoint with ObsidianRouter. Like every 68 | GraphQL server, ObsidianRouter requires a GraphQL schema to define the 69 | structure of our data. obsidian{' '} 70 | provides gql, a{' '} 71 | 72 | tagged template literal 73 | {' '} 74 | that allows ObsidianRouter to read your GraphQL schema. Let's construct 75 | our schema now: 76 |

77 | 78 | {`// server.tsx 79 | const types = (gql as any)\` 80 | type Movie { 81 | id: ID 82 | title: String 83 | releaseYear: Int 84 | } 85 | 86 | type Query { 87 | getMovie: Movie 88 | } 89 | \`;`} 90 | 91 |
92 |

Resolvers

93 |

94 | Great, we have a schema! But in order for ObsidianRouter to do something 95 | with your schema, we need to give it resolvers. Resolvers tell 96 | ObsidianRouter what to do with incoming queries and mutations. Let's 97 | create a resolver for our{' '} 98 | getMovie query: 99 |

100 | 101 | {`// server.tsx 102 | const resolvers = { 103 | Query: { 104 | getMovie: () => { 105 | return { 106 | id: "1", 107 | title: "Up", 108 | releaseYear: 2009 109 | }; 110 | }, 111 | }, 112 | };`} 113 | 114 |
115 |

116 | NOTE - Resolvers typically do not return hardcoded data like we 117 | have here. Your resolvers can fetch data from anywhere you might 118 | normally fetch data from, like a database or another API, but for the 119 | sake of simplicity our example includes a hardcoded response. 120 |

121 |

ObsidianRouter Setup

122 |

123 | We now have everything we need to create our GraphQL endpoint using 124 | ObsidianRouter. For now, we'll set{' '} 125 | useCache to{' '} 126 | false- we'll learn more about 127 | caching with ObsidianRouter{' '} 128 | props.setDocsPage('Server')}> 129 | later 130 | 131 | . Note that the router should come before your{' '} 132 | app.listen. 133 |

134 | 135 | {`// server.tsx 136 | interface ObsRouter extends Router { 137 | obsidianSchema?: any; 138 | } 139 | 140 | const GraphQLRouter = await ObsidianRouter({ 141 | Router, 142 | typeDefs: types, 143 | resolvers: resolvers, 144 | useCache: false, 145 | }); 146 | 147 | app.use(GraphQLRouter.routes(), GraphQLRouter.allowedMethods());`} 148 | 149 |
150 |

151 | NOTE - If you are building your server in TypeScript, as we are 152 | here, you will have to extend the Oak Router interface to create the 153 | ObsidianRouter. By exposing the{' '} 154 | obsidianSchema property on the 155 | ObsidianRouter, we open the door to a streamlined caching implementation 156 | for your client-side code, which we'll explore in{' '} 157 | props.setDocsPage('ServerSideRendering')}> 158 | server-side rendering 159 | 160 | . 161 |

162 |

Spin Up the Server

163 |

164 | Let's start the server! As Deno requires us to explicitly give 165 | permissions, our command to start up the server looks like this: 166 |

167 |

168 | 169 | deno run --allow-net --unstable server.tsx 170 | 171 |

172 |

GraphQL Playground

173 |

174 | To test our new GraphQL endpoint, head to{' '} 175 | localhost:8000/graphql to test your 176 | queries with GraphQL Playground. To learn more about this tool, check 177 | out their{' '} 178 | GitHub repo. 179 |

180 |

Recap & Next Up

181 |

182 | In this chapter we set up a simple server with Oak and a GraphQL 183 | endpoint with ObsidianRouter. Next, we'll explore ObsidianWrapper,{' '} 184 | obsidian's client 185 | implementation, via a React app built with server-side rendering. 186 |

187 |
188 | ); 189 | }; 190 | 191 | export default GettingStarted; 192 | -------------------------------------------------------------------------------- /client/Components/DocPages/Basics/ServerSideRendering.tsx: -------------------------------------------------------------------------------- 1 | import { React, CodeBlock, dracula } from '../../../../deps.ts'; 2 | 3 | const ServerSideRendering = (props: any) => { 4 | return ( 5 |
6 |

Server-Side Rendering

7 |

8 | In this chapter, we'll learn how to implement ObsidianWrapper,{' '} 9 | obsidian's GraphQL client, in a 10 | React app built with server-side rendering. 11 |

12 |

ObsidianWrapper

13 |

14 | Before we can discuss server-side rendering in Deno, we must first build 15 | out our client application. Setting up ObsidianWrapper is super simple: 16 | wrap your app with ObsidianWrapper and you are ready to start using{' '} 17 | obsidian's caching capabilities!{' '} 18 |

19 |

Installation

20 |

21 | Import React and ObsidianWrapper at your top-level component along with 22 | any child components: 23 |

24 | 25 | {`// App.tsx 26 | import React from 'https://dev.jspm.io/react'; 27 | import { ObsidianWrapper } from 'https://deno.land/x/obsidian/clientMod.ts'; 28 | import MainContainer from './MainContainer.tsx';`} 29 | 30 |
31 |

App Setup

32 |

33 | Wrap your main container in ObsidianWrapper. This exposes the{' '} 34 | useObsidian hook, which will 35 | enable us to make GraphQL requests and access our cache from anywhere in 36 | our app. 37 |

38 | 39 | {`// App.tsx 40 | declare global { 41 | namespace JSX { 42 | interface IntrinsicElements { 43 | [elemName: string]: any; 44 | } 45 | } 46 | } 47 | 48 | const App = () => { 49 | return ( 50 | 51 | 52 | 53 | ); 54 | }; 55 | 56 | export default App;`} 57 | 58 |
59 |

And let's set up our MainContainer with some static html:

60 | 61 | {`// MainContainer.tsx 62 | import React from 'https://dev.jspm.io/react'; 63 | 64 | const MainContainer = () => { 65 | return ( 66 |
67 |

Obsidian Film Showcase

68 |

Check out our favorite movie by clicking the button below

69 | 70 |
71 | ); 72 | }; 73 | 74 | export default MainContainer;`} 75 |
76 |
77 |

Serving Our App

78 |

79 | Now that we've built a simple React app, let's utilize server-side 80 | rendering to send a pre-rendered version to the client. 81 |

82 |

Router Setup

83 |

We can create a router for our base path like so:

84 | 85 | {`// server.tsx 86 | const router = new Router(); 87 | router.get('/', handlePage); 88 | 89 | app.use(router.routes(), router.allowedMethods());`} 90 | 91 |
92 |

renderToString

93 |

94 | At last, let's build our HTML file inside of our{' '} 95 | handlePage function, using 96 | ReactDomServer's renderToString{' '} 97 | method to insert our pre-rendered app inside the body. We'll also send 98 | our initialState object in the head, providing ObsidianWrapper all of 99 | the tools it needs to execute caching on the client-side: 100 |

101 | 102 | {`// server.tsx 103 | import React from 'https://dev.jspm.io/react'; 104 | import ReactDomServer from 'https://dev.jspm.io/react-dom/server'; 105 | import App from './App.tsx'; 106 | 107 | function handlePage(ctx: any) { 108 | try { 109 | const body = (ReactDomServer as any).renderToString(); 110 | ctx.response.body = \` 111 | 112 | 113 | 114 | Obsidian Film Showcase 115 | 116 | 117 |
\${body}
118 | 119 | 120 | \`; 121 | } catch (error) { 122 | console.error(error); 123 | } 124 | }`} 125 |
126 |
127 |

Hydration

128 |

129 | We're almost there! In order to reattach all of our React functionality 130 | to our pre-rendered app, we have to hydrate our root div. First, 131 | let's create the client.tsx file that will contain the hydrate 132 | functionality: 133 |

134 | 135 | {`// client.tsx 136 | import React from 'https://dev.jspm.io/react'; 137 | import ReactDom from 'https://dev.jspm.io/react-dom'; 138 | import App from './App.tsx'; 139 | 140 | (ReactDom as any).hydrate( 141 | , 142 | document.getElementById('root') 143 | );`} 144 | 145 |
146 |

147 | In the server, we'll use Deno's native emit method to wrap up all of 148 | the React logic contained in our app, ready to be reattached to the DOM 149 | via hydration: 150 |

151 | 152 | {`// server.tsx 153 | const { files, diagnostics } = await Deno.emit('./client/client.tsx', { 154 | bundle: 'esm', 155 | });`} 156 | 157 |
158 |

159 | Once our client code is bundled, we can send it to the client via 160 | another router in our server: 161 |

162 | 163 | {`// server.tsx 164 | const hydrateRouter = new Router(); 165 | 166 | hydrateRouter.get('/static/client.js', (context) => { 167 | context.response.headers.set('Content-Type', 'text/html'); 168 | context.response.body = files['deno:///bundle.js']; 169 | }); 170 | 171 | app.use(hydrateRouter.routes(), hydrateRouter.allowedMethods());`} 172 | 173 |
174 |

Compiling

175 |

176 | Just one more step before we're up and running: specify our compiler 177 | options with a tsconfig.json file. To learn more about TypeScript 178 | project configuration, check out the official documentation{' '} 179 | 180 | here 181 | 182 | . 183 |

184 | 185 | {`// tsconfig.json 186 | { 187 | "compilerOptions": { 188 | "jsx": "react", 189 | "target": "es6", 190 | "module": "commonjs", 191 | "lib": [ 192 | "DOM", 193 | "ES2017", 194 | "deno.ns" 195 | ], 196 | "strict": true, 197 | "esModuleInterop": true, 198 | "skipLibCheck": true, 199 | "forceConsistentCasingInFileNames": true 200 | } 201 | }`} 202 | 203 |
204 |

Spin Up the Server

205 |

206 | Our command to start our server has expanded now that we're bundling our 207 | client.tsx file. The new command to start up our server looks like this: 208 |

209 |

210 | 211 | deno run --allow-net --allow-read --unstable server.tsx -c 212 | tsconfig.json 213 | 214 |

215 |

Recap & Next Up

216 |

217 | In this chapter we set up a simple React app and implemented 218 | ObsidianWrapper, enabling fetching and caching at a global level. We 219 | utilized server-side rendering to send a pre-rendered version of our app 220 | to the client. Next, we'll take a look at querying with{' '} 221 | obsidian and the different 222 | methods and options available. 223 |

224 |
225 | ); 226 | }; 227 | 228 | export default ServerSideRendering; 229 | -------------------------------------------------------------------------------- /client/Components/ObsidianLogo.tsx: -------------------------------------------------------------------------------- 1 | import { React } from '../../deps.ts'; 2 | 3 | declare global { 4 | namespace JSX { 5 | interface IntrinsicElements { 6 | div: any; 7 | svg: any; 8 | path: any; 9 | rect: any; 10 | } 11 | } 12 | } 13 | // className="animate__animated animate__zoomInDown animate__slow" 14 | // className="animate__animated animate__pulse animate__delay-2s" 15 | 16 | const ObsidianLogo = (props: any) => { 17 | return ( 18 | 110 | ); 111 | }; 112 | 113 | export default ObsidianLogo; 114 | -------------------------------------------------------------------------------- /client/Components/Team.jsx: -------------------------------------------------------------------------------- 1 | import { React } from '../../deps.ts'; 2 | import TeamMember from './TeamMember.jsx'; 3 | 4 | const users = [ 5 | { 6 | firstName: 'Raymond', 7 | lastName: 'Ahn', 8 | image: 9 | 'https://res.cloudinary.com/os-labs/image/upload/v1625079464/Profile_Pic_tcytta.jpg', 10 | info: 'Raymond is a full-stack software engineer whose focus is on developing scalable and responsive programs through algorithmic optimization. During his spare time, he enjoys exploring new cuisines, game nights, fitness, traveling, and anything music-related.', 11 | linkedin: 'https://www.linkedin.com/in/raymondahn/', 12 | github: 'https://github.com/raymondcodes', 13 | }, 14 | { 15 | firstName: 'Kyung', 16 | lastName: 'Lee', 17 | image: 'https://res.cloudinary.com/os-labs/image/upload/v1625091906/IMG_1195_ptmaex.jpg', 18 | info: 'Kyung is a software engineer who loves diving deep into technologies and building robust and efficient web applications. Apart from coding, his hobbies include watching MMA and eating tacos or anything spicy.', 19 | linkedin: 'https://www.linkedin.com/in/kyung-lee-9414a6215/', 20 | github: 'https://github.com/kyunglee1', 21 | }, 22 | { 23 | firstName: 'Justin', 24 | lastName: 'McKay', 25 | image: 26 | 'https://res.cloudinary.com/os-labs/image/upload/v1625079448/Screen_Shot_2021-06-28_at_11.24.08_AM_kyfviq.png', 27 | info: 'Justin is a software engineer with a passion for solving complex problems and full stack system design. When not coding he enjoys music theory, fitness, and cold water.', 28 | linkedin: 'https://www.linkedin.com/in/justinwmckay/', 29 | github: 'https://github.com/justinwmckay', 30 | }, 31 | { 32 | firstName: 'Cameron', 33 | lastName: 'Simmons', 34 | image: 35 | 'https://res.cloudinary.com/os-labs/image/upload/v1625002810/3C1B71E4-CD2A-4D11-BBF0-B054E80BC132_1_201_a_afrjry.jpg', 36 | info: 'Cameron is a full-stack software engineer who gets excited about fast, full-featured applications and staying up-to-date on the latest tech trends. In his spare time he enjoys producing music and being outdoors.', 37 | linkedin: 'https://www.linkedin.com/in/camsimmons/', 38 | github: 'https://github.com/cssim22', 39 | }, 40 | { 41 | firstName: 'Patrick', 42 | lastName: 'Sullivan', 43 | image: 44 | 'https://res.cloudinary.com/os-labs/image/upload/v1625079443/PJS-Passport_hv6kvx.jpg', 45 | info: 'Patrick is a full-stack software engineer passionate about back-end tech. He loves backpacking in the Pacific Northwest and the Rockies, growing vegetables, and living fully.', 46 | linkedin: 'https://www.linkedin.com/in/patrick-j-m-sullivan/', 47 | github: 'https://github.com/pjmsullivan', 48 | }, 49 | { 50 | firstName: 'Nhan', 51 | lastName: 'Ly', 52 | image: 53 | 'https://res.cloudinary.com/dyigtncwy/image/upload/v1618454676/Nhan_hlhra2.jpg', 54 | info: 'Nhan is a software engineer that enjoys working on the entire stack and particularly enjoys playing with algorithms and learning about new technologies. When not coding Nhan usually listening to podcasts, reading, and taking (too long) breaks at the gym between sets.', 55 | linkedin: 'https://www.linkedin.com/in/nhanly/', 56 | github: 'https://github.com/NhanTLy', 57 | }, 58 | { 59 | firstName: 'Damon', 60 | lastName: 'Alfaro', 61 | image: 62 | 'https://res.cloudinary.com/dyigtncwy/image/upload/v1618454676/Damon_j1ehdh.jpg', 63 | info: 'As a software engineer Damon enjoys databases and experimenting with emerging back-end technologies. He spends his free time backpacking, running, and traveling in his converted school bus RV.', 64 | linkedin: 'https://www.linkedin.com/in/damon-alfaro-28530a74/', 65 | github: 'https://github.com/djdalfaro', 66 | }, 67 | { 68 | firstName: 'Adam', 69 | lastName: 'Wilson', 70 | image: 71 | 'https://res.cloudinary.com/dyigtncwy/image/upload/v1618454676/Adam_ddx07d.jpg', 72 | info: 'Adam is a full-stack software engineer originally from Cleveland, OH. He enjoys developing efficient and responsive web applications in addition to performant APIs, using React and Node.js. His favorite things include tacos and Irish wolfhounds.', 73 | linkedin: 'https://www.linkedin.com/in/adam-wilson-houston/', 74 | github: 'https://github.com/aswilson87', 75 | }, 76 | { 77 | firstName: 'Christy', 78 | lastName: 'Gomez', 79 | image: 80 | 'https://res.cloudinary.com/dyigtncwy/image/upload/v1618456773/IMG_1522_h9j9vi.jpg', 81 | info: 'Christy is a software engineer focused on developing full stack applications with responsive design. She enjoys salsa dancing, trying new restaurants, unplanned traveling, and being eco-friendly.', 82 | linkedin: 'https://www.linkedin.com/in/christy-gomez/', 83 | github: 'https://github.com/christygomez', 84 | }, 85 | { 86 | firstName: 'Geovanni', 87 | lastName: 'Alarcon', 88 | image: 89 | 'https://res.cloudinary.com/dyigtncwy/image/upload/v1618459654/geo2_jxodpa.jpg', 90 | info: 'Geo is a software engineer who loves building scalable and responsive software tools for the developer community. For fun, he enjoys traveling, and picnics.', 91 | linkedin: 'https://www.linkedin.com/in/geo-alarcon/', 92 | github: 'https://github.com/gealarcon', 93 | }, 94 | { 95 | firstName: 'Esma', 96 | lastName: 'Sahraoui', 97 | image: 98 | 'https://res.cloudinary.com/dsmiftdyz/image/upload/v1609992320/IMG_2063_htwead.jpg', 99 | info: 'Esma is a full-stack software engineer with a passion for building servers as well as building responsive user interfaces for web applications. In her free time, she is driven by traveling to discover new cultures and kayaking across rivers.', 100 | linkedin: 'https://www.linkedin.com/in/esma-sahraoui/', 101 | github: 'https://github.com/EsmaShr', 102 | }, 103 | { 104 | firstName: 'Derek', 105 | lastName: 'Miller', 106 | image: 107 | 'https://res.cloudinary.com/dsmiftdyz/image/upload/v1609990785/Derek-headshot_ofzix3.jpg', 108 | info: 'Derek is a full-stack software engineer with a focus on the MERN tech stack. Outside of coding he loves boardgames and rock climbing.', 109 | linkedin: 'https://www.linkedin.com/in/dsymiller', 110 | github: 'https://github.com/dsymiller', 111 | }, 112 | { 113 | firstName: 'Eric', 114 | lastName: 'Marcatoma', 115 | image: 116 | 'https://res.cloudinary.com/dsmiftdyz/image/upload/v1609989762/AE476873-B676-4D4D-AF9A-548B386F7AD7_1_201_a_mxvsgu.jpg', 117 | info: 'Eric is a software engineer from NYC who focuses on front-end development. During his spare time he loves to go to the gym, play basketball and trying new restaurants in his city.', 118 | linkedin: 'https://www.linkedin.com/in/ericmarc159', 119 | github: 'https://github.com/ericmarc159', 120 | }, 121 | { 122 | firstName: 'Lourent', 123 | lastName: 'Flores', 124 | image: 125 | 'https://res.cloudinary.com/dsmiftdyz/image/upload/v1609990832/headshot_e4ijvy.png', 126 | info: 'Lourent is a full-stack software engineer specializing in React and Node.js, with a passion for learning new technologies and optimizing frontend web design.', 127 | linkedin: 'https://www.linkedin.com/in/lourent-flores/', 128 | github: 'https://github.com/lourentflores', 129 | }, 130 | { 131 | firstName: 'Spencer', 132 | lastName: 'Stockton', 133 | image: 134 | 'https://res.cloudinary.com/dsmiftdyz/image/upload/v1609994346/obsidianpic_gzxcqe.jpg', 135 | info: 'Spencer is a software engineer that enjoys working on solving complex problems across the entire tech stack. When he is not programming, you can find him running on the East River or attending a concert around NYC.', 136 | linkedin: 'https://www.linkedin.com/in/spencer-stockton-643823a4/', 137 | github: 'https://github.com/tonstock', 138 | }, 139 | { 140 | firstName: 'Alonso', 141 | lastName: 'Garza', 142 | image: 143 | 'https://res.cloudinary.com/dkxftbzuu/image/upload/v1600479739/Obsidian/WhatsApp_Image_2020-09-18_at_8.38.09_PM_pgshgj.jpg', 144 | 145 | info: 'Alonso Garza is a full-stack software engineer from Austin, Texas. Alonso specializes in React, Node.js, Express, and is passionate about solving complex problems and working in teams. Outside of coding, he loves to travel around Latin America and scuba dive!', 146 | linkedin: 'https://www.linkedin.com/in/e-alonso-garza/', 147 | github: 'https://github.com/Alonsog66', 148 | }, 149 | { 150 | firstName: 'Travis', 151 | lastName: 'Frank', 152 | image: 153 | 'https://res.cloudinary.com/dkxftbzuu/image/upload/v1600462593/Obsidian/image_1_c7sggv.jpg', 154 | info: 'Travis is a software engineer at Place Exchange focused on building scalable backend solutions. When he isn’t coding he enjoys playing board games, eating copious amounts of sushi, and conducting pit orchestras.', 155 | linkedin: 'https://linkedin.com/in/travis-m-frank', 156 | github: 'https://github.com/TravisFrankMTG', 157 | }, 158 | { 159 | firstName: 'Matt', 160 | lastName: 'Meigs', 161 | image: 162 | 'https://res.cloudinary.com/dkxftbzuu/image/upload/v1600811069/Obsidian/IMG_2543_vrhknw.jpg', 163 | info: 'Matt is a software engineer interested in clear and responsive frontend web design and algorithmic optimization through the full stack. He’s an expat from the Deep South, a lover of far-flung travel, and a former Broadway actor.', 164 | linkedin: 'https://www.linkedin.com/in/matt-meigs/', 165 | github: 'https://github.com/mmeigs', 166 | }, 167 | { 168 | firstName: 'Burak', 169 | lastName: 'Caliskan', 170 | image: 171 | 'https://res.cloudinary.com/dkxftbzuu/image/upload/v1600462610/Obsidian/92C676E9-C9C4-4CFB-80A9-876B30C94732_copy_vfsnjl.jpg', 172 | info: 'Burak is a software engineer focused on developing full stack applications. Curious and constantly finding ways to use new ideas to solve problems and provide delight. For fun, he enjoys outdoor activities, traveling, and exploring new cuisines.', 173 | linkedin: 'https://www.linkedin.com/in/burakcaliskan/', 174 | github: 'https://github.com/CaliskanBurak', 175 | }, 176 | ]; 177 | 178 | const teamMembers = users.map((user, i) => ( 179 | 180 | )); 181 | function Team() { 182 | return ( 183 |
184 |

Contributors

185 |
{teamMembers}
186 |
187 | ); 188 | } 189 | 190 | export default Team; 191 | -------------------------------------------------------------------------------- /client/static/prism.js: -------------------------------------------------------------------------------- 1 | /* PrismJS 1.21.0 2 | https://prismjs.com/download.html#themes=prism&languages=markup+css+clike+javascript */ 3 | var _self="undefined"!=typeof window?window:"undefined"!=typeof WorkerGlobalScope&&self instanceof WorkerGlobalScope?self:{},Prism=function(u){var c=/\blang(?:uage)?-([\w-]+)\b/i,n=0,M={manual:u.Prism&&u.Prism.manual,disableWorkerMessageHandler:u.Prism&&u.Prism.disableWorkerMessageHandler,util:{encode:function e(n){return n instanceof W?new W(n.type,e(n.content),n.alias):Array.isArray(n)?n.map(e):n.replace(/&/g,"&").replace(/=l.reach);k+=y.value.length,y=y.next){var b=y.value;if(t.length>n.length)return;if(!(b instanceof W)){var x=1;if(h&&y!=t.tail.prev){m.lastIndex=k;var w=m.exec(n);if(!w)break;var A=w.index+(f&&w[1]?w[1].length:0),P=w.index+w[0].length,S=k;for(S+=y.value.length;S<=A;)y=y.next,S+=y.value.length;if(S-=y.value.length,k=S,y.value instanceof W)continue;for(var E=y;E!==t.tail&&(Sl.reach&&(l.reach=j);var C=y.prev;L&&(C=I(t,C,L),k+=L.length),z(t,C,x);var _=new W(o,g?M.tokenize(O,g):O,v,O);y=I(t,C,_),N&&I(t,y,N),1"+a.content+""},!u.document)return u.addEventListener&&(M.disableWorkerMessageHandler||u.addEventListener("message",function(e){var n=JSON.parse(e.data),t=n.language,r=n.code,a=n.immediateClose;u.postMessage(M.highlight(r,M.languages[t],t)),a&&u.close()},!1)),M;var e=M.util.currentScript();function t(){M.manual||M.highlightAll()}if(e&&(M.filename=e.src,e.hasAttribute("data-manual")&&(M.manual=!0)),!M.manual){var r=document.readyState;"loading"===r||"interactive"===r&&e&&e.defer?document.addEventListener("DOMContentLoaded",t):window.requestAnimationFrame?window.requestAnimationFrame(t):window.setTimeout(t,16)}return M}(_self);"undefined"!=typeof module&&module.exports&&(module.exports=Prism),"undefined"!=typeof global&&(global.Prism=Prism); 4 | Prism.languages.markup={comment://,prolog:/<\?[\s\S]+?\?>/,doctype:{pattern:/"'[\]]|"[^"]*"|'[^']*')+(?:\[(?:[^<"'\]]|"[^"]*"|'[^']*'|<(?!!--)|)*\]\s*)?>/i,greedy:!0,inside:{"internal-subset":{pattern:/(\[)[\s\S]+(?=\]>$)/,lookbehind:!0,greedy:!0,inside:null},string:{pattern:/"[^"]*"|'[^']*'/,greedy:!0},punctuation:/^$|[[\]]/,"doctype-tag":/^DOCTYPE/,name:/[^\s<>'"]+/}},cdata://i,tag:{pattern:/<\/?(?!\d)[^\s>\/=$<%]+(?:\s(?:\s*[^\s>\/=]+(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^\s'">=]+(?=[\s>]))|(?=[\s/>])))+)?\s*\/?>/,greedy:!0,inside:{tag:{pattern:/^<\/?[^\s>\/]+/,inside:{punctuation:/^<\/?/,namespace:/^[^\s>\/:]+:/}},"attr-value":{pattern:/=\s*(?:"[^"]*"|'[^']*'|[^\s'">=]+)/,inside:{punctuation:[{pattern:/^=/,alias:"attr-equals"},/"|'/]}},punctuation:/\/?>/,"attr-name":{pattern:/[^\s>\/]+/,inside:{namespace:/^[^\s>\/:]+:/}}}},entity:[{pattern:/&[\da-z]{1,8};/i,alias:"named-entity"},/&#x?[\da-f]{1,8};/i]},Prism.languages.markup.tag.inside["attr-value"].inside.entity=Prism.languages.markup.entity,Prism.languages.markup.doctype.inside["internal-subset"].inside=Prism.languages.markup,Prism.hooks.add("wrap",function(a){"entity"===a.type&&(a.attributes.title=a.content.replace(/&/,"&"))}),Object.defineProperty(Prism.languages.markup.tag,"addInlined",{value:function(a,e){var s={};s["language-"+e]={pattern:/(^$)/i,lookbehind:!0,inside:Prism.languages[e]},s.cdata=/^$/i;var n={"included-cdata":{pattern://i,inside:s}};n["language-"+e]={pattern:/[\s\S]+/,inside:Prism.languages[e]};var t={};t[a]={pattern:RegExp("(<__[^]*?>)(?:))*\\]\\]>|(?!)".replace(/__/g,function(){return a}),"i"),lookbehind:!0,greedy:!0,inside:n},Prism.languages.insertBefore("markup","cdata",t)}}),Prism.languages.html=Prism.languages.markup,Prism.languages.mathml=Prism.languages.markup,Prism.languages.svg=Prism.languages.markup,Prism.languages.xml=Prism.languages.extend("markup",{}),Prism.languages.ssml=Prism.languages.xml,Prism.languages.atom=Prism.languages.xml,Prism.languages.rss=Prism.languages.xml; 5 | !function(e){var s=/("|')(?:\\(?:\r\n|[\s\S])|(?!\1)[^\\\r\n])*\1/;e.languages.css={comment:/\/\*[\s\S]*?\*\//,atrule:{pattern:/@[\w-]+[\s\S]*?(?:;|(?=\s*\{))/,inside:{rule:/^@[\w-]+/,"selector-function-argument":{pattern:/(\bselector\s*\((?!\s*\))\s*)(?:[^()]|\((?:[^()]|\([^()]*\))*\))+?(?=\s*\))/,lookbehind:!0,alias:"selector"},keyword:{pattern:/(^|[^\w-])(?:and|not|only|or)(?![\w-])/,lookbehind:!0}}},url:{pattern:RegExp("\\burl\\((?:"+s.source+"|(?:[^\\\\\r\n()\"']|\\\\[^])*)\\)","i"),greedy:!0,inside:{function:/^url/i,punctuation:/^\(|\)$/,string:{pattern:RegExp("^"+s.source+"$"),alias:"url"}}},selector:RegExp("[^{}\\s](?:[^{};\"']|"+s.source+")*?(?=\\s*\\{)"),string:{pattern:s,greedy:!0},property:/[-_a-z\xA0-\uFFFF][-\w\xA0-\uFFFF]*(?=\s*:)/i,important:/!important\b/i,function:/[-a-z0-9]+(?=\()/i,punctuation:/[(){};:,]/},e.languages.css.atrule.inside.rest=e.languages.css;var t=e.languages.markup;t&&(t.tag.addInlined("style","css"),e.languages.insertBefore("inside","attr-value",{"style-attr":{pattern:/\s*style=("|')(?:\\[\s\S]|(?!\1)[^\\])*\1/i,inside:{"attr-name":{pattern:/^\s*style/i,inside:t.tag.inside},punctuation:/^\s*=\s*['"]|['"]\s*$/,"attr-value":{pattern:/.+/i,inside:e.languages.css}},alias:"language-css"}},t.tag))}(Prism); 6 | Prism.languages.clike={comment:[{pattern:/(^|[^\\])\/\*[\s\S]*?(?:\*\/|$)/,lookbehind:!0},{pattern:/(^|[^\\:])\/\/.*/,lookbehind:!0,greedy:!0}],string:{pattern:/(["'])(?:\\(?:\r\n|[\s\S])|(?!\1)[^\\\r\n])*\1/,greedy:!0},"class-name":{pattern:/(\b(?:class|interface|extends|implements|trait|instanceof|new)\s+|\bcatch\s+\()[\w.\\]+/i,lookbehind:!0,inside:{punctuation:/[.\\]/}},keyword:/\b(?:if|else|while|do|for|return|in|instanceof|function|new|try|throw|catch|finally|null|break|continue)\b/,boolean:/\b(?:true|false)\b/,function:/\w+(?=\()/,number:/\b0x[\da-f]+\b|(?:\b\d+\.?\d*|\B\.\d+)(?:e[+-]?\d+)?/i,operator:/[<>]=?|[!=]=?=?|--?|\+\+?|&&?|\|\|?|[?*/~^%]/,punctuation:/[{}[\];(),.:]/}; 7 | Prism.languages.javascript=Prism.languages.extend("clike",{"class-name":[Prism.languages.clike["class-name"],{pattern:/(^|[^$\w\xA0-\uFFFF])[_$A-Z\xA0-\uFFFF][$\w\xA0-\uFFFF]*(?=\.(?:prototype|constructor))/,lookbehind:!0}],keyword:[{pattern:/((?:^|})\s*)(?:catch|finally)\b/,lookbehind:!0},{pattern:/(^|[^.]|\.\.\.\s*)\b(?:as|async(?=\s*(?:function\b|\(|[$\w\xA0-\uFFFF]|$))|await|break|case|class|const|continue|debugger|default|delete|do|else|enum|export|extends|for|from|function|(?:get|set)(?=\s*[\[$\w\xA0-\uFFFF])|if|implements|import|in|instanceof|interface|let|new|null|of|package|private|protected|public|return|static|super|switch|this|throw|try|typeof|undefined|var|void|while|with|yield)\b/,lookbehind:!0}],number:/\b(?:(?:0[xX](?:[\dA-Fa-f](?:_[\dA-Fa-f])?)+|0[bB](?:[01](?:_[01])?)+|0[oO](?:[0-7](?:_[0-7])?)+)n?|(?:\d(?:_\d)?)+n|NaN|Infinity)\b|(?:\b(?:\d(?:_\d)?)+\.?(?:\d(?:_\d)?)*|\B\.(?:\d(?:_\d)?)+)(?:[Ee][+-]?(?:\d(?:_\d)?)+)?/,function:/#?[_$a-zA-Z\xA0-\uFFFF][$\w\xA0-\uFFFF]*(?=\s*(?:\.\s*(?:apply|bind|call)\s*)?\()/,operator:/--|\+\+|\*\*=?|=>|&&=?|\|\|=?|[!=]==|<<=?|>>>?=?|[-+*/%&|^!=<>]=?|\.{3}|\?\?=?|\?\.?|[~:]/}),Prism.languages.javascript["class-name"][0].pattern=/(\b(?:class|interface|extends|implements|instanceof|new)\s+)[\w.\\]+/,Prism.languages.insertBefore("javascript","keyword",{regex:{pattern:/((?:^|[^$\w\xA0-\uFFFF."'\])\s]|\b(?:return|yield))\s*)\/(?:\[(?:[^\]\\\r\n]|\\.)*]|\\.|[^/\\\[\r\n])+\/[gimyus]{0,6}(?=(?:\s|\/\*(?:[^*]|\*(?!\/))*\*\/)*(?:$|[\r\n,.;:})\]]|\/\/))/,lookbehind:!0,greedy:!0,inside:{"regex-source":{pattern:/^(\/)[\s\S]+(?=\/[a-z]*$)/,lookbehind:!0,alias:"language-regex",inside:Prism.languages.regex},"regex-flags":/[a-z]+$/,"regex-delimiter":/^\/|\/$/}},"function-variable":{pattern:/#?[_$a-zA-Z\xA0-\uFFFF][$\w\xA0-\uFFFF]*(?=\s*[=:]\s*(?:async\s*)?(?:\bfunction\b|(?:\((?:[^()]|\([^()]*\))*\)|[_$a-zA-Z\xA0-\uFFFF][$\w\xA0-\uFFFF]*)\s*=>))/,alias:"function"},parameter:[{pattern:/(function(?:\s+[_$A-Za-z\xA0-\uFFFF][$\w\xA0-\uFFFF]*)?\s*\(\s*)(?!\s)(?:[^()]|\([^()]*\))+?(?=\s*\))/,lookbehind:!0,inside:Prism.languages.javascript},{pattern:/[_$a-z\xA0-\uFFFF][$\w\xA0-\uFFFF]*(?=\s*=>)/i,inside:Prism.languages.javascript},{pattern:/(\(\s*)(?!\s)(?:[^()]|\([^()]*\))+?(?=\s*\)\s*=>)/,lookbehind:!0,inside:Prism.languages.javascript},{pattern:/((?:\b|\s|^)(?!(?:as|async|await|break|case|catch|class|const|continue|debugger|default|delete|do|else|enum|export|extends|finally|for|from|function|get|if|implements|import|in|instanceof|interface|let|new|null|of|package|private|protected|public|return|set|static|super|switch|this|throw|try|typeof|undefined|var|void|while|with|yield)(?![$\w\xA0-\uFFFF]))(?:[_$A-Za-z\xA0-\uFFFF][$\w\xA0-\uFFFF]*\s*)\(\s*|\]\s*\(\s*)(?!\s)(?:[^()]|\([^()]*\))+?(?=\s*\)\s*\{)/,lookbehind:!0,inside:Prism.languages.javascript}],constant:/\b[A-Z](?:[A-Z_]|\dx?)*\b/}),Prism.languages.insertBefore("javascript","string",{"template-string":{pattern:/`(?:\\[\s\S]|\${(?:[^{}]|{(?:[^{}]|{[^}]*})*})+}|(?!\${)[^\\`])*`/,greedy:!0,inside:{"template-punctuation":{pattern:/^`|`$/,alias:"string"},interpolation:{pattern:/((?:^|[^\\])(?:\\{2})*)\${(?:[^{}]|{(?:[^{}]|{[^}]*})*})+}/,lookbehind:!0,inside:{"interpolation-punctuation":{pattern:/^\${|}$/,alias:"punctuation"},rest:Prism.languages.javascript}},string:/[\s\S]+/}}}),Prism.languages.markup&&Prism.languages.markup.tag.addInlined("script","javascript"),Prism.languages.js=Prism.languages.javascript; 8 | -------------------------------------------------------------------------------- /client/static/style.css: -------------------------------------------------------------------------------- 1 | .app, 2 | #root { 3 | /* font-size: 14px; */ 4 | margin: 0; 5 | padding: 0; 6 | height: 100vh; 7 | width: 100vw; 8 | } 9 | 10 | body { 11 | font-family: 'Nunito Sans', sans-serif !important; 12 | } 13 | 14 | #root { 15 | background-image: url(https://images.unsplash.com/photo-1597871119022-42906cd7c9b9?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=1350&q=80); 16 | background-repeat: no-repeat; 17 | background-size: cover; 18 | 19 | display: flex; 20 | justify-content: space-between; 21 | align-items: center; 22 | } 23 | 24 | .app { 25 | display: flex; 26 | justify-content: space-between; 27 | align-items: center; 28 | background: rgba(0, 0, 0, 0.6); 29 | } 30 | 31 | .pinkATags a { 32 | color: #e83e8c !important; 33 | text-decoration: none; 34 | } 35 | 36 | a:hover { 37 | text-decoration: underline; 38 | } 39 | 40 | .navBar { 41 | display: flex; 42 | flex-direction: column; 43 | justify-content: space-around; 44 | 45 | width: 20%; 46 | height: 100%; 47 | max-width: 140px; 48 | max-height: 600px; 49 | padding: 3rem 0.5rem; 50 | } 51 | 52 | .navBtn { 53 | border-radius: 10px; 54 | /* // border: '1px solid #EBEBEC', */ 55 | background-color: rgb(53, 54, 56); 56 | border: none; 57 | border-style: none; 58 | padding: 2% 3%; 59 | height: 20%; 60 | max-height: 100px; 61 | min-height: 60px; 62 | animation: fadeInDown; 63 | animation-duration: 1s; 64 | cursor: pointer; 65 | } 66 | /* Demo Styling */ 67 | 68 | .pre-block { 69 | width: 60%; 70 | border: 1px solid black; 71 | display: block; 72 | background-color: #fff; 73 | padding: 10px; 74 | margin: 1vw; 75 | } 76 | /* .pre-block, */ 77 | .code-block { 78 | border: 1px solid black; 79 | display: block; 80 | padding: 20px; 81 | margin: 1vw; 82 | } 83 | 84 | .code-block p { 85 | margin-top: 1rem; 86 | } 87 | 88 | #demo-block { 89 | padding: 2vh; 90 | display: flex; 91 | justify-content: center; 92 | align-items: center; 93 | flex-direction: column; 94 | font-size: 1vw; 95 | } 96 | 97 | #demo-block label { 98 | margin: 0 10px; 99 | } 100 | 101 | .optionsForCountry { 102 | margin-top: 5%; 103 | } 104 | 105 | pre { 106 | white-space: pre-wrap; /* Since CSS 2.1 */ 107 | word-wrap: break-word; /* Internet Explorer 5.5+ */ 108 | } 109 | 110 | .demoInput { 111 | display: flex; 112 | justify-content: center; 113 | width: 100%; 114 | } 115 | 116 | .query-timer { 117 | font-size: 1.7rem; 118 | } 119 | 120 | .buildQueryTitle { 121 | text-align: center; 122 | font-size: 3em; 123 | } 124 | 125 | .showQuery, 126 | .buildQuery { 127 | display: inline-block; 128 | } 129 | 130 | #stretchQuery { 131 | margin: auto; 132 | width: 80%; 133 | } 134 | 135 | .buildQuery { 136 | color: rgb(20, 20, 20); 137 | border-radius: 10px; 138 | background-color: rgb(212, 212, 212); 139 | width: 40%; 140 | padding: 2%; 141 | } 142 | 143 | #fetchBtn { 144 | margin-top: 5%; 145 | } 146 | 147 | .showQuery { 148 | width: 40%; 149 | } 150 | 151 | #code-black { 152 | background-color: rgb(20, 20, 20); 153 | color: white; 154 | } 155 | 156 | #code-yellow { 157 | background-color: rgb(20, 20, 20); 158 | color: rgb(255, 255, 138); 159 | } 160 | 161 | #code-pink { 162 | line-height: 0.5rem; 163 | background-color: rgb(20, 20, 20); 164 | color: pink; 165 | } 166 | #demo-form { 167 | padding: 1rem; 168 | justify-content: center; 169 | align-items: center; 170 | } 171 | /* .navBtn:active { 172 | border: none; 173 | } */ 174 | .navBtn:focus { 175 | border: none; 176 | } 177 | .navBtnText { 178 | font-size: 1em; 179 | color: white; 180 | } 181 | 182 | .navBtn svg { 183 | width: 40%; 184 | } 185 | 186 | .navBarGitLink, 187 | .navBarDenoLink { 188 | position: absolute; 189 | display: none; 190 | visibility: hidden; 191 | } 192 | 193 | .mainContainer { 194 | width: 70vw; 195 | height: 94vh; 196 | background-color: rgba(219, 219, 219, 0.514); 197 | margin: 3vh 0 3vh 0; 198 | border-radius: 10px; 199 | overflow-y: auto; 200 | overflow-x: hidden; 201 | } 202 | 203 | .demoContainer { 204 | width: 100vw; 205 | height: 94vh; 206 | background-color: rgba(219, 219, 219, 0.514); 207 | margin: 0 10vh 0 0; 208 | border-radius: 10px; 209 | overflow-y: hidden; 210 | overflow-x: hidden; 211 | border: none; 212 | /* margin-top: 3vh; 213 | margin-right: 140px; */ 214 | } 215 | .mainContainerLogo { 216 | width: 70vw; 217 | height: 94vh; 218 | /* background-color: rgba(219, 219, 219, 0.514); */ 219 | margin: 3vh 0 3vh 0; 220 | border-radius: 10px; 221 | overflow: hidden; 222 | } 223 | /* SIDEBAR STYLING */ 224 | 225 | .sidebar { 226 | display: flex; 227 | flex-direction: column; 228 | justify-content: space-around; 229 | background-color: rgba(219, 219, 219, 0.1); 230 | /* border-color: rgba(252, 121, 121, 0.5); */ 231 | border-radius: 10px; 232 | width: 20%; 233 | height: 94%; 234 | margin: auto 15px; 235 | } 236 | 237 | .homePage { 238 | display: flex; 239 | justify-content: center; 240 | align-items: center; 241 | /* background-color: #242424; */ 242 | height: 100%; 243 | width: 100%; 244 | } 245 | 246 | div#logo { 247 | width: 75%; 248 | height: 75%; 249 | position: absolute; 250 | top: 0; 251 | left: 0; 252 | right: 0; 253 | bottom: 0; 254 | margin: auto; 255 | z-index: 0; 256 | } 257 | 258 | #logo svg { 259 | border-radius: 10px; 260 | } 261 | 262 | /* path#frontLeg { 263 | animation: bounce; 264 | animation-duration: 2s; 265 | } 266 | 267 | path.bullHead { 268 | animation: headShake; 269 | animation-duration: 2s; 270 | } */ 271 | .sidebar a:hover { 272 | text-decoration: none; 273 | } 274 | 275 | .sidebar .codeLinks .codeLinkDiv::before { 276 | content: ''; 277 | display: block; 278 | height: 0.5px; 279 | width: 0%; 280 | background-color: #f7f7f7; 281 | opacity: 90%; 282 | border-radius: 5px; 283 | z-index: 1; 284 | position: absolute; 285 | transition: all ease-in-out 350ms; 286 | } 287 | 288 | .sidebar .codeLinks .codeLinkDiv:hover::before { 289 | width: 16%; 290 | } 291 | 292 | .sidebar .codeLinks { 293 | height: 15%; 294 | max-height: 60px; 295 | display: flex; 296 | flex-direction: column; 297 | align-items: center; 298 | justify-content: space-around; 299 | } 300 | 301 | .sidebar .codeLinks .codeLinkDiv { 302 | display: flex; 303 | justify-content: center; 304 | align-content: center; 305 | } 306 | 307 | .sidebar .codeLinks img { 308 | width: 20px; 309 | height: 20px; 310 | display: inline-block; 311 | z-index: 2; 312 | } 313 | 314 | #denoImg { 315 | width: 26px; 316 | height: 26px; 317 | } 318 | 319 | #githubLogo { 320 | margin-right: 3%; 321 | } 322 | 323 | .sidebar .codeLinks h4 { 324 | display: inline-block; 325 | margin: 0; 326 | color: #f7f7f7; 327 | z-index: 2; 328 | margin-left: 1%; 329 | } 330 | 331 | .sidebar .sideContent { 332 | background-color: rgba(219, 219, 219, 0.25); 333 | height: 85%; 334 | overflow-y: scroll; 335 | padding: 0.6rem; 336 | } 337 | 338 | /* SIDEBAR SOCIAL ACCOUNT BUTTONS */ 339 | .social-button { 340 | display: flex; 341 | justify-content: center; 342 | padding-top: 50px; 343 | } 344 | 345 | .fab { 346 | color: black; 347 | margin: 5px; 348 | } 349 | 350 | .fab:hover { 351 | color: #fff; 352 | } 353 | 354 | .user-info { 355 | text-align: center; 356 | padding: 20px 3px 0 3px; 357 | } 358 | 359 | /* Carousel Styling! */ 360 | #about::before, 361 | #about::after { 362 | box-sizing: border-box; 363 | position: relative; 364 | } 365 | 366 | #about { 367 | height: 100%; 368 | width: 100%; 369 | margin: 0; 370 | padding: 0; 371 | font-size: 3vmin; 372 | display: flex; 373 | justify-content: center; 374 | align-items: center; 375 | } 376 | 377 | .slides { 378 | display: grid; 379 | } 380 | 381 | .slides > .slide { 382 | grid-area: 1 / -1; 383 | } 384 | 385 | .slides > button { 386 | appearance: none; 387 | background: transparent; 388 | border: none; 389 | color: black; 390 | position: relative; 391 | font-size: 5rem; 392 | height: 0rem; 393 | top: 30%; 394 | transition: opacity 0.3s; 395 | opacity: 0.7; 396 | z-index: 5; 397 | } 398 | 399 | .slides > button:hover { 400 | opacity: 1; 401 | } 402 | 403 | .slides > button:focus { 404 | outline: none; 405 | } 406 | 407 | .slides > button:first-child { 408 | left: -50%; 409 | } 410 | 411 | .slides > button:last-child { 412 | right: -50%; 413 | } 414 | 415 | .slideContent { 416 | /* width: 15vw; */ 417 | height: 25vh; 418 | background-size: cover; 419 | background-position: center center; 420 | background-repeat: no-repeat; 421 | border-radius: 30px; 422 | 423 | transition: transform 0.5s ease-in-out; 424 | opacity: 0.7; 425 | } 426 | 427 | .slideContentInner { 428 | text-align: center; 429 | transform-style: preserve-3d; 430 | transform: translateZ(2rem); 431 | transition: opacity 0.3s linear; 432 | text-shadow: 0 0.1rem 1rem #000; 433 | opacity: 0; 434 | } 435 | 436 | .slideContentInner .slideSubtitle, 437 | .slideContentInner .slideTitle { 438 | font-size: 2rem; 439 | font-weight: bold; 440 | color: #fff; 441 | letter-spacing: 0.2ch; 442 | text-transform: uppercase; 443 | margin: 0; 444 | } 445 | 446 | .slide[data-active] { 447 | z-index: 2; 448 | pointer-events: auto; 449 | } 450 | 451 | .slide[data-active] .slideContentInner { 452 | opacity: 1; 453 | } 454 | 455 | .slide[data-active] .slideContent { 456 | --x: calc(var(--px) - 0.5); 457 | --y: calc(var(--py) - 0.5); 458 | opacity: 1; 459 | transform: perspective(1000px); 460 | } 461 | 462 | .slide[data-active] .slideContent:hover { 463 | transition: none; 464 | transform: perspective(1000px) rotateY(calc(var(--x) * 20deg)) 465 | rotateX(calc(var(--y) * -20deg)); 466 | } 467 | 468 | .tab { 469 | margin-left: 30px; 470 | } 471 | 472 | .nested { 473 | margin: 10px 0 0 30px; 474 | } 475 | 476 | .p { 477 | margin-bottom: 1rem; 478 | } 479 | 480 | .key { 481 | color: rgb(0, 139, 23); 482 | } 483 | 484 | /* STYLES DOCS PAGE */ 485 | 486 | #docsTOC .list-group-item.active { 487 | color: rgba(5, 5, 5, 1) !important; 488 | border-color: rgb(80, 250, 123) !important; 489 | background-color: rgb(80, 250, 123) !important; 490 | } 491 | 492 | #docsTOC .list-group-item { 493 | color: #fff !important; 494 | background-color: rgba(5, 5, 5, 0.7) !important; 495 | } 496 | 497 | .docContainer { 498 | padding: 2rem 3rem; 499 | width: 100%; 500 | height: 100%; 501 | overflow: scroll; 502 | background-color: rgba(219, 219, 219, 0.7); 503 | } 504 | 505 | .docContainer h1, 506 | .mainContainer h1 { 507 | text-align: center; 508 | font-size: 3.5rem; 509 | font-weight: 700; 510 | margin-bottom: 1.5rem; 511 | } 512 | 513 | .docContainer h2 { 514 | font-size: 3rem; 515 | font-weight: 700; 516 | margin-bottom: 1.5rem; 517 | } 518 | 519 | .docContainer h3 { 520 | font-size: 2rem; 521 | font-weight: 700; 522 | margin-bottom: 1.5rem; 523 | } 524 | 525 | .docContainer h3 { 526 | font-size: 1.5rem; 527 | font-weight: 700; 528 | margin-bottom: 1.5rem; 529 | } 530 | 531 | .docContainer p { 532 | font-size: 1rem; 533 | font-weight: 400; 534 | margin-bottom: 1.5rem; 535 | } 536 | 537 | .docContainer .docAside { 538 | border: 2px; 539 | border-style: dashed; 540 | padding: 1rem; 541 | } 542 | 543 | .docContainer code { 544 | font-family: monospace !important; 545 | } 546 | 547 | .obsidianInline { 548 | font-size: 100%; 549 | color: #212529 !important; 550 | } 551 | 552 | .mission { 553 | font-style: italic; 554 | font-size: 1.5rem; 555 | padding-left: 2rem; 556 | padding-right: 2rem; 557 | } 558 | 559 | .token.operator { 560 | background: transparent !important; 561 | } 562 | 563 | .apiLink { 564 | margin-top: 30px; 565 | } 566 | 567 | .apiLink p { 568 | color: rgb(246, 246, 246); 569 | } 570 | 571 | .apiLink a { 572 | color: rgb(35, 35, 255); 573 | text-emphasis-style: bold; 574 | } 575 | 576 | #mobile-collapse { 577 | display: none; 578 | opacity: 0; 579 | } 580 | 581 | /* phone sizing */ 582 | 583 | @media only screen and (max-width: 700px) { 584 | html { 585 | font-size: 1em; 586 | } 587 | 588 | .app { 589 | flex-direction: column; 590 | justify-content: flex-start; 591 | align-items: center; 592 | } 593 | 594 | .mainContainerLogo { 595 | height: 60vh; 596 | } 597 | 598 | .navBar { 599 | flex-direction: row; 600 | width: 100%; 601 | height: 11%; 602 | z-index: 10; 603 | max-width: 100%; 604 | justify-content: center; 605 | align-items: center; 606 | margin-right: 0.5vw; 607 | padding: 0; 608 | margin-top: 5%; 609 | } 610 | 611 | .navBtn, 612 | #demo-form { 613 | max-width: 15%; 614 | z-index: 10; 615 | padding: 0; 616 | } 617 | 618 | .mainContainer { 619 | width: 90%; 620 | height: 65%; 621 | margin: 0 5%; 622 | } 623 | .demoContainer { 624 | margin: 0; 625 | } 626 | 627 | .sidebar { 628 | position: absolute; 629 | flex-direction: row; 630 | justify-content: flex-start; 631 | z-index: 1; 632 | width: 94%; 633 | height: 20%; 634 | max-width: 94%; 635 | margin: 15px auto; 636 | background-color: rgba(0, 0, 0, 0.9); 637 | transition: height 1s, top 1s; 638 | } 639 | 640 | #mobile-collapse { 641 | width: 6vh; 642 | height: 6vh; 643 | background-color: rgba(0, 0, 0, 0); 644 | color: white; 645 | display: block; 646 | position: relative; 647 | border-radius: 20px; 648 | /* margin-top: 1%; */ 649 | margin: 1% 3% 1% 1%; 650 | opacity: 1; 651 | border: none; 652 | } 653 | 654 | #mobile-collapse:focus { 655 | outline: none; 656 | } 657 | 658 | /* .sideContent { 659 | padding: 0; 660 | } */ 661 | 662 | .codeLinks { 663 | position: absolute; 664 | display: none; 665 | visibility: hidden; 666 | } 667 | 668 | .navBarGitLink { 669 | margin-left: 7%; 670 | } 671 | 672 | .navBarGitLink, 673 | .navBarDenoLink { 674 | position: relative; 675 | display: inline-block; 676 | visibility: visible; 677 | max-width: 10%; 678 | } 679 | 680 | #githubLogoTop { 681 | width: 70%; 682 | } 683 | 684 | .navBarDenoLink img { 685 | width: 90%; 686 | } 687 | 688 | .social-button { 689 | padding-top: 0; 690 | } 691 | /* fix font size in Docs */ 692 | .docContainer { 693 | padding: 2rem 1rem; 694 | } 695 | 696 | .docContainer h1, 697 | .mainContainer h1 { 698 | font-size: 2.5rem; 699 | } 700 | 701 | .docContainer h2 { 702 | font-size: 2rem; 703 | } 704 | 705 | .docContainer h3 { 706 | font-size: 1.8rem; 707 | } 708 | 709 | .docContainer h3 { 710 | font-size: 1.5rem; 711 | } 712 | 713 | .docContainer p { 714 | font-size: 1rem; 715 | } 716 | } 717 | 718 | .container { 719 | max-width: 1280px; 720 | margin-left: 5%; 721 | margin-right: 5%; 722 | } 723 | 724 | .team { 725 | text-align: center; 726 | } 727 | 728 | .team h2 { 729 | margin-bottom: 8%; 730 | } 731 | 732 | .team-wrapper { 733 | margin: 5%; 734 | display: flex; 735 | justify-content: center; 736 | color: white; 737 | } 738 | 739 | .developerBio { 740 | margin-bottom: 1.5rem; 741 | text-align: center; 742 | } 743 | 744 | .developerName { 745 | margin: 0; 746 | } 747 | 748 | .devGrid { 749 | display: flex; 750 | flex-wrap: wrap; 751 | justify-content: space-evenly; 752 | padding: 1.5vw; 753 | } 754 | 755 | .developerImage { 756 | width: 80%; 757 | border-radius: 50%; 758 | opacity: 0.7; 759 | } 760 | 761 | a.githubLink { 762 | color: white; 763 | text-decoration: none; 764 | } 765 | --------------------------------------------------------------------------------