├── .env.example ├── public ├── favicon.ico ├── favicon2.ico └── fonts │ ├── Ubuntu-Bold.ttf │ ├── CalSans-SemiBold.ttf │ └── CalSans-SemiBold.woff2 ├── src ├── images │ ├── logo.png │ ├── avatar.png │ └── projects │ │ ├── yc.png │ │ ├── frize.png │ │ ├── formulator.png │ │ ├── trackrBot.png │ │ ├── shouldreads.png │ │ └── total_recall.png ├── components │ ├── Prose.jsx │ ├── FormatDate.jsx │ ├── PageViews.jsx │ ├── Section.jsx │ ├── Container.jsx │ ├── SimpleLayout.jsx │ ├── Card.jsx │ ├── Footer.jsx │ ├── Button.jsx │ ├── LikeButton.jsx │ ├── BlogCard.jsx │ ├── RenderNotion.jsx │ └── Header.jsx ├── lib │ ├── initSupabase.js │ ├── gtag.js │ ├── getArticlePositions.js │ ├── notion.js │ └── generateRssFeed.js ├── seo.config.js ├── pages │ ├── api │ │ ├── views │ │ │ └── [slug].js │ │ └── og.js │ ├── _document.jsx │ ├── _app.jsx │ ├── blog │ │ ├── index.jsx │ │ └── [slug].jsx │ ├── index.jsx │ ├── projects.jsx │ └── about.jsx ├── styles │ └── tailwind.css └── data │ └── projects.js ├── .eslintrc.json ├── jsconfig.json ├── prettier.config.js ├── postcss.config.js ├── .gitignore ├── README.md ├── next.config.mjs ├── package.json └── tailwind.config.js /.env.example: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_SITE_URL=https://example.com 2 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rittikbasu/website/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/favicon2.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rittikbasu/website/HEAD/public/favicon2.ico -------------------------------------------------------------------------------- /src/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rittikbasu/website/HEAD/src/images/logo.png -------------------------------------------------------------------------------- /src/images/avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rittikbasu/website/HEAD/src/images/avatar.png -------------------------------------------------------------------------------- /src/images/projects/yc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rittikbasu/website/HEAD/src/images/projects/yc.png -------------------------------------------------------------------------------- /public/fonts/Ubuntu-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rittikbasu/website/HEAD/public/fonts/Ubuntu-Bold.ttf -------------------------------------------------------------------------------- /src/images/projects/frize.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rittikbasu/website/HEAD/src/images/projects/frize.png -------------------------------------------------------------------------------- /public/fonts/CalSans-SemiBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rittikbasu/website/HEAD/public/fonts/CalSans-SemiBold.ttf -------------------------------------------------------------------------------- /src/images/projects/formulator.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rittikbasu/website/HEAD/src/images/projects/formulator.png -------------------------------------------------------------------------------- /src/images/projects/trackrBot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rittikbasu/website/HEAD/src/images/projects/trackrBot.png -------------------------------------------------------------------------------- /public/fonts/CalSans-SemiBold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rittikbasu/website/HEAD/public/fonts/CalSans-SemiBold.woff2 -------------------------------------------------------------------------------- /src/images/projects/shouldreads.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rittikbasu/website/HEAD/src/images/projects/shouldreads.png -------------------------------------------------------------------------------- /src/images/projects/total_recall.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rittikbasu/website/HEAD/src/images/projects/total_recall.png -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals", 3 | "rules": { 4 | "react/no-unknown-property": "off" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "paths": { 5 | "@/*": ["src/*"] 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | singleQuote: true, 3 | semi: false, 4 | plugins: [require('prettier-plugin-tailwindcss')], 5 | } 6 | -------------------------------------------------------------------------------- /src/components/Prose.jsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx' 2 | 3 | export function Prose({ children, className }) { 4 | return ( 5 |
{children}
6 | ) 7 | } 8 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/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/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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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/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/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 | 83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 | 92 | 93 | 94 |
95 |
96 |
97 | 98 | ) 99 | } 100 | -------------------------------------------------------------------------------- /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 | 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/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 |
54 |
61 |
68 |
75 |
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/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 | {`Screenshot 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/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/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 |