Full Code of mitul-s/mitul.ca for AI

master 9d1be5d0baed cached
89 files
204.8 KB
57.4k tokens
123 symbols
1 requests
Download .txt
Showing preview only (226K chars total). Download the full file or copy to clipboard to get everything.
Repository: mitul-s/mitul.ca
Branch: master
Commit: 9d1be5d0baed
Files: 89
Total size: 204.8 KB

Directory structure:
gitextract_iezpmlwg/

├── .gitignore
├── LICENSE.txt
├── README.md
├── app/
│   ├── (with-layout)/
│   │   ├── about/
│   │   │   └── page.tsx
│   │   ├── layout.tsx
│   │   ├── page.client.tsx
│   │   └── page.tsx
│   ├── (without-root-layout)/
│   │   ├── layout.tsx
│   │   ├── p/
│   │   │   ├── 2024/
│   │   │   │   ├── page.mdx
│   │   │   │   └── smol-txt.tsx
│   │   │   ├── 2025/
│   │   │   │   └── page.mdx
│   │   │   ├── components/
│   │   │   │   ├── callout.tsx
│   │   │   │   ├── figure.tsx
│   │   │   │   ├── img-container.tsx
│   │   │   │   └── small-text.tsx
│   │   │   ├── layout.tsx
│   │   │   ├── q1-2025/
│   │   │   │   ├── page.mdx
│   │   │   │   └── two-imgs.tsx
│   │   │   ├── q2-2025/
│   │   │   │   └── page.mdx
│   │   │   ├── q3-2025/
│   │   │   │   └── page.mdx
│   │   │   └── substack-embed.module.css
│   │   └── visitors/
│   │       ├── actions.ts
│   │       ├── all/
│   │       │   ├── page.tsx
│   │       │   └── visitors-all.module.css
│   │       ├── gang/
│   │       │   └── page.tsx
│   │       ├── login/
│   │       │   └── page.tsx
│   │       ├── notes.module.css
│   │       └── page.tsx
│   ├── actions.ts
│   ├── api/
│   │   ├── login/
│   │   │   └── route.ts
│   │   ├── md/
│   │   │   └── route.ts
│   │   ├── send/
│   │   │   └── route.ts
│   │   └── spotify/
│   │       └── route.ts
│   ├── feed.xml/
│   │   └── route.ts
│   ├── globals.css
│   ├── layout.tsx
│   ├── llms.txt/
│   │   └── route.ts
│   ├── robots.txt
│   └── sitemap.ts
├── atoms/
│   └── guestbook.tsx
├── components/
│   ├── collapsible.tsx
│   ├── copy-email-button.tsx
│   ├── email-template.tsx
│   ├── footer/
│   │   ├── footer-date.tsx
│   │   └── index.tsx
│   ├── gallery.tsx
│   ├── json-ld.tsx
│   ├── link-primitive.tsx
│   ├── morphing-dialog.tsx
│   ├── music-player.tsx
│   ├── now-playing-client.tsx
│   ├── photo.tsx
│   ├── scroll-area.tsx
│   ├── section.tsx
│   ├── shader.tsx
│   ├── theme-switcher.tsx
│   ├── tree-client.tsx
│   ├── tree.tsx
│   ├── twitter-x-loop.tsx
│   ├── video-hover-preview.tsx
│   ├── video-pause-button.tsx
│   └── visitors/
│       ├── approve-btn.tsx
│       ├── cta.tsx
│       ├── drag.tsx
│       ├── field.tsx
│       ├── guestbook-entries.tsx
│       ├── note.tsx
│       ├── polaroid.tsx
│       ├── stickers.tsx
│       └── visitors.module.css
├── content.ts
├── hooks/
│   ├── useClickOutside.tsx
│   └── useMaxZIndex.tsx
├── lib/
│   ├── blog-posts.ts
│   ├── literal.ts
│   ├── openai.ts
│   ├── redis.ts
│   ├── spotify.ts
│   └── utils.ts
├── mdx-components.tsx
├── microfrontends.json
├── next-env.d.ts
├── next.config.mjs
├── package.json
├── postcss.config.js
├── proxy.ts
├── public/
│   └── font/
│       ├── ABCMonumentGrotesk-Medium-Trial.otf
│       └── ABCMonumentGrotesk-Regular-Trial.otf
└── tsconfig.json

================================================
FILE CONTENTS
================================================

================================================
FILE: .gitignore
================================================
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.js

# testing
/coverage

# next.js
/.next/
/out/

# production
/build

# misc
.DS_Store
*.pem

# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# local env files
.env
.env.local
.env.development.local
.env.test.local
.env.production.local

# vercel
.vercel
.env*.local


================================================
FILE: LICENSE.txt
================================================
MIT License

Copyright (c) 2024 Mitul Shah

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

================================================
FILE: README.md
================================================
<!-- This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). -->

