├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .github ├── FUNDING.yml ├── dependabot.yml └── workflows │ └── pull_request.yml ├── .gitignore ├── .prettierrc ├── .vscode └── settings.json ├── LICENSE.md ├── README.md ├── dev-example ├── .env.example ├── .gitignore ├── LICENSE ├── README.md ├── components │ ├── CustomCode.jsx │ ├── Header │ │ ├── index.js │ │ └── styles.module.css │ ├── TableOfContents.js │ └── ThemeToggler │ │ ├── constants.js │ │ ├── index.js │ │ └── styles.module.css ├── data │ ├── blocks.json │ ├── codeBlocks.json │ ├── mockVideos.json │ └── title.json ├── lib │ └── notion.js ├── next.config.js ├── package.json ├── pages │ ├── [id].js │ ├── _app.js │ ├── blog.js │ ├── index.js │ ├── index.module.css │ └── test │ │ ├── Text.js │ │ └── Text.json ├── public │ ├── favicon.ico │ ├── loremVideo.mp4 │ └── vercel.svg ├── styles │ └── globals.css └── utils │ └── isNavigatorDarkTheme.js ├── jest.config.js ├── package.json ├── src ├── components │ ├── common │ │ ├── Callout │ │ │ └── index.tsx │ │ ├── Code │ │ │ └── index.tsx │ │ ├── Divider │ │ │ └── index.tsx │ │ ├── DummyText │ │ │ └── index.tsx │ │ ├── Embed │ │ │ ├── index.tsx │ │ │ └── wrappedEmbed.tsx │ │ ├── EmptyBlock │ │ │ └── index.tsx │ │ ├── File │ │ │ └── index.tsx │ │ ├── Image │ │ │ ├── index.tsx │ │ │ └── wrappedImage.tsx │ │ ├── Link │ │ │ └── index.tsx │ │ ├── List │ │ │ ├── components │ │ │ │ ├── Checkbox │ │ │ │ │ └── index.tsx │ │ │ │ └── ListItem │ │ │ │ │ └── index.tsx │ │ │ ├── index.tsx │ │ │ └── styles.module.css │ │ ├── Paragraph │ │ │ └── index.tsx │ │ ├── Quote │ │ │ └── index.tsx │ │ ├── Table │ │ │ └── index.tsx │ │ ├── TableOfContents │ │ │ └── index.tsx │ │ ├── Title │ │ │ └── index.tsx │ │ └── Video │ │ │ ├── constants.tsx │ │ │ ├── index.tsx │ │ │ └── wrappedVideo.tsx │ └── core │ │ ├── Render │ │ └── index.tsx │ │ └── Text │ │ └── index.tsx ├── constants │ └── BlockComponentsMapper │ │ ├── index.ts │ │ └── types.ts ├── hoc │ ├── withContentValidation │ │ ├── constants.tsx │ │ └── index.tsx │ └── withCustomComponent │ │ ├── constants.ts │ │ └── index.tsx ├── index.tsx ├── styles │ ├── components.css │ └── index.css ├── types │ ├── Block.ts │ ├── BlockTypes.ts │ ├── NotionBlock.ts │ └── Text.ts ├── typings.d.ts └── utils │ ├── getBlocksToRender.ts │ ├── getClassname.ts │ ├── indexGenerator.ts │ └── slugify.ts ├── tsconfig.json └── tsconfig.test.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | build/ 2 | dist/ 3 | node_modules/ 4 | .snapshots/ 5 | *.min.js -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "env": { 4 | "jest": true 5 | }, 6 | "parserOptions": { 7 | "ecmaVersion": "latest", 8 | "sourceType": "module" 9 | } 10 | } -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | ko_fi: 9gustin 4 | custom: https://cafecito.app/9gustin 5 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "npm" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "daily" 12 | -------------------------------------------------------------------------------- /.github/workflows/pull_request.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | pull_request: 4 | branches: [main] 5 | types: [opened, synchronize] 6 | jobs: 7 | build: 8 | name: Build, lint, and test 9 | 10 | runs-on: ubuntu-18.04 11 | 12 | steps: 13 | - name: Checkout repo 14 | uses: actions/checkout@v2 15 | with: 16 | fetch-depth: 0 17 | 18 | - name: Use Node 19 | uses: actions/setup-node@v2 20 | with: 21 | node-version: 14 22 | 23 | - name: Install deps and build 24 | run: npm install 25 | 26 | - name: Lint 27 | run: yarn lint 28 | 29 | - name: Build 30 | run: yarn build 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # See https://help.github.com/ignore-files/ for more about ignoring files. 3 | 4 | # dependencies 5 | node_modules 6 | 7 | # builds 8 | build 9 | dist 10 | .rpt2_cache 11 | 12 | # misc 13 | .DS_Store 14 | .env 15 | .env.local 16 | .env.development.local 17 | .env.test.local 18 | .env.production.local 19 | 20 | npm-debug.log* 21 | yarn-debug.log* 22 | yarn-error.log* 23 | 24 | package-lock.json 25 | yarn.lock 26 | coverage 27 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "jsxSingleQuote": true, 4 | "semi": false, 5 | "tabWidth": 2, 6 | "bracketSpacing": true, 7 | "jsxBracketSameLine": false, 8 | "arrowParens": "always", 9 | "trailingComma": "none" 10 | } 11 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib" 3 | } -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 9gustin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |

React Notion Render

3 | 4 |

A library to render notion pages

