├── .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 | 
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 | 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 |
22 |
23 |
24 |
25 |
26 |
27 |
28 | {props.children}
29 |
30 |
31 | ) : (
32 |
33 |
36 |
37 |
38 |
39 |
40 |
41 |
42 | {props.children}
43 |
44 |
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 |
48 | {children}
49 |
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 |
74 |
75 |
76 | )}
77 | {children}
78 |
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 |
12 | )
13 | })
14 |
15 | const InnerContainer = forwardRef(function InnerContainer(
16 | { className, children, ...props },
17 | ref
18 | ) {
19 | return (
20 |
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 |
56 |
57 | Home
58 | About
59 | Blog
60 | Projects
61 |
62 | Close
63 |
64 |
65 |
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 |
99 |
100 | Home
101 | About
102 | Blog
103 | Projects
104 |
105 |
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 |
138 |
139 |
140 |
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 |
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 |
278 |
279 |
280 |
281 |
282 |
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 |
= 5
38 | ? 'py-2 duration-1000 md:transition-all md:duration-[2000ms]'
39 | : 'group relative mx-auto inline-flex w-32 items-center justify-center overflow-hidden rounded-full bg-zinc-200 px-8 py-2 transition dark:bg-zinc-800'
40 | )}
41 | onClick={updateLikes}
42 | disabled={likeLimit >= 5 || isLiked}
43 | >
44 | {likeLimit < 5 && (
45 | <>
46 |
49 |
50 |
51 |
52 |
53 | >
54 | )}
55 |
56 | = 5
62 | ? 'fill-indigo-400 duration-1000 dark:fill-indigo-500'
63 | : 'fill-red-500 duration-500 dark:fill-red-600'
64 | )}
65 | />
66 |
67 | {likes}
68 |
69 |
70 |
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 |
84 | )
85 | } else if (type === 'image') {
86 | return (
87 | <>
88 |
89 | setLoading(false)}
99 | />
100 |
101 | {caption && {caption} }
102 | >
103 | )
104 | } else {
105 | return Not supported
106 | }
107 | }
108 |
109 | const renderNestedList = (block) => {
110 | const { type } = block
111 | const value = block[type]
112 | if (!value) return null
113 |
114 | const isNumberedList = value.children[0].type === 'numbered_list_item'
115 |
116 | if (isNumberedList) {
117 | return {value.children.map((block) => renderBlock(block))}
118 | }
119 | return {value.children.map((block) => renderBlock(block))}
120 | }
121 |
122 | export const renderBlock = (block) => {
123 | const { type, id } = block
124 | const value = block[type]
125 |
126 | switch (type) {
127 | case 'paragraph':
128 | return (
129 |
130 |
131 |
132 | )
133 | case 'heading_1':
134 | return (
135 |
136 |
137 |
138 | )
139 | case 'heading_2':
140 | return (
141 |
142 |
143 |
144 | )
145 | case 'heading_3':
146 | return (
147 |
148 |
149 |
150 | )
151 | case 'bulleted_list_item':
152 | return (
153 |
154 |
155 |
156 | {!!value.children && renderNestedList(block)}
157 |
158 |
159 | )
160 | case 'numbered_list_item':
161 | return (
162 |
163 |
164 |
165 | {!!value.children && renderNestedList(block)}
166 |
167 |
168 | )
169 | case 'to_do':
170 | return (
171 |
172 |
173 | {' '}
174 |
175 |
176 |
177 | )
178 | case 'toggle':
179 | return (
180 |
181 |
182 |
183 |
184 | {value.children?.map((block) => (
185 | {renderBlock(block)}
186 | ))}
187 |
188 | )
189 | case 'child_page':
190 | return {value.title}
191 | case 'image':
192 | case 'video':
193 | case 'embed':
194 | return {Embed(value, type)}
195 | case 'divider':
196 | return
197 | case 'quote':
198 | return {value.rich_text[0].plain_text}
199 | case 'code':
200 | const language = value.language.replace(' ', '').toLowerCase()
201 | const code = value.rich_text[0].plain_text
202 | let codeHighlight
203 | try {
204 | codeHighlight = hljs.highlight(code, {
205 | language: language,
206 | }).value
207 | } catch (err) {
208 | codeHighlight = hljs.highlight(code, {
209 | language: 'plaintext',
210 | }).value
211 | }
212 | return (
213 |
214 |
218 |
219 | )
220 | case 'pdf':
221 | case 'file':
222 | const src_file =
223 | value.type === 'external' ? value.external.url : value.file.url
224 | const splitSourceArray = src_file.split('/')
225 | const lastElementInArray = splitSourceArray[splitSourceArray.length - 1]
226 | const caption_file = value.caption ? value.caption[0]?.plain_text : ''
227 | return (
228 |
229 |
230 | 📎{' '}
231 |
232 | {lastElementInArray.split('?')[0]}
233 |
234 |
235 | {caption_file && {caption_file} }
236 |
237 | )
238 | // TODO: support table block
239 | // case "table":
240 | // console.log(value.children);
241 | // return (
242 | //
243 | //
244 | //
245 | // );
246 | default:
247 | return `❌ Unsupported block (${
248 | type === 'unsupported' ? 'unsupported by Notion API' : type
249 | })`
250 | }
251 | }
252 |
--------------------------------------------------------------------------------
/src/components/Section.jsx:
--------------------------------------------------------------------------------
1 | import { useId } from 'react'
2 |
3 | export function Section({ title, children }) {
4 | let id = useId()
5 |
6 | return (
7 |
11 |
12 |
16 | {title}
17 |
18 |
{children}
19 |
20 |
21 | )
22 | }
23 |
--------------------------------------------------------------------------------
/src/components/SimpleLayout.jsx:
--------------------------------------------------------------------------------
1 | import { Container } from '@/components/Container'
2 |
3 | export function SimpleLayout({ title, intro, preTitle, postTitle, children }) {
4 | const gradientClasses =
5 | 'animate-gradient bg-gradient-to-r from-purple-500 via-indigo-500 to-pink-500 bg-clip-text text-transparent dark:from-purple-400 dark:via-indigo-400 dark:to-pink-400 animate-gradient bg-gradient-to-r from-purple-500 via-indigo-500 to-pink-500 bg-clip-text text-transparent dark:from-purple-400 dark:via-indigo-400 dark:to-pink-400'
6 | return (
7 |
8 |
9 |
12 | {' '}
13 | {preTitle && {preTitle} }{' '}
14 | {title}{' '}
15 | {postTitle && {postTitle} }{' '}
16 |
17 |
18 | {intro}
19 |
20 |
21 | {children}
22 |
23 | )
24 | }
25 |
--------------------------------------------------------------------------------
/src/data/projects.js:
--------------------------------------------------------------------------------
1 | import trackrBot from '@/images/projects/trackrBot.png'
2 | import yc from '@/images/projects/yc.png'
3 | import frize from '@/images/projects/frize.png'
4 | import formulator from '@/images/projects/formulator.png'
5 | import shouldreads from '@/images/projects/shouldreads.png'
6 | import total_recall from '@/images/projects/total_recall.png'
7 |
8 | const data = [
9 | {
10 | title: 'Total Recall',
11 | description:
12 | 'The smartest way to recall, summarise or chat with a YouTube video. Just paste the link, hit enter, and ask questions about the video in seconds and get lightning fast answers.',
13 | techUsed: ['Next.js', 'Tailwind', 'Supabase', 'Groq', 'AWS Lambda'],
14 | image: total_recall,
15 | link: 'https://totalrecall.rittik.io',
16 | },
17 | {
18 | title: 'Formulator',
19 | description:
20 | "A Formula 1 client that keeps you updated with all the stats, scores, and standings in a beautifully designed UI that's as fast as your favourite team on race day!",
21 | techUsed: ['Next.js', 'Tailwind', 'OpenF1', 'Ergast'],
22 | image: formulator,
23 | link: 'https://formu1ator.vercel.app',
24 | github: 'https://github.com/rittikbasu/formulator',
25 | },
26 | {
27 | title: 'Frize',
28 | description:
29 | 'An interactive dashboard that transforms time-tracking data from Rize into beautiful charts, graphs and insights.',
30 | techUsed: ['Next.js', 'Tailwind', 'Tremor UI', 'Supabase', 'OpenAI'],
31 | image: frize,
32 | link: 'https://frize.rittik.io',
33 | github: 'https://github.com/rittikbasu/frize',
34 | },
35 | {
36 | title: 'Price Alert Bot (31 ⭐ on GitHub)',
37 | description:
38 | 'A Telegram chatbot that helps you set price alerts for amazon products and sends you an alert message when it reaches the target price.',
39 | techUsed: ['Python', 'Telegram Bot API', 'ScraperAPI', 'Google Sheets API'],
40 | image: trackrBot,
41 | link: 'https://telegram.me/PriceA1ertBot',
42 | github: 'https://github.com/rittikbasu/trackrBot',
43 | },
44 | {
45 | title: 'Shouldreads',
46 | description:
47 | 'A compilation of the most important books to read, scraped from twitter with natural language search and advanced filtering functionality',
48 | techUsed: ['Next.js', 'Tailwind', 'SQLite', 'OpenAI'],
49 | image: shouldreads,
50 | link: 'https://shouldreads.vercel.app/',
51 | github: 'https://github.com/rittikbasu/shouldreads',
52 | },
53 | {
54 | title: 'Job Client for Hacker News',
55 | description:
56 | 'A fast and lightweight job client for Hacker News that helps you find Y Combinator startups that are currently hiring.',
57 | techUsed: ['Next.js', 'Tailwind', 'Hacker News API'],
58 | image: yc,
59 | link: 'https://yc.rittik.io',
60 | github: 'https://github.com/rittikbasu/yc-job-client',
61 | },
62 | ]
63 |
64 | export default data
65 |
--------------------------------------------------------------------------------
/src/images/avatar.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rittikbasu/website/e11112fb5df65209e5bc54cb8a19dec71a648337/src/images/avatar.png
--------------------------------------------------------------------------------
/src/images/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rittikbasu/website/e11112fb5df65209e5bc54cb8a19dec71a648337/src/images/logo.png
--------------------------------------------------------------------------------
/src/images/projects/formulator.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rittikbasu/website/e11112fb5df65209e5bc54cb8a19dec71a648337/src/images/projects/formulator.png
--------------------------------------------------------------------------------
/src/images/projects/frize.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rittikbasu/website/e11112fb5df65209e5bc54cb8a19dec71a648337/src/images/projects/frize.png
--------------------------------------------------------------------------------
/src/images/projects/shouldreads.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rittikbasu/website/e11112fb5df65209e5bc54cb8a19dec71a648337/src/images/projects/shouldreads.png
--------------------------------------------------------------------------------
/src/images/projects/total_recall.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rittikbasu/website/e11112fb5df65209e5bc54cb8a19dec71a648337/src/images/projects/total_recall.png
--------------------------------------------------------------------------------
/src/images/projects/trackrBot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rittikbasu/website/e11112fb5df65209e5bc54cb8a19dec71a648337/src/images/projects/trackrBot.png
--------------------------------------------------------------------------------
/src/images/projects/yc.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rittikbasu/website/e11112fb5df65209e5bc54cb8a19dec71a648337/src/images/projects/yc.png
--------------------------------------------------------------------------------
/src/lib/generateRssFeed.js:
--------------------------------------------------------------------------------
1 | import ReactDOMServer from 'react-dom/server'
2 | import { Feed } from 'feed'
3 | import { mkdir, writeFile } from 'fs/promises'
4 |
5 | import { getAllArticles } from './getAllArticles'
6 |
7 | export async function generateRssFeed() {
8 | let articles = await getAllArticles()
9 | let siteUrl = process.env.NEXT_PUBLIC_SITE_URL
10 | let author = {
11 | name: 'Spencer Sharp',
12 | email: 'spencer@planetaria.tech',
13 | }
14 |
15 | let feed = new Feed({
16 | title: author.name,
17 | description: 'Your blog description',
18 | author,
19 | id: siteUrl,
20 | link: siteUrl,
21 | image: `${siteUrl}/favicon.ico`,
22 | favicon: `${siteUrl}/favicon.ico`,
23 | copyright: `All rights reserved ${new Date().getFullYear()}`,
24 | feedLinks: {
25 | rss2: `${siteUrl}/rss/feed.xml`,
26 | json: `${siteUrl}/rss/feed.json`,
27 | },
28 | })
29 |
30 | for (let article of articles) {
31 | let url = `${siteUrl}/blog/${article.slug}`
32 | let html = ReactDOMServer.renderToStaticMarkup(
33 |
34 | )
35 |
36 | feed.addItem({
37 | title: article.title,
38 | id: url,
39 | link: url,
40 | description: article.description,
41 | content: html,
42 | author: [author],
43 | contributor: [author],
44 | date: new Date(article.date),
45 | })
46 | }
47 |
48 | await mkdir('./public/rss', { recursive: true })
49 | await Promise.all([
50 | writeFile('./public/rss/feed.xml', feed.rss2(), 'utf8'),
51 | writeFile('./public/rss/feed.json', feed.json1(), 'utf8'),
52 | ])
53 | }
54 |
--------------------------------------------------------------------------------
/src/lib/getArticlePositions.js:
--------------------------------------------------------------------------------
1 | export function getArticlePositions(totalArticles) {
2 | const firstColumn = Math.min(Math.ceil(totalArticles / 3), totalArticles)
3 | let remainingItems = totalArticles - firstColumn
4 | const secondColumn = Math.min(Math.ceil(remainingItems / 2), remainingItems)
5 | const thirdColumn = remainingItems - secondColumn
6 |
7 | const result = []
8 |
9 | if (firstColumn > 0) {
10 | result.push([...Array(firstColumn).keys()].map((x) => x + 1))
11 | }
12 |
13 | if (secondColumn > 0) {
14 | result.push([...Array(secondColumn).keys()].map((x) => x + firstColumn + 1))
15 | }
16 |
17 | if (thirdColumn > 0) {
18 | result.push(
19 | [...Array(thirdColumn).keys()].map(
20 | (x) => x + firstColumn + secondColumn + 1
21 | )
22 | )
23 | }
24 |
25 | const columns = result
26 | const numRows = columns[0].length
27 | const positions = {}
28 | let count = 0
29 | for (let i = 0; i < numRows; i++) {
30 | for (let j = 0; j < columns.length; j++) {
31 | count += 1
32 | const value = columns[j][i]
33 | if (value === undefined) break
34 | positions[count] = value
35 | }
36 | }
37 |
38 | return positions
39 | }
40 |
--------------------------------------------------------------------------------
/src/lib/gtag.js:
--------------------------------------------------------------------------------
1 | export const GA_MEASUREMENT_ID = process.env.NEXT_PUBLIC_MEASUREMENT_ID
2 |
3 | export const pageView = (url, title) => {
4 | window.gtag('config', GA_MEASUREMENT_ID, {
5 | page_location: url,
6 | page_title: title,
7 | })
8 | }
9 |
10 | export const event = ({ action, category, label, value }) => {
11 | window.gtag('event', action, {
12 | event_category: category,
13 | event_label: label,
14 | value: value,
15 | })
16 | }
17 |
--------------------------------------------------------------------------------
/src/lib/initSupabase.js:
--------------------------------------------------------------------------------
1 | import { createClient } from '@supabase/supabase-js'
2 |
3 | const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL || ''
4 | const supabaseClientKey = process.env.NEXT_PUBLIC_SUPABASE_CLIENT_KEY || ''
5 |
6 | export const SupabaseClient = createClient(supabaseUrl, supabaseClientKey)
7 |
--------------------------------------------------------------------------------
/src/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, sortProperty, sort) => {
8 | // const response = await notion.databases.query({
9 | // database_id: databaseId,
10 | // });
11 | const env = process.env.NODE_ENV
12 | const status = env === 'development' ? 'preview' : 'publish'
13 | const response = await notion.databases.query({
14 | database_id: databaseId,
15 | filter: {
16 | or: [
17 | {
18 | property: status,
19 | checkbox: {
20 | equals: true,
21 | },
22 | },
23 | ],
24 | },
25 | sorts: [
26 | {
27 | property: sortProperty,
28 | direction: sort,
29 | },
30 | ],
31 | })
32 | // remove databaseId from response
33 | response.results.forEach((result) => {
34 | delete result.parent.database_id
35 | })
36 | return response.results
37 | }
38 |
39 | export const getPage = async (pageId) => {
40 | const response = await notion.pages.retrieve({ page_id: pageId })
41 | return response
42 | }
43 |
44 | export const getBlocks = async (blockId) => {
45 | const blocks = []
46 | let cursor
47 | while (true) {
48 | const { results, next_cursor } = await notion.blocks.children.list({
49 | start_cursor: cursor,
50 | block_id: blockId,
51 | })
52 | blocks.push(...results)
53 | if (!next_cursor) {
54 | break
55 | }
56 | cursor = next_cursor
57 | }
58 | return blocks
59 | }
60 |
--------------------------------------------------------------------------------
/src/pages/_app.jsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useRef } from 'react'
2 | import Script from 'next/script'
3 | import { useRouter } from 'next/router'
4 | import { Analytics } from '@vercel/analytics/react'
5 | import { SpeedInsights } from '@vercel/speed-insights/next'
6 |
7 | import { Footer } from '@/components/Footer'
8 | import { Header } from '@/components/Header'
9 |
10 | import '@/styles/tailwind.css'
11 | import 'focus-visible'
12 | import 'highlight.js/styles/github-dark.css'
13 | import { DefaultSeo } from 'next-seo'
14 | import seoOptions from '../seo.config'
15 | import * as gtag from '@/lib/gtag'
16 | import { Work_Sans } from 'next/font/google'
17 | import { Poppins } from 'next/font/google'
18 | import localFont from 'next/font/local'
19 |
20 | const calSans = localFont({
21 | display: 'swap',
22 | subsets: ['latin'],
23 | src: '../../public/fonts/CalSans-SemiBold.woff2',
24 | variable: '--font-calsans',
25 | })
26 |
27 | const workSans = Work_Sans({
28 | display: 'swap',
29 | subsets: ['latin'],
30 | variable: '--font-worksans',
31 | weight: ['400', '500', '700'],
32 | })
33 |
34 | const poppins = Poppins({
35 | display: 'swap',
36 | subsets: ['latin'],
37 | variable: '--font-poppins',
38 | weight: ['400', '500', '700'],
39 | })
40 |
41 | const GA_MEASUREMENT_ID = process.env.NEXT_PUBLIC_MEASUREMENT_ID
42 |
43 | function usePrevious(value) {
44 | let ref = useRef()
45 |
46 | useEffect(() => {
47 | ref.current = value
48 | }, [value])
49 |
50 | return ref.current
51 | }
52 |
53 | export default function App({ Component, pageProps, router }) {
54 | let previousPathname = usePrevious(router.pathname)
55 | const pageRouter = useRouter()
56 | useEffect(() => {
57 | const handleRouteChange = (url) => {
58 | gtag.pageView(url)
59 | }
60 | pageRouter.events.on('routeChangeComplete', handleRouteChange)
61 | return () => {
62 | pageRouter.events.off('routeChangeComplete', handleRouteChange)
63 | }
64 | }, [pageRouter.events])
65 |
66 | return (
67 |
70 |
71 |
75 |
83 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 | )
99 | }
100 |
--------------------------------------------------------------------------------
/src/pages/_document.jsx:
--------------------------------------------------------------------------------
1 | import { Head, Html, Main, NextScript } from 'next/document'
2 |
3 | const modeScript = `
4 | let darkModeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
5 |
6 | updateMode()
7 | darkModeMediaQuery.addEventListener('change', updateModeWithoutTransitions)
8 | window.addEventListener('storage', updateModeWithoutTransitions)
9 |
10 | function updateMode() {
11 | let isSystemDarkMode = darkModeMediaQuery.matches
12 | let isDarkMode = window.localStorage.isDarkMode === 'true' || (!('isDarkMode' in window.localStorage) && isSystemDarkMode)
13 |
14 | if (isDarkMode) {
15 | document.documentElement.classList.add('dark')
16 | } else {
17 | document.documentElement.classList.remove('dark')
18 | }
19 |
20 | if (isDarkMode === isSystemDarkMode) {
21 | delete window.localStorage.isDarkMode
22 | }
23 | }
24 |
25 | function disableTransitionsTemporarily() {
26 | document.documentElement.classList.add('[&_*]:!transition-none')
27 | window.setTimeout(() => {
28 | document.documentElement.classList.remove('[&_*]:!transition-none')
29 | }, 0)
30 | }
31 |
32 | function updateModeWithoutTransitions() {
33 | disableTransitionsTemporarily()
34 | updateMode()
35 | }
36 | `
37 |
38 | export default function Document() {
39 | return (
40 |
41 |
42 |
43 | {/*
48 | */}
53 |
54 |
55 |
56 |
57 |
58 |
59 | )
60 | }
61 |
--------------------------------------------------------------------------------
/src/pages/about.jsx:
--------------------------------------------------------------------------------
1 | import Image from 'next/image'
2 | import Link from 'next/link'
3 | import clsx from 'clsx'
4 | import { NextSeo } from 'next-seo'
5 |
6 | import { BsTwitter, BsGithub } from 'react-icons/bs'
7 | import { MdEmail } from 'react-icons/md'
8 | import { FaLinkedinIn } from 'react-icons/fa'
9 | import {
10 | SiJavascript,
11 | SiReact,
12 | SiNextdotjs,
13 | SiPython,
14 | SiTailwindcss,
15 | SiFirebase,
16 | SiJupyter,
17 | SiFlask,
18 | SiPostgresql,
19 | } from 'react-icons/si'
20 |
21 | import { Container } from '@/components/Container'
22 | import { Button } from '@/components/Button'
23 | import portraitImage from '@/images/avatar.png'
24 | import { baseUrl } from '../seo.config'
25 |
26 | function SocialLink({ className, href, children, icon: Icon }) {
27 | return (
28 |
29 |
36 |
37 | {children}
38 |
39 |
40 | )
41 | }
42 |
43 | function Skills({ className, icon: Icon, children }) {
44 | return (
45 |
46 |
47 |
53 | {children}
54 |
55 |
56 | )
57 | }
58 |
59 | export default function About() {
60 | return (
61 | <>
62 |
81 |
82 |
83 |
96 |
97 |
98 | I'm{' '}
99 |
104 | Rittik Basu.
105 | {' '}
106 | I live in India, where I break things & learn fast.
107 |
108 |
109 |
110 | Although I have a degree in computer science, I still consider
111 | myself a self taught developer. I got into coding in the second
112 | year of my undergrad, but I think I've always had the
113 | mindset of a developer.
114 |
115 |
116 | As a child, I had an obsession with optimization which has
117 | stayed with me till this day. During my time in Mumbai, I would
118 | take local trains and constantly find ways to optimise my
119 | journey by discovering the shortest routes and fastest trains. I
120 | also loved playing video games, and I was always trying to find
121 | the best way to beat the game. I think that's what got me
122 | into coding. I wanted to find the best way to solve a problem.
123 |
124 |
125 | Initially, I started learning Python to automate mundane tasks
126 | in my daily life, but I soon fell in love with programming and
127 | began exploring other languages as well. I have been working
128 | with web technologies for the past 4 years now and I have been
129 | able to learn new tools & frameworks independently, applying
130 | them to real-world problems.
131 |
132 |
133 |
134 | Skills I have
135 |
136 |
140 |
144 | Javascript
145 |
146 |
150 | React
151 |
152 |
156 | Next.js
157 |
158 |
162 | Flask
163 |
164 |
168 | Tailwind
169 |
170 |
174 | PostgreSQL
175 |
176 |
180 | Jupyter
181 |
182 |
186 | Python
187 |
188 |
192 | Firebase
193 |
194 |
195 |
196 |
197 |
198 |
199 | See my projects
200 |
201 |
202 |
203 |
204 |
205 |
206 |
207 |
212 | _rittik
213 |
214 |
219 | rittikbasu
220 |
221 |
226 | rittikbasu
227 |
228 |
231 |
236 | hello@rittik.io
237 |
238 |
239 | See my projects
240 |
241 |
242 |
243 |
244 |
245 |
246 |
247 | >
248 | )
249 | }
250 |
--------------------------------------------------------------------------------
/src/pages/api/og.js:
--------------------------------------------------------------------------------
1 | import { ImageResponse } from '@vercel/og'
2 |
3 | export const config = {
4 | runtime: 'edge',
5 | }
6 | const font = fetch(
7 | new URL('/public/fonts/CalSans-SemiBold.ttf', import.meta.url)
8 | ).then((res) => res.arrayBuffer())
9 |
10 | export default async function handler(req) {
11 | try {
12 | const { searchParams } = new URL(req.url)
13 | const title = searchParams.get('title')
14 | const hasDate = searchParams.has('date')
15 | const date = hasDate ? searchParams.get('date') : hasDate
16 | const footerFontSize = hasDate ? 'xl' : '2xl'
17 | const titleFontSize = hasDate ? '6xl' : '7xl'
18 | const marginTop = hasDate ? '12' : '24'
19 | const letterSpacing = hasDate ? 'normal' : 'widest'
20 |
21 | const fontData = await font
22 |
23 | return new ImageResponse(
24 | (
25 |
41 |
45 | {title === 'home' ? (
46 |
47 | RittikBasu
48 |
49 | ) : (
50 | title
51 | )}
52 |
53 |
76 |
77 | {hasDate && (
78 | {date}
79 | )}
80 |
84 | rittik.io
85 |
86 |
87 |
88 | ),
89 | {
90 | width: 1200,
91 | height: 630,
92 | fonts: [
93 | {
94 | name: 'Cal Sans',
95 | data: fontData,
96 | style: 'normal',
97 | },
98 | ],
99 | }
100 | )
101 | } catch (e) {
102 | console.log(`${e.message}`)
103 | return new Response(`Failed to generate the image`, {
104 | status: 500,
105 | })
106 | }
107 | }
108 |
--------------------------------------------------------------------------------
/src/pages/api/views/[slug].js:
--------------------------------------------------------------------------------
1 | import { createClient } from '@supabase/supabase-js'
2 |
3 | const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL || ''
4 | const supabaseServerKey = process.env.SUPABASE_SERVICE_KEY || ''
5 |
6 | export const SupabaseAdmin = createClient(supabaseUrl, supabaseServerKey)
7 |
8 | export default async (req, res) => {
9 | if (req.method === 'POST') {
10 | // Call our stored procedure with the page_slug set by the request params slug
11 | await SupabaseAdmin.rpc('increment_views', {
12 | page_slug: req.query.slug,
13 | })
14 | return res.status(200).json({
15 | message: `Successfully incremented page: ${req.query.slug}`,
16 | })
17 | }
18 |
19 | if (req.method === 'GET') {
20 | // Query the pages table in the database where slug equals the request params slug.
21 | const { data } = await SupabaseAdmin.from('analytics')
22 | .select('views')
23 | .filter('slug', 'eq', req.query.slug)
24 |
25 | if (data) {
26 | return res.status(200).json({
27 | total: data[0]?.views || null,
28 | })
29 | }
30 | }
31 |
32 | return res.status(400).json({
33 | message: 'Unsupported Request',
34 | })
35 | }
36 |
--------------------------------------------------------------------------------
/src/pages/blog/[slug].jsx:
--------------------------------------------------------------------------------
1 | import { Fragment, useState } from 'react'
2 | import Link from 'next/link'
3 | import { NextSeo, ArticleJsonLd } from 'next-seo'
4 | import Image from 'next/image'
5 | import clsx from 'clsx'
6 | import slugify from 'slugify'
7 |
8 | import { Container } from '@/components/Container'
9 | import { Text, renderBlock } from '@/components/RenderNotion'
10 | import { LikeBtn } from '@/components/LikeButton'
11 | import { Prose } from '@/components/Prose'
12 | import { getDatabase, getPage, getBlocks } from '@/lib/notion'
13 | import { baseUrl } from '../../seo.config'
14 | import { UpdateViews } from '@/components/PageViews'
15 |
16 | import { BsArrowLeft } from 'react-icons/bs'
17 |
18 | const databaseId = process.env.NOTION_BLOG_DB_ID
19 |
20 | export default function Post({
21 | article,
22 | dateUtc,
23 | dateFormatted,
24 | lastEditedUtc,
25 | lastEditedFormatted,
26 | blocks,
27 | slug,
28 | }) {
29 | const [isLoading, setLoading] = useState(true)
30 | if (!article || !blocks) {
31 | return
32 | }
33 | const articleTitle = article.properties.name.title
34 | const articleDescription = article.properties.description.rich_text
35 | const wordCount = article.properties.wordCount.number
36 | const readingTime = Math.ceil(wordCount === null ? 0 : wordCount / 265)
37 | const env = process.env.NODE_ENV
38 |
39 | const coverImgFn = () => {
40 | if (article.cover) {
41 | const imgType = article.cover.type
42 | const image =
43 | imgType === 'external'
44 | ? article.cover.external.url
45 | : article.cover.file.url
46 | return image
47 | } else {
48 | return false
49 | }
50 | }
51 | const coverImg = coverImgFn()
52 | const coverImgCaption = article.properties.coverImgCaption.rich_text.length
53 | ? article.properties.coverImgCaption.rich_text[0].plain_text
54 | : false
55 | // if env is production, update the views
56 | if (env === 'production') {
57 | UpdateViews(slug)
58 | }
59 | // console.log(
60 | // `${baseUrl}api/og?title=${encodeURIComponent(
61 | // articleTitle[0].plain_text
62 | // ).replaceAll('&', '%26')}&date=${encodeURIComponent(lastEditedFormatted).replace(
63 | // '%2C',
64 | // '%2c'
65 | // )}`
66 | // )
67 | return (
68 |
69 |
97 |
98 |
113 |
114 |
115 |
116 |
121 |
122 |
123 |
124 |
162 |
163 | {blocks.map((block) => (
164 | {renderBlock(block)}
165 | ))}
166 |
167 |
168 | This post was last updated on {lastEditedFormatted}
169 |
170 |
171 |
172 |
173 |
174 |
175 |
176 | )
177 | }
178 |
179 | export const getStaticPaths = async () => {
180 | const database = await getDatabase(databaseId, 'date', 'descending')
181 | return {
182 | paths: database.map((article) => ({
183 | params: {
184 | slug: slugify(article.properties.name.title[0].plain_text, {
185 | strict: true,
186 | lower: true,
187 | }),
188 | },
189 | })),
190 | fallback: true,
191 | }
192 | }
193 |
194 | export const getStaticProps = async (context) => {
195 | const { slug } = context.params
196 | const database = await getDatabase(databaseId, 'date', 'descending')
197 | const id = database.find(
198 | (post) =>
199 | slugify(post.properties.name.title[0].plain_text, {
200 | strict: true,
201 | lower: true,
202 | }) === slug
203 | ).id
204 | const article = await getPage(id)
205 | const lastEditedUtc = article.last_edited_time
206 | const lastEditedFormatted = new Date(lastEditedUtc).toLocaleDateString(
207 | 'en-US',
208 | {
209 | month: 'short',
210 | day: '2-digit',
211 | year: 'numeric',
212 | }
213 | )
214 | const dateUtc = article.properties.date.date.start
215 | const dateFormatted = new Date(dateUtc).toLocaleDateString('en-US', {
216 | month: 'short',
217 | day: '2-digit',
218 | year: 'numeric',
219 | })
220 | const blocks = await getBlocks(id)
221 |
222 | // Retrieve block children for nested blocks (one level deep), for example toggle blocks
223 | // https://developers.notion.com/docs/working-with-page-content#reading-nested-blocks
224 | const childBlocks = await Promise.all(
225 | blocks
226 | .filter((block) => block.has_children)
227 | .map(async (block) => {
228 | return {
229 | id: block.id,
230 | children: await getBlocks(block.id),
231 | }
232 | })
233 | )
234 | const blocksWithChildren = blocks.map((block) => {
235 | // Add child blocks if the block should contain children but none exists
236 | if (block.has_children && !block[block.type].children) {
237 | block[block.type]['children'] = childBlocks.find(
238 | (x) => x.id === block.id
239 | )?.children
240 | }
241 | return block
242 | })
243 |
244 | return {
245 | props: {
246 | article,
247 | dateUtc,
248 | dateFormatted,
249 | lastEditedUtc,
250 | lastEditedFormatted,
251 | blocks: blocksWithChildren,
252 | slug: slug,
253 | },
254 | revalidate: 1,
255 | }
256 | }
257 |
--------------------------------------------------------------------------------
/src/pages/blog/index.jsx:
--------------------------------------------------------------------------------
1 | import { NextSeo } from 'next-seo'
2 | import slugify from 'slugify'
3 |
4 | import { SimpleLayout } from '@/components/SimpleLayout'
5 | import { BlogCard } from '@/components/BlogCard'
6 | import { getDatabase } from '@/lib/notion'
7 | import { getArticlePositions } from '@/lib/getArticlePositions'
8 | import { baseUrl } from '../../seo.config'
9 |
10 | import { createClient } from '@supabase/supabase-js'
11 |
12 | export default function Blog({ articles, articlePositions }) {
13 | const rearrangedArticles = Object.values(articlePositions).map(
14 | (pos) => articles[pos - 1]
15 | )
16 | return (
17 | <>
18 |
37 |
42 |
43 |
44 | {rearrangedArticles.map((article, index) => (
45 |
46 | ))}
47 |
48 |
49 | {articles.map((article, index) => (
50 |
51 | ))}
52 |
53 |
54 |
55 | >
56 | )
57 | }
58 | export const getStaticProps = async () => {
59 | const databaseId = process.env.NOTION_BLOG_DB_ID
60 | const database = await getDatabase(databaseId, 'date', 'descending')
61 | const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL || ''
62 | const supabaseServerKey = process.env.SUPABASE_SERVICE_KEY || ''
63 | const SupabaseAdmin = createClient(supabaseUrl, supabaseServerKey)
64 |
65 | // Fetch pageViews data for each article and update the database object
66 | for (const article of database) {
67 | const title = slugify(article.properties?.name.title[0].plain_text, {
68 | strict: true,
69 | lower: true,
70 | })
71 | const response = await SupabaseAdmin.from('analytics')
72 | .select('views')
73 | .filter('slug', 'eq', title)
74 | const pageViews = response.data[0]?.views || 0
75 |
76 | // Update the article object with the pageViews data
77 | article.pageViews = pageViews
78 | }
79 |
80 | return {
81 | props: {
82 | articles: database,
83 | articlePositions: getArticlePositions(database.length),
84 | },
85 | revalidate: 1,
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/src/pages/index.jsx:
--------------------------------------------------------------------------------
1 | import clsx from 'clsx'
2 | import { NextSeo } from 'next-seo'
3 |
4 | import { Button } from '@/components/Button'
5 | import { Container } from '@/components/Container'
6 | // import { generateRssFeed } from '@/lib/generateRssFeed'
7 | import { baseUrl } from '../seo.config'
8 |
9 | export default function Home({ previousPathname }) {
10 | return (
11 | <>
12 |
13 |
14 |
15 |
21 | Hi, my name is
22 |
23 | {/*
*/}
24 |
31 | Rittik Basu.
32 |
33 | {/*
*/}
34 |
37 |
42 | I build things for the web.
43 |
44 |
45 |
51 | I'm a full-stack engineer specializing in building & designing
52 | scalable applications with great user experience. My current tech
53 | stack includes Next.js, Typescript & Tailwind and I occasionally
54 | dabble in AI & blockchain technology.
55 |
56 |
57 |
64 | Learn More
65 |
66 |
67 |
68 | >
69 | )
70 | }
71 |
72 | // export async function getStaticProps() {
73 | // if (process.env.NODE_ENV === 'production') {
74 | // await generateRssFeed()
75 | // }
76 |
77 | // return {
78 | // props: {
79 | // articles: (await getAllArticles())
80 | // .slice(0, 4)
81 | // .map(({ component, ...meta }) => meta),
82 | // },
83 | // }
84 | // }
85 |
--------------------------------------------------------------------------------
/src/pages/projects.jsx:
--------------------------------------------------------------------------------
1 | import { useState, Fragment } from 'react'
2 | import Image from 'next/image'
3 | import Link from 'next/link'
4 | import { NextSeo } from 'next-seo'
5 | import clsx from 'clsx'
6 |
7 | import { Card } from '@/components/Card'
8 | import { SimpleLayout } from '@/components/SimpleLayout'
9 | import { baseUrl } from '../seo.config'
10 | import data from '@/data/projects.js'
11 |
12 | import { BsLink45Deg, BsGithub } from 'react-icons/bs'
13 |
14 | const delay = ['', 'delay-200', 'delay-500', 'delay-1000']
15 |
16 | function Project({ project, index }) {
17 | const [isLoading, setLoading] = useState(true)
18 | const projectTitle = project.title
19 | const projectDescription = project.description
20 | const techUsed = project.techUsed
21 | const github = project.github
22 | const link = project.link
23 | const image = project.image
24 | return (
25 |
26 |
27 | setLoading(false)}
38 | />
39 |
40 |
41 | {projectTitle}
42 |
43 |
44 |
45 | {techUsed.map((item, i) => {
46 | return (
47 |
48 |
49 | {item}
50 |
51 | {techUsed.length - 1 !== i && (
52 | |
53 | )}
54 |
55 | )
56 | })}
57 |
58 | {projectDescription}
59 |
60 | {github && (
61 |
65 |
66 | Source Code
67 |
68 | )}
69 | {link && (
70 |
74 |
75 | Live Demo
76 |
77 | )}
78 |
79 |
80 | )
81 | }
82 |
83 | export default function ProjectsIndex() {
84 | return (
85 | <>
86 |
104 |
109 |
113 | {data.map((project, index) => (
114 |
115 | ))}
116 |
117 |
118 | >
119 | )
120 | }
121 |
--------------------------------------------------------------------------------
/src/seo.config.js:
--------------------------------------------------------------------------------
1 | export const baseUrl = process.env.NEXT_PUBLIC_WEBSITE_URL
2 |
3 | const seoConfig = {
4 | defaultTitle: 'Rittik Basu | Full Stack Developer',
5 | titleTemplate: '%s | Rittik Basu',
6 | description:
7 | 'A full-stack engineer specializing in building & designing scalable applications with great user experience.',
8 | openGraph: {
9 | title: 'Rittik Basu',
10 | description:
11 | 'A full-stack engineer specializing in building & designing scalable applications with great user experience.',
12 | images: [
13 | {
14 | url: `${baseUrl}api/og?title=home`,
15 | width: 1200,
16 | height: 600,
17 | alt: `Rittik Basu | Full Stack Developer`,
18 | },
19 | ],
20 | type: 'website',
21 | locale: 'en_US',
22 | url: baseUrl,
23 | site_name: 'Rittik Basu',
24 | },
25 | twitter: {
26 | handle: '@_rittik',
27 | site: '@_rittik',
28 | cardType: 'summary_large_image',
29 | },
30 | }
31 |
32 | export default seoConfig
33 |
--------------------------------------------------------------------------------
/src/styles/tailwind.css:
--------------------------------------------------------------------------------
1 | @import 'tailwindcss/base';
2 |
3 | @layer base {
4 | .text-edge-outline-dark {
5 | -webkit-text-stroke: 0.5px rgba(255,255,255,0.3);
6 | }
7 |
8 | .text-edge-outline-light {
9 | -webkit-text-stroke: 0.4px rgba(0,0,0,0.5);
10 | }
11 |
12 | body {
13 | scrollbar-width: thin;
14 | scrollbar-color: #a5b4fc;
15 | overflow-x: hidden;
16 | }
17 |
18 | body::-webkit-scrollbar {
19 | width: 4px;
20 | }
21 |
22 | body::-webkit-scrollbar-thumb {
23 | background-color: #a5b4fc;
24 | border-radius: 14px;
25 | }
26 | /* Hide scrollbar for Chrome, Safari and Opera */
27 | .custom-scrollbar::-webkit-scrollbar {
28 | height: 4px;
29 | }
30 |
31 | .custom-scrollbar::-webkit-scrollbar-thumb {
32 | background-color: #a5b4fc;
33 | border-radius: 14px;
34 | }
35 |
36 | /* Hide scrollbar for IE, Edge and Firefox */
37 | .custom-scrollbar {
38 | -ms-overflow-style: none; /* IE and Edge */
39 | scrollbar-width: thin; /* Firefox */
40 | scrollbar-color: #a5b4fc;
41 | }
42 | }
43 |
44 | @import 'tailwindcss/components';
45 | @import 'tailwindcss/utilities';
46 |
47 | @layer utilities {
48 | .masonry {
49 | column-gap: 1.5em;
50 | column-count: 1;
51 | }
52 | .masonry-sm {
53 | column-gap: 1.5em;
54 | column-count: 2;
55 | }
56 | .masonry-md {
57 | column-gap: 1.5em;
58 | column-count: 3;
59 | }
60 | .break-inside {
61 | break-inside: avoid;
62 | }
63 | }
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | const defaultTheme = require('tailwindcss/defaultTheme')
2 |
3 | /** @type {import('tailwindcss').Config} */
4 | module.exports = {
5 | content: ['./src/**/*.{js,jsx}'],
6 | darkMode: 'class',
7 | future: {
8 | hoverOnlyWhenSupported: true,
9 | },
10 | plugins: [require('@tailwindcss/typography')],
11 | theme: {
12 | extend: {
13 | animation: {
14 | gradient: 'text 4s ease infinite',
15 | // glow: 'glow 2s ease-in-out alternate infinite',
16 | 'fade-in': 'fade-in 3s ease-in-out forwards',
17 | title: 'title 3s ease-out forwards',
18 | 'fade-left': 'fade-left 3s ease-in-out forwards',
19 | 'fade-right': 'fade-right 3s ease-in-out forwards',
20 | heartbeat: 'heartbeat 1s ease-in-out',
21 | },
22 | keyframes: {
23 | text: {
24 | '0%, 100%': {
25 | 'background-size': '200% 200%',
26 | 'background-position': 'left center',
27 | },
28 | '50%': {
29 | 'background-size': '200% 200%',
30 | 'background-position': 'right center',
31 | },
32 | },
33 | glow: {
34 | to: {
35 | 'text-shadow': '0 0 1px #818cf8',
36 | },
37 | },
38 | 'fade-in': {
39 | '0%': {
40 | opacity: '0%',
41 | },
42 | '75%': {
43 | opacity: '0%',
44 | },
45 | '100%': {
46 | opacity: '100%',
47 | },
48 | },
49 | 'fade-left': {
50 | '0%': {
51 | transform: 'translateX(100%)',
52 | opacity: '0%',
53 | },
54 |
55 | '30%': {
56 | transform: 'translateX(0%)',
57 | opacity: '100%',
58 | },
59 | '100%': {
60 | opacity: '0%',
61 | },
62 | },
63 | 'fade-right': {
64 | '0%': {
65 | transform: 'translateX(-100%)',
66 | opacity: '0%',
67 | },
68 |
69 | '30%': {
70 | transform: 'translateX(0%)',
71 | opacity: '100%',
72 | },
73 | '100%': {
74 | opacity: '0%',
75 | },
76 | },
77 | title: {
78 | '0%': {
79 | 'line-height': '0%',
80 | 'letter-spacing': '0.5em',
81 | opacity: '0',
82 | },
83 | '25%': {
84 | 'line-height': '0%',
85 | opacity: '0%',
86 | },
87 | '80%': {
88 | opacity: '100%',
89 | },
90 |
91 | '100%': {
92 | 'line-height': '100%',
93 | opacity: '100%',
94 | },
95 | },
96 | heartbeat: {
97 | '0%': {
98 | transform: 'scale(1)',
99 | },
100 | '25%': {
101 | transform: 'scale(1.4)',
102 | },
103 | '50%': {
104 | transform: 'scale(1)',
105 | },
106 | '75%': {
107 | transform: 'scale(1.4)',
108 | },
109 | '100%': {
110 | transform: 'scale(1)',
111 | },
112 | },
113 | },
114 | },
115 | fontFamily: {
116 | sans: ['var(--font-worksans)', ...defaultTheme.fontFamily.sans],
117 | poppins: ['var(--font-poppins)'],
118 | heading: ['var(--font-calsans)'],
119 | },
120 | fontSize: {
121 | xs: ['0.8125rem', { lineHeight: '1.5rem' }],
122 | sm: ['0.875rem', { lineHeight: '1.5rem' }],
123 | base: ['1rem', { lineHeight: '1.75rem' }],
124 | lg: ['1.125rem', { lineHeight: '1.75rem' }],
125 | xl: ['1.25rem', { lineHeight: '2rem' }],
126 | '2xl': ['1.5rem', { lineHeight: '2rem' }],
127 | '3xl': ['1.875rem', { lineHeight: '2.25rem' }],
128 | '4xl': ['2rem', { lineHeight: '2.5rem' }],
129 | '5xl': ['3rem', { lineHeight: '3.5rem' }],
130 | '6xl': ['3.75rem', { lineHeight: '1' }],
131 | '7xl': ['4.5rem', { lineHeight: '1' }],
132 | '8xl': ['6rem', { lineHeight: '1' }],
133 | '9xl': ['8rem', { lineHeight: '1' }],
134 | },
135 | screens: {
136 | sm: '500px',
137 |
138 | tab: '601px',
139 |
140 | md: '768px',
141 |
142 | lg: '1024px',
143 |
144 | xl: '1280px',
145 | },
146 | typography: (theme) => ({
147 | invert: {
148 | css: {
149 | '--tw-prose-body': 'var(--tw-prose-invert-body)',
150 | '--tw-prose-headings': 'var(--tw-prose-invert-headings)',
151 | '--tw-prose-links': 'var(--tw-prose-invert-links)',
152 | '--tw-prose-links-hover': 'var(--tw-prose-invert-links-hover)',
153 | '--tw-prose-underline': 'var(--tw-prose-invert-underline)',
154 | '--tw-prose-underline-hover':
155 | 'var(--tw-prose-invert-underline-hover)',
156 | '--tw-prose-bold': 'var(--tw-prose-invert-bold)',
157 | '--tw-prose-counters': 'var(--tw-prose-invert-counters)',
158 | '--tw-prose-bullets': 'var(--tw-prose-invert-bullets)',
159 | '--tw-prose-hr': 'var(--tw-prose-invert-hr)',
160 | '--tw-prose-quote-borders': 'var(--tw-prose-invert-quote-borders)',
161 | '--tw-prose-captions': 'var(--tw-prose-invert-captions)',
162 | '--tw-prose-code': 'var(--tw-prose-invert-code)',
163 | '--tw-prose-code-bg': 'var(--tw-prose-invert-code-bg)',
164 | '--tw-prose-pre-code': 'var(--tw-prose-invert-pre-code)',
165 | '--tw-prose-pre-bg': 'var(--tw-prose-invert-pre-bg)',
166 | '--tw-prose-pre-border': 'var(--tw-prose-invert-pre-border)',
167 | '--tw-prose-th-borders': 'var(--tw-prose-invert-th-borders)',
168 | '--tw-prose-td-borders': 'var(--tw-prose-invert-td-borders)',
169 | },
170 | },
171 | DEFAULT: {
172 | css: {
173 | '--tw-prose-body': theme('colors.zinc.600'),
174 | '--tw-prose-headings': theme('colors.zinc.900'),
175 | '--tw-prose-links': theme('colors.zinc.800'),
176 | '--tw-prose-links-hover': theme('colors.indigo.600'),
177 | '--tw-prose-underline': theme('colors.zinc.800 / 0.8'),
178 | '--tw-prose-underline-hover': theme('colors.indigo.500'),
179 | '--tw-prose-bold': theme('colors.zinc.900'),
180 | '--tw-prose-counters': theme('colors.zinc.900'),
181 | '--tw-prose-bullets': theme('colors.zinc.900'),
182 | '--tw-prose-hr': theme('colors.zinc.100'),
183 | '--tw-prose-quote-borders': theme('colors.zinc.200'),
184 | '--tw-prose-captions': theme('colors.zinc.400'),
185 | '--tw-prose-code': theme('colors.zinc.700'),
186 | '--tw-prose-code-bg': theme('colors.zinc.300 / 0.2'),
187 | '--tw-prose-pre-code': theme('colors.zinc.100'),
188 | '--tw-prose-pre-bg': theme('colors.zinc.900'),
189 | '--tw-prose-pre-border': 'transparent',
190 | '--tw-prose-th-borders': theme('colors.zinc.200'),
191 | '--tw-prose-td-borders': theme('colors.zinc.100'),
192 |
193 | '--tw-prose-invert-body': theme('colors.zinc.400'),
194 | '--tw-prose-invert-headings': theme('colors.zinc.200'),
195 | '--tw-prose-invert-links': theme('colors.zinc.300'),
196 | '--tw-prose-invert-links-hover': theme('colors.indigo.400'),
197 | '--tw-prose-invert-underline': theme('colors.zinc.300 / 0.8'),
198 | '--tw-prose-invert-underline-hover': theme('colors.indigo.400'),
199 | '--tw-prose-invert-bold': theme('colors.zinc.200'),
200 | '--tw-prose-invert-counters': theme('colors.zinc.200'),
201 | '--tw-prose-invert-bullets': theme('colors.zinc.200'),
202 | '--tw-prose-invert-hr': theme('colors.zinc.700 / 0.4'),
203 | '--tw-prose-invert-quote-borders': theme('colors.zinc.500'),
204 | '--tw-prose-invert-captions': theme('colors.zinc.500'),
205 | '--tw-prose-invert-code': theme('colors.zinc.300'),
206 | '--tw-prose-invert-code-bg': theme('colors.zinc.200 / 0.08'),
207 | '--tw-prose-invert-pre-code': theme('colors.zinc.100'),
208 | '--tw-prose-invert-pre-bg': 'rgb(0 0 0 / 0.4)',
209 | '--tw-prose-invert-pre-border': theme('colors.zinc.200 / 0.1'),
210 | '--tw-prose-invert-th-borders': theme('colors.zinc.700'),
211 | '--tw-prose-invert-td-borders': theme('colors.zinc.800'),
212 |
213 | // Base
214 | color: 'var(--tw-prose-body)',
215 | lineHeight: theme('lineHeight.7'),
216 | '> *': {
217 | marginTop: theme('spacing.5'),
218 | marginBottom: theme('spacing.5'),
219 | },
220 | p: {
221 | marginTop: theme('spacing.3'),
222 | marginBottom: theme('spacing.3'),
223 | },
224 |
225 | // Headings
226 | 'h1, h2, h3': {
227 | color: 'var(--tw-prose-headings)',
228 | },
229 | h1: {
230 | fontSize: theme('fontSize.2xl')[0],
231 | lineHeight: theme('lineHeight.7'),
232 | marginTop: theme('spacing.10'),
233 | marginBottom: theme('spacing.4'),
234 | },
235 | h2: {
236 | fontSize: theme('fontSize.lg')[0],
237 | lineHeight: theme('lineHeight.5'),
238 | marginTop: theme('spacing.10'),
239 | marginBottom: theme('spacing.4'),
240 | },
241 | h3: {
242 | fontSize: theme('fontSize.base')[0],
243 | lineHeight: theme('lineHeight.3'),
244 | marginTop: theme('spacing.8'),
245 | marginBottom: theme('spacing.3'),
246 | },
247 | ':is(h1, h2, h3) + *': {
248 | marginTop: theme('spacing.0'),
249 | },
250 | ':is(h1) + h3': {
251 | marginTop: theme('spacing.6'),
252 | },
253 | ':is(h1) + h2': {
254 | marginTop: theme('spacing.6'),
255 | },
256 | ':is(h2) + h3': {
257 | marginTop: theme('spacing.6'),
258 | },
259 |
260 | // Images
261 | img: {
262 | borderRadius: theme('borderRadius.xl'),
263 | },
264 |
265 | // Inline elements
266 | a: {
267 | color: 'var(--tw-prose-links)',
268 | fontWeight: theme('fontWeight.normal'),
269 | textDecoration: 'underline',
270 | textDecorationColor: 'var(--tw-prose-underline)',
271 | // underline offset
272 | textUnderlineOffset: 0.2 + 'em',
273 | transitionProperty: 'color, text-decoration-color',
274 | transitionDuration: theme('transitionDuration.150'),
275 | transitionTimingFunction: theme('transitionTimingFunction.in-out'),
276 | },
277 | 'a:hover': {
278 | color: 'var(--tw-prose-links-hover)',
279 | textDecorationColor: 'var(--tw-prose-underline-hover)',
280 | },
281 | strong: {
282 | color: 'var(--tw-prose-bold)',
283 | fontWeight: theme('fontWeight.semibold'),
284 | },
285 | code: {
286 | display: 'inline-block',
287 | color: 'var(--tw-prose-code)',
288 | fontSize: theme('fontSize.sm')[0],
289 | fontWeight: theme('fontWeight.normal'),
290 | backgroundColor: 'var(--tw-prose-code-bg)',
291 | borderRadius: theme('borderRadius.md'),
292 | paddingLeft: theme('spacing.1'),
293 | paddingRight: theme('spacing.1'),
294 | },
295 | 'a code': {
296 | color: 'inherit',
297 | },
298 | ':is(h2, h3) code': {
299 | fontWeight: theme('fontWeight.bold'),
300 | },
301 |
302 | // Quotes
303 | blockquote: {
304 | paddingLeft: theme('spacing.6'),
305 | borderLeftWidth: theme('borderWidth.2'),
306 | borderLeftColor: 'var(--tw-prose-quote-borders)',
307 | fontStyle: 'italic',
308 | },
309 |
310 | // Figures
311 | figcaption: {
312 | color: 'var(--tw-prose-captions)',
313 | fontSize: theme('fontSize.sm')[0],
314 | lineHeight: theme('lineHeight.6'),
315 | marginTop: theme('spacing.3'),
316 | fontStyle: 'italic',
317 | },
318 | 'figcaption > p': {
319 | margin: 0,
320 | },
321 |
322 | // Lists
323 | ul: {
324 | listStyleType: 'disc',
325 | },
326 | ol: {
327 | listStyleType: 'decimal',
328 | },
329 | 'ul, ol': {
330 | paddingLeft: theme('spacing.6'),
331 | },
332 | li: {
333 | marginTop: theme('spacing.3'),
334 | marginBottom: theme('spacing.3'),
335 | paddingLeft: theme('spacing[3.5]'),
336 | },
337 | 'li::marker': {
338 | fontSize: theme('fontSize.sm')[0],
339 | fontWeight: theme('fontWeight.semibold'),
340 | },
341 | 'ol > li::marker': {
342 | color: 'var(--tw-prose-counters)',
343 | },
344 | 'ul > li::marker': {
345 | color: 'var(--tw-prose-bullets)',
346 | },
347 | 'li :is(ol, ul)': {
348 | marginTop: theme('spacing.4'),
349 | marginBottom: theme('spacing.4'),
350 | },
351 | 'li :is(li, p)': {
352 | marginTop: theme('spacing.3'),
353 | marginBottom: theme('spacing.3'),
354 | },
355 |
356 | // Code blocks
357 | pre: {
358 | color: 'var(--tw-prose-pre-code)',
359 | fontSize: theme('fontSize.sm')[0],
360 | fontWeight: theme('fontWeight.medium'),
361 | backgroundColor: 'var(--tw-prose-pre-bg)',
362 | borderRadius: theme('borderRadius.xl'),
363 | padding: theme('spacing.6'),
364 | overflowX: 'auto',
365 | border: '1px solid',
366 | borderColor: 'var(--tw-prose-pre-border)',
367 | },
368 | 'pre code': {
369 | display: 'inline',
370 | color: 'inherit',
371 | fontSize: 'inherit',
372 | fontWeight: 'inherit',
373 | backgroundColor: 'transparent',
374 | borderRadius: 0,
375 | padding: 0,
376 | },
377 |
378 | // Horizontal rules
379 | hr: {
380 | marginTop: theme('spacing.10'),
381 | marginBottom: theme('spacing.10'),
382 | borderTopWidth: '1px',
383 | borderColor: 'var(--tw-prose-hr)',
384 | '@screen lg': {
385 | marginLeft: `calc(${theme('spacing.12')} * -1)`,
386 | marginRight: `calc(${theme('spacing.12')} * -1)`,
387 | },
388 | },
389 |
390 | // Tables
391 | table: {
392 | width: '100%',
393 | tableLayout: 'auto',
394 | textAlign: 'left',
395 | fontSize: theme('fontSize.sm')[0],
396 | },
397 | thead: {
398 | borderBottomWidth: '1px',
399 | borderBottomColor: 'var(--tw-prose-th-borders)',
400 | },
401 | 'thead th': {
402 | color: 'var(--tw-prose-headings)',
403 | fontWeight: theme('fontWeight.semibold'),
404 | verticalAlign: 'bottom',
405 | paddingBottom: theme('spacing.2'),
406 | },
407 | 'thead th:not(:first-child)': {
408 | paddingLeft: theme('spacing.2'),
409 | },
410 | 'thead th:not(:last-child)': {
411 | paddingRight: theme('spacing.2'),
412 | },
413 | 'tbody tr': {
414 | borderBottomWidth: '1px',
415 | borderBottomColor: 'var(--tw-prose-td-borders)',
416 | },
417 | 'tbody tr:last-child': {
418 | borderBottomWidth: 0,
419 | },
420 | 'tbody td': {
421 | verticalAlign: 'baseline',
422 | },
423 | tfoot: {
424 | borderTopWidth: '1px',
425 | borderTopColor: 'var(--tw-prose-th-borders)',
426 | },
427 | 'tfoot td': {
428 | verticalAlign: 'top',
429 | },
430 | ':is(tbody, tfoot) td': {
431 | paddingTop: theme('spacing.2'),
432 | paddingBottom: theme('spacing.2'),
433 | },
434 | ':is(tbody, tfoot) td:not(:first-child)': {
435 | paddingLeft: theme('spacing.2'),
436 | },
437 | ':is(tbody, tfoot) td:not(:last-child)': {
438 | paddingRight: theme('spacing.2'),
439 | },
440 | },
441 | },
442 | }),
443 | },
444 | }
445 |
--------------------------------------------------------------------------------