Skip to content

Commit d037f9c

Browse files
committed
Add fullPage mode
- Wraps content with correct page width (full size or regular) - Adds page cover if available - Renders page icon + title Things changed along the way: - Style improvements throughout - Allow custom images for icons - Add support for whole-block styles
1 parent a6e2ece commit d037f9c

File tree

10 files changed

+292
-101
lines changed

10 files changed

+292
-101
lines changed

README.md

Lines changed: 23 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -93,27 +93,29 @@ List of pages that implement this library.
9393

9494
Most common block types are supported. We happily accept pull requests to add support for the missing blocks.
9595

96-
| Block Type | Supported | Notes |
97-
| ----------------- | ---------- | -------------------- |
98-
| Text | ✅ Yes | |
99-
| Heading | ✅ Yes | |
100-
| Image | ✅ Yes | |
101-
| Image Caption | ✅ Yes | |
102-
| Bulleted List | ✅ Yes | |
103-
| Numbered List | ✅ Yes | |
104-
| Quote | ✅ Yes | |
105-
| Callout | ✅ Yes | |
106-
| Column | ✅ Yes | |
107-
| iframe | ✅ Yes | |
108-
| Video | ✅ Yes | Only embedded videos |
109-
| Divider | ✅ Yes | |
110-
| Link | ✅ Yes | |
111-
| Code | ✅ Yes | |
112-
| Web Bookmark | ✅ Yes | |
113-
| Toggle List | ✅ Yes | |
114-
| Databases | ❌ Missing | |
115-
| Checkbox | ❌ Missing | |
116-
| Table Of Contents | ❌ Missing | |
96+
| Block Type | Supported | Notes |
97+
| ----------------- | ---------- | ---------------------- |
98+
| Text | ✅ Yes | |
99+
| Heading | ✅ Yes | |
100+
| Image | ✅ Yes | |
101+
| Image Caption | ✅ Yes | |
102+
| Bulleted List | ✅ Yes | |
103+
| Numbered List | ✅ Yes | |
104+
| Quote | ✅ Yes | |
105+
| Callout | ✅ Yes | |
106+
| Column | ✅ Yes | |
107+
| iframe | ✅ Yes | |
108+
| Video | ✅ Yes | Only embedded videos |
109+
| Divider | ✅ Yes | |
110+
| Link | ✅ Yes | |
111+
| Code | ✅ Yes | |
112+
| Web Bookmark | ✅ Yes | |
113+
| Toggle List | ✅ Yes | |
114+
| Page Links | ✅ Yes | |
115+
| Header | ✅ Yes | Enable with `fullPage` |
116+
| Databases | ❌ Missing | |
117+
| Checkbox | ❌ Missing | |
118+
| Table Of Contents | ❌ Missing | |
117119

118120
## Credits
119121