[![Twitter URL](https://img.shields.io/twitter/url/https/twitter.com/typicalmitul.svg?style=social&label=Follow%20%40typicalmitul&ref_src=twsrc%5Etfw)](https://twitter.com/typicalmitul)

Hi! My name is Mitul 🏄‍♂️ Welcome to the code that controls my evergrowing space on the internet. 

Building out my personal website has brought me a lot of joy over the years, and it's transformed many, many times. I actually try to update it every few weeks. 

If you have any questions, suggestions – feel free to [tweet me](https://twitter.com/typicalmitul) 👋

##### Built with
* [Next.js](https://nextjs.org/)
* [Vercel](https://vercel.com/)
* [Tailwind CSS](https://tailwindcss.com/)
* [Radix Primitives](https://radix-ui.com/)

### Getting Started
_Note: I do have an .env.example file, but it could take awhile to get those keys. You could delete all the code related to those endpoints if it makes things easier._

```bash
$ git clone https://github.com/mitul-s/$ mitul.ca.git
$ cd mitul.ca
$ npm install
$ npm run dev
# or yarn if you prefer that
```

Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.

You can start editing the page by modifying `pages/index.js`. The page auto-updates as you edit the file.

---

#### Deploy on Vercel 
[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/git/external?repository-url=https%3A%2F%2Fgithub.com%2Fmitul-s%2Fmitul.ca)


================================================
FILE: app/(with-layout)/about/page.tsx
================================================
import LinkPrimitive from "@/components/link-primitive";
import { beliefs, bucketList, Status } from "@/content";
import { ArrowLeft } from "@phosphor-icons/react/dist/ssr/ArrowLeft";
import { cva } from "class-variance-authority";
import Link from "next/link";
import type { Metadata } from "next/types";
import Section from "@/components/section";

export const metadata: Metadata = {
  title: "About",
  alternates: {
    canonical: "https://mitul.ca/about",
  },
};

const bucketItem = cva(["self-start"], {
  variants: {
    status: {
      none: "",
      completed: ["line-through", "text-gray-11"],
      progress: [
        "before:content-['']",
        "before:w-1",
        "before:h-1",
        "before:bg-accent",
        "before:inline-flex",
        "before:-mt-px",
        "before:rounded-full",
        "before:animate-pulse",
        "before:mr-1",
        "flex",
        "items-center",
      ],
    },
  },
});

const BucketItem = ({
  item,
  status,
}: {
  item: string;
  status: keyof typeof Status;
}) => {
  return <li className={bucketItem({ status: Status[status] })}>{item}</li>;
};

const About = () => {
  return (
    <div className="justify-between md:flex animate-in fade-in duration-500">
      <div className="md:max-w-[450px] flex flex-col md:gap-y-0 gap-y-6 p-6">
        <Link
          href="/"
          className="flex gap-x-1 bg-accent text-gray-1 w-fit rounded-sm pl-0.5 pr-1 py-0.5 leading-none items-center hover:bg-accent/50 transition duration-100 mx-1 md:mx-4"
          aria-label="Back"
        >
          <ArrowLeft size={16} className="shrink-0" />
          <span className="text-sm font-medium">Home</span>
        </Link>
        <Section heading="I'm still figuring it out">
          <div className="space-y-4">
            <p>
              Hey, my name is Mitul and welcome to my space on the internet. I'm
              a self-taught{" "}
              <LinkPrimitive
                href="https://bradfrost.com/blog/post/frontend-design/"
                external
              >
                design engineer
              </LinkPrimitive>{" "}
              based in Toronto, Canada.
            </p>
            <p>
              Learning to code has felt like a superpower for me, it allows me
              to bring any idea I can imagine to life. I love creating and
              focusing on the little things that enhance our experiences as we
              dive into the abyss of the web.
            </p>
            <p>
              Apart from all of that, a strong sense of curiosity about the
              world has always driven me. Travel, and specifically the diverse
              experiences gained from exploring different places, cultures, and
              landscapes, have significantly influenced my personal growth.
            </p>
            <p>
              My life thrives on both chaos and serendipity. I'm just tryna
              channel the same spirit of adventure as Ferris Bueller.
            </p>
          </div>
        </Section>

        <Section heading="Beliefs">
          <ul className="flex flex-col gap-y-1">
            {beliefs.map((belief) => {
              return <li key={belief}>{belief}</li>;
            })}
          </ul>
        </Section>
        <Section heading="Bucket List">
          <ul className="flex flex-col gap-y-1">
            {bucketList.map((item) => {
              return (
                <BucketItem
                  key={item.item}
                  item={item.item}
                  status={item.status}
                />
              );
            })}
          </ul>
        </Section>
      </div>
    </div>
  );
};

export default About;


================================================
FILE: app/(with-layout)/layout.tsx
================================================
import type { Viewport } from "next";

export const viewport: Viewport = {
  themeColor: "#0F0F0F",
};

const Layout = ({ children }: { children: React.ReactNode }) => {
  return (
    <div className="h-full relative">
      <div className="main-noise" aria-hidden />
      {children}
    </div>
  );
};
export default Layout;


================================================
FILE: app/(with-layout)/page.client.tsx
================================================
"use client";

import { Globe, Terminal } from "@phosphor-icons/react";
import { track } from "@vercel/analytics";
import Link from "next/link";

interface ProjectProps {
  title: string;
  description: string;
  hrefs: {
    live?: string;
    code?: string;
  };
}

const Project = ({
  title,
  description,
  hrefs: { live, code },
}: ProjectProps) => {
  return (
    <div className="px-4 pt-4 pb-5 flex flex-col gap-y-1">
      <h3 className="font-medium">{title}</h3>
      <p>{description}</p>
      <div className="flex items-center mt-2 gap-x-2">
        {live ? (
          <Link
            className="flex gap-x-1.5 items-center bg-accent hover:bg-accent/80 transition text-gray-1 py-0.5 pl-1 pr-1.5 rounded-[2px] cursor-pointer text-sm"
            href={live}
            target="_blank"
            rel="noopener noreferrer"
            onClick={() => track("project_live_link_clicked", { title })}
          >
            <Globe
              aria-hidden={true}
              size={12}
              className="shrink-0 text-gray-1"
            />
            Live{" "}
          </Link>
        ) : null}
        {code ? (
          <Link
            className="flex gap-x-1.5 items-center bg-accent hover:bg-accent/80 transition text-gray-1 py-0.5 pl-1 pr-1.5 rounded-[2px] cursor-pointer text-sm"
            href={code}
            target="_blank"
            rel="noopener noreferrer"
            onClick={() => track("project_code_link_clicked", { title })}
          >
            <Terminal
              aria-hidden={true}
              size={12}
              className="shrink-0 text-gray-1"
            />
            Code{" "}
          </Link>
        ) : null}
      </div>
    </div>
  );
};

export { Project };


================================================
FILE: app/(with-layout)/page.tsx
================================================
import { Accordion, AccordionItem } from "@/components/collapsible";
import VideoHoverPreview from "@/components/video-hover-preview";
import { experiences, photos } from "@/content";
import { ArrowRight } from "@phosphor-icons/react/dist/ssr/ArrowRight";
import Link from "next/link";
import Gallery from "@/components/gallery";
import { PencilSimpleLine } from "@phosphor-icons/react/dist/ssr/PencilSimpleLine";
import TwitterXMotion from "@/components/twitter-x-loop";
import {
  CopyEmailButton,
  CopyEmailButtonAlt,
} from "@/components/copy-email-button";
import Footer from "@/components/footer";
import { cn } from "@/lib/utils";
import { ScribbleLoop } from "@phosphor-icons/react/dist/ssr/ScribbleLoop";
import MusicPlayer from "@/components/music-player";
import Shader from "@/components/shader";
import { Project } from "./page.client";
import VideoPauseButton from "@/components/video-pause-button";

const DottedSpacer = ({
  lines = 3,
  className,
}: {
  lines?: number;
  className?: string;
}) => {
  return (
    <div className={cn("flex gap-y-0.5 my-0.5 flex-col", className)}>
      {Array.from({ length: lines }).map((_, index) => (
        <span
          // biome-ignore lint/suspicious/noArrayIndexKey: this is a static list
          key={index}
          className="h-px border-b border-dotted border-accent"
          aria-hidden
        />
      ))}
    </div>
  );
};

const SectionTitle = ({ children }: { children: React.ReactNode }) => {
  return (
    <h2 className="md:py-4 pt-4 pl-4 md:pr-4 font-medium h-full md:ml-auto">
      {children}
    </h2>
  );
};

const Section = ({
  title,
  children,
}: {
  title?: string;
  children: React.ReactNode;
}) => {
  return (
    <section className="md:grid grid-cols-[160px_500px_auto] md:divide-x w-full border-b border-accent divide-accent text-accent">
      {title ? <SectionTitle>{title}</SectionTitle> : null}
      <SectionContent>{children}</SectionContent>
    </section>
  );
};

const SocialLink = ({
  href,
  social,
  children,
}: {
  href: string;
  social: string;
  children: React.ReactNode;
}) => {
  return (
    <div className="grid grid-cols-[75px_auto_auto] gap-x-1 items-center px-4 py-2">
      <p className="font-medium">{social}</p>
      <Link
        href={href}
        target="_blank"
        rel="noopener noreferrer"
        className="hover:underline underline-offset-2"
      >
        {children}
      </Link>
    </div>
  );
};

const SectionContent = ({ children }: { children: React.ReactNode }) => {
  return <div className="md:border-r">{children}</div>;
};

export default function Home() {
  return (
    <>
      <Shader />
      <div className="justify-between md:flex animate-in fade-in duration-500 select flex-col">
        <nav className="min-md:absolute top-4 right-4 flex max-md:p-4 gap-1 text-accent font-medium">
          <Link
            href="/os"
            target="_blank"
            rel="noopener noreferrer"
            className="flex gap-x-1.5 items-center bg-accent hover:bg-accent/80 transition text-gray-1 py-0.5 pl-1.5 pr-1.5 rounded-[2px] cursor-pointer"
          >
            Public Archive
          </Link>
          <Link
            href="/visitors"
            className="flex gap-x-1.5 items-center bg-accent hover:bg-accent/80 transition text-gray-1 py-0.5 pl-1.5 pr-1.5 rounded-[2px] cursor-pointer"
          >
            Guestbook
          </Link>
          <CopyEmailButtonAlt />
          <VideoPauseButton />
          {/* <ThemeChanger /> */}
        </nav>

        <Section title=" ">
          <div className="px-4 min-md:pt-8 pb-6 col-start-2">
            <h1 className="font-medium flex items-center gap-x-1.5 text-[24px]">
              Mitul Shah
            </h1>
            <span>Photographer, design engineer, and a bit more.</span>
            <p className="mt-4">
              Crafting memorable interfaces with a deep attention to detail. I
              dedicate most my time to continuous learning and refining my
              skillset.
            </p>
            <p className="mt-2">
              I'm a creative{" "}
              <VideoHoverPreview href="https://www.youtube.com/watch?v=jG7dSXcfVqE">
                doing what I can't
              </VideoHoverPreview>
            </p>
            <span className="font-medium text-sm tracking-tight mt-4 -mb-2 block">
              Currently
            </span>
            <MusicPlayer />
            <div className="flex gap-x-2">
              <Link
                href="/visitors"
                className="rounded-4 bg-accent transition hover:bg-accent/90 text-light-green font-medium px-2 py-1 flex gap-x-1.5 items-center"
                style={{
                  boxShadow:
                    "0 4px 4px #08080814, 0 1px 2px #08080833, inset 0 6px 12px #ffffff1f, inset 0 1px 1px #fff3",
                }}
              >
                Sign guestbook
                <ScribbleLoop
                  size={12}
                  weight="bold"
                  aria-hidden={true}
                  className="shrink-0 text-light-green"
                />
              </Link>
            </div>
          </div>
        </Section>
        <Section title="Experience">
          <DottedSpacer className="mt-4 md:mt-0.5 mb-0" />
          <Accordion className="h-full flex flex-col divide-y divide-dotted">
            {experiences.map((role) => {
              return (
                <AccordionItem
                  key={role.company}
                  role={role.role}
                  company={role.company}
                  range={role.range}
                  description={role.description}
                  // skills={role.skills}
                  link={role.link}
                />
              );
            })}
            <DottedSpacer lines={2} />
          </Accordion>
        </Section>
        <Section title="Projects">
          <Project
            title="Daybloom"
            description="A mindful daily journal that combines your photos and thoughts into calendar view, helping you capture memories and reflect on life's meaningful moments."
            hrefs={{
              live: "https://daybloom.app",
            }}
          />
          <DottedSpacer className="my-0" />
          <Project
            title="Montreal in Motion"
            description="A documentation of the brutalist and distinctly designed metro stations. The project uses CSS 3D transforms and noise to mirror the architecutral character of the spaces."
            hrefs={{
              live: "https://typicalmitul.com/montreal-in-motion",
              code: "https://github.com/mitul-s/typicalmitul.com",
            }}
          />
          <DottedSpacer className="my-0" />
          <Project
            title="Places to Read"
            description="A microsite to discover community submitted parks around the world where you can sit down, chill and enjoy reading a book."
            hrefs={{ live: "https://placestoread.xyz" }}
          />
        </Section>
        <Section title="Photography">
          <div className="flex flex-col gap-y-1.5 pt-4 pb-8">
            <div className="flex flex-col gap-y-1.5 px-4">
              <p>
                I've built up my craft as a photographer over a number of years
                and thrived in turning it into an indepedent business.
              </p>
              <span>
                <span className="font-medium">
                  Notable achievements include
                </span>
                <ul>
                  <li className="relative flex items-center before:w-1 before:h-1 before:bg-accent before:rounded-full before:leading-none gap-x-2 ">
                    being a personal photographer for the Uber CEO
                  </li>
                  <li className="relative flex items-center before:w-1 before:h-1 before:bg-accent before:rounded-full before:leading-none gap-x-2 ">
                    featured in local Toronto newspapers
                  </li>
                  <li className="relative flex items-center before:w-1 before:h-1 before:bg-accent before:rounded-full before:leading-none gap-x-2 ">
                    having a photo as a wallpaper in every Google device
                  </li>
                </ul>
              </span>
              <p>
                Today, my focus is on music photography where I capture my
                favourite artists at concerts or festivals. You can learn a
                little more by visiting my portfolio below.
              </p>
              <Link
                href="https://typicalmitul.com"
                target="_blank"
                className="flex w-fit gap-x-2 items-center transition hover:bg-accent/90 rounded-4 bg-accent text-light-green font-medium px-2 py-1 mt-2 mb-4"
                style={{
                  boxShadow:
                    "0 4px 4px #08080814, 0 1px 2px #08080833, inset 0 6px 12px #ffffff1f, inset 0 1px 1px #fff3",
                }}
              >
                Visit my portfolio
                <ArrowRight size={12} aria-hidden={true} />
              </Link>
            </div>
            <div className="scroll-pl-4 scroll-pr-4">
              <Gallery photos={photos} />
            </div>
          </div>
        </Section>
        <Section title="Contact">
          <p className="pb-4 px-4 pt-4">
            Let's hang, up for a chat or just say hi. If you're into
            photography, film or music, I'd love to hear from you.
          </p>
          <DottedSpacer lines={2} />
          <div className="grid gap-x-4 divide-y border-y divide-dotted border-dotted">
            <div className="grid grid-cols-[75px_auto_1fr] gap-x-1.5 items-center px-4 py-2">
              <p className="font-medium">Mail</p>
              <Link href="mailto:mitulxshah@gmail.com">
                mitulxshah@gmail.com
              </Link>
              <div className="flex gap-x-1 ml-auto">
                <Link
                  href="mailto:mitulxshah@gmail.com"
                  className="hidden min-md:flex gap-x-1.5 items-center bg-accent hover:bg-accent/80 transition text-gray-1 py-0.5 pl-1 pr-1.5 rounded-[2px] cursor-pointer text-sm w-fit"
                >
                  <PencilSimpleLine size={12} aria-hidden={true} />
                  Compose
                </Link>
                <CopyEmailButton />
              </div>
            </div>
            <TwitterXMotion className="grid grid-cols-[75px_auto_1fr] gap-x-1.5 items-center px-4 py-2 overflow-hidden" />
            <SocialLink
              social="Instagram"
              href="https://instagram.com/typicalmitul"
            >
              @typicalmitul
            </SocialLink>
            <SocialLink social="GitHub" href="https://github.com/mitul-s">
              mitul-s
            </SocialLink>
          </div>
          <DottedSpacer lines={2} />
        </Section>

        <Footer />
      </div>
    </>
  );
}


================================================
FILE: app/(without-root-layout)/layout.tsx
================================================
const Layout = ({ children }: { children: React.ReactNode }) => {
  return <>{children}</>;
};

export default Layout;


================================================
FILE: app/(without-root-layout)/p/2024/page.mdx
================================================
import Smol from "./smol-txt";
import Figure from "./../components/figure";
import { BlogPostJsonLd } from "@/components/json-ld";

import tropez from "./tropez.jpeg";
import timechamber from "./timechamber.jpeg";

export const metadata = {
  title: "[Annual Review] 2024",
  description: "Everything I want.",
  authors: [{ name: "Mitul Shah", url: "https://mitul.ca" }],
  alternates: {
    canonical: "/p/2024",
  },
  openGraph: {
    type: "article",
    title: "[Annual Review] 2024 / Mitul Shah",
    description: "Everything I want.",
    publishedTime: "2024-12-31",
    authors: ["Mitul Shah"],
  },
  twitter: {
    card: "summary_large_image",
    title: "[Annual Review] 2024 / Mitul Shah",
    description: "Everything I want.",
  },
};

<BlogPostJsonLd slug="2024" />

# [Annual Review] 2024

<Figure
  src={tropez}
  alt="Saint-Tropez, Maximilien Luce – 1893"
  caption="Saint-Tropez, Maximilien Luce – 1893"
/>

I know I say it all the time, but every year is the best year of my life. I will always do my best to achieve everything I set out to.

The past few years were dedicated to exploration and understanding — 2024 allowed me to reap the rewards of that journey. I can't pinpoint exactly what shifted, maybe it doesn't matter, but for the first time, everything feels right.

I feel in control.

This clarity has reflected positively in all my relationships; with friends, love, and family. Building an understanding of what you want and what you will tolerate makes things easier for both you and everyone around you.

## Goals

Comparing my [2023](https://futureland.tv/@mitul/%E2%9C%A6-2023-%E2%9C%A6) journal to [2024](https://futureland.tv/@mitul/%E2%88%97-2024-%E2%88%97), it's obvious there's nearly no structure lol. This was intentional as I only had two primary goals for the year. For the smaller ones, things didn't always go as planned, but I'm okay with that.

### Code

My creative aspirations this year kind of ended up on a back burner. While I initially set out to complete six projects; it was not a priority as I was traveling. I managed to bring three and a half projects to life which I'm super proud of.

- [Montreal in Motion](https://typicalmitul.com/montreal-in-motion)
- [Guestbook](/visitors)
- A website for a friend and a hackathon project

I have a massive project that's close to completion, but it's been ongoing for so long that I just can't get myself back into it. I'll keep trying.

An unexpected highlight was that [Places to Read](https://placestoread.xyz) went viral through an Instagram reel. The response was overwhelming – over 300 submissions from strangers across the globe, each sharing their own favourite reading spaces. It was a good reminder of why I believe being able to code is a superpower.

### Photography

My concert photography declined significantly this year, mainly because I felt burnt out. I shot fewer than 10 shows this year—I haven't kept exact count. While it's personally fulfilling, it offers few tangible benefits right now.

In contrast, I photographed +40 acts last year including festivals and didn't make a dime (not that $ is the priority). It's tough.

I'm at a crossroads: though I'm confident in my skills, I'm unsure how to grow this work into something bigger.

### Cooking and relationships

I love cooking for people. I hosted a potluck, partly to see if I could make food people enjoy.

More importantly, there was something deeply satisfying about creating a space where different friend groups could come together over a shared meal, each person bringing their own piece of themselves to the table.

## Highlights

### New Demos 1

The year started off with [New Demos](https://www.newdemos.ca/) 1, where I got the opportunity to present my project [Montreal in Motion](https://typicalmitul.com/montreal-in-motion).

The night after, I landed a contract gig that funded my travels.

ND1 kicked off a domino effect in Toronto that brought 1,000s of people together for the sake of progress and growth. It also sparked countless new meaningful friendships which shaped my summer back home after my travels.

I'm forever grateful to [internetVin](https://x.com/internetvin) and [Tommy Trinh](https://x.com/tommytrxnh) for what they've done for the city of Toronto.

### The Last Rodeo: Four months of travel

Travel has been a core part of my life for the past two years and I wanted to do one last, long backpacking trip, specifically one where I wasn't tied to a job or major responsibilities that affected how I travelled. I don't know if I'll ever do this again, but I'm so glad I did it one last time.

- London
- Turkey (Istanbul, Antalya, Cappadocia, back to Istanbul)
- [Bosnia and Herzegovina](https://x.com/typicalmitul/status/1777433751737258475) (Sarjevo, Mostar)
- Crotia (Split, Zagreb)
- Slovenia (Ljubljana, Lake Bled)
- Budapest
- Prague
- 6 weeks in Spain (Barcelona, Valencia, Alicante, Malaga, Granada, Madrid, Barcelona again and back to Madrid, Seville, Cadiz, Tarifa)
- Morocco via boat from Spain (Tangier, Chefchaouen, Fes, Casablanca, Essaouira, nine nights in Taghazout)
- Porto, Portugal to end my travels

Part of me still misses travelling, though I do recognize it as a form of escapism. While I'll always deeply cherish the lifelong friends and memories I've made, I'm glad to be back home. I mean, I can't lie, it's pretty cool I can randomly bump into travel friends in so many places of the world.

I missed out on a lot in Toronto, and it was a trade-off I was willing to make. I knew this trip was something I had to do.

I could write a million words on my adventures, but that would be cliche. The overall goal was simple: travel until I land a full time job.

### Vercel

While travelling, I landed a role at [Vercel](https://x.com/typicalmitul/status/1812914498619253065) as a Design Engineer. This was a dream company and I'm grateful to be here. It's an absolute honour to be working alongside the people I've always looked up to.

Next.js changed my life, and that's probably an understatement.

## 2025

A goal I've been trying to achieve for nearly two years is almost in fruition. That's my main priority for the next little bit.

Every year is quite significantly different from the last, but 2025 is when I introduce stability into my life and I am excited for that.

<Figure
  src={timechamber}
  alt="Hyperbolic Time Chamber from Dragonball Z"
  caption="One year inside the chamber equates to just one day in the outside world. The air grows denser, and the temperature fluctuates more intensely the deeper one goes into the training area."
/>

---

<Smol />


================================================
FILE: app/(without-root-layout)/p/2024/smol-txt.tsx
================================================
import Link from "next/link";
import React from "react";

const Smol = () => {
  return (
    <p className="text-gray-11 text-[12px]">
      Previous years [
      <Link
        className="text-blue-9 hover:text-blue-10 hover:underline"
        href="https://futureland.tv/@mitul/entry/385302"
        target="_blank"
        rel="noreferrer noopener"
      >
        2023
      </Link>
      ]
    </p>
  );
};

export default Smol;


================================================
FILE: app/(without-root-layout)/p/2025/page.mdx
================================================
import Smol from "./smol-txt";
import Figure from "./../components/figure";
import ImageContainer from "../components/img-container";
import Callout from "../components/callout";
import monet from "./opengraph-image.jpg";
import { BlogPostJsonLd } from "@/components/json-ld";

export const metadata = {
  title: "[Annual Review] 2025",
  description: "A year in review: moving, adjusting, and finding breathing room",
  authors: [{ name: "Mitul Shah", url: "https://mitul.ca" }],
  alternates: {
    canonical: "/p/2025",
  },
  openGraph: {
    type: "article",
    title: "[Annual Review] 2025 / Mitul Shah",
    description: "A year in review: moving, adjusting, and finding breathing room",
    publishedTime: "2025-12-31",
    authors: ["Mitul Shah"],
  },
  twitter: {
    card: "summary_large_image",
    title: "[Annual Review] 2025 / Mitul Shah",
    description: "A year in review: moving, adjusting, and finding breathing room",
  },
};

<BlogPostJsonLd slug="2025" />

# [Annual Review] 2025

<Figure
  src={monet}
  alt="The Seine at Lavacourt, Claude Monet – 1880"
  caption="The Seine at Lavacourt, Claude Monet – 1880"
/>

<Callout>
You can also read this on Substack
</Callout>

I started the year in the middle of Buenos Aires watching fireworks with strangers and wondered why everyone was wearing white. I missed my flight home and reconsidered all my life decisions, but thankfully I had friends in the city that I could spend time with.

I was living with the belief I would soon move to New York City once I was home, but little did I know, my drives to Detroit and Buffalo would end with rejections.

I switched to a sublet on King St West and made a point to spend time with my friends while I waited for my visa approval, knowing they soon wouldn’t be a 10 minute walk from me. I spent most of my days at [New Stadium](https://x.com/newsystems_).

By March, my visa was approved and I got ready to move away from home for the first time.

June, I was in New York City. September, I signed a lease.

I’ll be spending my new year in Guatemala and El Salvador, likely hiking, surfing, being on the beach and getting the time away that I sometimes find myself missing. Most often when my travel friends reach out reminding me of our memories together.

<Figure src="https://inqeleafibjx2dzc.public.blob.vercel-storage.com/main/p2025/dbrand%20Twitter%20Post.png" 
alt="Messages from travel friends on Instagram reminding me of our travels" />

I met [Shen](https://shen.land) in person this year and she's a great reminder to be more myself.

<ImageContainer>
![.png](https://inqeleafibjx2dzc.public.blob.vercel-storage.com/main/p2025/IMG_3948.PNG)
</ImageContainer>

I was the first guest on [Rudy’s podcast](https://open.spotify.com/episode/3ZFlTNlsLa8e2nYINUG0UF?si=73c3fe14627e4450).

<Figure src="https://inqeleafibjx2dzc.public.blob.vercel-storage.com/main/p2025/Annual%20Review%202025%20Image%20%282%29.jpg" 
alt="Comments from people saying they enjoyed me on Rudy's podcast" />

I hosted weekly dinners. I made new friends. I lost new friends. I started working from an office again. I flew home to see the Blue Jays win (and my friends and family). The Blue Jays lost.

<ImageContainer>
![.png](https://inqeleafibjx2dzc.public.blob.vercel-storage.com/main/p2025/IMG_7821.jpg)
</ImageContainer>

There’s a certain beauty in knowing I achieved the things I planned for this year. Write more – I mean, here you are. Take photos. Move to New York City. Get my first apartment. Travel less. I think I did it all.

<ImageContainer>
![.png](https://inqeleafibjx2dzc.public.blob.vercel-storage.com/main/p2025/IMG_7112.jpg)
</ImageContainer>

Though there is something disorienting about getting what you wanted. An old friend always used to ask me what’s next.

After years of thinking about it, what’s next is not the point.

--

I went to Naples and watched them win the championship, my first time in Italy. With a need for stability in my life, I only travelled twice this year. Quite significantly less than the years prior. We’re probably going to pick that back up by a bit.

I said goodbye to my friends. I still can’t believe Daniil cooked for everyone.

<Figure src="https://inqeleafibjx2dzc.public.blob.vercel-storage.com/main/p2025/DSC02050.JPG" 
alt="Group photo with most my friends, ironically missing Daniil"
/>

Life is a never ending set of side quests. I don’t really like letting the mundane set in.

Focus on building your Dad lore.

<ImageContainer>
![.png](https://inqeleafibjx2dzc.public.blob.vercel-storage.com/main/p2025/Annual%20Review%202025%20Image%20%281%29.jpg)
</ImageContainer>


There’s a lot this year I did but it primarily revolved around moving to a new city for the first time. I haven’t really figured things out here yet, but I finally feel like I have breathing room to get back to things I’d like to do. The side quests.

I found my first apartment. Though it took awhile, it was nice to jump between Bushwick, Williamsburg and eventually settle into LES.

I hosted a little Friendsgiving party; it wasn’t anything like the one I hosted back home in Toronto but it’s a learning process.

<ImageContainer>
![.png](https://inqeleafibjx2dzc.public.blob.vercel-storage.com/main/p2025/IMG_1834.jpeg)
</ImageContainer>

Being in a new city hasn’t been the easiest; I miss the sense of familiarity more than anything. I am happier here, or happier overall. The reality, the texture of moving to a new big city and acknowledging what the changes actually feel like rather than what they’re supposed to feel like is a bit hard, but it’s part of the human experience.

Can I make my life feel like an episode of Friends/Seinfeld/HIMYM? I’d bet so. It just takes time. It already feels like a loop of Ferris Bueller’s Day Off.

<ImageContainer>
![.png](https://inqeleafibjx2dzc.public.blob.vercel-storage.com/main/p2025/Annual%20Review%202025%20Image%20%283%29.jpg)
</ImageContainer>

--

I don't know who I am if I am not in a state of discomfort.

It’s more I feel like I’ve reached a position where I’m deeply familiar with the things I do love and enjoy, and have done a great job of instilling them in my life but yet lost touch with them as I played with new, unfamiliar things for the sake of curiosity. That’s probably the best way I can phrase it.


<Figure src="https://inqeleafibjx2dzc.public.blob.vercel-storage.com/main/p2025/Annual%20Review%202025%20Image%20%284%29.jpg" alt="Pokemon Ruby Generation screen with a motivational quote" />

So for the upcoming year, I want to be extremely disciplined. Here’s some examples of what that could include:

Write more. Let’s double or triple the amount of write ups I share publicly.

No more solo travelling. Either a) travel with friends, or b) meet travel friends in their respective countries. Solo travel did wonders for me, it helped me shape who I am. Travelling with friends is hard, there’s different levels of comfort, schedules, risk tolerance and such but the times where it all aligns, makes it all worth it.

Take even more photos but print them. I told my friend the other day, I don’t want to forget what I looked like today. I want to capture myself, my friends and everything in meaningful ways - something more than a 0.5*. So when the time comes and I share my photos (with friends, my kids, my family), there’s a certain level of love thought attached to it.

I got a 3D printer. I want to design something of my own.

I’m going to pick up film photography again. I have been anti-film as every camera I’ve owned has broke, but I’ll remember to be more careful this year.

---

Never did I enjoy the beach much, but now I seem to be pulled towards it. I think a lot of the influence certain people have on my life, I changed because of them. I want to spend more time in the ocean, more time surfing, maybe learn to swim (better).

Anyways. Everyday is an adventure, and it will continue to be.

If you haven’t noticed I’m bad at telling the “why”, I kind of don’t want to unless someone asks me explicitly. If you think we’d get along, or I can help you, or you can help me. Email me here [mitulxshah@gmail.com](mailto:mitulxshah@gmail.com).

<ImageContainer>
![.png](https://inqeleafibjx2dzc.public.blob.vercel-storage.com/main/p2025/IMG_1145.jpg)
</ImageContainer>

**2026**

Though internalized, a reminder, whatever it takes.

<ImageContainer>
![.png](https://inqeleafibjx2dzc.public.blob.vercel-storage.com/main/p2025/Annual%20Review%202025%20Image%20%285%29.jpg)
</ImageContainer>

*****

See previous years [[2024](/p/2024), [2023](https://futureland.tv/@mitul/entry/385302)]

See the quarters for 2025 [[Q1](/p/q1-2025), [Q2](/p/q2-2025), [Q3](/p/q3-2025)]



================================================
FILE: app/(without-root-layout)/p/components/callout.tsx
================================================
"use client";

import { ArrowUpRight } from "@phosphor-icons/react";
import Link from "next/link";

const Callout = ({ children }: { children: React.ReactNode }) => {
    return (
    <Link href="https://fieldnotesbymitul.substack.com/p/annual-review-2025" target="_blank" rel="noopener noreferrer" className="bg-accent text-gray-1 p-4 rounded-md flex items-center justify-between hover:bg-accent/80 transition">
      <p className="text-sm">{children}</p>
      <ArrowUpRight size={14} className="shrink-0" />
    </Link>
  );
};

export default Callout;

================================================
FILE: app/(without-root-layout)/p/components/figure.tsx
================================================
import Image, { type StaticImageData } from "next/image";

interface FigureProps {
  src: string | StaticImageData;
  alt: string;
  caption?: string;
  width?: number;
  height?: number;
}

const Figure = (props: FigureProps) => {
  const { src, alt, caption, width, height } = props;
  const isExternalUrl = typeof src === "string";

  // For external URLs without dimensions, use unoptimized mode
  const imageProps = isExternalUrl && !width
    ? { unoptimized: true, width: 1400, height: 900 }
    : width && height
      ? { width, height }
      : {};

  if (caption !== undefined) {
    return (
      <figure>
        <Image
          className="rounded-4 border border-gray-6"
          src={src}
          alt={alt}
          fetchPriority="high"
          quality={30}
          placeholder={isExternalUrl ? "empty" : "blur"}
          sizes="(max-width: 768px) 100vw, 700px"
          {...imageProps}
        />
        <figcaption className="text-sm text-gray-11 mt-1.5">
          {caption}
        </figcaption>
      </figure>
    );
  }
  return (
    <Image
      className="rounded-4 border border-gray-6"
      src={src}
      alt={alt}
      {...imageProps}
    />
  );
};

export default Figure;


================================================
FILE: app/(without-root-layout)/p/components/img-container.tsx
================================================
const ImgContainer = ({ children }: { children: React.ReactNode }) => {
  return (
    <div className="[&_img]:!object-contain [&_img]:!w-[300px]">
      {children}
    </div>
  );
};

export default ImgContainer;


================================================
FILE: app/(without-root-layout)/p/components/small-text.tsx
================================================
const SmallText = ({ children }: { children: React.ReactNode }) => {
  return (
    <div className="text-gray-11 text-[12px] flex flex-col">{children}</div>
  );
};

export default SmallText;


================================================
FILE: app/(without-root-layout)/p/layout.tsx
================================================
import type { Viewport } from "next";
import Link from "next/link";
import styles from "./substack-embed.module.css";

export const viewport: Viewport = {
  themeColor: "#F9F2E2",
};

const Layout = ({ children }: { children: React.ReactNode }) => {
  return (
    <div className="min-h-screen flex flex-col justify-between pt-0 md:pt-8 p-8 bg-[#F9F2E2] text-gray-12 text-[16px]">
      <nav className="max-w-[60ch] mx-auto w-full mt-6 flex gap-x-2.5 justify-between">
        <div className="flex gap-x-2.5 items-center">
          <Link
            href="/"
            className="underline underline-offset-4 decoration-from-font"
          >
            Home
          </Link>
          <Link
            href="/visitors"
            className="underline underline-offset-4 decoration-from-font"
          >
            Guestbook
          </Link>
        </div>
        <Link
          href="https://x.com/typicalmitul"
          target="_blank"
          rel="noopener noreferrer"
          className="underline underline-offset-4 decoration-from-font"
        >
          @typicalmitul
        </Link>
      </nav>
      <main className="max-w-[60ch] mx-auto w-full space-y-6 pb-12">
        {children}
      </main>
      <div className={styles.wrapper}>
        <iframe
          src="https://fieldnotesbymitul.substack.com/embed"
          className={styles.iframe}
          frameBorder="0"
          scrolling="no"
        />
        <div className={styles.overlay} />
        <div className={styles.logoCover} />
      </div>
      <footer className="max-w-[60ch] mx-auto w-full text-left">
        <p className="text-[10px] text-gray-10 font-medium">
          from <span className="line-through">toronto</span> nyc, with love /
          typicalmitul
        </p>
      </footer>
    </div>
  );
};

export default Layout;


================================================
FILE: app/(without-root-layout)/p/q1-2025/page.mdx
================================================
import Images from "./two-imgs";
import Figure from "../components/figure";
import SmallText from "../components/small-text";
import steps from "./steps.jpg";
import yt from "./yt.png";
import { BlogPostJsonLd } from "@/components/json-ld";

export const metadata = {
  title: "[Quarter Review] Q1/25 / Mitul Shah",
  description: "Thriving on chaos",
  authors: [{ name: "Mitul Shah", url: "https://mitul.ca" }],
  alternates: {
    canonical: "/p/q1-2025",
  },
  openGraph: {
    type: "article",
    title: "[Quarter Review] Q1/25 / Mitul Shah",
    description: "Thriving on chaos",
    publishedTime: "2025-03-31",
    authors: ["Mitul Shah"],
  },
  twitter: {
    card: "summary_large_image",
    title: "[Quarter Review] Q1/25 / Mitul Shah",
    description: "Thriving on chaos",
  },
};

<BlogPostJsonLd slug="q1-2025" />

# Q1/25

<Figure
  src={steps}
  alt="Les Marches du Palais Versailles, Henri Le Sidaner – 1925"
  caption="Les Marches du Palais Versailles, Henri Le Sidaner – 1925"
/>

Well, it’s been an extremely eventful three months. With intentions of building stability [see [annual review](https://mitul.ca/p/2024)], it turned out to be everything but that. Things are good though, I’m thriving in a different way. I’ve been productive but not in the traditional sense where I’m working on my projects all day. Cliche, but I think it would be better said that I’m working on myself.

One of my primary goals for the year is to document more of my life. It has proved to be a better way to understand myself in a sense of where I’m good, or where I can make improvements, but also having a collection of memories I can reflect on. While spending a month in Argentina, I recorded a daily journal which was a [super rewarding experience](https://www.youtube.com/watch?v=J05WVHp5cJw&ab_channel=mitul).

<Figure src={yt} />

My time in Argentina was more special than many of my other travels. I keep thinking of a word to describe it, and I always land on "complete."

Everything felt right. ¹

I felt like I had everything I wanted, with a strong, fulfilling routine in my days that offered balance and stability. The trip was a reset moment for me.

Before I left for the trip, I let a stream of consciousness flow regarding my goals for the year, and without sounding dramatic, one of them was to expect radical honesty from everyone around me. I think what made Argentina so meaningful is that I actually found it — and in turn, started offering it too ².

I don't expect to be travelling much this year, this is the first time in three years I'm not kicking off a long-term backpacking trip in April. It feels odd, but I remind myself my priorities and goals have changed. I have some trips but they're short or work related.

Here's some highlights over the past few months, and things I continue to keep working towards:

- Consistent with the gym, I hit 16 workouts in March!!
- Fridays at [New Stadium](https://x.com/newsystems_/status/1887512072172241371) are so much fun
- Working on cooking every meal of day
- Spending time with people that value me as much as I them
- Created a small [travel log](https://x.com/typicalmitul/status/1894073979733766157) of my time in Buenos Aires
- Reading for at least ~20 minutes a day
- Vercel had a company wide offsite in Monterrey, where I got to meet all my coworkers
- I [did a podcast](https://open.spotify.com/episode/3ZFlTNlsLa8e2nYINUG0UF?si=c207029fd02a4087) to support my friend Rudy, I didn't think much about it initially until friends and strangers told me they enjoyed the listen

### Going forward

By default, I’d consider myself a fairly open person, everything I do is shaped by my curiosity of the world. While acknowledging that, I decided to make a change this year and apply the same principles to things I have previously had an aversion to. It’s been teaching me a lot, while sometimes validating for why I was initially averse to certain things.

Relationships have been changing. I've created distance in relationships where I felt I wasn't being offered the same level of transparency. And working on strengthening the relationships that matter to me, whether it's through hosting weekly dinners or simply consistently showing up.

At the moment, I am having trouble finding balance. I try to live my life with a max level of serendipity and spontaneity, but in reality, it's more closer to chaos. I want to slow things down and hopefully the intention of building stability will soon bring that.

<Images />

Things are still expected to significantly change soon, and hopefully the next update will be around that :)

Anyways, thanks for reading, see you soon.

---

<SmallText>
  1. man, it was probably just being in sun everyday in the middle of December
  lol
  <></>
  2. I love an em-dash, leave me alone
</SmallText>


================================================
FILE: app/(without-root-layout)/p/q1-2025/two-imgs.tsx
================================================
import img from "./img.jpg";
import img2 from "./img2.jpg";
import Figure from "../components/figure";

const Images = () => {
  return (
    <div className="relative grid gap-x-2 grid-cols-2 h-full items-center">
      <Figure src={img} alt="" />
      <Figure src={img2} alt="" />
    </div>
  );
};

export default Images;


================================================
FILE: app/(without-root-layout)/p/q2-2025/page.mdx
================================================
import { BlogPostJsonLd } from "@/components/json-ld";

export const metadata = {
  title: "[Quarter Review] Q2/25 / Mitul Shah",
  description: "Moving to NYC",
  authors: [{ name: "Mitul Shah", url: "https://mitul.ca" }],
  alternates: {
    canonical: "/p/q2-2025",
  },
  openGraph: {
    type: "article",
    title: "[Quarter Review] Q2/25 / Mitul Shah",
    description: "Moving to NYC",
    publishedTime: "2025-06-30",
    authors: ["Mitul Shah"],
  },
  twitter: {
    card: "summary_large_image",
    title: "[Quarter Review] Q2/25 / Mitul Shah",
    description: "Moving to NYC",
  },
};

<BlogPostJsonLd slug="q2-2025" />

# Q2/25

I finally moved to New York City. That's the most important part so might as well say it first. It's been everything I expected and nothing I could have prepared for, at the same time.

Everything I wrote about in the [last review](/p/q1-2025); my gym routine, daily cooking and following through with documenting my life essentially disappeared. While acknowledging my time in Toronto was soon coming to an end, I consciously made the decision to let go of structure in service of spending time with my friends and family.

That kind of summarizes the last three months of my life in the best way possible. It all felt right, even with the ups and downs that came with it.

![DSC02050.JPG](https://inqeleafibjx2dzc.public.blob.vercel-storage.com/bf0f0160-8ef8-47ea-aa85-7a57632e95aa.jpeg)

### Highlights

- Hosted weekly dinners for my closest friends
- Daytrip down to Buffalo to get the TN Visa approved
- Trip to London for Figma Config and [Italy](/os/notes/italy) for two weeks (Naples, Palermo, back to Naples, Sorrento, Rome)
- Reunited with so many friends in London. I feel extremely privileged and grateful to have friends in so many places of the world
- Goodbye Party
  - I brought together friends from all parts of my life
  - My friend Daniil cooked for about ~35 people and I'm forever grateful
- All my friends always give me cards / gifts that are so Toronto skyline related which has been cute
- Moved to NYC
  - My first few days were so deeply overwhelming, Toronto is almost, if not, everything I've ever known
  - That feeling eventually faded, if there's anything I know about myself is that I'm capable of adapting quickly
  - I'm loving my time, slowly and intentionally building up a new life
- Vercel Ship

I always thought I missed travelling, but I miss the memories I have of it. The little trip I had to Italy kind of validated that I'm no longer in dire need for it. Maybe I'll change my mind, but for now I'm so glad to be staying still.

### What's next?

- Finding a place to live and neighbourhood in NYC
- Build a foundation, and introduce familiar routine back into my life
- Stay connected to my friends back home
- Let things unfold without forcing it

If you're in NYC, I'd love to hang out sometime. Being new means new friends, new outlets. I'm going to be dedicating more time to photography and video, so I'm keen to meet people who I can learn from, or potentially work with.

![ .png](https://inqeleafibjx2dzc.public.blob.vercel-storage.com/concert-grid)

Thanks for reading, see you in a bit!


================================================
FILE: app/(without-root-layout)/p/q3-2025/page.mdx
================================================
import ImageContainer from "../components/img-container";
import Figure from "../components/figure";
import sunlight from "./opengraph-image.jpg";
import SmallText from "../components/small-text";
import { BlogPostJsonLd } from "@/components/json-ld";

export const metadata = {
  title: "[Quarter Review] Q3/25 / Mitul Shah",
  description: "Three months in NYC: Starting from scratch",
  authors: [{ name: "Mitul Shah", url: "https://mitul.ca" }],
  alternates: {
    canonical: "/p/q3-2025",
  },
  openGraph: {
    type: "article",
    title: "[Quarter Review] Q3/25 / Mitul Shah",
    description: "Three months in NYC: Starting from scratch",
    publishedTime: "2025-09-30",
    authors: ["Mitul Shah"],
  },
  twitter: {
    card: "summary_large_image",
    title: "[Quarter Review] Q3/25 / Mitul Shah",
    description: "Three months in NYC: Starting from scratch",
  },
};

<BlogPostJsonLd slug="q3-2025" />

# Q3/25

<Figure
  src={sunlight}
  alt="Boulevard des Italiens, Morning, Sunlight, Camille Pissarro – 1897"
  caption="Boulevard des Italiens, Morning, Sunlight, Camille Pissarro – 1897"
/>

It's hard to compress the past three months in a short write up, there's just so much that has happened. There's a lot about moving to a new country that I didn't account for, that I didn't expect both positive and bad.

The first few weeks or so were filled with me navigating the bureaucratic systems and being a degenerate. I was soaking in as much of the New York City summer as I possibly could, while being confused on how to get a social security number, with frequent reminders that they don't call it a SIN here.

I spent 6 weeks subletting in Bushwick, with retired Bengali parents as my roommates which I was happy about as it alleviated some stresses while I figured out the city¹. Being in Bushwick as a landing spot was an experience in itself, with visiting grocery shops and speaking only Spanish. This later inspired me to double down on learning more Spanish and sign up for 8 weeks of classes.

I moved onto Williamsburg for a month, at the Bedford stop, and hated it to say the least.

I ended up settling in the Lower East Side because ChatGPT (and friends) told me it's a neighbourhood the most similar to ones I enjoyed from home – it wasn't wrong. With it being my first year here, it just felt like it made sense to put myself in the heart of it all.

I got my first ever apartment, the bonus is it being in New York City.

<ImageContainer>
  ![
  .png](https://inqeleafibjx2dzc.public.blob.vercel-storage.com/IMG_8456.jpeg)
</ImageContainer>

Things in New York move extremely fast. I have made countless friends, I have met countless people that I can't remember. My phone filled with unsaved numbers, my Instagram filled with stories of people I met for a few minutes and will never see again. In the span of three months, I have also managed to lose friends. It's a little insane, but there's a part of me that absolutely loves the rush.

Day to day life has differed a bit as time has passed. Week one, I find myself in a Ridgewood grocery store, a bit excited that I can practice my Spanish and such, but by week four I'm just trying to find paneer between all the Oaxacan cheese. You notice there's tiny things you didn't think about – ranging between realizing you actually won't find your favourite brands to being confused because the bread tag doesn't have an expiry date.

Things are different in the most subtle ways; politics are more integral, holidays are more widely celebrated, and nobody cares if you bump into them.

I’ve essentially lived out of a backpack for the past three years, jumping between countries, sublets, random hostels and whatever. I don't own much stuff nor have I ever felt that I need much stuff but obviously I can't live in a house without any furniture. It's been a bit of a challenge forcing myself to accumulate things; stuff that does make me happy and adds value to my life but it's a different… playing field I guess.

But now that I do have an apartment that I am slowly furnishing to my taste and at a pace I'm comfortable with. I can find my grounding. I'm building out friend groups, routines, and I guess more wholly said, building a new life.

I don't want to make this entire reflection about New York (I did) but I'll leave it with a final point: there's a certain level of openness in this city that I have never been able to find anywhere else, and it feels so fitting for me. I love being able to interact with anyone, and the fact that anyone feels comfortable enough to interact with me.

Toronto is home, and I do miss it in the sense of familiarity and my friends. It felt odd going from a goodbye party with my closest friends, to celebrating my birthday with people I have yet to know. Yes, there’s something novel about it but sometimes, you just wish you had your favourite bar around the corner instead of finding the replacement.

--

I'm grateful to live a life where I am able to chase my dreams. I looked through the goals I set for myself earlier this year, and I had achieved nearly everything I wrote – even the ones I thought were far fetched. Maybe I should be aiming higher.

### What's next?

Honestly, just settling in and building out a life. Still furnishing my apartment, hosting more events and so on. I've started a new photography project that I'm constantly thinking about. I think I have to take some time and think about what my next long term goals are.

I finished writing this when there's one month left in the year, so I'll start working on that annual review right now…

<SmallText>
  1. They treated me like one of their own! My first time having roommates and
  they were incredibly sweet
</SmallText>


================================================
FILE: app/(without-root-layout)/p/substack-embed.module.css
================================================
.wrapper {
  position: relative;
  width: 480px;
  max-width: 100%;
  height: 150px;
  margin: -20px auto 2rem;
}

.iframe {
  width: 100%;
  height: 100%;
  border: none;
}

/* .overlay {
  position: absolute;
  inset: 0;
  background: #f9f2e2;
  mix-blend-mode: multiply;
  pointer-events: none;
} */

.logoCover {
  position: absolute;
  bottom: 0;
  right: 0;
  width: 100px;
  height: 28px;
  background: #f9f2e2;
  pointer-events: none;
}


================================================
FILE: app/(without-root-layout)/visitors/actions.ts
================================================
"use server";

import { z } from "zod";
import { saveGuestbookEntry } from "@/app/actions";
import { sql } from "@vercel/postgres";

const GuestbookEntrySchema = z.object({
  created_by: z
    .string()
    .min(1, "pls fill out all fields")
    .max(50, "ur name is too long"),
  entry: z
    .string()
    .min(1, "pls fill out all fields")
    .max(200, "love ur long entry, but can u make it shorter?"),

  signature: z.string().optional(),
  local_entry_id: z.string().optional(),
  hasCreatedEntryBefore: z.string().optional(),
  local_created_by_id: z.string().optional(),
});

export async function validateAndSaveEntry(
  formData: FormData,
  validateOnly = false
) {
  try {
    GuestbookEntrySchema.parse(Object.fromEntries(formData));

    if (validateOnly) {
      return { success: true };
    }

    await saveGuestbookEntry("", formData);

    sendEmail(formData);

    return { success: true };
  } catch (error) {
    if (error instanceof z.ZodError) {
      return { success: false, errors: error.flatten().fieldErrors };
    }
    return {
      success: false,
      errors: { form: ["An unexpected error occurred"] },
    };
  }
}

async function sendEmail(formData: FormData) {
  try {
    const response = await fetch("https://mitul.ca/api/send", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify({ entry: Object.fromEntries(formData) }),
      keepalive: true,
    });

    if (!response.ok) {
      console.error("Failed to send email:", await response.text());
    }
  } catch (error) {
    console.error("Error sending email:", error);
  }
}

export const getGuestbookEntries = async () => {
  const { rows } = await sql`
      SELECT * FROM "guestbook"
  WHERE approved = true AND id > (
    SELECT FLOOR(RANDOM() * (SELECT MAX(id) FROM guestbook WHERE approved = true))
  )
  ORDER BY id
  LIMIT 30;
  `;

  return rows;
};


================================================
FILE: app/(without-root-layout)/visitors/all/page.tsx
================================================
import * as React from "react";
import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority";
import Note from "@/components/visitors/note";
import { cn } from "@/lib/utils";
import { sql } from "@vercel/postgres";
import styles from "./visitors-all.module.css";
import LinkPrimitive from "@/components/link-primitive";
import { Suspense } from "react";
import Link from "next/link";

const ITEMS_PER_PAGE = 50;

export default function Page(
  props: {
    searchParams: Promise<{ page?: string }>;
  }
) {
  return (
    <div className="relative">
      <div className="fixed top-8 left-8 text-gray-2 z-10 isolate flex gap-x-4">
        <LinkPrimitive
          href="/visitors"
          variant="route"
          className="rounded-full text-gray-12 px-3 shadow-md font-medium"
        >
          return to the visitor's log
        </LinkPrimitive>
        <LinkPrimitive
          href="/"
          variant="route"
          className="rounded-full text-gray-12 px-3 shadow-md font-medium"
        >
          return home
        </LinkPrimitive>
      </div>
      <Suspense fallback={<p className="text-center mt-12">Loading entries...</p>}>
        <PageContent searchParams={props.searchParams} />
      </Suspense>
    </div>
  );
}

async function PageContent({ searchParams }: { searchParams: Promise<{ page?: string }> }) {
  const params = await searchParams;
  const currentPage = Number(params.page) || 1;

  return (
    <Suspense key={currentPage} fallback={<p>Loading entries...</p>}>
      <EntriesList currentPage={currentPage} />
    </Suspense>
  );
}

async function EntriesList({ currentPage }: { currentPage: number }) {
  const { entries, totalPages } = await getGuestbookEntries(currentPage);

  if (!entries.length) {
    return <p>No entries found.</p>;
  }

  return (
    <div className="flex flex-col items-center">
      <div
        className={cn(
          "flex flex-wrap gap-x-4 gap-y-4 *:relative! *:transform-none! *:rotate-0! py-12",
          styles.container
        )}
      >
        {entries.map((entry) => (
          <Note
            key={entry.id}
            name={entry.created_by}
            content={entry.body}
            signature={entry.signature}
          />
        ))}
      </div>
      <Pagination currentPage={currentPage} totalPages={totalPages} />
    </div>
  );
}

function Pagination({
  currentPage,
  totalPages,
}: {
  currentPage: number;
  totalPages: number;
}) {
  return (
    <div className="flex justify-center space-x-2 mt-8 text-[black] pb-12">
      <Link
        href={`/visitors/all?page=${currentPage - 1}`}
        aria-disabled={currentPage <= 1}
        className={cn(currentPage <= 1 ? "pointer-events-none" : "")}
        tabIndex={currentPage <= 1 ? -1 : undefined}
      >
        <Button variant="outline" disabled={currentPage <= 1}>
          Previous
        </Button>
      </Link>
      <span className="py-2 px-3 bg-gray-100 rounded">
        Page {currentPage} of {totalPages}
      </span>
      <Link
        href={`/visitors/all?page=${currentPage + 1}`}
        aria-disabled={currentPage >= totalPages}
        className={cn(currentPage >= totalPages ? "pointer-events-none" : "")}
        tabIndex={currentPage >= totalPages ? -1 : undefined}
      >
        <Button variant="outline" disabled={currentPage >= totalPages}>
          Next
        </Button>
      </Link>
    </div>
  );
}

const buttonVariants = cva(
  "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
  {
    variants: {
      variant: {
        default: "bg-primary text-primary-foreground hover:bg-primary/90",
        destructive:
          "bg-destructive text-destructive-foreground hover:bg-destructive/90",
        outline:
          "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
        secondary:
          "bg-secondary text-secondary-foreground hover:bg-secondary/80",
        ghost: "hover:bg-accent hover:text-accent-foreground",
        link: "text-primary underline-offset-4 hover:underline",
      },
      size: {
        default: "h-10 px-4 py-2",
        sm: "h-9 rounded-md px-3",
        lg: "h-11 rounded-md px-8",
        icon: "h-10 w-10",
      },
    },
    defaultVariants: {
      variant: "default",
      size: "default",
    },
  }
);

export interface ButtonProps
  extends React.ButtonHTMLAttributes<HTMLButtonElement>,
    VariantProps<typeof buttonVariants> {
  asChild?: boolean;
}

const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
  ({ className, variant, size, asChild = false, ...props }, ref) => {
    const Comp = asChild ? Slot : "button";
    return (
      <Comp
        className={cn(buttonVariants({ variant, size, className }))}
        ref={ref}
        {...props}
      />
    );
  }
);

Button.displayName = "Button";

async function getGuestbookEntries(page: number) {
  const offset = (page - 1) * ITEMS_PER_PAGE;
  try {
    const [entriesResult, countResult] = await Promise.all([
      sql`
        SELECT id, created_by, body, signature
        FROM guestbook
        WHERE approved = true
        ORDER BY last_modified DESC
        LIMIT ${ITEMS_PER_PAGE} OFFSET ${offset}
      `,
      sql`SELECT COUNT(*) FROM guestbook WHERE approved = true`,
    ]);

    const totalEntries = Number(countResult.rows[0].count);
    const totalPages = Math.ceil(totalEntries / ITEMS_PER_PAGE);

    return {
      entries: entriesResult.rows,
      totalPages,
    };
  } catch (error) {
    console.error("Failed to fetch guestbook entries:", error);
    throw new Error("Failed to fetch guestbook entries");
  }
}


================================================
FILE: app/(without-root-layout)/visitors/all/visitors-all.module.css
================================================
.container {
  display: flex;
  align-items: center;
  justify-content: center;
}

.container :global(.note-item) {
  box-shadow: none;
  transform: none !important;
  max-width: 100% !important;
}


================================================
FILE: app/(without-root-layout)/visitors/gang/page.tsx
================================================
import ApproveButton from "@/components/visitors/approve-btn";

import { sql } from "@vercel/postgres";
import { cookies } from "next/headers";
import { redirect } from "next/navigation";
import { Suspense } from "react";

export default function ProtectedPage() {
  return (
    <Suspense fallback={<div className="bg-gray-1 text-gray-12 p-12 h-screen">Loading...</div>}>
      <AuthenticatedContent />
    </Suspense>
  );
}

async function AuthenticatedContent() {
  const cookieStore = await cookies();
  const isAuthenticated = cookieStore.get("auth");

  if (!isAuthenticated) {
    redirect("/visitors/login");
  }

  return (
    <div className="bg-gray-1 text-gray-12 p-12 h-screen">
      <h1>Welcome to the gang page</h1>
      <p>Only authenticated users can access this page.</p>
      <Suspense fallback={<div className="text-gray-11">Loading entries...</div>}>
        <div
          className="grid gap-4 mt-6"
          style={{
            gridTemplateColumns: "repeat(auto-fill, minmax(300px, 1fr))",
          }}
        >
          <GuestbookEntries />
        </div>
      </Suspense>
    </div>
  );
}

const colorMap = new Map();
const colors = ["red", "green", "blue", "orange", "purple", "yellow"];

// we gotta improve this a bit to be easier to scan
function getColor(id: string) {
  if (!colorMap.has(id)) {
    const color = colors[colorMap.size % colors.length];
    colorMap.set(id, color);
  }
  return colorMap.get(id);
}

async function GuestbookEntries() {
  const { rows } =
    await sql`SELECT * from "guestbook" WHERE approved = false ORDER BY id DESC;`;

  return rows.map((entry) => (
    <div key={entry.id} className="w-fit h-fit border border-gray-10 p-2">
      <div className="border border-gray-6 bg-gray-3 rounded-[3px] flex items-center justify-center overflow-hidden">
        <div
          className="object-contain"
          // biome-ignore lint/security/noDangerouslySetInnerHtml: <explanation>
          dangerouslySetInnerHTML={{ __html: entry.signature }}
        />
      </div>
      <div className="w-full text-sm break-words mt-1.5">
        <span className="text-gray-12 text-[14px] mr-1 font-semibold">
          {entry.created_by}
        </span>
        <div className="text-[16px] font-medium">{entry.body}</div>
      </div>
      <div className="flex flex-col">
        <span>
          {entry.hascreatedentrybefore ? "has created an entry before" : "new"}
        </span>

        {entry.local_created_by_id && (
          <span style={{ color: getColor(entry.local_created_by_id) }}>
            created by: {entry.local_created_by_id}
          </span>
        )}
      </div>
      <ApproveButton id={entry.id} />
    </div>
  ));
}


================================================
FILE: app/(without-root-layout)/visitors/login/page.tsx
================================================
"use client";

import { useState } from "react";
import { useRouter } from "next/navigation";

export default function LoginPage() {
  const [password, setPassword] = useState("");
  const router = useRouter();

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    const response = await fetch("/api/login", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ password }),
    });

    if (response.ok) {
      router.push("/visitors/gang");
    } else {
      alert("Incorrect password");
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="password"
        value={password}
        onChange={(e) => setPassword(e.target.value)}
        placeholder="Enter password"
      />
      <button type="submit">Login</button>
    </form>
  );
}


================================================
FILE: app/(without-root-layout)/visitors/notes.module.css
================================================
.matContainer {
  border-radius: 10px;
  backdrop-filter: blur(10px);
  box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2), 0 8px 16px rgba(0, 0, 0, 0.2),
    0 16px 32px rgba(0, 0, 0, 0.2);
  background-color: #0a4a31;
}

.matContainer:after {
  content: "";
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  border-radius: 10px;
  box-shadow: inset 0 0 0 1.5px #fff6;
}

.matGrid {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background-image: linear-gradient(
      to right,
      rgba(255, 255, 255, 0.1) 1px,
      transparent 1px
    ),
    linear-gradient(to bottom, rgba(255, 255, 255, 0.1) 1px, transparent 1px);
  background-size: 2vmin 2vmin;
  background-position: center;
  border-radius: 6px;
  margin: 30px;
  border: 1px solid rgba(255, 255, 255, 0.1);
}

.matGrid::after {
  content: "";
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background-image: linear-gradient(
      to right,
      rgba(255, 255, 255, 0.2) 1px,
      transparent 1px
    ),
    linear-gradient(to bottom, rgba(255, 255, 255, 0.2) 1px, transparent 1px);
  background-size: 10vmin 10vmin;
  background-position: center;
}

@media (max-width: 768px) {
  .matGrid {
    background-size: 10vmin 10vmin;
    margin: 10px;
  }

  .matGrid:after {
    background-size: 30vmin 30vmin;
  }
}

.matTexture {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;

  background: linear-gradient(
      45deg,
      rgba(255, 255, 255, 0.2) 0%,
      rgba(255, 255, 255, 0) 60%
    ),
    url(/noise.svg);
  opacity: 0.75;
  mix-blend-mode: overlay;
  pointer-events: none;
}

.window {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background-image: url(/images/Layer-88.png);
  background-size: cover;
  background-position: center;
  opacity: 0.3;
  pointer-events: none;
  mix-blend-mode: normal;
  z-index: 10000;
  animation: window-shadow 12s cubic-bezier(0.45, 0.05, 0.55, 0.95) infinite
    alternate;
  border-radius: 10px;
  user-select: none;
}

@keyframes window-shadow {
  0% {
    transform: translate(0px, 0px);
    opacity: 0.5;
    scale: 1.05;
  }
  50% {
    transform: translate(20px, 10px);
    opacity: 0.3;
    scale: 1.05;
  }
  100% {
    transform: translate(30px, 5px);
    opacity: 0.2;
    scale: 1.05;
  }
}

.diagonalLines {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background-image: linear-gradient(
      45deg,
      transparent 49.5%,
      rgba(255, 255, 255, 0.2) 49.5%,
      rgba(255, 255, 255, 0.2) 50.5%,
      transparent 50.5%
    ),
    linear-gradient(
      -45deg,
      transparent 49.5%,
      rgba(255, 255, 255, 0.2) 49.5%,
      rgba(255, 255, 255, 0.2) 50.5%,
      transparent 50.5%
    );
  background-size: 10vmin 10vmin;
  background-position: center;
  pointer-events: none;
}

.clip {
  position: absolute;
  top: 0px;
  left: 0;
  right: 0;
  bottom: 0;
  width: 60px;
  transform: rotate(-8deg);
}

.homeBtn {
  box-shadow: rgba(0, 0, 0, 0.4) 0px 2px 4px,
    rgba(0, 0, 0, 0.3) 0px 7px 13px -3px, rgba(0, 0, 0, 0.2) 0px -3px 0px inset,
    inset 0 1px 0 0 #ffffff52;
}

.stamp {
  position: absolute;
  top: 5vmin;
  right: 5vmin;
  width: 11vmin;
  height: 11vmin;
  border: 2px solid rgba(255, 255, 255, 0.5);
  border-radius: 50%;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  color: rgba(255, 255, 255, 0.5);
  word-wrap: break-word;
  word-spacing: 1vmin;
  text-transform: uppercase;
  text-align: center;
  transform: rotate(15deg);
  font-weight: 600;
  font-size: 1.25vmin;
  letter-spacing: -0.05px;
  user-select: none;
}

.star {
  position: absolute;
  bottom: 3.5vmin;
  left: 5vmin;
  width: 11vmin;
  height: 11vmin;
  transform: rotate(-15deg);
  mix-blend-mode: hard-light;
  filter: invert(60%) blur(0.6px);
}

/* .moreNoise {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background-image: url(/images/024.png);
  opacity: 0.6;
  pointer-events: none;
  mix-blend-mode: screen;
}
*/


================================================
FILE: app/(without-root-layout)/visitors/page.tsx
================================================
import type { Viewport } from "next";
import { Provider } from "jotai";
import { cn } from "@/lib/utils";
import styles from "./notes.module.css";
import Polaroid from "@/components/visitors/polaroid";
import WriteNoteCTA from "@/components/visitors/cta";
import { ArrowLeft } from "@phosphor-icons/react/dist/ssr/ArrowLeft";
import { ArrowRight } from "@phosphor-icons/react/dist/ssr/ArrowRight";
import Link from "next/link";
import {
  VercelLogo,
  Sticker,
  NextWordmark,
} from "@/components/visitors/stickers";
import Image from "next/image";
import GuestbookEntries from "@/components/visitors/guestbook-entries";
import { Suspense } from "react";


export const viewport: Viewport = {
  themeColor: "#FCFCFC",
};

const Page = () => {
  return (
    <Provider>
      <div className={cn("h-[100dvh] sm:h-[100vh] p-1 sm:p-6 bg-gray-1")}>
        <div
          id="mat-container"
          className={cn(
            "relative w-full h-full overflow-hidden",
            styles.matContainer
          )}
        >
          <div className="z-10">
            <div id="mat-texture" className={styles.matTexture} />
            <div aria-hidden className={styles.window} />
            <div aria-hidden className={styles.star}>
              <Image
                alt="star drawing"
                width={80}
                height={80}
                src="/images/Star_002.png"
              />
            </div>
            {/* <div aria-hidden className={styles.moreNoise} /> */}
            <div id="mat-grid" className={styles.matGrid}>
              <div id="diagonal-lines" className={styles.diagonalLines} />
            </div>
          </div>
          <main className="relative z-20 h-full w-full">
            <div className={styles.stamp}>
              <span>Premium made in toronto quality</span>
            </div>

            <Suspense fallback={null}>
              <GuestbookEntries />
              <Polaroid src="/images/banff-2.jpg" alt="toronto" />
              <Polaroid src="/images/toronto.jpg" alt="toronto" />
              <Polaroid src="/images/nyc.jpg" alt="toronto" />
              <Sticker>
                <img
                  className="w-36"
                  src="/images/spiderman.png"
                  alt="Spiderman sticker"
                  draggable={false}
                />
              </Sticker>
              <Sticker>
                <img
                  className="w-24"
                  src="/images/cntower.png"
                  alt="CN Tower sticker"
                  draggable={false}
                />
              </Sticker>
              <Sticker>
                <VercelLogo />
              </Sticker>
              <Sticker>
                <NextWordmark />
              </Sticker>
            </Suspense>

            <Link
              href="/"
              className={cn(
                "mr-auto rounded-full bg-[#027582] hover:bg-[#027582]/90 transition hover:scale-105 hover:-rotate-6 px-3 py-1.5 flex gap-x-1.5 items-center justify-center text-gray-1 font-semibold w-fit h-fit z-50 absolute top-4 left-4 sm:top-10 sm:left-10",
                styles.homeBtn
              )}
            >
              <ArrowLeft width={16} height={16} />
              take me home
            </Link>
            <Link
              href="/visitors/all"
              className={cn(
                "mr-auto rounded-full bg-[#027582] hover:bg-[#027582]/90 transition hover:scale-105 hover:-rotate-6 px-3 py-1.5 flex gap-x-1.5 items-center justify-center text-gray-1 font-semibold w-fit h-fit z-50 absolute top-14 left-4 sm:bottom-10 sm:right-10 sm:left-auto sm:top-auto",
                styles.homeBtn
              )}
            >
              see all notes
              <ArrowRight width={16} height={16} />
            </Link>
            <WriteNoteCTA />
          </main>
        </div>
      </div>
    </Provider>
  );
};

export default Page;


================================================
FILE: app/actions.ts
================================================
"use server";
import { sql } from "@vercel/postgres";
import { revalidatePath } from "next/cache";

export async function saveGuestbookEntry(state: unknown, formData: FormData) {
  const local_entry_id = formData.get("local_entry_id")?.toString();
  const created_by = formData.get("created_by")?.toString() || "";
  const signature = formData.get("signature")?.toString() || "";
  const hasCreatedEntryBefore = formData
    .get("hasCreatedEntryBefore")
    ?.toString();
  const local_created_by_id = formData.get("local_created_by_id")?.toString();
  const entry = formData.get("entry")?.toString() || "";
  const body = entry.slice(0, 500);

  await sql`
    INSERT INTO "guestbook" (created_by, body, last_modified, signature, hasCreatedEntryBefore, local_created_by_id, local_entry_id) 
    VALUES (${created_by}, ${body}, ${new Date().toISOString()}, ${signature}, ${hasCreatedEntryBefore}, ${local_created_by_id}, ${local_entry_id});
  `;

  revalidatePath("/visitors");
}

export async function approveGuestbookEntry(id: string) {
  await sql`
    UPDATE "guestbook" SET approved = true WHERE id = ${id};
  `;

  revalidatePath("/visitors");
}

export async function declineGuestbookEntry(id: string) {
  await sql`
    DELETE FROM "guestbook" WHERE id = ${id};
  `;

  revalidatePath("/visitors");
}


================================================
FILE: app/api/login/route.ts
================================================
import { NextResponse } from "next/server";
import { cookies } from "next/headers";

export async function POST(request: Request) {
  const { password } = await request.json();

  if (password === process.env.ROUTE_PASSWORD) {
    (await cookies()).set("auth", "true", { httpOnly: true });
    return NextResponse.json({ success: true });
  }
  return NextResponse.json({ success: false }, { status: 401 });
}


================================================
FILE: app/api/md/route.ts
================================================
import { NextRequest } from "next/server";
import { experiences, beliefs, bucketList, Status } from "@/content";
import { blogPosts, getBlogPost } from "@/lib/blog-posts";
import { readFileSync } from "fs";
import { join } from "path";

const BASE_URL = "https://mitul.ca";

function getHomeMarkdown(): string {
  return `# Mitul Shah

> Design engineer, photographer, and a bit more.

Crafting memorable interfaces with deep attention to detail. I dedicate most my time to continuous learning and refining my skillset.

## Experience

${experiences.map((exp) => `### ${exp.company} - ${exp.role} (${exp.range})
${exp.description}
${exp.link ? `[Visit](${exp.link})` : ""}`).join("\n\n")}

## Projects

### Daybloom
A mindful daily journal that combines your photos and thoughts into calendar view, helping you capture memories and reflect on life's meaningful moments.
[Visit](https://daybloom.app)

### Montreal in Motion
A documentation of the brutalist and distinctly designed metro stations. The project uses CSS 3D transforms and noise to mirror the architectural character of the spaces.
[Visit](https://typicalmitul.com/montreal-in-motion)

### Places to Read
A microsite to discover community submitted parks around the world where you can sit down, chill and enjoy reading a book.
[Visit](https://placestoread.xyz)

## Photography

I've built up my craft as a photographer over a number of years and thrived in turning it into an independent business.

Notable achievements include:
- Being a personal photographer for the Uber CEO
- Featured in local Toronto newspapers
- Having a photo as a wallpaper in every Google device

Today, my focus is on music photography where I capture my favourite artists at concerts or festivals.

[Visit my portfolio](https://typicalmitul.com)

## Contact

- Email: mitulxshah@gmail.com
- Twitter: [@typicalmitul](https://x.com/typicalmitul)
- Instagram: [@typicalmitul](https://instagram.com/typicalmitul)
- GitHub: [mitul-s](https://github.com/mitul-s)

## Blog Posts

${blogPosts.map((post) => `- [${post.title}](${BASE_URL}/p/${post.slug}) - ${post.description}`).join("\n")}
`;
}

function getAboutMarkdown(): string {
  return `# About Mitul Shah

Hey, my name is Mitul and welcome to my space on the internet. I'm a self-taught design engineer based in Toronto, Canada.

Learning to code has felt like a superpower for me, it allows me to bring any idea I can imagine to life. I love creating and focusing on the little things that enhance our experiences as we dive into the abyss of the web.

Apart from all of that, a strong sense of curiosity about the world has always driven me. Travel, and specifically the diverse experiences gained from exploring different places, cultures, and landscapes, have significantly influenced my personal growth.

My life thrives on both chaos and serendipity. I'm just tryna channel the same spirit of adventure as Ferris Bueller.

## Beliefs

${beliefs.map((belief) => `- ${belief}`).join("\n")}

## Bucket List

${bucketList.map((item) => `- ${item.item}${item.status === Status.completed ? " ✓" : item.status === Status.progress ? " (in progress)" : ""}`).join("\n")}
`;
}

function getBlogPostMarkdown(slug: string): string | null {
  const post = getBlogPost(slug);
  if (!post) return null;

  // Try to read the MDX file content
  try {
    const mdxPath = join(
      process.cwd(),
      "app/(without-root-layout)/p",
      slug,
      "page.mdx"
    );
    let content = readFileSync(mdxPath, "utf-8");

    // Remove import statements and export metadata
    content = content
      .replace(/^import.*$/gm, "")
      .replace(/export const metadata = \{[\s\S]*?\};/m, "")
      .replace(/<BlogPostJsonLd.*?\/>/g, "")
      .replace(/<Figure[\s\S]*?\/>/g, (match) => {
        // Extract alt text from Figure components
        const altMatch = match.match(/alt="([^"]*)"/);
        const srcMatch = match.match(/src=\{?([^}\s]*)\}?/);
        if (altMatch) return `*${altMatch[1]}*`;
        return "";
      })
      .replace(/<ImageContainer>[\s\S]*?<\/ImageContainer>/g, "")
      .replace(/<Callout>[\s\S]*?<\/Callout>/g, "")
      .replace(/<SmallText>[\s\S]*?<\/SmallText>/g, "")
      .replace(/<[^>]+>/g, "") // Remove remaining JSX tags
      .trim();

    return `# ${post.title}

*Published: ${post.datePublished}*

${content}

---
[Back to home](${BASE_URL})
`;
  } catch {
    // Fallback if we can't read the file
    return `# ${post.title}

${post.description}

*Published: ${post.datePublished}*

[Read the full article](${BASE_URL}/p/${slug})
`;
  }
}

function getVisitorsMarkdown(): string {
  return `# Guestbook

Welcome to the guestbook! This is an interactive page where visitors can leave notes.

[Visit the guestbook](${BASE_URL}/visitors) to sign it.
`;
}

export async function GET(request: NextRequest) {
  // Try searchParams first, then x-original-path header from proxy
  const path =
    request.nextUrl.searchParams.get("path") ||
    request.headers.get("x-original-path") ||
    "/";

  let markdown: string;

  if (path === "/" || path === "") {
    markdown = getHomeMarkdown();
  } else if (path === "/about") {
    markdown = getAboutMarkdown();
  } else if (path === "/visitors") {
    markdown = getVisitorsMarkdown();
  } else if (path.startsWith("/p/")) {
    const slug = path.replace("/p/", "");
    const content = getBlogPostMarkdown(slug);
    if (content) {
      markdown = content;
    } else {
      markdown = `# Not Found\n\nThe requested page was not found.`;
    }
  } else {
    markdown = `# Not Found\n\nThe requested page was not found.`;
  }

  return new Response(markdown, {
    headers: {
      "Content-Type": "text/markdown; charset=utf-8",
      "Cache-Control": "public, max-age=3600, s-maxage=3600",
    },
  });
}


================================================
FILE: app/api/send/route.ts
================================================
import { EmailTemplate } from "@/components/email-template";
import { Resend } from "resend";

const resend = new Resend(process.env.RESEND_API_KEY);

export async function POST(request: Request) {
  try {
    const { entry } = await request.json();
    const { data, error } = await resend.emails.send({
      from: "Guestbook <hi@daybloom.app>",
      to: ["mitulxshah@gmail.com"],
      subject: "New Submission!",
      react: await EmailTemplate({ entry }),
    });

    if (error) {
      return Response.json({ error }, { status: 500 });
    }

    return Response.json(data);
  } catch (error) {
    return Response.json({ error }, { status: 500 });
  }
}


================================================
FILE: app/api/spotify/route.ts
================================================
import { NextResponse } from "next/server";
import { getSpotifyData } from "@/lib/spotify";


export async function GET() {
  const data = await getSpotifyData();
  return NextResponse.json(data);
}


================================================
FILE: app/feed.xml/route.ts
================================================
import { blogPosts } from "@/lib/blog-posts";

const BASE_URL = "https://mitul.ca";

function escapeXml(text: string): string {
  return text
    .replace(/&/g, "&amp;")
    .replace(/</g, "&lt;")
    .replace(/>/g, "&gt;")
    .replace(/"/g, "&quot;")
    .replace(/'/g, "&apos;");
}

function generateRssFeed(): string {
  const sortedPosts = [...blogPosts].sort(
    (a, b) => new Date(b.datePublished).getTime() - new Date(a.datePublished).getTime()
  );

  const items = sortedPosts
    .map((post) => {
      const pubDate = new Date(post.datePublished).toUTCString();
      return `
    <item>
      <title>${escapeXml(post.title)}</title>
      <link>${BASE_URL}/p/${post.slug}</link>
      <description>${escapeXml(post.description)}</description>
      <pubDate>${pubDate}</pubDate>
      <guid isPermaLink="true">${BASE_URL}/p/${post.slug}</guid>
    </item>`;
    })
    .join("");

  return `<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>Mitul Shah</title>
    <link>${BASE_URL}</link>
    <description>Design engineer, photographer, and a bit more. Annual reviews and quarterly reflections.</description>
    <language>en-us</language>
    <lastBuildDate>${new Date().toUTCString()}</lastBuildDate>
    <atom:link href="${BASE_URL}/feed.xml" rel="self" type="application/rss+xml"/>
    ${items}
  </channel>
</rss>`;
}

export function GET() {
  const feed = generateRssFeed();

  return new Response(feed, {
    headers: {
      "Content-Type": "application/xml; charset=utf-8",
      "Cache-Control": "public, max-age=3600, s-maxage=3600",
    },
  });
}


================================================
FILE: app/globals.css
================================================
@import "@radix-ui/colors/gray.css" layer(base);
@import "@radix-ui/colors/blue.css" layer(base);

@import "tailwindcss";

[data-theme="green"] {
  --color-accent-rgb: 19, 50, 18;
  --color-accent: rgb(var(--color-accent-rgb));
  --color-accent-hex: #133212;
}

[data-theme="red"] {
  --color-accent-rgb: 178, 0, 36;
  --color-accent: rgb(var(--color-accent-rgb));
  --color-accent-hex: #b20024;
}

[data-theme="blue"] {
  --color-accent-rgb: 2, 16, 147;
  --color-accent: rgb(var(--color-accent-rgb));
  --color-accent-hex: #021093;
}

@theme {
  --color-*: initial;
  --color-gray-1: var(--gray-1);
  --color-gray-2: var(--gray-2);
  --color-gray-3: var(--gray-3);
  --color-gray-4: var(--gray-4);
  --color-gray-5: var(--gray-5);
  --color-gray-6: var(--gray-6);
  --color-gray-7: var(--gray-7);
  --color-gray-8: var(--gray-8);
  --color-gray-9: var(--gray-9);
  --color-gray-10: var(--gray-10);
  --color-gray-11: var(--gray-11);
  --color-gray-12: var(--gray-12);
  --color-gray-a1: var(--grayA-1);
  --color-gray-a2: var(--grayA-2);
  --color-gray-a3: var(--grayA-3);
  --color-gray-a4: var(--grayA-4);
  --color-gray-a5: var(--grayA-5);
  --color-gray-a6: var(--grayA-6);
  --color-gray-a7: var(--grayA-7);
  --color-gray-a8: var(--grayA-8);
  --color-gray-a9: var(--grayA-9);
  --color-gray-a10: var(--grayA-10);
  --color-gray-a11: var(--grayA-11);
  --color-gray-a12: var(--grayA-12);

  --color-blue-1: var(--blue-1);
  --color-blue-2: var(--blue-2);
  --color-blue-3: var(--blue-3);
  --color-blue-4: var(--blue-4);
  --color-blue-5: var(--blue-5);
  --color-blue-6: var(--blue-6);
  --color-blue-7: var(--blue-7);
  --color-blue-8: var(--blue-8);
  --color-blue-9: var(--blue-9);
  --color-blue-10: var(--blue-10);
  --color-blue-11: var(--blue-11);
  --color-blue-12: var(--blue-12);
  --color-blue-a1: var(--blueA-1);
  --color-blue-a2: var(--blueA-2);
  --color-blue-a3: var(--blueA-3);
  --color-blue-a4: var(--blueA-4);
  --color-blue-a5: var(--blueA-5);
  --color-blue-a6: var(--blueA-6);
  --color-blue-a7: var(--blueA-7);
  --color-blue-a8: var(--blueA-8);
  --color-blue-a9: var(--blueA-9);
  --color-blue-a10: var(--blueA-10);
  --color-blue-a11: var(--blueA-11);
  --color-blue-a12: var(--blueA-12);

  /* --color-accent: #b3fc03; */
  /* --color-accent: #133212; */
  /* --color-accent: #021093; */
  --color-accent: #b20024;
  /* --color-accent: #ff7f00; */
  --color-light-green: #e2f3e3;
  --color-white: #fff;

  --text-*: initial;
  --text-sm: 0.75rem;
  --text-base: 0.875rem;

  --radius-*: initial;
  --radius-4: 4px;
  --radius-6: 6px;
  --radius-sm: 1px;
  --radius-md: 2px;
  --radius-full: 9999px;

  --background-image-gradient-radial: radial-gradient(var(--tw-gradient-stops));
  --background-image-gradient-conic: conic-gradient(
    from 180deg at 50% 50%,
    var(--tw-gradient-stops)
  );

  --animate-accordion-down: accordion-down 0.2s ease-out;
  --animate-accordion-up: accordion-up 0.2s ease-out;
  --animate-slide-up-and-fade: slide-up-and-fade 0.25s
    cubic-bezier(0.16, 0, 0.13, 1);
  --animate-slide-down-and-fade: slide-down-and-fade 0.25s
    cubic-bezier(0.16, 0, 0.13, 1);

  --animate-blur-and-slide-up: blur-and-slide-up 0.25s
    cubic-bezier(0.16, 0, 0.13, 1);
  --animate-blur-and-slide-down: blur-and-slide-down 0.25s
    cubic-bezier(0.16, 0, 0.13, 1);

  @keyframes accordion-down {
    from {
      height: 0;
      opacity: 0;
      filter: blur(4px);
      transform: translateY(-1rem);
    }
    to {
      height: var(--radix-accordion-content-height);
      opacity: 1;
      filter: blur(0);
      transform: translateY(0);
    }
  }

  @keyframes accordion-up {
    from {
      height: var(--radix-accordion-content-height);
      opacity: 1;
      filter: blur(0);
    }
    to {
      height: 0;
      opacity: 0;
      filter: blur(4px);
    }
  }

  @keyframes slide-up-and-fade {
    0% {
      opacity: 0;
      transform: translateY(1rem);
    }
    100% {
      opacity: 1;
      transform: translateY(0);
    }
  }
  @keyframes slide-down-and-fade {
    0% {
      opacity: 0;
      transform: translateY(-1rem);
    }
    100% {
      opacity: 1;
      transform: translateY(0);
    }
  }

  @keyframes blur-and-slide-up {
    from {
      opacity: 0;
      filter: blur(4px);
      transform: translateY(-1.5rem) scale(0.9);
    }
    to {
      opacity: 1;
      filter: blur(0);
      transform: translateY(0) scale(1);
    }
  }

  @keyframes blur-and-slide-down {
    from {
      opacity: 0;
      filter: blur(4px);
      transform: translateY(1.5rem) scale(0.9);
    }
    to {
      opacity: 1;
      filter: blur(0);
      transform: translateY(0) scale(1);
    }
  }
}

/*
  The default border color has changed to `currentColor` in Tailwind CSS v4,
  so we've added these compatibility styles to make sure everything still
  looks the same as it did with Tailwind CSS v3.

  If we ever want to remove these styles, we need to add an explicit border
  color utility to any element that depends on these defaults.
*/
@layer base {
  *,
  ::after,
  ::before,
  ::backdrop,
  ::file-selector-button {
    border-color: var(--color-gray-200, currentColor);
  }
}

.select ::-moz-selection {
  @apply text-accent bg-accent/10;
}

.select ::selection {
  @apply text-accent bg-accent/10;
}

a,
button {
  touch-action: manipulation;
}

html {
  /* background-color: var(--color-accent); */
}

body {
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-rendering: geometricPrecision;
  font-family: "Monument Grotesk", sans-serif;
  /* font-family: "Helvetica Neue", sans-serif; */
  font-size: 14px;
  /* background-color: white; */
  /* background: linear-gradient(
      to bottom,
      transparent,
      rgb(var(--background-end-rgb))
    )
    rgb(var(--background-start-rgb)); */
}

.bg {
  background-image: linear-gradient(to right, #101010, rgba(0, 0, 0, 0.83)),
    url(/noise.svg), linear-gradient(#b3fc03 1px, transparent 1px),
    linear-gradient(to right, #b3fc03 1px, #000 1px);
  background-size: auto, auto, 20px 20px, 20px 20px;
}

.accordion-grid-cols {
  grid-template-columns: min-content 125px 1fr min-content min-content;
}

.main-noise {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  /* background: linear-gradient(
      45deg,
      rgba(255, 255, 255, 0.1) 0%,
      var(--color-light-green) 100%
    ),
    url(/noise.svg); */
  background: url(/noise.svg);
  opacity: 0.4;
  mix-blend-mode: overlay;
  pointer-events: none;
}


================================================
FILE: app/layout.tsx
================================================
import { Analytics } from "@vercel/analytics/react";
import { SpeedInsights } from "@vercel/speed-insights/next";
import type { Metadata } from "next";
import localFont from "next/font/local";
import "./globals.css";
import { cn } from "@/lib/utils";
import { ThemeProvider } from "next-themes";
import { PersonJsonLd, WebSiteJsonLd } from "@/components/json-ld";

// const monument = localFont({
//   src: [
//     {
//       path: "../public/font/ABCMonumentGrotesk-Medium-Trial.otf",
//       weight: "500",
//       style: "medium",
//     },
//     {
//       path: "../public/font/ABCMonumentGrotesk-Regular-Trial.otf",
//       weight: "400",
//       style: "regular",
//     },
//   ],
// });

// const helvetica = localFont({
//   src: [
//     {
//       path: "../public/font/HelveticaNeueBold.ttf",
//       weight: "700",
//       style: "bold",
//     },
//     {
//       path: "../public/font/HelveticaNeue-Medium.otf",
//       weight: "500",
//       style: "medium",
//     },
//     {
//       path: "../public/font/HelveticaNeue-Roman.otf",
//       weight: "400",
//       style: "regular",
//     },
//   ],
// });

const chico = localFont({
  src: [
    {
      path: "../public/font/PPNeueMontreal-SemiBold.woff2",
      weight: "500",
      style: "medium",
    },
    {
      path: "../public/font/PPNeueMontreal-Medium.woff2",
      weight: "400",
      style: "regular",
    },
  ],
});

export const metadata: Metadata = {
  title: {
    template: "%s / Mitul Shah",
    default: "Mitul Shah / @typicalmitul",
  },
  description: "Design engineer, photographer, and a bit more.",
  openGraph: {
    title: "Mitul Shah",
    description: "Design engineer, photographer, and a bit more.",
    images: "/og-2.png",
    url: "https://mitul.ca",
  },
  twitter: {
    card: "summary_large_image",
    site: "@typicalmitul",
    creator: "@typicalmitul",
  },
  alternates: {
    canonical: "https://mitul.ca",
    types: {
      "application/rss+xml": "https://mitul.ca/feed.xml",
    },
  },
};

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en" suppressHydrationWarning>
      <body className={cn(chico.className)}>
        <PersonJsonLd />
        <WebSiteJsonLd />
        <ThemeProvider
          themes={["blue", "green", "red"]}
          defaultTheme="blue"
          storageKey="ms-theme"
        >
          {children}
          <Analytics />
          <SpeedInsights />
        </ThemeProvider>
      </body>
    </html>
  );
}


================================================
FILE: app/llms.txt/route.ts
================================================
import { experiences } from "@/content";
import { readdirSync } from "fs";
import { join } from "path";

const BASE_URL = "https://mitul.ca";

function getBlogSlugs(): string[] {
  const postsDir = join(process.cwd(), "app/(without-root-layout)/p");
  try {
    return readdirSync(postsDir, { withFileTypes: true })
      .filter((entry) => entry.isDirectory())
      .map((entry) => entry.name);
  } catch {
    return [];
  }
}

function generateLlmsTxt(): string {
  const blogSlugs = getBlogSlugs();

  return `# Mitul Shah

> Personal portfolio and blog of Mitul Shah, a design engineer and photographer based in New York City (originally from Toronto, Canada).

## About

Mitul is a self-taught design engineer currently working at Vercel. He crafts memorable interfaces with deep attention to detail and dedicates time to continuous learning. He's also a professional photographer specializing in music photography, with notable achievements including being a personal photographer for the Uber CEO and having photos featured as wallpapers on Google devices.

## Contact

- Email: mitulxshah@gmail.com
- Twitter/X: @typicalmitul
- Instagram: @typicalmitul
- GitHub: mitul-s

## Pages

- [Home](${BASE_URL}): Main portfolio with experience, projects, photography, and contact info
- [About](${BASE_URL}/about): Personal background, beliefs, and bucket list
- [Guestbook](${BASE_URL}/visitors): Interactive guestbook for visitors to leave notes
- [Photography Portfolio](https://typicalmitul.com): External photography portfolio site

## Blog Posts

${blogSlugs.map((slug) => `- [${slug}](${BASE_URL}/p/${slug})`).join("\n")}

## Experience

${experiences.map((exp) => `- **${exp.company}** (${exp.range === "Now" ? "Current" : exp.range}): ${exp.role} - ${exp.description}`).join("\n")}

## Projects

- **Daybloom** (https://daybloom.app): A mindful daily journal combining photos and thoughts into calendar view
- **Montreal in Motion** (https://typicalmitul.com/montreal-in-motion): CSS 3D documentation of Montreal's brutalist metro stations
- **Places to Read** (https://placestoread.xyz): Community-submitted parks for reading around the world
`;
}

export function GET() {
  return new Response(generateLlmsTxt(), {
    headers: {
      "Content-Type": "text/plain; charset=utf-8",
      "Cache-Control": "public, max-age=3600, s-maxage=3600",
    },
  });
}


================================================
FILE: app/robots.txt
================================================
User-Agent: *
Allow: /
Disallow: /api/
Disallow: /visitors/login

Sitemap: https://mitul.ca/sitemap.xml


================================================
FILE: app/sitemap.ts
================================================
import type { MetadataRoute } from "next";
import { readdirSync } from "fs";
import { join } from "path";

const BASE_URL = "https://mitul.ca";

function getBlogSlugs(): string[] {
  const postsDir = join(process.cwd(), "app/(without-root-layout)/p");
  try {
    return readdirSync(postsDir, { withFileTypes: true })
      .filter((entry) => entry.isDirectory())
      .map((entry) => entry.name);
  } catch {
    return [];
  }
}

export default function sitemap(): MetadataRoute.Sitemap {
  const blogSlugs = getBlogSlugs();

  return [
    {
      url: BASE_URL,
      lastModified: new Date(),
      changeFrequency: "monthly",
      priority: 1,
    },
    {
      url: `${BASE_URL}/about`,
      lastModified: new Date(),
      changeFrequency: "monthly",
      priority: 0.8,
    },
    {
      url: `${BASE_URL}/visitors`,
      lastModified: new Date(),
      changeFrequency: "weekly",
      priority: 0.6,
    },
    ...blogSlugs.map((slug) => ({
      url: `${BASE_URL}/p/${slug}`,
      lastModified: new Date(),
      changeFrequency: "yearly" as const,
      priority: 0.7,
    })),
  ];
}


================================================
FILE: atoms/guestbook.tsx
================================================
import { atom } from "jotai";
import { atomWithStorage } from "jotai/utils";

export type Entry = {
  // biome-ignore lint/suspicious/noExplicitAny: <explanation>
  initialX?: any;
  // biome-ignore lint/suspicious/noExplicitAny: <explanation>
  initialY?: any;
  id: string;
  created_by: string;
  local_entry_id?: string;
  body: string;
  signature: string;
  approved?: boolean;
  last_modified?: string;
};

export const serverEntriesAtom = atom<Entry[]>([]);
export const localEntriesAtom = atomWithStorage<Entry[]>(
  "localGuestbookEntries",
  []
);

export const hasCreatedEntryBeforeAtom = atomWithStorage(
  "hasCreatedEntryBefore",
  false
);
export const localCreatedByIdAtom = atomWithStorage("localCreatedById", "");

export const allEntriesAtom = atom((get) => {
  const serverEntries = get(serverEntriesAtom);
  const localEntries = get(localEntriesAtom);
  return [...localEntries, ...serverEntries];
});


================================================
FILE: components/collapsible.tsx
================================================
"use client";

import { CaretRight } from "@phosphor-icons/react";
import * as AccordionPrimitive from "@radix-ui/react-accordion";

//@ts-ignore
import useSound from "use-sound";
import { ArrowUpRight } from "@phosphor-icons/react/dist/ssr/ArrowUpRight";
import { AnimatePresence, motion } from "motion/react";
import { useState } from "react";

interface ExpandingLinkProps {
  link: string;
  company: string;
}

export const ExpandingLink: React.FC<ExpandingLinkProps> = ({
  link,
  company,
}) => {
  const [isHovered, setIsHovered] = useState(false);

  return (
    <motion.a
      className="cursor-pointer text-sm z-[2] flex items-center overflow-clip hover:group-data-[state=open]:bg-accent hover:text-gray-1 transition-colors duration-100 ease-in opacity-0 group-data-[state=open]:opacity-100 group-data-[state=open]:text-accent group-hover:opacity-100 text-gray-1 h-4"
      animate={{ width: isHovered ? "auto" : "20px" }}
      onHoverStart={() => setIsHovered(true)}
      onHoverEnd={() => setIsHovered(false)}
      transition={{
        type: "spring",
        stiffness: 500,
        damping: 30,
        mass: 0.5,
      }}
      href={link}
      target="_blank"
      rel="noopener noreferrer"
      aria-label={`Link to ${company} website`}
      onClick={(e) => e.stopPropagation()}
      layout
    >
      <ArrowUpRight
        aria-hidden={true}
        size={12}
        className="shrink-0 ml-0.5 translate-y-[0.5px]"
      />

      <AnimatePresence>
        {isHovered && (
          <motion.span
            initial={{ opacity: 0, width: 0 }}
            animate={{ opacity: 1, width: "auto" }}
            exit={{ opacity: 0, width: 0 }}
            className="ml-1 whitespace-nowrap text-xs mr-1 peer"
          >
            Visit
          </motion.span>
        )}
      </AnimatePresence>
    </motion.a>
  );
};

const AccordionItem = ({
  role,
  company,
  description,
  range,
  skills,
  link,
}: {
  role: string;
  company: string;
  description: string;
  range: string;
  skills?: string[];
  link?: string;
}) => {
  // Short range for mobile view - not enough space
  const shortRange = range.includes(" - ")
    ? range.split(" - ")[1].trim()
    : range?.trim() || "";

  const [play] = useSound("/sounds/trigger.mp3");

  return (
    <AccordionPrimitive.Item value={company}>
      <AccordionPrimitive.Trigger
        onClick={play}
        className="flex justify-between items-center text-left w-full px-4 py-2 hover:bg-accent hover:text-white data-[state=open]:bg-accent/10 transition-all duration-150 hover:data-[state=open]:text-accent cursor-pointer group"
      >
        <div className="flex items-center gap-x-1">
          <CaretRight
            size={11}
            className="text-accent group-data-[state=open]:rotate-90 transition duration-150 mr-1 group-hover:text-gray-1 group-focus-visible:text-gray-12 group-data-[state=open]:text-accent -ml-1"
            aria-hidden={true}
          />
          <span className="font-medium">{company}</span>
          <span className="whitespace-nowrap">{role}</span>
          {link && <ExpandingLink link={link} company={company} />}
        </div>

        <span className="hidden sm:block ml-auto">{range}</span>
        <span className="sm:hidden ml-auto text-sm">{shortRange}</span>
      </AccordionPrimitive.Trigger>
      <AccordionPrimitive.Content className="overflow-clip transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down data-[state=open]:bg-accent/10">
        <div className="px-4 pb-4">
          <p>{description}</p>
          <div className="flex gap-x-1.5">
            {skills?.map((skill) => {
              return (
                <div
                  className="rounded-sm text-accent/70 text-sm mt-2"
                  key={skill}
                >
                  {skill}
                </div>
              );
            })}
          </div>
        </div>
      </AccordionPrimitive.Content>
    </AccordionPrimitive.Item>
  );
};

const Accordion = ({
  className,
  children,
}: {
  children: React.ReactNode;
  className: string;
}) => {
  return (
    <AccordionPrimitive.Root className={className} type="single" collapsible>
      {children}
    </AccordionPrimitive.Root>
  );
};

export { Accordion, AccordionItem };


================================================
FILE: components/copy-email-button.tsx
================================================
"use client";

import { CopySimple } from "@phosphor-icons/react";
import { AnimatePresence, motion, type Variants } from "motion/react";
import { useState } from "react";

//@ts-ignore
import useSound from "use-sound";

const motionVariants: Variants = {
  initial: { y: 5, opacity: 0 },
  animate: { y: 0, opacity: 1 },
  exit: { y: -5, opacity: 0 },
};

export const CopyEmailButton = () => {
  const [play] = useSound("/sounds/copy.mp3");
  const [copied, setCopied] = useState(false);
  const handleCopy = (text: string) => {
    play();
    setTimeout(() => {
      setCopied(false);
    }, 2000);
    navigator.clipboard.writeText(text);
    setCopied(true);
  };
  return (
    <button
      type="button"
      onClick={() => handleCopy("mitulxshah@gmail.com")}
      className="bg-accent hover:bg-accent/80 transition text-gray-1 py-0.5 pl-1 pr-1.5 rounded-[2px] cursor-pointer text-sm w-[56px]"
    >
      <AnimatePresence mode="wait" initial={false}>
        <motion.div
          className="flex gap-x-1.5 items-center justify-center"
          variants={motionVariants}
          key={copied ? "Copied!" : "Copy"}
          initial="initial"
          animate="animate"
          exit="exit"
          transition={{ duration: 0.05 }}
        >
          {copied ? (
            "Copied!"
          ) : (
            <>
              <CopySimple className="shrink-0" size={12} aria-hidden={true} />
              Copy
            </>
          )}
        </motion.div>
      </AnimatePresence>
    </button>
  );
};

export const CopyEmailButtonAlt = () => {
  const [play] = useSound("/sounds/copy.mp3");
  const [copied, setCopied] = useState(false);
  const handleCopy = (text: string) => {
    play();
    setTimeout(() => {
      setCopied(false);
    }, 2000);
    navigator.clipboard.writeText(text);
    setCopied(true);
  };

  return (
    <button
      type="button"
      className="flex gap-x-1.5 items-center bg-accent hover:bg-accent/80 transition text-gray-1 py-0.5 pl-1.5 pr-1.5 rounded-[2px] cursor-pointer"
      onClick={() => handleCopy("mitulxshah@gmail.com")}
    >
      <AnimatePresence mode="wait" initial={false}>
        <motion.div
          variants={motionVariants}
          key={copied ? "Copied!" : "Copy"}
          initial="initial"
          animate="animate"
          exit="exit"
          transition={{ duration: 0.05 }}
        >
          {copied ? "Copied!" : "Email"}
        </motion.div>
      </AnimatePresence>
    </button>
  );
};


================================================
FILE: components/email-template.tsx
================================================
import type * as React from "react";

interface EmailTemplateProps {
  entry: {
    created_by: string;
    entry: string;
  };
}

export const EmailTemplate: React.FC<EmailTemplateProps> = ({ entry }) => (
  <div>
    <h1>A new submission was made!</h1>
    <p>
      <strong>Name:</strong> {entry.created_by}
    </p>
    <p>
      <strong>Email:</strong> {entry.entry}
    </p>
  </div>
);


================================================
FILE: components/footer/footer-date.tsx
================================================
import { formatDate } from "@/lib/utils";
import Link from "next/link";

const FooterDate = async () => {
  const data = await fetch(
    "https://api.github.com/repos/mitul-s/mitul.ca/commits",
    {
      method: "GET",
      headers: {
        Accept: "application/vnd.github.v3+json",
      },
    }
  ).then((res) => res.json());

  // hack lazy way to bypass rate limit without going through auth
  // to add proper stuff later!
  const lastCommit = !data.message
    ? data.map(
        (commit: { commit: { committer: { date: string } } }) =>
          commit.commit.committer.date
      )[0]
    : "";

  const formattedDate = lastCommit
    ? formatDate(new Date(lastCommit))
    : "2025/01/20";

  return (
    <Link href="https://github.com/mitul-s/mitul.ca" target="_blank">
      {formattedDate}
    </Link>
  );
};

export default FooterDate;


================================================
FILE: components/footer/index.tsx
================================================
import { Suspense } from "react";
import FooterDate from "./footer-date";

const Footer = () => {
  return (
    <footer className="bg-accent text-gray-1 text-[12px] py-24">
      <div className="md:grid grid-cols-[160px_500px_auto]">
        <div className="grid grid-cols-[auto_1fr_auto] gap-x-4 items-center px-4 col-start-2">
          <span>
            Last updated on{" "}
            <Suspense>
              <FooterDate />
            </Suspense>
          </span>
          <div className="h-px bg-gray-2/40" aria-hidden />
          <p className="whitespace-nowrap">With love, from Toronto, Canada.</p>
        </div>
      </div>
    </footer>
  );
};

export default Footer;


================================================
FILE: components/gallery.tsx
================================================
import ScrollArea from "@/components/scroll-area";
import MorphingImageDialog from "@/components/photo";

const Gallery = ({
  photos,
}: {
  photos: {
    src: string;
    alt: string;
  }[];
}) => {
  return (
    <ScrollArea className="relative md:w-full">
      <div className="flex w-full h-full gap-x-2 px-4">
        {photos.map((photo) => (
          <MorphingImageDialog
            key={photo.src}
            src={photo.src}
            alt={photo.alt}
          />
        ))}
      </div>
    </ScrollArea>
  );
};

export default Gallery;


================================================
FILE: components/json-ld.tsx
================================================
export function PersonJsonLd() {
  const jsonLd = {
    "@context": "https://schema.org",
    "@type": "Person",
    name: "Mitul Shah",
    url: "https://mitul.ca",
    image: "https://mitul.ca/og-2.png",
    jobTitle: "Design Engineer",
    worksFor: {
      "@type": "Organization",
      name: "Vercel",
      url: "https://vercel.com",
    },
    description: "Design engineer, photographer, and a bit more.",
    sameAs: [
      "https://twitter.com/typicalmitul",
      "https://instagram.com/typicalmitul",
      "https://github.com/mitul-s",
      "https://typicalmitul.com",
    ],
  };

  return (
    <script
      type="application/ld+json"
      dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
    />
  );
}

export function WebSiteJsonLd() {
  const jsonLd = {
    "@context": "https://schema.org",
    "@type": "WebSite",
    name: "Mitul Shah",
    url: "https://mitul.ca",
    description: "Personal portfolio and blog of Mitul Shah",
    author: {
      "@type": "Person",
      name: "Mitul Shah",
    },
  };

  return (
    <script
      type="application/ld+json"
      dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
    />
  );
}

import { getBlogPost } from "@/lib/blog-posts";

const BASE_URL = "https://mitul.ca";

interface BlogPostJsonLdProps {
  slug: string;
}

export function BlogPostJsonLd({ slug }: BlogPostJsonLdProps) {
  const post = getBlogPost(slug);
  if (!post) return null;

  const jsonLd = {
    "@context": "https://schema.org",
    "@type": "BlogPosting",
    headline: post.title,
    description: post.description,
    url: `${BASE_URL}/p/${post.slug}`,
    image: post.image || `${BASE_URL}/og-2.png`,
    datePublished: post.datePublished,
    dateModified: post.datePublished,
    author: {
      "@type": "Person",
      name: "Mitul Shah",
      url: BASE_URL,
    },
    publisher: {
      "@type": "Person",
      name: "Mitul Shah",
      url: BASE_URL,
    },
  };

  return (
    <script
      type="application/ld+json"
      dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
    />
  );
}


================================================
FILE: components/link-primitive.tsx
================================================
import Link from "next/link";
import { cva } from "class-variance-authority";
import { cn } from "@/lib/utils";

export const link = cva(["flex", "items-center", "gap-x-0.5", "w-fit"], {
  variants: {
    variant: {
      route: [
        "text-gray-11 text-sm hover:bg-accent hover:text-gray-12 px-1.5 py-1 rounded-sm -mx-1.5 font-medium border border-gray-12 hover:border-accent",
      ],
      default: [
        "hover:bg-accent hover:text-gray-1 after:content-[''] after:absolute after:bottom-px after:left-0 after:w-full after:h-px after:bg-accent relative inline-flex",
      ],
    },
    popover: {
      true: ["bg-accent/20 mt-0.5"],
    },
  },
  defaultVariants: {
    variant: "default",
    popover: false,
  },
});

const LinkPrimitive = ({
  href,
  external,
  className,
  variant = "default",
  popover,
  children,
  onClick,
  ...props
}: {
  href: string;
  external?: boolean;
  className?: string;
  variant?: "default" | "route";
  popover?: boolean;
  children: React.ReactNode;
  onClick?: (e: React.MouseEvent<HTMLAnchorElement>) => void;
}) => {
  const Component = external ? "a" : Link;
  return (
    <Component
      className={cn(link({ variant: variant, popover: popover }), className)}
      target={external ? "_blank" : undefined}
      href={href}
      onClick={onClick}
      {...props}
    >
      {children}
    </Component>
  );
};

export default LinkPrimitive;


================================================
FILE: components/morphing-dialog.tsx
================================================
"use client";

import React, {
  useCallback,
  useContext,
  useEffect,
  useId,
  useMemo,
  useRef,
  useState,
} from "react";
import { motion, MotionConfig, AnimatePresence } from "motion/react";
import type { Transition, Variant } from "motion/react";
import { createPortal } from "react-dom";
import { cn } from "@/lib/utils";
import { X as XIcon } from "@phosphor-icons/react";
import useClickOutside from "@/hooks/useClickOutside";
import Image from "next/image";

export type MorphingDialogContextType = {
  isOpen: boolean;
  setIsOpen: React.Dispatch<React.SetStateAction<boolean>>;
  uniqueId: string;
  triggerRef: React.RefObject<HTMLDivElement | null>;
};

const MorphingDialogContext =
  React.createContext<MorphingDialogContextType | null>(null);

function useMorphingDialog() {
  const context = useContext(MorphingDialogContext);
  if (!context) {
    throw new Error(
      "useMorphingDialog must be used within a MorphingDialogProvider"
    );
  }
  return context;
}

export type MorphingDialogProviderProps = {
  children: React.ReactNode;
  transition?: Transition;
};

function MorphingDialogProvider({
  children,
  transition,
}: MorphingDialogProviderProps) {
  const [isOpen, setIsOpen] = useState(false);
  const uniqueId = useId();
  const triggerRef = useRef<HTMLDivElement>(null!);

  const contextValue = useMemo(
    () => ({
      isOpen,
      setIsOpen,
      uniqueId,
      triggerRef,
    }),
    [isOpen, uniqueId]
  );

  return (
    <MorphingDialogContext.Provider value={contextValue}>
      <MotionConfig transition={transition}>{children}</MotionConfig>
    </MorphingDialogContext.Provider>
  );
}

export type MorphingDialogProps = {
  children: React.ReactNode;
  transition?: Transition;
};

function MorphingDialog({ children, transition }: MorphingDialogProps) {
  return (
    <MorphingDialogProvider>
      <MotionConfig transition={transition}>{children}</MotionConfig>
    </MorphingDialogProvider>
  );
}

export type MorphingDialogTriggerProps = {
  children: React.ReactNode;
  className?: string;
  style?: React.CSSProperties;
  triggerRef?: React.RefObject<HTMLDivElement | null>;
};

function MorphingDialogTrigger({
  children,
  className,
  style,
  triggerRef,
}: MorphingDialogTriggerProps) {
  const { setIsOpen, isOpen, uniqueId } = useMorphingDialog();

  const handleClick = useCallback(() => {
    setIsOpen(!isOpen);
  }, [isOpen, setIsOpen]);

  const handleKeyDown = useCallback(
    (event: React.KeyboardEvent) => {
      if (event.key === "Enter" || event.key === " ") {
        event.preventDefault();
        setIsOpen(!isOpen);
      }
    },
    [isOpen, setIsOpen]
  );

  return (
    <motion.div
      ref={triggerRef}
      layoutId={`dialog-${uniqueId}`}
      className={cn("relative cursor-pointer", className)}
      onClick={handleClick}
      onKeyDown={handleKeyDown}
      style={style}
      role="button"
      aria-haspopup="dialog"
      aria-expanded={isOpen}
      aria-controls={`motion-ui-morphing-dialog-content-${uniqueId}`}
      aria-label={`Open dialog ${uniqueId}`}
    >
      {children}
    </motion.div>
  );
}

export type MorphingDialogContentProps = {
  children: React.ReactNode;
  className?: string;
  style?: React.CSSProperties;
};

function MorphingDialogContent({
  children,
  className,
  style,
}: MorphingDialogContentProps) {
  const { setIsOpen, isOpen, uniqueId, triggerRef } = useMorphingDialog();
  const containerRef = useRef<HTMLDivElement>(null!);
  const [firstFocusableElement, setFirstFocusableElement] =
    useState<HTMLElement | null>(null);
  const [lastFocusableElement, setLastFocusableElement] =
    useState<HTMLElement | null>(null);

  useEffect(() => {
    const handleKeyDown = (event: KeyboardEvent) => {
      if (event.key === "Escape") {
        setIsOpen(false);
      }
      if (event.key === "Tab") {
        if (!firstFocusableElement || !lastFocusableElement) return;

        if (event.shiftKey) {
          if (document.activeElement === firstFocusableElement) {
            event.preventDefault();
            lastFocusableElement.focus();
          }
        } else {
          if (document.activeElement === lastFocusableElement) {
            event.preventDefault();
            firstFocusableElement.focus();
          }
        }
      }
    };

    document.addEventListener("keydown", handleKeyDown);

    return () => {
      document.removeEventListener("keydown", handleKeyDown);
    };
  }, [setIsOpen, firstFocusableElement, lastFocusableElement]);

  useEffect(() => {
    if (isOpen) {
      document.body.classList.add("overflow-hidden");
      const focusableElements = containerRef.current?.querySelectorAll(
        'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
      );
      if (focusableElements && focusableElements.length > 0) {
        setFirstFocusableElement(focusableElements[0] as HTMLElement);
        setLastFocusableElement(
          focusableElements[focusableElements.length - 1] as HTMLElement
        );
        (focusableElements[0] as HTMLElement).focus();
      }
    } else {
      document.body.classList.remove("overflow-hidden");
      triggerRef.current?.focus();
    }
  }, [isOpen, triggerRef]);

  useClickOutside(containerRef, () => {
    if (isOpen) {
      setIsOpen(false);
    }
  });

  return (
    <motion.div
      ref={containerRef}
      layoutId={`dialog-${uniqueId}`}
      className={cn("overflow-hidden", className)}
      style={style}
      role="dialog"
      aria-modal="true"
      aria-labelledby={`motion-ui-morphing-dialog-title-${uniqueId}`}
      aria-describedby={`motion-ui-morphing-dialog-description-${uniqueId}`}
    >
      {children}
    </motion.div>
  );
}

export type MorphingDialogContainerProps = {
  children: React.ReactNode;
  className?: string;
  style?: React.CSSProperties;
};

function MorphingDialogContainer({ children }: MorphingDialogContainerProps) {
  const { isOpen, uniqueId } = useMorphingDialog();
  const [mounted, setMounted] = useState(false);

  useEffect(() => {
    setMounted(true);
    return () => setMounted(false);
  }, []);

  if (!mounted) return null;

  return createPortal(
    <AnimatePresence initial={false} mode="sync">
      {isOpen && (
        <>
          <motion.div
            key={`backdrop-${uniqueId}`}
            className="fixed inset-0 h-full w-full bg-white/95 z-50"
            initial={{ opacity: 0 }}
            animate={{ opacity: 1 }}
            exit={{ opacity: 0 }}
          />
          <div className="fixed inset-0 z-50 flex items-center justify-center">
            {children}
          </div>
        </>
      )}
    </AnimatePresence>,
    document.body
  );
}

export type MorphingDialogTitleProps = {
  children: React.ReactNode;
  className?: string;
  style?: React.CSSProperties;
};

function MorphingDialogTitle({
  children,
  className,
  style,
}: MorphingDialogTitleProps) {
  const { uniqueId } = useMorphingDialog();

  return (
    <motion.div
      layoutId={`dialog-title-container-${uniqueId}`}
      className={className}
      style={style}
      layout
    >
      {children}
    </motion.div>
  );
}

export type MorphingDialogSubtitleProps = {
  children: React.ReactNode;
  className?: string;
  style?: React.CSSProperties;
};

function MorphingDialogSubtitle({
  children,
  className,
  style,
}: MorphingDialogSubtitleProps) {
  const { uniqueId } = useMorphingDialog();

  return (
    <motion.div
      layoutId={`dialog-subtitle-container-${uniqueId}`}
      className={className}
      style={style}
    >
      {children}
    </motion.div>
  );
}

export type MorphingDialogDescriptionProps = {
  children: React.ReactNode;
  className?: string;
  disableLayoutAnimation?: boolean;
  variants?: {
    initial: Variant;
    animate: Variant;
    exit: Variant;
  };
};

function MorphingDialogDescription({
  children,
  className,
  variants,
  disableLayoutAnimation,
}: MorphingDialogDescriptionProps) {
  const { uniqueId } = useMorphingDialog();

  return (
    <motion.div
      key={`dialog-description-${uniqueId}`}
      layoutId={
        disableLayoutAnimation
          ? undefined
          : `dialog-description-content-${uniqueId}`
      }
      variants={variants}
      className={className}
      initial="initial"
      animate="animate"
      exit="exit"
      id={`dialog-description-${uniqueId}`}
    >
      {children}
    </motion.div>
  );
}

export type MorphingDialogImageProps = {
  src: string;
  alt: string;
  className?: string;
  style?: React.CSSProperties;
};

const MotionImage = motion(Image);

function MorphingDialogImage({
  src,
  alt,
  className,
  style,
}: MorphingDialogImageProps) {
  const { uniqueId } = useMorphingDialog();

  return (
    <MotionImage
      src={src}
      alt={alt}
      width={1000}
      height={1000}
      className={cn("object-cover object-center w-full h-full", className)}
      layoutId={`dialog-img-${uniqueId}`}
      style={style}
      quality={40}
    />
  );
}

export type MorphingDialogCloseProps = {
  children?: React.ReactNode;
  className?: string;
  variants?: {
    initial: Variant;
    animate: Variant;
    exit: Variant;
  };
};

function MorphingDialogClose({
  children,
  className,
  variants,
}: MorphingDialogCloseProps) {
  const { setIsOpen, uniqueId } = useMorphingDialog();

  const handleClose = useCallback(() => {
    setIsOpen(false);
  }, [setIsOpen]);

  return (
    <motion.button
      onClick={handleClose}
      type="button"
      aria-label="Close dialog"
      key={`dialog-close-${uniqueId}`}
      className={cn("absolute right-6 top-6", className)}
      initial="initial"
      animate="animate"
      exit="exit"
      variants={variants}
    >
      {children || <XIcon size={24} />}
    </motion.button>
  );
}

export {
  MorphingDialog,
  MorphingDialogTrigger,
  MorphingDialogContainer,
  MorphingDialogContent,
  MorphingDialogClose,
  MorphingDialogTitle,
  MorphingDialogSubtitle,
  MorphingDialogDescription,
  MorphingDialogImage,
};


================================================
FILE: components/music-player.tsx
================================================
import getLastPlayed from "@/lib/spotify";
import Image from "next/image";
import { getShelves } from "@/lib/literal";
import NowPlayingClient from "./now-playing-client";
import Link from "next/link";
import { cacheLife } from "next/cache";

const MusicPlayer = async () => {
  "use cache";
  cacheLife("minutes");
  const { data: song } = await getLastPlayed();
  const { reading } = await getShelves();

  return (
    <div className="grid grid-cols-2 gap-x-4 gap-y-4 my-4">
      <NowPlayingClient initial={song} />
      <Link
        href={`https://literal.club/ms/book/${reading.slug}`}
        target="_blank"
        className="flex flex-row items-center gap-x-1.5 w-fit overflow-hidden group"
      >
        <div className="rounded-md border border-gray-6 h-16 w-16 aspect-square relative">
          <Image
            src={reading.cover}
            fill
            alt="Album cover"
            className="object-cover"
            quality={40}
          />
        </div>
        <div className="flex flex-col gap-y-1 justify-center leading-none">
          <span className="font-medium text-accent truncate max-w-20 min-md:max-w-32 group-hover:underline">
            {reading.title}
          </span>
          <span className="text-sm">{reading.author}</span>
        </div>
      </Link>
    </div>
  );
};

export default MusicPlayer;


================================================
FILE: components/now-playing-client.tsx
================================================
"use client";

import useSWR from "swr";
import Image from "next/image";
import Filter from "bad-words";

const fetcher = (url: string) =>
  fetch(url, { cache: "no-store" }).then((r) => r.json());

export default function NowPlayingClient({ initial }: { initial: any }) {
  const { data } = useSWR("/api/spotify", fetcher, {
    refreshInterval: 60000,
    fallbackData: initial,
    revalidateOnFocus: true,
  });

  if (!data) return null;
  const song = data?.data || initial;

  const recent = song.is_playing ? song.item : song.items[0].track;

  if (!recent)
    return (
      <div className="h-16 w-full">
        im probably being rate limited by spotify if u see this
      </div>
    );

  const filter = new Filter();

  const track = {
    title: filter.clean(recent.name),
    artist: recent.artists
      .map((_artist: { name: string }) => _artist.name)
      .shift(),
    songUrl: recent.external_urls.spotify,
    coverArt: recent.album.images[0].url,
    previewUrl: recent.preview_url,
  };

  return (
    <div className="flex flex-row items-center gap-x-1.5 w-fit overflow-hidden">
      <div className="rounded-md border border-gray-6 h-16 w-16 aspect-square relative">
        <Image
          src={track.coverArt}
          fill
          alt="Album cover"
          className="object-cover"
          quality={40}
        />
      </div>
      <div className="flex flex-col gap-y-1 justify-center leading-none">
        <span className="font-medium text-accent">{track.title}</span>
        <span className="text-sm">{track.artist}</span>
      </div>
    </div>
  );
}


================================================
FILE: components/photo.tsx
================================================
"use client";

import {
  MorphingDialog,
  MorphingDialogTrigger,
  MorphingDialogContent,
  MorphingDialogClose,
  MorphingDialogImage,
  MorphingDialogContainer,
} from "@/components/morphing-dialog";
import { X as XIcon } from "@phosphor-icons/react";

export function MorphingImageDialog({
  src,
  alt,
}: {
  src: string;
  alt: string;
}) {
  return (
    <MorphingDialog
      transition={{
        duration: 0.3,
        ease: "easeInOut",
      }}
    >
      <MorphingDialogTrigger className="relative overflow-hidden rounded-4 w-60 h-80 shrink-0 shadow border border-accent/50">
        <MorphingDialogImage
          src={src}
          alt={alt}
          className="object-cover object-center w-full h-full"
        />
      </MorphingDialogTrigger>
      <MorphingDialogContainer>
        <MorphingDialogContent className="relative">
          <MorphingDialogImage
            src={src}
            alt={alt}
            className="h-auto w-full max-w-[90vw] rounded-[4px] object-cover lg:h-[90vh]"
          />
        </MorphingDialogContent>
        <MorphingDialogClose
          className="fixed right-6 top-6 h-fit w-fit rounded-full bg-white p-1"
          variants={{
            initial: { opacity: 0 },
            animate: {
              opacity: 1,
              transition: { delay: 0.3, duration: 0.1 },
            },
            exit: { opacity: 0, transition: { duration: 0 } },
          }}
        >
          <XIcon className="h-5 w-5 text-zinc-500" />
        </MorphingDialogClose>
      </MorphingDialogContainer>
    </MorphingDialog>
  );
}

export default MorphingImageDialog;


================================================
FILE: components/scroll-area.tsx
================================================
"use client";
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area";

export default function ScrollArea({
  children,
  className,
}: {
  children: React.ReactNode;
  className?: string;
}) {
  return (
    <ScrollAreaPrimitive.Root type="always" className={className}>
      <ScrollAreaPrimitive.Viewport className="w-full h-full">
        {children}
        <ScrollAreaPrimitive.Scrollbar
          className="flex select-none -mb-2 w-[calc(100%-32px)] mx-auto rounded-md touch-none bg-gray-6 transition-all duration-100 ease-out data-[orientation=horizontal]:flex-col data-[orientation=horizontal]:h-0.5 hover:data-[orientation=horizontal]:h-1 group"
          orientation="horizontal"
        >
          <ScrollAreaPrimitive.Thumb className="flex-1 bg-accent/50 group-hover:bg-accent rounded-md relative before:content-[''] before:absolute before:top-1/2 before:left-1/2 before:-translate-x-1/2 before:-translate-y-1/2 before:w-full before:h-full before:min-w-[44px] before:min-h-[44px]" />
        </ScrollAreaPrimitive.Scrollbar>
      </ScrollAreaPrimitive.Viewport>
    </ScrollAreaPrimitive.Root>
  );
}


================================================
FILE: components/section.tsx
================================================
import { cn } from "@/lib/utils";

const Section = ({
  heading,
  children,
  className,
}: {
  heading?: string;
  className?: string;
  children: React.ReactNode;
}) => {
  return (
    <div className={cn("p-1 md:p-4", className)}>
      {heading && <h2 className="mb-2 font-medium text-gray-11">{heading}</h2>}
      {children}
    </div>
  );
};

export default Section;


================================================
FILE: components/shader.tsx
================================================
"use client";

import { cn } from "@/lib/utils";
import type React from "react";
import { useEffect, useRef, useState } from "react";

// --- WebGL helpers -----------------------------------------------------------
function createShader(gl: WebGLRenderingContext, type: number, source: string) {
  const shader = gl.createShader(type)!;
  gl.shaderSource(shader, source);
  gl.compileShader(shader);
  if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
    const info = gl.getShaderInfoLog(shader);
    gl.deleteShader(shader);
    throw new Error("Shader compile failed: " + info);
  }
  return shader;
}

function createProgram(
  gl: WebGLRenderingContext,
  vsSource: string,
  fsSource: string
) {
  const vs = createShader(gl, gl.VERTEX_SHADER, vsSource);
  const fs = createShader(gl, gl.FRAGMENT_SHADER, fsSource);
  const program = gl.createProgram()!;
  gl.attachShader(program, vs);
  gl.attachShader(program, fs);
  gl.linkProgram(program);
  if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
    const info = gl.getProgramInfoLog(program);
    gl.deleteProgram(program);
    throw new Error("Program link failed: " + info);
  }
  gl.deleteShader(vs);
  gl.deleteShader(fs);
  return program;
}

// --- Shaders -----------------------------------------------------------------
const VERT = `
attribute vec2 aPos;
varying vec2 vUV;
void main() {
  vUV = aPos * 0.5 + 0.5;
  gl_Position = vec4(aPos, 0.0, 1.0);
}
`;

const FRAG = `
precision highp float;

varying vec2 vUV;
uniform sampler2D uTex;
uniform vec2 uResolution;
uniform vec2 uVideoSize;
uniform float uDotSize;
uniform float uAngle;
uniform vec3 uInkColor;

vec2 coverMap(vec2 uv, vec2 canvasSize, vec2 contentSize) {
  vec2 pos = (uv - 0.5) * canvasSize;
  float s = max(canvasSize.x / contentSize.x, canvasSize.y / contentSize.y);
  vec2 src = pos / s + 0.5 * contentSize;
  return src / contentSize;
}

vec2 rotate2D(vec2 p, float a) {
  float s = sin(a), c = cos(a);
  return mat2(c, -s, s, c) * p;
}

void main() {
  vec2 uv = coverMap(vUV, uResolution, uVideoSize);
  vec3 vid = texture2D(uTex, uv).rgb;

  float lum = dot(vid, vec3(0.299, 0.587, 0.114));

  vec2 p = gl_FragCoord.xy - 0.5 * uResolution;
  p = rotate2D(p, uAngle);
  p += 0.5 * uResolution;

  vec2 g = p / max(uDotSize, 1.0);
  vec2 cell = fract(g) - 0.5;

  float radius = (1.0 - lum) * 0.5;
  float dist = length(cell);

  float feather = 0.35 / max(uDotSize, 1.0);
  float dotMask = 1.0 - smoothstep(radius, radius + feather, dist);

  gl_FragColor = vec4(uInkColor, dotMask);
}
`;

// Fixed color (keeps perf; avoids per-frame style reads)
const INK_COLOR: [number, number, number] = [2 / 255, 16 / 255, 147 / 255];

const prefersReducedMotion = () => {
  if (typeof window === "undefined") return false;
  return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
};

export default function DitherShaderCanvas() {
  const containerRef = useRef<HTMLDivElement | null>(null);
  const canvasRef = useRef<HTMLCanvasElement | null>(null);
  const videoRef = useRef<HTMLVideoElement | null>(null);

  const [fallback, setFallback] = useState(false);
  const [ready, setReady] = useState(false); // <- canvas will fade in after first GPU upload
  const didFirstDrawRef = useRef(false);

  // Tunables
  const DOT_SIZE = 5;
  const ANGLE_DEG = 68;
  const RES_SCALE = 0.75;
  const FPS_CAP_NO_RVFC = 30;

  // GL refs
  const rafRef = useRef<number | null>(null);
  const glRef = useRef<WebGLRenderingContext | null>(null);
  const programRef = useRef<WebGLProgram | null>(null);
  const bufRef = useRef<WebGLBuffer | null>(null);
  const textureRef = useRef<WebGLTexture | null>(null);

  const uTexRef = useRef<WebGLUniformLocation | null>(null);
  const uResolutionRef = useRef<WebGLUniformLocation | null>(null);
  const uVideoSizeRef = useRef<WebGLUniformLocation | null>(null);
  const uDotSizeRef = useRef<WebGLUniformLocation | null>(null);
  const uAngleRef = useRef<WebGLUniformLocation | null>(null);
  const uInkColorRef = useRef<WebGLUniformLocation | null>(null);

  // Render gating
  const hasRVFCRef = useRef(false);
  const needsUploadRef = useRef(false);
  const drawPendingRef = useRef(false);
  const lastTimeRef = useRef(-1);
  const lastDrawMsRef = useRef(0);

  // Visibility gating
  const isVisibleRef = useRef(true);
  const isDocVisibleRef = useRef(
    typeof document !== "undefined" ? !document.hidden : true
  );
  const isActive = () => isVisibleRef.current && isDocVisibleRef.current;

  const scheduleRender = () => {
    if (drawPendingRef.current) return;
    drawPendingRef.current = true;
    rafRef.current = requestAnimationFrame(drawOnce);
  };

  const setSize = () => {
    const canvas = canvasRef.current;
    const gl = glRef.current;
    if (!canvas || !gl) return;

    const dpr = Math.min(window.devicePixelRatio || 1, 2);
    const scale = RES_SCALE;
    const w = Math.max(1, Math.floor(canvas.clientWidth * dpr * scale));
    const h = Math.max(1, Math.floor(canvas.clientHeight * dpr * scale));
    if (canvas.width !== w || canvas.height !== h) {
      canvas.width = w;
      canvas.height = h;
      gl.viewport(0, 0, w, h);
      scheduleRender();
    }
  };

  const drawOnce = () => {
    drawPendingRef.current = false;
    const gl = glRef.current;
    const v = videoRef.current;
    const program = programRef.current;
    const canvas = canvasRef.current;
    if (!gl || !program || !v || !canvas) return;
    if (!isActive()) return;

    if (!hasRVFCRef.current) {
      const now = performance.now();
      const minDelta = 1000 / FPS_CAP_NO_RVFC;
      if (now - lastDrawMsRef.current < minDelta) {
        drawPendingRef.current = true;
        rafRef.current = requestAnimationFrame(drawOnce);
        return;
      }
      lastDrawMsRef.current = now;
    }

    gl.useProgram(program);

    if (uResolutionRef.current)
      gl.uniform2f(uResolutionRef.current, canvas.width, canvas.height);
    const vw = v.videoWidth || 1920;
    const vh = v.videoHeight || 1080;
    if (uVideoSizeRef.current) gl.uniform2f(uVideoSizeRef.current, vw, vh);

    const dpr = Math.min(window.devicePixelRatio || 1, 2);
    if (uDotSizeRef.current)
      gl.uniform1f(uDotSizeRef.current, Math.max(1, DOT_SIZE) * dpr);
    if (uAngleRef.current)
      gl.uniform1f(uAngleRef.current, (ANGLE_DEG * Math.PI) / 180.0);

    // Detect new frames if no RVFC
    if (!hasRVFCRef.current && v.readyState >= 2) {
      const t = v.currentTime;
      if (t !== lastTimeRef.current) {
        needsUploadRef.current = true;
        lastTimeRef.current = t;
      }
    }

    let didUpload = false;
    if (needsUploadRef.current && v.readyState >= 2) {
      const texture = textureRef.current!;
      gl.bindTexture(gl.TEXTURE_2D, texture);
      try {
        gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, v);
        needsUploadRef.current = false;
        didUpload = true;
      } catch {
        setFallback(true);
        return;
      }
    }

    gl.clearColor(0, 0, 0, 0);
    gl.clear(gl.COLOR_BUFFER_BIT);
    gl.drawArrays(gl.TRIANGLES, 0, 3);

    // Mark ready immediately after the first successful GPU upload + draw
    if (didUpload && !didFirstDrawRef.current) {
      didFirstDrawRef.current = true;
      // Defer state change to next macrotask to ensure the first frame is painted
      setTimeout(() => setReady(true), 0);
    }

    if (!hasRVFCRef.current && isActive()) {
      drawPendingRef.current = true;
      rafRef.current = requestAnimationFrame(drawOnce);
    }
  };

  // Init video
  useEffect(() => {
    const v = videoRef.current;
    if (!v) return;
    v.muted = true;
    v.defaultMuted = true;

    const START_TIME = 5;

    v.currentTime = START_TIME;

    const handleEnded = () => {
      v.currentTime = START_TIME;
      v.play().catch(() => {});
    };

    v.addEventListener("ended", handleEnded);

    const tryPlay = () => v.play().catch(() => {});
    tryPlay();

    if (v.readyState >= 2) {
      needsUploadRef.current = true;
      scheduleRender();
    } else {
      const onLoadedData = () => {
        needsUploadRef.current = true;
        tryPlay();
        scheduleRender();
      };
      v.addEventListener("loadeddata", onLoadedData, { once: true });
      return () => {
        v.removeEventListener("loadeddata", onLoadedData);
        v.removeEventListener("ended", handleEnded);
      };
    }

    return () => {
      v.removeEventListener("ended", handleEnded);
    };
  }, []);

  // Main GL setup
  useEffect(() => {
    const canvas = canvasRef.current;
    const video = videoRef.current;
    if (!canvas || !video) return;

    const gl = canvas.getContext("webgl", {
      alpha: true,
      antialias: false,
      depth: false,
      stencil: false,
      premultipliedAlpha: true,
      preserveDrawingBuffer: false,
      desynchronized: true,
      powerPreference: "low-power",
    });
    if (!gl) {
      setFallback(true);
      return;
    }
    glRef.current = gl;

    gl.disable(gl.DITHER);
    gl.disable(gl.DEPTH_TEST);
    gl.disable(gl.CULL_FACE);
    gl.disable(gl.SCISSOR_TEST);
    gl.enable(gl.BLEND);
    gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);

    const program = createProgram(gl, VERT, FRAG);
    programRef.current = program;
    gl.useProgram(program);

    // Full-screen single triangle
    const tri = new Float32Array([-1, -1, 3, -1, -1, 3]);
    const buf = gl.createBuffer();
    bufRef.current = buf;
    gl.bindBuffer(gl.ARRAY_BUFFER, buf);
    gl.bufferData(gl.ARRAY_BUFFER, tri, gl.STATIC_DRAW);
    const aPos = gl.getAttribLocation(program, "aPos");
    gl.enableVertexAttribArray(aPos);
    gl.vertexAttribPointer(aPos, 2, gl.FLOAT, false, 0, 0);

    // Uniforms
    uTexRef.current = gl.getUniformLocation(program, "uTex");
    uResolutionRef.current = gl.getUniformLocation(program, "uResolution");
    uVideoSizeRef.current = gl.getUniformLocation(program, "uVideoSize");
    uDotSizeRef.current = gl.getUniformLocation(program, "uDotSize");
    uAngleRef.current = gl.getUniformLocation(program, "uAngle");
    uInkColorRef.current = gl.getUniformLocation(program, "uInkColor");
    if (uInkColorRef.current)
      gl.uniform3f(
        uInkColorRef.current,
        INK_COLOR[0],
        INK_COLOR[1],
        INK_COLOR[2]
      );

    // Texture
    const texture = gl.createTexture();
    textureRef.current = texture;
    gl.activeTexture(gl.TEXTURE0);
    gl.bindTexture(gl.TEXTURE_2D, texture);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
    if (uTexRef.current) gl.uniform1i(uTexRef.current, 0);
    gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 1);

    // Seed transparent pixel
    const pixel = new Uint8Array([0, 0, 0, 0]);
    gl.texImage2D(
      gl.TEXTURE_2D,
      0,
      gl.RGBA,
      1,
      1,
      0,
      gl.RGBA,
      gl.UNSIGNED_BYTE,
      pixel
    );

    // Size / viewport
    setSize();
    const onResize = () => setSize();
    window.addEventListener("resize", onResize, { passive: true });

    // Prefer requestVideoFrameCallback
    const setupRVFC = () => {
      const vv: any = video;
      if (vv && typeof vv.requestVideoFrameCallback === "function") {
        hasRVFCRef.current = true;
        let id: any;
        const onFrame = () => {
          if (!isActive()) return;
          needsUploadRef.current = true;
          scheduleRender();
          id = vv.requestVideoFrameCallback(onFrame);
        };
        id = vv.requestVideoFrameCallback(onFrame);
        return () => vv.cancelVideoFrameCallback?.(id);
      }
      hasRVFCRef.current = false;
      scheduleRender();
      return () => {};
    };
    const cancelRVFC = setupRVFC();

    const onCanPlay = () => {
      needsUploadRef.current = true;
      video.play().catch(() => {});
      scheduleRender();
    };
    video.addEventListener("canplay", onCanPlay, { once: true });

    const onLost = (e: Event) => {
      e.preventDefault();
      if (rafRef.current) cancelAnimationFrame(rafRef.current);
      setFallback(true);
    };
    canvas.addEventListener("webglcontextlost", onLost as any, {
      passive: false,
    });

    return () => {
      if (rafRef.current) cancelAnimationFrame(rafRef.current);
      window.removeEventListener("resize", onResize);
      video.removeEventListener("canplay", onCanPlay);
      canvas.removeEventListener("webglcontextlost", onLost as any);
      cancelRVFC();

      const gl2 = glRef.current;
      const program2 = programRef.current;
      const buf2 = bufRef.current;
      const texture2 = textureRef.current;
      if (gl2) {
        if (texture2) gl2.deleteTexture(texture2);
        if (buf2) gl2.deleteBuffer(buf2);
        if (program2) gl2.deleteProgram(program2);
      }
      glRef.current = null;
      programRef.current = null;
      bufRef.current = null;
      textureRef.current = null;
    };
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  // Pause when off-screen
  useEffect(() => {
    const root = containerRef.current;
    if (!root) return;

    const obs = new IntersectionObserver(
      (entries) => {
        const entry = entries[0];
        isVisibleRef.current = !!entry?.isIntersecting;
        const v = videoRef.current;
        if (isActive()) {
          v?.play().catch(() => {});
          scheduleRender();
        } else {
          v?.pause();
        }
      },
      { root: null, threshold: 0.01 }
    );
    obs.observe(root);
    return () => obs.disconnect();
  }, []);

  // Pause when tab hidden
  useEffect(() => {
    const onVis = () => {
      isDocVisibleRef.current = !document.hidden;
      const v = videoRef.current;
      if (isActive()) {
        v?.play().catch(() => {});
        scheduleRender();
      } else {
        v?.pause();
      }
    };
    document.addEventListener("visibilitychange", onVis);
    return () => document.removeEventListener("visibilitychange", onVis);
  }, []);

  const reducedMotion = prefersReducedMotion();
  if (reducedMotion) {
    return <></>;
  }

  return (
    <div
      ref={containerRef}
      className="absolute -z-10 inset-0 w-full h-full overflow-hidden pointer-events-none"
    >
      {/* Show the raw video until the canvas has its first uploaded frame (or on fallback) */}
      <video
        ref={videoRef}
        src={
          "https://inqeleafibjx2dzc.public.blob.vercel-storage.com/main/shader-vid.mp4"
        }
        muted
        autoPlay={!reducedMotion}
        // loop
        playsInline
        preload="metadata"
        aria-hidden="true"
        crossOrigin="anonymous"
        className={cn(
          "absolute inset-0 w-full h-full object-cover transition-opacity duration-200 pointer-events-none",
          // TODO tweak? framer motion?
          !ready ? "opacity-100" : "opacity-0",
          fallback ? "opacity-0" : "opacity-0"
        )}
      />

      {/* Fade the shader canvas in only after first draw to avoid flash */}
      <canvas
        ref={canvasRef}
        className={
          fallback
            ? "hidden"
            : `absolute inset-0 w-full h-full block pointer-events-none transition-opacity duration-200 ${
                ready ? "opacity-70" : "opacity-0"
              }`
        }
      />
    </div>
  );
}


================================================
FILE: components/theme-switcher.tsx
================================================
"use client";

import { PaintRoller } from "@phosphor-icons/react";
import { useTheme } from "next-themes";

const ThemeChanger = () => {
  const { theme, setTheme } = useTheme();
  const themes = ["blue", "green", "red"];

  const toggleTheme = () => {
    const currentThemeIndex = themes.indexOf(theme ?? themes[0]);
    const nextThemeIndex = (currentThemeIndex + 1) % themes.length;
    setTheme(themes[nextThemeIndex]);
  };

  return (
    <div>
      <button
        type="button"
        onClick={toggleTheme}
        className="flex gap-x-1.5 items-center bg-accent hover:bg-accent/80 transition text-gray-1 py-0.5 pl-1.5 pr-1.5 rounded-[2px] cursor-pointer h-full"
        aria-label="Change theme"
      >
        <PaintRoller
          className="shrink-0"
          size={12}
          aria-hidden={true}
          weight="fill"
        />
      </button>
    </div>
  );
};

export default ThemeChanger;


================================================
FILE: components/tree-client.tsx
================================================
"use client";

import dynamic from "next/dynamic";

const P5AsciiTree = dynamic(() => import("@/components/tree"), {
  ssr: false,
  loading: () => <div className="w-[900px] h-[720px]" />,
});

const TreeClient = () => {
  return <P5AsciiTree />;
};

export default TreeClient;


================================================
FILE: components/tree.tsx
================================================
// @ts-nocheck

"use client";
import type React from "react";
import { useRef, useEffect, useState } from "react";
import { motion } from "motion/react";
import p5 from "p5";

interface Particle {
  x: number;
  y: number;
  originalX: number;
  originalY: number;
  vx: number;
  vy: number;
}

const P5AsciiTree: React.FC = () => {
  const sketchRef = useRef<HTMLDivElement>(null);
  const [ready, setReady] = useState(false);
  const readyRef = useRef(false);

  useEffect(() => {
    if (!sketchRef.current) return;
    const containerEl = sketchRef.current;
    containerEl.style.visibility = "hidden";
    let signaledReady = false;

    const sketch = (p: p5) => {
      let img: p5.Image;
      const density = "⋅";
      let particles: Particle[] = [];
      const maxSpeed = 0.5;
      const influenceRadius = 60;
      let quadtree: any;
      let prevMouseX: number;
      let prevMouseY: number;

      p.preload = () => {
        img = p.loadImage(
          "https://hebbkx1anhila5yf.public.blob.vercel-storage.com/Tree%20PNG%203498-WZXyiQf0MZdOkGZGNuQvBFOgvMHxgZ.png"
          // "https://inqeleafibjx2dzc.public.blob.vercel-storage.com/Tree%20PNG%20Image-FuCcTHTGIQhPV1Q9zND4BeFX4yuyn2.png"
        );
      };

      class QuadTree {
        boundary: { x: number; y: number; w: number; h: number };
        capacity: number;
        points: Particle[];
        divided: boolean;
        northwest: QuadTree | null;
        northeast: QuadTree | null;
        southwest: QuadTree | null;
        southeast: QuadTree | null;

        constructor(
          boundary: { x: number; y: number; w: number; h: number },
          capacity: number
        ) {
          this.boundary = boundary;
          this.capacity = capacity;
          this.points = [];
          this.divided = false;
          this.northwest = null;
          this.northeast = null;
          this.southwest = null;
          this.southeast = null;
        }

        insert(point: Particle) {
          if (!this.contains(point)) {
            return false;
          }

          if (this.points.length < this.capacity) {
            this.points.push(point);
            return true;
          }

          if (!this.divided) {
            this.subdivide();
          }

          return (
            this.northwest!.insert(point) ||
            this.northeast!.insert(point) ||
            this.southwest!.insert(point) ||
            this.southeast!.insert(point)
          );
        }

        subdivide() {
          const x = this.boundary.x;
          const y = this.boundary.y;
          const w = this.boundary.w / 2;
          const h = this.boundary.h / 2;

          this.northwest = new QuadTree(
            { x: x, y: y, w: w, h: h },
            this.capacity
          );
          this.northeast = new QuadTree(
            { x: x + w, y: y, w: w, h: h },
            this.capacity
          );
          this.southwest = new QuadTree(
            { x: x, y: y + h, w: w, h: h },
            this.capacity
          );
          this.southeast = new QuadTree(
            { x: x + w, y: y + h, w: w, h: h },
            this.capacity
          );

          this.divided = true;
        }

        contains(point: Particle) {
          return (
            point.x >= this.boundary.x &&
            point.x < this.boundary.x + this.boundary.w &&
            point.y >= this.boundary.y &&
            point.y < this.boundary.y + this.boundary.h
          );
        }

        query(range: { x: number; y: number; r: number }, found: Particle[]) {
          if (!this.intersects(range)) {
            return;
          }

          for (let p of this.points) {
            if (this.inCircle(range, p)) {
              found.push(p);
            }
          }

          if (this.divided) {
            this.northwest!.query(range, found);
            this.northeast!.query(range, found);
            this.southwest!.query(range, found);
            this.southeast!.query(range, found);
          }
        }

        intersects(range: { x: number; y: number; r: number }) {
          return !(
            range.x - range.r > this.boundary.x + this.boundary.w ||
            range.x + range.r < this.boundary.x ||
            range.y - range.r > this.boundary.y + this.boundary.h ||
            range.y + range.r < this.boundary.y
          );
        }

        inCircle(range: { x: number; y: number; r: number }, point: Particle) {
          let dx = range.x - point.x;
          let dy = range.y - point.y;
          return dx * dx + dy * dy <= range.r * range.r;
        }
      }

      p.setup = () => {
        p.createCanvas(900, 720);
        p.clear();
        p.textSize(16);
        p.textAlign(p.CENTER, p.CENTER);

        img.loadPixels();
        const stepSize = 6;
        const w = p.width / img.width;
        const h = p.height / img.height;

        // Define the area to exclude (top-left corner)
        const excludeWidth = img.width * 0.2; // Exclude 20% from the left
        const excludeHeight = img.height * 0.2; // Exclude 20% from the top

        for (let i = 0; i < img.width; i += stepSize) {
          for (let j = 0; j < img.height; j += stepSize) {
            // Skip particles in the top-left corner
            if (i < excludeWidth && j < excludeHeight) continue;

            const pixelIndex = (i + j * img.width) * 4;
            const r = img.pixels[pixelIndex];
            const g = img.pixels[pixelIndex + 1];
            const b = img.pixels[pixelIndex + 2];
            const brightness = (r + g + b) / 3;

            if (brightness > 50) {
              particles.push({
                x: i * w,
                y: j * h,
                originalX: i * w,
                originalY: j * h,
                vx: 0,
                vy: 0,
              });
            }
          }
        }

        quadtree = new QuadTree({ x: 0, y: 0, w: p.width, h: p.height }, 4);
        prevMouseX = p.mouseX;
        prevMouseY = p.mouseY;
      };

      p.draw = () => {
        p.clear();

        // Only access document on the client side
        let r = 2,
          g = 16,
          b = 147; // Default fallback values
        if (typeof document !== "undefined") {
          const colorString = getComputedStyle(document.documentElement)
            .getPropertyValue("--color-accent-rgb")
            .trim();

          const colorValues = colorString
            .split(",")
            .map((str) => Number.parseInt(str.trim(), 10));

          if (colorValues.length === 3 && !colorValues.some(isNaN)) {
            [r, g, b] = colorValues;
          }
        }

        p.fill(r, g, b);
        // p.fill(2, 16, 147);
        // p.fill(19, 50, 18);
        // p.fill(178, 0, 36);

        quadtree = new QuadTree({ x: 0, y: 0, w: p.width, h: p.height }, 4);
        for (let particle of particles) {
          quadtree.insert(particle);
        }

        const mouseVelocityX = p.mouseX - prevMouseX;
        const mouseVelocityY = p.mouseY - prevMouseY;

        const range = { x: p.mouseX, y: p.mouseY, r: influenceRadius };
        const found: Particle[] = [];
        quadtree.query(range, found);

        for (let particle of found) {
          const dx = p.mouseX - particle.x;
          const dy = p.mouseY - particle.y;
          const distance = p.sqrt(dx * dx + dy * dy);

          if (distance < influenceRadius) {
            const distanceInfluence = p.map(
              distance,
              0,
              influenceRadius,
              1,
              0,
              true
            );
            const force = maxSpeed * distanceInfluence * 0.1;

            particle.vx = mouseVelocityX * force;
            particle.vy = mouseVelocityY * force;
          }
        }

        for (let particle of particles) {
          particle.x += particle.vx;
          particle.y += particle.vy;

          // Immediate return to original position
          particle.x = p.lerp(particle.x, particle.originalX, 0.1);
          particle.y = p.lerp(particle.y, particle.originalY, 0.1);

          p.text(density, particle.x, particle.y);
        }

        if (!signaledReady) {
          signaledReady = true;
          if (containerEl) containerEl.style.visibility = "visible";
          if (!readyRef.current) {
            readyRef.current = true;
            setReady(true);
          }
        }
        prevMouseX = p.mouseX;
        prevMouseY = p.mouseY;
      };
    };

    const p5Instance = new p5(sketch, sketchRef.current);

    return () => {
      p5Instance.remove();
    };
  }, []);

  return (
    <motion.div
      className="flex items-center justify-center overflow-hidden"
      initial={false}
      animate={{ opacity: ready ? 1 : 0 }}
      exit={{ opacity: 0 }}
      transition={{ duration: 0.6, ease: "easeOut" }}
      style={{ opacity: 0, willChange: "opacity" }}
    >
      <div ref={sketchRef} style={{ opacity: 0.65, visibility: "hidden" }} />
    </motion.div>
  );
};

export default P5AsciiTree;


================================================
FILE: components/twitter-x-loop.tsx
================================================
"use client";

import { useState, useRef, useCallback } from "react";
import { motion, AnimatePresence, type Variants } from "motion/react";
import Link from "next/link";

const motionVariants: Variants = {
  initial: { y: 10, opacity: 0, filter: "blur(2px)" },
  animate: { y: 0, opacity: 1, filter: "blur(0px)" },
  exit: { y: -10, opacity: 0, filter: "blur(2px)" },
};

export default function TwitterXMotion({ className }: { className: string }) {
  const [isX, setIsX] = useState(false);
  const timeoutRef = useRef<NodeJS.Timeout | null>(null);

  const handleMouseEnter = useCallback(() => {
    timeoutRef.current = setTimeout(() => {
      setIsX(true);
    }, 400);
  }, []);

  const handleMouseLeave = useCallback(() => {
    if (timeoutRef.current) {
      clearTimeout(timeoutRef.current);
    }
    setIsX(false);
  }, []);

  return (
    <div
      className={className}
      onMouseEnter={handleMouseEnter}
      onMouseLeave={handleMouseLeave}
    >
      <AnimatePresence mode="wait" initial={false}>
        <motion.div
          className="w-12 font-medium"
          key={isX ? "X" : "Twitter"}
          variants={motionVariants}
          initial="initial"
          animate="animate"
          exit="exit"
          transition={{ duration: 0.1 }}
        >
          {isX ? "X 🫨" : "Twitter"}
        </motion.div>
      </AnimatePresence>
      <Link
        href="https://twitter.com/typicalmitul"
        target="_blank"
        rel="noopener noreferrer"
        className="hover:underline underline-offset-2"
      >
        @typicalmitul
      </Link>
    </div>
  );
}


================================================
FILE: components/video-hover-preview.tsx
================================================
"use client";

import { useState } from "react";
import { cn } from "@/lib/utils";
import LinkPrimitive, { link } from "./link-primitive";
import Link from "next/link";
import Image from "next/image";
import * as HoverCard from "@radix-ui/react-hover-card";
import { track } from "@vercel/analytics/react";

const extractYouTubeId = (url: string): string | null => {
  const patterns = [
    /(?:youtube\.com\/watch\?v=|youtu\.be\/|youtube\.com\/embed\/)([^&\n?#]+)/,
    /youtube\.com\/v\/([^&\n?#]+)/,
  ];

  for (const pattern of patterns) {
    const match = url.match(pattern);
    if (match) return match[1];
  }
  return null;
};

const VideoHoverPreview = ({
  href,
  children,
}: {
  href: string;
  children: React.ReactNode;
}) => {
  const [imageLoaded, setImageLoaded] = useState(true);

  const videoId = extractYouTubeId(href);
  const thumbnailUrl = videoId
    ? `https://img.youtube.com/vi/${videoId}/maxresdefault.jpg`
    : null;

  return (
    <HoverCard.Root openDelay={75} closeDelay={150}>
      <HoverCard.Trigger asChild>
        <LinkPrimitive
          className={cn(link({ variant: "default", popover: false }))}
          external
          href={href}
          onClick={(e) => {
            e.stopPropagation();
            track("video_hover_preview_clicked");
          }}
        >
          {children}
        </LinkPrimitive>
      </HoverCard.Trigger>

      <HoverCard.Portal>
        <HoverCard.Content
          className="rounded-[2px] border border-accent overflow-hidden w-30 z-50 transform-gpu origin-center transition-all 
          data-[side=bottom]:animate-blur-and-slide-up data-[side=top]:animate-blur-and-slide-down data-[state=open]:transition-all hover:cursor-pointer hover:-rotate-3"
          sideOffset={8}
          align="center"
          side="top"
        >
          <Link
            href={href}
            className="block relative aspect-video rounded-[2px]"
            target="_blank"
            rel="noopener noreferrer"
            onClick={(e) => {
              e.stopPropagation();
              track("video_hover_preview_clicked");
            }}
          >
            {thumbnailUrl ? (
              <Image
                src={thumbnailUrl}
                alt={"Thumbnail for Casey Neistat's Doing What I Can't"}
                fill
                className={cn("w-full h-full object-cover")}
                quality={40}
              />
            ) : (
              <div className="w-full h-full bg-gray-3 animate-pulse flex items-center justify-center">
                <div className="text-gray-9 text-sm">Loading thumbnail...</div>
              </div>
            )}
          </Link>
        </HoverCard.Content>
      </HoverCard.Portal>
    </HoverCard.Root>
  );
};

export default VideoHoverPreview;


================================================
FILE: components/video-pause-button.tsx
================================================
"use client";

import { Pause, Play } from "@phosphor-icons/react";
import { useState } from "react";

export default function VideoPauseButton() {
  const [isVideoPaused, setIsVideoPaused] = useState(false);

  const handleToggleVideo = () => {
    setIsVideoPaused(!isVideoPaused);
    const video = document.querySelector(
      'video[src*="shader-vid.mp4"]'
    ) as HTMLVideoElement;
    if (video) {
      if (video.paused) {
        video.play();
      } else {
        video.pause();
      }
    }
  };

  return (
    <button
      onClick={handleToggleVideo}
      className="flex gap-x-1.5 items-center bg-accent hover:bg-accent/80 transition text-gray-1 py-0.5 pl-1.5 pr-1.5 rounded-[2px] cursor-pointer font-medium"
      aria-label="Toggle background video playback"
    >
      {isVideoPaused ? (
        <Play weight="fill" size={12} aria-hidden={true} />
      ) : (
        <Pause weight="fill" size={12} aria-hidden={true} />
      )}
    </button>
  );
}


================================================
FILE: components/visitors/approve-btn.tsx
================================================
"use client";
import { approveGuestbookEntry, declineGuestbookEntry } from "@/app/actions";
import { localEntriesAtom } from "@/atoms/guestbook";
import { useSetAtom } from "jotai";
import { useActionState } from "react";
import { Spinner } from "@phosphor-icons/react";

const ApproveButton = ({ id }: { id: string }) => {
  const setLocalEntries = useSetAtom(localEntriesAtom);
  
  const [approveState, approveAction, approvePending] = useActionState(
    async () => {
      await approveGuestbookEntry(id);
      setLocalEntries((prev) => prev.filter((entry) => entry.id !== id));
    },
    null
  );

  const [declineState, declineAction, declinePending] = useActionState(
    async () => {
      await declineGuestbookEntry(id);
      setLocalEntries((prev) => prev.filter((entry) => entry.id !== id));
    },
    null
  );

  return (
    <div className="flex gap-x-2">
      <form action={approveAction}>
        <button
          className="bg-[#267E5C] rounded-[6px] px-2 py-1 text-gray-1 font-medium mt-2 hover:bg-[#267E5C]/80 disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-1"
          type="submit"
          disabled={approvePending || declinePending}
        >
          {approvePending && <Spinner className="animate-spin" size={16} />}
          Approve
        </button>
      </form>
      <form action={declineAction}>
        <button
          className="bg-[#F74F00] rounded-[6px] px-2 py-1 text-gray-1 font-medium mt-2 hover:bg-[#F74F00]/80 disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-1"
          type="submit"
          disabled={approvePending || declinePending}
        >
          {declinePending && <Spinner className="animate-spin" size={16} />}
          Decline
        </button>
      </form>
    </div>
  );
};

export default ApproveButton;


================================================
FILE: components/visitors/cta.tsx
================================================
"use client";

import { useEffect, useRef, useState } from "react";
import { useFormStatus } from "react-dom";
import useMeasure from "react-use-measure";
import { AnimatePresence, motion, MotionConfig } from "motion/react";
import { cn } from "@/lib/utils";
import useClickOutside from "@/hooks/useClickOutside";
import Signature, { type SignatureRef } from "@uiw/react-signature";
import styles from "./visitors.module.css";
import { ArrowClockwise } from "@phosphor-icons/react";
import { validateAndSaveEntry } from "@/app/(without-root-layout)/visitors/actions";
import Field from "./field";
import { useAtom, useSetAtom } from "jotai";
import {
  hasCreatedEntryBeforeAtom,
  localCreatedByIdAtom,
  localEntriesAtom,
} from "@/atoms/guestbook";

const transition = {
  type: "spring",
  bounce: 0.1,
  duration: 0.25,
} as const;

export default function WriteNoteCTA() {
  const [step, setStep] = useState<number>(0);
  const [contentRef, { height: heightContent }] = useMeasure();
  const [menuRef, { width: widthContainer }] = useMeasure();
  const [maxWidth, setMaxWidth] = useState(0);
  const [formInfo, setFormInfo] = useState({
    created_by: "",
    entry: "",
    signature: "",
  });
  const [isOpen, setIsOpen] = useState(false);
  const [errors, setErrors] = useState<Record<string, string[]> | null>(null);

  const setLocalEntries = useSetAtom(localEntriesAtom);
  const [hasCreatedEntryBefore, setHasCreatedEntryBefore] = useAtom(
    hasCreatedEntryBeforeAtom
  );
  const [localCreatedById, setLocalCreatedById] = useAtom(localCreatedByIdAtom);

  const buttonText = ["Write me a note", "Next", "Submit", "Thanks!"][step];

  const ref = useRef<HTMLDivElement>(null);
  const formRef = useRef<HTMLFormElement>(null);
  const $svg = useRef<SignatureRef>(null);
  const { pending } = useFormStatus();
  const [loading, setLoading] = useState(false);

  const handleCreatedByChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    setFormInfo({
      ...formInfo,
      created_by: e.target.value,
    });
  };

  const handleEntryChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    setFormInfo({
      ...formInfo,
      entry: e.target.value,
    });
  };

  const handleSVGCapture = () => {
    const svgelm = $svg.current?.svg?.cloneNode(true) as SVGSVGElement;
    const clientWidth = $svg.current?.svg?.clientWidth;
    const clientHeight = $svg.current?.svg?.clientHeight;
    svgelm.removeAttribute("style");
    svgelm.setAttribute("width", `${clientWidth}px`);
    svgelm.setAttribute("height", `${clientHeight}px`);
    svgelm.setAttribute("viewbox", `${clientWidth} ${clientHeight}`);
    setFormInfo((prev) => ({
      ...prev,
      signature: svgelm.outerHTML,
    }));
    return svgelm.outerHTML;
  };

  const stepConent = (
    step: number,
    svgRef: React.RefObject<SignatureRef | null>
  ) => {
    switch (step) {
      case 1:
        return (
          <fieldset className="flex flex-col gap-y-4 p-2">
            <Field
              label="ur name, handle, something"
              value={formInfo.created_by}
              name="created_by"
              placeholder="uncle ben"
              onChange={handleCreatedByChange}
            />
            <Field
              label="a sweet likkle note"
              value={formInfo.entry}
              name="entry"
              placeholder="with great power..."
              onChange={handleEntryChange}
            />
          </fieldset>
        );
      case 2:
        return (
          <div className="rounded-6 overflow-hidden bg-gray-1 p-0.5 flex flex-col relative h-36">
            <Signature
              ref={svgRef}
              options={{
                size: 10,
                thinning: 0.25,
              }}
            />
            <input type="hidden" value={formInfo.signature} />
            <button
              aria-label="clear signature"
              className="rounded-[4px] text-gray-11 font-medium self-end absolute bottom-1 left-1 bg-gray-6 p-1 group hover:bg-gray-8 hover:text-gray-12 transition duration-200"
              type="button"
              onClick={() => svgRef.current?.clear()}
            >
              <ArrowClockwise className="group-hover:rotate-180 transition duration-200 " />
            </button>
          </div>
        );
      default:
        return null;
    }
  };

  const validateStep = async (currentStep: number) => {
    setLoading(true);
    if (currentStep === 1) {
      const formData = new FormData();
      formData.append("created_by", formInfo.created_by);
      formData.append("entry", formInfo.entry);

      const result = await validateAndSaveEntry(formData, true);

      if (!result.success) {
        //@ts-ignore
        setErrors(result.errors);
        setLoading(false);
        return false;
      }
      setErrors(null);
      setLoading(false);
      return true;
    }
    setLoading(false);
    return true;
  };

  const handleClick = async () => {
    if (!localCreatedById) setLocalCreatedById(crypto.randomUUID());
    if (step === 3) {
      return;
    }

    if (!isOpen && step === 0) {
      setIsOpen(true);
      setStep(1);
      return;
    }

    if (!isOpen) {
      setIsOpen(true);
      return;
    }

    if (step === 1) {
      const isValid = await validateStep(step);
      if (!isValid) return;
    }

    if (step === 2) {
      setLoading(true);
      const s = handleSVGCapture();
      if (!s) {
        setLoading(false);
        return;
      }

      const formData = new FormData();
      formData.append("local_entry_id", crypto.randomUUID());
      formData.append("created_by", formInfo.created_by);
      formData.append("entry", formInfo.entry);
      formData.append("signature", s);
      formData.append(
        "hasCreatedEntryBefore",
        hasCreatedEntryBefore.toString()
      );
      formData.append("local_created_by_id", localCreatedById);
      await handleSubmit(formData);
      return;
    }

    setStep((prev) => prev + 1);
  };

  const getRandomPosition = (min: number, max: number) =>
    Math.random() * (max - min) + min;

  const handleSubmit = async (formData: FormData) => {
    const result = await validateAndSaveEntry(formData);
    if (!result.success) {
      //@ts-ignore
      setErrors(result.errors);
      setLoading(false);
      return;
    }

    const newEntry = {
      id: crypto.randomUUID(),
      local_entry_id: formData.get("local_entry_id") as string,
      created_by: formData.get("created_by") as string,
      body: formData.get("entry") as string,
      signature: formData.get("signature") as string,
      initialX: getRandomPosition(100, window.innerWidth - 100),
      initialY: getRandomPosition(100, window.innerHeight - 100),
    };
    setLocalEntries((prev) => [newEntry, ...prev]);

    setStep(3);
    setIsOpen(false);
    setLoading(false);
    formRef.current?.reset();
    setHasCreatedEntryBefore(true);
  };

  useClickOutside(ref, () => {
    setIsOpen(false);
  });

  useEffect(() => {
    if (!widthContainer || maxWidth > 0) return;
    setMaxWidth(widthContainer);
  }, [widthContainer, maxWidth]);

  return (
    <div className="bottom-10 left-1/2 -translate-x-1/2 absolute z-300">
      <div
        className={cn(
          "rounded-6 bg-[#F04F1F] transition text-[1.5rem] flex gap-x-1.5 items-center justify-center text-gray-1 font-semibold h-fit w-72",
          styles.homeBtn
        )}
      >
        <MotionConfig transition={transition}>
          <div className="h-full w-full" ref={ref}>
            <form ref={formRef}>
              <div className="overflow-hidden w-full">
                <AnimatePresence initial={false} mode="sync">
                  {isOpen ? (
                    <motion.div
                      key="content"
                      initial={{ height: 0 }}
                      animate={{ height: heightContent || 0 }}
                      exit={{ height: 0 }}
                      style={{
                        width: maxWidth,
                      }}
                    >
                      <div ref={contentRef} className="w-full">
                        <motion.div
                          key={"notes"}
                          initial={{ opacity: 0 }}
                          animate={{ opacity: isOpen ? 1 : 0 }}
                          exit={{ opacity: 0 }}
                        >
                          <div
                            className={cn(
                              "px-2 pt-2 text-sm",
                              isOpen ? "block" : "hidden"
                            )}
                          >
                            <AnimatePresence>
                              {step === 1 && (
                                <motion.div
                                  className={cn(
                                    "absolute -top-[4.5rem] w-full left-0 bg-[#101B1D] text-[1rem] rounded-6 shadow-lg px-4 py-2 font-medium text-center transition",
                                    errors
                                      ? "ring-2 ring-[red]/60"
                                      : "text-gray-1"
                                  )}
                                  style={{
                                    textWrap: "balance",
                                  }}
                                  initial={{ opacity: 0, y: -20 }}
                                  animate={{ opacity: 1, y: 0 }}
                                  exit={{
                                    opacity: 0,
                                    y: -20,
                                    transition: {
                                      duration: 0.1,
                                    },
                                  }}
                                  transition={{
                                    type: "spring",
                                    duration: 0.05,
                                    bounce: 0.02,
                                    restDelta: 0.01,
                                  }}
                                >
                                  <AnimatePresence mode="wait" initial={false}>
                                    <motion.p
                                      key={
                                        errors?.created_by || errors?.entry
                                          ? "error"
                                          : "default"
                                      }
                                      initial={{ opacity: 0, y: -10 }}
                                      animate={{ opacity: 1, y: 0 }}
                                      exit={{ opacity: 0, y: 10 }}
                                      transition={{ duration: 0.05 }}
                                    >
                                      {errors?.created_by || errors?.entry
                                        ? errors?.created_by || errors?.entry
                                        : `tnx for visiting! leave ur name and a note if u
                                want... <3`}
                                    </motion.p>
                                  </AnimatePresence>
                                </motion.div>
                              )}
                              {step === 2 && (
                                <motion.div
                                  className="absolute -top-[4.5rem] w-full left-0 bg-[#101B1D] text-[1rem] rounded-6 shadow-lg px-4 py-2 font-medium text-center transition"
                                  style={{
                                    textWrap: "balance",
                                  }}
                                  initial={{ opacity: 0, y: -20 }}
                                  animate={{ opacity: 1, y: 0 }}
                                  exit={{
                                    opacity: 0,
                                    y: -20,
                                    transition: {
                                      duration: 0.1,
                                    },
                                  }}
                                >
                                  why not a little drawing as well!{" "}
                                  <span>
                                    be creative!! no more smiley faces
                                  </span>
                                </motion.div>
                              )}
                            </AnimatePresence>
                            {stepConent(step, $svg)}
                          </div>
                        </motion.div>
                      </div>
                    </motion.div>
                  ) : null}
                </AnimatePresence>
              </div>

              <button
                ref={menuRef}
                aria-label={"notes"}
                className={cn(
                  "relative flex py-4 w-full shrink-0 scale-100 select-none appearance-none items-center justify-center transition focus-visible:ring-2 active:scale-[0.98] lowercase",
                  loading ? "cursor-not-allowed opacity-50" : "cursor-pointer"
                )}
                type="button"
                disabled={pending || loading}
                onClick={handleClick}
              >
                {isOpen || step === 3 ? buttonText : "write me a note"}
              </button>
            </form>
          </div>
        </MotionConfig>
      </div>
    </div>
  );
}


================================================
FILE: components/visitors/drag.tsx
================================================
"use client";

import { useState } from "react";
import useMaxZIndex from "@/hooks/useMaxZIndex";
import { cn, getRandomRotation } from "@/lib/utils";
import { motion, type PanInfo, useAnimation } from "motion/react";
import React from "react";

const Drag = React.memo(
  ({
    children,
    className,
    initialX,
    initialY,
    ...props
  }: {
    children: React.ReactNode;
    className?: string;
    initialX?: number;
    initialY?: number;
  }) => {
    const [zIndex, updateZIndex] = useMaxZIndex();
    const controls = useAnimation();
    const r = getRandomRotation();
    const [initialRotate] = useState(r);
    const [x, y] = [
      initialX ?? Math.floor(Math.random() * 1300),
      initialY ?? Math.floor(Math.random() * 900),
    ];

    const handleDragEnd = (event: MouseEvent, info: PanInfo) => {
      const direction = info.offset.x > 0 ? 1 : -1;
      const velocity = Math.min(Math.abs(info.velocity.x), 1);
      controls.start({
        rotate: Math.floor(initialRotate + velocity * 20 * direction),
        transition: {
          type: "spring",
          duration: 1,
          stiffness: 50,
          damping: 30,
          mass: 1,
          restDelta: 0.001,
        },
      });
    };

    return (
      <motion.div
        drag
        dragElastic={0.2}
        className={cn(
          "select-none w-fit h-fit drag-elements absolute",
          className
        )}
        dragTransition={{ power: 0.2, timeConstant: 200 }}
        onMouseDown={updateZIndex}
        onTouchStart={updateZIndex}
        onDragEnd={handleDragEnd}
        animate={controls}
        initial={{
          rotate: r,
          x,
          y,
        }}
        style={{
          zIndex,
        }}
        {...props}
      >
        {children}
      </motion.div>
    );
  }
);

export default Drag;


================================================
FILE: components/visitors/field.tsx
================================================
"use client";

import { cn } from "@/lib/utils";
import { useId } from "react";
import styles from "./visitors.module.css";

interface FieldProps {
  value: string;
  label: string;
  name: string;
  placeholder?: string;
  onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
}

const Field = ({
  value,
  label,
  onChange,
  name,
  placeholder,
  ...props
}: FieldProps) => {
  const id = useId();

  return (
    <div className="flex flex-col gap-y-1">
      <label className="font-medium text-[14px]" htmlFor={id}>
        {label}
      </label>
      <input
        type="text"
        name={name}
        id={id}
        placeholder={placeholder}
        required
        autoComplete="off"
        autoCorrect="off"
        className={cn(
          "bg-[#101B1D]/30 focus:bg-gray-1 transition-all focus:placeholder:text-gray-9 text-[16px] outline-hidden text-gray-2 focus:text-gray-12 font-normal rounded-[6px] p-3 w-full placeholder:text-white/40 ",
          styles.input
        )}
        onChange={onChange}
        value={value}
        {...props}
      />
    </div>
  );
};

export default Field;


================================================
FILE: components/visitors/guestbook-entries.tsx
================================================
"use client";

import { useEffect } from "react";
import { useAtom } from "jotai";
import Note from "@/components/visitors/note";
import {
  allEntriesAtom,
  localEntriesAtom,
  serverEntriesAtom,
} from "@/atoms/guestbook";
import { getGuestbookEntries } from "@/app/(without-root-layout)/visitors/actions";

function GuestbookEntries() {
  const [allEntries] = useAtom(allEntriesAtom);
  const [, setServerEntries] = useAtom(serverEntriesAtom);
  const [, setLocalEntries] = useAtom(localEntriesAtom);

  useEffect(() => {
    const fetchEntries = async () => {
      const entries = await getGuestbookEntries();
      // @ts-ignore
      setServerEntries(entries);

      // if entries contains an approved entry that matches an entry in localEntries, remove it
      setLocalEntries((prev) =>
        prev.filter(
          (localEntry) =>
            !entries.some(
              (entry) =>
                (entry.local_entry_id === localEntry.local_entry_id &&
                  entry.approved) ||
                (entry.signature === localEntry.signature && entry.approved)
            )
        )
      );
    };

    fetchEntries();
  }, [setServerEntries, setLocalEntries]);

  return allEntries.map((entry) => (
    <Note
      key={entry.id}
      name={entry.created_by}
      content={entry.body}
      signature={entry.signature}
      initialX={entry.initialX}
      initialY={entry.initialY}
    />
  ));
}

export default GuestbookEntries;


================================================
FILE: components/visitors/note.tsx
================================================
"use client";

import { cn } from "@/lib/utils";
import styles from "./visitors.module.css";
import Image from "next/image";
import Drag from "./drag";
import React from "react";
import { motion } from "motion/react";

const Note = React.memo(
  ({
    name,
    content,
    signature,
    initialX,
    initialY,
  }: {
    name: string;
    content: string;
    signature: string;
    initialX?: number;
    initialY?: number;
  }) => {
    return (
      <Drag
        className={cn("z-10 max-w-[200px]")}
        initialX={initialX}
        initialY={initialY}
      >
        <motion.div
          initial={{ opacity: 0, y: 2 }}
          animate={{ opacity: 1, y: 0 }}
          transition={{ duration: 0.3, ease: "easeOut" }}
          className={cn(
            "bg-gray-1 text-gray-12 w-fit max-w-[165px] scale-75! px-1.5 pt-1.5 pb-2 transition-shadow duration-300 ease-out hover:shadow-md note-item",
            styles.note
          )}
        >
          {signature ? (
            <div
              className={cn(
                "border border-gray-6 bg-gray-3 rounded-[4px] flex items-center justify-center overflow-hidden relative"
              )}
            >
              <div
                className="object-contain z-10"
                // biome-ignore lint/security/noDangerouslySetInnerHtml: <explanation>
                dangerouslySetInnerHTML={{ __html: signature }}
              />
              <Image
                src="/images/33.jpeg"
                className="absolute object-cover"
                fill
                draggable={false}
                quality={25}
                alt=""
              />
            </div>
          ) : null}
          <div className="w-full text-sm break-words mt-1.5">
            <span className="text-gray-11 text-[14px] mr-1 font-semibold">
              {name}
            </span>
            <div className="text-[16px] font-medium leading-tight">
              {content}
            </div>
          </div>
        </motion.div>
      </Drag>
    );
  }
);

export default Note;


================================================
FILE: components/visitors/polaroid.tsx
================================================
"use client";

import { cn } from "@/lib/utils";
import styles from "./visitors.module.css";
import React from "react";
import Drag from "./drag";
import Image from "next/image";

const Polaroid = ({ src, alt }: { src: string; alt: string }) => {
  return (
    <Drag
      className={cn(
        "p-1 pb-6 bg-gray-1 rounded-[8px] transition-all duration-300 ease-out hover:shadow-md",
        styles.polaroid
      )}
    >
      <div className="h-full overflow-hidden relative">
        <Image
          src={src}
          alt={alt}
          fill
          sizes="(max-width: 768px) 20vw, 35vw"
          className="max-w-full h-fit object-contain"
          draggable={false}
          quality={5}
        />
      </div>
    </Drag>
  );
};

export default Polaroid;


================================================
FILE: components/visitors/stickers.tsx
================================================
"use client";

import Drag from "./drag";

const NextWordmark = () => {
  return (
    <svg
      xmlns="http://www.w3.org/2000/svg"
      viewBox="0 0 394 79"
      className="w-24 bg-gray-1 rounded-full p-2 border border-gray-6 overflow-visible"
    >
      <title>Next.js Logo</title>
      <path d="M261.919 0.0330722H330.547V12.7H303.323V79.339H289.71V12.7H261.919V0.0330722Z" />
      <path d="M149.052 0.0330722V12.7H94.0421V33.0772H138.281V45.7441H94.0421V66.6721H149.052V79.339H80.43V12.7H80.4243V0.0330722H149.052Z" />
      <path d="M183.32 0.0661486H165.506L229.312 79.3721H247.178L215.271 39.7464L247.127 0.126654L229.312 0.154184L206.352 28.6697L183.32 0.0661486Z" />
      <path d="M201.6 56.7148L192.679 45.6229L165.455 79.4326H183.32L201.6 56.7148Z" />
      <path
        clipRule="evenodd"
        d="M80.907 79.339L17.0151 0H0V79.3059H13.6121V16.9516L63.8067 79.339H80.907Z"
        fillRule="evenodd"
      />
      <path d="M333.607 78.8546C332.61 78.8546 331.762 78.5093 331.052 77.8186C330.342 77.1279 329.991 76.2917 330 75.3011C329.991 74.3377 330.342 73.5106 331.052 72.8199C331.762 72.1292 332.61 71.7838 333.607 71.7838C334.566 71.7838 335.405 72.1292 336.115 72.8199C336.835 73.5106 337.194 74.3377 337.204 75.3011C337.194 75.9554 337.028 76.5552 336.696 77.0914C336.355 77.6368 335.922 78.064 335.377 78.373C334.842 78.6911 334.252 78.8546 333.607 78.8546Z" />
      <path d="M356.84 45.4453H362.872V68.6846C362.863 70.8204 362.401 72.6472 361.498 74.1832C360.585 75.7191 359.321 76.8914 357.698 77.7185C356.084 78.5364 354.193 78.9546 352.044 78.9546C350.079 78.9546 348.318 78.6001 346.75 77.9094C345.182 77.2187 343.937 76.1826 343.024 74.8193C342.101 73.456 341.649 71.7565 341.649 69.7207H347.691C347.7 70.6114 347.903 71.3838 348.29 72.0291C348.677 72.6744 349.212 73.1651 349.895 73.5105C350.586 73.8559 351.38 74.0286 352.274 74.0286C353.243 74.0286 354.073 73.8286 354.746 73.4196C355.419 73.0197 355.936 72.4199 356.296 71.6201C356.646 70.8295 356.831 69.8479 356.84 68.6846V45.4453Z" />
      <path d="M387.691 54.5338C387.544 53.1251 386.898 52.0254 385.773 51.2438C384.638 50.4531 383.172 50.0623 381.373 50.0623C380.11 50.0623 379.022 50.2532 378.118 50.6258C377.214 51.0075 376.513 51.5164 376.033 52.1617C375.554 52.807 375.314 53.5432 375.295 54.3703C375.295 55.061 375.461 55.6608 375.784 56.1607C376.107 56.6696 376.54 57.0968 377.103 57.4422C377.656 57.7966 378.274 58.0874 378.948 58.3237C379.63 58.56 380.313 58.76 380.995 58.9236L384.14 59.6961C385.404 59.9869 386.631 60.3778 387.802 60.8776C388.973 61.3684 390.034 61.9955 390.965 62.7498C391.897 63.5042 392.635 64.413 393.179 65.4764C393.723 66.5397 394 67.7848 394 69.2208C394 71.1566 393.502 72.8562 392.496 74.3285C391.491 75.7917 390.043 76.9369 388.143 77.764C386.252 78.582 383.965 79 381.272 79C378.671 79 376.402 78.6002 374.493 77.8004C372.575 77.0097 371.08 75.8463 370.001 74.3194C368.922 72.7926 368.341 70.9294 368.258 68.7391H374.235C374.318 69.8842 374.687 70.8386 375.314 71.6111C375.95 72.3745 376.78 72.938 377.795 73.3197C378.819 73.6923 379.962 73.8832 381.226 73.8832C382.545 73.8832 383.707 73.6832 384.712 73.2924C385.708 72.9016 386.492 72.3564 387.055 71.6475C387.627 70.9476 387.913 70.1206 387.922 69.1754C387.913 68.312 387.654 67.5939 387.156 67.0304C386.649 66.467 385.948 65.9944 385.053 65.6127C384.15 65.231 383.098 64.8856 381.899 64.5857L378.081 63.6223C375.323 62.9225 373.137 61.8592 371.541 60.4323C369.937 59.0054 369.143 57.115 369.143 54.7429C369.143 52.798 369.678 51.0894 370.758 49.6261C371.827 48.1629 373.294 47.0268 375.148 46.2179C377.011 45.4 379.114 45 381.456 45C383.836 45 385.92 45.4 387.719 46.2179C389.517 47.0268 390.929 48.1538 391.952 49.5897C392.976 51.0257 393.511 52.6707 393.539 54.5338H387.691Z" />
    </svg>
  );
};

const VercelLogo = ({ ...props }) => {
  return (
    <svg
      width="76"
      height="65"
      viewBox="0 0 76 65"
      fill="none"
      xmlns="http://www.w3.org/2000/svg"
      style={{
        shapeRendering: "crispEdges",
        filter: "drop-shadow(0 2px 1px rgba(0,0,0,.1))",
        overflow: "visible",
      }}
      aria-label="Vercel logo as sticker"
      {...props}
    >
      <title>Vercel Logo</title>
      <path
        d="M37.5274 0L75.0548 65H0L37.5274 0Z"
        fill="#000"
        stroke="#fff"
        strokeWidth="4"
        strokeLinejoin="round"
        strokeLinecap="round"
      />
    </svg>
  );
};

const Sticker = ({ children }: { children: React.ReactNode }) => {
  return <Drag className="drop-shadow-xs">{children}</Drag>;
};

export { VercelLogo, Sticker, NextWordmark };


================================================
FILE: components/visitors/visitors.module.css
================================================
.note {
  z-index: 10;
  border: 1px solid rgba(0, 0, 0, 0.1);
  /* box-shadow: 0.3400000035762787px 0.3400000035762787px 0.3400000035762787px
      #1d2d5233,
    0 0 0.3400000035762787px 0.3400000035762787px #1e2d520f; */
  border-radius: 8px;
  backdrop-blur: 6px;
  /* background-color: #0a4a31; */
  box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2), 0 8px 16px rgba(0, 0, 0, 0.2),
    0 16px 32px rgba(0, 0, 0, 0.2);
}

.polaroid {
  width: 125px;
  height: 160px;
  box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2), 0 8px 16px rgba(0, 0, 0, 0.2),
    0 16px 32px rgba(0, 0, 0, 0.2);
}

.polaroid img {
  width: 100%;
  height: 100%;
  border-radius: 4px;
  object-fit: cover;
  object-position: top;
}

.input {
  box-shadow: rgba(0, 0, 0, 0.06) 0px 2px 4px 0px inset;
}

.homeBtn {
  box-shadow: rgba(0, 0, 0, 0.4) 0px 2px 4px,
    rgba(0, 0, 0, 0.3) 0px 7px 13px -3px, rgba(0, 0, 0, 0.2) 0px -3px 0px inset,
    inset 0 1px 0 0 #ffffff52;
}


================================================
FILE: content.ts
================================================
export const experiences = [
  {
    company: "Vercel",
    role: "Design Engineer",
    link: "https://vercel.com/?ref=mitulca",
    range: "Now",
    description: "Building for the future of the web while doing my best work",
  },
  {
    company: "Compound",
    role: "Software Engineer",
    link: "https://compoundplanning.com/?ref=mitulca",
    range: "2022 - 2023",
    description:
      "Built charting components, rebuilt the app navigation and worked on improving UX for advisors to thrive in supporting clients",
    skills: ["React", "TypeScript", "Redux", "CSS-in-JS", "Next.js"],
  },
  {
    company: "Composer",
    link: "https://composer.trade/?ref=mitulca",
    role: "Design Engineer",
    range: "2021 - 2022",
    description:
      "As an early employee, I built out over 90% of the application UI and a scalable and accessible component library",
    skills: ["React", "ClojureScript", "TailwindCSS", "Contentful", "Next.js"],
  },
  // {
  //   company: "Hypercontext",
  //   role: "Product Analyst",
  //   range: "2019 - 2020",
  //   description:
  //     "Designed growth experiments to convert users from free to paid, built the sales operations and email-marketing playbook from the ground up",
  // },
  {
    company: "Uber",
    role: "Operations Intern",
    range: "2018",
    description:
      "Led competitive research for Canada and supported the launch of 30 cities in 1 day through building courier acquisition campaigns",
  },
];

export const Status = {
  none: "none",
  progress: "progress",
  completed: "completed",
} as const;

export const photos = [
  {
    src: "/images/daniel.jpg",
    alt: "R&B artist Daniel Caesar performing at the Scotiabank Arena in Toronto, Canada",
  },
  {
    src: "/images/maggie.jpg",
    alt: "Indie artist Maggie Rogers performing at the Budweiser Stage in Toronto, Canada",
  },
  {
    src: "/images/toronto.jpg",
    alt: "A photo of the CN Tower in Toronto above the clouds with blue skies behind it",
  },
  {
    src: "/images/nyc.jpg",
    alt: "A classic photo of the New York City skyline taken at dusk from the Top of the Rock",
  },
  {
    src: "/images/banff-2.jpg",
    alt: "",
  },
  {
    src: "/images/banff.jpg",
    alt: "",
  },
];

export const bucketList = [
  {
    item: "Travel the world",
    status: Status.completed,
  },
  {
    item: "Visit Iceland",
    status: Status.none,
  },
  {
    item: "Do a backflip in every contintent",
    status: Status.none,
  },
  {
    item: "Go skydiving",
    status: Status.completed,
  },
  {
    item: "Solo backpack across Europe",
    status: Status.completed,
  },
  {
    item: "Photograph an artist at the MSG",
    status: Status.none,
  },
  {
    item: "Open a restaurant",
    status: Status.none,
  },
  {
    item: "Drive across North America",
    status: Status.none,
  },
  {
    item: "Live in New York City",
    status: Status.completed,
  },
  {
    item: "Do a month+ long hike",
    status: Status.none,
  },
  {
    item: "Go on tour with an artist",
    status: Status.none,
  },
  {
    item: "Climb a large mountain",
    status: Status.completed,
  },
  {
    item: "Help my parents retire",
    status: Status.progress,
  },
  {
    item: "Roadtrip with strangers",
    status: Status.completed,
  },
  {
    item: "Host a photo gallery",
    status: Status.progress,
  },
];

export const beliefs = [
  "Seek discomfort",
  "Do difficult things as they are the most rewarding",
  "Anything is possible with discipline",
];


================================================
FILE: hooks/useClickOutside.tsx
================================================
import { RefObject, useEffect } from "react";

function useClickOutside<T extends HTMLElement>(
  ref: RefObject<T | null>,
  handler: (event: MouseEvent | TouchEvent) => void
): void {
  useEffect(() => {
    const handleClickOutside = (event: MouseEvent | TouchEvent) => {
      if (!ref || !ref.current || ref.current.contains(event.target as Node)) {
        return;
      }

      handler(event);
    };

    document.addEventListener("mousedown", handleClickOutside);
    document.addEventListener("touchstart", handleClickOutside);

    return () => {
      document.removeEventListener("mousedown", handleClickOutside);
      document.removeEventListener("touchstart", handleClickOutside);
    };
  }, [ref, handler]);
}

export default useClickOutside;


================================================
FILE: hooks/useMaxZIndex.tsx
================================================
"use client";
import { useState, useCallback } from "react";

const useMaxZIndex = () => {
  const [
Download .txt
gitextract_iezpmlwg/

├── .gitignore
├── LICENSE.txt
├── README.md
├── app/
│   ├── (with-layout)/
│   │   ├── about/
│   │   │   └── page.tsx
│   │   ├── layout.tsx
│   │   ├── page.client.tsx
│   │   └── page.tsx
│   ├── (without-root-layout)/
│   │   ├── layout.tsx
│   │   ├── p/
│   │   │   ├── 2024/
│   │   │   │   ├── page.mdx
│   │   │   │   └── smol-txt.tsx
│   │   │   ├── 2025/
│   │   │   │   └── page.mdx
│   │   │   ├── components/
│   │   │   │   ├── callout.tsx
│   │   │   │   ├── figure.tsx
│   │   │   │   ├── img-container.tsx
│   │   │   │   └── small-text.tsx
│   │   │   ├── layout.tsx
│   │   │   ├── q1-2025/
│   │   │   │   ├── page.mdx
│   │   │   │   └── two-imgs.tsx
│   │   │   ├── q2-2025/
│   │   │   │   └── page.mdx
│   │   │   ├── q3-2025/
│   │   │   │   └── page.mdx
│   │   │   └── substack-embed.module.css
│   │   └── visitors/
│   │       ├── actions.ts
│   │       ├── all/
│   │       │   ├── page.tsx
│   │       │   └── visitors-all.module.css
│   │       ├── gang/
│   │       │   └── page.tsx
│   │       ├── login/
│   │       │   └── page.tsx
│   │       ├── notes.module.css
│   │       └── page.tsx
│   ├── actions.ts
│   ├── api/
│   │   ├── login/
│   │   │   └── route.ts
│   │   ├── md/
│   │   │   └── route.ts
│   │   ├── send/
│   │   │   └── route.ts
│   │   └── spotify/
│   │       └── route.ts
│   ├── feed.xml/
│   │   └── route.ts
│   ├── globals.css
│   ├── layout.tsx
│   ├── llms.txt/
│   │   └── route.ts
│   ├── robots.txt
│   └── sitemap.ts
├── atoms/
│   └── guestbook.tsx
├── components/
│   ├── collapsible.tsx
│   ├── copy-email-button.tsx
│   ├── email-template.tsx
│   ├── footer/
│   │   ├── footer-date.tsx
│   │   └── index.tsx
│   ├── gallery.tsx
│   ├── json-ld.tsx
│   ├── link-primitive.tsx
│   ├── morphing-dialog.tsx
│   ├── music-player.tsx
│   ├── now-playing-client.tsx
│   ├── photo.tsx
│   ├── scroll-area.tsx
│   ├── section.tsx
│   ├── shader.tsx
│   ├── theme-switcher.tsx
│   ├── tree-client.tsx
│   ├── tree.tsx
│   ├── twitter-x-loop.tsx
│   ├── video-hover-preview.tsx
│   ├── video-pause-button.tsx
│   └── visitors/
│       ├── approve-btn.tsx
│       ├── cta.tsx
│       ├── drag.tsx
│       ├── field.tsx
│       ├── guestbook-entries.tsx
│       ├── note.tsx
│       ├── polaroid.tsx
│       ├── stickers.tsx
│       └── visitors.module.css
├── content.ts
├── hooks/
│   ├── useClickOutside.tsx
│   └── useMaxZIndex.tsx
├── lib/
│   ├── blog-posts.ts
│   ├── literal.ts
│   ├── openai.ts
│   ├── redis.ts
│   ├── spotify.ts
│   └── utils.ts
├── mdx-components.tsx
├── microfrontends.json
├── next-env.d.ts
├── next.config.mjs
├── package.json
├── postcss.config.js
├── proxy.ts
├── public/
│   └── font/
│       ├── ABCMonumentGrotesk-Medium-Trial.otf
│       └── ABCMonumentGrotesk-Regular-Trial.otf
└── tsconfig.json
Download .txt
SYMBOL INDEX (123 symbols across 39 files)

FILE: app/(with-layout)/page.client.tsx
  type ProjectProps (line 7) | interface ProjectProps {

FILE: app/(with-layout)/page.tsx
  function Home (line 93) | function Home() {

FILE: app/(without-root-layout)/p/components/figure.tsx
  type FigureProps (line 3) | interface FigureProps {

FILE: app/(without-root-layout)/visitors/actions.ts
  function validateAndSaveEntry (line 23) | async function validateAndSaveEntry(
  function sendEmail (line 50) | async function sendEmail(formData: FormData) {

FILE: app/(without-root-layout)/visitors/all/page.tsx
  constant ITEMS_PER_PAGE (line 12) | const ITEMS_PER_PAGE = 50;
  function Page (line 14) | function Page(
  function PageContent (line 44) | async function PageContent({ searchParams }: { searchParams: Promise<{ p...
  function EntriesList (line 55) | async function EntriesList({ currentPage }: { currentPage: number }) {
  function Pagination (line 84) | function Pagination({
  type ButtonProps (line 149) | interface ButtonProps
  function getGuestbookEntries (line 170) | async function getGuestbookEntries(page: number) {

FILE: app/(without-root-layout)/visitors/gang/page.tsx
  function ProtectedPage (line 8) | function ProtectedPage() {
  function AuthenticatedContent (line 16) | async function AuthenticatedContent() {
  function getColor (line 46) | function getColor(id: string) {
  function GuestbookEntries (line 54) | async function GuestbookEntries() {

FILE: app/(without-root-layout)/visitors/login/page.tsx
  function LoginPage (line 6) | function LoginPage() {

FILE: app/actions.ts
  function saveGuestbookEntry (line 5) | async function saveGuestbookEntry(state: unknown, formData: FormData) {
  function approveGuestbookEntry (line 24) | async function approveGuestbookEntry(id: string) {
  function declineGuestbookEntry (line 32) | async function declineGuestbookEntry(id: string) {

FILE: app/api/login/route.ts
  function POST (line 4) | async function POST(request: Request) {

FILE: app/api/md/route.ts
  constant BASE_URL (line 7) | const BASE_URL = "https://mitul.ca";
  function getHomeMarkdown (line 9) | function getHomeMarkdown(): string {
  function getAboutMarkdown (line 62) | function getAboutMarkdown(): string {
  function getBlogPostMarkdown (line 83) | function getBlogPostMarkdown(slug: string): string | null {
  function getVisitorsMarkdown (line 137) | function getVisitorsMarkdown(): string {
  function GET (line 146) | async function GET(request: NextRequest) {

FILE: app/api/send/route.ts
  function POST (line 6) | async function POST(request: Request) {

FILE: app/api/spotify/route.ts
  function GET (line 5) | async function GET() {

FILE: app/feed.xml/route.ts
  constant BASE_URL (line 3) | const BASE_URL = "https://mitul.ca";
  function escapeXml (line 5) | function escapeXml(text: string): string {
  function generateRssFeed (line 14) | function generateRssFeed(): string {
  function GET (line 47) | function GET() {

FILE: app/layout.tsx
  function RootLayout (line 85) | function RootLayout({

FILE: app/llms.txt/route.ts
  constant BASE_URL (line 5) | const BASE_URL = "https://mitul.ca";
  function getBlogSlugs (line 7) | function getBlogSlugs(): string[] {
  function generateLlmsTxt (line 18) | function generateLlmsTxt(): string {
  function GET (line 59) | function GET() {

FILE: app/sitemap.ts
  constant BASE_URL (line 5) | const BASE_URL = "https://mitul.ca";
  function getBlogSlugs (line 7) | function getBlogSlugs(): string[] {
  function sitemap (line 18) | function sitemap(): MetadataRoute.Sitemap {

FILE: atoms/guestbook.tsx
  type Entry (line 4) | type Entry = {

FILE: components/collapsible.tsx
  type ExpandingLinkProps (line 12) | interface ExpandingLinkProps {

FILE: components/email-template.tsx
  type EmailTemplateProps (line 3) | interface EmailTemplateProps {

FILE: components/json-ld.tsx
  function PersonJsonLd (line 1) | function PersonJsonLd() {
  function WebSiteJsonLd (line 31) | function WebSiteJsonLd() {
  constant BASE_URL (line 54) | const BASE_URL = "https://mitul.ca";
  type BlogPostJsonLdProps (line 56) | interface BlogPostJsonLdProps {
  function BlogPostJsonLd (line 60) | function BlogPostJsonLd({ slug }: BlogPostJsonLdProps) {

FILE: components/morphing-dialog.tsx
  type MorphingDialogContextType (line 20) | type MorphingDialogContextType = {
  function useMorphingDialog (line 30) | function useMorphingDialog() {
  type MorphingDialogProviderProps (line 40) | type MorphingDialogProviderProps = {
  function MorphingDialogProvider (line 45) | function MorphingDialogProvider({
  type MorphingDialogProps (line 70) | type MorphingDialogProps = {
  function MorphingDialog (line 75) | function MorphingDialog({ children, transition }: MorphingDialogProps) {
  type MorphingDialogTriggerProps (line 83) | type MorphingDialogTriggerProps = {
  function MorphingDialogTrigger (line 90) | function MorphingDialogTrigger({
  type MorphingDialogContentProps (line 131) | type MorphingDialogContentProps = {
  function MorphingDialogContent (line 137) | function MorphingDialogContent({
  type MorphingDialogContainerProps (line 219) | type MorphingDialogContainerProps = {
  function MorphingDialogContainer (line 225) | function MorphingDialogContainer({ children }: MorphingDialogContainerPr...
  type MorphingDialogTitleProps (line 257) | type MorphingDialogTitleProps = {
  function MorphingDialogTitle (line 263) | function MorphingDialogTitle({
  type MorphingDialogSubtitleProps (line 282) | type MorphingDialogSubtitleProps = {
  function MorphingDialogSubtitle (line 288) | function MorphingDialogSubtitle({
  type MorphingDialogDescriptionProps (line 306) | type MorphingDialogDescriptionProps = {
  function MorphingDialogDescription (line 317) | function MorphingDialogDescription({
  type MorphingDialogImageProps (line 345) | type MorphingDialogImageProps = {
  function MorphingDialogImage (line 354) | function MorphingDialogImage({
  type MorphingDialogCloseProps (line 376) | type MorphingDialogCloseProps = {
  function MorphingDialogClose (line 386) | function MorphingDialogClose({

FILE: components/now-playing-client.tsx
  function NowPlayingClient (line 10) | function NowPlayingClient({ initial }: { initial: any }) {

FILE: components/photo.tsx
  function MorphingImageDialog (line 13) | function MorphingImageDialog({

FILE: components/scroll-area.tsx
  function ScrollArea (line 4) | function ScrollArea({

FILE: components/shader.tsx
  function createShader (line 8) | function createShader(gl: WebGLRenderingContext, type: number, source: s...
  function createProgram (line 20) | function createProgram(
  constant VERT (line 42) | const VERT = `
  constant FRAG (line 51) | const FRAG = `
  constant INK_COLOR (line 98) | const INK_COLOR: [number, number, number] = [2 / 255, 16 / 255, 147 / 255];
  function DitherShaderCanvas (line 105) | function DitherShaderCanvas() {

FILE: components/tree.tsx
  type Particle (line 9) | interface Particle {
  class QuadTree (line 46) | class QuadTree {
    method constructor (line 56) | constructor(
    method insert (line 70) | insert(point: Particle) {
    method subdivide (line 92) | subdivide() {
    method contains (line 118) | contains(point: Particle) {
    method query (line 127) | query(range: { x: number; y: number; r: number }, found: Particle[]) {
    method intersects (line 146) | intersects(range: { x: number; y: number; r: number }) {
    method inCircle (line 155) | inCircle(range: { x: number; y: number; r: number }, point: Particle) {

FILE: components/twitter-x-loop.tsx
  function TwitterXMotion (line 13) | function TwitterXMotion({ className }: { className: string }) {

FILE: components/video-pause-button.tsx
  function VideoPauseButton (line 6) | function VideoPauseButton() {

FILE: components/visitors/cta.tsx
  function WriteNoteCTA (line 27) | function WriteNoteCTA() {

FILE: components/visitors/field.tsx
  type FieldProps (line 7) | interface FieldProps {

FILE: components/visitors/guestbook-entries.tsx
  function GuestbookEntries (line 13) | function GuestbookEntries() {

FILE: hooks/useClickOutside.tsx
  function useClickOutside (line 3) | function useClickOutside<T extends HTMLElement>(

FILE: lib/blog-posts.ts
  type BlogPost (line 1) | interface BlogPost {
  function getBlogPost (line 43) | function getBlogPost(slug: string): BlogPost | undefined {

FILE: lib/literal.ts
  type Book (line 1) | interface Book {
  constant LITERAL_ENDPOINT (line 8) | const LITERAL_ENDPOINT = "https://api.literal.club/graphql";

FILE: lib/openai.ts
  function moderateText (line 9) | async function moderateText(text: string) {

FILE: lib/spotify.ts
  constant CLIENT_ID (line 1) | const CLIENT_ID = process.env.SPOTIFY_CLIENT_ID;
  constant CLIENT_SECRET (line 2) | const CLIENT_SECRET = process.env.SPOTIFY_CLIENT_SECRET;
  constant REFRESH_TOKEN (line 3) | const REFRESH_TOKEN = process.env.SPOTIFY_REFRESH_TOKEN;
  constant BASIC (line 5) | const BASIC = Buffer.from(`${CLIENT_ID}:${CLIENT_SECRET}`).toString("bas...
  constant NOW_PLAYING_ENDPOINT (line 6) | const NOW_PLAYING_ENDPOINT =
  constant RECENTLY_PLAYED_ENDPOINT (line 8) | const RECENTLY_PLAYED_ENDPOINT =
  constant TOKEN_ENDPOINT (line 10) | const TOKEN_ENDPOINT = "https://accounts.spotify.com/api/token";

FILE: lib/utils.ts
  function cn (line 4) | function cn(...inputs: ClassValue[]) {
  function pick (line 8) | function pick(object: Record<string, any>, keys: string[]) {
  function formatDate (line 23) | function formatDate(date: Date) {

FILE: mdx-components.tsx
  type HeadingProps (line 5) | type HeadingProps = ComponentPropsWithoutRef<"h1">;
  type ParagraphProps (line 6) | type ParagraphProps = ComponentPropsWithoutRef<"p">;
  type ListProps (line 7) | type ListProps = ComponentPropsWithoutRef<"ul">;
  type ListItemProps (line 8) | type ListItemProps = ComponentPropsWithoutRef<"li">;
  type AnchorProps (line 9) | type AnchorProps = ComponentPropsWithoutRef<"a">;
  type BlockquoteProps (line 10) | type BlockquoteProps = ComponentPropsWithoutRef<"blockquote">;
  type ImageProps (line 11) | type ImageProps = ComponentPropsWithoutRef<"img">;
  function useMDXComponents (line 102) | function useMDXComponents(

FILE: proxy.ts
  function parseMediaType (line 4) | function parseMediaType(mt: string): { type: string; quality: number } {
  function shouldServeMarkdown (line 19) | function shouldServeMarkdown(acceptHeader: string | null): boolean {
  constant LLM_USER_AGENTS (line 60) | const LLM_USER_AGENTS = [
  function isLLMAgent (line 73) | function isLLMAgent(userAgent: string | null): boolean {
  function proxy (line 80) | function proxy(request: NextRequest) {
Condensed preview — 89 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (225K chars).
[
  {
    "path": ".gitignore",
    "chars": 403,
    "preview": "# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# dependencies\n/node_modules\n/.pn"
  },
  {
    "path": "LICENSE.txt",
    "chars": 1066,
    "preview": "MIT License\n\nCopyright (c) 2024 Mitul Shah\n\nPermission is hereby granted, free of charge, to any person obtaining a copy"
  },
  {
    "path": "README.md",
    "chars": 1593,
    "preview": "<!-- This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/ne"
  },
  {
    "path": "app/(with-layout)/about/page.tsx",
    "chars": 3701,
    "preview": "import LinkPrimitive from \"@/components/link-primitive\";\nimport { beliefs, bucketList, Status } from \"@/content\";\nimport"
  },
  {
    "path": "app/(with-layout)/layout.tsx",
    "chars": 327,
    "preview": "import type { Viewport } from \"next\";\n\nexport const viewport: Viewport = {\n  themeColor: \"#0F0F0F\",\n};\n\nconst Layout = ("
  },
  {
    "path": "app/(with-layout)/page.client.tsx",
    "chars": 1745,
    "preview": "\"use client\";\n\nimport { Globe, Terminal } from \"@phosphor-icons/react\";\nimport { track } from \"@vercel/analytics\";\nimpor"
  },
  {
    "path": "app/(with-layout)/page.tsx",
    "chars": 10997,
    "preview": "import { Accordion, AccordionItem } from \"@/components/collapsible\";\nimport VideoHoverPreview from \"@/components/video-h"
  },
  {
    "path": "app/(without-root-layout)/layout.tsx",
    "chars": 119,
    "preview": "const Layout = ({ children }: { children: React.ReactNode }) => {\n  return <>{children}</>;\n};\n\nexport default Layout;\n"
  },
  {
    "path": "app/(without-root-layout)/p/2024/page.mdx",
    "chars": 6617,
    "preview": "import Smol from \"./smol-txt\";\nimport Figure from \"./../components/figure\";\nimport { BlogPostJsonLd } from \"@/components"
  },
  {
    "path": "app/(without-root-layout)/p/2024/smol-txt.tsx",
    "chars": 434,
    "preview": "import Link from \"next/link\";\nimport React from \"react\";\n\nconst Smol = () => {\n  return (\n    <p className=\"text-gray-11"
  },
  {
    "path": "app/(without-root-layout)/p/2025/page.mdx",
    "chars": 8701,
    "preview": "import Smol from \"./smol-txt\";\nimport Figure from \"./../components/figure\";\nimport ImageContainer from \"../components/im"
  },
  {
    "path": "app/(without-root-layout)/p/components/callout.tsx",
    "chars": 555,
    "preview": "\"use client\";\n\nimport { ArrowUpRight } from \"@phosphor-icons/react\";\nimport Link from \"next/link\";\n\nconst Callout = ({ c"
  },
  {
    "path": "app/(without-root-layout)/p/components/figure.tsx",
    "chars": 1219,
    "preview": "import Image, { type StaticImageData } from \"next/image\";\n\ninterface FigureProps {\n  src: string | StaticImageData;\n  al"
  },
  {
    "path": "app/(without-root-layout)/p/components/img-container.tsx",
    "chars": 214,
    "preview": "const ImgContainer = ({ children }: { children: React.ReactNode }) => {\n  return (\n    <div className=\"[&_img]:!object-c"
  },
  {
    "path": "app/(without-root-layout)/p/components/small-text.tsx",
    "chars": 192,
    "preview": "const SmallText = ({ children }: { children: React.ReactNode }) => {\n  return (\n    <div className=\"text-gray-11 text-[1"
  },
  {
    "path": "app/(without-root-layout)/p/layout.tsx",
    "chars": 1838,
    "preview": "import type { Viewport } from \"next\";\nimport Link from \"next/link\";\nimport styles from \"./substack-embed.module.css\";\n\ne"
  },
  {
    "path": "app/(without-root-layout)/p/q1-2025/page.mdx",
    "chars": 4825,
    "preview": "import Images from \"./two-imgs\";\nimport Figure from \"../components/figure\";\nimport SmallText from \"../components/small-t"
  },
  {
    "path": "app/(without-root-layout)/p/q1-2025/two-imgs.tsx",
    "chars": 326,
    "preview": "import img from \"./img.jpg\";\nimport img2 from \"./img2.jpg\";\nimport Figure from \"../components/figure\";\n\nconst Images = ("
  },
  {
    "path": "app/(without-root-layout)/p/q2-2025/page.mdx",
    "chars": 3188,
    "preview": "import { BlogPostJsonLd } from \"@/components/json-ld\";\n\nexport const metadata = {\n  title: \"[Quarter Review] Q2/25 / Mit"
  },
  {
    "path": "app/(without-root-layout)/p/q3-2025/page.mdx",
    "chars": 5735,
    "preview": "import ImageContainer from \"../components/img-container\";\nimport Figure from \"../components/figure\";\nimport sunlight fro"
  },
  {
    "path": "app/(without-root-layout)/p/substack-embed.module.css",
    "chars": 445,
    "preview": ".wrapper {\n  position: relative;\n  width: 480px;\n  max-width: 100%;\n  height: 150px;\n  margin: -20px auto 2rem;\n}\n\n.ifra"
  },
  {
    "path": "app/(without-root-layout)/visitors/actions.ts",
    "chars": 1926,
    "preview": "\"use server\";\n\nimport { z } from \"zod\";\nimport { saveGuestbookEntry } from \"@/app/actions\";\nimport { sql } from \"@vercel"
  },
  {
    "path": "app/(without-root-layout)/visitors/all/page.tsx",
    "chars": 5942,
    "preview": "import * as React from \"react\";\nimport { Slot } from \"@radix-ui/react-slot\";\nimport { cva, type VariantProps } from \"cla"
  },
  {
    "path": "app/(without-root-layout)/visitors/all/visitors-all.module.css",
    "chars": 198,
    "preview": ".container {\n  display: flex;\n  align-items: center;\n  justify-content: center;\n}\n\n.container :global(.note-item) {\n  bo"
  },
  {
    "path": "app/(without-root-layout)/visitors/gang/page.tsx",
    "chars": 2707,
    "preview": "import ApproveButton from \"@/components/visitors/approve-btn\";\n\nimport { sql } from \"@vercel/postgres\";\nimport { cookies"
  },
  {
    "path": "app/(without-root-layout)/visitors/login/page.tsx",
    "chars": 858,
    "preview": "\"use client\";\n\nimport { useState } from \"react\";\nimport { useRouter } from \"next/navigation\";\n\nexport default function L"
  },
  {
    "path": "app/(without-root-layout)/visitors/notes.module.css",
    "chars": 4046,
    "preview": ".matContainer {\n  border-radius: 10px;\n  backdrop-filter: blur(10px);\n  box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2), 0 8px "
  },
  {
    "path": "app/(without-root-layout)/visitors/page.tsx",
    "chars": 3936,
    "preview": "import type { Viewport } from \"next\";\nimport { Provider } from \"jotai\";\nimport { cn } from \"@/lib/utils\";\nimport styles "
  },
  {
    "path": "app/actions.ts",
    "chars": 1310,
    "preview": "\"use server\";\nimport { sql } from \"@vercel/postgres\";\nimport { revalidatePath } from \"next/cache\";\n\nexport async functio"
  },
  {
    "path": "app/api/login/route.ts",
    "chars": 410,
    "preview": "import { NextResponse } from \"next/server\";\nimport { cookies } from \"next/headers\";\n\nexport async function POST(request:"
  },
  {
    "path": "app/api/md/route.ts",
    "chars": 5787,
    "preview": "import { NextRequest } from \"next/server\";\nimport { experiences, beliefs, bucketList, Status } from \"@/content\";\nimport "
  },
  {
    "path": "app/api/send/route.ts",
    "chars": 664,
    "preview": "import { EmailTemplate } from \"@/components/email-template\";\nimport { Resend } from \"resend\";\n\nconst resend = new Resend"
  },
  {
    "path": "app/api/spotify/route.ts",
    "chars": 199,
    "preview": "import { NextResponse } from \"next/server\";\nimport { getSpotifyData } from \"@/lib/spotify\";\n\n\nexport async function GET("
  },
  {
    "path": "app/feed.xml/route.ts",
    "chars": 1648,
    "preview": "import { blogPosts } from \"@/lib/blog-posts\";\n\nconst BASE_URL = \"https://mitul.ca\";\n\nfunction escapeXml(text: string): s"
  },
  {
    "path": "app/globals.css",
    "chars": 6504,
    "preview": "@import \"@radix-ui/colors/gray.css\" layer(base);\n@import \"@radix-ui/colors/blue.css\" layer(base);\n\n@import \"tailwindcss\""
  },
  {
    "path": "app/layout.tsx",
    "chars": 2534,
    "preview": "import { Analytics } from \"@vercel/analytics/react\";\nimport { SpeedInsights } from \"@vercel/speed-insights/next\";\nimport"
  },
  {
    "path": "app/llms.txt/route.ts",
    "chars": 2371,
    "preview": "import { experiences } from \"@/content\";\nimport { readdirSync } from \"fs\";\nimport { join } from \"path\";\n\nconst BASE_URL "
  },
  {
    "path": "app/robots.txt",
    "chars": 104,
    "preview": "User-Agent: *\nAllow: /\nDisallow: /api/\nDisallow: /visitors/login\n\nSitemap: https://mitul.ca/sitemap.xml\n"
  },
  {
    "path": "app/sitemap.ts",
    "chars": 1106,
    "preview": "import type { MetadataRoute } from \"next\";\nimport { readdirSync } from \"fs\";\nimport { join } from \"path\";\n\nconst BASE_UR"
  },
  {
    "path": "atoms/guestbook.tsx",
    "chars": 924,
    "preview": "import { atom } from \"jotai\";\nimport { atomWithStorage } from \"jotai/utils\";\n\nexport type Entry = {\n  // biome-ignore li"
  },
  {
    "path": "components/collapsible.tsx",
    "chars": 4318,
    "preview": "\"use client\";\n\nimport { CaretRight } from \"@phosphor-icons/react\";\nimport * as AccordionPrimitive from \"@radix-ui/react-"
  },
  {
    "path": "components/copy-email-button.tsx",
    "chars": 2495,
    "preview": "\"use client\";\n\nimport { CopySimple } from \"@phosphor-icons/react\";\nimport { AnimatePresence, motion, type Variants } fro"
  },
  {
    "path": "components/email-template.tsx",
    "chars": 393,
    "preview": "import type * as React from \"react\";\n\ninterface EmailTemplateProps {\n  entry: {\n    created_by: string;\n    entry: strin"
  },
  {
    "path": "components/footer/footer-date.tsx",
    "chars": 858,
    "preview": "import { formatDate } from \"@/lib/utils\";\nimport Link from \"next/link\";\n\nconst FooterDate = async () => {\n  const data ="
  },
  {
    "path": "components/footer/index.tsx",
    "chars": 688,
    "preview": "import { Suspense } from \"react\";\nimport FooterDate from \"./footer-date\";\n\nconst Footer = () => {\n  return (\n    <footer"
  },
  {
    "path": "components/gallery.tsx",
    "chars": 553,
    "preview": "import ScrollArea from \"@/components/scroll-area\";\nimport MorphingImageDialog from \"@/components/photo\";\n\nconst Gallery "
  },
  {
    "path": "components/json-ld.tsx",
    "chars": 2088,
    "preview": "export function PersonJsonLd() {\n  const jsonLd = {\n    \"@context\": \"https://schema.org\",\n    \"@type\": \"Person\",\n    nam"
  },
  {
    "path": "components/link-primitive.tsx",
    "chars": 1409,
    "preview": "import Link from \"next/link\";\nimport { cva } from \"class-variance-authority\";\nimport { cn } from \"@/lib/utils\";\n\nexport "
  },
  {
    "path": "components/morphing-dialog.tsx",
    "chars": 10051,
    "preview": "\"use client\";\n\nimport React, {\n  useCallback,\n  useContext,\n  useEffect,\n  useId,\n  useMemo,\n  useRef,\n  useState,\n} fro"
  },
  {
    "path": "components/music-player.tsx",
    "chars": 1356,
    "preview": "import getLastPlayed from \"@/lib/spotify\";\nimport Image from \"next/image\";\nimport { getShelves } from \"@/lib/literal\";\ni"
  },
  {
    "path": "components/now-playing-client.tsx",
    "chars": 1598,
    "preview": "\"use client\";\n\nimport useSWR from \"swr\";\nimport Image from \"next/image\";\nimport Filter from \"bad-words\";\n\nconst fetcher "
  },
  {
    "path": "components/photo.tsx",
    "chars": 1621,
    "preview": "\"use client\";\n\nimport {\n  MorphingDialog,\n  MorphingDialogTrigger,\n  MorphingDialogContent,\n  MorphingDialogClose,\n  Mor"
  },
  {
    "path": "components/scroll-area.tsx",
    "chars": 1128,
    "preview": "\"use client\";\nimport * as ScrollAreaPrimitive from \"@radix-ui/react-scroll-area\";\n\nexport default function ScrollArea({\n"
  },
  {
    "path": "components/section.tsx",
    "chars": 376,
    "preview": "import { cn } from \"@/lib/utils\";\n\nconst Section = ({\n  heading,\n  children,\n  className,\n}: {\n  heading?: string;\n  cla"
  },
  {
    "path": "components/shader.tsx",
    "chars": 15504,
    "preview": "\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport type React from \"react\";\nimport { useEffect, useRef, useState } "
  },
  {
    "path": "components/theme-switcher.tsx",
    "chars": 919,
    "preview": "\"use client\";\n\nimport { PaintRoller } from \"@phosphor-icons/react\";\nimport { useTheme } from \"next-themes\";\n\nconst Theme"
  },
  {
    "path": "components/tree-client.tsx",
    "chars": 278,
    "preview": "\"use client\";\n\nimport dynamic from \"next/dynamic\";\n\nconst P5AsciiTree = dynamic(() => import(\"@/components/tree\"), {\n  s"
  },
  {
    "path": "components/tree.tsx",
    "chars": 9023,
    "preview": "// @ts-nocheck\n\n\"use client\";\nimport type React from \"react\";\nimport { useRef, useEffect, useState } from \"react\";\nimpor"
  },
  {
    "path": "components/twitter-x-loop.tsx",
    "chars": 1602,
    "preview": "\"use client\";\n\nimport { useState, useRef, useCallback } from \"react\";\nimport { motion, AnimatePresence, type Variants } "
  },
  {
    "path": "components/video-hover-preview.tsx",
    "chars": 2801,
    "preview": "\"use client\";\n\nimport { useState } from \"react\";\nimport { cn } from \"@/lib/utils\";\nimport LinkPrimitive, { link } from \""
  },
  {
    "path": "components/video-pause-button.tsx",
    "chars": 976,
    "preview": "\"use client\";\n\nimport { Pause, Play } from \"@phosphor-icons/react\";\nimport { useState } from \"react\";\n\nexport default fu"
  },
  {
    "path": "components/visitors/approve-btn.tsx",
    "chars": 1827,
    "preview": "\"use client\";\nimport { approveGuestbookEntry, declineGuestbookEntry } from \"@/app/actions\";\nimport { localEntriesAtom } "
  },
  {
    "path": "components/visitors/cta.tsx",
    "chars": 13483,
    "preview": "\"use client\";\n\nimport { useEffect, useRef, useState } from \"react\";\nimport { useFormStatus } from \"react-dom\";\nimport us"
  },
  {
    "path": "components/visitors/drag.tsx",
    "chars": 1830,
    "preview": "\"use client\";\n\nimport { useState } from \"react\";\nimport useMaxZIndex from \"@/hooks/useMaxZIndex\";\nimport { cn, getRandom"
  },
  {
    "path": "components/visitors/field.tsx",
    "chars": 1122,
    "preview": "\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport { useId } from \"react\";\nimport styles from \"./visitors.module.cs"
  },
  {
    "path": "components/visitors/guestbook-entries.tsx",
    "chars": 1459,
    "preview": "\"use client\";\n\nimport { useEffect } from \"react\";\nimport { useAtom } from \"jotai\";\nimport Note from \"@/components/visito"
  },
  {
    "path": "components/visitors/note.tsx",
    "chars": 2067,
    "preview": "\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport styles from \"./visitors.module.css\";\nimport Image from \"next/ima"
  },
  {
    "path": "components/visitors/polaroid.tsx",
    "chars": 773,
    "preview": "\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport styles from \"./visitors.module.css\";\nimport React from \"react\";\n"
  },
  {
    "path": "components/visitors/stickers.tsx",
    "chars": 4608,
    "preview": "\"use client\";\n\nimport Drag from \"./drag\";\n\nconst NextWordmark = () => {\n  return (\n    <svg\n      xmlns=\"http://www.w3.o"
  },
  {
    "path": "components/visitors/visitors.module.css",
    "chars": 933,
    "preview": ".note {\n  z-index: 10;\n  border: 1px solid rgba(0, 0, 0, 0.1);\n  /* box-shadow: 0.3400000035762787px 0.3400000035762787p"
  },
  {
    "path": "content.ts",
    "chars": 3507,
    "preview": "export const experiences = [\n  {\n    company: \"Vercel\",\n    role: \"Design Engineer\",\n    link: \"https://vercel.com/?ref="
  },
  {
    "path": "hooks/useClickOutside.tsx",
    "chars": 762,
    "preview": "import { RefObject, useEffect } from \"react\";\n\nfunction useClickOutside<T extends HTMLElement>(\n  ref: RefObject<T | nul"
  },
  {
    "path": "hooks/useMaxZIndex.tsx",
    "chars": 682,
    "preview": "\"use client\";\nimport { useState, useCallback } from \"react\";\n\nconst useMaxZIndex = () => {\n  const [zIndex, setZIndex] ="
  },
  {
    "path": "lib/blog-posts.ts",
    "chars": 1050,
    "preview": "export interface BlogPost {\n  slug: string;\n  title: string;\n  description: string;\n  datePublished: string;\n  image?: s"
  },
  {
    "path": "lib/literal.ts",
    "chars": 2716,
    "preview": "interface Book {\n  slug: string;\n  title: string;\n  authors: { name: string }[];\n  cover: string;\n}\n\nconst LITERAL_ENDPO"
  },
  {
    "path": "lib/openai.ts",
    "chars": 437,
    "preview": "\"use server\";\n\nimport OpenAI from \"openai\";\n\nconst openai = new OpenAI({\n  apiKey: process.env.OPENAI_API_KEY,\n});\n\nasyn"
  },
  {
    "path": "lib/redis.ts",
    "chars": 111,
    "preview": "import { Redis } from \"ioredis\";\n\nconst redis = new Redis(process.env.REDIS_URL ?? \"\");\n\nexport default redis;\n"
  },
  {
    "path": "lib/spotify.ts",
    "chars": 3167,
    "preview": "const CLIENT_ID = process.env.SPOTIFY_CLIENT_ID;\nconst CLIENT_SECRET = process.env.SPOTIFY_CLIENT_SECRET;\nconst REFRESH_"
  },
  {
    "path": "lib/utils.ts",
    "chars": 834,
    "preview": "import { clsx, type ClassValue } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\nexport function cn(...inputs: C"
  },
  {
    "path": "mdx-components.tsx",
    "chars": 3133,
    "preview": "import React, { type ComponentPropsWithoutRef } from \"react\";\nimport type { MDXComponents } from \"mdx/types\";\nimport Lin"
  },
  {
    "path": "microfrontends.json",
    "chars": 312,
    "preview": "{\n  \"$schema\": \"https://openapi.vercel.sh/microfrontends.json\",\n  \"applications\": {\n    \"mitul-ca\": {\n      \"development"
  },
  {
    "path": "next-env.d.ts",
    "chars": 251,
    "preview": "/// <reference types=\"next\" />\n/// <reference types=\"next/image-types/global\" />\nimport \"./.next/dev/types/routes.d.ts\";"
  },
  {
    "path": "next.config.mjs",
    "chars": 1371,
    "preview": "import createMDX from \"@next/mdx\";\nimport { withMicrofrontends } from \"@vercel/microfrontends/next/config\";\n\nconst withM"
  },
  {
    "path": "package.json",
    "chars": 1787,
    "preview": "{\n  \"name\": \"mitul-ca\",\n  \"version\": \"0.1.0\",\n  \"private\": true,\n  \"scripts\": {\n    \"dev\": \"next dev\",\n    \"build\": \"nex"
  },
  {
    "path": "postcss.config.js",
    "chars": 71,
    "preview": "module.exports = {\n  plugins: {\n    '@tailwindcss/postcss': {},\n  },\n}\n"
  },
  {
    "path": "proxy.ts",
    "chars": 3249,
    "preview": "import { NextResponse } from \"next/server\";\nimport type { NextRequest } from \"next/server\";\n\nfunction parseMediaType(mt:"
  },
  {
    "path": "tsconfig.json",
    "chars": 695,
    "preview": "{\n  \"compilerOptions\": {\n    \"target\": \"es5\",\n    \"lib\": [\n      \"dom\",\n      \"dom.iterable\",\n      \"esnext\"\n    ],\n    "
  }
]

// ... and 2 more files (download for full content)

About this extraction

This page contains the full source code of the mitul-s/mitul.ca GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 89 files (204.8 KB), approximately 57.4k tokens, and a symbol index with 123 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!