├── .eslintrc.json ├── .gitignore ├── .nvmrc ├── README.md ├── app ├── (site) │ ├── (main) │ │ ├── about │ │ │ └── page.tsx │ │ ├── layout.tsx │ │ ├── notes │ │ │ └── page.tsx │ │ └── page.tsx │ └── (single) │ │ ├── layout.tsx │ │ ├── note │ │ └── [...slug] │ │ │ └── page.tsx │ │ └── work │ │ └── [...slug] │ │ └── page.tsx ├── _components │ ├── animate │ │ ├── node.tsx │ │ └── wrapper.tsx │ ├── back-to-top.tsx │ ├── expand-button.tsx │ ├── external-link.tsx │ ├── icons │ │ ├── HeartIcon.tsx │ │ ├── LinkIcon.tsx │ │ ├── LinkedIcon.tsx │ │ ├── StarIcon.tsx │ │ └── TwitterIcon.tsx │ ├── mdx │ │ ├── image.tsx │ │ ├── index.tsx │ │ └── link.tsx │ ├── navbar │ │ └── index.tsx │ ├── note │ │ ├── index.ts │ │ ├── item.tsx │ │ └── share-note.tsx │ ├── providers.tsx │ ├── timeline.tsx │ ├── waving-hand.tsx │ └── work │ │ └── work-card.tsx ├── error.tsx ├── favicon.ico ├── layout.tsx ├── og │ └── route.tsx ├── robots.ts └── sitemap.ts ├── content ├── experiences │ ├── bitcs.json │ └── sapio.json ├── notes │ └── go-cli-ip-tracker │ │ └── go-cli-ip-tracker.mdx └── projects │ ├── poly.json │ ├── task-manager.json │ └── zaars.json ├── contentlayer.config.ts ├── hooks └── useScrollPosition.ts ├── lib ├── env.ts ├── fonts.ts ├── meta.ts └── utils │ ├── cn.ts │ ├── date.ts │ └── note-length.tsx ├── models ├── experience.ts ├── note.ts └── project.ts ├── next.config.js ├── package.json ├── postcss.config.js ├── public ├── fonts │ ├── acorn.woff │ └── gt.woff ├── images │ ├── notes │ │ ├── fire.png │ │ ├── go.webp │ │ └── iptracker-cli-root.webp │ ├── projects │ │ ├── poly │ │ │ ├── main.png │ │ │ ├── main2.png │ │ │ ├── main3.png │ │ │ └── poly.webp │ │ ├── task │ │ │ └── task.webp │ │ └── zaars │ │ │ └── zaars.webp │ └── star.svg ├── me.webp └── resume.pdf ├── styles ├── globals.css └── prose.css ├── tailwind.config.ts ├── tsconfig.json └── yarn.lock /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /.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 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | .contentlayer 38 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v18.17.1 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # nitinp.dev 3 | 4 | 5 | 6 | - **Framework**: [Next.js](https://nextjs.org/) 7 | - **Deployment**: [Vercel](https://vercel.com) 8 | - **Styling**: [Tailwind CSS](https://tailwindcss.com) 9 | - **Analytics**: [Vercel Analytics](https://vercel.com/analytics) 10 | -------------------------------------------------------------------------------- /app/(site)/(main)/about/page.tsx: -------------------------------------------------------------------------------- 1 | import NodeAnimate from '@/app/_components/animate/node' 2 | import { StarIcon } from '@/app/_components/icons/StarIcon' 3 | import { ExperienceItem } from '@/app/_components/timeline' 4 | import { ExternalLink } from '@/app/_components/external-link' 5 | import { generateMeta, keywords, getOgImage } from '@/lib/meta' 6 | import { allExperiences as experiences } from '@/.contentlayer/generated' 7 | import { Metadata } from 'next' 8 | 9 | export const metadata: Metadata = generateMeta({ 10 | title: 'About' + ' — Nitin Panwar', 11 | description: 'Get to know Nitin Panwar, a Software Engineer from India.', 12 | keywords: [...keywords, 'About'], 13 | ...getOgImage( 14 | 'About', 15 | 'Get to know Nitin Panwar, a Software Engineer from India.' 16 | ), 17 | }) 18 | 19 | export default function AboutPage() { 20 | return ( 21 |
22 | 23 |

24 | 25 |

26 | I'm Nitin. 27 | 28 | 29 | 30 |

31 |
32 |

33 |
34 | 35 | 36 |
37 |
38 |

39 | I'm a Software Engineer working remotely from the tech hub of 40 | India. 41 |

42 |

43 | In my dynamic 2+ years as a software developer, I've engaged 44 | in diverse areas, from coding efficiency to elevating user 45 | experiences. My journey includes front-end development, software 46 | architecture, and refining app UI/UX. I take pride in navigating 47 | various roles in this ever-evolving tech landscape. 48 |

49 |
50 |
51 |
52 | 53 | 57 | 58 |

Experience

59 | {experiences.map((exp, index) => { 60 | return 61 | })} 62 |
63 | View Resume 64 |
65 |
66 | 67 | 71 |

72 | Outside work, my downtime is a mix of pure enjoyment. Whether 73 | it's the thrill of a football match, the strategic game of pool, 74 | or just the laid-back moments hanging out with friends and family, 75 | each experience adds its own touch to the canvas of my life. These are 76 | the simple pleasures that keep the rhythm of my days vibrant and 77 | fulfilling. 78 |