example/pages/[pageId].tsx

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -35,24 +35,21 @@ const NotionPage = ({ blockMap }) => {
3535
blockMap[Object.keys(blockMap)[0]]?.value.properties.title[0][0];
3636

3737
return (
38-
<div
39-
style={{
40-
maxWidth: 708,
41-
margin: "0 auto",
42-
padding: "0 8px",
43-
fontFamily: `-apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, "Apple Color Emoji", Arial, sans-serif, "Segoe UI Emoji", "Segoe UI Symbol"`
44-
}}
45-
>
38+
<>
4639
<Head>
4740
<title>{title}</title>
4841
</Head>
49-
<NotionRenderer blockMap={blockMap} />
50-
<style jsx>{`
42+
<NotionRenderer blockMap={blockMap} fullPage />
43+
<style jsx global>{`
5144
div :global(.notion-code) {
5245
box-sizing: border-box;
5346
}
47+
body {
48+
padding: 0;
49+
margin: 0;
50+
}
5451
`}</style>
55-
</div>
52+
</>
5653
);
5754
};
5855

src/block.tsx

Lines changed: 90 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,19 @@
11
import * as React from "react";
2-
import { DecorationType, BlockType, ContentValueType } from "./types";
2+
import {
3+
DecorationType,
4+
BlockType,
5+
ContentValueType,
6+
BlockMapType
7+
} from "./types";
38
import Asset from "./components/asset";
49
import Code from "./components/code";
5-
import { classNames, getTextContent } from "./utils";
10+
import PageIcon from "./components/page-icon";
11+
import {
12+
classNames,
13+
getTextContent,
14+
getListNumber,
15+
toNotionImageUrl
16+
} from "./utils";
617

718
export const renderChildText = (properties: DecorationType[]) => {
819
return properties?.map(([text, decorations], i) => {
@@ -49,17 +60,73 @@ export type MapPageUrl = (pageId: string) => string;
4960
interface Block {
5061
block: BlockType;
5162
level: number;
52-
listNumber?: number;
63+
blockMap: BlockMapType;
64+
65+
fullPage?: boolean;
5366
mapPageUrl?: MapPageUrl;
5467
}
5568

5669
export const Block: React.FC<Block> = props => {
57-
const { block, children, listNumber, level } = props;
70+
const { block, children, level, fullPage, blockMap } = props;
5871
const blockValue = block?.value;
5972
switch (blockValue?.type) {
6073
case "page":
61-
if (level === 0) return <div className="notion">{children}</div>;
62-
else {
74+
if (level === 0) {
75+
if (fullPage) {
76+
if (!blockValue.properties) {
77+
return null;
78+
}
79+
80+
const {
81+
page_icon,
82+
page_cover,
83+
page_cover_position,
84+
page_full_width,
85+
page_small_text
86+
} = blockValue.format || {};
87+
88+
return (
89+
<div className="notion">
90+
{page_cover && (
91+
<img
92+
src={toNotionImageUrl(page_cover)}
93+
alt={getTextContent(blockValue.properties.title)}
94+
className="notion-page-cover"
95+
style={{
96+
objectPosition: `center ${
97+
(1 - (page_cover_position || 0.5)) * 100
98+
}%`
99+
}}
100+
/>
101+
)}
102+
<div
103+
className={classNames(
104+
"notion-page",
105+
!page_cover && "notion-page-offset",
106+
page_full_width && "notion-full-width",
107+
page_small_text && "notion-small-text"
108+
)}
109+
>
110+
{page_icon && (
111+
<PageIcon
112+
className={
113+
page_cover ? "notion-page-icon-offset" : undefined
114+
}
115+
block={block}
116+
big
117+
/>
118+
)}
119+
<div className="notion-title">
120+
{renderChildText(blockValue.properties.title)}
121+
</div>
122+
{children}
123+
</div>
124+
</div>
125+
);
126+
} else {
127+
return <div className="notion">{children}</div>;
128+
}
129+
} else {
63130
if (!blockValue.properties) return null;
64131
return (
65132
<a
@@ -68,7 +135,7 @@ export const Block: React.FC<Block> = props => {
68135
>
69136
{blockValue.format && (
70137
<div className="notion-page-icon">
71-
{blockValue.format.page_icon}
138+
<PageIcon block={block} />
72139
</div>
73140
)}
74141
<div className="notion-page-text">
@@ -102,10 +169,16 @@ export const Block: React.FC<Block> = props => {
102169
return <hr className="notion-hr" />;
103170
case "text":
104171
if (!blockValue.properties) {
105-
return <p style={{ height: "1rem" }}> </p>;
172+
return <div className="notion-blank" />;
106173
}
174+
const blockColor = blockValue.format?.block_color;
107175
return (
108-
<p className={`notion-text`}>
176+
<p
177+
className={classNames(
178+
`notion-text`,
179+
blockColor && `notion-${blockColor}`
180+
)}
181+
>
109182
{renderChildText(blockValue.properties.title)}
110183
</p>
111184
);
@@ -137,7 +210,11 @@ export const Block: React.FC<Block> = props => {
137210
) : null;
138211
}
139212

140-
return listNumber !== undefined ? wrapList(output, listNumber) : output;
213+
const isTopLevel =
214+
block.value.type !== blockMap[block.value.parent_id].value.type;
215+
const start = getListNumber(blockValue.id, blockMap);
216+
217+
return isTopLevel ? wrapList(output, start) : output;
141218

142219
case "image":
143220
case "embed":
@@ -199,7 +276,9 @@ export const Block: React.FC<Block> = props => {
199276
`notion-${blockValue.format.block_color}_co`
200277
)}
201278
>
202-
<div>{blockValue.format.page_icon}</div>
279+
<div>
280+
<PageIcon block={block} />
281+
</div>
203282
<div className="notion-callout-text">
204283
{renderChildText(blockValue.properties.title)}
205284
</div>

src/components/asset.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import * as React from "react";
22
import { BlockType, ContentValueType } from "../types";
3+
import { toNotionImageUrl } from "../utils";
34

45
const types = ["video", "image", "embed"];
56

@@ -34,9 +35,7 @@ const Asset: React.FC<{ block: BlockType }> = ({ block }) => {
3435
);
3536
}
3637

37-
const src = `https://notion.so/image/${encodeURIComponent(
38-
value.properties.source[0][0]
39-
)}`;
38+
const src = toNotionImageUrl(value.properties.source[0][0]);
4039

4140
if (type === "image") {
4241
const caption = value.properties.caption?.[0][0];

src/components/code.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,8 @@ const Code: React.FC<{ code: string; language: string }> = ({
1010
languages[language.toLowerCase()] || languages.javascript;
1111

1212
return (
13-
<pre>
13+
<pre className="notion-code">
1414
<code
15-
className="notion-code"
1615
dangerouslySetInnerHTML={{
1716
__html: highlight(code, prismLanguage, language)
1817
}}

src/components/page-icon.tsx

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import * as React from "react";
2+
import {
3+
BlockType,
4+
PageValueType,
5+
BlockValueType,
6+
CalloutValueType
7+
} from "../types";
8+
import { toNotionImageUrl, getTextContent, classNames } from "../utils";
9+
10+
const isIconBlock = (
11+
value: BlockValueType
12+
): value is PageValueType | CalloutValueType => {
13+
return value.type === "page" || value.type === "callout";
14+
};
15+
16+
interface AssetProps {
17+
block: BlockType;
18+
big?: boolean;
19+
className?: string;
20+
}
21+
22+
const PageIcon: React.FC<AssetProps> = ({ block, className, big }) => {
23+
if (!isIconBlock(block.value)) {
24+
return null;
25+
}
26+
const icon = block.value.format.page_icon;
27+
const title = block.value.properties?.title;
28+
29+
if (icon?.includes("http")) {
30+
return (
31+
<img
32+
className={classNames(
33+
className,
34+
big ? "notion-page-icon-cover" : "notion-page-icon"
35+
)}
36+
src={toNotionImageUrl(icon)}
37+
alt={title ? getTextContent(title) : "Icon"}
38+
/>
39+
);
40+
} else {
41+
return (
42+
<span
43+
className={classNames(
44+
className,
45+
big ? "notion-page-icon-cover" : "notion-page-icon"
46+
)}
47+
role="image"
48+
aria-label={icon}
49+
>
50+
{icon}
51+
</span>
52+
);
53+
}
54+
};
55+
56+
export default PageIcon;

src/renderer.tsx

Lines changed: 7 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,48 +2,34 @@ import React from "react";
22
import { BlockMapType } from "./types";
33
import { Block, MapPageUrl } from "./block";
44

5-
import { getListNumber } from "./utils";
6-
75
export interface NotionRendererProps {
86
blockMap: BlockMapType;
7+
mapPageUrl?: MapPageUrl;
8+
fullPage?: boolean;
9+
910
currentId?: string;
1011
level?: number;
11-
mapPageUrl?: MapPageUrl;
1212
}
1313

1414
export const NotionRenderer: React.FC<NotionRendererProps> = ({
1515
level = 0,
1616
currentId,
17-
blockMap,
18-
mapPageUrl
17+
...props
1918
}) => {
19+
const { blockMap } = props;
2020
const id = currentId || Object.keys(blockMap)[0];
2121
const currentBlock = blockMap[id];
22-
const parentBlock = blockMap[currentBlock.value.parent_id];
2322

2423
if (!currentBlock) return null;
2524

26-
const listNumber =
27-
currentBlock.value.type === "numbered_list" &&
28-
parentBlock.value.type !== "numbered_list"
29-
? getListNumber(id, blockMap)
30-
: undefined;
31-
3225
return (
33-
<Block
34-
key={id}
35-
level={level}
36-
block={currentBlock}
37-
listNumber={listNumber}
38-
mapPageUrl={mapPageUrl}
39-
>
26+
<Block key={id} level={level} block={currentBlock} {...props}>
4027
{currentBlock?.value?.content?.map(contentId => (
4128
<NotionRenderer
4229
key={contentId}
4330
currentId={contentId}
44-
blockMap={blockMap}
4531
level={level + 1}
46-
mapPageUrl={mapPageUrl}
32+
{...props}
4733
/>
4834
))}
4935
</Block>

0 commit comments

Comments
 (0)