Skip to content

Commit

Permalink
feat: block layout for image
Browse files Browse the repository at this point in the history
  • Loading branch information
alex-guoba committed Feb 3, 2024
1 parent b373b5f commit 322c4a6
Show file tree
Hide file tree
Showing 12 changed files with 315 additions and 106 deletions.
4 changes: 2 additions & 2 deletions app/(blog)/article/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,14 +59,14 @@ export default async function Page({ params }: { params: { slug: string } }) {
const title = page.properties?.Title.title[0].text.content;

return (
<Shell as="article">
<Shell as="article" className="relative flex min-h-screen flex-col">
<PageHeader>
<PageHeaderDescription variant="sm">{formatDate(page.last_edited_time)}</PageHeaderDescription>
<PageHeaderHeading>{title}</PageHeaderHeading>
</PageHeader>
<Separator className="mb-2.5" />

<section className="">
<section className="w-full">
{blocks.map((block: any) => (
<Fragment key={block.id}>{renderBlock(block)}</Fragment>
))}
Expand Down
2 changes: 0 additions & 2 deletions app/(lobby)/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
// import { currentUser } from "@clerk/nextjs"

import { SiteFooter } from "@/components/layouts/site-footer"
import { SiteHeader } from "@/components/layouts/site-header"

// interface LobyLayoutProps
// extends React.PropsWithChildren<{
Expand Down
27 changes: 27 additions & 0 deletions app/notion/_components/callout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { cn } from "@/lib/utils";
import { IconRender } from "./emoji";
import RichText from "../text";

interface CalloutBlockProps {
block: any;
className?: string | undefined;
}

export function CalloutRender({ block, className }: CalloutBlockProps) {
const {id, callout: {icon, rich_text}} = block;
return (
<div id={id}
className={cn(
className,
"inline-flex w-full items-center box-border border rounded-[4px] border-none p-4 bg-stone-100"
)}
>
{icon && (
<IconRender type={icon.type} emoji={icon.emoji} external={icon.external} file={icon.file}/>
)}
<div className="whitespace-pre-wrap break-words ml-3">
<RichText title={rich_text} />
</div>
</div>
);
}
8 changes: 4 additions & 4 deletions app/notion/_components/code.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import React from "react";
// import copyToClipboard from 'clipboard-copy'
// import { CodeBlock } from 'notion-types'
// import { getBlockTitle } from 'notion-utils'
import Prism, { highlightElement } from "prismjs";
import { languages as Lan } from "prismjs";
import { highlightElement } from "prismjs";
// import { languages as Lan } from "prismjs";

// TODO: make it to dynamic
import "prismjs/components/prism-clike.min.js";
Expand Down Expand Up @@ -65,8 +65,6 @@ interface CodeBlockProps {


export function CodeRender({block, defaultLanguage, className} : CodeBlockProps) {


const codeRef = React.useRef<HTMLDivElement>(null);
React.useEffect(() => {
if (codeRef.current) {
Expand All @@ -93,6 +91,8 @@ export function CodeRender({block, defaultLanguage, className} : CodeBlockProps)
code.rich_text?.map((item: any) => {
codes += item?.plain_text
})

// TODO: add copy to clipboard feature
return (
<div id={id} className={cn(className, "text-sm")}>
<pre className="rounded py-8">
Expand Down
70 changes: 70 additions & 0 deletions app/notion/_components/emoji.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// import { cn } from "@/lib/utils";

import { cn } from "@/lib/utils";
import Image from "next/image";


interface IconProps {
type: string;
emoji?: string | undefined;
external?:
| {
url: string;
}
| undefined;
file?:
| {
url: string;
expiry_time: string;
}
| undefined;
}

export function IconRender(
{ type, emoji, external, file }: IconProps,
className?: string | undefined
) {
if (type == "emoji") {
return (
<div
className={cn(className, "self-start w-6 h-6 text-xl leading-[1em]")}
>
{emoji}
</div>
);
}
const url = external?.url || file?.url;
if (!url) {
return null;
}

// TODO: use nextconfig.js to get the base path
const u = new URL(url || "");
if (
u.hostname.endsWith("s3.us-west-2.amazonaws.com") ||
u.hostname.startsWith("www.notion.so")
) {
return (
<Image
src={url}
className={cn("self-start w-6 h-6 text-[1em] leading-[1em]", className)}
width={40}
height={40}
loading="lazy"
decoding="async"
alt="external icon"
/>
);
}

return (
// eslint-disable-next-line @next/next/no-img-element
<img
src={url}
className={cn("self-start w-6 h-6 text-[1em] leading-[1em]", className)}
loading="lazy"
decoding="async"
alt="external icon"
/>
);
}
133 changes: 133 additions & 0 deletions app/notion/_components/image.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
// import { cn } from "@/lib/utils";

import { cn } from "@/lib/utils";
import Image from "next/image";
import RichText from "../text";

// "image": {
// "caption": [
// {
// "type": "text",
// "text": {
// "content": "External Image",
// "link": null
// },
// "annotations": {
// "bold": false,
// "italic": false,
// "strikethrough": false,
// "underline": false,
// "code": false,
// "color": "default"
// },
// "plain_text": "External Image",
// "href": null
// }
// ],
// "type": "external",
// "external": {
// "url": "https://images.unsplash.com/photo-1524995997946-a1c2e315a42f?ixlib=rb-1.2.1&q=85&fm=jpg&crop=entropy&cs=srgb"
// }
// }
// "image": {
// "caption": [
// {
// "type": "text",
// "text": {
// "content": "Notion-hosted image",
// "link": null
// },
// "annotations": {
// "bold": false,
// "italic": false,
// "strikethrough": false,
// "underline": false,
// "code": false,
// "color": "default"
// },
// "plain_text": "Notion-hosted image",
// "href": null
// }
// ],
// "type": "file",
// "file": {
// "url": "https://prod-files-secure.s3.us-west-2.amazonaws.com/0bb5601d-5b11-49c2-a0fd-29f4c2b522b6/d61aedd6-da5f-473d-a08a-da664b6c1456/isaac-martin-Apkr4nfK1mU-unsplash.jpg?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=AKIAT73L2G45HZZMZUHI%2F20240201%2Fus-west-2%2Fs3%2Faws4_request&X-Amz-Date=20240201T112113Z&X-Amz-Expires=3600&X-Amz-Signature=112d428a047b0f58c46e0c1a00278728e3099b737b63d25df5ea09f97f5771f4&X-Amz-SignedHeaders=host&x-id=GetObject",
// "expiry_time": "2024-02-01T12:21:13.031Z"
// }
// }

// interface ImageProps {
// id: string;
// type: string;
// external?:
// | {
// url: string;
// }
// | undefined;
// file?:
// | {
// url: string;
// expiry_time: string;
// }
// | undefined;
// }

export function ImageRender({block, className, size = "md"}: {block: any, className?: string | undefined, size ?: string | undefined}) {
const { id, image: {caption, type, external, file} } = block;

const url = external?.url || file?.url;
if (!url) {
return null;
}

// TODO: use nextconfig.js to get the base path
let hosted = false;
const u = new URL(url || "");
if (
u.hostname.endsWith("s3.us-west-2.amazonaws.com") ||
u.hostname.startsWith("www.notion.so")
) {
hosted = true;
}

// TODO: Notion API don't return the correct image size. May be we need to set the image size in caption?
return (
<figure
key={id}
className={cn(
className,
"max-w-[100vw] min-w-full self-center flex flex-col mx-0 my-2 text-sm "
)}
>
{hosted ? (
<Image
src={url}
className={cn("rounded-lg shadow-md w-fit")}
width={448}
height={448}
sizes="(min-width: 1024px) 384px, (min-width: 768px) 288px, (min-width: 640px) 224px, 100vw"
alt={caption[0]?.plain_text}
/>
) : (
// eslint-disable-next-line @next/next/no-img-element
<img
src={url}
width="auto"
className={cn("rounded-lg shadow-md w-fit", {
"h-32": size === "sxm",
"h-64": size === "md",
"h-96": size === "lg",
})}
loading="lazy"
decoding="async"
alt={caption[0]?.plain_text}
/>
)}
{caption && caption.length > 0 && (
<figcaption className="font-normal">
<RichText title={caption} />
</figcaption>
)}
</figure>
);
}
35 changes: 13 additions & 22 deletions app/notion/render.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,17 @@ import RichText from './text';
import styles from './post.module.css';
import { bulletListStyle, numberListStyle } from './tools';
import { cn } from '@/lib/utils';
import { CodeRender } from './_components/code';
import React from 'react';
import { CodeRender } from './_components/code';
import { CalloutRender } from './_components/callout';
import { ImageRender } from './_components/image';


export function renderBlock(block: any, level: number = 1) {
const { type, id } = block;
const value = block[type];

// console.log(type, id);
console.log(type, id);

switch (type) {
// https://developers.notion.com/reference/block#paragraph
Expand All @@ -23,7 +25,7 @@ export function renderBlock(block: any, level: number = 1) {
// - mention: no rich_text exist
case 'paragraph':
return (
<p id={id} className='mt-1.5'>
<p key={id} id={id} className='mt-1.5'>
<RichText title={value.rich_text} />
</p>
);
Expand Down Expand Up @@ -101,16 +103,9 @@ export function renderBlock(block: any, level: number = 1) {
{block.children.map((child: any) => renderBlock(child, level + 1))}
</div>
);
case 'image': {
const src = value.type === 'external' ? value.external.url : value.file.url;
const caption = value.caption ? value.caption[0]?.plain_text : '';
return (
<figure>
<img src={src} alt={caption} />
{caption && <figcaption>{caption}</figcaption>}
</figure>
);
}

case 'image':
return <ImageRender block={block} className='mt-1.5'></ImageRender>

case 'divider':
return <hr className='mt-1.5 border-gray-200' key={id} />;
Expand All @@ -119,20 +114,12 @@ export function renderBlock(block: any, level: number = 1) {
return <blockquote className='mt-1.5 border-black border-l-4 pl-4 py-1.5 whitespace-pre-wrap' key={id}>{value.rich_text[0].plain_text}</blockquote>;

case 'code':
// return (
// <pre className={styles.pre}>
// <code className={styles.code_block} key={id}>
// {value.rich_text[0].plain_text}
// </code>
// </pre>
// );
return (
<React.Suspense fallback={<div>Loading...</div>}>
<CodeRender block={block} className='mt-1.5'></CodeRender>
</React.Suspense>
)
// <CodeRender block={block}></CodeRender>
// );

case 'file': {
const srcFile = value.type === 'external' ? value.external.url : value.file.url;
const splitSourceArray = srcFile.split('/');
Expand Down Expand Up @@ -190,6 +177,10 @@ export function renderBlock(block: any, level: number = 1) {
case 'column': {
return <div>{block.children.map((child: any) => renderBlock(child, level + 1))}</div>;
}

case 'callout':
return <CalloutRender block={block} className='mt-1.5'></CalloutRender>;

default:
return `❌ Unsupported block (${
type === 'unsupported' ? 'unsupported by Notion API' : type
Expand Down
Loading

0 comments on commit 322c4a6

Please sign in to comment.