79 |
80 |
81 | ) 82 | } 83 | -------------------------------------------------------------------------------- /app/(site)/(main)/layout.tsx: -------------------------------------------------------------------------------- 1 | import Navbar from '@/app/_components/navbar' 2 | 3 | export default function RootLayout({ 4 | children, 5 | }: { 6 | children: React.ReactNode 7 | }) { 8 | return ( 9 | <> 10 | 11 |
{children}
12 | 13 | ) 14 | } 15 | -------------------------------------------------------------------------------- /app/(site)/(main)/notes/page.tsx: -------------------------------------------------------------------------------- 1 | import { generateMeta, getOgImage, keywords } from '@/lib/meta' 2 | import { Metadata } from 'next' 3 | import { allNotes } from 'contentlayer/generated' 4 | import NodeAnimate from '@/app/_components/animate/node' 5 | import { NoteItem } from '@/app/_components/note' 6 | 7 | export const metadata: Metadata = generateMeta({ 8 | title: 'Notes' + ' — Nitin Panwar', 9 | description: 'Diving into Dev: Thoughts on code, design, and more.', 10 | keywords: [...keywords, 'Notes', 'Thoughts', 'Code', 'Design'], 11 | ...getOgImage( 12 | 'Notes', 13 | 'Diving into Dev: Thoughts on code, design, and more.' 14 | ), 15 | }) 16 | 17 | export default async function NotesPage() { 18 | return ( 19 |
20 | 21 |

Notes

22 |
23 | 24 |

25 | Diving into Dev: Thoughts on code, design and more. 26 |

27 |
28 | 29 |
30 |
31 | {allNotes.map((note, index) => { 32 | return ( 33 | 34 | 35 | 36 | ) 37 | })} 38 |
39 |
40 |
41 | ) 42 | } 43 | -------------------------------------------------------------------------------- /app/(site)/(main)/page.tsx: -------------------------------------------------------------------------------- 1 | import WorkCard from '@/app/_components/work/work-card' 2 | import { generateMeta } from '@/lib/meta' 3 | import { Metadata } from 'next' 4 | import NodeAnimate from '@/app/_components/animate/node' 5 | import { allProjects } from 'contentlayer/generated' 6 | 7 | export const metadata: Metadata = generateMeta() 8 | 9 | export default function Home() { 10 | return ( 11 |
12 | 13 |

14 | 15 | Hi. I'm Nitin. 16 | 17 | A Developer. 18 |

19 |
20 | 21 |

22 | I love building software that's functional, user-friendly, 23 | scalable, and ensures a seamless experience for end-users. 24 |

25 |
26 | 30 |
31 | {allProjects.map((project) => { 32 | return ( 33 | 39 | ) 40 | })} 41 |
42 |
43 |
44 | ) 45 | } 46 | -------------------------------------------------------------------------------- /app/(site)/(single)/layout.tsx: -------------------------------------------------------------------------------- 1 | export default function RootLayout({ 2 | children, 3 | }: { 4 | children: React.ReactNode 5 | }) { 6 | return
{children}
7 | } 8 | -------------------------------------------------------------------------------- /app/(site)/(single)/note/[...slug]/page.tsx: -------------------------------------------------------------------------------- 1 | import { allNotes } from '@/.contentlayer/generated' 2 | import NoteWrapper from '@/app/_components/animate/wrapper' 3 | import { BackToTop } from '@/app/_components/back-to-top' 4 | import { Mdx } from '@/app/_components/mdx' 5 | import { ShareNote } from '@/app/_components/note/share-note' 6 | import { generateMeta, getOgImage } from '@/lib/meta' 7 | import { formatDate } from '@/lib/utils/date' 8 | import { getNoteLenghtIcon } from '@/lib/utils/note-length' 9 | import { Metadata } from 'next' 10 | import { notFound } from 'next/navigation' 11 | 12 | interface Props { 13 | params: { slug: string[] } 14 | } 15 | 16 | export async function generateMetadata({ params }: Props): Promise { 17 | const slug = params.slug[0] 18 | 19 | const note = allNotes.find((doc) => doc.slug === slug) 20 | 21 | if (!note) { 22 | return { 23 | title: '404 — Not Found', 24 | } 25 | } 26 | 27 | const { title, summary: description, publishedAt: publishedTime } = note 28 | 29 | const ogImage = getOgImage(title, description, { 30 | openGraph: { 31 | type: 'article', 32 | publishedTime, 33 | url: `https://nitinp.dev/note/${slug}`, 34 | }, 35 | }) 36 | 37 | return generateMeta({ 38 | title, 39 | description, 40 | ...ogImage, 41 | }) 42 | } 43 | 44 | export const dynamic = 'force-static' 45 | 46 | export default function Page({ params }: Props) { 47 | const note = allNotes.find((doc) => doc.slug === params.slug[0]) 48 | 49 | if (!note) { 50 | notFound() 51 | } 52 | 53 | const formattedDate = formatDate(note.publishedAt) 54 | 55 | return ( 56 | 57 | 63 |
64 |

65 | {note.title} 66 |

67 |
68 | 69 | 72 | 73 | 74 | 75 | {getNoteLenghtIcon(note.length)} 76 |

{note.length}

77 |
78 |
79 | 80 | 81 | 82 |
83 | ) 84 | } 85 | -------------------------------------------------------------------------------- /app/(site)/(single)/work/[...slug]/page.tsx: -------------------------------------------------------------------------------- 1 | import { notFound } from 'next/navigation' 2 | import AnimationWrapper from '@/app/_components/animate/wrapper' 3 | import { Metadata } from 'next' 4 | import { generateMeta, getOgImage, keywords } from '@/lib/meta' 5 | import { allProjects as projects } from 'contentlayer/generated' 6 | import Image from 'next/image' 7 | import { ExternalLink } from '@/app/_components/external-link' 8 | 9 | interface Props { 10 | params: { slug: string[] } 11 | } 12 | 13 | export async function generateMetadata({ params }: Props): Promise { 14 | const slug = params.slug[0] 15 | 16 | const project = projects.find((project) => project.slug === slug) 17 | 18 | if (!project) { 19 | return { 20 | title: '404 — Not Found', 21 | } 22 | } 23 | 24 | const { title, overview } = project 25 | 26 | const ogImage = getOgImage(title, overview, { 27 | openGraph: { 28 | type: 'article', 29 | url: `https://nitinp.dev/note/${slug}`, 30 | }, 31 | }) 32 | 33 | return generateMeta({ 34 | title: title + ' — Nitin Panwar', 35 | description: overview, 36 | keywords: [...keywords, 'Projects', 'Work', title], 37 | ...ogImage, 38 | }) 39 | } 40 | 41 | export default function Page({ params }: Props) { 42 | const project = projects.find((project) => project.slug === params.slug[0]) 43 | 44 | if (!project) { 45 | notFound() 46 | } 47 | 48 | return ( 49 | 50 |
51 |
52 |

{project.title}

53 |
54 |
55 |

{project.overview}

56 |
57 | {project.links?.map((link) => ( 58 | 59 | {link.name} 60 | 61 | ))} 62 |
63 |
64 |
65 | {project.content?.map((content, index) => ( 66 |

67 | {content} 68 |

69 | ))} 70 |
71 |
72 |
73 |
74 |
75 |
76 | Main Image 85 |
86 |
87 |
88 | ) 89 | } 90 | -------------------------------------------------------------------------------- /app/_components/animate/node.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { cn } from '@/lib/utils/cn' 4 | import { CustomDomComponent, cubicBezier, motion } from 'framer-motion' 5 | import { PropsWithChildren } from 'react' 6 | 7 | interface Props extends PropsWithChildren { 8 | delay?: number 9 | duration?: number 10 | ease?: (t: number) => number 11 | as?: keyof JSX.IntrinsicElements 12 | className?: string 13 | } 14 | 15 | export default function NodeAnimate({ 16 | children, 17 | delay = 0, 18 | duration = 0.5, 19 | ease = cubicBezier(0.6, 0.05, -0.01, 0.9), 20 | as: Component = 'div', 21 | className, 22 | }: Props) { 23 | const MotionComponent = motion(Component) as CustomDomComponent< 24 | JSX.IntrinsicElements[keyof JSX.IntrinsicElements] 25 | > 26 | return ( 27 | 39 | {children} 40 | 41 | ) 42 | } 43 | -------------------------------------------------------------------------------- /app/_components/animate/wrapper.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { cn } from '@/lib/utils/cn' 4 | import { Variants, cubicBezier, motion } from 'framer-motion' 5 | import Link from 'next/link' 6 | import { PropsWithChildren, useState } from 'react' 7 | 8 | const containerVariant: Variants = { 9 | initial: { 10 | y: 30, 11 | opacity: 0, 12 | }, 13 | animate: { 14 | y: 0, 15 | opacity: 1, 16 | transition: { 17 | duration: 0.8, 18 | ease: cubicBezier(0.6, 0.05, -0.01, 0.9), 19 | }, 20 | }, 21 | } 22 | 23 | interface Props extends PropsWithChildren { 24 | backTo: string 25 | } 26 | 27 | export default function Wrapper({ backTo, children }: Props) { 28 | const [isHovered, setHovered] = useState(false) 29 | 30 | return ( 31 | 37 | setHovered(true)} 39 | onMouseLeave={() => setHovered(false)} 40 | href={backTo} 41 | className='absolute w-10 h-10 top-9 rounded-full flex items-center justify-center border-border border-2 left-[50%] hover:shadow-link transition-shadow duration-500 -translate-x-[50%]'> 42 | 47 | 51 | 55 | 64 | 73 | 74 | 75 | 76 | 77 |
84 | {children} 85 |
86 |
87 | ) 88 | } 89 | -------------------------------------------------------------------------------- /app/_components/back-to-top.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { useEffect, useState } from 'react' 4 | import ExpandButton from './expand-button' 5 | import { cn } from '@/lib/utils/cn' 6 | 7 | /** 8 | * Renders a button that allows the user to scroll back to the top of the page. 9 | * 10 | * @return {JSX.Element} The JSX element representing the back to top button. 11 | */ 12 | export const BackToTop = () => { 13 | const [isExpanded, setIsExpanded] = useState(false) 14 | const [showButton, setShowButton] = useState(false) 15 | 16 | const scrollToTop = () => { 17 | window.scrollTo({ 18 | top: 0, 19 | behavior: 'smooth', 20 | }) 21 | } 22 | 23 | const handleScroll = () => { 24 | const scrollPosition = window.scrollY 25 | const scrollHeight = document.documentElement.scrollHeight 26 | const clientHeight = document.documentElement.clientHeight 27 | const scrollPercentage = 28 | (scrollPosition / (scrollHeight - clientHeight)) * 100 29 | setShowButton(scrollPercentage > 40) 30 | } 31 | 32 | useEffect(() => { 33 | window.addEventListener('scroll', handleScroll) 34 | 35 | return () => { 36 | window.removeEventListener('scroll', handleScroll) 37 | } 38 | }, []) 39 | 40 | return ( 41 | 51 | ) 52 | } 53 | -------------------------------------------------------------------------------- /app/_components/expand-button.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import React from 'react' 4 | import { motion, Variants, cubicBezier } from 'framer-motion' 5 | import { cn } from '@/lib/utils/cn' 6 | 7 | interface Props { 8 | isExpanded: boolean 9 | text: string 10 | } 11 | 12 | const buttonVariants: Variants = { 13 | expanded: { 14 | width: 'auto', 15 | transition: { duration: 0.8, ease: [0.6, 0.05, -0.01, 0.9] }, 16 | }, 17 | collapsed: { 18 | width: '44px', 19 | transition: { duration: 0.8, ease: [0.6, 0.05, -0.01, 0.9] }, 20 | }, 21 | } 22 | 23 | const textVariants: Variants = { 24 | show: { 25 | opacity: 1, 26 | x: 0, 27 | transition: { 28 | duration: 0.6, 29 | ease: cubicBezier(0.6, 0.05, -0.01, 0.9), 30 | delay: 0.3, 31 | }, 32 | }, 33 | hide: { 34 | opacity: 0, 35 | x: -12, 36 | transition: { 37 | duration: 0.6, 38 | ease: cubicBezier(0.6, 0.05, -0.01, 0.9), 39 | }, 40 | }, 41 | } 42 | 43 | const ExpandButton: React.FC = ({ isExpanded, text }) => { 44 | return ( 45 | 55 | 60 | {text} 61 | 62 |
63 | 69 | 73 | 83 | 93 | 94 | 99 | 100 |
101 |
102 | ) 103 | } 104 | 105 | export default ExpandButton 106 | -------------------------------------------------------------------------------- /app/_components/external-link.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { PropsWithChildren } from 'react' 4 | import { motion } from 'framer-motion' 5 | 6 | type Props = PropsWithChildren & React.AnchorHTMLAttributes 7 | 8 | export const ExternalLink = ({ children, href }: Props) => { 9 | return ( 10 | 16 |

{children}

17 | 29 | 30 | 40 | 50 | 51 | 56 | 57 |
58 | ) 59 | } 60 | -------------------------------------------------------------------------------- /app/_components/icons/HeartIcon.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { SVGProps } from 'react' 3 | 4 | const HeartIcon = (props: SVGProps) => ( 5 | 12 | 20 | 21 | ) 22 | export default HeartIcon 23 | -------------------------------------------------------------------------------- /app/_components/icons/LinkIcon.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { SVGProps } from 'react' 3 | const LinkIcon = (props: SVGProps) => ( 4 | 11 | 12 | 13 | 14 | 15 | ) 16 | export default LinkIcon 17 | -------------------------------------------------------------------------------- /app/_components/icons/LinkedIcon.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { SVGProps } from 'react' 3 | const LinkedinIcon = (props: SVGProps) => ( 4 | 11 | 20 | 27 | 28 | 34 | 35 | ) 36 | export default LinkedinIcon 37 | -------------------------------------------------------------------------------- /app/_components/icons/StarIcon.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { SVGProps } from 'react' 3 | 4 | export const StarIcon = (props: SVGProps) => ( 5 | 12 | 16 | 17 | ) 18 | -------------------------------------------------------------------------------- /app/_components/icons/TwitterIcon.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { SVGProps } from 'react' 3 | const TwitterIcon = (props: SVGProps) => ( 4 | 11 | 17 | 18 | ) 19 | export default TwitterIcon 20 | -------------------------------------------------------------------------------- /app/_components/mdx/image.tsx: -------------------------------------------------------------------------------- 1 | import Image, { ImageProps } from 'next/image' 2 | 3 | export const RoundedImage: React.FC = (props) => { 4 | const { alt, ...rest } = props 5 | return {props.alt} 6 | } 7 | -------------------------------------------------------------------------------- /app/_components/mdx/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { useMDXComponent } from 'next-contentlayer/hooks' 3 | import { MDXComponents } from 'mdx/types' 4 | import { CustomLink } from './link' 5 | import { RoundedImage } from './image' 6 | 7 | const components: MDXComponents = { 8 | Image: RoundedImage, 9 | a: CustomLink, 10 | } 11 | 12 | export function Mdx({ code }: { code: string }) { 13 | const Component = useMDXComponent(code) 14 | 15 | return ( 16 |
17 | 18 |
19 | ) 20 | } 21 | -------------------------------------------------------------------------------- /app/_components/mdx/link.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link' 2 | 3 | export const CustomLink: React.FC< 4 | React.AnchorHTMLAttributes 5 | > = (props) => { 6 | const href = props.href 7 | 8 | if (href?.startsWith('/')) { 9 | return ( 10 | 11 | {props.children} 12 | 13 | ) 14 | } 15 | 16 | if (href?.startsWith('#')) { 17 | return 18 | } 19 | 20 | return 21 | } 22 | -------------------------------------------------------------------------------- /app/_components/navbar/index.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import useScrollPosition from '@/hooks/useScrollPosition' 4 | import { cn } from '@/lib/utils/cn' 5 | import { motion } from 'framer-motion' 6 | import Link from 'next/link' 7 | import { usePathname } from 'next/navigation' 8 | import { useLayoutEffect, useMemo, useRef, useState } from 'react' 9 | 10 | const Navbar = () => { 11 | const pathname = usePathname() 12 | const { isScrolled } = useScrollPosition({ threshold: 130 }) 13 | 14 | const currentElRef = useRef>({}) 15 | const [highlighterDimensions, setHighlighterDimensions] = useState({ 16 | width: 200, 17 | left: 0, 18 | }) 19 | 20 | const handleOnClick = (key: string) => () => { 21 | setHighlighterDimensions({ 22 | width: currentElRef.current[key]?.offsetWidth as number, 23 | left: currentElRef.current[key]?.offsetLeft as number, 24 | }) 25 | } 26 | 27 | useLayoutEffect(() => { 28 | const path = pathname?.split('/')[1] 29 | setHighlighterDimensions({ 30 | width: currentElRef.current[path]?.offsetWidth as number, 31 | left: currentElRef.current[path]?.offsetLeft as number, 32 | }) 33 | }, []) 34 | 35 | const navLinks = useMemo(() => { 36 | return [ 37 | { 38 | href: '', 39 | label: 'Work', 40 | }, 41 | { 42 | href: 'about', 43 | label: 'About', 44 | }, 45 | { 46 | href: 'notes', 47 | label: 'Notes', 48 | }, 49 | ] 50 | }, []) 51 | 52 | return ( 53 |
54 | 96 |
97 | ) 98 | } 99 | 100 | export default Navbar 101 | -------------------------------------------------------------------------------- /app/_components/note/index.ts: -------------------------------------------------------------------------------- 1 | export * from './item' 2 | -------------------------------------------------------------------------------- /app/_components/note/item.tsx: -------------------------------------------------------------------------------- 1 | import { Note } from 'contentlayer/generated' 2 | import Image from 'next/image' 3 | import Link from 'next/link' 4 | 5 | interface Props { 6 | note: Note 7 | } 8 | 9 | export function NoteItem({ note }: Props) { 10 | return ( 11 | 14 |
15 |
16 |
17 | {note.title} 18 |
19 |

{note.summary}

20 |
21 |
22 |
23 | {note.title} 30 |
31 |
32 |
33 | 34 | ) 35 | } 36 | -------------------------------------------------------------------------------- /app/_components/note/share-note.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import HeartIcon from '../icons/HeartIcon' 4 | import LinkIcon from '../icons/LinkIcon' 5 | import LinkedinIcon from '../icons/LinkedIcon' 6 | import TwitterIcon from '../icons/TwitterIcon' 7 | 8 | interface ShareNoteProps { 9 | noteSlug: string 10 | } 11 | 12 | export const ShareNote: React.FC = ({ noteSlug }) => { 13 | const link = `https://nitinp.dev/note/${noteSlug}` 14 | const copyCurrentLink = () => { 15 | navigator.clipboard.writeText(link) 16 | } 17 | 18 | return ( 19 |
46 | ) 47 | } 48 | -------------------------------------------------------------------------------- /app/_components/providers.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { ThemeProvider } from 'next-themes' 4 | import { PropsWithChildren } from 'react' 5 | import { ReactLenis } from '@studio-freight/react-lenis' 6 | 7 | export const Providers: React.FC = ({ children }) => { 8 | return ( 9 | 10 | {children} 11 | 12 | ) 13 | } 14 | -------------------------------------------------------------------------------- /app/_components/timeline.tsx: -------------------------------------------------------------------------------- 1 | import { Experience } from '@/.contentlayer/generated' 2 | import { formatDate } from '@/lib/utils/date' 3 | 4 | const DATE_FORMAT = 'MMMM YYYY' 5 | 6 | export function ExperienceItem({ 7 | exp: { content, company, title, link, startDate, endDate, location }, 8 | }: { 9 | exp: Experience 10 | }) { 11 | return ( 12 | <> 13 |
14 |
15 |
16 | 22 | 26 | 27 |

{title}

28 | 29 |
30 | {link ? ( 31 | 36 | {company} 37 | 38 | ) : ( 39 |

{company}

40 | )} 41 | 42 |

{location}

43 |
44 |

45 | {formatDate(startDate, DATE_FORMAT)} -{' '} 46 | {endDate ? formatDate(endDate, DATE_FORMAT) : 'Present'} 47 |

48 |
49 |
50 |
53 |
54 | 55 | ) 56 | } 57 | -------------------------------------------------------------------------------- /app/_components/waving-hand.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { motion } from 'framer-motion' 4 | 5 | export const WavingHand = () => { 6 | return ( 7 | 23 | 👋 24 | 25 | ) 26 | } 27 | -------------------------------------------------------------------------------- /app/_components/work/work-card.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { cn } from '@/lib/utils/cn' 4 | import Image from 'next/image' 5 | import ExpandButton from '../expand-button' 6 | import { useState } from 'react' 7 | import Link from 'next/link' 8 | 9 | interface CardProps { 10 | src: string 11 | alt: string 12 | to: string 13 | className?: string 14 | } 15 | 16 | const WorkCard: React.FC = ({ src, alt, to, className }) => { 17 | const [isHovered, setHovered] = useState(false) 18 | const [isImageLoaded, setImageLoaded] = useState(false) 19 | 20 | // Render Button only when image has already loaded for better UX 21 | const handleImageLoad = () => { 22 | setImageLoaded(true) 23 | } 24 | 25 | return ( 26 | setHovered(true)} 29 | onMouseLeave={() => setHovered(false)} 30 | className={cn( 31 | className, 32 | 'relative inline-flex rounded-3xl overflow-hidden max-h-[20rem] md:max-h-[550px] cursor-pointer max-w-full md:max-w-[47%]' 33 | )}> 34 | {alt} 43 | {isImageLoaded && ( 44 |
45 | 46 |
47 | )} 48 | 49 | ) 50 | } 51 | 52 | export default WorkCard 53 | -------------------------------------------------------------------------------- /app/error.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { useEffect } from 'react' 4 | 5 | export default function Error({ 6 | error, 7 | reset, 8 | }: { 9 | error: Error 10 | reset: () => void 11 | }) { 12 | useEffect(() => { 13 | // Log the error to an error reporting service 14 | console.error(error) 15 | }, [error]) 16 | 17 | return ( 18 |
19 |

Oh no, something went wrong... maybe refresh?

20 |
21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nitintf/www/8d9c401e9678c01a8d0657dad49b3da56e1233a6/app/favicon.ico -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import '../styles/globals.css' 2 | import '../styles/prose.css' 3 | import { Providers } from '@/app/_components/providers' 4 | import { cn } from '@/lib/utils/cn' 5 | import { primary, secondary } from '@/lib/fonts' 6 | import { Analytics } from '@vercel/analytics/react' 7 | import { SpeedInsights } from '@vercel/speed-insights/next' 8 | 9 | export default function RootLayout({ 10 | children, 11 | }: { 12 | children: React.ReactNode 13 | }) { 14 | return ( 15 | 16 | 23 | {children} 24 | 25 | 26 | 27 | 28 | ) 29 | } 30 | -------------------------------------------------------------------------------- /app/og/route.tsx: -------------------------------------------------------------------------------- 1 | import { ImageResponse } from 'next/og' 2 | import { NextRequest } from 'next/server' 3 | 4 | export const runtime = 'edge' 5 | 6 | export async function GET(req: NextRequest) { 7 | const { searchParams } = req.nextUrl 8 | const pageTitle = searchParams.get('title') 9 | const pageDesc = searchParams.get('desc') 10 | const primaryFont = fetch( 11 | new URL('../../public/fonts/acorn.woff', import.meta.url) 12 | ).then((res) => res.arrayBuffer()) 13 | const primaryFontData = await primaryFont 14 | 15 | const secondaryFont = fetch( 16 | new URL('../../public/fonts/gt.woff', import.meta.url) 17 | ).then((res) => res.arrayBuffer()) 18 | const secondaryFontData = await secondaryFont 19 | 20 | return new ImageResponse( 21 | ( 22 |
34 |

46 | nitinp.dev 47 |

48 |
61 | {pageTitle} 62 |
63 |

76 | {pageDesc} 77 |

78 |
79 | ), 80 | { 81 | width: 1920, 82 | height: 1080, 83 | fonts: [ 84 | { 85 | name: 'Acorn', 86 | data: primaryFontData, 87 | style: 'normal', 88 | }, 89 | { 90 | name: 'GT', 91 | data: secondaryFontData, 92 | style: 'normal', 93 | }, 94 | ], 95 | } 96 | ) 97 | } 98 | -------------------------------------------------------------------------------- /app/robots.ts: -------------------------------------------------------------------------------- 1 | import { MetadataRoute } from 'next' 2 | 3 | export default function robots(): MetadataRoute.Robots { 4 | return { 5 | rules: { 6 | userAgent: '*', 7 | allow: '/', 8 | }, 9 | sitemap: 'https://nitinp.dev/sitemap.xml', 10 | host: 'https://nitinp.dev', 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /app/sitemap.ts: -------------------------------------------------------------------------------- 1 | import { allNotes, allProjects as projects } from '@/.contentlayer/generated' 2 | 3 | export default async function sitemap() { 4 | const projectsRoutes = projects.map((project) => ({ 5 | url: `https://nitinp.dev/work/${project.slug}`, 6 | lastModified: new Date().toISOString().split('T')[0], 7 | })) 8 | 9 | const routes = ['', '/about', '/codelabs', '/notes'].map((route) => ({ 10 | url: `https://nitinp.dev${route}`, 11 | lastModified: new Date().toISOString().split('T')[0], 12 | })) 13 | 14 | const notesRoutes = allNotes.map((note) => ({ 15 | url: `https://nitinp.dev/note/${note.slug}`, 16 | lastModified: new Date().toISOString().split('T')[0], 17 | })) 18 | 19 | return [...routes, ...projectsRoutes, ...notesRoutes] 20 | } 21 | -------------------------------------------------------------------------------- /content/experiences/bitcs.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Experience", 3 | "title": "Software Engineer", 4 | "company": "BITCS", 5 | "link": "https://bitcs.in", 6 | "content": "led Raven system creation for streamlined issue management. Ensured end-to-end functionality, improved stability by implementing error handling, and achieved a +75% increase in test coverage for enhanced reliability.", 7 | "startDate": "2022-01-01", 8 | "endDate": null, 9 | "location": "Noida, India" 10 | } 11 | -------------------------------------------------------------------------------- /content/experiences/sapio.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Experience", 3 | "title": "Frontend Developer Intern", 4 | "company": "Sapio Analytics", 5 | "link": "https://sapioanalytics.com/", 6 | "content": "As a Frontend Developer Intern at Sapio Analytics, led the creation of an interactive dashboard for insightful analytics. Collaborated with cross-functional teams for real-time updates, integrated MapBox for intuitive map features, enhancing the user experience.", 7 | "startDate": "2021-09-01", 8 | "endDate": "2021-12-31", 9 | "location": "Mumbai, India" 10 | } 11 | -------------------------------------------------------------------------------- /content/notes/go-cli-ip-tracker/go-cli-ip-tracker.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | type: Note 3 | title: 'Go CLI Magic.' 4 | summary: 'Build your own IP tracker CLI tool using Go and Cobra' 5 | publishedAt: 2023-12-11 6 | length: Long 7 | image: go 8 | --- 9 | 10 | Go, a popular programming language, is celebrated for its simplicity, speed, and 11 | reliability, making it perfect for crafting command-line tools. 12 | 13 | Building a CLI tool involves handling command line inputs, outputs, and other 14 | system resources. As your app grows, managing this complexity can get tricky. 15 | 16 | Enter [Cobra](https://cobra.dev/), a fantastic Go library. It simplifies 17 | building scalable, maintainable, and extensible command-line interfaces. 18 | 19 | In this tutorial, I'll guide you through using Cobra to create a Go CLI tool. 20 | We'll focus on building an Ip Tracker tool to effortlessly track IP addresses. 21 | Let's explore Cobra's key features and benefits along the way! 22 | 23 | ### Prerequisites 24 | 25 | 26 | - Basic understanding of Go programming. - If you're new to Go, check out the 27 | [official Go tutorial](https://tour.golang.org/welcome/1). - Ensure you have 28 | Go version 1.19 or above installed. You can download it 29 | [here](https://golang.org/dl/). 30 | 31 | 32 | ## Install Cobra Generator: 33 | 34 | To streamline the integration of Cobra into your application, we recommend using 35 | the Cobra CLI. Install it effortlessly with the following command: 36 | 37 | ```bash 38 | go install github.com/spf13/cobra-cli@latest 39 | ``` 40 | 41 | Once installed, access the CLI by typing `cobra-cli` in the terminal. 42 | 43 | ## Create a New Project: 44 | 45 | Start by setting up the project directory. Execute the following commands to 46 | create a new folder for your application and navigate into it: 47 | 48 | ```bash 49 | mkdir ip_tracker && cd ip_tracker 50 | ``` 51 | 52 | Now, initiate a Go module for your project: 53 | 54 | ```bash 55 | go mod init iptracker 56 | ``` 57 | 58 | With the module in place, create the application using the Cobra CLI 59 | 60 | ```bash 61 | cobra-cli init 62 | ``` 63 | 64 | ## Adding Commands: 65 | 66 | In the world of CLIs, commands are like the heartbeats—they do things. They're 67 | the main players in your application, each representing a specific action. 68 | 69 | Every Cobra app has a root command. When you say the CLI's name, this command 70 | wakes up. Normally, it shares info about the app, but you can make it do 71 | something special. You'll find this root command in `cmd/root.go`. Open it up 72 | and make it look like this: 73 | 74 | ```go 75 | package cmd 76 | 77 | import ( 78 | "fmt" 79 | "github.com/spf13/cobra" 80 | "os" 81 | ) 82 | 83 | var rootCmd = &cobra.Command{ 84 | Use: "iptracker", 85 | Short: "Track IP addresses", 86 | Long: `A Go based CLI tool to track IP addresses`, 87 | Run: func(cmd *cobra.Command, args []string) { 88 | fmt.Println("Welcome to iptracker_cli!") 89 | }, 90 | } 91 | 92 | func Execute() { 93 | if err := rootCmd.Execute(); err != nil { 94 | fmt.Println(err) 95 | os.Exit(1) 96 | } 97 | } 98 | 99 | func init() {} 100 | ``` 101 | 102 | Now you need to call this `Execute` in the `main.go` file in the root dir. 103 | 104 | ```go 105 | package main 106 | 107 | import "github.com/nitintf/iptracker/cmd" 108 | 109 | func main() { 110 | cmd.Execute() 111 | } 112 | ``` 113 | 114 | The `Execute` function is responsible for running the root command of the CLI 115 | tool. It calls `rootCmd.Execute()`, which initiates the execution of the command 116 | and any sub-commands. If an error occurs during execution, the function exits 117 | the program with an exit status of 1. 118 | 119 | This function is typically found in the main file 120 | of your Go application. It acts as the entry point, ensuring that the Cobra 121 | commands are executed appropriately. 122 | 123 | The `init` function is a special Go function that is automatically called before 124 | the `main` function is executed. In this case, the `init` function is currently 125 | empty, but you can use it for any initialization code that needs to run before 126 | the CLI tool starts. 127 | 128 | Common use cases for the init function include 129 | setting up configuration, initializing variables, or registering flags or 130 | subcommands. If your application doesn't require any specific initialization at 131 | this stage, the init function can remain empty or you can remove 132 | it. 133 | 134 | ## Root Command: What Happends on Execution 135 | 136 | In the heart of our CLI is the root command. This command comes to life when we 137 | call our CLI name `iptracker`. Let's break down what happens when this command 138 | is executed in the `cmd/root.go`file: 139 | 140 | ```go 141 | Run: func(cmd *cobra.Command, args []string) { 142 | ipAdd, err := getPrivateIpAddress() 143 | 144 | if err != nil { 145 | print.Error("Unable to retrieve Private IP Address") 146 | } 147 | 148 | publicIpAddr, err := getPublicIPAddress() 149 | 150 | if err != nil { 151 | print.Error("Unable to retrieve Public IP Address") 152 | return 153 | } 154 | 155 | print.Info("\nPrivate IP Address: " + ipAdd) 156 | ip.ShowIpData(publicIpAddr) 157 | }, 158 | ``` 159 | 160 | ### Here's a step-by-step breakdown: 161 | 162 | 1. Get Private IP Address: 163 | 164 | - Call getPrivateIpAddress to obtain the private IP address. 165 | - Print an error message if there's an issue. 166 | 167 | 2. Get Public IP Address: 168 | 169 | - Call getPublicIPAddress to obtain the public IP address using an external 170 | API. 171 | - Print an error message and exit if there's an issue. 172 | 173 | 3. Display Information: 174 | - If both private and public IP addresses are successfully obtained, print 175 | the private IP address. 176 | - Call 177 | [ip.ShowIpData](https://github.com/nitintf/iptracker-cli/blob/main/internal/ip/ip.go) 178 | to display information about the public IP address. 179 | 180 | ## Implementing IP Address Retrieval Functions 181 | 182 | Now that we've laid the groundwork for our 'iptracker_cli' project, it's time to 183 | implement the functions responsible for fetching both private and public IP 184 | addresses. 185 | 186 | ```go 187 | type PublicIp struct { 188 | Origin string `json:"origin"` 189 | } 190 | 191 | func getPublicIPAddress() (string, error) { 192 | resp, err := http.Get("http://httpbin.org/ip") 193 | if err != nil { 194 | return "", err 195 | } 196 | defer resp.Body.Close() 197 | 198 | body, err := ioutil.ReadAll(resp.Body) 199 | if err != nil { 200 | return "", err 201 | } 202 | 203 | data := PublicIp{} 204 | 205 | err = json.Unmarshal(body, &data) 206 | 207 | if err != nil { 208 | return "", err 209 | } 210 | 211 | return data.Origin, nil 212 | } 213 | ``` 214 | 215 | In this function: 216 | 217 | - We make an HTTP request to http://httpbin.org/ip. 218 | - Read the response body and use JSON unmarshaling to extract the public IP 219 | address. 220 | - Return the public IP address and any potential errors. 221 | 222 | ```go 223 | func getPrivateIpAddress() (string, error) { 224 | addrs, err := net.InterfaceAddrs() 225 | 226 | if err != nil { 227 | return "", err 228 | } 229 | 230 | var ipAddress string 231 | 232 | for _, address := range addrs { 233 | if ipnet, ok := address.(*net.IPNet); ok && !ipnet.IP.IsLoopback() { 234 | if ipnet.IP.To4() != nil { 235 | ipAddress = ipnet.IP.String() 236 | break 237 | } 238 | } 239 | } 240 | 241 | if ipAddress == "" { 242 | return "", err 243 | } 244 | 245 | return ipAddress, nil 246 | } 247 | ``` 248 | 249 | Here's what happens in this function: 250 | 251 | - We use `net.InterfaceAddrs()` to retrieve network interface addresses. 252 | - Iterate through these addresses, filtering out loopback and multicast 253 | addresses. 254 | - Return the first `IPv4` address found, which represents the private IP 255 | address. 256 | 257 | With these functions in place, it's time to put `iptracker` to test. now, your 258 | 'iptracker' is ready to be executed. Simply call the following commands: 259 | 260 | ```bash 261 | go run main.go 262 | ``` 263 | 264 | Result for root command 270 | 271 | ## Introducing the `track` Sub-Command 272 | 273 | Now, let's enhance the capabilities of our `iptracker` with a sleek new 274 | sub-command: `track` subcommand allows you to track an IP address instead of 275 | getting your own IP address. It takes a single argument - the IP address. 276 | 277 | ### Creating the 'track' Sub-Command 278 | 279 | ```bash 280 | # Create a new file in cmd for our new command 281 | 282 | touch cmd/track.go 283 | ``` 284 | 285 | Let's add basic cobra arguments to it 286 | 287 | ```go 288 | package cmd 289 | 290 | import ( 291 | "regexp" 292 | 293 | "github.com/nitintf/iptracker/internal/ip" 294 | "github.com/nitintf/iptracker/internal/print" 295 | "github.com/spf13/cobra" 296 | ) 297 | 298 | var trackCmd = &cobra.Command{ 299 | Use: "track", 300 | Short: "Track IP address information", 301 | Long: `sub-command for the IP address CLI tool, allowing you to track and gather information about a specific IP address`, 302 | Args: cobra.ExactArgs(1), 303 | Run: func(cmd *cobra.Command, args []string) {}, 304 | } 305 | 306 | func init() { 307 | rootCmd.AddCommand(trackCmd) 308 | } 309 | ``` 310 | 311 | Behind the scenes, in the `init()` function, we're 312 | essentially saying, "Hey, `iptracker` meet `track`. This makes `track` part of 313 | the big family, making it easy for you to use commands like 314 | `iptracker track `. It's like giving our CLI a new skill, allowing 315 | you to explore specific IP addresses effortlessly. 316 | 317 | Now, add this to our `track` root method 318 | 319 | ```go {17-29} 320 | package cmd 321 | 322 | import ( 323 | "regexp" 324 | 325 | "github.com/nitintf/iptracker/internal/ip" 326 | "github.com/nitintf/iptracker/internal/print" 327 | "github.com/spf13/cobra" 328 | ) 329 | 330 | var trackCmd = &cobra.Command{ 331 | Use: "track", 332 | Short: "Track IP address information", 333 | Long: `sub-command for the IP address CLI tool, allowing you to track and gather information about a specific IP address`, 334 | Args: cobra.ExactArgs(1), 335 | Run: func(cmd *cobra.Command, args []string) { 336 | if len(args) > 0 { 337 | ipAdd := args[0] 338 | isValid := isValidIPAddress(ipAdd) 339 | 340 | if isValid { 341 | ip.ShowIpData(ipAdd) 342 | } else { 343 | print.Error("Invalid IP address.") 344 | } 345 | } else { 346 | print.Error("Please provide IP to trace.") 347 | } 348 | }, 349 | } 350 | 351 | func init() { 352 | rootCmd.AddCommand(trackCmd) 353 | } 354 | ``` 355 | 356 | ### Executing the 'track' Sub-Command 357 | 358 | To execute the `track` subcommand and see its functionality in action, you can 359 | run the following command from your terminal: 360 | 361 | ```bash 362 | go run main.go track 8.8.8.8 363 | ``` 364 | 365 | ## Install the CLI 366 | 367 | At the moment, you have to run the main.go file in order to run your commands. 368 | However, there’s one more thing you can do to access your commands without even 369 | accessing your code. You can build your application as an executable binary and 370 | install it to your $GOPATH/bin folder. Do this by running the following 371 | commands. 372 | 373 | ```bash 374 | go build main.go 375 | go install 376 | ``` 377 | 378 | Now, run your application by typing in the following command. 379 | 380 | ```bash 381 | iptracker 382 | # OR iptracker track 383 | ``` 384 | 385 | ## There you go! 386 | 387 | You have now created a simple `iptracker` CLI using Cobra and Go that can be 388 | used to get information about an IP Address. That's it for this tutorial on 389 | creating a simple `Go` based Command Line Interface (CLI). I hope that helps 390 | inspire you to create more CLI tools and explore the vast possibilities of Go 391 | and Cobra. 392 | 393 | Feel free to explore the entire codebase on 394 | [GitHub](https://github.com/nitintf/iptracker-cli/) if you ever need a helping 395 | hand. 396 | -------------------------------------------------------------------------------- /content/projects/poly.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Project", 3 | "cover": "/images/projects/poly/poly.webp", 4 | "title": "Poly", 5 | "slug": "poly", 6 | "role": "Fullstack Developer", 7 | "links": [ 8 | { 9 | "name": "Github", 10 | "href": "https://github.com/nitintf/poly-client" 11 | } 12 | ], 13 | "overview": "Poly is a robust project management tool designed to streamline collaboration and enhance efficiency within teams.", 14 | "content": [ 15 | "Poly is a robust project management tool designed to streamline collaboration and enhance efficiency within teams. Built with a tech stack including React, GraphQL, Apollo, Tailwind, and Vite on the frontend, and NestJs, PostgreSQL, Prisma, GraphQL, and Resend on the backend, Poly offers a comprehensive suite of features that enable teams to manage tasks, share resources, and communicate seamlessly. With a user-friendly interface and real-time updates, Poly ensures that teams can stay on top of their projects without any hassle.", 16 | "Poly streamlines task tracking for teams. It enables easy assignment, prioritization, and progress monitoring. It fosters collaboration with a centralized workspace and intuitive interface. Its advanced tech stack ensures a reliable user experience." 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /content/projects/task-manager.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Project", 3 | "cover": "/images/projects/task/task.webp", 4 | "title": "Task Manager", 5 | "slug": "task-manager", 6 | "role": "App Developer", 7 | "links": [ 8 | { 9 | "name": "Github", 10 | "href": "https://github.com/nitintf/poly" 11 | } 12 | ], 13 | "overview": "Poly Task Manager allows easy task management on iOS and Android. Create, edit, and complete tasks on your mobile device.", 14 | "content": [ 15 | "Poly Task Manager, built with a frontend tech stack of React Native, Expo, and MobX, and a backend powered by Supabase, is a robust and intuitive task management application. It empowers users to manage their tasks with utmost ease and efficiency, regardless of the platform they are on - be it iOS or Android.", 16 | 17 | "Poly Task Manager streamlines task management, allowing task creation, editing, and completion from your mobile device, whether you're at home, work, or on the go. It also offers efficient organization with workspaces, powered by Supabase, for grouping tasks based on projects, teams, or custom categories to suit your workflow." 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /content/projects/zaars.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Project", 3 | "cover": "/images/projects/zaars/zaars.webp", 4 | "title": "Zaars", 5 | "slug": "zaars", 6 | "role": "Lead Developer", 7 | "links": [ 8 | { 9 | "name": "Web", 10 | "href": "https://zaars.co" 11 | } 12 | ], 13 | "overview": "Zaars is a dynamic multivendor ecommerce platform, for showcasing and selling diverse ethnic fashion.", 14 | "content": [ 15 | "Zaars is a vibrant and dynamic multivendor ecommerce platform that celebrates diversity in ethnic fashion. Built on WordPress and powered by PHP, Zaars provides a unique marketplace where individuals can sign up, become sellers, and showcase a stunning array of both new and preused ethnic wears, Zaars is your one-stop destination for all things ethnic and fashionable.", 16 | 17 | "Zaars provides a comprehensive ecommerce experience with real-time order tracking, secure payments, and a user-friendly interface." 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /contentlayer.config.ts: -------------------------------------------------------------------------------- 1 | import { makeSource } from 'contentlayer/source-files' 2 | import remarkGfm from 'remark-gfm' 3 | import rehypePrettyCode from 'rehype-pretty-code' 4 | import rehypeSlug from 'rehype-slug' 5 | import rehypeAutolinkHeadings from 'rehype-autolink-headings' 6 | 7 | // Models 8 | import { Note } from './models/note' 9 | import { Project } from './models/project' 10 | import { Experience } from './models/experience' 11 | 12 | export default makeSource({ 13 | contentDirPath: 'content', 14 | documentTypes: [Note, Project, Experience], 15 | mdx: { 16 | remarkPlugins: [remarkGfm], 17 | rehypePlugins: [ 18 | rehypeSlug, 19 | [ 20 | rehypePrettyCode, 21 | { 22 | theme: 'nord', 23 | onVisitLine(node: any) { 24 | // Prevent lines from collapsing in `display: grid` mode, and allow empty 25 | // lines to be copy/pasted 26 | if (node.children.length === 0) { 27 | node.children = [{ type: 'text', value: ' ' }] 28 | } 29 | }, 30 | onVisitHighlightedLine(node: any) { 31 | node.properties.className = ['line--highlighted'] 32 | }, 33 | onVisitHighlightedChars(node: any) { 34 | node.properties.className = ['word--highlighted'] 35 | }, 36 | }, 37 | ], 38 | [ 39 | rehypeAutolinkHeadings, 40 | { 41 | properties: { 42 | className: ['anchor'], 43 | }, 44 | }, 45 | ], 46 | ], 47 | }, 48 | }) 49 | -------------------------------------------------------------------------------- /hooks/useScrollPosition.ts: -------------------------------------------------------------------------------- 1 | import { useMotionValueEvent, useScroll } from 'framer-motion' 2 | import { useState } from 'react' 3 | 4 | interface ScrollPositionProps { 5 | threshold: number 6 | } 7 | 8 | const useScrollPosition = ({ threshold }: ScrollPositionProps) => { 9 | const [isScrolled, setIsScrolled] = useState(false) 10 | const { scrollY } = useScroll() 11 | 12 | useMotionValueEvent(scrollY, 'change', (latest) => { 13 | setIsScrolled(latest >= threshold) 14 | }) 15 | 16 | return { isScrolled } 17 | } 18 | 19 | export default useScrollPosition 20 | -------------------------------------------------------------------------------- /lib/env.ts: -------------------------------------------------------------------------------- 1 | export function assertValue(v: T | undefined, errorMessage: string): T { 2 | if (v === undefined) { 3 | throw new Error(errorMessage) 4 | } 5 | 6 | return v 7 | } 8 | 9 | export const IsDev = process.env.NODE_ENV === 'development' 10 | -------------------------------------------------------------------------------- /lib/fonts.ts: -------------------------------------------------------------------------------- 1 | import localFont from 'next/font/local' 2 | 3 | export const primary = localFont({ 4 | src: '../public/fonts/acorn.woff', 5 | variable: '--font-primary', 6 | }) 7 | 8 | export const secondary = localFont({ 9 | src: [ 10 | { 11 | path: '../public/fonts/gt.woff', 12 | weight: '500', 13 | }, 14 | ], 15 | variable: '--font-secondary', 16 | }) 17 | -------------------------------------------------------------------------------- /lib/meta.ts: -------------------------------------------------------------------------------- 1 | import type { Metadata } from 'next' 2 | import { IsDev } from './env' 3 | 4 | const title = `Nitin Panwar` 5 | const description = `Fullstack Software engineer & Designer from India` 6 | const domain = `nitinp.dev` 7 | const twitter = `@nitinpanwarr` 8 | const meta = `Software Engineer` 9 | const site = `https://${domain}` 10 | const ogUrl = IsDev ? 'http://localhost:3000' : 'https://nitinp.dev' 11 | export const keywords = [ 12 | 'Nitin', 13 | 'Nitin Panwar', 14 | 'Software Engineer', 15 | 'Fullstack Developer', 16 | ] 17 | 18 | export const getOgImage = ( 19 | title: string, 20 | desc: string, 21 | meta?: Pick 22 | ) => { 23 | const ogImg = `${ogUrl}/og?title=${title}&desc=${desc}` 24 | 25 | return { 26 | openGraph: { 27 | title, 28 | type: 'website', 29 | url: site, 30 | siteName: title, 31 | description: desc, 32 | images: [ 33 | { 34 | url: ogImg, 35 | width: 1920, 36 | height: 1080, 37 | alt: title, 38 | }, 39 | ], 40 | locale: 'en-US', 41 | ...meta?.openGraph, 42 | }, 43 | twitter: { 44 | creator: twitter, 45 | card: 'summary_large_image', 46 | site: twitter, 47 | title, 48 | images: [ 49 | { 50 | url: ogImg, 51 | alt: title, 52 | }, 53 | ], 54 | }, 55 | } 56 | } 57 | 58 | export const seo: Metadata = { 59 | title: title + ' — ' + meta, 60 | description, 61 | ...getOgImage(title + ' — ' + meta, description), 62 | metadataBase: new URL('https://nitinp.dev'), 63 | icons: { 64 | icon: '/favicon.ico', 65 | }, 66 | keywords, 67 | robots: { 68 | index: true, 69 | follow: true, 70 | googleBot: { 71 | index: true, 72 | follow: true, 73 | 'max-video-preview': -1, 74 | 'max-image-preview': 'large', 75 | 'max-snippet': -1, 76 | }, 77 | }, 78 | } 79 | 80 | export const generateMeta = (meta?: Metadata) => ({ 81 | ...seo, 82 | ...meta, 83 | }) 84 | -------------------------------------------------------------------------------- /lib/utils/cn.ts: -------------------------------------------------------------------------------- 1 | import { type ClassValue, clsx } from 'clsx' 2 | import { twMerge } from 'tailwind-merge' 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)) 6 | } 7 | -------------------------------------------------------------------------------- /lib/utils/date.ts: -------------------------------------------------------------------------------- 1 | import dayjs from 'dayjs' 2 | import relativeTime from 'dayjs/plugin/relativeTime' 3 | dayjs.extend(relativeTime) 4 | 5 | /** 6 | * Formats a date. 7 | * 8 | * @param date - The date to format. 9 | * @returns The formatted date. 10 | */ 11 | export function formatDate( 12 | date: Date | string, 13 | format = 'MMM D, YYYY' 14 | ): string { 15 | const twoMonthsAgo = dayjs().subtract(2, 'month') 16 | const inputDate = dayjs(date) 17 | if (inputDate.isAfter(twoMonthsAgo)) { 18 | return inputDate.fromNow() 19 | } else { 20 | return inputDate.format(format) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /lib/utils/note-length.tsx: -------------------------------------------------------------------------------- 1 | import { Note } from '@/.contentlayer/generated' 2 | 3 | type NoteLength = Pick['length'] 4 | 5 | export const getNoteLenghtIcon = (length: NoteLength) => { 6 | const trimmedLength = length.trim() 7 | 8 | return ( 9 | 14 | {trimmedLength === 'Short' && ( 15 | 19 | )} 20 | {trimmedLength === 'Medium' && ( 21 | 25 | )} 26 | {trimmedLength === 'Long' && ( 27 | 31 | )} 32 | 33 | ) 34 | } 35 | -------------------------------------------------------------------------------- /models/experience.ts: -------------------------------------------------------------------------------- 1 | import { defineDocumentType } from 'contentlayer/source-files' 2 | 3 | export const Experience = defineDocumentType(() => ({ 4 | name: 'Experience', 5 | filePathPattern: 'data/experiences/*.json', 6 | bodyType: 'data', 7 | fields: { 8 | title: { 9 | type: 'string', 10 | required: true, 11 | }, 12 | company: { 13 | type: 'string', 14 | required: true, 15 | }, 16 | link: { 17 | type: 'string', 18 | }, 19 | content: { 20 | type: 'string', 21 | required: true, 22 | }, 23 | location: { 24 | type: 'string', 25 | }, 26 | startDate: { 27 | type: 'date', 28 | required: true, 29 | }, 30 | endDate: { 31 | type: 'date', 32 | }, 33 | skillsUsed: { 34 | type: 'list', 35 | of: { 36 | type: 'string', 37 | }, 38 | }, 39 | }, 40 | })) 41 | -------------------------------------------------------------------------------- /models/note.ts: -------------------------------------------------------------------------------- 1 | import { Document } from 'contentlayer/core' 2 | import { ComputedFields, defineDocumentType } from 'contentlayer/source-files' 3 | 4 | const computedFields: ComputedFields<'Note'> = { 5 | slug: { 6 | type: 'string', 7 | resolve: (doc: Document) => { 8 | const notesPrefix = 'notes/' 9 | let pathWithoutPrefix = doc._raw.flattenedPath.replace(notesPrefix, '') 10 | let pathParts = pathWithoutPrefix.split('/') 11 | pathParts.pop() // use .shift() to remove the first "note-name" 12 | pathWithoutPrefix = pathParts.join('/') 13 | return pathWithoutPrefix 14 | }, 15 | }, 16 | structuredData: { 17 | type: 'json', 18 | resolve: (doc: Document) => ({ 19 | '@context': 'https://schema.org', 20 | '@type': 'BlogPosting', 21 | headline: doc.title, 22 | datePublished: doc.publishedAt, 23 | dateModified: doc.publishedAt, 24 | description: doc.summary, 25 | image: doc.image 26 | ? `https://nitinp.dev/${doc.image}` 27 | : `https://nitinp.dev/og?title=${doc.title}`, 28 | url: `https://nitinp.dev/note/${doc._raw.flattenedPath}`, 29 | author: { 30 | '@type': 'Person', 31 | name: 'Nitin Panwar', 32 | }, 33 | }), 34 | }, 35 | } 36 | 37 | export const Note = defineDocumentType(() => ({ 38 | name: 'Note', 39 | filePathPattern: `content/notes/**/*.mdx`, 40 | contentType: 'mdx', 41 | fields: { 42 | title: { 43 | type: 'string', 44 | required: true, 45 | }, 46 | publishedAt: { 47 | type: 'string', 48 | required: true, 49 | }, 50 | summary: { 51 | type: 'string', 52 | required: true, 53 | }, 54 | image: { 55 | type: 'string', 56 | required: true, 57 | }, 58 | length: { 59 | options: ['Medium', 'Long', 'Short'], 60 | default: 'Medium', 61 | type: 'enum', 62 | }, 63 | }, 64 | computedFields, 65 | })) 66 | -------------------------------------------------------------------------------- /models/project.ts: -------------------------------------------------------------------------------- 1 | import { defineDocumentType } from 'contentlayer/source-files' 2 | 3 | export const Project = defineDocumentType(() => ({ 4 | name: 'Project', 5 | filePathPattern: 'content/projects/*.json', // Adjust the path pattern as needed 6 | contentType: 'data', 7 | fields: { 8 | cover: { 9 | type: 'string', 10 | required: true, 11 | }, 12 | title: { 13 | type: 'string', 14 | required: true, 15 | }, 16 | slug: { 17 | type: 'string', 18 | required: true, 19 | }, 20 | role: { 21 | type: 'string', 22 | required: true, 23 | }, 24 | links: { 25 | type: 'list', 26 | of: { 27 | type: 'json', 28 | fields: { 29 | name: { type: 'string' }, 30 | href: { type: 'string' }, 31 | }, 32 | }, 33 | }, 34 | overview: { 35 | type: 'string', 36 | required: true, 37 | }, 38 | content: { 39 | type: 'list', 40 | of: { type: 'string' }, 41 | }, 42 | }, 43 | })) 44 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const { withContentlayer } = require('next-contentlayer') 3 | 4 | const nextConfig = { 5 | reactStrictMode: true, 6 | swcMinify: true, 7 | async redirects() { 8 | const commonRedirects = [ 9 | { 10 | source: '/resume', 11 | destination: '/resume.pdf', 12 | permanent: true, 13 | } 14 | ] 15 | 16 | return commonRedirects 17 | }, 18 | headers() { 19 | return [ 20 | { 21 | source: '/(.*)', 22 | headers: securityHeaders, 23 | }, 24 | ]; 25 | }, 26 | images: { 27 | formats: ['image/avif', 'image/webp'], 28 | } 29 | } 30 | 31 | // https://nextjs.org/docs/advanced-features/security-headers 32 | const ContentSecurityPolicy = ` 33 | default-src 'self' vercel.live; 34 | script-src 'self' 'unsafe-eval' 'unsafe-inline' cdn.vercel-insights.com vercel.live va.vercel-scripts.com; 35 | style-src 'self' 'unsafe-inline'; 36 | img-src * blob: data:; 37 | media-src 'none'; 38 | connect-src *; 39 | font-src 'self' data:; 40 | `; 41 | 42 | const securityHeaders = [ 43 | // https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP 44 | { 45 | key: 'Content-Security-Policy', 46 | value: ContentSecurityPolicy.replace(/\n/g, ''), 47 | }, 48 | // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referrer-Policy 49 | { 50 | key: 'Referrer-Policy', 51 | value: 'origin-when-cross-origin', 52 | }, 53 | // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options 54 | { 55 | key: 'X-Frame-Options', 56 | value: 'DENY', 57 | }, 58 | // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Content-Type-Options 59 | { 60 | key: 'X-Content-Type-Options', 61 | value: 'nosniff', 62 | }, 63 | // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-DNS-Prefetch-Control 64 | { 65 | key: 'X-DNS-Prefetch-Control', 66 | value: 'on', 67 | }, 68 | // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Strict-Transport-Security 69 | { 70 | key: 'Strict-Transport-Security', 71 | value: 'max-age=31536000; includeSubDomains; preload', 72 | }, 73 | // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Feature-Policy 74 | { 75 | key: 'Permissions-Policy', 76 | value: 'camera=(), microphone=(), geolocation=()', 77 | }, 78 | ]; 79 | 80 | module.exports = withContentlayer(nextConfig) 81 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nitinp.dev", 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 | "dependencies": { 12 | "@studio-freight/react-lenis": "^0.0.47", 13 | "@vercel/analytics": "^1.1.1", 14 | "@vercel/speed-insights": "^1.0.2", 15 | "class-variance-authority": "^0.7.0", 16 | "clsx": "^2.0.0", 17 | "contentlayer": "^0.3.4", 18 | "dayjs": "^1.11.10", 19 | "framer-motion": "^10.16.4", 20 | "next": "14.0.1", 21 | "next-contentlayer": "^0.3.4", 22 | "next-sanity": "^6.0.1", 23 | "next-themes": "^0.2.1", 24 | "react": "^18", 25 | "react-dom": "^18", 26 | "rehype-autolink-headings": "^7.1.0", 27 | "rehype-pretty-code": "^0.10.2", 28 | "rehype-slug": "^6.0.0", 29 | "remark-gfm": "3.0.1", 30 | "shiki": "^0.14.5", 31 | "shikiji": "^0.8.0", 32 | "tailwind-merge": "^2.3.0" 33 | }, 34 | "devDependencies": { 35 | "@tailwindcss/typography": "^0.5.10", 36 | "@types/mdx": "^2.0.11", 37 | "@types/node": "^20", 38 | "@types/react": "^18", 39 | "@types/react-dom": "^18", 40 | "autoprefixer": "^10.0.1", 41 | "eslint": "^8", 42 | "eslint-config-next": "14.0.1", 43 | "mdx": "^0.3.1", 44 | "postcss": "^8", 45 | "styled-components": "^6.1.1", 46 | "tailwindcss": "^3.3.0", 47 | "typescript": "^5" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /public/fonts/acorn.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nitintf/www/8d9c401e9678c01a8d0657dad49b3da56e1233a6/public/fonts/acorn.woff -------------------------------------------------------------------------------- /public/fonts/gt.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nitintf/www/8d9c401e9678c01a8d0657dad49b3da56e1233a6/public/fonts/gt.woff -------------------------------------------------------------------------------- /public/images/notes/fire.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nitintf/www/8d9c401e9678c01a8d0657dad49b3da56e1233a6/public/images/notes/fire.png -------------------------------------------------------------------------------- /public/images/notes/go.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nitintf/www/8d9c401e9678c01a8d0657dad49b3da56e1233a6/public/images/notes/go.webp -------------------------------------------------------------------------------- /public/images/notes/iptracker-cli-root.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nitintf/www/8d9c401e9678c01a8d0657dad49b3da56e1233a6/public/images/notes/iptracker-cli-root.webp -------------------------------------------------------------------------------- /public/images/projects/poly/main.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nitintf/www/8d9c401e9678c01a8d0657dad49b3da56e1233a6/public/images/projects/poly/main.png -------------------------------------------------------------------------------- /public/images/projects/poly/main2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nitintf/www/8d9c401e9678c01a8d0657dad49b3da56e1233a6/public/images/projects/poly/main2.png -------------------------------------------------------------------------------- /public/images/projects/poly/main3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nitintf/www/8d9c401e9678c01a8d0657dad49b3da56e1233a6/public/images/projects/poly/main3.png -------------------------------------------------------------------------------- /public/images/projects/poly/poly.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nitintf/www/8d9c401e9678c01a8d0657dad49b3da56e1233a6/public/images/projects/poly/poly.webp -------------------------------------------------------------------------------- /public/images/projects/task/task.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nitintf/www/8d9c401e9678c01a8d0657dad49b3da56e1233a6/public/images/projects/task/task.webp -------------------------------------------------------------------------------- /public/images/projects/zaars/zaars.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nitintf/www/8d9c401e9678c01a8d0657dad49b3da56e1233a6/public/images/projects/zaars/zaars.webp -------------------------------------------------------------------------------- /public/images/star.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /public/me.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nitintf/www/8d9c401e9678c01a8d0657dad49b3da56e1233a6/public/me.webp -------------------------------------------------------------------------------- /public/resume.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nitintf/www/8d9c401e9678c01a8d0657dad49b3da56e1233a6/public/resume.pdf -------------------------------------------------------------------------------- /styles/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | * { 7 | box-sizing: border-box; 8 | -webkit-font-smoothing: antialiased; 9 | -moz-osx-font-smoothing: grayscale; 10 | text-rendering: optimizeLegibility; 11 | } 12 | 13 | :root { 14 | /* Colors */ 15 | --background: hsl(0, 24%, 96%); 16 | --foreground: hsl(216, 28%, 7%); 17 | --secondary: #4f576c; 18 | --border: transparent; 19 | --nav-link-highlight: rgb(255, 255, 255); 20 | --nav-link-bg: rgba(0, 0, 0, 0.04); 21 | --opaque: rgba(0, 0, 0, 0.094); 22 | 23 | /* Shadows */ 24 | --link: rgb(0, 0, 0, 0.08) 0px 0px 0px 2px; 25 | --link-hover: rgb(0, 0, 0, 0.01) 0px 0px 0px 5px, rgb(0, 0, 0, 0.05) 0px 0px 0px 4px; 26 | --expand: rgb(255, 255, 255) 0px 0px 0px 1px, rgba(255, 255, 255, 0.5) 0px 0px 0px 6px; 27 | --shadow-lg: 0px 50px 100px -20px rgba(0, 0, 0, .15); 28 | 29 | /* Sizes */ 30 | --logo: 35px; 31 | 32 | /* Fonts size */ 33 | --font-h1: clamp(3.3rem, .5692rem + 7.238vw, 11.75rem); 34 | --font-h2: clamp(1.5rem, 1.0982rem + 1.7143vw, 2.8125rem); 35 | --font-h3: clamp(1.375rem, 1.1837rem + .8163vw, 2rem); 36 | --font-h4: clamp(1.375rem, 1.1837rem + .8163vw, 1.6rem); 37 | --font-h5: clamp(1rem, .9235rem + .3265vw, 1.25rem); 38 | --font-h6: clamp(1rem, .9617rem + .1633vw, 1.125rem); 39 | --font-body: clamp(1rem, .8852rem + .4898vw, 1.375rem); 40 | --font-content: clamp(1rem, .8852rem + .4898vw, 1.075rem); 41 | --font-link: clamp(.875rem, .7985rem + .3265vw, 1.125rem); 42 | --font-mini: clamp(.875rem, .8367rem + .1633vw, 1rem); 43 | --font-h2-display: clamp(1.875rem, 1.301rem + 2.449vw, 3.95rem); 44 | 45 | 46 | /* Sizes */ 47 | --14px: .875rem; 48 | --15px: .9375rem; 49 | --16px: 1rem; 50 | --17px: 1.0625rem; 51 | --18px: 1.125rem; 52 | --19px: 1.1875rem; 53 | --20px: 1.25rem; 54 | --21px: 1.3125rem; 55 | --24px: 1.5rem; 56 | --42px: 2.652rem; 57 | --64px: 4rem; 58 | 59 | /* Spacings */ 60 | --spacing-xxl: max( 130px, calc(130px + (260 - 130) * ((100vw - 375px) / (1600 - 375))) ); 61 | --spacing-xl: max( var(--64px), calc(var(--64px) + (128 - 64) * ((100vw - 375px) / (1600 - 375))) ); 62 | --spacing-l: max( var(--42px), calc(var(--42px) + (84 - 42) * ((100vw - 375px) / (1600 - 375))) ); 63 | --spacing-m: max( var(--24px), calc(var(--24px) + (48 - 24) * ((100vw - 375px) / (1600 - 375))) ); 64 | --spacing-s: max( var(--21px), calc(var(--21px) + (42 - 21) * ((100vw - 375px) / (1600 - 375))) ); 65 | --spacing-xs: max( var(--14px), calc(var(--14px) + (28 - 14) * ((100vw - 375px) / (1600 - 375))) ); 66 | --spacing-xxs: max( calc(var(--14px) / 2), calc((var(--14px) / 2) + (14 - 7) * ((100vw - 375px) / (1600 - 375))) ); 67 | 68 | /* Width */ 69 | --max-width: 1600px; 70 | 71 | /* Transitions */ 72 | --transition-bounce: cubic-bezier(.175,.885,.32,1.275); 73 | --custom-ease: cubic-bezier(0.6, 0.05, -0.01, 0.9); 74 | --transition-ease: ease-in-out; 75 | 76 | /* Z Index */ 77 | --zindex-base: 1; 78 | --zindex-2: 2; 79 | --zindex-content-lower-2: 40; 80 | --zindex-content-lower-1: 45; 81 | --zindex-content: 50; 82 | --zindex-top: 100; 83 | --zindex-nav: 1000; 84 | --zindex-nav-menu: 1010; 85 | --zindex-modal: 2000; 86 | 87 | /* Radius */ 88 | --radius-base: .75rem; 89 | } 90 | 91 | [data-theme='dark'] { 92 | /* Colors */ 93 | --background: hsl(214, 28%, 5%); 94 | --foreground: hsl(0, 0%, 83%); 95 | --secondary: #98a1b6; 96 | --border: rgb(48, 54, 61); 97 | --nav-link-highlight: rgb(33, 38, 45); 98 | --nav-link-bg: hsla(216, 24%, 8%, 0.6); 99 | 100 | /* Shadows */ 101 | --link: rgb(48, 54, 61) 0px 0px 0px 2px; 102 | --link-hover: rgb(48, 54, 61) 0px 0px 0px 5px; 103 | 104 | /* Spacings */ 105 | --top-navbar-height: 136px; 106 | } 107 | 108 | h1, h2, h3, h4, h5, h6 { 109 | font-family: var(--font-primary); 110 | font-weight: 700; 111 | } 112 | 113 | h1 { 114 | display: block; 115 | margin-block-start: 0.67em; 116 | margin-block-end: 0.67em; 117 | margin-inline-start: 0px; 118 | margin-inline-end: 0px; 119 | } 120 | 121 | h1 { 122 | @apply text-h1 text-center leading-[100%] -tracking-[2px] 123 | } 124 | 125 | @keyframes scaleUp { 126 | from { 127 | transform: scale(0.5); 128 | } 129 | to { 130 | transform: scale(1); 131 | } 132 | } 133 | } 134 | 135 | .contact { 136 | font-size: 14px; 137 | cursor: pointer; 138 | position: relative; 139 | } 140 | 141 | .contact::before { 142 | position: absolute; 143 | width: 100%; 144 | height: 1px; 145 | background: var(--foreground); 146 | top: 100%; 147 | left: 0; 148 | pointer-events: none; 149 | content: ''; 150 | transform-origin: 100% 50%; 151 | transform: scale3d(0, 1, 1); 152 | transition: transform 0.3s; 153 | } 154 | 155 | .contact:hover::before { 156 | transform-origin: 0% 50%; 157 | transform: scale3d(1, 1, 1); 158 | } 159 | 160 | @media (max-width: 600px) { 161 | .contact { 162 | position: fixed; 163 | bottom: 30px; 164 | right: 30px; 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /styles/prose.css: -------------------------------------------------------------------------------- 1 | 2 | code { 3 | counter-reset: line; 4 | } 5 | 6 | code > [data-line]::before { 7 | counter-increment: line; 8 | content: counter(line); 9 | @apply inline-block w-4 mr-8 text-gray-500 pl-4; 10 | } 11 | 12 | pre { 13 | @apply !pl-0 !pr-0 14 | } 15 | 16 | code[data-line-numbers-max-digits='2'] > [data-line]::before { 17 | width: 1rem; 18 | } 19 | 20 | code[data-line-numbers-max-digits='3'] > [data-line]::before { 21 | width: 3rem; 22 | } 23 | 24 | [data-rehype-pretty-code-fragment] .line--highlighted { 25 | @apply bg-[#88C0D0] bg-opacity-10 border-l-4 border-[#88C0D0] pl-0; 26 | } 27 | [data-rehype-pretty-code-fragment] .line-highlighted span { 28 | @apply relative; 29 | } 30 | [data-rehype-pretty-code-fragment] .word--highlighted { 31 | @apply rounded-md bg-[#88C0D0] bg-opacity-30 p-1; 32 | } 33 | [data-rehype-pretty-code-title] { 34 | @apply px-4 py-3 font-mono text-xs font-medium border rounded-t-lg text-neutral-200 border-[#333333] bg-[#1c1c1c]; 35 | } 36 | [data-rehype-pretty-code-title] + pre { 37 | @apply mt-0 rounded-t-none border-t-0; 38 | } 39 | 40 | [data-rehype-pretty-code-title] { 41 | @apply bg-transparent; 42 | } 43 | 44 | [data-rehype-pretty-code-title] { 45 | @apply bg-transparent border border-[#4b5563]; 46 | } 47 | 48 | /* Callouts */ 49 | .prose callout-muted a, 50 | .prose callout-info a, 51 | .prose callout-warning a, 52 | .prose callout-danger a, 53 | .prose callout-success a { 54 | text-decoration: underline; 55 | } 56 | 57 | .prose callout-muted p, 58 | .prose callout-info p, 59 | .prose callout-warning p, 60 | .prose callout-danger p, 61 | .prose callout-success p { 62 | margin-bottom: 0; 63 | } 64 | 65 | .prose callout-muted, 66 | .prose callout-info, 67 | .prose callout-warning, 68 | .prose callout-danger, 69 | .prose callout-success { 70 | margin-top: 0; 71 | margin-bottom: 2rem; 72 | } 73 | 74 | .prose callout-muted, 75 | .prose callout-info, 76 | .prose callout-warning, 77 | .prose callout-danger, 78 | .prose callout-success { 79 | display: block; 80 | border-left: solid 4px; 81 | padding: 1rem 1rem; 82 | position: relative; 83 | border-top-right-radius: 0.5rem; 84 | border-bottom-right-radius: 0.5rem; 85 | } 86 | 87 | .prose callout-muted, 88 | .prose callout-info, 89 | .prose callout-warning, 90 | .prose callout-danger, 91 | .prose callout-success, 92 | .prose callout-muted *, 93 | .prose callout-info *, 94 | .prose callout-warning *, 95 | .prose callout-danger *, 96 | .prose callout-success * { 97 | font-size: 1rem; 98 | } 99 | 100 | .prose callout-muted.aside, 101 | .prose callout-info.aside, 102 | .prose callout-warning.aside, 103 | .prose callout-danger.aside, 104 | .prose callout-success.aside, 105 | .prose callout-muted.aside *, 106 | .prose callout-info.aside *, 107 | .prose callout-warning.aside *, 108 | .prose callout-danger.aside *, 109 | .prose callout-success.aside * { 110 | font-size: 1rem; 111 | } 112 | 113 | .prose callout-muted.important, 114 | .prose callout-info.important, 115 | .prose callout-warning.important, 116 | .prose callout-danger.important, 117 | .prose callout-success.important, 118 | .prose callout-muted.important *, 119 | .prose callout-info.important *, 120 | .prose callout-warning.important *, 121 | .prose callout-danger.important *, 122 | .prose callout-success.important * { 123 | font-size: 1.4rem; 124 | font-weight: bold; 125 | } 126 | 127 | .prose callout-muted:before, 128 | .prose callout-info:before, 129 | .prose callout-warning:before, 130 | .prose callout-danger:before, 131 | .prose callout-success:before { 132 | border-top-right-radius: 0.5rem; 133 | border-bottom-right-radius: 0.5rem; 134 | content: ''; 135 | position: absolute; 136 | inset: 0; 137 | opacity: 0.1; 138 | pointer-events: none; 139 | } 140 | 141 | /* the warning yellow is really inaccessible in light mode, so we have a special case for light mode */ 142 | .light .prose callout-warning, 143 | .light .prose callout-warning ol > li:before { 144 | color: #676000; 145 | } 146 | .light .prose callout-warning:before { 147 | background: #ffd800; 148 | } 149 | .prose callout-warning, 150 | .prose callout-warning ol > li:before { 151 | color: #ffd644; 152 | } 153 | .prose callout-warning:before { 154 | background: #ffd644; 155 | } 156 | 157 | /* the muted gray is really inaccessible in light mode, so we have a special case for light mode */ 158 | .light .prose callout-muted, 159 | .light .prose callout-muted ol > li:before { 160 | color: #4c4b5e; 161 | } 162 | .light .prose callout-muted:before { 163 | background: #3c3e4d; 164 | } 165 | 166 | .prose callout-muted, 167 | .prose callout-muted ol > li:before { 168 | color: #b9b9c3; 169 | } 170 | .prose callout-muted:before { 171 | background: #484950; 172 | } 173 | 174 | .prose callout-info, 175 | .prose callout-info ol > li:before { 176 | color: #88C0D0; 177 | } 178 | .prose callout-info:before { 179 | background: #88C0D0; 180 | } 181 | 182 | .prose callout-danger, 183 | .prose callout-danger ol > li:before { 184 | color: #ff4545; 185 | } 186 | .prose callout-danger:before { 187 | background: #ff4545; 188 | } 189 | 190 | .prose callout-success, 191 | .prose callout-success ol > li:before { 192 | color: #30c85e; 193 | } 194 | .prose callout-success:before { 195 | background: #30c85e; 196 | } 197 | -------------------------------------------------------------------------------- /tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from 'tailwindcss' 2 | 3 | const config: Config = { 4 | content: [ 5 | './pages/**/*.{js,ts,jsx,tsx,mdx}', 6 | './components/**/*.{js,ts,jsx,tsx,mdx}', 7 | './app/**/*.{js,ts,jsx,tsx,mdx}', 8 | ], 9 | theme: { 10 | container: { 11 | center: true, 12 | padding: '4vw', 13 | screens: { 14 | '2xl': '1400px', 15 | }, 16 | }, 17 | extend: { 18 | fontFamily: { 19 | primary: ['var(--font-primary)', 'fallbackFontHere'], 20 | secondary: ['var(--font-secondary)', 'fallbackFontHere'], 21 | }, 22 | spacing: { 23 | xxl: 'var(--spacing-xxl)', 24 | xl: 'var(--spacing-xl)', 25 | l: 'var(--spacing-l)', 26 | m: 'var(--spacing-m)', 27 | s: 'var(--spacing-s)', 28 | xs: 'var(--spacing-xs)', 29 | xxs: 'var(--spacing-xxs)', 30 | }, 31 | colors: { 32 | background: 'var(--background)', 33 | foreground: 'var(--foreground)', 34 | border: 'var(--border)', 35 | highlight: 'var(--nav-link-highlight)', 36 | secondary: 'var(--secondary)', 37 | navBg: 'var(--nav-link-bg)', 38 | opaque: 'var(--opaque)', 39 | link: 'var(--link)', 40 | }, 41 | height: { 42 | navbar: 'var(--top-navbar-height)', 43 | logo: 'var(--logo)', 44 | }, 45 | width: { 46 | logo: 'var(--logo)', 47 | }, 48 | maxWidth: { 49 | content: '1600px', 50 | }, 51 | boxShadow: { 52 | link: 'var(--link)', 53 | expand: 'var(--expand)', 54 | linkhover: 'var(--link-hover)', 55 | }, 56 | fontSize: { 57 | h1: 'var(--font-h1)', 58 | h2: 'var(--font-h2)', 59 | h3: 'var(--font-h3)', 60 | h4: 'var(--font-h4)', 61 | h5: 'var(--font-h5)', 62 | h6: 'var(--font-h6)', 63 | body: 'var(--font-body)', 64 | content: 'var(--font-content)', 65 | link: 'var(--font-link)', 66 | mini: 'var(--font-mini)', 67 | h2Display: 'var(--font-h2-display)', 68 | }, 69 | padding: { 70 | link: 'calc(1rem - 10px) calc(1rem - 6px)', 71 | }, 72 | zIndex: { 73 | nav: '1000', 74 | }, 75 | transitionTimingFunction: { 76 | custom: 'var(--custom-ease)', 77 | }, 78 | animation: { 79 | scale: 'scaleUp 0.3s ease-in-out 0.8s', 80 | }, 81 | borderRadius: { 82 | base: 'var(--radius-base)', 83 | }, 84 | }, 85 | }, 86 | plugins: [require('@tailwindcss/typography')], 87 | } 88 | export default config 89 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "noEmit": true, 9 | "esModuleInterop": true, 10 | "module": "esnext", 11 | "moduleResolution": "bundler", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "preserve", 15 | "incremental": true, 16 | "baseUrl": ".", 17 | "plugins": [ 18 | { 19 | "name": "next" 20 | } 21 | ], 22 | "paths": { 23 | "@/*": ["./*"], 24 | "contentlayer/generated": ["./.contentlayer/generated"] 25 | } 26 | }, 27 | "include": [ 28 | "next-env.d.ts", 29 | "**/*.ts", 30 | "**/*.tsx", 31 | ".next/types/**/*.ts", 32 | ".contentlayer/generated" 33 | ], 34 | "exclude": ["node_modules"] 35 | } 36 | --------------------------------------------------------------------------------