├── .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 | [](https://www.npmjs.com/package/@9gustin/react-notion-render)
9 | 
10 | 
11 | 
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 |
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 | 
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 | 
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 | #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 | 
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 |
44 | );
45 |
46 | const [isCopied, setIsCopied] = useState(false);
47 | const copyToClipboard = (str) => {
48 | const el = document.createElement('textarea');
49 | el.value = str;
50 | el.setAttribute('readonly', '');
51 | el.style.position = 'absolute';
52 | el.style.left = '-9999px';
53 | document.body.appendChild(el);
54 | el.select();
55 | document.execCommand('copy');
56 | document.body.removeChild(el);
57 | };
58 |
59 | return (
60 |
66 | {({ className, style, tokens, getLineProps, getTokenProps }) => (
67 |
71 | {
73 | copyToClipboard(children);
74 | setIsCopied(true);
75 | setTimeout(() => setIsCopied(false), 3000);
76 | }}
77 | >
78 | {isCopied ? '🎉 Copied!' : 'Copy'}
79 |
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 |
38 | {THEMES_LABELS[selectedTheme]}
39 |
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": "#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": "#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 {children}\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 {children}\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 {children}\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 {children}\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 |
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 |
46 |
47 |
48 |
49 |
50 | {/* {renderTitle(post.properties.Name)} */}
51 |
52 |
53 |
54 | {date}
55 |
56 | Read post →
57 |
58 |
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 |
32 | )
33 | }
34 |
35 | export default Embed
36 |
--------------------------------------------------------------------------------
/src/components/common/Embed/wrappedEmbed.tsx:
--------------------------------------------------------------------------------
1 | import Embed from '.'
2 | import withContentValidation from '../../../hoc/withContentValidation'
3 |
4 | export default withContentValidation(Embed)
5 |
--------------------------------------------------------------------------------
/src/components/common/EmptyBlock/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | function EmptyBlock() {
4 | return (
5 |
6 | )
7 | }
8 |
9 | export default EmptyBlock
10 |
--------------------------------------------------------------------------------
/src/components/common/File/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import withContentValidation, { DropedProps } from '../../../hoc/withContentValidation'
3 | import Link from '../Link'
4 |
5 | interface Props {
6 | className?: string
7 | media?: DropedProps['media']
8 | }
9 |
10 | function File({ media, className }: Props) {
11 | if (!media) return null
12 |
13 | const { src, name, extension } = media
14 |
15 | const cn = `block ${className ?? ''}`
16 |
17 | return (
18 |
19 | {name}.{extension}
20 |
21 | )
22 | }
23 |
24 | export default withContentValidation(File)
25 |
--------------------------------------------------------------------------------
/src/components/common/Image/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { DropedProps } from '../../../hoc/withContentValidation'
3 |
4 | export interface Props {
5 | className?: string
6 | media?: DropedProps['media']
7 | }
8 |
9 | function Image({ className, media }: Props) {
10 | if (!media) return null
11 | const { src, alt, href } = media
12 |
13 | const img =
14 |
15 | return href
16 | ? (
17 |
18 | {img}
19 |
20 | )
21 | : (
22 | img
23 | )
24 | }
25 |
26 | export default Image
27 |
--------------------------------------------------------------------------------
/src/components/common/Image/wrappedImage.tsx:
--------------------------------------------------------------------------------
1 | import Image from '.'
2 | import withContentValidation from '../../../hoc/withContentValidation'
3 |
4 | export default withContentValidation(Image)
5 |
--------------------------------------------------------------------------------
/src/components/common/Link/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { LinkAttributes } from '../../core/Render'
3 |
4 | export interface Props {
5 | url: string
6 | children: React.ReactNode
7 | className?: string
8 | linkAttributes?: (url: string) => LinkAttributes
9 | }
10 |
11 | function Link({ url, children, className, linkAttributes }: Props) {
12 | const defaultRedirectProps = url.startsWith('#')
13 | ? {}
14 | : {
15 | target: '_blank',
16 | rel: 'noreferrer'
17 | }
18 |
19 | const redirectProps = linkAttributes
20 | ? linkAttributes(url)
21 | : defaultRedirectProps
22 |
23 | return (
24 |
25 | {children}
26 |
27 | )
28 | }
29 |
30 | export default Link
31 |
--------------------------------------------------------------------------------
/src/components/common/List/components/Checkbox/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | import styles from '../../styles.module.css'
4 |
5 | interface Props {
6 | checked?: boolean
7 | }
8 |
9 | function Checkbox({ checked }: Props) {
10 | return (
11 |
17 | )
18 | }
19 |
20 | export default Checkbox
21 |
--------------------------------------------------------------------------------
/src/components/common/List/components/ListItem/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { Fragment, useMemo } from 'react'
2 |
3 | import { blockEnum } from '../../../../../types/BlockTypes'
4 | import withContentValidation, {
5 | DropedProps
6 | } from '../../../../../hoc/withContentValidation'
7 |
8 | import styles from '../../styles.module.css'
9 |
10 | import Checkbox from '../Checkbox'
11 |
12 | function ListItem({ children, config, className, checked, blockComponentsMapper }: DropedProps) {
13 | const { notionType: type, items } = config.block
14 |
15 | const renderChildren = useMemo(() => {
16 | if (type === blockEnum.CHECK_LIST) {
17 | return (
18 |
19 |
20 | {children}
21 |
22 | )
23 | } else if (type === blockEnum.TOGGLE_LIST && items) {
24 | return (
25 |
26 | {children}
27 |
28 | {items.map((block) => {
29 | const Component = block.getComponent(blockComponentsMapper)
30 |
31 | return Component
32 | ? (
33 |
34 | )
35 | : null
36 | })}
37 |
38 |
39 | )
40 | }
41 | return children
42 | }, [type, children, checked])
43 |
44 | return {renderChildren}
45 | }
46 |
47 | export default withContentValidation(ListItem)
48 |
--------------------------------------------------------------------------------
/src/components/common/List/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useCallback } from 'react'
2 |
3 | import { blockEnum } from '../../../types/BlockTypes'
4 | import withContentValidation, {
5 | DropedProps
6 | } from '../../../hoc/withContentValidation'
7 |
8 | import ListItem from './components/ListItem'
9 |
10 | import styles from './styles.module.css'
11 |
12 | function List({ className, config }: DropedProps) {
13 | const { notionType: type, items } = config.block
14 |
15 | const cn = `${
16 | type === blockEnum.CHECK_LIST || type === blockEnum.TOGGLE_LIST
17 | ? styles['remove-style']
18 | : ''
19 | } ${className}`
20 |
21 | const renderList = useCallback(
22 | (children: React.ReactNode) => {
23 | if (type === blockEnum.ENUM_LIST) { return {children} }
24 |
25 | return
26 | },
27 | [type]
28 | )
29 |
30 | return renderList(
31 | items?.map((item) => (
32 |
33 | ))
34 | )
35 | }
36 |
37 | export default withContentValidation(List)
38 |
--------------------------------------------------------------------------------
/src/components/common/List/styles.module.css:
--------------------------------------------------------------------------------
1 | .remove-style {
2 | list-style: none;
3 | padding-left: 12px;
4 | }
5 |
6 | .check {
7 | margin-right: 10px;
8 | }
9 |
10 | .drop-children {
11 | margin-left: calc(var(--children-spacing) - 12px)
12 | }
13 |
14 | .drop-button{
15 | cursor: pointer;
16 | }
--------------------------------------------------------------------------------
/src/components/common/Paragraph/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | import withContentValidation, { DropedProps } from '../../../hoc/withContentValidation'
4 |
5 | function Paragraph({ children, className }: DropedProps) {
6 | return {children}
7 | }
8 |
9 | export default withContentValidation(Paragraph)
10 |
--------------------------------------------------------------------------------
/src/components/common/Quote/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 Quote({ className, config }: DropedProps) {
9 | const {
10 | block: { content }
11 | } = config
12 |
13 | if (!content) return null
14 | return (
15 |
16 | {content.text.map((text, index) => (
17 |
18 | ))}
19 |
20 | )
21 | }
22 |
23 | export default withContentValidation(Quote)
24 |
--------------------------------------------------------------------------------
/src/components/common/Table/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | import withContentValidation, {
4 | DropedProps
5 | } from '../../../hoc/withContentValidation'
6 | import IText from '../../../types/Text'
7 | import { blockTypeClassname } from '../../../utils/getClassname'
8 |
9 | import Text from '../../core/Text'
10 |
11 | type tableRowProps = {
12 | cells: IText[][]
13 | className?: string
14 | id: string
15 | }
16 | function TableRow({ cells, className, id }: tableRowProps) {
17 | return (
18 |
19 | {cells.map((cellTexts, i) => (
20 |
21 | {cellTexts.map((text, textI) => )}
22 |
23 | ))}
24 |
25 | )
26 | }
27 |
28 | function Table({ className, config }: DropedProps) {
29 | const { content, items } = config.block
30 | const rows = items?.filter(({ content }) => content?.cells)
31 |
32 | if (!rows) return null
33 |
34 | const cn = `${className} ${content?.hasColumnHeader ? 'has-column-header' : ''
35 | } ${content?.hasRowHeader ? 'has-row-header' : ''}`.trim()
36 |
37 | return (
38 |
39 |
40 | {rows.map(({ notionType, content, id }) => (
41 |
42 | ))}
43 |
44 |
45 | )
46 | }
47 |
48 | export default withContentValidation(Table)
49 |
--------------------------------------------------------------------------------
/src/components/common/TableOfContents/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import withContentValidation, {
3 | DropedProps
4 | } from '../../../hoc/withContentValidation'
5 | import { blockTypeClassname } from '../../../utils/getClassname'
6 |
7 | type tableItemProps ={slugifyFn: ((text: string) => string) | null, plainText: string}
8 | function TableItem ({ slugifyFn, plainText }: tableItemProps) {
9 | if (!slugifyFn) return {plainText}
10 |
11 | return (
12 |
13 | {plainText}
14 |
15 | )
16 | }
17 |
18 | function TableOfContents({ className, index, slugifyFn }: DropedProps) {
19 | if (!index) return null
20 |
21 | return (
22 |
23 | {index.map(({ id, plainText, type }) => (
24 |
25 |
26 |
27 | ))}
28 |
29 | )
30 | }
31 |
32 | export default withContentValidation(TableOfContents)
33 |
--------------------------------------------------------------------------------
/src/components/common/Title/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useMemo } from 'react'
2 |
3 | import { blockEnum } from '../../../types/BlockTypes'
4 |
5 | import withContentValidation, {
6 | DropedProps
7 | } from '../../../hoc/withContentValidation'
8 |
9 | function Title({
10 | children,
11 | className,
12 | plainText,
13 | config,
14 | slugifyFn
15 | }: DropedProps) {
16 | const { notionType: type } = config.block
17 |
18 | const renderTitle = useMemo(() => {
19 | const props = {
20 | className,
21 | children,
22 | ...(slugifyFn ? { id: slugifyFn(plainText || '') } : {})
23 | }
24 |
25 | if (type === blockEnum.HEADING2) {
26 | return
27 | } else if (type === blockEnum.HEADING3) {
28 | return
29 | }
30 |
31 | return
32 | }, [className, children, plainText])
33 |
34 | return slugifyFn
35 | ? (
36 |
37 | {renderTitle}
38 |
39 | )
40 | : renderTitle
41 | }
42 |
43 | export default withContentValidation(Title)
44 |
--------------------------------------------------------------------------------
/src/components/common/Video/constants.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Embed from '../Embed'
3 |
4 | const MATCHERS = [
5 | {
6 | name: 'youtube',
7 | REGEXP:
8 | /(?:youtu\.be\/|youtube\.com(?:\/embed\/|\/v\/|\/watch\?v=|\/user\/\S+|\/ytscreeningroom\?v=))([\w\-]{10,12})\b/,
9 | getUrl: (src: string) => {
10 | const GET_ID =
11 | /(?:youtu\.be\/|youtube\.com(?:\/embed\/|\/v\/|\/watch\?v=|\/user\/\S+|\/ytscreeningroom\?v=|\/sandalsResorts#\w\/\w\/.*\/))([^\/&]{10,12})/
12 | const id = src.match(GET_ID)?.[1]
13 | return `https://www.youtube.com/embed/${id}`
14 | }
15 | },
16 | {
17 | name: 'googleDrive',
18 | REGEXP: /drive.google.com/,
19 | getUrl: (src: string) => {
20 | const videoUrl = src.split('/')
21 | videoUrl.pop()
22 | return `${videoUrl.join('/')}/preview`
23 | }
24 | }
25 | ]
26 |
27 | export function getPlayer(src: string, alt: string, className?: string) {
28 | const match = MATCHERS.find((option) => option.REGEXP.test(src))
29 |
30 | if (!match) return null
31 |
32 | return PLAYERS[match.name](
33 | match.getUrl ? match.getUrl(src) : src,
34 | alt,
35 | className
36 | )
37 | }
38 |
39 | const PLAYERS = {
40 | youtube: (url: string, title: string, className?: string) => (
41 |
47 | ),
48 | googleDrive: (url: string, title: string, className?: string) => (
49 |
54 | )
55 | }
56 |
57 | export default PLAYERS
58 |
--------------------------------------------------------------------------------
/src/components/common/Video/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { DropedProps } from '../../../hoc/withContentValidation'
3 | import { getPlayer } from './constants'
4 |
5 | export type Props = {
6 | className?: string
7 | media?: DropedProps['media']
8 | }
9 |
10 | function Video({ media, className }: Props) {
11 | if (!media) return null
12 |
13 | const { src, alt } = media
14 |
15 | const player = getPlayer(src, alt, className)
16 |
17 | return (
18 | player ?? (
19 |
20 |
21 | {alt}
22 |
23 | )
24 | )
25 | }
26 |
27 | export default Video
28 |
--------------------------------------------------------------------------------
/src/components/common/Video/wrappedVideo.tsx:
--------------------------------------------------------------------------------
1 | import Video from '.'
2 | import withContentValidation from '../../../hoc/withContentValidation'
3 |
4 | export default withContentValidation(Video)
5 |
--------------------------------------------------------------------------------
/src/components/core/Render/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useMemo } from 'react'
2 |
3 | import { BlockComponentsMapperType } from '../../../constants/BlockComponentsMapper/types'
4 | import { NotionBlock } from '../../../types/NotionBlock'
5 | import getBlocksToRender from '../../../utils/getBlocksToRender'
6 | import { indexGenerator } from '../../../utils/indexGenerator'
7 |
8 | export interface LinkAttributes {
9 | target?: string
10 | rel?: string
11 | }
12 |
13 | interface Props {
14 | blocks: NotionBlock[]
15 | useStyles?: boolean
16 | classNames?: boolean
17 | emptyBlocks?: boolean
18 | slugifyFn?: (text: string) => string
19 | mapPageUrlFn?: (input: any) => string
20 | simpleTitles?: boolean
21 | blockComponentsMapper?: BlockComponentsMapperType
22 | linkAttributes?: (url: string) => LinkAttributes
23 | }
24 |
25 | function Render({
26 | blocks,
27 | classNames,
28 | emptyBlocks,
29 | useStyles,
30 | slugifyFn,
31 | mapPageUrlFn,
32 | simpleTitles,
33 | blockComponentsMapper,
34 | linkAttributes
35 | }: Props) {
36 | if (!blocks || !blocks.length) return null
37 |
38 | const render = useMemo(() => {
39 | const renderBlocks = getBlocksToRender(blocks)
40 | const index = indexGenerator(blocks)
41 |
42 | return renderBlocks.map((block) => {
43 | const Component = block.getComponent(blockComponentsMapper)
44 |
45 | return Component ? (
46 |
58 | ) : null
59 | })
60 | }, [blocks])
61 |
62 | return useStyles ? (
63 | {render}
64 | ) : (
65 | {render}
66 | )
67 | }
68 |
69 | export default Render
70 |
--------------------------------------------------------------------------------
/src/components/core/Text/index.tsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable camelcase */
2 | import React, { Fragment, ReactElement } from 'react'
3 |
4 | import IText from '../../../types/Text'
5 | import { getClassname } from '../../../utils/getClassname'
6 | import Link from '../../common/Link'
7 | import withCustomComponent from '../../../hoc/withCustomComponent'
8 |
9 | export function Text(props: IText) {
10 | const {
11 | text,
12 | annotations,
13 | type,
14 | href,
15 | plain_text,
16 | mapPageUrlFn,
17 | linkAttributes
18 | } = props
19 | const className = getClassname(annotations)
20 |
21 | if (type === 'mention') {
22 | const redirectProps =
23 | props.mention?.type === 'page'
24 | ? {
25 | target: '_blank',
26 | rel: 'noreferrer'
27 | }
28 | : {}
29 |
30 | return (
31 |
32 | {plain_text}
33 |
34 | )
35 | }
36 |
37 | if (!text) return null
38 |
39 | let element: ReactElement = {text.content}
40 |
41 | if (className) element = {text.content}
42 |
43 | if (annotations.bold) {
44 | element = {text.content}
45 | } else if (annotations.code) {
46 | element = {text.content}
47 | } else if (annotations.italic) {
48 | element = {text.content}
49 | } else if (annotations.strikethrough) {
50 | element = {text.content}
51 | } else if (annotations.underline) {
52 | element = {text.content}
53 | }
54 |
55 | if (text.link) {
56 | let {
57 | link: { url }
58 | } = text
59 | if (url[0] === '/' && mapPageUrlFn) {
60 | url = mapPageUrlFn(url.slice(1))
61 | }
62 | element = (
63 |
64 | {element}
65 |
66 | )
67 | }
68 |
69 | return element
70 | }
71 |
72 | export default withCustomComponent(Text)
73 | /* eslint-enable camelcase */
74 |
--------------------------------------------------------------------------------
/src/constants/BlockComponentsMapper/index.ts:
--------------------------------------------------------------------------------
1 | import { blockEnum } from '../../types/BlockTypes'
2 |
3 | import List from '../../components/common/List'
4 | import Code from '../../components/common/Code'
5 | import File from '../../components/common/File'
6 | import Title from '../../components/common/Title'
7 | import Quote from '../../components/common/Quote'
8 | import Table from '../../components/common/Table'
9 | import Callout from '../../components/common/Callout'
10 | import Divider from '../../components/common/Divider'
11 | import DummyText from '../../components/common/DummyText'
12 | import Paragraph from '../../components/common/Paragraph'
13 | import Image from '../../components/common/Image/wrappedImage'
14 | import Video from '../../components/common/Video/wrappedVideo'
15 | import Embed from '../../components/common/Embed/wrappedEmbed'
16 | import TableOfContents from '../../components/common/TableOfContents'
17 |
18 | import { BlockComponentsMapperType } from './types'
19 |
20 | export const BlockComponentsMapper: BlockComponentsMapperType = {
21 | [blockEnum.PARAGRAPH]: Paragraph,
22 | [blockEnum.HEADING1]: Title,
23 | [blockEnum.HEADING2]: Title,
24 | [blockEnum.HEADING3]: Title,
25 | [blockEnum.DOTS_LIST]: List,
26 | [blockEnum.ENUM_LIST]: List,
27 | [blockEnum.CHECK_LIST]: List,
28 | [blockEnum.TOGGLE_LIST]: List,
29 | [blockEnum.VIDEO]: Video,
30 | [blockEnum.FILE]: File,
31 | [blockEnum.PDF]: Embed,
32 | [blockEnum.EMBED]: Embed,
33 | [blockEnum.TITLE]: DummyText,
34 | [blockEnum.IMAGE]: Image,
35 | [blockEnum.CALLOUT]: Callout,
36 | [blockEnum.QUOTE]: Quote,
37 | [blockEnum.DIVIDER]: Divider,
38 | [blockEnum.CODE]: Code,
39 | [blockEnum.TABLE_OF_CONTENTS]: TableOfContents,
40 | [blockEnum.TABLE]: Table,
41 | [blockEnum.TABLE_ROW]: undefined,
42 | [blockEnum.SYNCED_BLOCK]: undefined,
43 | [blockEnum.BOOKMARK]: undefined
44 | }
45 |
--------------------------------------------------------------------------------
/src/constants/BlockComponentsMapper/types.ts:
--------------------------------------------------------------------------------
1 | import { blockEnum } from '../../types/BlockTypes'
2 | import { WithContentValidationProps } from '../../hoc/withContentValidation'
3 |
4 | type BlockComponent = React.FC | undefined
5 |
6 | export type BlockComponentsMapperType = {
7 | [blockEnum.PARAGRAPH]?: BlockComponent
8 | [blockEnum.HEADING1]?: BlockComponent
9 | [blockEnum.HEADING2]?: BlockComponent
10 | [blockEnum.HEADING3]?: BlockComponent
11 | [blockEnum.DOTS_LIST]?: BlockComponent
12 | [blockEnum.ENUM_LIST]?: BlockComponent
13 | [blockEnum.CHECK_LIST]?: BlockComponent
14 | [blockEnum.TOGGLE_LIST]?: BlockComponent
15 | [blockEnum.VIDEO]?: BlockComponent
16 | [blockEnum.FILE]?: BlockComponent
17 | [blockEnum.PDF]?: BlockComponent
18 | [blockEnum.EMBED]?: BlockComponent
19 | [blockEnum.TITLE]?: BlockComponent
20 | [blockEnum.IMAGE]?: BlockComponent
21 | [blockEnum.CALLOUT]?: BlockComponent
22 | [blockEnum.QUOTE]?: BlockComponent
23 | [blockEnum.DIVIDER]?: BlockComponent
24 | [blockEnum.CODE]?: BlockComponent
25 | [blockEnum.TABLE_OF_CONTENTS]?: BlockComponent
26 | [blockEnum.TABLE]?: BlockComponent
27 | [blockEnum.TABLE_ROW]?: BlockComponent
28 | [blockEnum.SYNCED_BLOCK]?: BlockComponent
29 | [blockEnum.BOOKMARK]?: BlockComponent
30 | }
31 |
--------------------------------------------------------------------------------
/src/hoc/withContentValidation/constants.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { WithContentValidationProps } from '.'
3 | import IText from '../../types/Text'
4 |
5 | import WrappedText, { Text } from '../../components/core/Text'
6 |
7 | export function getMediaProps(props: WithContentValidationProps) {
8 | const { block } = props
9 | const url = block.getUrl()
10 |
11 | if (!url) return undefined
12 |
13 | const urlParts = url.match(/\/?([^/.]*)\.?([^/]*)$/)
14 |
15 | const name = urlParts?.[1] ?? ''
16 | const extension = urlParts?.[2].split('?')[0] ?? ''
17 |
18 | return {
19 | name,
20 | extension,
21 | alt: block.getPlainText(),
22 | src: url
23 | }
24 | }
25 |
26 | export function getDefaultProps(props: WithContentValidationProps) {
27 | const { block, mapPageUrlFn, linkAttributes } = props
28 | const plainText = block.getPlainText()
29 |
30 | return {
31 | checked: Boolean(block.content?.checked),
32 | plainText: plainText,
33 | children: block.content?.text.map((text: IText, index: number) => {
34 | let TextComponent = Text
35 | if (block.supportCustomComponents() && !text.annotations.code) {
36 | TextComponent = WrappedText
37 | }
38 | return (
39 |
45 | )
46 | }),
47 | language: block.content?.language,
48 | index: props.index,
49 | blockComponentsMapper: props.blockComponentsMapper,
50 | linkAttributes: props.linkAttributes
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/src/hoc/withContentValidation/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { PropsWithChildren } from 'react'
2 |
3 | import { ParsedBlock, SimpleBlock } from '../../types/Block'
4 |
5 | import EmptyBlock from '../../components/common/EmptyBlock'
6 | import { LinkAttributes } from '../../components/core/Render'
7 | import { BlockComponentsMapperType } from '../../constants/BlockComponentsMapper/types'
8 | import { blockTypeClassname } from '../../utils/getClassname'
9 | import { slugify } from '../../utils/slugify'
10 | import { getDefaultProps, getMediaProps } from './constants'
11 |
12 | export interface WithContentValidationProps {
13 | classNames?: boolean
14 | emptyBlocks?: boolean
15 | block: ParsedBlock
16 | slugifyFn?: (text: string) => string
17 | mapPageUrlFn?: (input: any) => string
18 | simpleTitles?: boolean
19 | index?: SimpleBlock[]
20 | blockComponentsMapper?: BlockComponentsMapperType
21 | linkAttributes?: (url: string) => LinkAttributes
22 | }
23 |
24 | export type DropedProps = PropsWithChildren<{
25 | className?: string
26 | checked: boolean
27 | plainText: string
28 | config: WithContentValidationProps
29 | slugifyFn: ((text: string) => string) | null
30 | language?: string
31 | media?: {
32 | alt: string
33 | src: string
34 | href?: string
35 | name?: string
36 | extension?: string
37 | player?: string
38 | }
39 | index?: SimpleBlock[]
40 | blockComponentsMapper?: BlockComponentsMapperType
41 | }>
42 |
43 | function withContentValidation(
44 | Component: React.ComponentType
45 | ): React.FC {
46 | return ({
47 | emptyBlocks,
48 | slugifyFn,
49 | classNames,
50 | simpleTitles,
51 | ...props
52 | }: WithContentValidationProps) => {
53 | const hasContent = props.block.hasContent()
54 | if (!hasContent && !emptyBlocks) {
55 | return null
56 | }
57 |
58 | let returnedProps: DropedProps = {
59 | checked: false,
60 | children: null,
61 | plainText: '',
62 | slugifyFn: simpleTitles ? null : slugifyFn ?? slugify,
63 | className: classNames
64 | ? blockTypeClassname(props.block.notionType)
65 | : undefined,
66 | config: {
67 | classNames: classNames,
68 | block: props.block,
69 | blockComponentsMapper: props.blockComponentsMapper,
70 | emptyBlocks,
71 | linkAttributes: props.linkAttributes
72 | }
73 | }
74 |
75 | if (props.block.isMedia()) {
76 | returnedProps.media = getMediaProps(props)
77 | } else {
78 | returnedProps = {
79 | ...returnedProps,
80 | ...getDefaultProps(props)
81 | }
82 | }
83 |
84 | return hasContent ? :
85 | }
86 | }
87 |
88 | export default withContentValidation
89 |
--------------------------------------------------------------------------------
/src/hoc/withCustomComponent/constants.ts:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | import Text from '../../types/Text'
4 | import { getClassname } from '../../utils/getClassname'
5 |
6 | import Image, { Props as ImageProps } from '../../components/common/Image'
7 | import Link, { Props as LinkProps } from '../../components/common/Link'
8 | import Video, { Props as VideoProps } from '../../components/common/Video'
9 |
10 | type WrappedComponentPropsType = Text
11 | export type CustomComponentPropsType = ImageProps | LinkProps | VideoProps
12 |
13 | interface CustomComponent {
14 | match: RegExp
15 | component: React.ComponentType
16 | transformProps?: (
17 | props: WrappedComponentPropsType
18 | ) => CustomComponentPropsType
19 | }
20 |
21 | export const customComponents: CustomComponent[] = [
22 | {
23 | match: /-\[[^\]]*\]\((.*?)\s*("(?:.*[^"])")?\s*\)/,
24 | // eslint-disable-next-line camelcase
25 | transformProps: ({ plain_text }) => ({
26 | media: {
27 | alt: plain_text.split('-[')[1].split(']')[0],
28 | src: plain_text.split('(')[1].split(')')[0],
29 | player: plain_text.indexOf('#') < 0 ? undefined : plain_text.substr(plain_text.indexOf('#')).replace('#', '')
30 | }
31 | }),
32 | component: Video
33 | },
34 | {
35 | match: /!\[[^\]]*\]\((.*?)\s*("(?:.*[^"])")?\s*\)/,
36 | // eslint-disable-next-line camelcase
37 | transformProps: ({ plain_text }) => ({
38 | media: {
39 | alt: plain_text.split('![')[1].split(']')[0],
40 | src: plain_text.split('(')[1].split(')')[0],
41 | href: plain_text.indexOf('#') < 0 ? undefined : plain_text.substr(plain_text.indexOf('#')).replace('#', '')
42 | }
43 | }),
44 | component: Image
45 | },
46 | {
47 | match: /[[^\]]*\]\((.*?)\s*("(?:.*[^"])")?\s*\)/,
48 | // eslint-disable-next-line camelcase
49 | transformProps: ({ plain_text, annotations }) => ({
50 | url: plain_text.split('(')[1].split(')')[0],
51 | children: plain_text.split('[')[1].split(']')[0],
52 | className: getClassname(annotations)
53 | }),
54 | component: Link
55 | }
56 | ]
57 |
--------------------------------------------------------------------------------
/src/hoc/withCustomComponent/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useMemo } from 'react'
2 |
3 | import Text from '../../types/Text'
4 | import { CustomComponentPropsType, customComponents } from './constants'
5 |
6 | function withCustomComponent(
7 | TempComponent: React.ComponentType
8 | ): React.FC {
9 | return (props: PropsType & Text) => {
10 | const customComponent = customComponents.find((component) =>
11 | component.match.test(props.plain_text)
12 | )
13 |
14 | const renderComponent = useMemo(() => {
15 | const Component: React.ComponentType<
16 | PropsType | CustomComponentPropsType
17 | > = customComponent?.component || TempComponent
18 |
19 | if (customComponent?.transformProps) {
20 | const newProps = { ...props, ...customComponent.transformProps(props) }
21 | return
22 | }
23 |
24 | return
25 | }, [customComponent])
26 |
27 | return renderComponent
28 | }
29 | }
30 |
31 | export default withCustomComponent
32 |
--------------------------------------------------------------------------------
/src/index.tsx:
--------------------------------------------------------------------------------
1 | import './styles/index.css'
2 | import './styles/components.css'
3 |
4 | export { default as getBlocksToRender } from './utils/getBlocksToRender'
5 | export { slugify as rnrSlugify } from './utils/slugify'
6 | export { indexGenerator } from './utils/indexGenerator'
7 |
8 | export { ParsedBlock } from './types/Block'
9 | export { NotionBlock } from './types/NotionBlock'
10 | export { blockEnum, UNSUPPORTED_TYPE } from './types/BlockTypes'
11 | export { default as Text } from './types/Text'
12 |
13 | export { default as Render } from './components/core/Render'
14 | export { default as RenderText } from './components/core/Text'
15 |
16 | export { default as withContentValidation } from './hoc/withContentValidation'
17 | export { default as withCustomComponent } from './hoc/withCustomComponent'
18 |
19 | export { BlockComponentsMapperType } from './constants/BlockComponentsMapper/types'
20 |
--------------------------------------------------------------------------------
/src/styles/components.css:
--------------------------------------------------------------------------------
1 | /*
2 | Global styles for the components, only when are in rnr-container
3 | */
4 |
5 | /* Callout */
6 | .rnr-container .rnr-callout {
7 | background-color: var(--light-color);
8 | display: flex;
9 | gap: 16px;
10 | margin: var(--spacing) 0;
11 | padding: 14px;
12 | }
13 |
14 | /* Quote */
15 | .rnr-container .rnr-quote {
16 | border-left: 4px solid var(--color-link);
17 | margin: var(--spacing) 0;
18 | padding-left: var(--children-spacing);
19 | }
20 |
21 | /* Table of contents */
22 | .rnr-container .rnr-table_of_contents {
23 | list-style: none;
24 | padding: 0;
25 | }
26 |
27 | .rnr-container .rnr-table_of_contents > li {
28 | padding: 3px 0;
29 | }
30 |
31 | .rnr-container .rnr-table_of_contents > .rnr-heading_2 {
32 | margin-left: 30px;
33 | }
34 |
35 | .rnr-container .rnr-table_of_contents > .rnr-heading_3 {
36 | margin-left: 60px;
37 | }
38 |
39 | /* Table */
40 | .rnr-container .rnr-table {
41 | border-collapse: collapse;
42 | }
43 |
44 | .rnr-container .rnr-table td {
45 | border: 1px solid var(--color-gray);
46 | padding: 5px;
47 | }
48 |
49 | .rnr-container .has-column-header tr:first-child > td {
50 | background-color: var(--light-color);
51 | }
52 |
53 | .rnr-container .has-row-header tr > td:first-child {
54 | background-color: var(--light-color);
55 | }
--------------------------------------------------------------------------------
/src/styles/index.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --font-size-display-text: 32px;
3 | --font-size-title: 28px;
4 | --font-size-sub-title: 22px;
5 | --font-size-primary: 16px;
6 |
7 | --normal-weight: 400;
8 | --medium-weight: 500;
9 |
10 | --color-red: #ff2525;
11 | --color-gray: #979797;
12 | --color-brown: #816868;
13 | --color-orange: #FE9920;
14 | --color-yellow: #F1DB4B;
15 | --color-green: #22ae65;
16 | --color-blue: #0eb7e4;
17 | --color-purple: #a842ec;
18 | --color-pink: #FE5D9F;
19 | --color-link: #4e71da;
20 | --light-color: rgba(0,0,0,.2);
21 |
22 | --children-spacing: 25px;
23 | --spacing: 10px;
24 | }
25 |
26 | .rnr-container *{
27 | font-size: var(--font-size-primary);
28 | font-weight: var(--normal-weight);
29 | }
30 |
31 | .rnr-container > .block{
32 | display: block;
33 | }
34 |
35 | .rnr-container h1,
36 | .rnr-container h1 > * {
37 | font-size: var(--font-size-display-text);
38 | font-weight: var(--medium-weight);
39 | }
40 |
41 | .rnr-container h2,
42 | .rnr-container h2 > * {
43 | font-size: var(--font-size-title);
44 | font-weight: var(--medium-weight);
45 | }
46 |
47 | .rnr-container h3,
48 | .rnr-container h3 > * {
49 | font-size: var(--font-size-sub-title);
50 | }
51 |
52 | .rnr-container a,
53 | .rnr-container :any-link {
54 | color: var(--color-link);
55 | opacity: .8;
56 | transition: opacity .3s;
57 | }
58 |
59 | .rnr-container a:focus,
60 | .rnr-container a:hover {
61 | opacity: 1;
62 | }
63 |
64 | .rnr-container img {
65 | max-width: 100%;
66 | }
67 |
68 | .rnr-container :any-link.title {
69 | color: inherit;
70 | text-decoration: none;
71 | }
72 |
73 | .rnr-container .rnr-empty-block {
74 | height: var(--spacing);
75 | }
76 |
77 | .rnr-container .rnr-bold {
78 | font-weight: var(--medium-weight);
79 | }
80 |
81 | .rnr-container .rnr-italic {
82 | font-style: italic;
83 | }
84 |
85 | .rnr-container .rnr-strikethrough {
86 | text-decoration: line-through;
87 | }
88 |
89 | .rnr-container .rnr-underline {
90 | text-decoration: underline;
91 | }
92 |
93 | .rnr-container .rnr-underline.rnr-strikethrough {
94 | text-decoration: underline line-through;
95 | }
96 |
97 | .rnr-container .rnr-red {
98 | color: var(--color-red);
99 | }
100 |
101 | .rnr-container .rnr-gray {
102 | color: var(--color-gray);
103 | }
104 |
105 | .rnr-container .rnr-brown {
106 | color: var(--color-brown);
107 | }
108 |
109 | .rnr-container .rnr-orange {
110 | color: var(--color-orange);
111 | }
112 |
113 | .rnr-container .rnr-yellow {
114 | color: var(--color-yellow);
115 | }
116 |
117 | .rnr-container .rnr-green {
118 | color: var(--color-green);
119 | }
120 |
121 | .rnr-container .rnr-blue {
122 | color: var(--color-blue);
123 | }
124 |
125 | .rnr-container .rnr-purple {
126 | color: var(--color-purple);
127 | }
128 |
129 | .rnr-container .rnr-pink {
130 | color: var(--color-pink);
131 | }
132 |
--------------------------------------------------------------------------------
/src/types/Block.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable camelcase */
2 | import Text from './Text'
3 | import { blockEnum } from './BlockTypes'
4 | import { NotionBlock } from './NotionBlock'
5 | import { BlockComponentsMapperType } from '../constants/BlockComponentsMapper/types'
6 | import { BlockComponentsMapper } from '../constants/BlockComponentsMapper'
7 |
8 | export class ParsedBlock {
9 | id: string
10 | notionType: blockEnum
11 | items: ParsedBlock[] | null
12 | content: null | {
13 | text: Text[]
14 | checked?: boolean
15 | caption?: Text[]
16 | type?: 'external' | 'file'
17 | external?: {
18 | url: string
19 | }
20 | file?: {
21 | url: string
22 | }
23 | url?: string
24 | icon?: {
25 | type: 'emoji'
26 | emoji: string
27 | }
28 | language?: string
29 | hasColumnHeader?: boolean
30 | hasRowHeader?: boolean
31 | cells?: (Text[])[]
32 | }
33 |
34 | constructor(initialValues: NotionBlock, isChild?: boolean) {
35 | const notionType = initialValues.type as blockEnum
36 | const content = initialValues[notionType]
37 |
38 | if (!notionType || !content) return
39 |
40 | this.id = initialValues.id
41 | this.notionType = notionType
42 |
43 | if (initialValues.type === blockEnum.TITLE && 'title' in initialValues) {
44 | this.items = null
45 | this.content = { text: initialValues.title }
46 | } else if (this.isList() && !isChild) {
47 | this.content = null
48 | this.items = [new ParsedBlock(initialValues, true)]
49 | } else {
50 | const {
51 | rich_text,
52 | text,
53 | checked,
54 | caption,
55 | type,
56 | external,
57 | file,
58 | url,
59 | icon,
60 | language,
61 | has_column_header,
62 | has_row_header,
63 | cells
64 | } = content
65 |
66 | this.items =
67 | content.children?.map(
68 | (child: NotionBlock) => new ParsedBlock(child, true)
69 | ) ?? null
70 | this.content = {
71 | text: rich_text ?? text ?? [],
72 | checked,
73 | caption,
74 | type,
75 | external,
76 | file,
77 | url,
78 | icon,
79 | language,
80 | hasColumnHeader: has_column_header,
81 | hasRowHeader: has_row_header,
82 | cells
83 | }
84 | }
85 | }
86 |
87 | getComponent(customMapper?: BlockComponentsMapperType) {
88 | const mapper = { ...BlockComponentsMapper, ...customMapper }
89 |
90 | return mapper[this.notionType]
91 | }
92 |
93 | getUrl() {
94 | if (!this.content) return null
95 |
96 | let url = null
97 |
98 | if (this.isEmbed()) {
99 | url = this.content.url
100 | } else if (this.isMedia() && this.content?.type) {
101 | url = this.content[this.content.type]?.url
102 | }
103 | return url || null
104 | }
105 |
106 | getType() {
107 | switch (this.notionType) {
108 | case blockEnum.TOGGLE_LIST:
109 | case blockEnum.DOTS_LIST:
110 | case blockEnum.CHECK_LIST:
111 | case blockEnum.ENUM_LIST:
112 | return 'LIST'
113 | case blockEnum.HEADING1:
114 | case blockEnum.HEADING2:
115 | case blockEnum.HEADING3:
116 | return 'TITLE'
117 | case blockEnum.FILE:
118 | case blockEnum.VIDEO:
119 | case blockEnum.IMAGE:
120 | case blockEnum.PDF:
121 | case blockEnum.EMBED:
122 | return 'MEDIA'
123 | case blockEnum.SYNCED_BLOCK:
124 | return 'CONTAINER'
125 | case blockEnum.TABLE:
126 | case blockEnum.TABLE_OF_CONTENTS:
127 | return 'TABLE'
128 | case blockEnum.CODE:
129 | return 'CODE'
130 | default:
131 | return 'ELEMENT'
132 | }
133 | }
134 |
135 | getPlainText() {
136 | const textComponent = this.isMedia()
137 | ? this.content?.caption
138 | : this.content?.text
139 |
140 | return textComponent?.map((text: Text) => text.plain_text).join(' ') ?? ''
141 | }
142 |
143 | isList() {
144 | return this.getType() === 'LIST'
145 | }
146 |
147 | isCode() {
148 | return this.getType() === 'CODE'
149 | }
150 |
151 | isTitle() {
152 | return this.getType() === 'TITLE'
153 | }
154 |
155 | isMedia() {
156 | return this.getType() === 'MEDIA'
157 | }
158 |
159 | isEmbed() {
160 | return this.getType() === 'MEDIA' && this.notionType === blockEnum.EMBED
161 | }
162 |
163 | isContainer() {
164 | return this.getType() === 'CONTAINER'
165 | }
166 |
167 | isTable() {
168 | return this.getType() === 'TABLE'
169 | }
170 |
171 | equalsType(type: blockEnum) {
172 | return this.notionType === type
173 | }
174 |
175 | addItem(block: NotionBlock) {
176 | if (!this.items) this.items = []
177 |
178 | this.items.push(new ParsedBlock(block, true))
179 | }
180 |
181 | hasContent() {
182 | return (
183 | this.getUrl() ||
184 | this.getPlainText().trim() !== '' ||
185 | this.items?.length ||
186 | this.isTable()
187 | )
188 | }
189 |
190 | supportCustomComponents () {
191 | return !this.isCode()
192 | }
193 | }
194 |
195 | export type SimpleBlock = {
196 | id: string
197 | type: blockEnum
198 | text: Text[] | undefined
199 | plainText: string
200 | subItems?: SimpleBlock[]
201 | }
202 |
--------------------------------------------------------------------------------
/src/types/BlockTypes.ts:
--------------------------------------------------------------------------------
1 | export enum blockEnum {
2 | HEADING1 = 'heading_1',
3 | HEADING2 = 'heading_2',
4 | HEADING3 = 'heading_3',
5 | PARAGRAPH = 'paragraph',
6 | TOGGLE_LIST = 'toggle',
7 | DOTS_LIST = 'bulleted_list_item',
8 | ENUM_LIST = 'numbered_list_item',
9 | CHECK_LIST = 'to_do',
10 | TITLE = 'title',
11 | VIDEO = 'video',
12 | IMAGE = 'image',
13 | EMBED = 'embed',
14 | FILE = 'file',
15 | PDF = 'pdf',
16 | BOOKMARK = 'bookmark',
17 | CALLOUT = 'callout',
18 | QUOTE = 'quote',
19 | DIVIDER = 'divider',
20 | CODE = 'code',
21 | SYNCED_BLOCK = 'synced_block',
22 | TABLE_OF_CONTENTS = 'table_of_contents',
23 | TABLE = 'table',
24 | TABLE_ROW = 'table_row',
25 | }
26 |
27 | export const UNSUPPORTED_TYPE = 'unsupported'
28 |
--------------------------------------------------------------------------------
/src/types/NotionBlock.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable camelcase */
2 | import { blockEnum } from './BlockTypes'
3 | import Text from './Text'
4 |
5 | interface Title {
6 | id: 'title';
7 | type: 'title';
8 | title: Text[];
9 | }
10 |
11 | interface BlockTypeContent {
12 | text: Text[]
13 | checked?: boolean
14 | children?: Block[]
15 | }
16 |
17 | interface Block {
18 | id: string
19 | type: blockEnum | string
20 | object: 'block' | 'database' | 'page' | string
21 | created_time: Date | string
22 | last_edited_time: Date | string
23 | has_children: boolean
24 | [blockEnum.HEADING1]?: BlockTypeContent
25 | [blockEnum.HEADING2]?: BlockTypeContent
26 | [blockEnum.HEADING3]?: BlockTypeContent
27 | [blockEnum.PARAGRAPH]?: BlockTypeContent
28 | [blockEnum.DOTS_LIST]?: BlockTypeContent
29 | [blockEnum.ENUM_LIST]?: BlockTypeContent
30 | [blockEnum.CHECK_LIST]?: BlockTypeContent
31 | [blockEnum.TOGGLE_LIST]?: BlockTypeContent
32 | [blockEnum.TABLE]?: BlockTypeContent & {
33 | has_column_header: boolean
34 | has_row_header: boolean
35 | table_width: number
36 | }
37 | [blockEnum.TABLE_ROW]?: BlockTypeContent & {
38 | cells: Text[];
39 | }
40 | }
41 |
42 | export type NotionBlock = Block | Title
43 |
--------------------------------------------------------------------------------
/src/types/Text.ts:
--------------------------------------------------------------------------------
1 | import { LinkAttributes } from '../components/core/Render'
2 |
3 | type textTypes = 'text' | 'mention' | string
4 |
5 | type Link = {
6 | url: string
7 | }
8 |
9 | export default interface Text {
10 | type: textTypes
11 | text?: {
12 | content: string
13 | link: Link | null
14 | }
15 | annotations: {
16 | bold: boolean
17 | italic: boolean
18 | strikethrough: boolean
19 | underline: boolean
20 | code: boolean
21 | color: string
22 | }
23 | // eslint-disable-next-line camelcase
24 | plain_text: string
25 | href?: string
26 | mention?: {
27 | type: string
28 | }
29 | mapPageUrlFn?: (input: any) => string
30 | linkAttributes?: (url: string) => LinkAttributes
31 | }
32 |
--------------------------------------------------------------------------------
/src/typings.d.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Default CSS definition for typescript,
3 | * will be overridden with file-specific definitions by rollup
4 | */
5 | declare module '*.css' {
6 | const content: { [className: string]: string }
7 | export default content
8 | }
9 |
10 | interface SvgrComponent
11 | extends React.StatelessComponent> {}
12 |
13 | declare module '*.svg' {
14 | const svgUrl: string
15 | const svgComponent: SvgrComponent
16 | export default svgUrl
17 | export { svgComponent as ReactComponent }
18 | }
19 |
--------------------------------------------------------------------------------
/src/utils/getBlocksToRender.ts:
--------------------------------------------------------------------------------
1 | import { ParsedBlock } from '../types/Block'
2 | import { UNSUPPORTED_TYPE } from '../types/BlockTypes'
3 | import { NotionBlock } from '../types/NotionBlock'
4 |
5 | function areRelated(previous: ParsedBlock, current: ParsedBlock) {
6 | return previous.isList() && previous.equalsType(current.notionType)
7 | }
8 |
9 | /**
10 | * The objetive of this function its remove blocks that are not supported and
11 | * put together the items of the same list to render easily
12 | * @param blocks the entire list of blocks
13 | * @returns
14 | */
15 | export default function getBlocksToRender(blocks: NotionBlock[]): ParsedBlock[] {
16 | const cleanBlocks = blocks.filter(
17 | ({ type }) => type !== UNSUPPORTED_TYPE
18 | )
19 |
20 | if (!cleanBlocks.length) return []
21 |
22 | const returnBlocks: ParsedBlock[] = []
23 |
24 | for (let i = 0; i < cleanBlocks.length; i++) {
25 | const previousBlock = returnBlocks[returnBlocks.length - 1]
26 | const block = new ParsedBlock(cleanBlocks[i])
27 |
28 | if (previousBlock && areRelated(previousBlock, block)) {
29 | previousBlock.addItem(cleanBlocks[i])
30 | } else {
31 | if (block.isContainer() && block.items) {
32 | returnBlocks.push(...block.items)
33 | } else {
34 | returnBlocks.push(block)
35 | }
36 | }
37 | }
38 |
39 | return returnBlocks
40 | }
41 |
--------------------------------------------------------------------------------
/src/utils/getClassname.ts:
--------------------------------------------------------------------------------
1 | import { blockEnum } from '..'
2 | import Text from '../types/Text'
3 |
4 | const DEFAULT_COLOR = 'default'
5 |
6 | export function getClassname(annotations: Text['annotations']) {
7 | return `
8 | ${annotations.bold ? 'rnr-bold' : ''}
9 | ${annotations.code ? 'rnr-inline-code' : ''}
10 | ${annotations.italic ? 'rnr-italic' : ''}
11 | ${annotations.strikethrough ? 'rnr-strikethrough' : ''}
12 | ${annotations.underline ? 'rnr-underline' : ''}
13 | ${annotations.color !== DEFAULT_COLOR ? `rnr-${annotations.color}` : ''}
14 | `.trim()
15 | }
16 |
17 | export function blockTypeClassname(notionType: blockEnum) {
18 | return `rnr-${notionType}`
19 | }
20 |
--------------------------------------------------------------------------------
/src/utils/indexGenerator.ts:
--------------------------------------------------------------------------------
1 | import { ParsedBlock, SimpleBlock } from '../types/Block'
2 | import { NotionBlock } from '../types/NotionBlock'
3 |
4 | export function indexGenerator(blocks: NotionBlock[]): SimpleBlock[] {
5 | const parsedBlocks = blocks.map(block => new ParsedBlock(block))
6 | const titles = []
7 |
8 | for (let i = 0; i < parsedBlocks.length; i++) {
9 | if (parsedBlocks[i].isTitle()) {
10 | titles.push(parsedBlocks[i])
11 | } else if (parsedBlocks[i].isContainer() && parsedBlocks[i].items) {
12 | titles.push(...parsedBlocks[i].items!.filter(block => block.isTitle()))
13 | }
14 | }
15 |
16 | return titles.map((title) => ({
17 | id: title.id,
18 | type: title.notionType,
19 | text: title.content?.text,
20 | plainText: title.getPlainText()
21 | })) ?? []
22 | }
23 |
--------------------------------------------------------------------------------
/src/utils/slugify.ts:
--------------------------------------------------------------------------------
1 | export function slugify(text: string) {
2 | return text
3 | .toString()
4 | .trim()
5 | .toLowerCase()
6 | .replace(/\s+/g, '-')
7 | .replace(/[^\w\-]+/g, '')
8 | .replace(/\-\-+/g, '-')
9 | .replace(/^-+/, '')
10 | .replace(/-+$/, '')
11 | }
12 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "outDir": "dist",
4 | "module": "esnext",
5 | "lib": ["dom", "esnext"],
6 | "moduleResolution": "node",
7 | "jsx": "react",
8 | "sourceMap": true,
9 | "declaration": true,
10 | "esModuleInterop": true,
11 | "noImplicitReturns": true,
12 | "noImplicitThis": true,
13 | "noImplicitAny": true,
14 | "strictNullChecks": true,
15 | "suppressImplicitAnyIndexErrors": true,
16 | "noUnusedLocals": true,
17 | "noUnusedParameters": true,
18 | "allowSyntheticDefaultImports": true
19 | },
20 | "include": ["src"],
21 | "exclude": ["node_modules", "dist", "example"]
22 | }
23 |
--------------------------------------------------------------------------------
/tsconfig.test.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "module": "commonjs"
5 | }
6 | }
--------------------------------------------------------------------------------