├── .gitignore
├── README.md
├── app
├── (more)
│ ├── anime
│ │ ├── anime.ts
│ │ └── page.tsx
│ ├── books
│ │ ├── books.ts
│ │ └── page.tsx
│ ├── faqs
│ │ ├── faqs.ts
│ │ └── page.tsx
│ ├── layout.tsx
│ ├── music
│ │ └── page.tsx
│ ├── nsfw
│ │ └── page.tsx
│ ├── papers
│ │ └── page.tsx
│ ├── stats
│ │ └── page.tsx
│ ├── uses
│ │ └── page.tsx
│ └── work
│ │ └── page.tsx
├── api
│ ├── discord
│ │ └── route.ts
│ ├── lastfm
│ │ └── latest
│ │ │ ├── get-album-cover.ts
│ │ │ ├── get-latest-song.ts
│ │ │ └── route.ts
│ ├── letterboxd
│ │ └── latest
│ │ │ ├── get-latest-film.ts
│ │ │ └── route.ts
│ └── og
│ │ └── route.tsx
├── env.ts
├── globals.css
├── layout.tsx
├── notes
│ ├── [slug]
│ │ └── page.tsx
│ └── page.tsx
├── page.tsx
├── robots.ts
├── rss.xml
│ └── route.ts
└── sitemap.ts
├── biome.json
├── bun.lockb
├── components.json
├── components
├── icons
│ ├── archlinux.tsx
│ ├── cloudflare.tsx
│ ├── cpp.tsx
│ ├── discord.tsx
│ ├── docker.tsx
│ ├── elixir.tsx
│ ├── figma.tsx
│ ├── firebase.tsx
│ ├── git.tsx
│ ├── godot.tsx
│ ├── golang.tsx
│ ├── graphql.tsx
│ ├── index.ts
│ ├── jest.tsx
│ ├── lastfm.tsx
│ ├── music.tsx
│ ├── mysql.tsx
│ ├── nextjs.tsx
│ ├── nodejs.tsx
│ ├── postgres.tsx
│ ├── prisma.tsx
│ ├── python.tsx
│ ├── react.tsx
│ ├── tailwindcss.tsx
│ ├── twitter.tsx
│ ├── typescript.tsx
│ ├── vite.tsx
│ └── vscode.tsx
├── layouts
│ ├── footer.tsx
│ ├── header.tsx
│ ├── mdx.tsx
│ ├── tweet.css
│ └── tweet.tsx
├── misc
│ ├── (anime)
│ │ └── anime-card.tsx
│ ├── (book)
│ │ └── latest-book.tsx
│ ├── (home)
│ │ ├── cards
│ │ │ ├── animelink-card.tsx
│ │ │ ├── books-card.tsx
│ │ │ ├── contact-card.tsx
│ │ │ ├── current-time.tsx
│ │ │ ├── dc-status.tsx
│ │ │ ├── gh-link.tsx
│ │ │ ├── gh-stats.tsx
│ │ │ ├── images-card.tsx
│ │ │ ├── index.ts
│ │ │ ├── links-card.tsx
│ │ │ ├── locate-me.tsx
│ │ │ ├── music-card.tsx
│ │ │ ├── stacks-card.tsx
│ │ │ └── wakatime-stats.tsx
│ │ ├── grid-cards.tsx
│ │ └── intro.tsx
│ ├── (music)
│ │ ├── album-card.tsx
│ │ ├── latest-song.tsx
│ │ └── top-albums.tsx
│ ├── (theme)
│ │ ├── theme-provider.tsx
│ │ └── theme-switch.tsx
│ ├── (uses)
│ │ ├── colophon.tsx
│ │ ├── design-colors.tsx
│ │ ├── my-logo.tsx
│ │ ├── typography.tsx
│ │ ├── uses-tab.tsx
│ │ └── uses-tabs-comps.tsx
│ ├── analytics.tsx
│ └── emoji.tsx
├── nav
│ ├── nav-drawer.tsx
│ ├── navbar.tsx
│ └── navmenu.tsx
├── ui
│ ├── accordion.tsx
│ ├── background-gradient.tsx
│ ├── badge.tsx
│ ├── button.tsx
│ ├── carousel.tsx
│ ├── drawer.tsx
│ ├── dropdown-menu.tsx
│ ├── form.tsx
│ ├── hover-card.tsx
│ ├── input.tsx
│ ├── label.tsx
│ ├── navigation-menu.tsx
│ ├── sonner.tsx
│ ├── table.tsx
│ ├── tabs.tsx
│ └── textarea.tsx
└── utils
│ ├── characters.tsx
│ ├── relative-date.tsx
│ └── skeleton.tsx
├── content
├── demo.mdx
└── ext_uses.mdx
├── hooks
├── use-has-mounted.ts
└── use-latest-song.ts
├── lib
├── blog.ts
├── format-date.ts
├── get-gh-stats.ts
├── octokit.ts
├── report.ts
├── submit-form.tsx
└── utils.ts
├── next.config.mjs
├── package.json
├── postcss.config.js
├── public
├── favicon.ico
├── favicons
│ ├── favicon-dark.ico
│ └── favicon-light.ico
├── fonts
│ └── kaisei-tokumin-bold.ttf
├── images
│ ├── (anime)
│ │ ├── angel.webp
│ │ ├── aot.webp
│ │ ├── attack.webp
│ │ ├── coe.webp
│ │ ├── darling.webp
│ │ ├── game.webp
│ │ ├── jujutsu.webp
│ │ ├── korosensei.webp
│ │ ├── lain.webp
│ │ ├── promised.webp
│ │ ├── rezero.webp
│ │ └── sao.webp
│ ├── (home)
│ │ ├── 0001.jpg
│ │ ├── 0002.jpg
│ │ ├── 0003.jpg
│ │ ├── 0004.jpg
│ │ ├── 0005.jpg
│ │ ├── 0006.jpg
│ │ ├── 0007.jpg
│ │ ├── books.jpg
│ │ └── fallbackMusicCover.jpg
│ ├── (misc)
│ │ └── kolkata.png
│ ├── (nav)
│ │ ├── anime.webp
│ │ ├── books.webp
│ │ ├── faqs.webp
│ │ ├── music.webp
│ │ ├── uses.webp
│ │ └── work.webp
│ ├── (uses)
│ │ ├── bspwm.png
│ │ ├── bspwm.webp
│ │ └── dwm.png
│ ├── gradient.webp
│ └── webp.sh
├── meta
│ ├── meta.png
│ └── og.png
├── public.asc
└── vids
│ └── cars.mp4
├── tailwind.config.ts
└── tsconfig.json
/.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
30 | .env*.local
31 |
32 | # vercel
33 | .vercel
34 |
35 | # typescript
36 | *.tsbuildinfo
37 | next-env.d.ts
38 |
39 | #self
40 | TODO.md
41 |
42 | # turbopack
43 | .turbo/
44 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | frontend is boring.
--------------------------------------------------------------------------------
/app/(more)/anime/anime.ts:
--------------------------------------------------------------------------------
1 | export interface Anime {
2 | title: string;
3 | altTitle?: string;
4 | href: string;
5 | imgName: string;
6 | starred?: boolean;
7 | }
8 |
9 | export const AnimeData: Anime[] = [
10 | {
11 | title: "Serial Experiments Lain",
12 | href: "https://myanimelist.net/anime/339/Serial_Experiments_Lain",
13 | imgName: "lain",
14 | starred: true,
15 | },
16 | {
17 | title: "Classroom of the Elite",
18 | href: "https://myanimelist.net/anime/35507/Youkoso_Jitsuryoku_Shijou_Shugi_no_Kyoushitsu_e",
19 | imgName: "coe",
20 | starred: true,
21 | },
22 | {
23 | title: "Assassination Classroom",
24 | href: "https://myanimelist.net/anime/24833/Ansatsu_Kyoushitsu",
25 | imgName: "korosensei",
26 | starred: true,
27 | },
28 | {
29 | title: "Darling in the FranXX",
30 | href: "https://myanimelist.net/anime/35849/Darling_in_the_FranXX",
31 | imgName: "darling",
32 | starred: true,
33 | },
34 | {
35 | title: "Angel Beats!",
36 | href: "https://myanimelist.net/anime/6547/Angel_Beats",
37 | imgName: "angel",
38 | },
39 | {
40 | title: "No Game No Life",
41 | href: "https://myanimelist.net/anime/19815/No_Game_No_Life",
42 | imgName: "game",
43 | },
44 | {
45 | title: "Re:Zero kara Hajimeru Isekai Seikatsu",
46 | altTitle: "Re:Zero − Starting Life in Another World",
47 | href: "https://myanimelist.net/anime/31240/Re_Zero_kara_Hajimeru_Isekai_Seikatsu",
48 | imgName: "rezero",
49 | starred: true,
50 | },
51 | {
52 | title: "Shingeki no Kyojin",
53 | altTitle: "Attack on Titan",
54 | href: "https://myanimelist.net/anime/16498/Shingeki_no_Kyojin",
55 | imgName: "aot",
56 | },
57 | {
58 | title: "Sword Art Online",
59 | href: "https://myanimelist.net/anime/11757/Sword_Art_Online",
60 | imgName: "sao",
61 | },
62 | {
63 | title: "Yakusoku no Neverland",
64 | altTitle: "The Promised Neverland",
65 | href: "https://myanimelist.net/anime/37779/Yakusoku_no_Neverland",
66 | imgName: "promised",
67 | starred: true,
68 | },
69 | ];
70 |
--------------------------------------------------------------------------------
/app/(more)/anime/page.tsx:
--------------------------------------------------------------------------------
1 | import { AnimeCard } from "@/components/misc/(anime)/anime-card";
2 | import type { Metadata } from "next";
3 | import { type Anime, AnimeData } from "./anime";
4 |
5 | export const metadata: Metadata = {
6 | title: "vimfn // anime",
7 | description: "Find a list of my fav and currently watching anime.",
8 | };
9 |
10 | const animePage = () => {
11 | return (
12 |
13 |
14 |
Anime
15 |
16 | I have loved watching anime since childhood. My first anime was Death
17 | Note, which is one of the most popular ones. It was suggested to me by
18 | a friend. Since then, I have watched many anime of different genres,
19 | but Isekai, Mecha, and Slice of Life are some of my favorites.
20 |
21 |
22 |
23 | {AnimeData.map((anime: Anime) => (
24 |
32 | ))}
33 |
34 |
35 | );
36 | };
37 |
38 | export default animePage;
39 |
--------------------------------------------------------------------------------
/app/(more)/books/books.ts:
--------------------------------------------------------------------------------
1 | export interface books {
2 | title: string;
3 | author: string;
4 | year: number;
5 | type: "audiobook" | "book";
6 | poster: string;
7 | url: string;
8 | readingNow?: boolean;
9 | startDate?: string;
10 | }
11 |
12 | export const booksData: books[] = [
13 | {
14 | title: "A Brief History of Time",
15 | author: "Stephen Hawking",
16 | year: 1998,
17 | type: "audiobook",
18 | poster:
19 | "https://assets.literal.club/4/ckiuwi808258410zk5mcgpjult.jpg?size=600",
20 | url: "https://literal.club/book/a-brief-history-of-time-rh9hz",
21 | readingNow: true,
22 | startDate: "2023-12-26",
23 | },
24 | {
25 | title: "1984",
26 | author: "George Orwell",
27 | year: 1994,
28 | type: "book",
29 | poster:
30 | "https://assets.literal.club/2/cks96wdkv16083271k980gtmkfvs.jpg?size=600",
31 | url: "https://literal.club/book/bin-dokuz-yuz-seksen-dort-ofejk",
32 | },
33 | {
34 | title: "The Great Gatsby",
35 | author: "F. Scott Fitzgerald",
36 | year: 2013,
37 | type: "book",
38 | poster:
39 | "https://assets.literal.club/cover/5/ckhkj670c04430zhwrp3k4n64.jpg?size=600",
40 | url: "https://literal.club/book/the-great-gatsby-mlbi0",
41 | },
42 | {
43 | title: "The Lord of the Rings",
44 | author: "J.R.R. Tolkien",
45 | year: 2001,
46 | type: "book",
47 | poster:
48 | "https://assets.literal.club/4/ckiu5jcug456940z612u5mo3xp.jpg?size=600",
49 | url: "https://literal.club/book/the-lord-of-the-rings-n69jk",
50 | },
51 | {
52 | title: "The Hound of the Baskervilles",
53 | author: "Arthur Conan Doyle",
54 | year: 2008,
55 | type: "book",
56 | poster:
57 | "https://assets.literal.club/4/cklgk3oh9147141ifbfwmmbysn.jpg?size=600",
58 | url: "https://literal.club/book/the-hound-of-the-baskervilles-jy7gm",
59 | },
60 | {
61 | title: "The Da Vinci Code",
62 | author: "Dan Brown",
63 | year: 2010,
64 | type: "book",
65 | poster:
66 | "https://assets.literal.club/2/ckgexuagf38640y8qcxjon51e.jpg?size=600",
67 | url: "https://literal.club/book/the-da-vinci-code-npgdh",
68 | },
69 | {
70 | title: "The Alchemist",
71 | author: "Paulo Coelho",
72 | year: 2014,
73 | type: "book",
74 | poster:
75 | "https://assets.literal.club/4/ckmbkav5b03041kjayv3n4jwh.jpg?size=600",
76 | url: "https://literal.club/book/the-alchemist-pjupi",
77 | },
78 | {
79 | title: "The Diary of a Young Girl",
80 | author: "Anne Frank",
81 | year: 1982,
82 | type: "book",
83 | poster:
84 | "https://assets.literal.club/cover/5/ckr1u3exn17ju01crrsb5m7s6.jpg?size=600",
85 | url: "https://literal.club/book/anne-frankthe-diary-of-a-young-girl-kqfon",
86 | },
87 | {
88 | title: "The Shadow of the Wind",
89 | author: "Carlos Ruiz Zafón",
90 | year: 2004,
91 | type: "book",
92 | poster:
93 | "https://assets.literal.club/4/ckn6dmh66108831i8djowf9emm.jpg?size=600",
94 | url: "https://literal.club/book/the-shadow-of-the-wind-idfkm",
95 | },
96 | {
97 | title: "A Promised Land",
98 | author: "Barack Obama",
99 | year: 2020,
100 | type: "book",
101 | poster:
102 | "https://assets.literal.club/1/ckiuwfgtn111020zk55i06ub6z.jpg?size=600",
103 | url: "https://literal.club/book/a-promised-land-3fyay",
104 | },
105 | {
106 | title: "The Girl with the Dragon Tattoo",
107 | author: "Stieg Larsson",
108 | year: 2010,
109 | type: "book",
110 | poster:
111 | "https://assets.literal.club/1/ckh0ih3ul88870z6kqd9psdir.jpg?size=600",
112 | url: "https://literal.club/book/the-girl-with-the-dragon-tattoo-0h52q",
113 | },
114 | ];
115 |
--------------------------------------------------------------------------------
/app/(more)/books/page.tsx:
--------------------------------------------------------------------------------
1 | import { LatestBook } from "@/components/misc/(book)/latest-book";
2 | import type { Metadata } from "next";
3 | import { booksData } from "./books";
4 |
5 | export const metadata: Metadata = {
6 | title: "vimfn // books",
7 | description:
8 | "Find some of my favorite book collections and the one currently reading.",
9 | };
10 |
11 | const booksPage = () => {
12 | return (
13 |
14 |
15 |
Books
16 |
17 | Aside from reading many lines of code, errors, and pages of
18 | documentation, when I find some time or feel like delving into a topic
19 | in depth, I often turn to books for their excellent in-depth
20 | explanations and often fun.
21 |
22 |
23 | If it is not something related to my work, or a mystery or sci-fi
24 | story book or novel, I opt for the audiobook version and enjoy it
25 | either while traveling or at bedtime.
26 |
27 | {booksData.map(
28 | (book) => book.readingNow &&
,
29 | )}
30 |
31 | Below are some of my favorite collections of various authors.
32 |
33 |
34 | {booksData.map(
35 | (book) =>
36 | !book.readingNow && ,
37 | )}
38 |
39 |
40 |
41 | );
42 | };
43 |
44 | export default booksPage;
45 |
--------------------------------------------------------------------------------
/app/(more)/faqs/faqs.ts:
--------------------------------------------------------------------------------
1 | export interface FaqTypes {
2 | question: string;
3 | answer: string;
4 | }
5 |
6 | export const faqs: FaqTypes[] = [
7 | {
8 | question: "When did you start coding?",
9 | answer:
10 | "Technically, I began coding around the age of 12, but I took it more seriously during the lockdown due to the COVID-19 pandemic when schools were closed.",
11 | },
12 | {
13 | question: "Can I see your resume?",
14 | answer:
15 | "Certainly! If you're a recruiter, please contact me via email at hi@vimfn.in . You can also find my LinkedIn profile here .",
16 | },
17 | {
18 | question: "How did you learn to code? Can you share some resources?",
19 | answer:
20 | "Mostly by playing around and building things. Documentation is the best place to learn about something, but if you are a beginner, you can start with the book Automate the Boring Stuff with Python or refer to freeCodeCamp videos on YouTube.",
21 | },
22 | {
23 | question: "What is your favorite web framework?",
24 | answer:
25 | "Next.js is my favorite framework because of its server-side rendering, efficient routing, and seamless integration with React. Additionally, I'm keeping a close eye on Solidstart for its innovative approach, moving away from the virtual DOM and its reactive programming model.",
26 | },
27 | {
28 | question:
29 | "What terminal emulator, code editor, theme, and font do you use?",
30 | answer:
31 | "I have written in detail about my setup on the uses page of this site, please read that.",
32 | },
33 | {
34 | question: "Which university do you attend?",
35 | answer: "Please refrain from asking personal questions. Thank you.",
36 | },
37 | {
38 | question: "What are some of your favorite YouTube channels?",
39 | answer:
40 | "Well, there are plenty of them, to be honest, but Fireship , Theo - t3.gg , and ThePrimeTime are some of my favorites.",
41 | },
42 | {
43 | question: "How can I donate or sponsor you? Is there a way to do that?",
44 | answer:
45 | "I appreciate your willingness to support me. You can sponsor me on my GitHub page .",
46 | },
47 | {
48 | question: "Where can I find your GPG keys?",
49 | answer:
50 | "If you're interested in sending me an encrypted message, you can find my GPG keys here .",
51 | },
52 | // {
53 | // question: "",
54 | // answer: "",
55 | // },
56 | ];
57 |
--------------------------------------------------------------------------------
/app/(more)/faqs/page.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Accordion,
3 | AccordionContent,
4 | AccordionItem,
5 | AccordionTrigger,
6 | } from "@/components/ui/accordion";
7 | import type { Metadata } from "next";
8 | import { faqs } from "./faqs";
9 |
10 | export const metadata: Metadata = {
11 | title: "vimfn // faqs",
12 | description:
13 | "Redirected here? It means you have asked me something that has already been asked many times. Please don't think I am rude.",
14 | };
15 |
16 | const pinsPage = () => {
17 | return (
18 |
19 |
20 |
FAQs
21 |
22 | I've gathered some commonly asked questions here to save time for both
23 | of us. If you have any other inquiries, feel free to reach out to me
24 | at{" "}
25 |
31 | x.com
32 |
33 | {""}. Thanks!
34 |
35 |
36 |
37 | {faqs.map((faq, i) => (
38 |
39 |
40 |
41 | {faq.question}
42 |
43 |
44 |
45 |
46 |
47 |
48 | ))}
49 |
50 | );
51 | };
52 |
53 | export default pinsPage;
54 |
--------------------------------------------------------------------------------
/app/(more)/layout.tsx:
--------------------------------------------------------------------------------
1 | export default function MoreLayout({
2 | children,
3 | }: {
4 | children: React.ReactNode;
5 | }) {
6 | return (
7 |
8 | {children}
9 |
10 | );
11 | }
12 |
--------------------------------------------------------------------------------
/app/(more)/music/page.tsx:
--------------------------------------------------------------------------------
1 | import { LatestSong } from "@/components/misc/(music)/latest-song";
2 | import TopAblums from "@/components/misc/(music)/top-albums";
3 | import type { Metadata } from "next";
4 |
5 | export const metadata: Metadata = {
6 | title: "vimfn // music",
7 | description: "Get an Idea of my music taste.",
8 | };
9 |
10 | const MusicPage = () => {
11 | return (
12 |
13 |
14 |
Music
15 |
16 | Music has always been something near to my heart. Whether it's a
17 | happy day or a sad one, there is a memory linked with it, and a song
18 | that accompanies the moment.
19 |
20 | {/*
21 | You can find a screencast my 2023 Spotify wrapped{""}
22 |
27 | here
28 |
29 | {""}.
30 |
31 | */}
32 |
33 |
34 | Fav Tracks
35 |
36 | I listen to a lot of Spotify, Over the last 12 months, I've played
37 | the song イザベラの唄 by Takahiro Obata exactly 146 times! Below you can
38 | find an up-to-date collection of my favourite songs from the past ~4
39 | weeks.
40 |
41 |
42 |
43 | );
44 | };
45 |
46 | export default MusicPage;
47 |
--------------------------------------------------------------------------------
/app/(more)/nsfw/page.tsx:
--------------------------------------------------------------------------------
1 | import type { Metadata } from "next";
2 |
3 | export const metadata: Metadata = {
4 | title: "vimfn // nsfw",
5 | description: "🥵",
6 | };
7 |
8 | const nsfwPage = () => {
9 | return (
10 |
11 |
NSFW
12 |
16 |
17 | );
18 | };
19 |
20 | export default nsfwPage;
21 |
--------------------------------------------------------------------------------
/app/(more)/papers/page.tsx:
--------------------------------------------------------------------------------
1 | export default function PapersPage() {
2 | return (
3 |
4 |
Papers
5 | wip.
6 |
7 | );
8 | }
9 |
--------------------------------------------------------------------------------
/app/(more)/stats/page.tsx:
--------------------------------------------------------------------------------
1 | const StatsPage = () => {
2 | return WIP, Please check back later.
;
3 | };
4 |
5 | export default StatsPage;
6 |
--------------------------------------------------------------------------------
/app/(more)/uses/page.tsx:
--------------------------------------------------------------------------------
1 | import { UsesTabs } from "@/components/misc/(uses)/uses-tab";
2 | import type { Metadata } from "next/types";
3 |
4 | export const metadata: Metadata = {
5 | title: "vimfn // uses",
6 | description: "A list of software and hardware that I use.",
7 | };
8 |
9 | const usesPage = () => {
10 | return (
11 |
12 |
13 |
Uses
14 | Here's a list of software and hardware that I use on a regular basis.
15 |
16 |
17 |
18 | Every once in a while someone asks me about my development environment
19 | or has questions about certain hardware. I thought it would be fun to
20 | list out everything I use here. Keep in mind, I change things around
21 | quite a bit, but I will try to keep this page updated. If I missed
22 | anything, please let me know.
23 |
24 |
25 |
26 |
27 | );
28 | };
29 |
30 | export default usesPage;
31 |
--------------------------------------------------------------------------------
/app/api/discord/route.ts:
--------------------------------------------------------------------------------
1 | import { NextResponse } from "next/server";
2 |
3 | enum status {
4 | online = 0,
5 | idle = 1,
6 | dnd = 2,
7 | // offline,
8 | }
9 |
10 | export type LanyardResponse = {
11 | data: {
12 | discord_user: {
13 | id: string;
14 | username: string;
15 | discriminator: string;
16 | avatar: string;
17 | };
18 | discord_status: status;
19 | active_on_discord_web: boolean;
20 | active_on_discord_desktop: boolean;
21 | active_on_discord_mobile: boolean;
22 | listening_to_spotify: boolean;
23 | activities: {
24 | id: string;
25 | name: string;
26 | type: number;
27 | state: string;
28 | timestamps: {
29 | end: number;
30 | };
31 | emoji: {
32 | name: string;
33 | };
34 | created_at: number;
35 | }[];
36 | success: boolean;
37 | };
38 | };
39 |
40 | export const dynamic = "force-dynamic";
41 |
42 | export const GET = async () => {
43 | const res = await fetch(
44 | "https://api.lanyard.rest/v1/users/947145304757641216",
45 | {
46 | headers: {
47 | "Content-Type": "application/json",
48 | "cache-control": "public, s-maxage=60, stale-while-revalidate=30",
49 | },
50 | },
51 | );
52 |
53 | return NextResponse.json(await res.json());
54 | };
55 |
--------------------------------------------------------------------------------
/app/api/lastfm/latest/get-album-cover.ts:
--------------------------------------------------------------------------------
1 | import { env } from "@/app/env";
2 |
3 | const ENDPOINT = "https://api.spotify.com/v1/search?q=";
4 |
5 | const CLIENT_ID = env.SPOTIFY_CLIENT_ID as string;
6 | const CLIENT_SECRET = env.SPOTIFY_CLIENT_SECRET as string;
7 | const REFRESH_TOKEN = env.SPOTIFY_REFRESH_TOKEN as string;
8 |
9 | const BASIC = Buffer.from(`${CLIENT_ID}:${CLIENT_SECRET}`).toString("base64");
10 |
11 | const TOKEN_ENDPOINT = "https://accounts.spotify.com/api/token";
12 |
13 | const getAccessToken = async () => {
14 | const response = await fetch(TOKEN_ENDPOINT, {
15 | method: "POST",
16 | headers: {
17 | Authorization: `Basic ${BASIC}`,
18 | "Content-Type": "application/x-www-form-urlencoded",
19 | },
20 | body: new URLSearchParams({
21 | grant_type: "refresh_token",
22 | refresh_token: REFRESH_TOKEN,
23 | }),
24 | });
25 | return await response.json();
26 | };
27 |
28 | export const getAlbumCover = async (track: string) => {
29 | const { access_token } = await getAccessToken();
30 | const URL = ENDPOINT + encodeURI(track) + "&type=track&market=IN&limit=1";
31 | const res = await fetch(URL, {
32 | headers: {
33 | Authorization: `Bearer ${access_token}`,
34 | },
35 | });
36 | const data = await res.json();
37 |
38 | const { name, album, external_urls, preview_url } = data.tracks.items[0];
39 |
40 | return {
41 | name,
42 | releaseDate: album.release_date,
43 | coverArt: album.images[1],
44 | external_urls,
45 | preview_url,
46 | };
47 | };
48 |
--------------------------------------------------------------------------------
/app/api/lastfm/latest/get-latest-song.ts:
--------------------------------------------------------------------------------
1 | import { env } from "@/app/env";
2 | import { getAlbumCover } from "./get-album-cover";
3 |
4 | const LASTFM_API = "https://ws.audioscrobbler.com/2.0";
5 | const LASTFM_USERNAME = "vimfnx";
6 | const LASTFM_ENDPOINT = `${LASTFM_API}?method=user.getRecentTracks&api_key=${env.LASTFM_API_TOKEN}&format=json&user=${LASTFM_USERNAME}&limit=1`;
7 |
8 | type Boolean = "0" | "1";
9 |
10 | interface Text {
11 | "#text": T;
12 | }
13 |
14 | interface MusicBrainzID extends Text {
15 | mbid: string;
16 | }
17 |
18 | interface Image extends Text {
19 | size: "extralarge" | "large" | "medium" | "small";
20 | }
21 |
22 | interface TrackDate extends Text {
23 | uts: string;
24 | }
25 |
26 | interface RecentTrackAttributes {
27 | nowplaying: string;
28 | }
29 |
30 | interface RecentTrack {
31 | "@attr"?: RecentTrackAttributes;
32 | album: MusicBrainzID;
33 | artist: MusicBrainzID;
34 | date?: TrackDate;
35 | image: Image[];
36 | mbid: string;
37 | name: string;
38 | streamable: Boolean;
39 | url: string;
40 | }
41 |
42 | interface RecentTracksAttributes {
43 | page: string;
44 | perPage: string;
45 | total: string;
46 | totalPages: string;
47 | }
48 |
49 | interface RecentTracks {
50 | "@attr": RecentTracksAttributes;
51 |
52 | track: RecentTrack[];
53 | }
54 |
55 | interface LastFmResponse {
56 | recenttracks: RecentTracks;
57 | }
58 |
59 | export interface Response {
60 | artist: string;
61 | cover?: string;
62 | date?: number;
63 | playing: boolean;
64 | title: string;
65 | url: string;
66 | year?: number;
67 | }
68 |
69 | export async function getLatestSong(): Promise {
70 | try {
71 | const response: LastFmResponse = await fetch(LASTFM_ENDPOINT, {
72 | cache: "no-store",
73 | }).then((response) => {
74 | if (!response.ok) {
75 | throw new Error("There was an error while querying the Last.fm API.");
76 | }
77 |
78 | return response.json();
79 | });
80 |
81 | const song = response.recenttracks?.track?.[0];
82 | const date = song.date?.uts ? Number(song.date?.uts) : undefined;
83 | // const cover = song.image[3]["#text"] ? song.image[3]["#text"] : undefined;
84 |
85 | const sp = await getAlbumCover(song.name);
86 |
87 | const year: number = sp.releaseDate.split("-")[0];
88 |
89 | const data = {
90 | title: song.name,
91 | artist: song.artist["#text"],
92 | year,
93 | date,
94 | url: song.url,
95 | cover: sp.coverArt.url as string,
96 | playing: Boolean(song["@attr"]?.nowplaying) ?? !date,
97 | };
98 | return data;
99 | } catch (error) {
100 | console.error(error);
101 | return;
102 | }
103 | }
104 |
--------------------------------------------------------------------------------
/app/api/lastfm/latest/route.ts:
--------------------------------------------------------------------------------
1 | import { NextResponse } from "next/server";
2 | import { getLatestSong } from "./get-latest-song";
3 |
4 | export const runtime = "edge";
5 | /**
6 | * A Route Handler fetching the latest song I listened to from Last.fm.
7 | */
8 | export async function GET() {
9 | const song = await getLatestSong();
10 |
11 | return song
12 | ? NextResponse.json(song)
13 | : new Response(undefined, { status: 500 });
14 | }
15 |
16 | export const dynamic = "force-dynamic";
17 |
--------------------------------------------------------------------------------
/app/api/letterboxd/latest/get-latest-film.ts:
--------------------------------------------------------------------------------
1 | import { XMLParser } from "fast-xml-parser";
2 | import { decode } from "html-entities";
3 |
4 | const LETTERBOXD_USERNAME = "arnvgh";
5 | const LETTERBOXD_URL = "https://letterboxd.com";
6 | const LETTERBOXD_FEED = `${LETTERBOXD_URL}/${LETTERBOXD_USERNAME}/rss/`;
7 | const LETTERBOXD_FILM_URL = (film: string) => `${LETTERBOXD_URL}/film/${film}/`;
8 |
9 | interface XMLParserDocument {
10 | rss: T;
11 | }
12 |
13 | interface FilmEntry {
14 | description: string;
15 | guid: string;
16 | "letterboxd:filmTitle": string;
17 | "letterboxd:filmYear": number;
18 | "letterboxd:memberRating": number;
19 | "letterboxd:rewatch": "No" | "Yes";
20 | "letterboxd:watchedDate": string;
21 | link: string;
22 | title: string;
23 | }
24 |
25 | interface LetterboxdResponse {
26 | channel: {
27 | description: string;
28 |
29 | item: FilmEntry[];
30 |
31 | link: string;
32 |
33 | title: string;
34 | };
35 | }
36 |
37 | export interface Response {
38 | date: string;
39 | poster?: string;
40 |
41 | rating?: number;
42 | title: string;
43 | url: string;
44 | year: number;
45 | }
46 |
47 | // TODO: refactor
48 | export async function getLatestFilm(): Promise {
49 | try {
50 | const response = await fetch(LETTERBOXD_FEED, {
51 | method: "GET",
52 | headers: {
53 | accept:
54 | "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7",
55 | "accept-language": "en-US,en;q=0.9",
56 | "cache-control": "max-age=0",
57 | "sec-ch-ua":
58 | '"Chromium";v="118", "Google Chrome";v="118", "Not=A?Brand";v="99"',
59 | "sec-ch-ua-mobile": "?0",
60 | "sec-ch-ua-platform": '"Linux"',
61 | "sec-fetch-dest": "document",
62 | "sec-fetch-mode": "navigate",
63 | "sec-fetch-site": "none",
64 | "sec-fetch-user": "?1",
65 | "upgrade-insecure-requests": "1",
66 | },
67 | referrerPolicy: "strict-origin-when-cross-origin",
68 | mode: "cors",
69 | credentials: "include",
70 | }).then((response) => {
71 | if (!response.ok) {
72 | throw new Error(
73 | "There was an error while fetching the Letterboxd feed.",
74 | );
75 | }
76 | return response.text();
77 | });
78 |
79 | const parser = new XMLParser();
80 | const { rss }: XMLParserDocument =
81 | parser.parse(response);
82 |
83 | const [film] = rss.channel.item.sort((a, b) =>
84 | b["letterboxd:watchedDate"].localeCompare(a["letterboxd:watchedDate"]),
85 | );
86 | const [poster] =
87 | film.description.match(/(http(s?):)([\s\w./|-])*\.jpg/) ?? [];
88 | const [, slug] = film.link.match(/film\/([^/]*)\/?/) ?? [];
89 |
90 | return {
91 | title: decode(film["letterboxd:filmTitle"]),
92 | year: film["letterboxd:filmYear"],
93 | rating: film["letterboxd:memberRating"],
94 | date: film["letterboxd:watchedDate"],
95 | poster,
96 | url: LETTERBOXD_FILM_URL(slug),
97 | };
98 | } catch (error) {
99 | console.error(error);
100 | return;
101 | }
102 | }
103 |
--------------------------------------------------------------------------------
/app/api/letterboxd/latest/route.ts:
--------------------------------------------------------------------------------
1 | import { NextResponse } from "next/server";
2 | import { getLatestFilm } from "./get-latest-film";
3 |
4 | /**
5 | * A Route Handler fetching the latest film I watched from Letterboxd.
6 | */
7 | export async function GET() {
8 | const film = await getLatestFilm();
9 |
10 | return film
11 | ? NextResponse.json(film)
12 | : new Response(undefined, { status: 500 });
13 | }
14 |
15 | export const runtime = "edge";
16 |
17 | export const dynamic = "force-dynamic";
18 |
--------------------------------------------------------------------------------
/app/api/og/route.tsx:
--------------------------------------------------------------------------------
1 | import { ImageResponse } from "next/og";
2 | import type { 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 postTitle = searchParams.get("title");
9 | const font = fetch(
10 | new URL("../../../public/fonts/kaisei-tokumin-bold.ttf", import.meta.url),
11 | ).then((res) => res.arrayBuffer());
12 | const fontData = await font;
13 |
14 | return new ImageResponse(
15 |
26 |
40 | {postTitle}
41 |
42 |
,
43 | {
44 | width: 1920,
45 | height: 1080,
46 | fonts: [
47 | {
48 | name: "Kaisei Tokumin",
49 | data: fontData,
50 | style: "normal",
51 | },
52 | ],
53 | },
54 | );
55 | }
56 |
--------------------------------------------------------------------------------
/app/env.ts:
--------------------------------------------------------------------------------
1 | import { createEnv } from "@t3-oss/env-nextjs";
2 | import { z } from "zod";
3 |
4 | export const env = createEnv({
5 | server: {
6 | LASTFM_API_TOKEN: z.string().length(32),
7 | GITHUB_TOKEN: z.string().startsWith("ghp_"),
8 | WAKATIME_API_KEY: z.string().min(1),
9 | SPOTIFY_CLIENT_ID: z.string().min(1),
10 | SPOTIFY_CLIENT_SECRET: z.string().min(1),
11 | SPOTIFY_REFRESH_TOKEN: z.string().min(1),
12 | DISCORD_WEBHOOK_URL: z.string().min(30),
13 | NODE_ENV: z
14 | .enum(["development", "test", "production"])
15 | .default("development"),
16 | },
17 | client: {
18 | NEXT_PUBLIC_UMAMI_URL: z.string().url(),
19 | NEXT_PUBLIC_UMAMI_WEBSITE_ID: z.string().uuid(),
20 | },
21 | experimental__runtimeEnv: {
22 | NEXT_PUBLIC_UMAMI_URL: process.env.NEXT_PUBLIC_UMAMI_URL,
23 | NEXT_PUBLIC_UMAMI_WEBSITE_ID: process.env.NEXT_PUBLIC_UMAMI_WEBSITE_ID,
24 | },
25 | });
26 |
--------------------------------------------------------------------------------
/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import type { Metadata } from "next";
2 | import { Inter } from "next/font/google";
3 | import "@/app/globals.css";
4 | import { Footer } from "@/components/layouts/footer";
5 | import Header from "@/components/layouts/header";
6 | import { ThemeProvider } from "@/components/misc/(theme)/theme-provider";
7 | import { Analytics } from "@/components/misc/analytics";
8 | import { Toaster } from "@/components/ui/sonner";
9 | import gradientImg from "@/public/images/gradient.webp";
10 | import Image from "next/image";
11 |
12 | const inter = Inter({ subsets: ["latin"] });
13 |
14 | const info = {
15 | name: "arunava",
16 | twitter: "@vimfnx",
17 | description: "19yo Software Engineer, India",
18 | url: "https://vimfn.in",
19 | image: "/meta/meta.png",
20 | };
21 |
22 | export const metadata: Metadata = {
23 | metadataBase: new URL(info.url),
24 | title: info.name,
25 | description: info.description,
26 | authors: {
27 | name: info.name,
28 | url: info.url,
29 | },
30 | creator: info.name,
31 | openGraph: {
32 | type: "website",
33 | url: info.url,
34 | title: info.name,
35 | description: info.description,
36 | images: info.image,
37 | },
38 | twitter: {
39 | card: "summary_large_image",
40 | title: info.name,
41 | description: info.description,
42 | creator: info.twitter,
43 | images: info.image,
44 | },
45 | };
46 |
47 | interface ChildrenProps {
48 | readonly children: React.ReactNode;
49 | }
50 |
51 | export default function RootLayout({ children }: ChildrenProps) {
52 | return (
53 |
54 |
55 |
61 |
62 |
63 |
64 | {children}
65 |
66 |
67 |
74 |
75 |
76 |
77 |
78 |
79 |
84 |
89 |
90 | );
91 | }
92 |
--------------------------------------------------------------------------------
/app/notes/[slug]/page.tsx:
--------------------------------------------------------------------------------
1 | import { CustomMDX } from "@/components/layouts/mdx";
2 | import { getBlogPosts } from "@/lib/blog";
3 | import { formatDate } from "@/lib/format-date";
4 | import type { Metadata } from "next";
5 | import { notFound } from "next/navigation";
6 |
7 | export async function generateMetadata({
8 | params,
9 | }: {
10 | params: Promise<{ slug: string }>;
11 | }): Promise {
12 | const slug = (await params).slug;
13 | const post = getBlogPosts().find((post) => post.slug === slug);
14 | if (!post) {
15 | return;
16 | }
17 |
18 | const {
19 | title,
20 | publishedAt: publishedTime,
21 | summary: description,
22 | image,
23 | } = post.metadata;
24 | const ogImage = image
25 | ? `https://beta.vimfn.in/${image}`
26 | : `https://beta.vimfn.in/api/og?title=${title}`;
27 |
28 | return {
29 | title,
30 | description,
31 | openGraph: {
32 | title,
33 | description,
34 | type: "article",
35 | publishedTime,
36 | url: `https://beta.vimfn.in/notes/${post.slug}`,
37 | images: [
38 | {
39 | url: ogImage,
40 | },
41 | ],
42 | },
43 | twitter: {
44 | card: "summary_large_image",
45 | title,
46 | description,
47 | images: [ogImage],
48 | },
49 | };
50 | }
51 |
52 | export default async function Blog({
53 | params,
54 | }: {
55 | params: Promise<{ slug: string }>;
56 | }) {
57 | const slug = (await params).slug;
58 | const post = getBlogPosts().find((post) => post.slug === slug);
59 |
60 | if (!post) {
61 | notFound();
62 | }
63 |
64 | return (
65 |
66 |
88 |
89 | {post.metadata.title}
90 |
91 |
92 |
93 | {formatDate(post.metadata.publishedAt)}
94 |
95 |
96 |
97 |
98 |
99 |
100 | );
101 | }
102 |
--------------------------------------------------------------------------------
/app/notes/page.tsx:
--------------------------------------------------------------------------------
1 | import { getBlogPosts } from "@/lib/blog";
2 | import { extractDate } from "@/lib/utils";
3 | import { Rss } from "lucide-react";
4 | import type { Metadata } from "next";
5 | import Link from "next/link";
6 |
7 | export const metadata: Metadata = {
8 | title: "vimfn // notes",
9 | description: "notes I guess.",
10 | };
11 |
12 | const notesPage = () => {
13 | const allBlogs = getBlogPosts();
14 |
15 | return (
16 |
110 | );
111 | };
112 |
113 | export default notesPage;
114 |
--------------------------------------------------------------------------------
/app/page.tsx:
--------------------------------------------------------------------------------
1 | import ContactCard from "@/components/misc/(home)/cards/contact-card";
2 | import { GridCards } from "@/components/misc/(home)/grid-cards";
3 | import { Intro } from "@/components/misc/(home)/intro";
4 |
5 | const Home = () => {
6 | return (
7 |
8 |
9 | arunava
10 |
11 |
12 | software engineer, in/remote
13 |
14 |
15 |
16 |
17 |
18 | );
19 | };
20 |
21 | export default Home;
22 |
--------------------------------------------------------------------------------
/app/robots.ts:
--------------------------------------------------------------------------------
1 | export default function robots() {
2 | return {
3 | rules: [
4 | {
5 | userAgent: "*",
6 | },
7 | ],
8 | sitemap: "https://beta.vimfn.in/sitemap.xml",
9 | host: "https://beta.vimfn.in",
10 | };
11 | }
12 |
--------------------------------------------------------------------------------
/app/rss.xml/route.ts:
--------------------------------------------------------------------------------
1 | import { env } from "@/app/env";
2 | import { getBlogPosts } from "@/lib/blog";
3 | import { NextResponse } from "next/server";
4 | import RSS from "rss";
5 |
6 | export const GET = () => {
7 | const SITE_URL =
8 | env.NODE_ENV === "production"
9 | ? "http://localhost:3000"
10 | : "https://beta.vimfn.in";
11 |
12 | const feed = new RSS({
13 | title: "notes // vimfn",
14 | description: "Read my articles about tech, life and anything in between.",
15 | site_url: `${SITE_URL}`,
16 | feed_url: `${SITE_URL}/rss.xml`,
17 | language: "en-US",
18 | image_url: `${SITE_URL}/meta/og.png`,
19 | });
20 |
21 | const posts = getBlogPosts();
22 |
23 | for (const post of posts) {
24 | feed.item({
25 | title: post.metadata.title,
26 | url: `${SITE_URL}/wiriting/${post.slug}`,
27 | date: post.metadata.publishedAt,
28 | description: post.content,
29 | author: "arunava",
30 | });
31 | }
32 |
33 | return new NextResponse(feed.xml({ indent: true }), {
34 | headers: {
35 | "Content-Type": "application/xml",
36 | },
37 | });
38 | };
39 |
--------------------------------------------------------------------------------
/app/sitemap.ts:
--------------------------------------------------------------------------------
1 | import { getBlogPosts } from "@/lib/blog";
2 |
3 | export default async function sitemap() {
4 | const blogs = getBlogPosts().map((post) => ({
5 | url: `https://beta.vimfn.in/writing/${post.slug}`,
6 | lastModified: post.metadata.publishedAt,
7 | }));
8 |
9 | const routes = [
10 | "",
11 | "/about",
12 | "/writing",
13 | "/uses",
14 | "/work",
15 | "/music",
16 | "/anime",
17 | "/books",
18 | "/nsfw",
19 | "/faqs",
20 | ].map((route) => ({
21 | url: `https://beta.vimfn.in${route}`,
22 | lastModified: new Date().toISOString().split("T")[0],
23 | }));
24 |
25 | return [...routes, ...blogs];
26 | }
27 |
--------------------------------------------------------------------------------
/biome.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
3 | "vcs": {
4 | "enabled": false,
5 | "clientKind": "git",
6 | "useIgnoreFile": false
7 | },
8 | "files": {
9 | "ignoreUnknown": false,
10 | "ignore": []
11 | },
12 | "formatter": {
13 | "enabled": true,
14 | "indentStyle": "space"
15 | },
16 | "organizeImports": {
17 | "enabled": true
18 | },
19 | "linter": {
20 | "enabled": true,
21 | "rules": {
22 | "recommended": true
23 | }
24 | },
25 | "javascript": {
26 | "formatter": {
27 | "quoteStyle": "double"
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/bun.lockb:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arnvgh/www/8ce12004458edb22e5e2b5df984f42e5f93da12c/bun.lockb
--------------------------------------------------------------------------------
/components.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://ui.shadcn.com/schema.json",
3 | "style": "default",
4 | "rsc": true,
5 | "tsx": true,
6 | "tailwind": {
7 | "config": "tailwind.config.ts",
8 | "css": "app/globals.css",
9 | "baseColor": "neutral",
10 | "cssVariables": true,
11 | "prefix": ""
12 | },
13 | "aliases": {
14 | "components": "@/components",
15 | "utils": "@/lib/utils"
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/components/icons/archlinux.tsx:
--------------------------------------------------------------------------------
1 | import type React from "react";
2 |
3 | export function LogosArchlinux(props: React.SVGProps) {
4 | return (
5 |
12 |
16 |
17 | );
18 | }
19 |
--------------------------------------------------------------------------------
/components/icons/cloudflare.tsx:
--------------------------------------------------------------------------------
1 | import type React from "react";
2 |
3 | export const IconCloudflare = (props: React.SVGAttributes) => {
4 | return (
5 |
13 |
14 |
15 |
19 |
23 |
27 |
28 |
29 |
30 |
36 |
37 |
38 |
39 | );
40 | };
41 |
--------------------------------------------------------------------------------
/components/icons/cpp.tsx:
--------------------------------------------------------------------------------
1 | import type { SVGProps } from "react";
2 |
3 | export function VscodeIconsFileTypeCpp3(props: SVGProps) {
4 | return (
5 |
12 |
16 |
20 |
24 |
28 |
32 |
33 | );
34 | }
35 |
--------------------------------------------------------------------------------
/components/icons/discord.tsx:
--------------------------------------------------------------------------------
1 | export function LogosDiscordIcon(props: React.SVGProps) {
2 | return (
3 |
10 |
14 |
15 | );
16 | }
17 |
--------------------------------------------------------------------------------
/components/icons/docker.tsx:
--------------------------------------------------------------------------------
1 | import type React from "react";
2 |
3 | export function SkillIconsDocker(props: React.SVGProps) {
4 | return (
5 |
12 |
13 |
14 |
18 |
19 |
20 | );
21 | }
22 |
--------------------------------------------------------------------------------
/components/icons/figma.tsx:
--------------------------------------------------------------------------------
1 | import type React from "react";
2 |
3 | export const IconFigma = (props: React.SVGAttributes) => {
4 | return (
5 |
13 |
14 |
15 |
19 |
23 |
27 |
31 |
35 |
36 |
37 |
38 |
44 |
45 |
46 |
47 | );
48 | };
49 |
--------------------------------------------------------------------------------
/components/icons/git.tsx:
--------------------------------------------------------------------------------
1 | import type React from "react";
2 |
3 | export const IconGit = (props: React.SVGAttributes) => {
4 | return (
5 |
13 | {" "}
14 |
15 |
16 |
20 |
21 |
22 |
23 |
29 |
30 |
31 |
32 | );
33 | };
34 |
--------------------------------------------------------------------------------
/components/icons/godot.tsx:
--------------------------------------------------------------------------------
1 | import type { SVGProps } from "react";
2 |
3 | export function SkillIconsGodotLight(props: SVGProps) {
4 | return (
5 |
12 |
13 |
14 |
18 |
22 |
26 |
30 |
34 |
38 |
42 |
43 |
44 | );
45 | }
46 |
--------------------------------------------------------------------------------
/components/icons/golang.tsx:
--------------------------------------------------------------------------------
1 | import type { SVGProps } from "react";
2 |
3 | export function SkillIconsGolang(props: SVGProps) {
4 | return (
5 |
12 |
13 |
14 |
18 |
22 |
23 |
24 | );
25 | }
26 |
--------------------------------------------------------------------------------
/components/icons/graphql.tsx:
--------------------------------------------------------------------------------
1 | import type { SVGProps } from "react";
2 |
3 | export function SkillIconsGraphqlDark(props: SVGProps) {
4 | return (
5 |
12 |
13 |
14 |
18 |
19 |
23 |
27 |
31 |
32 |
36 |
40 |
41 |
42 | );
43 | }
44 |
--------------------------------------------------------------------------------
/components/icons/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./cloudflare";
2 | export * from "./figma";
3 | export * from "./firebase";
4 | export * from "./git";
5 | export * from "./jest";
6 | export * from "./mysql";
7 | export * from "./nextjs";
8 | export * from "./nodejs";
9 | export * from "./postgres";
10 | export * from "./prisma";
11 | export * from "./python";
12 | export * from "./react";
13 | export * from "./tailwindcss";
14 | export * from "./typescript";
15 | export * from "./vite";
16 | export * from "./elixir";
17 | export * from "./docker";
18 | export * from "./archlinux";
19 | export * from "./cpp";
20 | export * from "./graphql";
21 | export * from "./golang";
22 | export * from "./godot";
23 | export * from "./music";
24 | export * from "./lastfm";
25 | export * from "./vscode";
26 | export * from "./discord";
27 | export * from "./twitter";
28 |
--------------------------------------------------------------------------------
/components/icons/lastfm.tsx:
--------------------------------------------------------------------------------
1 | export function ArcticonsLastfmscrobbler(
2 | props: React.SVGAttributes,
3 | ) {
4 | return (
5 |
12 |
19 |
26 |
33 |
40 |
41 | );
42 | }
43 |
--------------------------------------------------------------------------------
/components/icons/music.tsx:
--------------------------------------------------------------------------------
1 | import type React from "react";
2 |
3 | export const IconMusic = (props: React.SVGAttributes) => {
4 | return (
5 |
11 |
15 |
16 | );
17 | };
18 |
--------------------------------------------------------------------------------
/components/icons/mysql.tsx:
--------------------------------------------------------------------------------
1 | import type React from "react";
2 |
3 | export const IconMySQL = (props: React.SVGAttributes) => {
4 | return (
5 |
13 |
14 |
15 |
21 |
22 |
23 |
24 |
30 |
31 |
32 |
33 | );
34 | };
35 |
--------------------------------------------------------------------------------
/components/icons/nextjs.tsx:
--------------------------------------------------------------------------------
1 | import type React from "react";
2 |
3 | export const IconNextJS = (props: React.SVGAttributes) => {
4 | return (
5 |
13 |
14 |
18 |
19 | );
20 | };
21 |
--------------------------------------------------------------------------------
/components/icons/nodejs.tsx:
--------------------------------------------------------------------------------
1 | import type React from "react";
2 |
3 | export const IconNodeJS = (props: React.SVGAttributes) => {
4 | return (
5 |
13 |
14 |
18 |
22 |
23 | );
24 | };
25 |
--------------------------------------------------------------------------------
/components/icons/postgres.tsx:
--------------------------------------------------------------------------------
1 | import type React from "react";
2 |
3 | export const IconPostgres = (props: React.SVGAttributes) => {
4 | return (
5 |
13 |
14 |
18 |
25 |
32 |
38 |
45 |
51 |
57 |
64 |
65 | );
66 | };
67 |
--------------------------------------------------------------------------------
/components/icons/prisma.tsx:
--------------------------------------------------------------------------------
1 | import type React from "react";
2 |
3 | export const IconPrisma = (props: React.SVGAttributes) => {
4 | return (
5 |
13 |
14 |
20 |
21 | );
22 | };
23 |
--------------------------------------------------------------------------------
/components/icons/python.tsx:
--------------------------------------------------------------------------------
1 | import type React from "react";
2 |
3 | export const IconPython = (props: React.SVGAttributes) => {
4 | return (
5 |
13 |
14 |
18 |
22 |
23 |
31 |
32 |
33 |
34 |
42 |
43 |
44 |
45 |
46 |
47 | );
48 | };
49 |
--------------------------------------------------------------------------------
/components/icons/react.tsx:
--------------------------------------------------------------------------------
1 | import type React from "react";
2 |
3 | export const IconReactJS = (props: React.SVGAttributes) => {
4 | return (
5 |
13 |
14 |
18 |
25 |
32 |
39 |
40 | );
41 | };
42 |
--------------------------------------------------------------------------------
/components/icons/tailwindcss.tsx:
--------------------------------------------------------------------------------
1 | import type React from "react";
2 |
3 | export const IconTailwindcss = (props: React.SVGAttributes) => {
4 | return (
5 |
13 |
14 |
20 |
21 |
29 |
30 |
31 |
32 |
33 |
34 | );
35 | };
36 |
--------------------------------------------------------------------------------
/components/icons/twitter.tsx:
--------------------------------------------------------------------------------
1 | export function RiTwitterXFill(props: React.SVGProps) {
2 | return (
3 |
10 |
14 |
15 | );
16 | }
17 |
--------------------------------------------------------------------------------
/components/icons/typescript.tsx:
--------------------------------------------------------------------------------
1 | import type React from "react";
2 |
3 | export const IconTypescript = (props: React.SVGAttributes) => {
4 | return (
5 |
13 |
14 |
18 |
22 |
23 | );
24 | };
25 |
--------------------------------------------------------------------------------
/components/icons/vite.tsx:
--------------------------------------------------------------------------------
1 | import type React from "react";
2 |
3 | export const IconVite = (props: React.SVGAttributes) => {
4 | return (
5 |
13 |
14 |
18 |
22 |
23 |
31 |
32 |
33 |
34 |
42 |
43 |
44 |
45 |
46 |
47 |
48 | );
49 | };
50 |
--------------------------------------------------------------------------------
/components/icons/vscode.tsx:
--------------------------------------------------------------------------------
1 | export function LogosVisualStudioCode(props: React.SVGProps) {
2 | return (
3 |
10 |
11 |
18 |
19 |
20 |
21 |
25 |
26 |
27 |
28 |
29 |
34 |
39 |
44 |
50 |
51 | );
52 | }
53 |
--------------------------------------------------------------------------------
/components/layouts/footer.tsx:
--------------------------------------------------------------------------------
1 | import { Emoji } from "@/components/misc/emoji";
2 | import { clsx } from "clsx";
3 | import type { ComponentProps } from "react";
4 |
5 | function getLatestCommit() {
6 | const sha = process.env.NEXT_PUBLIC_VERCEL_GIT_COMMIT_SHA;
7 | const label = sha ? sha.slice(0, 7) : "🏗️";
8 | return label;
9 | }
10 |
11 | export const Footer = ({ className, ...props }: ComponentProps<"footer">) => {
12 | const commit = getLatestCommit();
13 | const year = String(new Date().getFullYear());
14 |
15 | return (
16 |
61 | );
62 | };
63 |
--------------------------------------------------------------------------------
/components/layouts/header.tsx:
--------------------------------------------------------------------------------
1 | import ThemeSwitch from "@/components/misc/(theme)/theme-switch";
2 | import Navbar from "@/components/nav/navbar";
3 |
4 | const Header = () => {
5 | return (
6 |
12 | );
13 | };
14 |
15 | export default Header;
16 |
--------------------------------------------------------------------------------
/components/layouts/tweet.css:
--------------------------------------------------------------------------------
1 | /* Light theme (default) */
2 | .tweet .react-tweet-theme {
3 | /* margin is handled by our wrappers */
4 | --tweet-container-margin: 0;
5 | --tweet-font-family: inherit;
6 | --tweet-font-color: inherit;
7 |
8 | /* Light colors */
9 | --tweet-bg-color: #fff;
10 | --tweet-bg-color-hover: var(--tweet-bg-color);
11 | --tweet-color-blue-secondary: theme("colors.gray.600");
12 | --tweet-color-blue-secondary-hover: theme("colors.gray.100");
13 | --tweet-font-color-secondary: theme("colors.gray.500");
14 |
15 | /* Common properties for both themes */
16 | --tweet-quoted-bg-color-hover: rgba(0, 0, 0, 0.03);
17 | --tweet-border: 1px solid rgb(207, 217, 222);
18 | --tweet-skeleton-gradient: linear-gradient(
19 | 270deg,
20 | #fafafa,
21 | #eaeaea,
22 | #eaeaea,
23 | #fafafa
24 | );
25 | --tweet-color-red-primary: rgb(249, 24, 128);
26 | --tweet-color-red-primary-hover: rgba(249, 24, 128, 0.1);
27 | --tweet-color-green-primary: rgb(0, 186, 124);
28 | --tweet-color-green-primary-hover: rgba(0, 186, 124, 0.1);
29 | --tweet-twitter-icon-color: var(--tweet-font-color);
30 | --tweet-verified-old-color: rgb(130, 154, 171);
31 | --tweet-verified-blue-color: var(--tweet-color-blue-primary);
32 |
33 | --tweet-actions-font-weight: 500;
34 | --tweet-replies-font-weight: 500;
35 | }
36 |
37 | /* Dark theme */
38 | /* @media (prefers-color-scheme: dark) { */
39 | .dark .tweet .react-tweet-theme {
40 | /* Dark theme colors */
41 | --tweet-bg-color: #222;
42 | --tweet-bg-color-hover: var(--tweet-bg-color);
43 | --tweet-quoted-bg-color-hover: rgba(255, 255, 255, 0.03);
44 | --tweet-border: 1px solid #333;
45 | --tweet-color-blue-secondary: theme("colors.white");
46 | --tweet-color-blue-secondary-hover: #333;
47 | --tweet-font-color-secondary: theme("colors.gray.400");
48 | }
49 | /* } */
50 |
51 | /* Common styles for both themes */
52 | .tweet .react-tweet-theme p {
53 | font-size: inherit;
54 | line-height: 1.3rem;
55 | }
56 |
57 | .tweet .react-tweet-theme p a {
58 | @apply border-b transition-[border-color] border-gray-300 hover:border-gray-600;
59 | }
60 |
61 | /* Dark theme link styles */
62 | @media (prefers-color-scheme: dark) {
63 | .tweet .react-tweet-theme p a {
64 | @apply text-white border-gray-500 hover:border-white;
65 | }
66 | }
67 |
68 | /* Remove link underline on hover for both themes */
69 | .tweet .react-tweet-theme p a:hover {
70 | text-decoration: none;
71 | }
72 |
73 | .tweet a div {
74 | @apply font-medium tracking-tight;
75 | }
76 |
--------------------------------------------------------------------------------
/components/layouts/tweet.tsx:
--------------------------------------------------------------------------------
1 | import { EmbeddedTweet, TweetNotFound, type TweetProps } from "react-tweet";
2 | import { getTweet } from "react-tweet/api";
3 | import "./tweet.css";
4 |
5 | const TweetContent = async ({ id, components, onError }: TweetProps) => {
6 | let error;
7 | const tweet = id
8 | ? await getTweet(id).catch((err) => {
9 | if (onError) {
10 | error = onError(err);
11 | } else {
12 | console.error(err);
13 | error = err;
14 | }
15 | })
16 | : undefined;
17 |
18 | if (!tweet) {
19 | const NotFound = components?.TweetNotFound || TweetNotFound;
20 | return ;
21 | }
22 |
23 | return ;
24 | };
25 |
26 | export const ReactTweet = (props: TweetProps) => ;
27 |
28 | export async function TweetComponent({ id }: { id: string }) {
29 | return (
30 |
35 | );
36 | }
37 |
--------------------------------------------------------------------------------
/components/misc/(anime)/anime-card.tsx:
--------------------------------------------------------------------------------
1 | import type { Anime } from "@/app/(more)/anime/anime";
2 |
3 | export const AnimeCard = ({
4 | title,
5 | href,
6 | imgName,
7 | altTitle,
8 | starred,
9 | }: Anime) => {
10 | const altTitleElement = altTitle ? (
11 |
12 | ({altTitle})
13 |
14 | ) : null;
15 |
16 | return (
17 |
23 |
27 |
28 |
{title}
{altTitleElement}
29 |
30 |
31 | );
32 | };
33 |
--------------------------------------------------------------------------------
/components/misc/(home)/cards/animelink-card.tsx:
--------------------------------------------------------------------------------
1 | import animePic from "@/public/images/(nav)/anime.webp";
2 | import { ArrowUpRight } from "lucide-react";
3 | import Image from "next/image";
4 | import Link from "next/link";
5 |
6 | export const AnimeLinkCard = () => {
7 | return (
8 |
9 |
10 |
17 |
18 | こんにちは
19 |
20 |
21 |
22 |
23 | );
24 | };
25 |
--------------------------------------------------------------------------------
/components/misc/(home)/cards/books-card.tsx:
--------------------------------------------------------------------------------
1 | import books from "@/public/images/(home)/books.jpg";
2 | import { Quote } from "lucide-react";
3 | import Image from "next/image";
4 | import Link from "next/link";
5 |
6 | export const BooksCard = () => {
7 | return (
8 |
9 | {" "}
10 |
11 | {/*
Books
*/}
12 |
17 |
18 |
19 |
I lived in book than anywhere else.
20 |
― Neil Gaiman
21 |
22 |
23 |
24 | );
25 | };
26 |
--------------------------------------------------------------------------------
/components/misc/(home)/cards/contact-card.tsx:
--------------------------------------------------------------------------------
1 | // TODO: Add rate limiting and handle errors.
2 | "use client";
3 |
4 | import { Button } from "@/components/ui/button";
5 | import { Input } from "@/components/ui/input";
6 | import { Textarea } from "@/components/ui/textarea";
7 |
8 | import { toast } from "sonner";
9 |
10 | import { zodResolver } from "@hookform/resolvers/zod";
11 | import { useForm } from "react-hook-form";
12 | import { z } from "zod";
13 |
14 | import {
15 | Form,
16 | FormControl,
17 | FormField,
18 | FormItem,
19 | FormMessage,
20 | } from "@/components/ui/form";
21 | import { submitForm } from "@/lib/submit-form";
22 |
23 | const formSchema = z.object({
24 | email: z
25 | .string()
26 | .min(1, { message: "This field has to be filled." })
27 | .email("This is not a valid email.")
28 | .max(64),
29 | message: z.string().max(1500, {
30 | message: "Max 1500 characaters, if you need more please send me an email.",
31 | }),
32 | });
33 |
34 | const ContactCard = () => {
35 | const form = useForm>({
36 | resolver: zodResolver(formSchema),
37 | // https://react.dev/reference/react-dom/components/input#im-getting-an-error-a-component-is-changing-an-uncontrolled-input-to-be-controlled
38 | // the library should handle this by themselves bruh
39 | // i really hate how react got around being huge piece of mess, needs fix.
40 | defaultValues: {
41 | email: "",
42 | message: "",
43 | },
44 | });
45 |
46 | function onSubmit(values: z.infer) {
47 | toast("Message sent!", {
48 | description: "Thanks, I'll get back to you ASAP.",
49 | action: {
50 | label: "Undo",
51 | // TODO: Undo Functionality + x3 for :)
52 | onClick: () => console.log("implement undo function"),
53 | },
54 | });
55 | const { email, message } = values;
56 | submitForm(email, message);
57 | form.reset(
58 | {
59 | email: "",
60 | message: "",
61 | },
62 | {
63 | keepValues: false,
64 | },
65 | );
66 | }
67 |
68 | return (
69 |
113 | );
114 | };
115 |
116 | export default ContactCard;
117 |
--------------------------------------------------------------------------------
/components/misc/(home)/cards/current-time.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { cn } from "@/lib/utils";
4 | import { CloudMoon, CloudSun } from "lucide-react";
5 | import { useEffect, useState } from "react";
6 |
7 | export const CurrentTime = () => {
8 | const [time, setTime] = useState(() => new Date());
9 |
10 | const isNight = time.getHours() >= 17 || time.getHours() < 6;
11 |
12 | useEffect(() => {
13 | const interval = setInterval(() => {
14 | setTime(new Date());
15 | }, 1000);
16 |
17 | return () => clearInterval(interval);
18 | }, []);
19 |
20 | const INTimeFormatter = new Intl.DateTimeFormat(undefined, {
21 | timeZone: "Asia/Kolkata",
22 | hour: "numeric",
23 | minute: "numeric",
24 | hour12: false,
25 | });
26 |
27 | return (
28 |
29 |
30 |
36 |
37 | {isNight ? (
38 |
39 | ) : (
40 |
41 | )}
42 |
43 | {INTimeFormatter.format(time)}
44 | (IST)
45 |
46 |
47 |
48 |
49 | );
50 | };
51 |
--------------------------------------------------------------------------------
/components/misc/(home)/cards/dc-status.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import type { LanyardResponse } from "@/app/api/discord/route";
4 | import { LogosVisualStudioCode } from "@/components/icons";
5 | import fetcher from "@/lib/utils";
6 | import { DiscordLogoIcon } from "@radix-ui/react-icons";
7 | import useSWR from "swr";
8 |
9 | export const DCStatus = () => {
10 | const { data, isLoading, error } = useSWR(
11 | "/api/discord",
12 | fetcher,
13 | );
14 |
15 | return (
16 |
17 |
18 | {/*
*/}
19 |
20 |
21 | {error || isLoading ? (
22 |
23 | offline
24 |
25 | ) : (
26 |
27 | {data?.data.discord_status}
28 |
(@vimfn)
29 |
30 | )}
31 |
32 |
33 | );
34 | };
35 |
--------------------------------------------------------------------------------
/components/misc/(home)/cards/gh-link.tsx:
--------------------------------------------------------------------------------
1 | import ghCat from "@/public/images/(home)/0002.jpg";
2 | import { Github } from "lucide-react";
3 | import Image from "next/image";
4 |
5 | export const GHLink = () => {
6 | return (
7 |
13 |
17 |
23 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 | GitHub
35 | My experiments (aka projects)
36 |
37 |
38 | );
39 | };
40 |
--------------------------------------------------------------------------------
/components/misc/(home)/cards/gh-stats.tsx:
--------------------------------------------------------------------------------
1 | import { getGHStats } from "@/lib/get-gh-stats";
2 |
3 | export const GHStats = async () => {
4 | const { issues, prs, followers, stars } = await getGHStats();
5 | return (
6 |
21 | );
22 | };
23 |
24 | const GitHubStatsData = ({
25 | label,
26 | value,
27 | }: {
28 | label: React.ReactNode;
29 | value: number;
30 | }) => {
31 | return (
32 |
33 |
34 | {label}:
35 |
36 | {value}
37 |
38 | );
39 | };
40 |
41 | const BackgroundPattern = () => {
42 | let seed = 1;
43 | function seededRandom() {
44 | const x = Math.sin(seed++) * 10000;
45 | return x - Math.floor(x);
46 | }
47 | const colours = ["#39d353", "#0e4429", "#0e4429", "#006d32", "#161b22"];
48 | const days = new Array(51)
49 | .fill(null)
50 | .map((_) => colours[Math.floor(seededRandom() * colours.length)]);
51 | return (
52 |
53 | {days.map((c, i) => (
54 |
59 | ))}
60 |
61 | );
62 | };
63 |
--------------------------------------------------------------------------------
/components/misc/(home)/cards/images-card.tsx:
--------------------------------------------------------------------------------
1 | import { getBlogPosts } from "@/lib/blog";
2 | import { cn, extractDate } from "@/lib/utils";
3 | import img1 from "@/public/images/(home)/0001.jpg";
4 | import img2 from "@/public/images/(home)/0002.jpg";
5 | import img3 from "@/public/images/(home)/0003.jpg";
6 | import img4 from "@/public/images/(home)/0004.jpg";
7 | import img5 from "@/public/images/(home)/0005.jpg";
8 | import img6 from "@/public/images/(home)/0006.jpg";
9 | import img7 from "@/public/images/(home)/0007.jpg";
10 | import { PenTool } from "lucide-react";
11 | import Link from "next/link";
12 |
13 | export const ImagesCard = () => {
14 | const allBlogs = getBlogPosts();
15 | allBlogs.toSorted((a, b) => {
16 | if (new Date(a.metadata.publishedAt) > new Date(b.metadata.publishedAt)) {
17 | return -1;
18 | }
19 | return 1;
20 | });
21 | return (
22 |
26 |
27 |
28 |
29 |
Latest Post
30 |
31 | {allBlogs[0].metadata.title}
32 |
33 |
34 |
35 |
36 |
37 |
38 | {extractDate(allBlogs[0].metadata.publishedAt)}
39 |
40 | {/*
41 | --- views
42 |
*/}
43 |
44 |
45 |
46 |
47 | );
48 | };
49 |
--------------------------------------------------------------------------------
/components/misc/(home)/cards/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./images-card";
2 | export * from "./gh-stats";
3 | export * from "./animelink-card";
4 | export * from "./dc-status";
5 | export * from "./locate-me";
6 | export * from "./wakatime-stats";
7 | export * from "./current-time";
8 | export * from "./stacks-card";
9 | export * from "./music-card";
10 | export * from "./links-card";
11 | export * from "./gh-link";
12 | export * from "./books-card";
13 |
--------------------------------------------------------------------------------
/components/misc/(home)/cards/links-card.tsx:
--------------------------------------------------------------------------------
1 | import { RiTwitterXFill } from "@/components/icons";
2 | import { CalendarDays, Linkedin } from "lucide-react";
3 |
4 | export const LinksCard = () => {
5 | return (
6 |
26 | );
27 | };
28 |
--------------------------------------------------------------------------------
/components/misc/(home)/cards/locate-me.tsx:
--------------------------------------------------------------------------------
1 | import Image from "next/image";
2 |
3 | import kolkataImg from "@/public/images/(misc)/kolkata.png";
4 |
5 | export const LocateMe = () => {
6 | return (
7 |
8 |
9 |
16 |
17 | 📍 Kolkata
18 |
19 |
20 |
21 | );
22 | };
23 |
--------------------------------------------------------------------------------
/components/misc/(home)/cards/music-card.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { ArcticonsLastfmscrobbler } from "@/components/icons";
4 | import { useLatestSong } from "@/hooks/use-latest-song";
5 | import { capitalize } from "@/lib/utils";
6 | import { formatDistanceToNow, isYesterday } from "date-fns";
7 | import Image from "next/image";
8 | import Link from "next/link";
9 | import { useMemo } from "react";
10 |
11 | export const MusicCard = () => {
12 | const { artist, cover, date, title, year, playing, url } = useLatestSong();
13 | const absoluteDate = useMemo(() => {
14 | if (!date) return;
15 |
16 | return new Date(date * 1000);
17 | }, [date]);
18 | const relativeDate = useMemo(() => {
19 | if (!absoluteDate) return;
20 |
21 | return isYesterday(absoluteDate)
22 | ? "Yesterday"
23 | : capitalize(formatDistanceToNow(absoluteDate, { addSuffix: true }));
24 | }, [absoluteDate]);
25 |
26 | return (
27 |
31 |
32 |
33 | {absoluteDate ? (
34 |
38 | {relativeDate}
39 |
40 | ) : (
41 |
42 | {playing ? "Listening" : "fetching.."}
43 |
44 | )}
45 |
46 | {title}
47 |
48 | {cover && title && (
49 | <>
50 |
57 |
58 |
68 | >
69 | )}
70 |
71 | );
72 | };
73 |
--------------------------------------------------------------------------------
/components/misc/(home)/cards/stacks-card.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | DeviconElixir,
3 | IconCloudflare,
4 | IconFigma,
5 | IconFirebase,
6 | IconGit,
7 | IconJest,
8 | IconMySQL,
9 | IconNextJS,
10 | IconNodeJS,
11 | IconPostgres,
12 | IconPrisma,
13 | IconPython,
14 | IconReactJS,
15 | IconTailwindcss,
16 | IconTypescript,
17 | IconVite,
18 | LogosArchlinux,
19 | SkillIconsDocker,
20 | SkillIconsGodotLight,
21 | SkillIconsGolang,
22 | SkillIconsGraphqlDark,
23 | VscodeIconsFileTypeCpp3,
24 | } from "@/components/icons";
25 | import { cn } from "@/lib/utils";
26 | import type React from "react";
27 |
28 | type MarqueeProps = {
29 | children: React.ReactNode;
30 | direction?: "left" | "up";
31 | pauseOnHover?: boolean;
32 | reverse?: boolean;
33 | fade?: boolean;
34 | className?: string;
35 | };
36 |
37 | const range = (start: number, end: number): number[] =>
38 | Array.from({ length: end - start }, (_, k) => k + start);
39 |
40 | const Marquee = (props: MarqueeProps) => {
41 | const {
42 | children,
43 | direction = "left",
44 | pauseOnHover = false,
45 | reverse = false,
46 | fade = false,
47 | className,
48 | } = props;
49 |
50 | const ifToRightOrToBottom = (direction: string) => {
51 | if (direction === "left") {
52 | return "to right";
53 | } else {
54 | return "to bottom";
55 | }
56 | };
57 |
58 | return (
59 |
81 | {range(0, 2).map((i) => (
82 |
94 | {children}
95 |
96 | ))}
97 |
98 | );
99 | };
100 |
101 | export const StacksCard = () => {
102 | return (
103 |
104 | {/*
105 | fun things
106 |
*/}
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 | );
137 | };
138 |
--------------------------------------------------------------------------------
/components/misc/(home)/cards/wakatime-stats.tsx:
--------------------------------------------------------------------------------
1 | import { env } from "@/app/env";
2 | import { LogosVisualStudioCode } from "@/components/icons";
3 | import { Code2 } from "lucide-react";
4 |
5 | type WakatimeRes = {
6 | data: {
7 | decimal: string;
8 | digital: string;
9 | is_up_to_date: boolean;
10 | percent_calculated: number;
11 | range: {
12 | end: string;
13 | end_date: string;
14 | end_text: string;
15 | start: string;
16 | start_date: string;
17 | start_text: string;
18 | timezone: string;
19 | };
20 | text: string;
21 | timeout: number;
22 | total_seconds: number;
23 | };
24 | };
25 |
26 | const getCodingHrs = async () => {
27 | return {
28 | seconds: (Math.random() * (3000 - 1400) + 1400) * 3600,
29 | };
30 | // UNREACHABLE
31 | const res = await fetch(
32 | "https://wakatime.com/api/v1/users/current/all_time_since_today",
33 | {
34 | headers: {
35 | Authorization: `Basic ${Buffer.from(env.WAKATIME_API_KEY).toString(
36 | "base64",
37 | )}`,
38 | },
39 | },
40 | );
41 | if (!res.ok) {
42 | console.log(`HTTP error! status: ${res.status}`);
43 | } else {
44 | const data: WakatimeRes = await res.json();
45 |
46 | return {
47 | seconds: data.data.total_seconds,
48 | };
49 | }
50 | };
51 |
52 | export const WakatimeStats = async () => {
53 | // TODO: I DON'T USE WAKATIME ANYMORE
54 | // EITHER REPLACE THIS CARD WITH SOMETHING ELSE OR THINK OTHERWISE.
55 |
56 | const { seconds } = await getCodingHrs();
57 |
58 | return (
59 |
62 |
63 |
64 |
65 |
66 | {Math.round(seconds / 3600)}h
67 |
68 | coding stats
69 | (wakatime)
70 |
71 |
72 | );
73 | };
74 |
--------------------------------------------------------------------------------
/components/misc/(home)/grid-cards.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | AnimeLinkCard,
3 | BooksCard,
4 | DCStatus,
5 | GHLink,
6 | GHStats,
7 | ImagesCard,
8 | LinksCard,
9 | MusicCard,
10 | StacksCard,
11 | WakatimeStats,
12 | } from "@/components/misc/(home)/cards";
13 |
14 | export const GridCards = () => {
15 | return (
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 | );
55 | };
56 |
--------------------------------------------------------------------------------
/components/misc/(home)/intro.tsx:
--------------------------------------------------------------------------------
1 | import { ArrowUpRight } from "lucide-react";
2 | import Link from "next/link";
3 |
4 | export const Intro = () => {
5 | return (
6 |
7 |
8 |
9 | hi, i'm arunava (a.k.a
10 |
14 | vimfn
15 | {" "}
16 | on the internet) currently a cs undergrad at [redacted], my interests
17 | include compilers, computer networks, and ctfs.
18 |
19 |
20 | beyond my studies, i enjoy my time playing chess, jamming to music,
21 | modding factorio or just tinkering with stuff to understand how they
22 | work.
23 |
24 |
25 | ps i love simplicity and a great admirer of unix philosophy,
26 | minimalism and hacker ethics.
27 |
28 |
29 | 🚧 note: this site isn't ready yet, you should better be visting
30 |
34 | vimfn.in
35 | for any info.
36 |
37 |
38 |
42 | notes{" "}
43 |
44 |
45 |
49 | work + exp.{" "}
50 |
51 |
52 |
53 |
54 |
62 |
66 |
67 |
86 |
87 | );
88 | };
89 |
--------------------------------------------------------------------------------
/components/misc/(music)/album-card.tsx:
--------------------------------------------------------------------------------
1 | interface Album {
2 | artist: string;
3 | name: string;
4 | coverImage: string;
5 | href: string;
6 | }
7 |
8 | export const AlbumCard = ({ artist, name, coverImage, href }: Album) => {
9 | return (
10 |
16 |
20 |
21 |
{artist}
22 |
{name}
23 |
24 |
25 | );
26 | };
27 |
--------------------------------------------------------------------------------
/components/misc/(music)/top-albums.tsx:
--------------------------------------------------------------------------------
1 | import { env } from "@/app/env";
2 | import { AlbumCard } from "@/components/misc/(music)/album-card";
3 |
4 | async function getData() {
5 | const res = await fetch(
6 | `http://ws.audioscrobbler.com/2.0/?method=user.gettopalbums&user=vimfnx&period=1month&api_key=${env.LASTFM_API_TOKEN}&format=json`,
7 | );
8 |
9 | if (!res.ok) {
10 | throw new Error("Failed to fetch data");
11 | }
12 |
13 | return res.json();
14 | }
15 |
16 | export default async function TopAblums() {
17 | const data = await getData();
18 |
19 | const truncate = (str: string, n: number) =>
20 | str.length > n ? str.substr(0, n - 1) + "..." : str;
21 |
22 | return (
23 |
24 |
25 | {data.topalbums.album
26 | .filter(
27 | (album: { image: { [x: string]: any }[] }) =>
28 | album.image[3]["#text"],
29 | )
30 | .map(
31 | (album: {
32 | artist: { name: string };
33 | name: any;
34 | image: { [x: string]: string }[];
35 | url: string;
36 | }) => (
37 |
44 | ),
45 | )}
46 |
47 |
48 | );
49 | }
50 |
--------------------------------------------------------------------------------
/components/misc/(theme)/theme-provider.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { ThemeProvider as NextThemesProvider } from "next-themes";
4 | import type { ThemeProviderProps } from "next-themes/dist/types";
5 | import * as React from "react";
6 |
7 | export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
8 | return {children} ;
9 | }
10 |
--------------------------------------------------------------------------------
/components/misc/(theme)/theme-switch.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { AnimatePresence, motion } from "framer-motion";
4 | import { Moon, Sun } from "lucide-react";
5 | import { useTheme } from "next-themes";
6 | import { useEffect, useState } from "react";
7 |
8 | export default function ThemeSwitch() {
9 | const { setTheme, resolvedTheme } = useTheme();
10 | const [mounted, setMounted] = useState(false);
11 |
12 | useEffect(() => {
13 | setMounted(true);
14 | }, []);
15 |
16 | if (!mounted) {
17 | return null;
18 | }
19 |
20 | function onThemeChange() {
21 | setTheme(resolvedTheme === "dark" ? "light" : "dark");
22 | }
23 |
24 | return (
25 | <>
26 |
27 |
33 |
34 | {resolvedTheme === "light" && (
35 |
41 |
42 |
43 | )}
44 | {resolvedTheme === "dark" && (
45 |
51 |
52 |
53 | )}
54 |
55 |
56 |
57 | >
58 | );
59 | }
60 |
--------------------------------------------------------------------------------
/components/misc/(uses)/design-colors.tsx:
--------------------------------------------------------------------------------
1 | //@ts-ignore
2 | const ColorSquare = ({ className, children }) => {
3 | return (
4 |
7 | {children}
8 |
9 | );
10 | };
11 |
12 | export const DesignAndColors = () => (
13 |
14 |
15 | I personally handpicked the color palette. The design is a blend of
16 | inspirations from various people and websites, tailored to my personal
17 | taste and enriched by valuable feedback from friends.
18 |
19 |
20 | 50
21 |
22 | 100
23 |
24 |
25 | 200
26 |
27 |
28 | 300
29 |
30 |
31 | 400
32 |
33 |
34 | 500
35 |
36 |
37 | 600
38 |
39 |
40 | 700
41 |
42 |
43 | 800
44 |
45 |
46 | 900
47 |
48 |
49 | 950
50 |
51 |
52 |
53 | );
54 |
--------------------------------------------------------------------------------
/components/misc/(uses)/my-logo.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | export const MyLogo = () => {
4 | return (
5 |
6 |
7 |
8 | ~
9 |
10 |
11 | ~
12 |
13 |
14 |
15 | );
16 | };
17 |
--------------------------------------------------------------------------------
/components/misc/(uses)/typography.tsx:
--------------------------------------------------------------------------------
1 | import { Kaisei_Tokumin } from "next/font/google";
2 |
3 | const Kaisei = Kaisei_Tokumin({
4 | subsets: ["latin"],
5 | weight: "500",
6 | });
7 |
8 | export const Typography = () => {
9 | return (
10 |
11 |
12 |
13 | Inter Regular
14 |
15 |
16 | Inter Medium
17 |
18 |
23 | Kaisei Tokumin
24 |
25 |
30 | Kaisei Tokumin Bold
31 |
32 |
33 |
34 | );
35 | };
36 |
--------------------------------------------------------------------------------
/components/misc/(uses)/uses-tab.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import {
4 | AllTabs,
5 | BrowserTab,
6 | CodingTab,
7 | EverydayTab,
8 | SoftwareTab,
9 | WebsiteTab,
10 | } from "@/components/misc/(uses)/uses-tabs-comps";
11 | import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
12 | import { useHasMounted } from "@/hooks/use-has-mounted";
13 | import { useEffect, useState } from "react";
14 |
15 | type TabKey =
16 | | "all"
17 | | "everyday"
18 | | "software"
19 | | "browser"
20 | | "coding"
21 | | "website";
22 |
23 | const tabs: Array<{ id: TabKey; title: string }> = [
24 | { id: "all", title: "All" },
25 | { id: "everyday", title: "Everyday" },
26 | { id: "software", title: "Software" },
27 | { id: "browser", title: "Browser" },
28 | { id: "coding", title: "Coding" },
29 | { id: "website", title: "Website" },
30 | ] as const;
31 |
32 | const tabContent: Record = {
33 | all: ,
34 | everyday: ,
35 | software: ,
36 | browser: ,
37 | coding: ,
38 | website: ,
39 | };
40 |
41 | export const UsesTabs = () => {
42 | const hasMounted = useHasMounted();
43 | //@ts-ignore
44 | const [currentTab, setCurrentTab] = useState(() => {
45 | try {
46 | const tabId = (window.location.hash || "#").substring(1);
47 | return tabs.some((tab) => tab.id === tabId) ? (tabId as TabKey) : "all";
48 | } catch (error) {}
49 | });
50 |
51 | useEffect(() => {
52 | if (hasMounted) {
53 | const tabId = (window.location.hash || "#").substring(1);
54 | try {
55 | if (tabs.some((tab) => tab.id === tabId)) {
56 | setCurrentTab(tabId as TabKey);
57 | } else {
58 | setCurrentTab("all");
59 | }
60 | } catch (error) {}
61 | }
62 | }, [hasMounted]);
63 |
64 | return (
65 |
66 |
67 |
68 | {tabs.map((tab) => (
69 | {
73 | setCurrentTab(tab.id);
74 | }}
75 | value={tab.id}
76 | >
77 | {tab.title}
78 |
79 | ))}
80 |
81 |
82 | {tabContent[currentTab]}
83 |
84 | );
85 | };
86 |
--------------------------------------------------------------------------------
/components/misc/analytics.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { env } from "@/app/env";
4 | import Script from "next/script";
5 |
6 | export const Analytics = () => {
7 | return (
8 | );
9 | };
10 |
--------------------------------------------------------------------------------
/components/misc/emoji.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import type { ComponentProps } from "react";
4 | import { useEffect, useState } from "react";
5 |
6 | const EMOJI = [
7 | "🤹",
8 | "👀",
9 | "🇮🇳",
10 | "⛺",
11 | "✨",
12 | "🌚",
13 | "🌱",
14 | "🌸",
15 | "🌹",
16 | "🍂",
17 | "🍬",
18 | "🍭",
19 | "🎀",
20 | "🎈",
21 | "🎉",
22 | "🎨",
23 | "🏝️",
24 | "👋",
25 | "👒",
26 | "📚",
27 | "🔮",
28 | "🗿",
29 | "🥖",
30 | "🦋",
31 | "🧩",
32 | "🧶",
33 | "🪀",
34 | "🪁",
35 | "🪐",
36 | ];
37 |
38 | function getRandomEmoji(exclude?: string) {
39 | const emoji = exclude ? EMOJI.filter((emoji) => emoji !== exclude) : EMOJI;
40 |
41 | return emoji[Math.trunc(emoji.length * Math.random())];
42 | }
43 |
44 | export function Emoji(props: ComponentProps<"span">) {
45 | const [emoji, setEmoji] = useState(EMOJI[0]);
46 |
47 | useEffect(() => {
48 | const interval = window.setInterval(() => {
49 | setEmoji((emoji) => getRandomEmoji(emoji));
50 | }, 500);
51 |
52 | return () => {
53 | window.clearInterval(interval);
54 | };
55 | }, []);
56 |
57 | return {emoji} ;
58 | }
59 |
--------------------------------------------------------------------------------
/components/nav/navbar.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { motion } from "framer-motion";
4 | import Link from "next/link";
5 | import { usePathname } from "next/navigation";
6 | import { NavMenu } from "./navmenu";
7 |
8 | type navItems = {
9 | name: string;
10 | href: string;
11 | }[];
12 |
13 | const navItems: navItems = [
14 | { name: "~", href: "/" },
15 | { name: "work", href: "/work" },
16 | { name: "notes", href: "/notes" },
17 | ];
18 |
19 | const NavBar = () => {
20 | const path = usePathname();
21 |
22 | return (
23 |
24 |
25 | {navItems.map(({ name, href }) => (
26 |
27 |
34 | {path == href && (
35 |
43 | )}
44 | {name}
45 |
46 |
47 | ))}
48 |
49 |
50 |
51 | );
52 | };
53 |
54 | export default NavBar;
55 |
--------------------------------------------------------------------------------
/components/ui/accordion.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as AccordionPrimitive from "@radix-ui/react-accordion";
4 | import { ChevronDown } from "lucide-react";
5 | import * as React from "react";
6 |
7 | import { cn } from "@/lib/utils";
8 |
9 | const Accordion = AccordionPrimitive.Root;
10 |
11 | const AccordionItem = React.forwardRef<
12 | React.ElementRef,
13 | React.ComponentPropsWithoutRef
14 | >(({ className, ...props }, ref) => (
15 |
20 | ));
21 | AccordionItem.displayName = "AccordionItem";
22 |
23 | const AccordionTrigger = React.forwardRef<
24 | React.ElementRef,
25 | React.ComponentPropsWithoutRef
26 | >(({ className, children, ...props }, ref) => (
27 |
28 | svg]:rotate-180",
32 | className,
33 | )}
34 | {...props}
35 | >
36 | {children}
37 |
38 |
39 |
40 | ));
41 | AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName;
42 |
43 | const AccordionContent = React.forwardRef<
44 | React.ElementRef,
45 | React.ComponentPropsWithoutRef
46 | >(({ className, children, ...props }, ref) => (
47 |
52 | {children}
53 |
54 | ));
55 |
56 | AccordionContent.displayName = AccordionPrimitive.Content.displayName;
57 |
58 | export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };
59 |
--------------------------------------------------------------------------------
/components/ui/background-gradient.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@/lib/utils";
2 | import { motion } from "framer-motion";
3 | import type React from "react";
4 |
5 | export const BackgroundGradient = ({
6 | children,
7 | className,
8 | containerClassName,
9 | animate = true,
10 | }: {
11 | children?: React.ReactNode;
12 | className?: string;
13 | containerClassName?: string;
14 | animate?: boolean;
15 | }) => {
16 | const variants = {
17 | initial: {
18 | backgroundPosition: "0 50%",
19 | },
20 | animate: {
21 | backgroundPosition: ["0, 50%", "100% 50%", "0 50%"],
22 | },
23 | };
24 | return (
25 |
26 |
47 |
68 |
69 |
{children}
70 |
71 | );
72 | };
73 |
--------------------------------------------------------------------------------
/components/ui/badge.tsx:
--------------------------------------------------------------------------------
1 | import { type VariantProps, cva } from "class-variance-authority";
2 | import type * as React from "react";
3 |
4 | import { cn } from "@/lib/utils";
5 |
6 | const badgeVariants = cva(
7 | "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
8 | {
9 | variants: {
10 | variant: {
11 | default:
12 | "border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
13 | secondary:
14 | "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
15 | destructive:
16 | "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
17 | outline: "text-foreground",
18 | },
19 | },
20 | defaultVariants: {
21 | variant: "default",
22 | },
23 | },
24 | );
25 |
26 | export interface BadgeProps
27 | extends React.HTMLAttributes,
28 | VariantProps {}
29 |
30 | function Badge({ className, variant, ...props }: BadgeProps) {
31 | return (
32 |
33 | );
34 | }
35 |
36 | export { Badge, badgeVariants };
37 |
--------------------------------------------------------------------------------
/components/ui/button.tsx:
--------------------------------------------------------------------------------
1 | import { Slot } from "@radix-ui/react-slot";
2 | import { type VariantProps, cva } from "class-variance-authority";
3 | import * as React from "react";
4 |
5 | import { cn } from "@/lib/utils";
6 |
7 | const buttonVariants = cva(
8 | "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50",
9 | {
10 | variants: {
11 | variant: {
12 | default:
13 | "bg-primary text-primary-foreground shadow hover:bg-primary/90",
14 | destructive:
15 | "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
16 | outline:
17 | "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
18 | secondary:
19 | "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
20 | ghost: "hover:bg-accent hover:text-accent-foreground",
21 | link: "text-primary underline-offset-4 hover:underline",
22 | },
23 | size: {
24 | default: "h-9 px-4 py-2",
25 | sm: "h-8 rounded-md px-3 text-xs",
26 | lg: "h-10 rounded-md px-8",
27 | icon: "h-9 w-9",
28 | },
29 | },
30 | defaultVariants: {
31 | variant: "default",
32 | size: "default",
33 | },
34 | },
35 | );
36 |
37 | export interface ButtonProps
38 | extends React.ButtonHTMLAttributes,
39 | VariantProps {
40 | asChild?: boolean;
41 | }
42 |
43 | const Button = React.forwardRef(
44 | ({ className, variant, size, asChild = false, ...props }, ref) => {
45 | const Comp = asChild ? Slot : "button";
46 | return (
47 |
52 | );
53 | },
54 | );
55 | Button.displayName = "Button";
56 |
57 | export { Button, buttonVariants };
58 |
--------------------------------------------------------------------------------
/components/ui/drawer.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import { Drawer as DrawerPrimitive } from "vaul";
5 |
6 | import { cn } from "@/lib/utils";
7 |
8 | const Drawer = ({
9 | shouldScaleBackground = true,
10 | ...props
11 | }: React.ComponentProps) => (
12 |
16 | );
17 | Drawer.displayName = "Drawer";
18 |
19 | const DrawerTrigger = DrawerPrimitive.Trigger;
20 |
21 | const DrawerPortal = DrawerPrimitive.Portal;
22 |
23 | const DrawerClose = DrawerPrimitive.Close;
24 |
25 | const DrawerOverlay = React.forwardRef<
26 | React.ElementRef,
27 | React.ComponentPropsWithoutRef
28 | >(({ className, ...props }, ref) => (
29 |
34 | ));
35 | DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName;
36 |
37 | const DrawerContent = React.forwardRef<
38 | React.ElementRef,
39 | React.ComponentPropsWithoutRef
40 | >(({ className, children, ...props }, ref) => (
41 |
42 |
43 |
51 |
52 | {children}
53 |
54 |
55 | ));
56 | DrawerContent.displayName = "DrawerContent";
57 |
58 | const DrawerHeader = ({
59 | className,
60 | ...props
61 | }: React.HTMLAttributes) => (
62 |
66 | );
67 | DrawerHeader.displayName = "DrawerHeader";
68 |
69 | const DrawerFooter = ({
70 | className,
71 | ...props
72 | }: React.HTMLAttributes) => (
73 |
77 | );
78 | DrawerFooter.displayName = "DrawerFooter";
79 |
80 | const DrawerTitle = React.forwardRef<
81 | React.ElementRef,
82 | React.ComponentPropsWithoutRef
83 | >(({ className, ...props }, ref) => (
84 |
92 | ));
93 | DrawerTitle.displayName = DrawerPrimitive.Title.displayName;
94 |
95 | const DrawerDescription = React.forwardRef<
96 | React.ElementRef,
97 | React.ComponentPropsWithoutRef
98 | >(({ className, ...props }, ref) => (
99 |
104 | ));
105 | DrawerDescription.displayName = DrawerPrimitive.Description.displayName;
106 |
107 | export {
108 | Drawer,
109 | DrawerPortal,
110 | DrawerOverlay,
111 | DrawerTrigger,
112 | DrawerClose,
113 | DrawerContent,
114 | DrawerHeader,
115 | DrawerFooter,
116 | DrawerTitle,
117 | DrawerDescription,
118 | };
119 |
--------------------------------------------------------------------------------
/components/ui/form.tsx:
--------------------------------------------------------------------------------
1 | import type * as LabelPrimitive from "@radix-ui/react-label";
2 | import { Slot } from "@radix-ui/react-slot";
3 | import * as React from "react";
4 | import {
5 | Controller,
6 | type ControllerProps,
7 | type FieldPath,
8 | type FieldValues,
9 | FormProvider,
10 | useFormContext,
11 | } from "react-hook-form";
12 |
13 | import { Label } from "@/components/ui/label";
14 | import { cn } from "@/lib/utils";
15 |
16 | const Form = FormProvider;
17 |
18 | type FormFieldContextValue<
19 | TFieldValues extends FieldValues = FieldValues,
20 | TName extends FieldPath = FieldPath,
21 | > = {
22 | name: TName;
23 | };
24 |
25 | const FormFieldContext = React.createContext(
26 | {} as FormFieldContextValue,
27 | );
28 |
29 | const FormField = <
30 | TFieldValues extends FieldValues = FieldValues,
31 | TName extends FieldPath = FieldPath,
32 | >({
33 | ...props
34 | }: ControllerProps) => {
35 | return (
36 |
37 |
38 |
39 | );
40 | };
41 |
42 | const useFormField = () => {
43 | const fieldContext = React.useContext(FormFieldContext);
44 | const itemContext = React.useContext(FormItemContext);
45 | const { getFieldState, formState } = useFormContext();
46 |
47 | const fieldState = getFieldState(fieldContext.name, formState);
48 |
49 | if (!fieldContext) {
50 | throw new Error("useFormField should be used within ");
51 | }
52 |
53 | const { id } = itemContext;
54 |
55 | return {
56 | id,
57 | name: fieldContext.name,
58 | formItemId: `${id}-form-item`,
59 | formDescriptionId: `${id}-form-item-description`,
60 | formMessageId: `${id}-form-item-message`,
61 | ...fieldState,
62 | };
63 | };
64 |
65 | type FormItemContextValue = {
66 | id: string;
67 | };
68 |
69 | const FormItemContext = React.createContext(
70 | {} as FormItemContextValue,
71 | );
72 |
73 | const FormItem = React.forwardRef<
74 | HTMLDivElement,
75 | React.HTMLAttributes
76 | >(({ className, ...props }, ref) => {
77 | const id = React.useId();
78 |
79 | return (
80 |
81 |
82 |
83 | );
84 | });
85 | FormItem.displayName = "FormItem";
86 |
87 | const FormLabel = React.forwardRef<
88 | React.ElementRef,
89 | React.ComponentPropsWithoutRef
90 | >(({ className, ...props }, ref) => {
91 | const { error, formItemId } = useFormField();
92 |
93 | return (
94 |
100 | );
101 | });
102 | FormLabel.displayName = "FormLabel";
103 |
104 | const FormControl = React.forwardRef<
105 | React.ElementRef,
106 | React.ComponentPropsWithoutRef
107 | >(({ ...props }, ref) => {
108 | const { error, formItemId, formDescriptionId, formMessageId } =
109 | useFormField();
110 |
111 | return (
112 |
123 | );
124 | });
125 | FormControl.displayName = "FormControl";
126 |
127 | const FormDescription = React.forwardRef<
128 | HTMLParagraphElement,
129 | React.HTMLAttributes
130 | >(({ className, ...props }, ref) => {
131 | const { formDescriptionId } = useFormField();
132 |
133 | return (
134 |
140 | );
141 | });
142 | FormDescription.displayName = "FormDescription";
143 |
144 | const FormMessage = React.forwardRef<
145 | HTMLParagraphElement,
146 | React.HTMLAttributes
147 | >(({ className, children, ...props }, ref) => {
148 | const { error, formMessageId } = useFormField();
149 | const body = error ? String(error?.message) : children;
150 |
151 | if (!body) {
152 | return null;
153 | }
154 |
155 | return (
156 |
162 | {body}
163 |
164 | );
165 | });
166 | FormMessage.displayName = "FormMessage";
167 |
168 | export {
169 | useFormField,
170 | Form,
171 | FormItem,
172 | FormLabel,
173 | FormControl,
174 | FormDescription,
175 | FormMessage,
176 | FormField,
177 | };
178 |
--------------------------------------------------------------------------------
/components/ui/hover-card.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as HoverCardPrimitive from "@radix-ui/react-hover-card";
4 | import * as React from "react";
5 |
6 | import { cn } from "@/lib/utils";
7 |
8 | const HoverCard = HoverCardPrimitive.Root;
9 |
10 | const HoverCardTrigger = HoverCardPrimitive.Trigger;
11 |
12 | const HoverCardContent = React.forwardRef<
13 | React.ElementRef,
14 | React.ComponentPropsWithoutRef
15 | >(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
16 |
26 | ));
27 | HoverCardContent.displayName = HoverCardPrimitive.Content.displayName;
28 |
29 | export { HoverCard, HoverCardTrigger, HoverCardContent };
30 |
--------------------------------------------------------------------------------
/components/ui/input.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 |
3 | import { cn } from "@/lib/utils";
4 |
5 | export interface InputProps
6 | extends React.InputHTMLAttributes {}
7 |
8 | const Input = React.forwardRef(
9 | ({ className, type, ...props }, ref) => {
10 | return (
11 |
20 | );
21 | },
22 | );
23 | Input.displayName = "Input";
24 |
25 | export { Input };
26 |
--------------------------------------------------------------------------------
/components/ui/label.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as LabelPrimitive from "@radix-ui/react-label";
4 | import { type VariantProps, cva } from "class-variance-authority";
5 | import * as React from "react";
6 |
7 | import { cn } from "@/lib/utils";
8 |
9 | const labelVariants = cva(
10 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
11 | );
12 |
13 | const Label = React.forwardRef<
14 | React.ElementRef,
15 | React.ComponentPropsWithoutRef &
16 | VariantProps
17 | >(({ className, ...props }, ref) => (
18 |
23 | ));
24 | Label.displayName = LabelPrimitive.Root.displayName;
25 |
26 | export { Label };
27 |
--------------------------------------------------------------------------------
/components/ui/sonner.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useTheme } from "next-themes";
4 | import { Toaster as Sonner } from "sonner";
5 |
6 | type ToasterProps = React.ComponentProps;
7 |
8 | const Toaster = ({ ...props }: ToasterProps) => {
9 | const { theme = "system" } = useTheme();
10 |
11 | return (
12 |
28 | );
29 | };
30 |
31 | export { Toaster };
32 |
--------------------------------------------------------------------------------
/components/ui/table.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 |
3 | import { cn } from "@/lib/utils";
4 |
5 | const Table = React.forwardRef<
6 | HTMLTableElement,
7 | React.HTMLAttributes
8 | >(({ className, ...props }, ref) => (
9 |
16 | ));
17 | Table.displayName = "Table";
18 |
19 | const TableHeader = React.forwardRef<
20 | HTMLTableSectionElement,
21 | React.HTMLAttributes
22 | >(({ className, ...props }, ref) => (
23 |
24 | ));
25 | TableHeader.displayName = "TableHeader";
26 |
27 | const TableBody = React.forwardRef<
28 | HTMLTableSectionElement,
29 | React.HTMLAttributes
30 | >(({ className, ...props }, ref) => (
31 |
36 | ));
37 | TableBody.displayName = "TableBody";
38 |
39 | const TableFooter = React.forwardRef<
40 | HTMLTableSectionElement,
41 | React.HTMLAttributes
42 | >(({ className, ...props }, ref) => (
43 | tr]:last:border-b-0",
47 | className,
48 | )}
49 | {...props}
50 | />
51 | ));
52 | TableFooter.displayName = "TableFooter";
53 |
54 | const TableRow = React.forwardRef<
55 | HTMLTableRowElement,
56 | React.HTMLAttributes
57 | >(({ className, ...props }, ref) => (
58 |
66 | ));
67 | TableRow.displayName = "TableRow";
68 |
69 | const TableHead = React.forwardRef<
70 | HTMLTableCellElement,
71 | React.ThHTMLAttributes
72 | >(({ className, ...props }, ref) => (
73 |
81 | ));
82 | TableHead.displayName = "TableHead";
83 |
84 | const TableCell = React.forwardRef<
85 | HTMLTableCellElement,
86 | React.TdHTMLAttributes
87 | >(({ className, ...props }, ref) => (
88 |
93 | ));
94 | TableCell.displayName = "TableCell";
95 |
96 | const TableCaption = React.forwardRef<
97 | HTMLTableCaptionElement,
98 | React.HTMLAttributes
99 | >(({ className, ...props }, ref) => (
100 |
105 | ));
106 | TableCaption.displayName = "TableCaption";
107 |
108 | export {
109 | Table,
110 | TableHeader,
111 | TableBody,
112 | TableFooter,
113 | TableHead,
114 | TableRow,
115 | TableCell,
116 | TableCaption,
117 | };
118 |
--------------------------------------------------------------------------------
/components/ui/tabs.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as TabsPrimitive from "@radix-ui/react-tabs";
4 | import * as React from "react";
5 |
6 | import { cn } from "@/lib/utils";
7 |
8 | const Tabs = TabsPrimitive.Root;
9 |
10 | const TabsList = React.forwardRef<
11 | React.ElementRef,
12 | React.ComponentPropsWithoutRef
13 | >(({ className, ...props }, ref) => (
14 |
22 | ));
23 | TabsList.displayName = TabsPrimitive.List.displayName;
24 |
25 | const TabsTrigger = React.forwardRef<
26 | React.ElementRef,
27 | React.ComponentPropsWithoutRef & {
28 | href: string;
29 | }
30 | >(({ className, href, ...props }, ref) => (
31 |
32 |
40 |
41 | ));
42 | TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
43 |
44 | const TabsContent = React.forwardRef<
45 | React.ElementRef,
46 | React.ComponentPropsWithoutRef
47 | >(({ className, ...props }, ref) => (
48 |
56 | ));
57 | TabsContent.displayName = TabsPrimitive.Content.displayName;
58 |
59 | export { Tabs, TabsList, TabsTrigger, TabsContent };
60 |
--------------------------------------------------------------------------------
/components/ui/textarea.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 |
3 | import { cn } from "@/lib/utils";
4 |
5 | export interface TextareaProps
6 | extends React.TextareaHTMLAttributes {}
7 |
8 | const Textarea = React.forwardRef(
9 | ({ className, ...props }, ref) => {
10 | return (
11 |
19 | );
20 | },
21 | );
22 | Textarea.displayName = "Textarea";
23 |
24 | export { Textarea };
25 |
--------------------------------------------------------------------------------
/components/utils/characters.tsx:
--------------------------------------------------------------------------------
1 | import type { CSSProperties, ComponentProps } from "react";
2 | import { useMemo } from "react";
3 |
4 | export interface CharactersProps
5 | extends Omit, "children"> {
6 | children?: string;
7 | }
8 |
9 | export function Characters({
10 | children = "",
11 | style,
12 | ...props
13 | }: CharactersProps) {
14 | //@ts-ignore
15 | const characters = useMemo(() => [...children], [children]);
16 |
17 | return (
18 |
19 | {characters.map((character, index) => (
20 |
27 | {character}
28 |
29 | ))}
30 |
31 | );
32 | }
33 |
--------------------------------------------------------------------------------
/components/utils/relative-date.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { capitalize } from "@/lib/utils";
4 | import { formatDistanceToNowStrict } from "date-fns";
5 | import type { ComponentProps } from "react";
6 | import { useMemo } from "react";
7 |
8 | interface Props extends ComponentProps<"time"> {
9 | date: Date | number | string;
10 | }
11 |
12 | export function RelativeDate({ date, ...props }: Props) {
13 | const parsedDate = useMemo(() => new Date(date), [date]);
14 | const normalizedDate = useMemo(() => parsedDate.toISOString(), [parsedDate]);
15 | const formattedDate = useMemo(() => {
16 | return `${capitalize(formatDistanceToNowStrict(parsedDate))} ago`;
17 | }, [parsedDate]);
18 |
19 | return (
20 |
21 | {formattedDate}
22 |
23 | );
24 | }
25 |
--------------------------------------------------------------------------------
/components/utils/skeleton.tsx:
--------------------------------------------------------------------------------
1 | import { clsx } from "clsx";
2 | import type { ComponentProps } from "react";
3 |
4 | export function Skeleton({ className, ...props }: ComponentProps<"span">) {
5 | return (
6 |
13 | );
14 | }
15 |
--------------------------------------------------------------------------------
/content/demo.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: markdown syntax
3 | publishedAt: "2025-01-01"
4 | summary: "summary goes here.."
5 | ---
6 |
7 | ## binary search
8 |
9 | ```rs
10 | use std::cmp::Ordering;
11 |
12 | fn binary_search(arr: &[T], desired: &T) -> Option {
13 | let mut low = 0;
14 | let mut high = arr.len() - 1;
15 |
16 | while low <= high {
17 | let mid = (low + high) / 2;
18 |
19 | match arr[mid].cmp(key) {
20 | Ordering::Less => low = mid + 1,
21 | Ordering::Greater => high = mid - 1,
22 | Ordering::Equal => return Some(mid),
23 | }
24 | }
25 | None
26 | }
27 | ```
28 |
29 |
30 |
31 | # h1
32 | ## h2
33 | ### h3
34 |
35 |
36 |
37 |
38 | Additionally, all opinions are mine, and they **can change as I gain more experience and learn new things**. This post is **not** some sort of advice or guide but just a reflection.
39 |
40 |
41 |
42 | ## emphasis
43 |
44 | *this text will be italic*
45 | _this will also be italic_
46 |
47 | **this text will be bold**
48 | __this will also be bold__
49 |
50 | _you **can** combine them_
51 |
52 | ## lists
53 |
54 | ### unordered
55 |
56 | * item 1
57 | * item 2
58 | * item 2a
59 | * item 2b
60 | * item 3a
61 | * item 3b
62 |
63 | ### ordered
64 |
65 | 1. item 1
66 | 2. item 2
67 | 3. item 3
68 | 1. item 3a
69 | 2. item 3b
70 |
71 | ## links
72 |
73 | find the source here [github/vimfn/www](https://github.com/vimfn/www)
74 |
75 |
76 | ## blockquotes
77 |
78 | > markdown is a lightweight markup language with plain-text-formatting syntax, created in 2004 by john gruber with aaron swartz.
79 | >
80 | >> markdown is often used to format readme files, for writing messages in online discussion forums, and to create rich text using a plain text editor.
81 |
82 |
--------------------------------------------------------------------------------
/content/ext_uses.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: "my workflow + note taking as a cs undergrad (osx/linux/android)"
3 | publishedAt: "2024-08-11"
4 | summary: "for those with adhd-mind or too lazy to read the whole thing a tl;dr is at the end."
5 | externalLink: https://www.vimfn.in/notes/workflow
6 | ---
7 |
--------------------------------------------------------------------------------
/hooks/use-has-mounted.ts:
--------------------------------------------------------------------------------
1 | "use client";
2 | import { useEffect, useState } from "react";
3 |
4 | export const useHasMounted = () => {
5 | const [hasMounted, setHasMounted] = useState(false);
6 | useEffect(() => {
7 | setHasMounted(true);
8 | }, []);
9 | return hasMounted;
10 | };
11 |
--------------------------------------------------------------------------------
/hooks/use-latest-song.ts:
--------------------------------------------------------------------------------
1 | import fetcher from "@/lib/utils";
2 | import useSWR from "swr";
3 | import type { Response } from "../app/api/lastfm/latest/get-latest-song";
4 |
5 | export function useLatestSong(): Partial {
6 | const { data } = useSWR("/api/lastfm/latest", fetcher);
7 |
8 | return data ?? {};
9 | }
10 |
--------------------------------------------------------------------------------
/lib/blog.ts:
--------------------------------------------------------------------------------
1 | import fs from "fs";
2 | import path from "path";
3 |
4 | type Metadata = {
5 | title: string;
6 | publishedAt: string;
7 | summary: string;
8 | image?: string;
9 | externalLink?: string;
10 | draft?: boolean;
11 | };
12 |
13 | function parseFrontmatter(fileContent: string) {
14 | const frontmatterRegex = /---\s*([\s\S]*?)\s*---/;
15 | const match = frontmatterRegex.exec(fileContent);
16 | const frontMatterBlock = match![1];
17 | const content = fileContent.replace(frontmatterRegex, "").trim();
18 | const frontMatterLines = frontMatterBlock.trim().split("\n");
19 | const metadata: Partial = {};
20 |
21 | frontMatterLines.forEach((line) => {
22 | const [key, ...valueArr] = line.split(": ");
23 | let value = valueArr.join(": ").trim();
24 | value = value.replace(/^['"](.*)['"]$/, "$1");
25 | // @ts-ignore
26 | metadata[key.trim() as keyof Metadata] = value;
27 | });
28 |
29 | // console.log(metadata)
30 | return { metadata: metadata as Metadata, content };
31 | }
32 |
33 | function getMDXFiles(dir: string) {
34 | return fs.readdirSync(dir).filter((file) => path.extname(file) === ".mdx");
35 | }
36 |
37 | function readMDXFile(filePath: string) {
38 | const rawContent = fs.readFileSync(filePath, "utf-8");
39 | return parseFrontmatter(rawContent);
40 | }
41 |
42 | function extractTweetIds(content: string) {
43 | const tweetMatches = content.match(/ /g);
44 | return tweetMatches?.map((tweet) => tweet.match(/[0-9]+/g)![0]) || [];
45 | }
46 |
47 | function getMDXData(dir: string) {
48 | const mdxFiles = getMDXFiles(dir);
49 | return mdxFiles.map((file) => {
50 | const { metadata, content } = readMDXFile(path.join(dir, file));
51 | const slug = path.basename(file, path.extname(file));
52 | const tweetIds = extractTweetIds(content);
53 | return {
54 | metadata,
55 | slug,
56 | tweetIds,
57 | content,
58 | };
59 | });
60 | }
61 |
62 | export function getBlogPosts() {
63 | return getMDXData(path.join(process.cwd(), "content"));
64 | }
65 |
--------------------------------------------------------------------------------
/lib/format-date.ts:
--------------------------------------------------------------------------------
1 | import {
2 | differenceInDays,
3 | differenceInMonths,
4 | differenceInYears,
5 | format,
6 | isBefore,
7 | isToday,
8 | } from "date-fns";
9 |
10 | export function formatDate(date: string) {
11 | const currentDate = new Date();
12 | if (!date.includes("T")) {
13 | date = `${date}T00:00:00`;
14 | }
15 | const targetDate = new Date(date);
16 | if (isToday(targetDate)) {
17 | const todayDate = format(currentDate, "MMMM d, yyyy");
18 | return `${todayDate} (Today)`;
19 | }
20 |
21 | const yearsDiff = differenceInYears(targetDate, currentDate);
22 | const monthsDiff = differenceInMonths(targetDate, currentDate);
23 | const daysDiff = differenceInDays(targetDate, currentDate);
24 |
25 | let formattedDate = "";
26 |
27 | if (isBefore(targetDate, currentDate)) {
28 | if (yearsDiff < 0) {
29 | formattedDate = `${yearsDiff * -1}y ago`;
30 | } else if (monthsDiff < 0) {
31 | formattedDate = `${monthsDiff * -1}mo ago`;
32 | } else if (daysDiff < 0) {
33 | formattedDate = `${daysDiff * -1}d ago`;
34 | }
35 | } else {
36 | if (yearsDiff > 0) {
37 | formattedDate = `${yearsDiff}y`;
38 | } else if (monthsDiff > 0) {
39 | formattedDate = `${monthsDiff}mo`;
40 | } else if (daysDiff > 0) {
41 | formattedDate = `${daysDiff}d`;
42 | }
43 | formattedDate += " remaining";
44 | }
45 |
46 | const fullDate = format(targetDate, "MMMM d, yyyy");
47 |
48 | return `${fullDate} (${formattedDate})`;
49 | }
50 |
--------------------------------------------------------------------------------
/lib/get-gh-stats.ts:
--------------------------------------------------------------------------------
1 | import { octokit } from "@/lib/octokit";
2 | import { unstable_cache as cache } from "next/cache";
3 |
4 | export const getGHStats = cache(
5 | async () => {
6 | const gql = String.raw;
7 | const { user } = await octokit.graphql<{
8 | user: {
9 | repositoriesContributedTo: { totalCount: number };
10 | pullRequests: { totalCount: number };
11 | openIssues: { totalCount: number };
12 | closedIssues: { totalCount: number };
13 | followers: { totalCount: number };
14 | repositories: {
15 | totalCount: number;
16 | nodes: {
17 | stargazers: { totalCount: number };
18 | }[];
19 | pageInfo: {
20 | hasNextPage: boolean;
21 | endCursor: string | null;
22 | };
23 | };
24 | };
25 | }>(
26 | gql`
27 | query ($login: String!) {
28 | user(login: $login) {
29 | pullRequests(first: 1) {
30 | totalCount
31 | }
32 | openIssues: issues(states: OPEN) {
33 | totalCount
34 | }
35 | closedIssues: issues(states: CLOSED) {
36 | totalCount
37 | }
38 | followers {
39 | totalCount
40 | }
41 | repositories(ownerAffiliations: OWNER, first: 100) {
42 | totalCount
43 | nodes {
44 | stargazers {
45 | totalCount
46 | }
47 | }
48 | pageInfo {
49 | hasNextPage
50 | endCursor
51 | }
52 | }
53 | }
54 | }
55 | `,
56 | { login: "vimfn" },
57 | );
58 | return {
59 | issues: user.closedIssues.totalCount + user.openIssues.totalCount,
60 | prs: user.pullRequests.totalCount,
61 | followers: user.followers.totalCount,
62 | stars: user.repositories.nodes.reduce(
63 | (totalStars, repo) => totalStars + repo.stargazers.totalCount,
64 | 0,
65 | ),
66 | };
67 | },
68 | [],
69 | { revalidate: 3600 }, // revalidates 1hr
70 | );
71 |
--------------------------------------------------------------------------------
/lib/octokit.ts:
--------------------------------------------------------------------------------
1 | import { env } from "@/app/env";
2 | import { Octokit } from "octokit";
3 |
4 | export const octokit = new Octokit({ auth: env.GITHUB_TOKEN });
5 |
--------------------------------------------------------------------------------
/lib/report.ts:
--------------------------------------------------------------------------------
1 | "use server";
2 |
3 | const report = async (e: any) => {
4 | // TODO: CRASH WEBHOOK URL
5 | console.log(e)
6 | return;
7 | };
8 |
9 | export default report;
10 |
--------------------------------------------------------------------------------
/lib/submit-form.tsx:
--------------------------------------------------------------------------------
1 | "use server";
2 |
3 | import { env } from "@/app/env";
4 | import { headers } from "next/headers";
5 |
6 | const WEBHOOK_URL = env.DISCORD_WEBHOOK_URL;
7 | const FALLBACK_IP_ADDRESS = "0.0.0.0";
8 |
9 | const Identifier = async () => {
10 | const h = await headers();
11 | const forwardedFor = h.get("x-forwarded-for");
12 |
13 | if (forwardedFor) {
14 | return forwardedFor.split(",")[0] ?? FALLBACK_IP_ADDRESS;
15 | }
16 |
17 | return h.get("x-real-ip") ?? FALLBACK_IP_ADDRESS;
18 | };
19 |
20 | export const submitForm = async (email: string, message: string) => {
21 | // TODO: use a token to verify email id provided, and only then dispatch the webhook.
22 |
23 | await fetch(WEBHOOK_URL, {
24 | method: "POST",
25 | headers: {
26 | Accept: "application/json",
27 | "Content-Type": "application/json",
28 | },
29 | body: JSON.stringify({
30 | content: Identifier() + "\n" + email + "\n" + message,
31 | }),
32 | });
33 | };
34 |
--------------------------------------------------------------------------------
/lib/utils.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 |
8 | export const extractDate = (dateString: string | number | Date) =>
9 | new Date(dateString).toLocaleDateString("en-US", {
10 | day: "numeric",
11 | month: "short",
12 | year: "numeric",
13 | });
14 |
15 | export function capitalize(string: string) {
16 | return string.charAt(0).toUpperCase() + string.slice(1);
17 | }
18 |
19 | export default async function fetcher(
20 | input: RequestInfo,
21 | init?: RequestInit,
22 | ): Promise {
23 | const res = await fetch(input, init);
24 | return res.json();
25 | }
26 |
--------------------------------------------------------------------------------
/next.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {
3 | images: {
4 | remotePatterns: [
5 | {
6 | protocol: "https",
7 | hostname: "lastfm.freetls.fastly.net",
8 | port: "",
9 | pathname: "/**",
10 | },
11 | {
12 | protocol: "https",
13 | hostname: "i.scdn.co",
14 | port: "",
15 | pathname: "/**",
16 | },
17 | ],
18 | },
19 | async redirects() {
20 | return [
21 | {
22 | source: "/(gh|github|git)/:slug*",
23 | destination: "https://github.com/vimfn/:slug*",
24 | permanent: true,
25 | },
26 | {
27 | source: "/sponsor",
28 | destination: "https://github.com/sponsors/vimfn",
29 | permanent: true,
30 | },
31 | {
32 | source: "/(twitter|x)",
33 | destination: "https://twitter.com/vimfnx",
34 | permanent: true,
35 | },
36 | {
37 | source: "/(linkedin|ln)",
38 | destination: "https://www.linkedin.com/in/vimfn/",
39 | permanent: true,
40 | },
41 | {
42 | source: "/(spotify|sp)",
43 | destination: "https://open.spotify.com/user/zfu9cur8fpnw6oc4q8vm55op6",
44 | permanent: true,
45 | },
46 | {
47 | source: "/feed.xml",
48 | destination: "https://beta.vimfn.in/rss.xml",
49 | permanent: true,
50 | },
51 | {
52 | source: "/feed",
53 | destination: "https://beta.vimfn.in/rss.xml",
54 | permanent: true,
55 | },
56 | {
57 | source: "/rss",
58 | destination: "https://beta.vimfn.in/rss.xml",
59 | permanent: true,
60 | },
61 | {
62 | source: "/gpg",
63 | destination: "https://github.com/vimfn.gpg",
64 | permanent: true,
65 | },
66 | ];
67 | },
68 | };
69 |
70 | export default nextConfig;
71 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "www",
3 | "version": "0.0.0",
4 | "scripts": {
5 | "dev": "next dev -p 6969",
6 | "build": "next build",
7 | "start": "next start",
8 | "check:preview": "biome check .",
9 | "check:write": "biome check --write ."
10 | },
11 | "dependencies": {
12 | "@hookform/resolvers": "^3.10.0",
13 | "@radix-ui/react-accordion": "^1.2.3",
14 | "@radix-ui/react-dialog": "^1.1.6",
15 | "@radix-ui/react-dropdown-menu": "^2.1.6",
16 | "@radix-ui/react-hover-card": "^1.1.6",
17 | "@radix-ui/react-icons": "^1.3.2",
18 | "@radix-ui/react-label": "^2.1.2",
19 | "@radix-ui/react-navigation-menu": "^1.2.5",
20 | "@radix-ui/react-slot": "^1.1.2",
21 | "@radix-ui/react-tabs": "^1.1.3",
22 | "@t3-oss/env-nextjs": "^0.11.1",
23 | "class-variance-authority": "^0.7.1",
24 | "clsx": "^2.1.1",
25 | "date-fns": "^3.6.0",
26 | "embla-carousel-autoplay": "8.2.0",
27 | "embla-carousel-react": "8.2.0",
28 | "fast-xml-parser": "^4.5.1",
29 | "framer-motion": "^11.18.2",
30 | "html-entities": "^2.5.2",
31 | "lucide-react": "^0.437.0",
32 | "next": "^15.1.6",
33 | "next-mdx-remote": "^5.0.0",
34 | "next-themes": "^0.3.0",
35 | "octokit": "^4.1.1",
36 | "plaiceholder": "^3.0.0",
37 | "react": "^19.0.0",
38 | "react-dom": "^19.0.0",
39 | "react-hook-form": "^7.54.2",
40 | "react-tweet": "^3.2.1",
41 | "rss": "^1.2.2",
42 | "sharp": "0.33.5",
43 | "sonner": "^1.7.4",
44 | "sugar-high": "^0.7.5",
45 | "swr": "^2.3.2",
46 | "tailwind-merge": "^2.6.0",
47 | "tailwindcss-animate": "^1.0.7",
48 | "vaul": "^0.9.9",
49 | "zod": "^3.24.1"
50 | },
51 | "devDependencies": {
52 | "@biomejs/biome": "1.9.4",
53 | "@eslint/js": "^9.19.0",
54 | "@octokit/types": "^13.8.0",
55 | "@tailwindcss/typography": "^0.5.16",
56 | "@types/node": "^22.13.1",
57 | "@types/react": "^18.3.18",
58 | "@types/react-dom": "^18.3.5",
59 | "@types/rss": "^0.0.32",
60 | "autoprefixer": "^10.4.20",
61 | "eslint": "^9.19.0",
62 | "eslint-config-next": "14.2.7",
63 | "eslint-plugin-react": "^7.37.4",
64 | "globals": "^15.14.0",
65 | "postcss": "^8.5.1",
66 | "tailwindcss": "^3.4.17",
67 | "typescript": "^5.7.3",
68 | "typescript-eslint": "^8.23.0"
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | };
7 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arnvgh/www/8ce12004458edb22e5e2b5df984f42e5f93da12c/public/favicon.ico
--------------------------------------------------------------------------------
/public/favicons/favicon-dark.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arnvgh/www/8ce12004458edb22e5e2b5df984f42e5f93da12c/public/favicons/favicon-dark.ico
--------------------------------------------------------------------------------
/public/favicons/favicon-light.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arnvgh/www/8ce12004458edb22e5e2b5df984f42e5f93da12c/public/favicons/favicon-light.ico
--------------------------------------------------------------------------------
/public/fonts/kaisei-tokumin-bold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arnvgh/www/8ce12004458edb22e5e2b5df984f42e5f93da12c/public/fonts/kaisei-tokumin-bold.ttf
--------------------------------------------------------------------------------
/public/images/(anime)/angel.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arnvgh/www/8ce12004458edb22e5e2b5df984f42e5f93da12c/public/images/(anime)/angel.webp
--------------------------------------------------------------------------------
/public/images/(anime)/aot.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arnvgh/www/8ce12004458edb22e5e2b5df984f42e5f93da12c/public/images/(anime)/aot.webp
--------------------------------------------------------------------------------
/public/images/(anime)/attack.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arnvgh/www/8ce12004458edb22e5e2b5df984f42e5f93da12c/public/images/(anime)/attack.webp
--------------------------------------------------------------------------------
/public/images/(anime)/coe.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arnvgh/www/8ce12004458edb22e5e2b5df984f42e5f93da12c/public/images/(anime)/coe.webp
--------------------------------------------------------------------------------
/public/images/(anime)/darling.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arnvgh/www/8ce12004458edb22e5e2b5df984f42e5f93da12c/public/images/(anime)/darling.webp
--------------------------------------------------------------------------------
/public/images/(anime)/game.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arnvgh/www/8ce12004458edb22e5e2b5df984f42e5f93da12c/public/images/(anime)/game.webp
--------------------------------------------------------------------------------
/public/images/(anime)/jujutsu.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arnvgh/www/8ce12004458edb22e5e2b5df984f42e5f93da12c/public/images/(anime)/jujutsu.webp
--------------------------------------------------------------------------------
/public/images/(anime)/korosensei.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arnvgh/www/8ce12004458edb22e5e2b5df984f42e5f93da12c/public/images/(anime)/korosensei.webp
--------------------------------------------------------------------------------
/public/images/(anime)/lain.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arnvgh/www/8ce12004458edb22e5e2b5df984f42e5f93da12c/public/images/(anime)/lain.webp
--------------------------------------------------------------------------------
/public/images/(anime)/promised.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arnvgh/www/8ce12004458edb22e5e2b5df984f42e5f93da12c/public/images/(anime)/promised.webp
--------------------------------------------------------------------------------
/public/images/(anime)/rezero.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arnvgh/www/8ce12004458edb22e5e2b5df984f42e5f93da12c/public/images/(anime)/rezero.webp
--------------------------------------------------------------------------------
/public/images/(anime)/sao.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arnvgh/www/8ce12004458edb22e5e2b5df984f42e5f93da12c/public/images/(anime)/sao.webp
--------------------------------------------------------------------------------
/public/images/(home)/0001.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arnvgh/www/8ce12004458edb22e5e2b5df984f42e5f93da12c/public/images/(home)/0001.jpg
--------------------------------------------------------------------------------
/public/images/(home)/0002.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arnvgh/www/8ce12004458edb22e5e2b5df984f42e5f93da12c/public/images/(home)/0002.jpg
--------------------------------------------------------------------------------
/public/images/(home)/0003.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arnvgh/www/8ce12004458edb22e5e2b5df984f42e5f93da12c/public/images/(home)/0003.jpg
--------------------------------------------------------------------------------
/public/images/(home)/0004.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arnvgh/www/8ce12004458edb22e5e2b5df984f42e5f93da12c/public/images/(home)/0004.jpg
--------------------------------------------------------------------------------
/public/images/(home)/0005.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arnvgh/www/8ce12004458edb22e5e2b5df984f42e5f93da12c/public/images/(home)/0005.jpg
--------------------------------------------------------------------------------
/public/images/(home)/0006.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arnvgh/www/8ce12004458edb22e5e2b5df984f42e5f93da12c/public/images/(home)/0006.jpg
--------------------------------------------------------------------------------
/public/images/(home)/0007.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arnvgh/www/8ce12004458edb22e5e2b5df984f42e5f93da12c/public/images/(home)/0007.jpg
--------------------------------------------------------------------------------
/public/images/(home)/books.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arnvgh/www/8ce12004458edb22e5e2b5df984f42e5f93da12c/public/images/(home)/books.jpg
--------------------------------------------------------------------------------
/public/images/(home)/fallbackMusicCover.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arnvgh/www/8ce12004458edb22e5e2b5df984f42e5f93da12c/public/images/(home)/fallbackMusicCover.jpg
--------------------------------------------------------------------------------
/public/images/(misc)/kolkata.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arnvgh/www/8ce12004458edb22e5e2b5df984f42e5f93da12c/public/images/(misc)/kolkata.png
--------------------------------------------------------------------------------
/public/images/(nav)/anime.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arnvgh/www/8ce12004458edb22e5e2b5df984f42e5f93da12c/public/images/(nav)/anime.webp
--------------------------------------------------------------------------------
/public/images/(nav)/books.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arnvgh/www/8ce12004458edb22e5e2b5df984f42e5f93da12c/public/images/(nav)/books.webp
--------------------------------------------------------------------------------
/public/images/(nav)/faqs.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arnvgh/www/8ce12004458edb22e5e2b5df984f42e5f93da12c/public/images/(nav)/faqs.webp
--------------------------------------------------------------------------------
/public/images/(nav)/music.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arnvgh/www/8ce12004458edb22e5e2b5df984f42e5f93da12c/public/images/(nav)/music.webp
--------------------------------------------------------------------------------
/public/images/(nav)/uses.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arnvgh/www/8ce12004458edb22e5e2b5df984f42e5f93da12c/public/images/(nav)/uses.webp
--------------------------------------------------------------------------------
/public/images/(nav)/work.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arnvgh/www/8ce12004458edb22e5e2b5df984f42e5f93da12c/public/images/(nav)/work.webp
--------------------------------------------------------------------------------
/public/images/(uses)/bspwm.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arnvgh/www/8ce12004458edb22e5e2b5df984f42e5f93da12c/public/images/(uses)/bspwm.png
--------------------------------------------------------------------------------
/public/images/(uses)/bspwm.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arnvgh/www/8ce12004458edb22e5e2b5df984f42e5f93da12c/public/images/(uses)/bspwm.webp
--------------------------------------------------------------------------------
/public/images/(uses)/dwm.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arnvgh/www/8ce12004458edb22e5e2b5df984f42e5f93da12c/public/images/(uses)/dwm.png
--------------------------------------------------------------------------------
/public/images/gradient.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arnvgh/www/8ce12004458edb22e5e2b5df984f42e5f93da12c/public/images/gradient.webp
--------------------------------------------------------------------------------
/public/images/webp.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | # sudo pacman -Sy libwebp
4 |
5 | for file in *.jpg; do cwebp -q 80 "$file" -o "${file%.jpg}.webp"; done
6 |
--------------------------------------------------------------------------------
/public/meta/meta.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arnvgh/www/8ce12004458edb22e5e2b5df984f42e5f93da12c/public/meta/meta.png
--------------------------------------------------------------------------------
/public/meta/og.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arnvgh/www/8ce12004458edb22e5e2b5df984f42e5f93da12c/public/meta/og.png
--------------------------------------------------------------------------------
/public/public.asc:
--------------------------------------------------------------------------------
1 | -----BEGIN PGP PUBLIC KEY BLOCK-----
2 |
3 | xjMEZaO6rxYJKwYBBAHaRw8BAQdA5/1lNs5Ugt1O9Op4q3C2DOyKQ6Z6yQ/nAiCh
4 | KZyYqOPNNEFydW5hdmEgR2hvc2ggKFBlcnNvbmFsIEdQRyBLZXlzKSA8YXJudmdo
5 | QGdtYWlsLmNvbT7CkwQTFgoAOxYhBKd2DiHvv+733hIOJHccAohvAyjhBQJlo7qv
6 | AhsDBQsJCAcCAiICBhUKCQgLAgQWAgMBAh4HAheAAAoJEHccAohvAyjhA0YBALWX
7 | IkJanxumb52FcBq+TJEjDU4xiByDINzyyjegxc2gAP9CVuvZBcW0kZKKK73eD3H8
8 | IJCJ0FdX8qf3MtDR1tHUAs44BGWjuq8SCisGAQQBl1UBBQEBB0CpuE9LvctcvijP
9 | Q2eycvS3OZ2TJIT2BRAXfmqLlPoqWgMBCAfCeAQYFgoAIBYhBKd2DiHvv+733hIO
10 | JHccAohvAyjhBQJlo7qvAhsMAAoJEHccAohvAyjhQvsBAJPfTO+C2pcQrlxZyUUI
11 | zMEJl3I4w3Jpz1LpCRpfElGEAP9/3Y/s5TkZw9q4Vt6zm9Z47ZcGU09ywiYBVZ4c
12 | SZzkBA==
13 | =jnQk
14 | -----END PGP PUBLIC KEY BLOCK-----
15 |
--------------------------------------------------------------------------------
/public/vids/cars.mp4:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arnvgh/www/8ce12004458edb22e5e2b5df984f42e5f93da12c/public/vids/cars.mp4
--------------------------------------------------------------------------------
/tailwind.config.ts:
--------------------------------------------------------------------------------
1 | import typography from "@tailwindcss/typography";
2 | import type { Config } from "tailwindcss";
3 | import animate from "tailwindcss-animate";
4 |
5 | const config = {
6 | darkMode: ["class"],
7 | content: [
8 | "./pages/**/*.{ts,tsx}",
9 | "./components/**/*.{ts,tsx}",
10 | "./app/**/*.{ts,tsx}",
11 | "./src/**/*.{ts,tsx}",
12 | ],
13 | prefix: "",
14 | theme: {
15 | container: {
16 | center: true,
17 | padding: "2rem",
18 | screens: {
19 | "2xl": "1400px",
20 | },
21 | },
22 | extend: {
23 | colors: {
24 | border: "hsl(var(--border))",
25 | input: "hsl(var(--input))",
26 | ring: "hsl(var(--ring))",
27 | background: "hsl(var(--background))",
28 | foreground: "hsl(var(--foreground))",
29 | primary: {
30 | DEFAULT: "hsl(var(--primary))",
31 | foreground: "hsl(var(--primary-foreground))",
32 | },
33 | secondary: {
34 | DEFAULT: "hsl(var(--secondary))",
35 | foreground: "hsl(var(--secondary-foreground))",
36 | },
37 | destructive: {
38 | DEFAULT: "hsl(var(--destructive))",
39 | foreground: "hsl(var(--destructive-foreground))",
40 | },
41 | muted: {
42 | DEFAULT: "hsl(var(--muted))",
43 | foreground: "hsl(var(--muted-foreground))",
44 | },
45 | accent: {
46 | DEFAULT: "hsl(var(--accent))",
47 | foreground: "hsl(var(--accent-foreground))",
48 | },
49 | popover: {
50 | DEFAULT: "hsl(var(--popover))",
51 | foreground: "hsl(var(--popover-foreground))",
52 | },
53 | card: {
54 | DEFAULT: "hsl(var(--card))",
55 | foreground: "hsl(var(--card-foreground))",
56 | },
57 | },
58 | borderRadius: {
59 | lg: "var(--radius)",
60 | md: "calc(var(--radius) - 2px)",
61 | sm: "calc(var(--radius) - 4px)",
62 | },
63 | keyframes: {
64 | "accordion-down": {
65 | from: { height: "0" },
66 | to: { height: "var(--radix-accordion-content-height)" },
67 | },
68 | "accordion-up": {
69 | from: { height: "var(--radix-accordion-content-height)" },
70 | to: { height: "0" },
71 | },
72 | "marquee-left": {
73 | from: { transform: "translateX(0)" },
74 | to: { transform: "translateX(calc(-100% - var(--gap)))" },
75 | },
76 | "marquee-up": {
77 | from: { transform: "translateY(0)" },
78 | to: { transform: "translateY(calc(-100% - var(--gap)))" },
79 | },
80 | },
81 | animation: {
82 | "accordion-down": "accordion-down 0.2s ease-out",
83 | "accordion-up": "accordion-up 0.2s ease-out",
84 | "marquee-left": "marquee-left var(--duration, 30s) linear infinite",
85 | "marquee-up": "marquee-up var(--duration, 30s) linear infinite",
86 | },
87 | },
88 | },
89 | plugins: [animate, typography],
90 | experimental: {
91 | optimizeUniversalDefaults: true,
92 | },
93 | future: {
94 | hoverOnlyWhenSupported: true,
95 | },
96 | } satisfies Config;
97 |
98 | export default config;
99 |
--------------------------------------------------------------------------------
/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 | "plugins": [
17 | {
18 | "name": "next"
19 | }
20 | ],
21 | "paths": {
22 | "@/*": ["./*"]
23 | }
24 | },
25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
26 | "exclude": ["node_modules"]
27 | }
28 |
--------------------------------------------------------------------------------