├── .env.example ├── .eslintrc.json ├── .gitignore ├── README.md ├── jsconfig.json ├── next.config.mjs ├── package-lock.json ├── package.json ├── postcss.config.js ├── prettier.config.js ├── public ├── favicon.ico ├── favicon2.ico └── fonts │ ├── CalSans-SemiBold.ttf │ ├── CalSans-SemiBold.woff2 │ └── Ubuntu-Bold.ttf ├── src ├── components │ ├── BlogCard.jsx │ ├── Button.jsx │ ├── Card.jsx │ ├── Container.jsx │ ├── Footer.jsx │ ├── FormatDate.jsx │ ├── Header.jsx │ ├── LikeButton.jsx │ ├── PageViews.jsx │ ├── Prose.jsx │ ├── RenderNotion.jsx │ ├── Section.jsx │ └── SimpleLayout.jsx ├── data │ └── projects.js ├── images │ ├── avatar.png │ ├── logo.png │ └── projects │ │ ├── formulator.png │ │ ├── frize.png │ │ ├── shouldreads.png │ │ ├── total_recall.png │ │ ├── trackrBot.png │ │ └── yc.png ├── lib │ ├── generateRssFeed.js │ ├── getArticlePositions.js │ ├── gtag.js │ ├── initSupabase.js │ └── notion.js ├── pages │ ├── _app.jsx │ ├── _document.jsx │ ├── about.jsx │ ├── api │ │ ├── og.js │ │ └── views │ │ │ └── [slug].js │ ├── blog │ │ ├── [slug].jsx │ │ └── index.jsx │ ├── index.jsx │ └── projects.jsx ├── seo.config.js └── styles │ └── tailwind.css └── tailwind.config.js /.env.example: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_SITE_URL=https://example.com 2 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals", 3 | "rules": { 4 | "react/no-unknown-property": "off" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /.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 | .pnpm-debug.log* 27 | 28 | # local env files 29 | .env*.local 30 | .env 31 | 32 | # vercel 33 | .vercel 34 | 35 | # generated files 36 | /public/rss/ 37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Digital Home of Rittik Basu 2 | 3 | ![screenshot of the website](https://ik.imagekit.io/zwcfsadeijm/screenshot-rocks__1__I_Dwm30CG.png?ik-sdk-version=javascript-1.4.3&updatedAt=1672573644499) 4 | 5 | This website is made with Next.js + Tailwind CSS and deployed on Vercel 6 | 7 | ## Features 8 | 9 | - Notion as a CMS for blog posts 10 | - Supabase to keep count of likes and views 11 | - Highlight.js for syntax highlighting 12 | - vercel/og to dynamically generate Open Graph images 13 | - vercel/analytics to track page views 14 | 15 | ## Setup 16 | 17 | - `git clone git@github.com:rittikbasu/website.git` 18 | - `cd website` 19 | - `npm install` 20 | - `npm run dev` 21 | - visit http://localhost:3000 22 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "paths": { 5 | "@/*": ["src/*"] 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | pageExtensions: ['jsx', 'js'], 4 | reactStrictMode: true, 5 | swcMinify: true, 6 | experimental: { 7 | scrollRestoration: true, 8 | }, 9 | transpilePackages: ['react-tweet'], 10 | images: { 11 | remotePatterns: [ 12 | { protocol: 'https', hostname: 'pbs.twimg.com' }, 13 | { protocol: 'https', hostname: 'abs.twimg.com' }, 14 | { protocol: 'https', hostname: 'ik.imagekit.io' }, 15 | { protocol: 'https', hostname: 'images.unsplash.com' }, 16 | { protocol: 'https', hostname: 's3.us-west-2.amazonaws.com' }, 17 | { protocol: 'https', hostname: 'www.notion.so' }, 18 | ], 19 | }, 20 | } 21 | 22 | export default nextConfig 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rittik-website", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "browserslist": "defaults, not ie <= 11", 12 | "dependencies": { 13 | "@headlessui/react": "^1.7.0", 14 | "@notionhq/client": "^2.2.0", 15 | "@supabase/supabase-js": "^2.0.1", 16 | "@tailwindcss/typography": "^0.5.4", 17 | "@vercel/analytics": "^0.1.1", 18 | "@vercel/og": "^0.0.19", 19 | "@vercel/speed-insights": "^1.0.2", 20 | "autoprefixer": "^10.4.7", 21 | "clsx": "^1.2.0", 22 | "eslint": "^7.32.0", 23 | "fast-glob": "^3.2.11", 24 | "feed": "^4.2.2", 25 | "focus-visible": "^5.2.0", 26 | "highlight.js": "^11.6.0", 27 | "next": "^14.0.4", 28 | "next-seo": "^5.5.0", 29 | "postcss-focus-visible": "^6.0.4", 30 | "react": "^18.2.0", 31 | "react-countup": "^6.4.2", 32 | "react-dom": "^18.2.0", 33 | "react-icons": "^4.4.0", 34 | "react-tweet": "^2.0.0", 35 | "react-twitter-embed": "^4.0.4", 36 | "slugify": "^1.6.5", 37 | "swr": "^1.3.0", 38 | "tailwindcss": "^3.1.4" 39 | }, 40 | "devDependencies": { 41 | "@types/react": "18.0.21", 42 | "eslint-config-next": "^14.0.4", 43 | "prettier": "^2.7.1", 44 | "prettier-plugin-tailwindcss": "^0.1.11" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | 'postcss-focus-visible': { 5 | replaceWith: '[data-focus-visible-added]', 6 | }, 7 | autoprefixer: {}, 8 | }, 9 | } 10 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | singleQuote: true, 3 | semi: false, 4 | plugins: [require('prettier-plugin-tailwindcss')], 5 | } 6 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rittikbasu/website/e11112fb5df65209e5bc54cb8a19dec71a648337/public/favicon.ico -------------------------------------------------------------------------------- /public/favicon2.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rittikbasu/website/e11112fb5df65209e5bc54cb8a19dec71a648337/public/favicon2.ico -------------------------------------------------------------------------------- /public/fonts/CalSans-SemiBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rittikbasu/website/e11112fb5df65209e5bc54cb8a19dec71a648337/public/fonts/CalSans-SemiBold.ttf -------------------------------------------------------------------------------- /public/fonts/CalSans-SemiBold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rittikbasu/website/e11112fb5df65209e5bc54cb8a19dec71a648337/public/fonts/CalSans-SemiBold.woff2 -------------------------------------------------------------------------------- /public/fonts/Ubuntu-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rittikbasu/website/e11112fb5df65209e5bc54cb8a19dec71a648337/public/fonts/Ubuntu-Bold.ttf -------------------------------------------------------------------------------- /src/components/BlogCard.jsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react' 2 | import Image from 'next/image' 3 | import Link from 'next/link' 4 | import clsx from 'clsx' 5 | import slugify from 'slugify' 6 | 7 | import { Text } from '@/components/RenderNotion' 8 | 9 | import { AiOutlineEye } from 'react-icons/ai' 10 | import { GoBook } from 'react-icons/go' 11 | import CountUp from 'react-countup' 12 | 13 | export function BlogCard({ article }) { 14 | const articleTitle = article.properties?.name.title[0].plain_text 15 | const articleDescription = article.properties.description?.rich_text 16 | const [status, setStatus] = useState(article.properties.Status?.status?.name) 17 | const fixedStatus = article.properties.Status?.status?.name 18 | const slug = slugify(articleTitle, { strict: true, lower: true }) 19 | const wordCount = article.properties.wordCount.number 20 | const readingTime = Math.ceil(wordCount === null ? 0 : wordCount / 265) 21 | const published = article.properties.publish.checkbox 22 | const views = article.pageViews 23 | const coverImgFn = () => { 24 | if (article.cover) { 25 | const imgType = article.cover.type 26 | const image = 27 | imgType === 'external' 28 | ? article.cover.external.url 29 | : article.cover.file.url 30 | return image 31 | } else { 32 | return false 33 | } 34 | } 35 | 36 | const coverImg = coverImgFn() 37 | 38 | const [isLoading, setLoading] = useState(true) 39 | const [statusBg, setStatusBg] = useState('bg-indigo-500/90') 40 | 41 | const handleClick = (e) => { 42 | if (status !== '🌱 Seedling') return 43 | e.preventDefault() 44 | setStatus('✍🏾 In Progress') 45 | setStatusBg('bg-pink-600/80 dark:bg-pink-500/80 duration-[5000ms]') 46 | setTimeout(() => { 47 | setStatus(article.properties.Status?.status?.name) 48 | setStatusBg('bg-indigo-500/90 duration-[3000ms]') 49 | }, 3000) 50 | } 51 | 52 | const ArticleWrapper = fixedStatus === '🌱 Seedling' ? 'div' : Link 53 | const linkProps = 54 | fixedStatus === '🌱 Seedling' ? {} : { href: '/blog/' + slug } 55 | return ( 56 |
64 |
71 | 72 | {status} 73 | 74 |
75 | 84 | {!!coverImg && ( 85 |
86 | {'Cover setLoading(false)} 96 | placeholder="blur" 97 | blurDataURL={coverImg} 98 | /> 99 |
100 | )} 101 |

102 |
107 | {articleTitle} 108 |
109 |

110 |

111 | 112 |

113 | {fixedStatus !== '🌱 Seedling' && ( 114 |
115 | 116 | 117 | 118 | 119 | 120 | 121 | {readingTime} min read 122 | 123 |
124 | )} 125 |
126 |
127 | ) 128 | } 129 | -------------------------------------------------------------------------------- /src/components/Button.jsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link' 2 | import clsx from 'clsx' 3 | 4 | const variantStyles = { 5 | primary: 6 | 'bg-white font-semibold text-zinc-700 md:hover:bg-indigo-300 active:bg-indigo-400 active:text-zinc-700/70 dark:bg-zinc-900 dark:text-zinc-200 dark:md:hover:bg-indigo-800/70 dark:active:bg-indigo-900 dark:active:text-zinc-200/70', 7 | secondary: 8 | 'bg-zinc-100 font-medium text-zinc-900 active:bg-zinc-200 active:text-zinc-900/60 dark:bg-zinc-800/50 dark:text-zinc-300 dark:md:hover:bg-zinc-800 dark:md:hover:text-zinc-50 dark:active:bg-zinc-800/50 dark:active:text-zinc-50/70', 9 | } 10 | 11 | export function Button({ className, href, ...props }) { 12 | className = clsx( 13 | 'group relative mx-auto inline-flex items-center overflow-hidden rounded-full bg-zinc-300 px-6 transition dark:bg-zinc-800', 14 | className 15 | ) 16 | 17 | return href ? ( 18 | 19 |
20 |
21 |
22 | 23 |
24 | 25 |
26 | 27 | 28 | {props.children} 29 | 30 | 31 | ) : ( 32 | 45 | ) 46 | } 47 | -------------------------------------------------------------------------------- /src/components/Card.jsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link' 2 | import clsx from 'clsx' 3 | 4 | export function Card({ as: Component = 'div', className, children }) { 5 | return ( 6 | 9 | {children} 10 | 11 | ) 12 | } 13 | 14 | Card.Link = function CardLink({ children, className, ...props }) { 15 | return ( 16 |
17 |
18 | 19 | 20 | {children} 21 | 22 |
23 | ) 24 | } 25 | 26 | Card.Title = function CardTitle({ as: Component = 'h2', href, children }) { 27 | return ( 28 | 29 | {href ? {children} : children} 30 | 31 | ) 32 | } 33 | 34 | Card.Description = function CardDescription({ children }) { 35 | return ( 36 |

37 | {children} 38 |

39 | ) 40 | } 41 | 42 | Card.Cta = function CardCta({ children }) { 43 | return ( 44 | 50 | ) 51 | } 52 | 53 | Card.Eyebrow = function CardEyebrow({ 54 | as: Component = 'p', 55 | decorate = false, 56 | className, 57 | children, 58 | ...props 59 | }) { 60 | return ( 61 | 69 | {decorate && ( 70 | 79 | ) 80 | } 81 | -------------------------------------------------------------------------------- /src/components/Container.jsx: -------------------------------------------------------------------------------- 1 | import { forwardRef } from 'react' 2 | import clsx from 'clsx' 3 | 4 | const OuterContainer = forwardRef(function OuterContainer( 5 | { className, children, ...props }, 6 | ref 7 | ) { 8 | return ( 9 |
10 |
{children}
11 |
12 | ) 13 | }) 14 | 15 | const InnerContainer = forwardRef(function InnerContainer( 16 | { className, children, ...props }, 17 | ref 18 | ) { 19 | return ( 20 |
25 |
{children}
26 |
27 | ) 28 | }) 29 | 30 | export const Container = forwardRef(function Container( 31 | { children, ...props }, 32 | ref 33 | ) { 34 | return ( 35 | 36 | {children} 37 | 38 | ) 39 | }) 40 | 41 | Container.Outer = OuterContainer 42 | Container.Inner = InnerContainer 43 | -------------------------------------------------------------------------------- /src/components/Footer.jsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link' 2 | import clsx from 'clsx' 3 | 4 | import { BsTwitter, BsGithub } from 'react-icons/bs' 5 | import { FaLinkedinIn } from 'react-icons/fa' 6 | import { MdEmail } from 'react-icons/md' 7 | 8 | import { Container } from '@/components/Container' 9 | 10 | function NavLink({ href, children }) { 11 | return ( 12 | 16 | {children} 17 | 18 | ) 19 | } 20 | 21 | function SocialLinkMobile({ className, icon: Icon, ...props }) { 22 | return ( 23 | 24 | 30 | 31 | ) 32 | } 33 | 34 | export function Footer() { 35 | return ( 36 |
37 | 38 |
39 | 40 |
41 |
42 | Twitter 43 | GitHub 44 | 45 | LinkedIn 46 | 47 | Mail 48 |
49 |
50 | 55 | 60 | 65 | 71 |
72 |

73 | © {new Date().getFullYear()} Rittik Basu. All rights 74 | reserved. 75 |

76 |
77 |
78 |
79 |
80 |
81 | ) 82 | } 83 | -------------------------------------------------------------------------------- /src/components/FormatDate.jsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react' 2 | 3 | export const FormatDate = (date) => { 4 | const [formattedDate, setFormattedDate] = useState(null) 5 | 6 | useEffect( 7 | () => 8 | setFormattedDate( 9 | new Date(date).toLocaleDateString('en-US', { 10 | month: 'short', 11 | day: '2-digit', 12 | year: 'numeric', 13 | }) 14 | ), 15 | [date] 16 | ) 17 | 18 | return formattedDate 19 | } 20 | -------------------------------------------------------------------------------- /src/components/Header.jsx: -------------------------------------------------------------------------------- 1 | import Image from 'next/image' 2 | import Link from 'next/link' 3 | import { useRouter } from 'next/router' 4 | import { Popover, Transition } from '@headlessui/react' 5 | import clsx from 'clsx' 6 | 7 | import { Container } from '@/components/Container' 8 | import avatarImage from '@/images/logo.png' 9 | import { Fragment, useEffect, useRef } from 'react' 10 | 11 | import { BsSun, BsMoon, BsChevronDown } from 'react-icons/bs' 12 | 13 | function MobileNavItem({ href, children }) { 14 | return ( 15 |
  • 16 | 17 | {children} 18 | 19 |
  • 20 | ) 21 | } 22 | 23 | function MobileNavigation(props) { 24 | return ( 25 | 26 | 27 | Menu 28 | 29 | 30 | 31 | 40 | 41 | 42 | 51 | 55 | 66 | 67 | 68 | 69 | 70 | ) 71 | } 72 | 73 | function NavItem({ href, children }) { 74 | let isActive = useRouter().pathname === href 75 | 76 | return ( 77 |
  • 78 | 87 | {children} 88 | {isActive && ( 89 | 90 | )} 91 | 92 |
  • 93 | ) 94 | } 95 | 96 | function DesktopNavigation(props) { 97 | return ( 98 | 106 | ) 107 | } 108 | 109 | function ModeToggle() { 110 | function disableTransitionsTemporarily() { 111 | document.documentElement.classList.add('[&_*]:!transition-none') 112 | window.setTimeout(() => { 113 | document.documentElement.classList.remove('[&_*]:!transition-none') 114 | }, 0) 115 | } 116 | 117 | function toggleMode() { 118 | disableTransitionsTemporarily() 119 | 120 | let darkModeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)') 121 | let isSystemDarkMode = darkModeMediaQuery.matches 122 | let isDarkMode = document.documentElement.classList.toggle('dark') 123 | 124 | if (isDarkMode === isSystemDarkMode) { 125 | delete window.localStorage.isDarkMode 126 | } else { 127 | window.localStorage.isDarkMode = isDarkMode 128 | } 129 | } 130 | 131 | return ( 132 | 141 | ) 142 | } 143 | 144 | function clamp(number, a, b) { 145 | let min = Math.min(a, b) 146 | let max = Math.max(a, b) 147 | return Math.min(Math.max(number, min), max) 148 | } 149 | 150 | function AvatarContainer({ className, ...props }) { 151 | return ( 152 |
    159 | ) 160 | } 161 | 162 | function Avatar({ className, ...props }) { 163 | return ( 164 | 170 |
    171 | Avatar Image of Rittik Basu 178 |
    179 | 180 | ) 181 | } 182 | 183 | export function Header({ previousPathname }) { 184 | let isHomePage = useRouter().pathname === '/' 185 | 186 | let headerRef = useRef() 187 | let avatarRef = useRef() 188 | let isInitial = useRef(true) 189 | 190 | useEffect(() => { 191 | let downDelay = avatarRef.current?.offsetTop ?? 0 192 | let upDelay = 64 193 | 194 | function setProperty(property, value) { 195 | document.documentElement.style.setProperty(property, value) 196 | } 197 | 198 | function removeProperty(property) { 199 | document.documentElement.style.removeProperty(property) 200 | } 201 | 202 | function updateHeaderStyles() { 203 | let { top, height } = headerRef.current.getBoundingClientRect() 204 | let scrollY = clamp( 205 | window.scrollY, 206 | 0, 207 | document.body.scrollHeight - window.innerHeight 208 | ) 209 | 210 | if (isInitial.current) { 211 | setProperty('--header-position', 'sticky') 212 | } 213 | 214 | setProperty('--content-offset', `${downDelay}px`) 215 | 216 | if (isInitial.current || scrollY < downDelay) { 217 | setProperty('--header-height', `${downDelay + height}px`) 218 | setProperty('--header-mb', `${-downDelay}px`) 219 | } else if (top + height < -upDelay) { 220 | let offset = Math.max(height, scrollY - upDelay) 221 | setProperty('--header-height', `${offset}px`) 222 | setProperty('--header-mb', `${height - offset}px`) 223 | } else if (top === 0) { 224 | setProperty('--header-height', `${scrollY + height}px`) 225 | setProperty('--header-mb', `${-scrollY}px`) 226 | } 227 | 228 | if (top === 0 && scrollY > 0 && scrollY >= downDelay) { 229 | setProperty('--header-inner-position', 'fixed') 230 | removeProperty('--header-top') 231 | } else { 232 | removeProperty('--header-inner-position') 233 | setProperty('--header-top', '0px') 234 | } 235 | } 236 | 237 | function updateStyles() { 238 | updateHeaderStyles() 239 | isInitial.current = false 240 | } 241 | 242 | updateStyles() 243 | window.addEventListener('scroll', updateStyles, { passive: true }) 244 | 245 | return () => { 246 | window.removeEventListener('scroll', updateStyles, { passive: true }) 247 | } 248 | }, [isHomePage]) 249 | 250 | return ( 251 | <> 252 |
    263 |
    268 | 272 |
    273 |
    274 | 275 | 276 | 277 |
    278 |
    279 | 280 | 281 |
    282 |
    283 |
    284 | 285 |
    286 |
    287 |
    288 |
    289 |
    290 |
    291 | {isHomePage &&
    } 292 | 293 | ) 294 | } 295 | -------------------------------------------------------------------------------- /src/components/LikeButton.jsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react' 2 | import clsx from 'clsx' 3 | import { SupabaseClient } from '@/lib/initSupabase' 4 | import { BsHeartFill } from 'react-icons/bs' 5 | 6 | export function LikeBtn({ slug }) { 7 | const [isLiked, setLiked] = useState(false) 8 | const [likes, setLikes] = useState() 9 | const [likeLimit, setLikeLimit] = useState(0) 10 | 11 | useEffect(() => { 12 | const getLikes = async () => { 13 | const { data } = await SupabaseClient.from('analytics') 14 | .select('likes') 15 | .filter('slug', 'eq', slug) 16 | setLikes(data[0]?.likes) 17 | } 18 | 19 | getLikes() 20 | }, [slug]) 21 | 22 | const updateLikes = async () => { 23 | setLikeLimit(likeLimit + 1) 24 | if (likeLimit < 5) { 25 | setLiked(true) 26 | const { data } = await SupabaseClient.rpc('increment_likes', { 27 | page_slug: slug, 28 | }) 29 | setLikes((prevLikes) => prevLikes + 1) 30 | setTimeout(() => setLiked(false), 1000) 31 | } 32 | } 33 | return ( 34 |
    35 | 71 |
    72 | ) 73 | } 74 | -------------------------------------------------------------------------------- /src/components/PageViews.jsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react' 2 | import useSWR from 'swr' 3 | 4 | async function fetcher(...args) { 5 | const res = await fetch(...args) 6 | return res.json() 7 | } 8 | 9 | export function PageViews({ slug }) { 10 | const { data } = useSWR(`/api/views/${slug}`, fetcher) 11 | const views = new Number(data?.total) 12 | 13 | return views >= 0 ? views.toLocaleString() : 0 14 | } 15 | 16 | export function UpdateViews(slug) { 17 | useEffect(() => { 18 | const registerView = () => 19 | fetch(`/api/views/${slug}`, { 20 | method: 'POST', 21 | }) 22 | 23 | registerView() 24 | }, [slug]) 25 | } 26 | -------------------------------------------------------------------------------- /src/components/Prose.jsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx' 2 | 3 | export function Prose({ children, className }) { 4 | return ( 5 |
    {children}
    6 | ) 7 | } 8 | -------------------------------------------------------------------------------- /src/components/RenderNotion.jsx: -------------------------------------------------------------------------------- 1 | import { Fragment, useState } from 'react' 2 | import Link from 'next/link' 3 | import Image from 'next/image' 4 | 5 | import clsx from 'clsx' 6 | import { Tweet } from 'react-tweet' 7 | 8 | import hljs from 'highlight.js/lib/core' 9 | // import individual languages 10 | import javascript from 'highlight.js/lib/languages/javascript' 11 | hljs.registerLanguage('javascript', javascript) 12 | import typescript from 'highlight.js/lib/languages/typescript' 13 | hljs.registerLanguage('typescript', typescript) 14 | import python from 'highlight.js/lib/languages/python' 15 | hljs.registerLanguage('python', python) 16 | import html from 'highlight.js/lib/languages/xml' 17 | hljs.registerLanguage('html', html) 18 | import plaintext from 'highlight.js/lib/languages/plaintext' 19 | hljs.registerLanguage('plaintext', plaintext) 20 | 21 | export const Text = ({ text, className }) => { 22 | if (!text) { 23 | return null 24 | } 25 | return text.map((value, index) => { 26 | const { 27 | annotations: { bold, code, color, italic, strikethrough, underline }, 28 | text, 29 | } = value 30 | return ( 31 | 43 | {text.link ? ( 44 | {text.content} 45 | ) : code ? ( 46 | {text.content} 47 | ) : ( 48 | text.content 49 | )} 50 | 51 | ) 52 | }) 53 | } 54 | 55 | const components = { 56 | AvatarImg: (props) => , 57 | MediaImg: (props) => , 58 | } 59 | 60 | const Embed = (value, type) => { 61 | let src 62 | const [isLoading, setLoading] = useState(true) 63 | try { 64 | src = value.type === 'external' ? value.external.url : value.file.url 65 | } catch { 66 | src = value.url 67 | } 68 | const caption = value.caption ? value.caption[0]?.plain_text : '' 69 | if (src.startsWith('https://twitter.com')) { 70 | const tweetId = src.match(/status\/(\d+)/)[1] 71 | return ( 72 |
    73 | 74 |
    75 | ) 76 | } else if (src.startsWith('https://www.youtube.com')) { 77 | src = src.replace('watch?v=', 'embed/') 78 | return ( 79 |