5 |
6 |
7 | 8 | [![NPM](https://img.shields.io/npm/v/@9gustin/react-notion-render.svg)](https://www.npmjs.com/package/@9gustin/react-notion-render) 9 | ![npm](https://img.shields.io/npm/dw/@9gustin/react-notion-render) 10 | ![PR](https://img.shields.io/badge/PRs-welcome-brightgreen.svg) 11 | ![Stars](https://img.shields.io/github/stars/9gustin/react-notion-render.svg?style=social) 12 | 13 | ## Table of contents 14 | - [Description](#description) 15 | - [Installation](#installation) 16 | - [Examples](#examples) 17 | - [Basic example](#basic-example) 18 | - [Blog with Notion as CMS](#blog-with-notion-as-cms) 19 | - [Notion page to single page](#notion-page-to-single-page) 20 | - [Usage](#usage) 21 | - [Override built-in components (new)](#override-built-in-components-new) 22 | - [Giving Styles](#giving-styles) 23 | - [...moreProps](#moreprops) 24 | - [Custom Components](#custom-components) 25 | - [Display a custom table of contents](#display-a-custom-table-of-contents) 26 | - [Guides](#guides) 27 | - [How to use code blocks](https://github.com/9gustin/react-notion-render/wiki/About-code-blocks-and-how-to-colorize-it-%F0%9F%8E%A8) 28 | - [Supported blocks](#supported-blocks) 29 | - [Contributions](#contributions) 30 | 31 | ## Description 32 | 33 | When we want to [retrieve the content of a Notion page](https://developers.notion.com/docs/working-with-page-content), using the Notion API we will obtain a complex block structure(like [this example](https://github.com/9gustin/react-notion-render/blob/main/dev-example/data/blocks.json)). This package solves that structure and takes care of rendering that response. 34 | 35 | ## Installation 36 | 37 | ```bash 38 | npm i @9gustin/react-notion-render 39 | ``` 40 | 41 | ## Examples 42 | 43 | ### Basic example 44 | I would use the package [@notionhq/client](https://www.npmjs.com/package/@notionhq/client) to get data from the Notion API and take this example of [Notion Service](https://github.com/samuelkraft/notion-blog-nextjs/blob/master/lib/notion.js) also you can fetch the data from the api. This example take pages of an database an render the first of list. This example is an Page in Next.js. 45 | 46 | ```jsx 47 | import { Render } from '@9gustin/react-notion-render' 48 | import { getBlocks, getDatabase } from '../services/notion' 49 | 50 | export default ({blocks}) => 51 | 52 | export const getStaticProps = async () => { 53 | const DATABASE_ID = '54d0ff3097694ad08bd21932d598b93d' 54 | const database = await getDatabase(DATABASE_ID) 55 | const blocks = await getBlocks(database[0].id) 56 | 57 | return { 58 | props: { 59 | blocks 60 | } 61 | } 62 | } 63 | ``` 64 | 65 | ### Blog with Notion as CMS 66 | 67 | I've maded a template to blog page, that use this package and allows you have a blog using notion as CMS.
68 | 69 | 📎 Repo: [@9gustin/notion-blog-nextjs](https://github.com/9gustin/notion-blog-nextjs)
70 | 📚 Notion Database: [notion/notion-blog-nextjs](https://9gustin.notion.site/a30378a9a7a74a398a17b733136a70d4?v=db951035b8c44968ae226f2c2d358529)
71 | ✨Web: [blog-template](https://notion-blog-nextjs-umber.vercel.app/)
72 | 73 | **Note**: My personal blog now it's using this template. Url: [9gu.dev](https://9gu.dev) 74 | 75 | ## Usage 76 | 77 | ### Override built-in components (new) 78 | You can override the package components, for example, if you want to use your own Code component or to replace native for NextImage. For this you have the prop `blockComponentsMapper`. 79 | 80 | This works to use your own styles, a library of components (like Chackra UI, ANT Design) or better components than natives. 81 | 82 | For example, if you want to use a custom H1: 83 | ```JSX 84 | const MyHeading = ({plainText}) => { 85 | return

H1! {plainText}

86 | } 87 | ``` 88 | 89 | And in the render you pass the prop `blockComponentsMapper` like: 90 | ```JSX 91 | 94 | ``` 95 | 96 | ### How works? 97 | **blockComponentsMapper**
98 | It prop receives an json of type BlockComponentsMapperType, the keys represents the notion type: 99 | https://github.com/9gustin/react-notion-render/blob/154e094e9477b5dada03358e2cecf695c06bb4d3/src/constants/BlockComponentsMapper/types.ts 100 |
101 | 102 | And here the notion types enum(you can import it): 103 | https://github.com/9gustin/react-notion-render/blob/main/src/types/BlockTypes.ts 104 | 105 | **withContentValidation**
106 | I recommend that you import withContentValidation HOC from the package and wrap component on it, this HOC parse props and make it more clean, here the font-code: 107 | https://github.com/9gustin/react-notion-render/blob/154e094e9477b5dada03358e2cecf695c06bb4d3/src/hoc/withContentValidation/index.tsx 108 | 109 |
110 | 111 | I must work on a more clear documentation about this prop, but for now you can explore it. 112 | 113 | ### Mapping page url 114 | 115 | In Notion, page IDs are used to link between Notion pages. For example, if you link to a Notion page titled "Test" at `notion.so/test-1a2b3c4d`, the underlying markup will look like this: 116 | 117 | ```HTML 118 |
Link to page 119 | 120 | Test 121 | 122 |
123 | ``` 124 | 125 | When building a website from Notion content, you may use a different logic for creating paths to access those Notion pages. For example, the page above may now be available at `/test` path. To rewrite `/1a2b3c4d` to `/test`, you can define your own function for mapping url and pass it to prop `mapPageUrlFn` of the Render component. 126 | 127 | ### Giving styles 128 | If you followed the [basic example](#basic-example), you may notice that the page are rendered without styles, only pure text. To solve that we can use the Render props, like the following cases. 129 | 130 | #### Using default styles 131 | This package give you default styles, colors, text styles(blod, italic) and some little things, if you want use have to add two things: 132 | 133 | First import the stylesheet 134 | ```jsx 135 | import '@9gustin/react-notion-render/dist/index.css' 136 | ``` 137 | And then add to the Render the prop **useStyles**, like that: 138 | ```jsx 139 | 140 | ``` 141 | 142 | And it's all, now the page looks some better, i tried to not manipulate that styles so much to preserve generic styles. 143 | 144 | #### Using your own styles 145 | If you want to add styles by your own, you can use the prop **classNames**, this props gives classes to the elements, it make more easier to identify them. For example to paragraphs give the class "rnr-paragraph", and you can add this class in your CSS and give styles. 146 | 147 | ```jsx 148 | 149 | ``` 150 | This is independient to the prop **useStyles**, you can combinate them or use separated. 151 | 152 | **Components Reference**
153 | 154 | | ClassName | Notion Reference | HTML Tag | 155 | | ------------------ | ------------------- | ------------------------------------------------ | 156 | | rnr-heading_1 | Heading 1 | h1 | 157 | | rnr-heading_2 | Heading 2 | h2 | 158 | | rnr-heading_3 | Heading 3 | h3 | 159 | | rnr-paragraph | Paragraph | p | 160 | | rnr-to_do | To-do List | ul | 161 | | rnr-bulleted_list_item | Bulleted List | ul | 162 | | rnr-numbered_list_item | Numered List | ol | 163 | | rnr-toggle | Toggle List | ul | 164 | | rnr-image | Image | a | 165 | | rnr-video | Video | external: **iframe**, notion uploaded video: **video** | 166 | | rnr-file | File | a | 167 | | rnr-embed | Embed | iframe | 168 | | rnr-pdf | PDF | iframe | 169 | | rnr-callout | Callout | div | 170 | | rnr-quote | Quote | blockquote | 171 | | rnr-divider | Divider | hr | 172 | | rnr-code | Code | pre > code | 173 | | rnr-table_of_contents | Table of contents | ul | 174 | | rnr-table | Table | table | 175 | | rnr-table_row | Table row | tr | 176 | 177 | 178 | **Text Styles**
179 | | ClassName | Notion Reference | 180 | | ------------------ | ------------------- | 181 | | rnr-bold | Bold | 182 | | rnr-italic | Italicize | 183 | | rnr-strikethrough | Strike Through | 184 | | rnr-underline | Underline | 185 | | rnr-inline-code | Code | 186 | 187 | **Text colors**
188 | | ClassName | HEX | 189 | | ------------------ | --- | 190 | | rnr-red | #ff2525 | 191 | | rnr-gray | #979797 | 192 | | rnr-brown | #816868 | 193 | | rnr-orange | #FE9920 | 194 | | rnr-yellow | #F1DB4B | 195 | | rnr-green | #22ae65 | 196 | | rnr-purple | #a842ec | 197 | | rnr-pink | #FE5D9F | 198 | | rnr-blue | #0eb7e4 | 199 | 200 | ### ...moreProps 201 | The Render component has two more props that you can use. 202 | 203 | #### Custom title url 204 | With this package you can pin the titles in the url to share it. For example, if you have a title like **My Title** and you click it, the url looks like **url.com#my-title**. The function that parse the text it's [here](https://github.com/9gustin/react-notion-render/blob/main/src/utils/slugify.ts), you can check it. But if you want some diferent conversion you can pass a custom slugify function. In case that you want to separate characthers by _ instead of - yo can pass the **slugifyFn** prop: 205 | ```jsx 206 | text.replace(/[^a-zA-Z0-9]/g,'_')} /> 207 | ``` 208 | Or whatever you want, slugifyFn should receive and return a string.
209 | If you dont want this functionality you can disable it with the prop **simpleTitles**: 210 | ```jsx 211 | 212 | ``` 213 | 214 | #### Preserve empty blocks 215 | Now by default the Render component discard the empty blocks that you put in your notion page. If you want to preserve you can pass the prop **emptyBlocks** and it be rendered. 216 | ```jsx 217 | 218 | ``` 219 | 220 | The empty blocks contain the class "**rnr-empty-block**", this class has default styles (with **useStyles**) but you can apply your own styles. 221 | 222 | ### Custom components 223 | Now Notion API only supports text blocks, like h1, h2, h3, paragraph, lists([Notion Doc.](https://developers.notion.com/reference/block)). Custom components are here for you, it allows you to use other important blocks.
224 | 225 | **Important**
226 | The text to custom components sould be plain text, when you paste a link in Notion he convert to a link. You should convert it to plain text with the "Remove link" button. Like there: 227 | ![image](https://user-images.githubusercontent.com/38046239/122657679-46bd8300-d13c-11eb-9736-8c67e81a9ba7.png) 228 | 229 | 230 | #### Link 231 | Now you can use links like Markdown, links are supported by Notion API, but this add the possibility to made autorreference links, as an index. 232 | 233 | **Example:**
234 | ``` 235 | Index: 236 | [1. Declarative](#declarative) 237 | [2. Component Based](#component-based) 238 | [3. About React](#about-react) 239 | ``` 240 | The link be maded with the slugifyFn, you can [check the default](https://github.com/9gustin/react-notion-render/blob/main/src/utils/slugify.ts), or [pass a custom](#custom-title-url). 241 | 242 | ### Image 243 | ⚠️ **Now we support native notion images**, if you add a image in your notion page this package would render it ;). This option would not be deprecated, just a suggestion.
244 | 245 | This it simple, allows you to use images(includes GIF's). The syntax are the same like [Markdown images](https://www.digitalocean.com/community/tutorials/markdown-markdown-images). For it you have to include next text into your notion page as simple text
246 | 247 | **Example:**
248 | ``` 249 | ![My github profile pic](https://avatars.githubusercontent.com/u/38046239) 250 | ``` 251 | 252 | **Plus**
253 | Also you can add a link to image, like an image anchor. This link would be opened when the user click the image. Thats works adding an # with the link after the markdown image. 254 | ``` 255 | ![My github profile pic](https://avatars.githubusercontent.com/u/38046239)#https://github.com/9gustin 256 | ``` 257 | So when the user click my image in the blog it will be redirected to my github profile.
258 | 259 | ### Video 260 | ⚠️ **Now we support native notion videos**, if you add a video in your notion page this package would render it ;). This option would not be deprecated, just a suggestion
261 | You can embed Videos. You have 3 ways to embed a video. 262 | 263 | - Local 264 | - Youtube 265 | - Google Drive (with a public share url) 266 | 267 | **Structure:**
268 | ``` 269 | -[title, or alternative text](url) 270 | ``` 271 | 272 | **Example:**
273 | ``` 274 | -[my youtube video](https://youtu.be/aA7si7AmPkY) 275 | ``` 276 | 277 | 278 | ### Display a custom table of contents 279 | 280 | Now we exporting the **indexGenerator** function, with that you can show a table of contents of your page content. This function receive a list of blocks and return only the title blocks. The structure of the result it's like: 281 | 282 | ![image](https://user-images.githubusercontent.com/38046239/129499362-28448241-3bf9-47b7-8629-d40d7e90a447.png) 283 | 284 | you can use it like that: 285 | ```jsx 286 | import { indexGenerator, rnrSlugify } from '@9gustin/react-notion-render' 287 | 288 | const TableOfContents = ({blocks}) => { 289 | return ( 290 | <> 291 | Table of contents: 292 | 303 | 304 | ) 305 | } 306 | 307 | export default TableOfContents 308 | 309 | ``` 310 | if you want to add links use **rnrSlugify** or your [custom slugify function](#custom-title-url) to generate the href. 311 | 312 | ## Guides 313 | 314 | ### How to use code blocks 315 | Checkout in this repo wiki: 316 | https://github.com/9gustin/react-notion-render/wiki/About-code-blocks-and-how-to-colorize-it-%F0%9F%8E%A8 317 | 318 | ## Supported blocks 319 | Most common block types are supported. We happily accept pull requests to add support for the missing blocks. 320 | 321 | | Block | Supported | 322 | |---------|-------------| 323 | | Text | ✅ | 324 | | Heading | ✅ | 325 | | Image | ✅ | 326 | | Image Caption | ✅ | 327 | | Bulleted List | ✅ | 328 | | Numbered List | ✅ | 329 | | Quote | ✅ | 330 | | Callout | ✅ | 331 | | iframe | ✅ | 332 | | Video | ✅ | 333 | | File | ✅ | 334 | | Divider | ✅ | 335 | | Link | ✅ | 336 | | Code | ✅ | 337 | | Toggle List | ✅ | 338 | | Page Links | ✅ | 339 | | Checkbox | ✅ (read-only) | 340 | | Table Of Contents | ✅ | 341 | | Table | ✅ | 342 | | Synced blocks | ✅ | 343 | | Web Bookmark | ❌ | 344 | 345 | ## Contributions: 346 | If you find a bug, or want to suggest a feature you can create a [New Issue](https://github.com/9gustin/react-notion-render/issues/new) and will be analized. **Contributions of any kind welcome!** 347 | 348 | ### Running the dev example 349 | In the repo we have a dev example, with this you can test what you are developing. 350 | 351 | Clone repo and install package dependencies 352 | 353 | ```BASH 354 | git clone https://github.com/9gustin/react-notion-render.git 355 | cd react-notion-render 356 | npm install 357 | ``` 358 | 359 | Run dev example to test added features. The example are in next.js, so have to install this dependency into dev-example folder. 360 |
361 | **IMPORTANT:** Install dependencies of dev-example with `npm install`, not with `yarn`. This is because the dev-example uses parent node_modules (with file:../node_modules) and if install it with yarn it has problems with sub dependencies. 362 | 363 | ```BASH 364 | cd dev-example 365 | npm install 366 | ``` 367 | 368 | Add .env file with your notion token and run the example.
369 | Inside of dev-example folder you find a .env.example file with the structure of .env file. Steps: 370 | 1. Go to [notion.so/my-integrations](https://www.notion.so/my-integrations) and generate a new integration, copy the `Internal Integration Token` and paste it into the .env file wit the key `NOTION_TOKEN`. 371 | 2. Go to your notion, create a database that you want to use as example. Enter in it and copy the database id from url. `https://www.notion.so/YOUR_PROFILE/DATABASE_ID?v=RANDOM` 372 | 3. Share the database with the integration. 373 | 374 | More detail in [developers.notion.com/docs/getting-started](https://developers.notion.com/docs/getting-started) 375 | 376 | Starting the dev example
377 | To run the dev example we must be in the root of the project, in the package.json we have the `dev` command, that starts package compiler and dev example together. 378 | ```BASH 379 | cd .. //if we be inside of /dev-example 380 | npm run dev 381 | ``` 382 | 383 | And voila. The app are running in port 3001 because a config in my pc, if you have problems with this you can change it in package.json, `dev-example` command 384 | 385 | ### Running another example 386 | 387 | In case you want to use another example to test what you are developing, please do the following: 388 | 389 | 1. In the `package.json` file of your example project, which can be located anywhere in your machine, link to the local version of `react-notion-render`: 390 | 391 | ``` 392 | "dependencies": { 393 | "@9gustin/react-notion-render": "path/to-package" 394 | } 395 | ``` 396 | 397 | This path can either be relative or absolute path. 398 | 399 | 2. Run `npm install` to install all the required packages for the example project, including the locally compiled version of `react-notion-render`. 400 | 401 | 3. Open a new terminal window and navigate to the `react-notion-render`. Run `npm start` to watch for changes you make to `react-notion-render` and build it on the go. 402 | 403 | 4. Go back to the terminal window with your example project and run `npm run dev` to test new changes of `react-notion-render` in the example. 404 | 405 | ### Project structure 406 | 407 | | Directory | Description 408 | | ---------- | ----------- | 409 | `dev-example` | App maded with next.js, this app have the output of `src` as a package. You can test what are you developing here. 410 | `src` | the package `@9gustin/react-notion-render` 411 | `src/components` | React components 412 | `src/components/common` | here are the "simple components", like all notion components and generic components(Link for example). 413 | `src/components/core` | here are the logic components, the core of the package 414 | `src/components/core/Render` | Render are the package exported component, the entry point of the package. It receives a list of blocks and render it. 415 | `src/components/core/Text` | The text in notion are complex, this component contemplate text variants, like bold, italic. Also contemplate links. 416 | `src/hoc` | Higher order components / in there we apply some logic rules. 417 | `src/hoc/withContentValidation` | This HOC it's a filter before to pass the `Notion block` to the common components. almost every components are wrapped by this, and this objetive it's simplify props that the component would receive, applying package rules. 418 | `src/hoc/withCustomComponent` | The package supports [custom components](#custom-components). This HOC make it possible. before to render text validate if the text are a custom component and render it. 419 | `src/styles` | package styles. We just use plain css, the objective it's not apply much style, just the necessary. We use :global() to avoid compile problems with the className 420 | `src/types` | Types of the package 421 | `src/utils` | Common functions 422 | `src/index.tsx` | All that the package exports outside 423 | 424 | 425 | ## License 426 | 427 | MIT © [9gustin](https://github.com/9gustin) 428 | -------------------------------------------------------------------------------- /dev-example/.env.example: -------------------------------------------------------------------------------- 1 | NOTION_TOKEN= 2 | NOTION_DATABASE_ID= 3 | -------------------------------------------------------------------------------- /dev-example/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env.local 29 | .env.development.local 30 | .env.test.local 31 | .env.production.local 32 | 33 | # vercel 34 | .vercel 35 | -------------------------------------------------------------------------------- /dev-example/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Samuel Kraft 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /dev-example/README.md: -------------------------------------------------------------------------------- 1 | # My personal page 2 | 3 | Maded with [samuelkraft/notion-blog-nextjs](https://github.com/samuelkraft/notion-blog-nextjs) 🙌 4 | -------------------------------------------------------------------------------- /dev-example/components/CustomCode.jsx: -------------------------------------------------------------------------------- 1 | import Highlight, { defaultProps } from 'prism-react-renderer'; 2 | import dracula from 'prism-react-renderer/themes/vsDark'; 3 | import { useState } from 'react'; 4 | 5 | const CustomCode = ({ 6 | plainText: children, 7 | className, 8 | language, 9 | highlight, 10 | }) => { 11 | if (!children) return null 12 | const lang = 13 | language || className?.replace(/language-/, '') || ('bash'); 14 | 15 | const getTokenSetup = ({ getTokenProps, token, key }) => { 16 | const tokenProps = getTokenProps({ token, key }); 17 | if (highlight && highlight.includes(token.content)) { 18 | return

{token.content}

; 19 | } 20 | return ; 21 | }; 22 | 23 | const Button = (props) => ( 24 | 80 | {tokens.map((line, i) => ( 81 |
82 | {line.map((token, key) => 83 | getTokenSetup({ getTokenProps, token, key }) 84 | )} 85 |
86 | ))} 87 | 88 | )} 89 | 90 | ); 91 | }; 92 | export default CustomCode; -------------------------------------------------------------------------------- /dev-example/components/Header/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import ThemeToggler from '../ThemeToggler' 4 | 5 | import styles from './styles.module.css' 6 | 7 | function Header({ children, title }) { 8 | return ( 9 |
10 |
11 | 17 | 21 | 22 | + 23 | 31 | 32 | 37 | 38 | 39 |
40 | 41 | {title &&

{title}

} 42 | {children} 43 |
44 | ) 45 | } 46 | 47 | export default Header 48 | -------------------------------------------------------------------------------- /dev-example/components/Header/styles.module.css: -------------------------------------------------------------------------------- 1 | .logos { 2 | display: flex; 3 | align-items: center; 4 | padding: 20px 0; 5 | } 6 | 7 | .header { 8 | align-items: center; 9 | display: flex; 10 | flex-wrap: wrap; 11 | justify-content: space-between; 12 | margin-bottom: 50px; 13 | } 14 | 15 | .header h1 { 16 | width: 100%; 17 | } 18 | 19 | .header p { 20 | opacity: 0.7; 21 | line-height: 1.5; 22 | } -------------------------------------------------------------------------------- /dev-example/components/TableOfContents.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { indexGenerator } from '@9gustin/react-notion-render' 3 | 4 | const MyTableOfContents = ({ blocks }) => { 5 | return ( 6 | <> 7 | Table of contents: 8 |
    9 | { 10 | indexGenerator(blocks).map(({ id, plainText, type }) => ( 11 |
  • 12 | {plainText} - {type} 13 |
  • 14 | )) 15 | } 16 |
17 | 18 | ) 19 | } 20 | 21 | export default MyTableOfContents 22 | -------------------------------------------------------------------------------- /dev-example/components/ThemeToggler/constants.js: -------------------------------------------------------------------------------- 1 | export const THEMES = { 2 | DARK: 'dark', 3 | LIGHT: 'light' 4 | } 5 | 6 | export const THEME_KEY = 'SELECTED_THEME' 7 | 8 | export const THEMES_LABELS = { 9 | [THEMES.DARK]: 'Use light theme', 10 | [THEMES.LIGHT]: 'Use dark theme' 11 | } 12 | -------------------------------------------------------------------------------- /dev-example/components/ThemeToggler/index.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react' 2 | import { THEMES, THEME_KEY, THEMES_LABELS } from './constants' 3 | import isNavigatorDarkTheme from '../../utils/isNavigatorDarkTheme' 4 | 5 | import styles from './styles.module.css' 6 | 7 | function ThemeToggler() { 8 | const [selectedTheme, setTheme] = useState() 9 | 10 | const handleChangeTheme = () => 11 | setTheme( 12 | THEMES[ 13 | Object.keys(THEMES).find((theme) => THEMES[theme] !== selectedTheme) 14 | ] 15 | ) 16 | 17 | const actualTheme = () => window.localStorage.getItem(THEME_KEY) 18 | 19 | useEffect(() => { 20 | setTheme( 21 | actualTheme() || (isNavigatorDarkTheme() ? THEMES.DARK : THEMES.LIGHT) 22 | ) 23 | }, []) 24 | 25 | useEffect(() => { 26 | if (selectedTheme) { 27 | window.localStorage.setItem(THEME_KEY, selectedTheme) 28 | if (selectedTheme === THEMES.DARK) { 29 | document.body.classList.add(THEMES.DARK) 30 | } else { 31 | document.body.classList.remove(THEMES.DARK) 32 | } 33 | } 34 | }, [selectedTheme]) 35 | 36 | return ( 37 | 40 | ) 41 | } 42 | 43 | export default ThemeToggler 44 | -------------------------------------------------------------------------------- /dev-example/components/ThemeToggler/styles.module.css: -------------------------------------------------------------------------------- 1 | .button { 2 | background: transparent; 3 | border: 2px solid var(--text-color); 4 | border-radius: 8px; 5 | color: var(--text-color); 6 | padding: 8px; 7 | transition: .4s; 8 | } 9 | 10 | .button:hover, 11 | .button:active, 12 | .button:focus { 13 | background: var(--text-color); 14 | color: var(--bkg-color) 15 | } 16 | -------------------------------------------------------------------------------- /dev-example/data/blocks.json: -------------------------------------------------------------------------------- 1 | { 2 | "object": "list", 3 | "results": [ 4 | { 5 | "object": "block", 6 | "id": "ba996dec-83f2-49b1-ba68-60a3a5ae8fdb", 7 | "created_time": "2021-05-23T18:00:16.286Z", 8 | "last_edited_time": "2021-05-23T18:02:00.000Z", 9 | "has_children": false, 10 | "type": "heading_1", 11 | "heading_1": { 12 | "text": [ 13 | { 14 | "type": "text", 15 | "text": { 16 | "content": "A JavaScript library for building user interfaces", 17 | "link": null 18 | }, 19 | "annotations": { 20 | "bold": false, 21 | "italic": false, 22 | "strikethrough": false, 23 | "underline": false, 24 | "code": false, 25 | "color": "blue" 26 | }, 27 | "plain_text": "A JavaScript library for building user interfaces", 28 | "href": null 29 | } 30 | ] 31 | } 32 | }, 33 | { 34 | "object": "block", 35 | "id": "f049bae6-b331-40af-ba64-8816f9019bac", 36 | "created_time": "2021-05-23T18:00:00.000Z", 37 | "last_edited_time": "2021-05-23T18:00:00.000Z", 38 | "has_children": false, 39 | "type": "paragraph", 40 | "paragraph": { "text": [] } 41 | }, 42 | { 43 | "object": "block", 44 | "id": "7652683a-e7d6-4caf-9e40-809313c4459c", 45 | "created_time": "2021-05-23T18:00:39.115Z", 46 | "last_edited_time": "2021-05-23T18:04:00.000Z", 47 | "has_children": false, 48 | "type": "heading_3", 49 | "heading_3": { 50 | "text": [ 51 | { 52 | "type": "text", 53 | "text": { "content": "Declarative", "link": null }, 54 | "annotations": { 55 | "bold": false, 56 | "italic": true, 57 | "strikethrough": false, 58 | "underline": true, 59 | "code": false, 60 | "color": "default" 61 | }, 62 | "plain_text": "Declarative", 63 | "href": null 64 | } 65 | ] 66 | } 67 | }, 68 | { 69 | "object": "block", 70 | "id": "d8642bb1-7561-477d-943b-f16ec951dc02", 71 | "created_time": "2021-05-23T18:00:00.000Z", 72 | "last_edited_time": "2021-05-23T18:01:00.000Z", 73 | "has_children": false, 74 | "type": "paragraph", 75 | "paragraph": { 76 | "text": [ 77 | { 78 | "type": "text", 79 | "text": { "content": "React ", "link": null }, 80 | "annotations": { 81 | "bold": true, 82 | "italic": false, 83 | "strikethrough": false, 84 | "underline": false, 85 | "code": false, 86 | "color": "default" 87 | }, 88 | "plain_text": "React ", 89 | "href": null 90 | }, 91 | { 92 | "type": "text", 93 | "text": { 94 | "content": "makes it painless to create interactive UIs. Design simple views for each state in your application, and ", 95 | "link": null 96 | }, 97 | "annotations": { 98 | "bold": false, 99 | "italic": false, 100 | "strikethrough": false, 101 | "underline": false, 102 | "code": false, 103 | "color": "default" 104 | }, 105 | "plain_text": "makes it painless to create interactive UIs. Design simple views for each state in your application, and ", 106 | "href": null 107 | }, 108 | { 109 | "type": "text", 110 | "text": { "content": "React ", "link": null }, 111 | "annotations": { 112 | "bold": true, 113 | "italic": false, 114 | "strikethrough": false, 115 | "underline": false, 116 | "code": false, 117 | "color": "default" 118 | }, 119 | "plain_text": "React ", 120 | "href": null 121 | }, 122 | { 123 | "type": "text", 124 | "text": { 125 | "content": "will efficiently update and render just the right components when your data changes.", 126 | "link": null 127 | }, 128 | "annotations": { 129 | "bold": false, 130 | "italic": false, 131 | "strikethrough": false, 132 | "underline": false, 133 | "code": false, 134 | "color": "default" 135 | }, 136 | "plain_text": "will efficiently update and render just the right components when your data changes.", 137 | "href": null 138 | } 139 | ] 140 | } 141 | }, 142 | { 143 | "object": "block", 144 | "id": "61ffef59-854b-41ee-b55b-c6d5bd85692b", 145 | "created_time": "2021-05-23T18:01:00.000Z", 146 | "last_edited_time": "2021-05-23T18:07:00.000Z", 147 | "has_children": false, 148 | "type": "paragraph", 149 | "paragraph": { 150 | "text": [ 151 | { 152 | "type": "text", 153 | "text": { "content": "Declarative ", "link": null }, 154 | "annotations": { 155 | "bold": false, 156 | "italic": false, 157 | "strikethrough": false, 158 | "underline": false, 159 | "code": false, 160 | "color": "red" 161 | }, 162 | "plain_text": "Declarative ", 163 | "href": null 164 | }, 165 | { 166 | "type": "text", 167 | "text": { "content": "views make your code more ", "link": null }, 168 | "annotations": { 169 | "bold": false, 170 | "italic": false, 171 | "strikethrough": false, 172 | "underline": false, 173 | "code": false, 174 | "color": "default" 175 | }, 176 | "plain_text": "views make your code more ", 177 | "href": null 178 | }, 179 | { 180 | "type": "text", 181 | "text": { "content": "predictable", "link": null }, 182 | "annotations": { 183 | "bold": false, 184 | "italic": false, 185 | "strikethrough": false, 186 | "underline": false, 187 | "code": false, 188 | "color": "green" 189 | }, 190 | "plain_text": "predictable", 191 | "href": null 192 | }, 193 | { 194 | "type": "text", 195 | "text": { "content": " and ", "link": null }, 196 | "annotations": { 197 | "bold": false, 198 | "italic": false, 199 | "strikethrough": false, 200 | "underline": false, 201 | "code": false, 202 | "color": "default" 203 | }, 204 | "plain_text": " and ", 205 | "href": null 206 | }, 207 | { 208 | "type": "text", 209 | "text": { "content": "easier to debug", "link": null }, 210 | "annotations": { 211 | "bold": false, 212 | "italic": false, 213 | "strikethrough": false, 214 | "underline": false, 215 | "code": false, 216 | "color": "pink" 217 | }, 218 | "plain_text": "easier to debug", 219 | "href": null 220 | }, 221 | { 222 | "type": "text", 223 | "text": { "content": ".", "link": null }, 224 | "annotations": { 225 | "bold": false, 226 | "italic": false, 227 | "strikethrough": false, 228 | "underline": false, 229 | "code": false, 230 | "color": "default" 231 | }, 232 | "plain_text": ".", 233 | "href": null 234 | } 235 | ] 236 | } 237 | }, 238 | { 239 | "object": "block", 240 | "id": "c5d55884-6707-4dcd-8a0d-4a2d5b448d9b", 241 | "created_time": "2021-05-23T18:00:43.238Z", 242 | "last_edited_time": "2021-05-23T18:03:00.000Z", 243 | "has_children": false, 244 | "type": "heading_3", 245 | "heading_3": { 246 | "text": [ 247 | { 248 | "type": "text", 249 | "text": { "content": "Component-Based", "link": null }, 250 | "annotations": { 251 | "bold": false, 252 | "italic": true, 253 | "strikethrough": false, 254 | "underline": true, 255 | "code": false, 256 | "color": "default" 257 | }, 258 | "plain_text": "Component-Based", 259 | "href": null 260 | } 261 | ] 262 | } 263 | }, 264 | { 265 | "object": "block", 266 | "id": "682088f2-04cc-4615-b89d-e9bdc979b03b", 267 | "created_time": "2021-05-23T18:01:00.000Z", 268 | "last_edited_time": "2021-05-23T18:01:00.000Z", 269 | "has_children": false, 270 | "type": "paragraph", 271 | "paragraph": { 272 | "text": [ 273 | { 274 | "type": "text", 275 | "text": { 276 | "content": "Build encapsulated components that manage their own state, then compose them to make complex UIs.", 277 | "link": null 278 | }, 279 | "annotations": { 280 | "bold": false, 281 | "italic": false, 282 | "strikethrough": false, 283 | "underline": false, 284 | "code": false, 285 | "color": "default" 286 | }, 287 | "plain_text": "Build encapsulated components that manage their own state, then compose them to make complex UIs.", 288 | "href": null 289 | } 290 | ] 291 | } 292 | }, 293 | { 294 | "object": "block", 295 | "id": "b4300e87-fc38-47c8-a7f4-8698c183ed8c", 296 | "created_time": "2021-05-23T18:01:00.000Z", 297 | "last_edited_time": "2021-05-23T18:02:00.000Z", 298 | "has_children": false, 299 | "type": "paragraph", 300 | "paragraph": { 301 | "text": [ 302 | { 303 | "type": "text", 304 | "text": { 305 | "content": "Since component logic is written in JavaScript instead of templates, you can easily pass rich data through your app and keep state out of the ", 306 | "link": null 307 | }, 308 | "annotations": { 309 | "bold": false, 310 | "italic": false, 311 | "strikethrough": false, 312 | "underline": false, 313 | "code": false, 314 | "color": "default" 315 | }, 316 | "plain_text": "Since component logic is written in JavaScript instead of templates, you can easily pass rich data through your app and keep state out of the ", 317 | "href": null 318 | }, 319 | { 320 | "type": "text", 321 | "text": { "content": "DOM", "link": null }, 322 | "annotations": { 323 | "bold": false, 324 | "italic": true, 325 | "strikethrough": false, 326 | "underline": false, 327 | "code": false, 328 | "color": "purple" 329 | }, 330 | "plain_text": "DOM", 331 | "href": null 332 | }, 333 | { 334 | "type": "text", 335 | "text": { "content": ".", "link": null }, 336 | "annotations": { 337 | "bold": false, 338 | "italic": false, 339 | "strikethrough": false, 340 | "underline": false, 341 | "code": false, 342 | "color": "default" 343 | }, 344 | "plain_text": ".", 345 | "href": null 346 | } 347 | ] 348 | } 349 | }, 350 | { 351 | "object": "block", 352 | "id": "f3efe0a7-5f21-486c-9011-dd2783ba9d41", 353 | "created_time": "2021-05-23T18:00:56.930Z", 354 | "last_edited_time": "2021-05-23T18:03:00.000Z", 355 | "has_children": false, 356 | "type": "heading_3", 357 | "heading_3": { 358 | "text": [ 359 | { 360 | "type": "text", 361 | "text": { "content": "Learn Once, Write Anywhere", "link": null }, 362 | "annotations": { 363 | "bold": false, 364 | "italic": true, 365 | "strikethrough": false, 366 | "underline": true, 367 | "code": false, 368 | "color": "default" 369 | }, 370 | "plain_text": "Learn Once, Write Anywhere", 371 | "href": null 372 | } 373 | ] 374 | } 375 | }, 376 | { 377 | "object": "block", 378 | "id": "cfd73a62-f10c-4c7d-9923-b0af537ab235", 379 | "created_time": "2021-05-23T18:00:00.000Z", 380 | "last_edited_time": "2021-05-23T18:01:00.000Z", 381 | "has_children": false, 382 | "type": "paragraph", 383 | "paragraph": { 384 | "text": [ 385 | { 386 | "type": "text", 387 | "text": { 388 | "content": "We don’t make assumptions about the rest of your technology stack, so you can develop new features in ", 389 | "link": null 390 | }, 391 | "annotations": { 392 | "bold": false, 393 | "italic": false, 394 | "strikethrough": false, 395 | "underline": false, 396 | "code": false, 397 | "color": "default" 398 | }, 399 | "plain_text": "We don’t make assumptions about the rest of your technology stack, so you can develop new features in ", 400 | "href": null 401 | }, 402 | { 403 | "type": "text", 404 | "text": { "content": "React ", "link": null }, 405 | "annotations": { 406 | "bold": true, 407 | "italic": false, 408 | "strikethrough": false, 409 | "underline": false, 410 | "code": false, 411 | "color": "default" 412 | }, 413 | "plain_text": "React ", 414 | "href": null 415 | }, 416 | { 417 | "type": "text", 418 | "text": { 419 | "content": "without rewriting existing code.", 420 | "link": null 421 | }, 422 | "annotations": { 423 | "bold": false, 424 | "italic": false, 425 | "strikethrough": false, 426 | "underline": false, 427 | "code": false, 428 | "color": "default" 429 | }, 430 | "plain_text": "without rewriting existing code.", 431 | "href": null 432 | } 433 | ] 434 | } 435 | }, 436 | { 437 | "object": "block", 438 | "id": "dfbd5367-e0fe-4c87-ad53-0cd589d224c8", 439 | "created_time": "2021-05-23T18:01:00.000Z", 440 | "last_edited_time": "2021-05-23T18:07:00.000Z", 441 | "has_children": false, 442 | "type": "paragraph", 443 | "paragraph": { 444 | "text": [ 445 | { 446 | "type": "text", 447 | "text": { "content": "React ", "link": null }, 448 | "annotations": { 449 | "bold": true, 450 | "italic": false, 451 | "strikethrough": false, 452 | "underline": false, 453 | "code": false, 454 | "color": "default" 455 | }, 456 | "plain_text": "React ", 457 | "href": null 458 | }, 459 | { 460 | "type": "text", 461 | "text": { "content": "can also ", "link": null }, 462 | "annotations": { 463 | "bold": false, 464 | "italic": false, 465 | "strikethrough": false, 466 | "underline": false, 467 | "code": false, 468 | "color": "default" 469 | }, 470 | "plain_text": "can also ", 471 | "href": null 472 | }, 473 | { 474 | "type": "text", 475 | "text": { "content": "render on the server ", "link": null }, 476 | "annotations": { 477 | "bold": false, 478 | "italic": false, 479 | "strikethrough": false, 480 | "underline": false, 481 | "code": false, 482 | "color": "gray" 483 | }, 484 | "plain_text": "render on the server ", 485 | "href": null 486 | }, 487 | { 488 | "type": "text", 489 | "text": { "content": "using ", "link": null }, 490 | "annotations": { 491 | "bold": false, 492 | "italic": false, 493 | "strikethrough": false, 494 | "underline": false, 495 | "code": false, 496 | "color": "default" 497 | }, 498 | "plain_text": "using ", 499 | "href": null 500 | }, 501 | { 502 | "type": "text", 503 | "text": { "content": "Node ", "link": null }, 504 | "annotations": { 505 | "bold": false, 506 | "italic": false, 507 | "strikethrough": false, 508 | "underline": false, 509 | "code": false, 510 | "color": "green" 511 | }, 512 | "plain_text": "Node ", 513 | "href": null 514 | }, 515 | { 516 | "type": "text", 517 | "text": { "content": "and power mobile apps using ", "link": null }, 518 | "annotations": { 519 | "bold": false, 520 | "italic": false, 521 | "strikethrough": false, 522 | "underline": false, 523 | "code": false, 524 | "color": "default" 525 | }, 526 | "plain_text": "and power mobile apps using ", 527 | "href": null 528 | }, 529 | { 530 | "type": "text", 531 | "text": { 532 | "content": "React Native", 533 | "link": { "url": "https://reactnative.dev/" } 534 | }, 535 | "annotations": { 536 | "bold": false, 537 | "italic": false, 538 | "strikethrough": false, 539 | "underline": false, 540 | "code": false, 541 | "color": "default" 542 | }, 543 | "plain_text": "React Native", 544 | "href": "https://reactnative.dev/" 545 | }, 546 | { 547 | "type": "text", 548 | "text": { "content": ".", "link": null }, 549 | "annotations": { 550 | "bold": false, 551 | "italic": false, 552 | "strikethrough": false, 553 | "underline": false, 554 | "code": false, 555 | "color": "default" 556 | }, 557 | "plain_text": ".", 558 | "href": null 559 | } 560 | ] 561 | } 562 | }, 563 | { 564 | "object": "block", 565 | "id": "09d82a52-6154-48fb-af59-9855bbcfed00", 566 | "created_time": "2021-05-23T18:03:27.159Z", 567 | "last_edited_time": "2021-05-23T18:03:00.000Z", 568 | "has_children": false, 569 | "type": "heading_3", 570 | "heading_3": { 571 | "text": [ 572 | { 573 | "type": "text", 574 | "text": { "content": "About React", "link": null }, 575 | "annotations": { 576 | "bold": false, 577 | "italic": false, 578 | "strikethrough": false, 579 | "underline": false, 580 | "code": false, 581 | "color": "brown" 582 | }, 583 | "plain_text": "About React", 584 | "href": null 585 | } 586 | ] 587 | } 588 | }, 589 | { 590 | "object": "block", 591 | "id": "50c478b5-d750-462e-a35a-094d41dfc70f", 592 | "created_time": "2021-05-23T18:02:50.348Z", 593 | "last_edited_time": "2021-05-23T18:02:00.000Z", 594 | "has_children": false, 595 | "type": "bulleted_list_item", 596 | "bulleted_list_item": { 597 | "text": [ 598 | { 599 | "type": "text", 600 | "text": { 601 | "content": "Create native apps for Android and iOS using React", 602 | "link": null 603 | }, 604 | "annotations": { 605 | "bold": false, 606 | "italic": false, 607 | "strikethrough": false, 608 | "underline": false, 609 | "code": false, 610 | "color": "default" 611 | }, 612 | "plain_text": "Create native apps for Android and iOS using React", 613 | "href": null 614 | } 615 | ] 616 | } 617 | }, 618 | { 619 | "object": "block", 620 | "id": "e64a31d0-ab75-417e-b6c5-340743616d8d", 621 | "created_time": "2021-05-23T18:02:00.000Z", 622 | "last_edited_time": "2021-05-23T18:02:00.000Z", 623 | "has_children": false, 624 | "type": "bulleted_list_item", 625 | "bulleted_list_item": { 626 | "text": [ 627 | { 628 | "type": "text", 629 | "text": { 630 | "content": "Written in JavaScript—rendered with native code", 631 | "link": null 632 | }, 633 | "annotations": { 634 | "bold": false, 635 | "italic": false, 636 | "strikethrough": false, 637 | "underline": false, 638 | "code": false, 639 | "color": "default" 640 | }, 641 | "plain_text": "Written in JavaScript—rendered with native code", 642 | "href": null 643 | } 644 | ] 645 | } 646 | }, 647 | { 648 | "object": "block", 649 | "id": "34bf23ba-5b11-4713-8d47-7a7e64d66573", 650 | "created_time": "2021-05-23T18:02:00.000Z", 651 | "last_edited_time": "2021-05-23T18:03:00.000Z", 652 | "has_children": false, 653 | "type": "bulleted_list_item", 654 | "bulleted_list_item": { 655 | "text": [ 656 | { 657 | "type": "text", 658 | "text": { 659 | "content": "Native Development For Everyone", 660 | "link": null 661 | }, 662 | "annotations": { 663 | "bold": false, 664 | "italic": false, 665 | "strikethrough": false, 666 | "underline": false, 667 | "code": false, 668 | "color": "default" 669 | }, 670 | "plain_text": "Native Development For Everyone", 671 | "href": null 672 | } 673 | ] 674 | } 675 | }, 676 | { 677 | "object": "block", 678 | "id": "fcf3e232-800a-4856-8241-dac3fcbe3465", 679 | "created_time": "2021-05-23T18:03:00.000Z", 680 | "last_edited_time": "2021-05-23T18:03:00.000Z", 681 | "has_children": false, 682 | "type": "bulleted_list_item", 683 | "bulleted_list_item": { 684 | "text": [ 685 | { 686 | "type": "text", 687 | "text": { "content": "Seamless Cross-Platform", "link": null }, 688 | "annotations": { 689 | "bold": false, 690 | "italic": false, 691 | "strikethrough": false, 692 | "underline": false, 693 | "code": false, 694 | "color": "default" 695 | }, 696 | "plain_text": "Seamless Cross-Platform", 697 | "href": null 698 | } 699 | ] 700 | } 701 | }, 702 | { 703 | "object": "block", 704 | "id": "67a4bff6-42ac-4cd3-ae38-ed2dc2a07a12", 705 | "created_time": "2021-05-23T18:03:00.000Z", 706 | "last_edited_time": "2021-05-23T18:03:00.000Z", 707 | "has_children": false, 708 | "type": "bulleted_list_item", 709 | "bulleted_list_item": { 710 | "text": [ 711 | { 712 | "type": "text", 713 | "text": { "content": "Fast Refresh", "link": null }, 714 | "annotations": { 715 | "bold": false, 716 | "italic": false, 717 | "strikethrough": false, 718 | "underline": false, 719 | "code": false, 720 | "color": "default" 721 | }, 722 | "plain_text": "Fast Refresh", 723 | "href": null 724 | } 725 | ] 726 | } 727 | }, 728 | { 729 | "object": "block", 730 | "id": "177ff824-a7f4-424a-a306-63021b3eaa2d", 731 | "created_time": "2021-05-23T18:03:00.000Z", 732 | "last_edited_time": "2021-05-23T18:03:00.000Z", 733 | "has_children": false, 734 | "type": "bulleted_list_item", 735 | "bulleted_list_item": { 736 | "text": [ 737 | { 738 | "type": "text", 739 | "text": { 740 | "content": "Facebook Supported, Community Driven", 741 | "link": null 742 | }, 743 | "annotations": { 744 | "bold": false, 745 | "italic": false, 746 | "strikethrough": false, 747 | "underline": false, 748 | "code": false, 749 | "color": "default" 750 | }, 751 | "plain_text": "Facebook Supported, Community Driven", 752 | "href": null 753 | } 754 | ] 755 | } 756 | }, 757 | { 758 | "object": "block", 759 | "id": "e0b01a3d-7b18-440e-8f6d-8c69d7c1e293", 760 | "created_time": "2021-05-28T21:21:37.075Z", 761 | "last_edited_time": "2021-05-28T21:21:00.000Z", 762 | "has_children": false, 763 | "type": "heading_3", 764 | "heading_3": { 765 | "text": [ 766 | { 767 | "type": "text", 768 | "text": { "content": "Learning React", "link": null }, 769 | "annotations": { 770 | "bold": false, 771 | "italic": false, 772 | "strikethrough": false, 773 | "underline": false, 774 | "code": false, 775 | "color": "default" 776 | }, 777 | "plain_text": "Learning React", 778 | "href": null 779 | } 780 | ] 781 | } 782 | }, 783 | { 784 | "object": "block", 785 | "id": "cc1b4f6c-681d-40c2-a20c-fa4b15c29b74", 786 | "created_time": "2021-05-28T21:21:03.070Z", 787 | "last_edited_time": "2021-05-28T21:22:00.000Z", 788 | "has_children": false, 789 | "type": "to_do", 790 | "to_do": { 791 | "text": [ 792 | { 793 | "type": "text", 794 | "text": { "content": "Create React App", "link": null }, 795 | "annotations": { 796 | "bold": false, 797 | "italic": false, 798 | "strikethrough": false, 799 | "underline": false, 800 | "code": false, 801 | "color": "default" 802 | }, 803 | "plain_text": "Create React App", 804 | "href": null 805 | } 806 | ], 807 | "checked": false 808 | } 809 | }, 810 | { 811 | "object": "block", 812 | "id": "bcf824fc-eb7d-46b9-ab40-032291c3247e", 813 | "created_time": "2021-05-28T21:21:00.000Z", 814 | "last_edited_time": "2021-05-29T00:01:00.000Z", 815 | "has_children": false, 816 | "type": "to_do", 817 | "to_do": { 818 | "text": [ 819 | { 820 | "type": "text", 821 | "text": { "content": "JSX", "link": null }, 822 | "annotations": { 823 | "bold": false, 824 | "italic": false, 825 | "strikethrough": false, 826 | "underline": false, 827 | "code": false, 828 | "color": "default" 829 | }, 830 | "plain_text": "JSX", 831 | "href": null 832 | } 833 | ], 834 | "checked": true 835 | } 836 | }, 837 | { 838 | "object": "block", 839 | "id": "0ecf373e-80c6-4bf3-a3f1-73383f817ccf", 840 | "created_time": "2021-05-28T21:22:00.000Z", 841 | "last_edited_time": "2021-05-28T21:22:00.000Z", 842 | "has_children": false, 843 | "type": "to_do", 844 | "to_do": { 845 | "text": [ 846 | { 847 | "type": "text", 848 | "text": { "content": "Functional Components", "link": null }, 849 | "annotations": { 850 | "bold": false, 851 | "italic": false, 852 | "strikethrough": false, 853 | "underline": false, 854 | "code": false, 855 | "color": "default" 856 | }, 857 | "plain_text": "Functional Components", 858 | "href": null 859 | } 860 | ], 861 | "checked": false 862 | } 863 | }, 864 | { 865 | "object": "block", 866 | "id": "4b33d264-c80a-4353-a844-a0cf9184eb39", 867 | "created_time": "2021-05-28T21:22:00.000Z", 868 | "last_edited_time": "2021-05-29T00:01:00.000Z", 869 | "has_children": false, 870 | "type": "to_do", 871 | "to_do": { 872 | "text": [ 873 | { 874 | "type": "text", 875 | "text": { "content": "Props vs State", "link": null }, 876 | "annotations": { 877 | "bold": false, 878 | "italic": false, 879 | "strikethrough": false, 880 | "underline": false, 881 | "code": false, 882 | "color": "default" 883 | }, 884 | "plain_text": "Props vs State", 885 | "href": null 886 | } 887 | ], 888 | "checked": true 889 | } 890 | }, 891 | { 892 | "object": "block", 893 | "id": "2b77ba56-17ae-470b-b800-e69c3b85e938", 894 | "created_time": "2021-05-28T21:23:00.000Z", 895 | "last_edited_time": "2021-05-28T21:23:00.000Z", 896 | "has_children": false, 897 | "type": "paragraph", 898 | "paragraph": { "text": [] } 899 | }, 900 | { 901 | "object": "block", 902 | "id": "dd52419b-d509-4a60-ad6d-8d0c4b06ca6e", 903 | "created_time": "2021-05-28T21:23:18.013Z", 904 | "last_edited_time": "2021-05-29T21:37:00.000Z", 905 | "has_children": true, 906 | "type": "toggle", 907 | "toggle": { 908 | "text": [ 909 | { 910 | "type": "text", 911 | "text": { "content": "A Simple Component", "link": null }, 912 | "annotations": { 913 | "bold": false, 914 | "italic": false, 915 | "strikethrough": false, 916 | "underline": false, 917 | "code": false, 918 | "color": "default" 919 | }, 920 | "plain_text": "A Simple Component", 921 | "href": null 922 | } 923 | ], 924 | "children": [ 925 | { 926 | "object": "block", 927 | "id": "710af982-db1c-4f6a-abd8-3097bc586446", 928 | "created_time": "2021-05-29T21:37:13.945Z", 929 | "last_edited_time": "2021-05-29T21:37:00.000Z", 930 | "has_children": false, 931 | "type": "heading_3", 932 | "heading_3": { 933 | "text": [ 934 | { 935 | "type": "text", 936 | "text": { "content": "Inner Title", "link": null }, 937 | "annotations": { 938 | "bold": false, 939 | "italic": false, 940 | "strikethrough": false, 941 | "underline": false, 942 | "code": false, 943 | "color": "default" 944 | }, 945 | "plain_text": "Inner Title", 946 | "href": null 947 | } 948 | ] 949 | } 950 | }, 951 | { 952 | "object": "block", 953 | "id": "baa84dfa-a8ff-459f-ba0d-eb766a451ad9", 954 | "created_time": "2021-05-28T21:25:00.000Z", 955 | "last_edited_time": "2021-05-29T21:37:00.000Z", 956 | "has_children": false, 957 | "type": "paragraph", 958 | "paragraph": { 959 | "text": [ 960 | { 961 | "type": "text", 962 | "text": { "content": "React ", "link": null }, 963 | "annotations": { 964 | "bold": false, 965 | "italic": true, 966 | "strikethrough": false, 967 | "underline": true, 968 | "code": false, 969 | "color": "default" 970 | }, 971 | "plain_text": "React ", 972 | "href": null 973 | }, 974 | { 975 | "type": "text", 976 | "text": { 977 | "content": "components implement a render() method that takes input data and returns what to display. This example uses an XML-like syntax called JSX. Input data that is passed into the component can be accessed by render() via this.props.", 978 | "link": null 979 | }, 980 | "annotations": { 981 | "bold": false, 982 | "italic": false, 983 | "strikethrough": false, 984 | "underline": false, 985 | "code": false, 986 | "color": "default" 987 | }, 988 | "plain_text": "components implement a render() method that takes input data and returns what to display. This example uses an XML-like syntax called JSX. Input data that is passed into the component can be accessed by render() via this.props.", 989 | "href": null 990 | } 991 | ] 992 | } 993 | }, 994 | { 995 | "object": "block", 996 | "id": "70929631-368b-4831-9c6d-04ae3355c1a5", 997 | "created_time": "2021-05-28T21:25:00.000Z", 998 | "last_edited_time": "2021-05-29T21:36:00.000Z", 999 | "has_children": false, 1000 | "type": "paragraph", 1001 | "paragraph": { 1002 | "text": [ 1003 | { 1004 | "type": "text", 1005 | "text": { "content": "JSX ", "link": null }, 1006 | "annotations": { 1007 | "bold": false, 1008 | "italic": false, 1009 | "strikethrough": false, 1010 | "underline": false, 1011 | "code": false, 1012 | "color": "red" 1013 | }, 1014 | "plain_text": "JSX ", 1015 | "href": null 1016 | }, 1017 | { 1018 | "type": "text", 1019 | "text": { "content": "is ", "link": null }, 1020 | "annotations": { 1021 | "bold": false, 1022 | "italic": false, 1023 | "strikethrough": false, 1024 | "underline": false, 1025 | "code": false, 1026 | "color": "default" 1027 | }, 1028 | "plain_text": "is ", 1029 | "href": null 1030 | }, 1031 | { 1032 | "type": "text", 1033 | "text": { "content": "optional ", "link": null }, 1034 | "annotations": { 1035 | "bold": false, 1036 | "italic": false, 1037 | "strikethrough": true, 1038 | "underline": false, 1039 | "code": false, 1040 | "color": "default" 1041 | }, 1042 | "plain_text": "optional ", 1043 | "href": null 1044 | }, 1045 | { 1046 | "type": "text", 1047 | "text": { 1048 | "content": "and not required to use React. Try the Babel REPL to see the raw JavaScript code produced by the JSX ", 1049 | "link": null 1050 | }, 1051 | "annotations": { 1052 | "bold": false, 1053 | "italic": false, 1054 | "strikethrough": false, 1055 | "underline": false, 1056 | "code": false, 1057 | "color": "default" 1058 | }, 1059 | "plain_text": "and not required to use React. Try the Babel REPL to see the raw JavaScript code produced by the JSX ", 1060 | "href": null 1061 | }, 1062 | { 1063 | "type": "text", 1064 | "text": { "content": "compilation ", "link": null }, 1065 | "annotations": { 1066 | "bold": false, 1067 | "italic": false, 1068 | "strikethrough": false, 1069 | "underline": false, 1070 | "code": false, 1071 | "color": "orange" 1072 | }, 1073 | "plain_text": "compilation ", 1074 | "href": null 1075 | }, 1076 | { 1077 | "type": "text", 1078 | "text": { "content": "step.", "link": null }, 1079 | "annotations": { 1080 | "bold": false, 1081 | "italic": false, 1082 | "strikethrough": false, 1083 | "underline": false, 1084 | "code": false, 1085 | "color": "default" 1086 | }, 1087 | "plain_text": "step.", 1088 | "href": null 1089 | } 1090 | ] 1091 | } 1092 | } 1093 | ] 1094 | } 1095 | }, 1096 | { 1097 | "object": "block", 1098 | "id": "ddcc8dc0-126f-47dc-bf03-a8ebb8228e69", 1099 | "created_time": "2021-05-28T21:26:12.759Z", 1100 | "last_edited_time": "2021-05-28T21:26:00.000Z", 1101 | "has_children": true, 1102 | "type": "toggle", 1103 | "toggle": { 1104 | "text": [ 1105 | { 1106 | "type": "text", 1107 | "text": { "content": "A Stateful Component", "link": null }, 1108 | "annotations": { 1109 | "bold": false, 1110 | "italic": false, 1111 | "strikethrough": false, 1112 | "underline": false, 1113 | "code": false, 1114 | "color": "default" 1115 | }, 1116 | "plain_text": "A Stateful Component", 1117 | "href": null 1118 | } 1119 | ], 1120 | "children": [ 1121 | { 1122 | "object": "block", 1123 | "id": "cc616514-1159-489b-9e85-19a204008642", 1124 | "created_time": "2021-05-28T21:26:00.000Z", 1125 | "last_edited_time": "2021-05-29T21:37:00.000Z", 1126 | "has_children": false, 1127 | "type": "paragraph", 1128 | "paragraph": { 1129 | "text": [ 1130 | { 1131 | "type": "text", 1132 | "text": { "content": "I", "link": null }, 1133 | "annotations": { 1134 | "bold": false, 1135 | "italic": false, 1136 | "strikethrough": false, 1137 | "underline": false, 1138 | "code": false, 1139 | "color": "default" 1140 | }, 1141 | "plain_text": "I", 1142 | "href": null 1143 | }, 1144 | { 1145 | "type": "text", 1146 | "text": { 1147 | "content": "n addition to taking input data (accessed via this.props), a component can maintain internal state data (accessed via this.state). When a component’s state data changes, the rendered markup will be updated by re-invoking render().", 1148 | "link": null 1149 | }, 1150 | "annotations": { 1151 | "bold": true, 1152 | "italic": false, 1153 | "strikethrough": false, 1154 | "underline": false, 1155 | "code": false, 1156 | "color": "purple" 1157 | }, 1158 | "plain_text": "n addition to taking input data (accessed via this.props), a component can maintain internal state data (accessed via this.state). When a component’s state data changes, the rendered markup will be updated by re-invoking render().", 1159 | "href": null 1160 | } 1161 | ] 1162 | } 1163 | } 1164 | ] 1165 | } 1166 | }, 1167 | { 1168 | "object": "block", 1169 | "id": "d99a085c-c313-427e-8662-b744095ba2dc", 1170 | "created_time": "2021-05-28T21:26:00.000Z", 1171 | "last_edited_time": "2021-05-28T21:26:00.000Z", 1172 | "has_children": true, 1173 | "type": "toggle", 1174 | "toggle": { 1175 | "text": [ 1176 | { 1177 | "type": "text", 1178 | "text": { "content": "An Application", "link": null }, 1179 | "annotations": { 1180 | "bold": false, 1181 | "italic": false, 1182 | "strikethrough": false, 1183 | "underline": false, 1184 | "code": false, 1185 | "color": "default" 1186 | }, 1187 | "plain_text": "An Application", 1188 | "href": null 1189 | } 1190 | ], 1191 | "children": [ 1192 | { 1193 | "object": "block", 1194 | "id": "4846d63f-ff99-44d2-8c5e-e59d9cfb6587", 1195 | "created_time": "2021-05-28T21:26:00.000Z", 1196 | "last_edited_time": "2021-05-28T21:26:00.000Z", 1197 | "has_children": false, 1198 | "type": "paragraph", 1199 | "paragraph": { 1200 | "text": [ 1201 | { 1202 | "type": "text", 1203 | "text": { 1204 | "content": "Using props and state, we can put together a small Todo application. This example uses state to track the current list of items as well as the text that the user has entered. Although event handlers appear to be rendered inline, they will be collected and implemented using event delegation.", 1205 | "link": null 1206 | }, 1207 | "annotations": { 1208 | "bold": false, 1209 | "italic": false, 1210 | "strikethrough": false, 1211 | "underline": false, 1212 | "code": false, 1213 | "color": "default" 1214 | }, 1215 | "plain_text": "Using props and state, we can put together a small Todo application. This example uses state to track the current list of items as well as the text that the user has entered. Although event handlers appear to be rendered inline, they will be collected and implemented using event delegation.", 1216 | "href": null 1217 | } 1218 | ] 1219 | } 1220 | } 1221 | ] 1222 | } 1223 | }, 1224 | { 1225 | "object": "block", 1226 | "id": "acc450e4-dcc3-425d-a88c-183789179ggb", 1227 | "created_time": "2021-05-23T18:04:00.000Z", 1228 | "last_edited_time": "2021-05-23T18:04:00.000Z", 1229 | "has_children": false, 1230 | "type": "paragraph", 1231 | "paragraph": { "text": [] } 1232 | }, 1233 | { 1234 | "object": "block", 1235 | "id": "6c0a547c-97f7-4783-9f35-6c28911825a3", 1236 | "created_time": "2021-05-23T18:03:00.000Z", 1237 | "last_edited_time": "2021-05-23T18:04:00.000Z", 1238 | "has_children": false, 1239 | "type": "paragraph", 1240 | "paragraph": { 1241 | "text": [ 1242 | { 1243 | "type": "text", 1244 | "text": { "content": "Reference: ", "link": null }, 1245 | "annotations": { 1246 | "bold": false, 1247 | "italic": false, 1248 | "strikethrough": false, 1249 | "underline": false, 1250 | "code": false, 1251 | "color": "default" 1252 | }, 1253 | "plain_text": "Reference: ", 1254 | "href": null 1255 | }, 1256 | { 1257 | "type": "text", 1258 | "text": { 1259 | "content": "React.js Official Documentation", 1260 | "link": { "url": "https://reactjs.org/" } 1261 | }, 1262 | "annotations": { 1263 | "bold": false, 1264 | "italic": false, 1265 | "strikethrough": false, 1266 | "underline": false, 1267 | "code": false, 1268 | "color": "default" 1269 | }, 1270 | "plain_text": "React.js Official Documentation", 1271 | "href": "https://reactjs.org/" 1272 | } 1273 | ] 1274 | } 1275 | }, 1276 | { 1277 | "object": "block", 1278 | "id": "4f8d1443-e343-4d15-bd0b-fd25c3a32322", 1279 | "created_time": "2021-05-23T18:04:00.000Z", 1280 | "last_edited_time": "2021-05-23T18:04:00.000Z", 1281 | "has_children": false, 1282 | "type": "paragraph", 1283 | "paragraph": { 1284 | "text": [ 1285 | { 1286 | "type": "text", 1287 | "text": { 1288 | "content": "![my github pic](https://avatars.githubusercontent.com/u/38046239?v=4)#https://github.com/9gustin", 1289 | "link": null 1290 | }, 1291 | "annotations": { 1292 | "bold": false, 1293 | "italic": false, 1294 | "strikethrough": false, 1295 | "underline": false, 1296 | "code": false, 1297 | "color": "default" 1298 | }, 1299 | "plain_text": "![my github pic](https://avatars.githubusercontent.com/u/38046239?v=4)#https://github.com/9gustin", 1300 | "href": null 1301 | } 1302 | ] 1303 | } 1304 | }, 1305 | { 1306 | "object": "block", 1307 | "id": "4f8d1kkkk443-e343-4d15-bd0b-fd25c3a32322", 1308 | "created_time": "2021-05-23T18:04:00.000Z", 1309 | "last_edited_time": "2021-05-23T18:04:00.000Z", 1310 | "has_children": false, 1311 | "type": "paragraph", 1312 | "paragraph": { 1313 | "text": [ 1314 | { 1315 | "type": "text", 1316 | "text": { 1317 | "content": "-[Local](/loremVideo.mp4)", 1318 | "link": null 1319 | }, 1320 | "annotations": { 1321 | "bold": false, 1322 | "italic": false, 1323 | "strikethrough": false, 1324 | "underline": false, 1325 | "code": false, 1326 | "color": "default" 1327 | }, 1328 | "plain_text": "-[Local](/loremVideo.mp4)", 1329 | "href": null 1330 | } 1331 | ] 1332 | } 1333 | }, 1334 | { 1335 | "object": "block", 1336 | "id": "4f8d1443-e343-jjj4d15-bd0b-fd25c3a32322", 1337 | "created_time": "2021-05-23T18:04:00.000Z", 1338 | "last_edited_time": "2021-05-23T18:04:00.000Z", 1339 | "has_children": false, 1340 | "type": "paragraph", 1341 | "paragraph": { 1342 | "text": [ 1343 | { 1344 | "type": "text", 1345 | "text": { 1346 | "content": "-[Youtube](https://youtu.be/aA7si7AmPkY)#youtube", 1347 | "link": null 1348 | }, 1349 | "annotations": { 1350 | "bold": false, 1351 | "italic": false, 1352 | "strikethrough": false, 1353 | "underline": false, 1354 | "code": false, 1355 | "color": "default" 1356 | }, 1357 | "plain_text": "-[Youtube](https://youtu.be/aA7si7AmPkY)#youtube", 1358 | "href": null 1359 | } 1360 | ] 1361 | } 1362 | }, 1363 | { 1364 | "object": "block", 1365 | "id": "4f8d1443-e34fff3-4d15-bd0b-fd25c3a32322", 1366 | "created_time": "2021-05-23T18:04:00.000Z", 1367 | "last_edited_time": "2021-05-23T18:04:00.000Z", 1368 | "has_children": false, 1369 | "type": "paragraph", 1370 | "paragraph": { 1371 | "text": [ 1372 | { 1373 | "type": "text", 1374 | "text": { 1375 | "content": "-[GoogleDrive](https://drive.google.com/file/d/1BmIxtck_9FuMfZOKfJDQK_WvIl8cDV11/view?usp=sharing)#googleDrive", 1376 | "link": null 1377 | }, 1378 | "annotations": { 1379 | "bold": false, 1380 | "italic": false, 1381 | "strikethrough": false, 1382 | "underline": false, 1383 | "code": false, 1384 | "color": "default" 1385 | }, 1386 | "plain_text": "-[GoogleDrive](https://drive.google.com/file/d/1BmIxtck_9FuMfZOKfJDQK_WvIl8cDV11/view?usp=sharing)#googleDrive", 1387 | "href": null 1388 | } 1389 | ] 1390 | } 1391 | }, 1392 | { 1393 | "object": "block", 1394 | "id": "4f8d144342343-e343-4d15-bd0b-fd25c3a323e1", 1395 | "created_time": "2021-05-23T18:04:00.000Z", 1396 | "last_edited_time": "2021-05-23T18:04:00.000Z", 1397 | "has_children": false, 1398 | "type": "paragraph", 1399 | "paragraph": { 1400 | "text": [ 1401 | { 1402 | "type": "text", 1403 | "text": { 1404 | "content": "[my github profile](https://github.com/9gustin)", 1405 | "link": null 1406 | }, 1407 | "annotations": { 1408 | "bold": false, 1409 | "italic": false, 1410 | "strikethrough": false, 1411 | "underline": false, 1412 | "code": false, 1413 | "color": "default" 1414 | }, 1415 | "plain_text": "[my github profile](https://github.com/9gustin)", 1416 | "href": null 1417 | } 1418 | ] 1419 | } 1420 | } 1421 | ], 1422 | "next_cursor": null, 1423 | "has_more": false 1424 | } 1425 | -------------------------------------------------------------------------------- /dev-example/data/codeBlocks.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "object": "block", 4 | "id": "5240d96c-3253-449d-8c8b-8bf9a18b79a4", 5 | "created_time": "2022-01-20T18:44:00.000Z", 6 | "last_edited_time": "2022-01-20T18:45:00.000Z", 7 | "has_children": false, 8 | "archived": false, 9 | "type": "paragraph", 10 | "paragraph": { 11 | "text": [ 12 | { 13 | "type": "text", 14 | "text": { "content": "CSS Code", "link": null }, 15 | "annotations": { 16 | "bold": false, 17 | "italic": false, 18 | "strikethrough": false, 19 | "underline": false, 20 | "code": false, 21 | "color": "default" 22 | }, 23 | "plain_text": "CSS Code", 24 | "href": null 25 | } 26 | ] 27 | } 28 | }, 29 | { 30 | "object": "block", 31 | "id": "7737e38a-3905-481e-a192-a96a4cd9fd1b", 32 | "created_time": "2022-01-20T18:43:00.000Z", 33 | "last_edited_time": "2022-01-20T18:44:00.000Z", 34 | "has_children": false, 35 | "archived": false, 36 | "type": "code", 37 | "code": { 38 | "caption": [], 39 | "text": [ 40 | { 41 | "type": "text", 42 | "text": { 43 | "content": "@import url('https://fonts.googleapis.com/css2?family=Montserrat&display=swap');\n/* @import url('https://fonts.googleapis.com/css2?family=Karla&display=swap'); */\n\n:root {\n --bg-color: #0D1117;\n --text-color: #F2F2F2;\n --bg-secondary-color: #BFBFBF;\n --theme-color: #1DAF54;\n\n --size: 1rem;\n --size-sm: .9rem;\n --size-xs: .8rem;\n\n --family:'Montserrat', sans-serif;\n /* --family:'Karla', sans-serif; */\n}\n\nbody {\n background-color: var(--bg-color);\n color: var(--text-color);\n font-family: var(--family);\n overflow: hidden;\n}\n\nbutton {\n background-color: var(--theme-color);\n border: none;\n border-radius: 10px;\n color: var(--text-color);\n min-height: 40px;\n max-width: 400px;\n width: 100%;\n}", 44 | "link": null 45 | }, 46 | "annotations": { 47 | "bold": false, 48 | "italic": false, 49 | "strikethrough": false, 50 | "underline": false, 51 | "code": false, 52 | "color": "default" 53 | }, 54 | "plain_text": "@import url('https://fonts.googleapis.com/css2?family=Montserrat&display=swap');\n/* @import url('https://fonts.googleapis.com/css2?family=Karla&display=swap'); */\n\n:root {\n --bg-color: #0D1117;\n --text-color: #F2F2F2;\n --bg-secondary-color: #BFBFBF;\n --theme-color: #1DAF54;\n\n --size: 1rem;\n --size-sm: .9rem;\n --size-xs: .8rem;\n\n --family:'Montserrat', sans-serif;\n /* --family:'Karla', sans-serif; */\n}\n\nbody {\n background-color: var(--bg-color);\n color: var(--text-color);\n font-family: var(--family);\n overflow: hidden;\n}\n\nbutton {\n background-color: var(--theme-color);\n border: none;\n border-radius: 10px;\n color: var(--text-color);\n min-height: 40px;\n max-width: 400px;\n width: 100%;\n}", 55 | "href": null 56 | } 57 | ], 58 | "language": "css" 59 | } 60 | }, 61 | { 62 | "object": "block", 63 | "id": "f4e9a408-a0d4-470e-b8c0-b55420422be7", 64 | "created_time": "2022-01-20T18:44:00.000Z", 65 | "last_edited_time": "2022-01-20T18:45:00.000Z", 66 | "has_children": false, 67 | "archived": false, 68 | "type": "paragraph", 69 | "paragraph": { 70 | "text": [ 71 | { 72 | "type": "text", 73 | "text": { "content": "JS Code", "link": null }, 74 | "annotations": { 75 | "bold": false, 76 | "italic": false, 77 | "strikethrough": false, 78 | "underline": false, 79 | "code": false, 80 | "color": "default" 81 | }, 82 | "plain_text": "JS Code", 83 | "href": null 84 | } 85 | ] 86 | } 87 | }, 88 | { 89 | "object": "block", 90 | "id": "4294a3d8-bc13-4d38-a359-64ae87e71523", 91 | "created_time": "2022-01-20T18:45:00.000Z", 92 | "last_edited_time": "2022-01-20T18:45:00.000Z", 93 | "has_children": false, 94 | "archived": false, 95 | "type": "code", 96 | "code": { 97 | "caption": [], 98 | "text": [ 99 | { 100 | "type": "text", 101 | "text": { 102 | "content": "import React from \"react\";\n\nconst Button = ({\n children,\n handleClick,\n variant = \"default\",\n type = \"button\",\n}) => (\n \n);\n\nexport default Button;", 103 | "link": null 104 | }, 105 | "annotations": { 106 | "bold": false, 107 | "italic": false, 108 | "strikethrough": false, 109 | "underline": false, 110 | "code": false, 111 | "color": "default" 112 | }, 113 | "plain_text": "import React from \"react\";\n\nconst Button = ({\n children,\n handleClick,\n variant = \"default\",\n type = \"button\",\n}) => (\n \n);\n\nexport default Button;", 114 | "href": null 115 | } 116 | ], 117 | "language": "javascript" 118 | } 119 | }, 120 | { 121 | "object": "block", 122 | "id": "595a2f03-7d4b-4b7f-b794-dde8fd692c45", 123 | "created_time": "2022-01-20T18:44:00.000Z", 124 | "last_edited_time": "2022-01-20T18:46:00.000Z", 125 | "has_children": false, 126 | "archived": false, 127 | "type": "paragraph", 128 | "paragraph": { 129 | "text": [ 130 | { 131 | "type": "text", 132 | "text": { "content": "TS code ", "link": null }, 133 | "annotations": { 134 | "bold": false, 135 | "italic": false, 136 | "strikethrough": false, 137 | "underline": false, 138 | "code": false, 139 | "color": "default" 140 | }, 141 | "plain_text": "TS code ", 142 | "href": null 143 | } 144 | ] 145 | } 146 | }, 147 | { 148 | "object": "block", 149 | "id": "4b48005d-005d-47a8-8bff-a96bc1f1d96c", 150 | "created_time": "2022-01-20T18:45:00.000Z", 151 | "last_edited_time": "2022-01-20T18:46:00.000Z", 152 | "has_children": false, 153 | "archived": false, 154 | "type": "code", 155 | "code": { 156 | "caption": [], 157 | "text": [ 158 | { 159 | "type": "text", 160 | "text": { 161 | "content": "import React, { PropsWithChildren } from \"react\";\n\ntype Props = PropsWithChildren<{\n type?: React.ButtonHTMLAttributes[\"type\"];\n size?: \"small\" | \"large\";\n handleClick?: (e?: React.MouseEvent) => void;\n variant?: \"default\" | \"secondary\" | \"error\";\n}>;\n\nconst Button = ({\n children,\n handleClick,\n variant = \"default\",\n type = \"button\",\n}: Props) => (\n \n);\n\nexport default Button;", 162 | "link": null 163 | }, 164 | "annotations": { 165 | "bold": false, 166 | "italic": false, 167 | "strikethrough": false, 168 | "underline": false, 169 | "code": false, 170 | "color": "default" 171 | }, 172 | "plain_text": "import React, { PropsWithChildren } from \"react\";\n\ntype Props = PropsWithChildren<{\n type?: React.ButtonHTMLAttributes[\"type\"];\n size?: \"small\" | \"large\";\n handleClick?: (e?: React.MouseEvent) => void;\n variant?: \"default\" | \"secondary\" | \"error\";\n}>;\n\nconst Button = ({\n children,\n handleClick,\n variant = \"default\",\n type = \"button\",\n}: Props) => (\n \n);\n\nexport default Button;", 173 | "href": null 174 | } 175 | ], 176 | "language": "typescript" 177 | } 178 | }, 179 | { 180 | "object": "block", 181 | "id": "97c562a3-f413-440c-96a3-56d850d1d86a", 182 | "created_time": "2022-01-20T18:46:00.000Z", 183 | "last_edited_time": "2022-01-20T18:46:00.000Z", 184 | "has_children": false, 185 | "archived": false, 186 | "type": "paragraph", 187 | "paragraph": { "text": [] } 188 | } 189 | ] 190 | -------------------------------------------------------------------------------- /dev-example/data/mockVideos.json: -------------------------------------------------------------------------------- 1 | { 2 | "object": "list", 3 | "results": [ 4 | { 5 | "object": "block", 6 | "id": "68139e5e-9d1d-44da-b07b-2cc5748a3c86", 7 | "created_time": "2021-08-29T00:42:00.000Z", 8 | "last_edited_time": "2021-08-29T00:42:00.000Z", 9 | "has_children": false, 10 | "type": "paragraph", 11 | "paragraph": { 12 | "text": [ 13 | { 14 | "type": "text", 15 | "text": { 16 | "content": "El objetivo es probar todas las posibilidades con video.", 17 | "link": null 18 | }, 19 | "annotations": { 20 | "bold": false, 21 | "italic": false, 22 | "strikethrough": false, 23 | "underline": false, 24 | "code": false, 25 | "color": "default" 26 | }, 27 | "plain_text": "El objetivo es probar todas las posibilidades con video.", 28 | "href": null 29 | } 30 | ] 31 | } 32 | }, 33 | { 34 | "object": "block", 35 | "id": "92cf141d-b612-4e21-9c05-d51d4c2aea33", 36 | "created_time": "2021-08-29T00:42:00.000Z", 37 | "last_edited_time": "2021-08-29T00:42:00.000Z", 38 | "has_children": false, 39 | "type": "paragraph", 40 | "paragraph": { 41 | "text": [ 42 | { 43 | "type": "text", 44 | "text": { 45 | "content": "Alternativas contempladas:", 46 | "link": null 47 | }, 48 | "annotations": { 49 | "bold": false, 50 | "italic": false, 51 | "strikethrough": false, 52 | "underline": false, 53 | "code": false, 54 | "color": "default" 55 | }, 56 | "plain_text": "Alternativas contempladas:", 57 | "href": null 58 | } 59 | ] 60 | } 61 | }, 62 | { 63 | "object": "block", 64 | "id": "06ff73af-0247-4a92-a4cd-815a258c4259", 65 | "created_time": "2021-08-29T00:42:00.000Z", 66 | "last_edited_time": "2021-08-29T00:43:00.000Z", 67 | "has_children": false, 68 | "type": "bulleted_list_item", 69 | "bulleted_list_item": { 70 | "text": [ 71 | { 72 | "type": "text", 73 | "text": { 74 | "content": "Custom component/ url interna", 75 | "link": null 76 | }, 77 | "annotations": { 78 | "bold": false, 79 | "italic": false, 80 | "strikethrough": false, 81 | "underline": false, 82 | "code": false, 83 | "color": "default" 84 | }, 85 | "plain_text": "Custom component/ url interna", 86 | "href": null 87 | } 88 | ] 89 | } 90 | }, 91 | { 92 | "object": "block", 93 | "id": "b35d9fc2-8ce9-47fb-88a5-6c1daed3e685", 94 | "created_time": "2021-08-29T00:43:00.000Z", 95 | "last_edited_time": "2021-08-29T00:43:00.000Z", 96 | "has_children": false, 97 | "type": "bulleted_list_item", 98 | "bulleted_list_item": { 99 | "text": [ 100 | { 101 | "type": "text", 102 | "text": { 103 | "content": "Custom component/ youtube", 104 | "link": null 105 | }, 106 | "annotations": { 107 | "bold": false, 108 | "italic": false, 109 | "strikethrough": false, 110 | "underline": false, 111 | "code": false, 112 | "color": "default" 113 | }, 114 | "plain_text": "Custom component/ youtube", 115 | "href": null 116 | } 117 | ] 118 | } 119 | }, 120 | { 121 | "object": "block", 122 | "id": "7ebaee8c-8fcd-42cc-8b36-b11f19412098", 123 | "created_time": "2021-08-29T00:43:00.000Z", 124 | "last_edited_time": "2021-08-29T00:43:00.000Z", 125 | "has_children": false, 126 | "type": "bulleted_list_item", 127 | "bulleted_list_item": { 128 | "text": [ 129 | { 130 | "type": "text", 131 | "text": { 132 | "content": "Custom component/ google drive", 133 | "link": null 134 | }, 135 | "annotations": { 136 | "bold": false, 137 | "italic": false, 138 | "strikethrough": false, 139 | "underline": false, 140 | "code": false, 141 | "color": "default" 142 | }, 143 | "plain_text": "Custom component/ google drive", 144 | "href": null 145 | } 146 | ] 147 | } 148 | }, 149 | { 150 | "object": "block", 151 | "id": "2011fbca-5192-441b-8ad0-18d24465b4ee", 152 | "created_time": "2021-08-29T00:43:00.000Z", 153 | "last_edited_time": "2021-08-29T00:43:00.000Z", 154 | "has_children": false, 155 | "type": "bulleted_list_item", 156 | "bulleted_list_item": { 157 | "text": [ 158 | { 159 | "type": "text", 160 | "text": { 161 | "content": "Nativo/ link interno", 162 | "link": null 163 | }, 164 | "annotations": { 165 | "bold": false, 166 | "italic": false, 167 | "strikethrough": false, 168 | "underline": false, 169 | "code": false, 170 | "color": "default" 171 | }, 172 | "plain_text": "Nativo/ link interno", 173 | "href": null 174 | } 175 | ] 176 | } 177 | }, 178 | { 179 | "object": "block", 180 | "id": "c6a20f66-5f0c-4c75-95fb-f111f41f9534", 181 | "created_time": "2021-08-29T00:43:00.000Z", 182 | "last_edited_time": "2021-08-29T00:43:00.000Z", 183 | "has_children": false, 184 | "type": "bulleted_list_item", 185 | "bulleted_list_item": { 186 | "text": [ 187 | { 188 | "type": "text", 189 | "text": { 190 | "content": "Nativo/ link externo", 191 | "link": null 192 | }, 193 | "annotations": { 194 | "bold": false, 195 | "italic": false, 196 | "strikethrough": false, 197 | "underline": false, 198 | "code": false, 199 | "color": "default" 200 | }, 201 | "plain_text": "Nativo/ link externo", 202 | "href": null 203 | } 204 | ] 205 | } 206 | }, 207 | { 208 | "object": "block", 209 | "id": "0d026b82-572b-4365-bf11-cf1dedd1bd13", 210 | "created_time": "2021-08-29T00:43:00.000Z", 211 | "last_edited_time": "2021-08-29T00:43:00.000Z", 212 | "has_children": false, 213 | "type": "heading_1", 214 | "heading_1": { 215 | "text": [ 216 | { 217 | "type": "text", 218 | "text": { 219 | "content": "Custom component/ url interna", 220 | "link": null 221 | }, 222 | "annotations": { 223 | "bold": false, 224 | "italic": false, 225 | "strikethrough": false, 226 | "underline": false, 227 | "code": false, 228 | "color": "default" 229 | }, 230 | "plain_text": "Custom component/ url interna", 231 | "href": null 232 | } 233 | ] 234 | } 235 | }, 236 | { 237 | "object": "block", 238 | "id": "b8f1fed6-343a-4012-a7c9-141bf623b7e4", 239 | "created_time": "2021-08-29T00:44:00.000Z", 240 | "last_edited_time": "2021-08-29T00:45:00.000Z", 241 | "has_children": false, 242 | "type": "paragraph", 243 | "paragraph": { 244 | "text": [ 245 | { 246 | "type": "text", 247 | "text": { 248 | "content": "-[Local](/loremVideo.mp4)", 249 | "link": null 250 | }, 251 | "annotations": { 252 | "bold": false, 253 | "italic": false, 254 | "strikethrough": false, 255 | "underline": false, 256 | "code": false, 257 | "color": "default" 258 | }, 259 | "plain_text": "-[Local](/loremVideo.mp4)", 260 | "href": null 261 | } 262 | ] 263 | } 264 | }, 265 | { 266 | "object": "block", 267 | "id": "4d3da782-e312-4934-ba7c-906c4f6a1d8f", 268 | "created_time": "2021-08-29T00:43:00.000Z", 269 | "last_edited_time": "2021-08-29T00:44:00.000Z", 270 | "has_children": false, 271 | "type": "heading_1", 272 | "heading_1": { 273 | "text": [ 274 | { 275 | "type": "text", 276 | "text": { 277 | "content": "Custom component/ youtube", 278 | "link": null 279 | }, 280 | "annotations": { 281 | "bold": false, 282 | "italic": false, 283 | "strikethrough": false, 284 | "underline": false, 285 | "code": false, 286 | "color": "default" 287 | }, 288 | "plain_text": "Custom component/ youtube", 289 | "href": null 290 | } 291 | ] 292 | } 293 | }, 294 | { 295 | "object": "block", 296 | "id": "86cb9fcd-7a03-4e50-a771-15189c41d9f9", 297 | "created_time": "2021-08-29T00:45:00.000Z", 298 | "last_edited_time": "2021-08-29T00:45:00.000Z", 299 | "has_children": false, 300 | "type": "paragraph", 301 | "paragraph": { 302 | "text": [ 303 | { 304 | "type": "text", 305 | "text": { 306 | "content": "-[Youtube](https://youtu.be/aA7si7AmPkY)#youtube", 307 | "link": null 308 | }, 309 | "annotations": { 310 | "bold": false, 311 | "italic": false, 312 | "strikethrough": false, 313 | "underline": false, 314 | "code": false, 315 | "color": "default" 316 | }, 317 | "plain_text": "-[Youtube](https://youtu.be/aA7si7AmPkY)#youtube", 318 | "href": null 319 | } 320 | ] 321 | } 322 | }, 323 | { 324 | "object": "block", 325 | "id": "e0c33fe4-3b62-4660-9ad9-6e46877e54f9", 326 | "created_time": "2021-08-29T00:43:00.000Z", 327 | "last_edited_time": "2021-08-29T00:44:00.000Z", 328 | "has_children": false, 329 | "type": "heading_1", 330 | "heading_1": { 331 | "text": [ 332 | { 333 | "type": "text", 334 | "text": { 335 | "content": "Custom component/ google drive", 336 | "link": null 337 | }, 338 | "annotations": { 339 | "bold": false, 340 | "italic": false, 341 | "strikethrough": false, 342 | "underline": false, 343 | "code": false, 344 | "color": "default" 345 | }, 346 | "plain_text": "Custom component/ google drive", 347 | "href": null 348 | } 349 | ] 350 | } 351 | }, 352 | { 353 | "object": "block", 354 | "id": "7affd4b1-0bb4-4628-b1b7-a27801e3e664", 355 | "created_time": "2021-08-29T00:46:00.000Z", 356 | "last_edited_time": "2021-08-29T00:47:00.000Z", 357 | "has_children": false, 358 | "type": "paragraph", 359 | "paragraph": { 360 | "text": [ 361 | { 362 | "type": "text", 363 | "text": { 364 | "content": "-[GoogleDrive](https://drive.google.com/file/d/1ywJdRoTv25tzvsazX3HpglP_3fNDLO2i/view?usp=sharing)#googleDrive", 365 | "link": null 366 | }, 367 | "annotations": { 368 | "bold": false, 369 | "italic": false, 370 | "strikethrough": false, 371 | "underline": false, 372 | "code": false, 373 | "color": "default" 374 | }, 375 | "plain_text": "-[GoogleDrive](https://drive.google.com/file/d/1ywJdRoTv25tzvsazX3HpglP_3fNDLO2i/view?usp=sharing)#googleDrive", 376 | "href": null 377 | } 378 | ] 379 | } 380 | }, 381 | { 382 | "object": "block", 383 | "id": "0c06ac16-1b0e-4776-a2f0-811b43202387", 384 | "created_time": "2021-08-29T00:43:00.000Z", 385 | "last_edited_time": "2021-08-29T00:44:00.000Z", 386 | "has_children": false, 387 | "type": "heading_1", 388 | "heading_1": { 389 | "text": [ 390 | { 391 | "type": "text", 392 | "text": { 393 | "content": "Nativo/ link interno", 394 | "link": null 395 | }, 396 | "annotations": { 397 | "bold": false, 398 | "italic": false, 399 | "strikethrough": false, 400 | "underline": false, 401 | "code": false, 402 | "color": "default" 403 | }, 404 | "plain_text": "Nativo/ link interno", 405 | "href": null 406 | } 407 | ] 408 | } 409 | }, 410 | { 411 | "object": "block", 412 | "id": "985a855a-1515-40e9-914f-91440705e247", 413 | "created_time": "2021-08-29T00:47:00.000Z", 414 | "last_edited_time": "2021-08-29T00:47:00.000Z", 415 | "has_children": false, 416 | "type": "video", 417 | "video": { 418 | "caption": [], 419 | "type": "file", 420 | "file": { 421 | "url": "https://s3.us-west-2.amazonaws.com/secure.notion-static.com/b97901f7-7dcf-4d05-b623-523379c4692c/samplevideo.mp4?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAT73L2G45O3KS52Y5%2F20210829%2Fus-west-2%2Fs3%2Faws4_request&X-Amz-Date=20210829T004817Z&X-Amz-Expires=3600&X-Amz-Signature=6e0c7cab0efec66e60ed58df75f92e0955e2865cb1156e8f314657ea2dde6f01&X-Amz-SignedHeaders=host", 422 | "expiry_time": "2021-08-29T01:48:17.296Z" 423 | } 424 | } 425 | }, 426 | { 427 | "object": "block", 428 | "id": "c5ad3c67-1b5e-490d-8ef7-d684eedf9531", 429 | "created_time": "2021-08-29T00:43:00.000Z", 430 | "last_edited_time": "2021-08-29T00:47:00.000Z", 431 | "has_children": false, 432 | "type": "heading_1", 433 | "heading_1": { 434 | "text": [ 435 | { 436 | "type": "text", 437 | "text": { 438 | "content": "Nativo/ link externo (youtube)", 439 | "link": null 440 | }, 441 | "annotations": { 442 | "bold": false, 443 | "italic": false, 444 | "strikethrough": false, 445 | "underline": false, 446 | "code": false, 447 | "color": "default" 448 | }, 449 | "plain_text": "Nativo/ link externo (youtube)", 450 | "href": null 451 | } 452 | ] 453 | } 454 | }, 455 | { 456 | "object": "block", 457 | "id": "574f8113-bce4-448f-98a1-22e499e71af7", 458 | "created_time": "2021-08-29T00:47:00.000Z", 459 | "last_edited_time": "2021-08-29T00:47:00.000Z", 460 | "has_children": false, 461 | "type": "video", 462 | "video": { 463 | "caption": [], 464 | "type": "external", 465 | "external": { 466 | "url": "https://youtu.be/aA7si7AmPkY" 467 | } 468 | } 469 | }, 470 | { 471 | "object": "block", 472 | "id": "5d95c6fd-0f03-41ab-8188-81d9479770aa", 473 | "created_time": "2021-08-29T00:47:00.000Z", 474 | "last_edited_time": "2021-08-29T00:47:00.000Z", 475 | "has_children": false, 476 | "type": "paragraph", 477 | "paragraph": { 478 | "text": [] 479 | } 480 | } 481 | ], 482 | "next_cursor": null, 483 | "has_more": false 484 | } 485 | -------------------------------------------------------------------------------- /dev-example/data/title.json: -------------------------------------------------------------------------------- 1 | { 2 | "properties": { 3 | "Name": { 4 | "id": "title", 5 | "type": "title", 6 | "title": [ 7 | { 8 | "type": "text", 9 | "text": { "content": "Mockup Page! ;)", "link": null }, 10 | "annotations": { 11 | "bold": false, 12 | "italic": false, 13 | "strikethrough": false, 14 | "underline": false, 15 | "code": false, 16 | "color": "default" 17 | }, 18 | "plain_text": "Mockup Page! ;)", 19 | "href": null 20 | } 21 | ] 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /dev-example/lib/notion.js: -------------------------------------------------------------------------------- 1 | import { Client } from '@notionhq/client' 2 | 3 | const notion = new Client({ 4 | auth: process.env.NOTION_TOKEN 5 | }) 6 | 7 | export const getDatabase = async (databaseId) => { 8 | const response = await notion.databases.query({ 9 | database_id: databaseId 10 | }) 11 | return response.results 12 | } 13 | 14 | export const getPage = async (pageId) => { 15 | const response = await notion.pages.retrieve({ page_id: pageId }) 16 | return response 17 | } 18 | 19 | export const getBlocks = async (blockId) => { 20 | const response = await notion.blocks.children.list({ 21 | block_id: blockId 22 | }) 23 | return response.results 24 | } 25 | -------------------------------------------------------------------------------- /dev-example/next.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | images: { 3 | domains: ['images.unsplash.com'] 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /dev-example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nextjs", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start" 9 | }, 10 | "dependencies": { 11 | "@9gustin/react-notion-render": "file:..", 12 | "@notionhq/client": "^1.0.4", 13 | "next": "^14.2.8", 14 | "prism-react-renderer": "^1.3.3", 15 | "react": "file:../node_modules/react", 16 | "react-dom": "file:../node_modules/react-dom" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /dev-example/pages/[id].js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Head from 'next/head' 3 | import NextImg from 'next/image' 4 | import { getDatabase, getPage, getBlocks } from '../lib/notion' 5 | import Link from 'next/link' 6 | import { databaseId } from './blog.js' 7 | 8 | import { Render, withContentValidation } from '@9gustin/react-notion-render' 9 | 10 | import Header from '../components/Header' 11 | import CustomCode from '../components/CustomCode' 12 | import MyTableOfContents from '../components/TableOfContents' 13 | 14 | import styles from './index.module.css' 15 | 16 | const myMapper = { 17 | heading_1: withContentValidation(({ children }) => ( 18 |

test:{children}

19 | )), 20 | image: withContentValidation(({ media }) => ( 21 | 22 | )), 23 | code: withContentValidation(CustomCode) 24 | } 25 | 26 | export default function Post({ page, blocks }) { 27 | if (!page || !blocks) { 28 | return
29 | } 30 | 31 | return ( 32 | <> 33 | 34 | {page.properties.Name.title[0].plain_text} 35 | 36 | 37 | 38 |
39 |
40 | 41 |
42 | 43 |
44 | 51 | 52 | ← Go home 53 | 54 |
55 |
56 |
57 | 58 | ) 59 | } 60 | 61 | export const getStaticPaths = async () => { 62 | const database = await getDatabase(databaseId) 63 | return { 64 | paths: database.map((page) => ({ params: { id: page.id } })), 65 | fallback: true 66 | } 67 | } 68 | 69 | export const getStaticProps = async (context) => { 70 | const { id } = context.params 71 | const page = await getPage(id) 72 | const blocks = await getBlocks(id) 73 | 74 | // Retrieve block children for nested blocks (one level deep), for example toggle blocks 75 | // https://developers.notion.com/docs/working-with-page-content#reading-nested-blocks 76 | const childBlocks = await Promise.all( 77 | blocks 78 | .filter((block) => block.has_children) 79 | .map(async (block) => { 80 | return { 81 | id: block.id, 82 | children: await getBlocks(block.id) 83 | } 84 | }) 85 | ) 86 | const blocksWithChildren = blocks.map((block) => { 87 | // Add child blocks if the block should contain children but none exists 88 | if (block.has_children && !block[block.type].children) { 89 | block[block.type].children = childBlocks.find( 90 | (x) => x.id === block.id 91 | )?.children 92 | } 93 | return block 94 | }) 95 | 96 | return { 97 | props: { 98 | page, 99 | blocks: blocksWithChildren 100 | }, 101 | revalidate: 1 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /dev-example/pages/_app.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import '@9gustin/react-notion-render/dist/index.css' 3 | import '../styles/globals.css' 4 | 5 | function MyApp({ Component, pageProps }) { 6 | return 7 | } 8 | 9 | export default MyApp 10 | -------------------------------------------------------------------------------- /dev-example/pages/blog.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Head from 'next/head' 3 | import Link from 'next/link' 4 | import { getDatabase } from '../lib/notion' 5 | import { Render } from '@9gustin/react-notion-render' 6 | import styles from './index.module.css' 7 | import Header from '../components/Header' 8 | 9 | export const databaseId = process.env.NOTION_DATABASE_ID 10 | 11 | export default function Home({ posts }) { 12 | return ( 13 |
14 | 15 | Notion Next.js blog 16 | 17 | 18 | 19 |
20 |
21 |

22 | This is an example of a Next.js blog with data fetched with Notions 23 | API. The data comes from{' '} 24 | this table. Get 25 | the source code on{' '} 26 | 27 | Github 28 | 29 | . 30 |

31 |
32 | 33 |

All Posts

34 |
    35 | {posts.map((post) => { 36 | const date = new Date(post.last_edited_time).toLocaleString( 37 | 'en-US', 38 | { 39 | month: 'short', 40 | day: '2-digit', 41 | year: 'numeric' 42 | } 43 | ) 44 | return ( 45 |
  1. 46 |

    47 | 48 | 49 | 50 | {/* {renderTitle(post.properties.Name)} */} 51 | 52 |

    53 | 54 |

    {date}

    55 | 56 | Read post → 57 | 58 |
  2. 59 | ) 60 | })} 61 |
62 |
63 |
64 | ) 65 | } 66 | 67 | export const getStaticProps = async () => { 68 | const database = await getDatabase(databaseId) 69 | 70 | return { 71 | props: { 72 | posts: database 73 | }, 74 | revalidate: 1 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /dev-example/pages/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Link from 'next/link' 3 | import { Render } from '@9gustin/react-notion-render' 4 | 5 | import notionResponse from '../data/mockVideos.json' 6 | import title from '../data/title.json' 7 | 8 | export default function mockedPage() { 9 | return ( 10 |
11 |

12 | This page is mockuped with data/blocks.json, also you can view{' '} 13 | /blog 14 |

15 | 16 |
17 | 18 |
19 |
20 | ) 21 | } 22 | -------------------------------------------------------------------------------- /dev-example/pages/index.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | padding: 20px; 3 | max-width: 700px; 4 | margin: 0 auto; 5 | } 6 | 7 | .plus { 8 | font-size: 20px; 9 | margin: 0 20px; 10 | } 11 | 12 | .heading { 13 | margin-bottom: 20px; 14 | padding-bottom: 20px; 15 | border-bottom: 1px solid var(--text-color); 16 | text-transform: uppercase; 17 | font-size: 15px; 18 | opacity: 0.6; 19 | letter-spacing: 0.5px; 20 | } 21 | 22 | .posts { 23 | list-style: none; 24 | margin: 0; 25 | padding: 0; 26 | } 27 | 28 | .post { 29 | margin-bottom: 50px; 30 | } 31 | 32 | .postTitle { 33 | margin-bottom: 10px; 34 | font-size: 1.4rem; 35 | } 36 | 37 | .postTitle a { 38 | color: inherit; 39 | } 40 | 41 | .postDescription { 42 | margin-top: 0; 43 | margin-bottom: 12px; 44 | opacity: 0.65; 45 | } 46 | -------------------------------------------------------------------------------- /dev-example/pages/test/Text.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Render } from '@9gustin/react-notion-render' 3 | 4 | import blocks from './Text.json' 5 | 6 | export default function Text() { 7 | return ( 8 |
9 | 10 |
11 | ) 12 | } 13 | -------------------------------------------------------------------------------- /dev-example/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/9gustin/react-notion-render/91da3edaa4e8a80c04593269175892e6c124aa5b/dev-example/public/favicon.ico -------------------------------------------------------------------------------- /dev-example/public/loremVideo.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/9gustin/react-notion-render/91da3edaa4e8a80c04593269175892e6c124aa5b/dev-example/public/loremVideo.mp4 -------------------------------------------------------------------------------- /dev-example/public/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /dev-example/styles/globals.css: -------------------------------------------------------------------------------- 1 | body { 2 | --text-color: #222; 3 | --bkg-color: #EEE; 4 | --anchor-color: #0033cc; 5 | } 6 | body.dark { 7 | --text-color: #D9D9D9; 8 | --bkg-color: #31313c; 9 | --anchor-color: #809fff; 10 | } 11 | 12 | html, 13 | body { 14 | padding: 0; 15 | margin: 0; 16 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, 17 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; 18 | color: var(--text-color); 19 | background-color: var(--bkg-color); 20 | } 21 | 22 | svg path{ 23 | fill: var(--text-color); 24 | } 25 | 26 | * { 27 | box-sizing: border-box; 28 | } 29 | 30 | h1 { 31 | font-weight: 800; 32 | } 33 | -------------------------------------------------------------------------------- /dev-example/utils/isNavigatorDarkTheme.js: -------------------------------------------------------------------------------- 1 | export const NAVIGATOR_DARK_COMPARISION = '(prefers-color-scheme: dark)' 2 | 3 | const isNavigatorDarkTheme = () => 4 | window.matchMedia(NAVIGATOR_DARK_COMPARISION).matches 5 | 6 | export default isNavigatorDarkTheme 7 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */ 2 | module.exports = { 3 | preset: 'ts-jest', 4 | testEnvironment: 'node', 5 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@9gustin/react-notion-render", 3 | "version": "3.11.4", 4 | "description": "A library to render notion content", 5 | "author": "9gustin", 6 | "keywords": [ 7 | "Notion", 8 | "Notion API", 9 | "React Notion", 10 | "Notion pages" 11 | ], 12 | "license": "MIT", 13 | "repository": "9gustin/react-notion-render", 14 | "homepage": "https://github.com/9gustin/react-notion-render/", 15 | "bugs": "https://github.com/9gustin/react-notion-render/issues", 16 | "main": "dist/index.js", 17 | "types": "dist/index.d.ts", 18 | "module": "dist/index.modern.mjs", 19 | "source": "src/index.tsx", 20 | "engines": { 21 | "node": ">=10" 22 | }, 23 | "scripts": { 24 | "build": "microbundle --jsx React.createElement --format modern,cjs --css-modules true", 25 | "start": "microbundle watch --jsx React.createElement --format modern,cjs --css-modules true", 26 | "dev": "npm-run-all --parallel start dev-example", 27 | "dev-example": "cd dev-example && npm run dev", 28 | "prepare": "run-s test:build", 29 | "test": "run-s test:unit test:lint test:build", 30 | "test:unit": "jest --config ./jest.config.js", 31 | "test:build": "run-s build", 32 | "test:lint": "eslint src/**/*.ts src/**/*.tsx", 33 | "lint": "eslint src/**/*.ts src/**/*.tsx" 34 | }, 35 | "peerDependencies": { 36 | "react": "^18.2.0", 37 | "react-dom": "^18.2.0" 38 | }, 39 | "devDependencies": { 40 | "@testing-library/jest-dom": "^5.16.4", 41 | "@testing-library/react": "^13.3.0", 42 | "@testing-library/user-event": "^14.2.6", 43 | "@typescript-eslint/parser": "^5.31.0", 44 | "eslint": "^8.20.0", 45 | "jest": "^28.1.3", 46 | "microbundle": "^0.15.0", 47 | "npm-run-all": "^4.1.5", 48 | "ts-jest": "^28.0.7", 49 | "typescript": "^4.7.4" 50 | }, 51 | "files": [ 52 | "dist" 53 | ] 54 | } 55 | -------------------------------------------------------------------------------- /src/components/common/Callout/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import withContentValidation, { 4 | DropedProps 5 | } from '../../../hoc/withContentValidation' 6 | import Text from '../../core/Text' 7 | 8 | function Callout({ className, config }: DropedProps) { 9 | const { 10 | block: { content } 11 | } = config 12 | 13 | if (!content) return null 14 | 15 | return ( 16 |
17 | {content.icon?.emoji} 18 | 19 | {content.text.map((text, index) => ( 20 | 21 | ))} 22 | 23 |
24 | ) 25 | } 26 | 27 | export default withContentValidation(Callout) 28 | -------------------------------------------------------------------------------- /src/components/common/Code/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import withContentValidation, { DropedProps } from '../../../hoc/withContentValidation' 3 | import { slugify } from '../../../utils/slugify' 4 | 5 | function Code({ className, children, language }: DropedProps) { 6 | let cn = className 7 | 8 | if (language) { 9 | cn += ` language-${slugify(language)}` 10 | } 11 | 12 | return ( 13 |
{children}
14 | ) 15 | } 16 | 17 | export default withContentValidation(Code) 18 | -------------------------------------------------------------------------------- /src/components/common/Divider/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const Divider = () =>
4 | 5 | export default Divider 6 | -------------------------------------------------------------------------------- /src/components/common/DummyText/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import withContentValidation, { 4 | DropedProps 5 | } from '../../../hoc/withContentValidation' 6 | 7 | function DummyText({ children }: DropedProps) { 8 | return {children} 9 | } 10 | 11 | export default withContentValidation(DummyText) 12 | -------------------------------------------------------------------------------- /src/components/common/Embed/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { DropedProps } from '../../../hoc/withContentValidation' 3 | 4 | export type Props = { 5 | className?: string 6 | media?: DropedProps['media'] 7 | frameBorder?: string 8 | allow?: string 9 | allowFullScreen?: boolean 10 | } 11 | 12 | function Embed({ 13 | media, 14 | className, 15 | frameBorder, 16 | allow, 17 | allowFullScreen 18 | }: Props) { 19 | if (!media) return null 20 | 21 | const { src, alt } = media 22 | 23 | return ( 24 |