├── .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 |
70 | {formattedDate}
71 |
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 |
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 | setIsExpanded(true)}
43 | onMouseLeave={() => setIsExpanded(false)}
44 | onClick={scrollToTop}
45 | className={cn('right-12 bottom-7', {
46 | fixed: showButton,
47 | hidden: !showButton,
48 | })}>
49 |
50 |
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
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 |
55 |
61 |
79 | {navLinks.map((link, index) => (
80 |
81 |
85 | currentElRef.current
86 | ? (currentElRef.current[link.href] = el)
87 | : null
88 | }
89 | onClick={handleOnClick(link.href)}>
90 | {link.label}
91 |
92 |
93 | ))}
94 |
95 |
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 |
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 |
20 |
21 |
22 |
Enjoy this note? Feel free to share!
23 |
24 |
45 |
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 |
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 |
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 |
--------------------------------------------------------------------------------