diff --git a/lib/markdown.ts b/lib/markdown.ts new file mode 100644 index 0000000..0544cc7 --- /dev/null +++ b/lib/markdown.ts @@ -0,0 +1,80 @@ +const renderNestedListMarkdown = (block:any) => { + const { type } = block; + const value = block[type]; + + if (!value) return ""; + + const isNumberedList = value.children?.type === "numbered_list_item"; + const listType = isNumberedList ? "ol" : "ul"; + + return value.children?.map((child:any) => { + const listItemText = child.numbered_list_item || child.bulleted_list_item; + if (listItemText) { + return ` - ${convertToMarkdown(listItemText.rich_text)}\n${convertToMarkdownNew(child)}`; + } + return ""; + }) + .join(""); + }; + + function convertToMarkdown(richText:any) { + return richText.map((textObj:any) => textObj.text.content).join(''); + } + + export const convertToMarkdownNew = (block:any) => { + const { type } = block; + const value = block[type]; + + switch (type) { + case "paragraph": + return `${convertToMarkdown(value.rich_text)}\n\n`; + case "heading_1": + return `# ${convertToMarkdown(value.rich_text)}\n\n`; + case "heading_2": + return `## ${convertToMarkdown(value.rich_text)}\n\n`; + case "heading_3": + return `### ${convertToMarkdown(value.rich_text)}\n\n`; + case "bulleted_list": + case "numbered_list": + return value.children?.map((child:any) => convertToMarkdownNew(child)) + .join(""); + case "bulleted_list_item": + case "numbered_list_item": + return `- ${convertToMarkdown(value.rich_text)}\n${renderNestedListMarkdown(block)}`; + case "to_do": + return `- [${value.checked ? "x" : " "}] ${convertToMarkdown(value.rich_text)}\n`; + case "toggle": + return `**${convertToMarkdown(value.rich_text)}**\n${block.children?.map((child:any) => convertToMarkdownNew(child)) + .join("")}`; + case "child_page": + return `**${value.title}**\n${block.children?.map((child:any) => convertToMarkdownNew(child)) + .join("")}`; + case "image": + const src = value.type === "external" ? value.external.url : value.file.url; + const caption = value.caption ? value.caption[0]?.plain_text : ""; + return `![${caption}](${src})\n\n`; + case "divider": + return "---\n\n"; + case "quote": + return `> ${convertToMarkdown(value.rich_text)}\n\n`; + case "code": + return `\`\`\`${value.language}\n` + convertToMarkdown(value.rich_text) + "\n```\n\n"; + case "file": + const srcFile = value.type === "external" ? value.external.url : value.file.url; + const captionFile = value.caption ? value.caption[0]?.plain_text : ""; + return `[${captionFile}](${srcFile})\n\n`; + case "bookmark": + return `[${value.url}](${value.url})\n\n`; + case "table": + return value.children?.map((child:any) => convertToMarkdownNew(child)) + .join(""); + case "column_list": + return block.children?.map((child:any) => convertToMarkdownNew(child)) + .join(""); + case "column": + return block.children?.map((child:any) => convertToMarkdownNew(child)) + .join(""); + default: + return `❌ Unsupported block (${type === "unsupported" ? "unsupported by Notion API" : type})\n\n`; + } + }; \ No newline at end of file diff --git a/lib/notion.ts b/lib/notion.ts index 725a03c..0652e09 100644 --- a/lib/notion.ts +++ b/lib/notion.ts @@ -1,6 +1,6 @@ import { Client } from "@notionhq/client"; -const notion = new Client({ +export const notion = new Client({ auth: process.env.NOTION_TOKEN, }); @@ -16,6 +16,13 @@ export const getPage = async (pageId : string) => { return response; }; +export const getBlocksPage = async (linkedPageId: string) => { + const response = await notion.blocks.children.list({ + block_id: linkedPageId, + }); + return response; +} + export const getBlocks = async (blockId : string) => { blockId = blockId.replaceAll("-", ""); @@ -68,4 +75,61 @@ function getRandomInt(min : number, max : number) { min = Math.ceil(min); max = Math.floor(max); return Math.floor(Math.random() * (max - min + 1)) + min; +} + +interface DevToPost { + title: string; + content: string; + tags: string; + coverImageUrl: string; + id?: string; +}; + +export const createDevToBlog = async ({title, content, tags, coverImageUrl,id}:DevToPost) => { + try { + if(id){ + const response = await fetch(`https://dev.to/api/articles/${id}`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + 'api-key': `${process.env.DEV_TO_API_KEY}`, + }, + body: JSON.stringify({ + article: { + title: title, + published: true, + body_markdown: content, + tags: tags, + coverImageUrl: coverImageUrl, + series: 'Notion To Dev To' + }, + }), + }) + const post = await response.json() + return post + } else { + const response = await fetch(`https://dev.to/api/articles`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'api-key': `${process.env.DEV_TO_API_KEY}`, + }, + body: JSON.stringify({ + article: { + title: title, + published: true, + body_markdown: content, + tags: tags, + coverImageUrl: coverImageUrl, + series: 'Notion To Dev To' + }, + }), + }) + const post = await response.json(); + return post + } + } catch(err:any){ + console.error('Error creating DEV.to blog post:', err.message); + throw new Error('Failed to create DEV.to blog post with code :', err.response.status); + } } \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 5c06b69..b046bf1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "@types/react": "18.2.15", "@types/react-dom": "18.2.7", "autoprefixer": "10.4.14", + "axios": "^1.4.0", "eslint": "8.45.0", "eslint-config-next": "13.4.12", "next": "13.4.12", @@ -842,6 +843,29 @@ "node": ">=4" } }, + "node_modules/axios": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.4.0.tgz", + "integrity": "sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA==", + "dependencies": { + "follow-redirects": "^1.15.0", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/axios/node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/axobject-query": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-3.2.1.tgz", @@ -1994,6 +2018,25 @@ "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.7.tgz", "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==" }, + "node_modules/follow-redirects": { + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", + "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, "node_modules/for-each": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", @@ -3600,6 +3643,11 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, "node_modules/punycode": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", diff --git a/pages/api/post/[pid].ts b/pages/api/post/[pid].ts new file mode 100644 index 0000000..1d54961 --- /dev/null +++ b/pages/api/post/[pid].ts @@ -0,0 +1,56 @@ +// Next.js API route support: https://nextjs.org/docs/api-routes/introduction +import type { NextApiRequest, NextApiResponse } from 'next' +import { getPage, createDevToBlog, getBlocksPage } from '@/lib/notion' +import { convertToMarkdownNew } from "@/lib/markdown"; + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse +) { + if (req.method === 'GET') { + try{ + const formattedResponse = []; + const {pid} = req.query; + const linkedPageResponse = await getBlocksPage(pid as string); + const linkedPage = await getPage(pid as string); + const contentMarkdown = linkedPageResponse.results.map((block: any) => { + convertToMarkdownNew(block).join("\n"); + }); + formattedResponse.push({ + title: linkedPage.properties.Name.title[0].plain_text, + content: contentMarkdown, + coverImageUrl: linkedPage.cover.external.url, + tags: linkedPage.properties['Tags'].multi_select.map((tag : any) => tag.name) + }); + const response_publish = createDevToBlog(formattedResponse[0]); + res.status(200).json({message:"Success",post:formattedResponse, details: response_publish}); + } catch (error:any) { + res.status(500).json({message:"Error",error}); + } + } else if (req.method === 'PUT') { + try { + const formattedResponse = []; + const {pid} = req.query; + const linkedPageResponse = await getBlocksPage(pid as string); + const linkedPage = await getPage(pid as string); + const contentMarkdown = linkedPageResponse.results.map((block: any) => { + convertToMarkdownNew(block).join("\n"); + }); + formattedResponse.push({ + title: linkedPage.properties.Name.title[0].plain_text, + content: contentMarkdown, + coverImageUrl: linkedPage.cover.external.url, + tags: linkedPage.properties['Tags'].multi_select.map((tag : any) => tag.name), + id: req.body?.id + }); + const response_publish = createDevToBlog(formattedResponse[0]); + res.status(200).json({message:"Success",post:formattedResponse, details: response_publish}); + } catch (error:any) { + res.status(500).json({message:"Error",error}); + } + } else { + res.status(405).json({message:"Method not allowed"}); + } + + res.status(200).json({ name: 'John Doe' }) +} \ No newline at end of file diff --git a/pages/blog/[id]/index.tsx b/pages/blog/[id]/index.tsx index 6c3c8ec..872854e 100644 --- a/pages/blog/[id]/index.tsx +++ b/pages/blog/[id]/index.tsx @@ -13,6 +13,7 @@ import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; import { oneDark } from 'react-syntax-highlighter/dist/cjs/styles/prism' import { CopyToClipboard } from 'react-copy-to-clipboard'; import Code from "@/components/Code"; +import notion from "../../../lib/notion"; const renderNestedList = (block: { [x: string]: any; type?: any; }) => { const { type } = block; @@ -252,6 +253,52 @@ export const getStaticProps: GetStaticProps = async (context: any) => { const page = await getPage(id); const blocks = await getBlocks(id); + const { href: currentUrl, pathname } = useUrl() ?? {}; + + if(page.properties.DevToPublish.checkbox === true) { + + try{ + const ID:number = page.properties.DevToId.number; + + console.log(`${ID?ID:id}`); + + const apiUrl = `https://dev.to/api/articles/${ID?ID:id}`; + + console.log(apiUrl); + + const response = await fetch(apiUrl); + + const post = await response.json() + + console.log(post); + + if(post.status === 200) { + const data = post.data; + await fetch(`${currentUrl}/api/post/${id}`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(data), + }); + } else { + const details = await fetch(`${currentUrl}/api/post/${id}`); + const FullDetails = await details.json() + const publish_DevTo_Id = await notion.pages.update({ + page_id: id, + properties: { + 'DevToID' : { + number: parseInt(FullDetails.details.id) + }, + } + }); + } + } catch(error:any) { + console.error('Some unexpected error occured: ', error.message); + throw new Error('Failed to create blog post with code: ', error.response.status); + } +} + return { props: { page,