Repository: j471n/j471n.in Branch: main Commit: 7f76fee83374 Files: 177 Total size: 481.1 KB Directory structure: gitextract_2ou2nl1y/ ├── .eslintrc.json ├── .gitignore ├── .vercelignore ├── LICENSE ├── README.md ├── components/ │ ├── Blog.tsx │ ├── BookCard.tsx │ ├── Contact/ │ │ ├── Contact.tsx │ │ ├── ContactForm.tsx │ │ └── index.tsx │ ├── CreateAnIssue.tsx │ ├── EpigraphCard.tsx │ ├── Footer.tsx │ ├── FramerMotion/ │ │ ├── AnimatedDiv.tsx │ │ ├── AnimatedHeading.tsx │ │ └── AnimatedText.tsx │ ├── GitHubActivityGraph.tsx │ ├── Home/ │ │ ├── BlogsSection.tsx │ │ ├── EpigraphsSection.tsx │ │ ├── HeroSection.tsx │ │ ├── SkillSection.tsx │ │ └── StatsSection.tsx │ ├── Instagram/ │ │ ├── InstagramPost.tsx │ │ ├── InstagramPostLoading.tsx │ │ └── InstagramSection.tsx │ ├── MDXComponents/ │ │ ├── Code.tsx │ │ ├── CodeSandbox.tsx │ │ ├── CodeTitle.tsx │ │ ├── Codepen.tsx │ │ ├── Danger.tsx │ │ ├── EmbedBlog.tsx │ │ ├── Figcaption.tsx │ │ ├── LinkedInEmbed.tsx │ │ ├── NextAndPreviousButton.tsx │ │ ├── Pre.tsx │ │ ├── Step.tsx │ │ ├── Tip.tsx │ │ ├── UrlMetaInfo.tsx │ │ ├── Warning.tsx │ │ ├── YouTube.tsx │ │ └── index.tsx │ ├── MetaData.tsx │ ├── MovieCard.tsx │ ├── Newsletter.tsx │ ├── OgImage.tsx │ ├── PageHeader.tsx │ ├── PageNotFound.tsx │ ├── PageTop.tsx │ ├── Project.tsx │ ├── QRCodeContainer.tsx │ ├── SVG/ │ │ ├── Ditto.tsx │ │ ├── Flameshot.tsx │ │ ├── Flux.tsx │ │ ├── Logo.tsx │ │ ├── MicrosoftToDo.tsx │ │ ├── RainDrop.tsx │ │ ├── ShareX.tsx │ │ ├── UPI.tsx │ │ ├── Zip7.tsx │ │ └── index.tsx │ ├── ScrollProgressBar.tsx │ ├── ScrollToTopButton.tsx │ ├── ShareOnSocialMedia.tsx │ ├── SnippetCard.tsx │ ├── SnowfallCanvas.tsx │ ├── StaticPage.tsx │ ├── Stats/ │ │ ├── Artist.tsx │ │ ├── MonkeyTypeStats.tsx │ │ ├── StatsCard.tsx │ │ └── Track.tsx │ ├── Support.tsx │ ├── TableOfContents.tsx │ └── TopNavbar.tsx ├── content/ │ ├── FramerMotionVariants.ts │ ├── meta.ts │ ├── siteConfig.ts │ ├── skillsData.ts │ ├── socialMedia.ts │ ├── support.ts │ ├── user.ts │ └── utilitiesData.ts ├── context/ │ └── darkModeContext.tsx ├── docs/ │ └── sanity-deploy.md ├── hooks/ │ ├── useBookmarkBlogs.ts │ ├── useDebounce.ts │ ├── useFetchWithSWR.ts │ ├── useScrollPercentage.ts │ ├── useShare.ts │ ├── useWindowLocation.ts │ └── useWindowSize.ts ├── layout/ │ ├── BlogLayout.tsx │ ├── Layout.tsx │ └── SnippetLayout.tsx ├── lib/ │ ├── MDXContent.ts │ ├── devto.ts │ ├── fetcher.ts │ ├── generateRSS.ts │ ├── github.ts │ ├── hardcover.ts │ ├── instaposts.ts │ ├── interface/ │ │ └── sanity.ts │ ├── interface.ts │ ├── sanityClient.ts │ ├── sanityContent.ts │ ├── sitemap.ts │ ├── spotify.ts │ ├── supabase.ts │ ├── tmdb.ts │ ├── toc.ts │ ├── types.ts │ └── windowsAnimation.ts ├── next-env.d.ts ├── next.config.js ├── package.json ├── pages/ │ ├── 404.tsx │ ├── _app.tsx │ ├── _document.tsx │ ├── about.tsx │ ├── api/ │ │ ├── books.ts │ │ ├── ga.ts │ │ ├── now-playing.ts │ │ ├── posts/ │ │ │ └── insta.ts │ │ ├── revalidate.ts │ │ ├── stats/ │ │ │ ├── artists.ts │ │ │ ├── devto.ts │ │ │ ├── github-contribution.ts │ │ │ ├── github.ts │ │ │ ├── monkeytype.ts │ │ │ └── tracks.ts │ │ ├── validate/ │ │ │ └── email.ts │ │ └── views/ │ │ ├── [slug].ts │ │ └── index.ts │ ├── blogs/ │ │ ├── [slug].tsx │ │ ├── bookmark.tsx │ │ └── index.tsx │ ├── books.tsx │ ├── certificates.tsx │ ├── epigraphs.tsx │ ├── index.tsx │ ├── privacy.tsx │ ├── projects.tsx │ ├── snippets/ │ │ ├── [slug].tsx │ │ └── index.tsx │ ├── stats.tsx │ ├── tet.json │ └── utilities.tsx ├── postcss.config.js ├── public/ │ ├── manifest.json │ └── robots.txt ├── sanity/ │ ├── .eslintrc │ ├── .gitignore │ ├── README.md │ ├── gist.md.ndjson │ ├── package.json │ ├── sanity.cli.ts │ ├── sanity.config.ts │ ├── schemas/ │ │ ├── author.ts │ │ ├── blockContent.ts │ │ ├── category.ts │ │ ├── epigraph.ts │ │ ├── index.ts │ │ ├── language.ts │ │ ├── organization.ts │ │ ├── post.ts │ │ ├── snippet.ts │ │ └── static_page.ts │ ├── static/ │ │ └── .gitkeep │ ├── tailwind.config.js │ └── tsconfig.json ├── sanity.cli.ts ├── styles/ │ └── globals.css ├── tailwind.config.js ├── tsconfig.json ├── utils/ │ ├── date.ts │ ├── functions.ts │ └── utils.ts └── vercel.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .eslintrc.json ================================================ { "extends": "next/core-web-vitals", "rules": { "react/no-unescaped-entities": 0, "@next/next/no-img-element": "off" } } ================================================ 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 # private markdown meta.md # misc .DS_Store *.pem # debug npm-debug.log* yarn-debug.log* yarn-error.log* # local env files .env.local .env.development.local .env.test.local .env.production.local # vercel .vercel # rss /public/rss /public/feed.xml public/sitemap.xml # pwa /public/sw.js /public/workbox-*.js /public/worker-*.js /public/sw.js.map /public/workbox-*.js.map /public/worker-*.js.map /store tsconfig.tsbuildinfo .env*.local .vscode/ ================================================ FILE: .vercelignore ================================================ /sanity ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2021 Jatin Sharma 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 ================================================
![Cover](https://imgur.com/Kpzk2LQ.png) ![Github stars](https://img.shields.io/github/stars/j471n/j471n.in?style=flat-square) ![Github Forks](https://img.shields.io/github/forks/j471n/j471n.in?style=flat-square) ![GitHub code size in bytes](https://img.shields.io/github/languages/code-size/j471n/j471n.in?style=flat-square) ![GitHub repo size](https://img.shields.io/github/repo-size/j471n/j471n.in?style=flat-square)
## Tools Used - **Framework**: [Next.js](https://nextjs.org/) - **Styling**: [Tailwind CSS](https://tailwindcss.com/) - **Content**: [MDX](https://github.com/mdx-js/mdx) - **Database**: [Supabase](https://supabase.com/) - **Animations**: [Framer Motion](https://framer.com/motion) - **Deployment**: [Vercel](https://vercel.com) - **Icons**: [React Icons](https://react-icons.github.io/react-icons/) - **Plugins**: [rehype](https://github.com/rehypejs/rehype) - **Analytics**: [Google Analytics](https://analytics.google.com/analytics/web/) - [SWR](https://swr.vercel.app/) - [Email.js](https://www.emailjs.com/) - [React Toastify](https://github.com/fkhadra/react-toastify) ## Run Locally Clone the project: ```bash git clone https://github.com/j471n/j471n.in.git ``` Go to the project directory: ```bash cd j471n.in ``` Install dependencies ```bash yarn # or npm install ``` Start the server: ```bash yarn dev # or npm run dev ``` After that server should be running on [localhost:3000](http://localhost:3000) > I am using [yarn](https://yarnpkg.com/) you can use [pnpm](https://pnpm.io/) or [npm](https://www.npmjs.com/) > Warning: You could run into errors if you don't populate the `.env.local` with the correct values ## Setting up the Environment Rename [`.env.example`](/.env.example) to `.env.local` and then you need to populate that with the respective values. ### Email Service Integration - `NEXT_PUBLIC_YOUR_SERVICE_ID`: Go to the [Admin Panel](https://dashboard.emailjs.com/admin) of [emailjs.com](https://emailjs.com). If you haven't already added a service then Click on the **Add Service** Button as shown in the image ![](https://i.imgur.com/bK5wzkD.png) Then choose any method you want I am using **Gmail** ![](https://i.imgur.com/zTrFCNJ.png) - Then first click on the **Connect Account and log** in with your Gmail account that you want to use to get the emails from. - In the second step click on **Create Service** and then copy the **Service ID** and add this ID to `NEXT_PUBLIC_YOUR_SERVICE_ID` in `.env.local` ![](https://i.imgur.com/c8ZkUf5.png) - `NEXT_PUBLIC_YOUR_TEMPLATE_ID`: To get the Template ID visit the [Email Templates](https://dashboard.emailjs.com/admin/templates) section and click on **Create New Template**. ![](https://i.imgur.com/TQLrQuz.png) And then you will see a window where you can edit your email template after you are satisfied with your template then click on the Save button in the top right corner. ![](https://i.imgur.com/98adqhN.png) After that you will have your Template ID as shown in the image below: ![](https://i.imgur.com/pcqKu3f.png) - `NEXT_PUBLIC_YOUR_USER_ID`: To get your User ID, Go to [Account](https://dashboard.emailjs.com/admin/account) and then you will be able to see it: ![](https://i.imgur.com/oU3tBiY.png) ### dev\.to Integration - `NEXT_PUBLIC_BLOGS_API`: I am using [Dev.to API](https://developers.forem.com/api) to fetch all the blog stats. You can get this API at the bottom of the [Extensions](https://dev.to/settings/extensions) section. ![](https://i.imgur.com/zh7V0ZB.png) ### Google Analytics - `NEXT_PUBLIC_GA_MEASUREMENT_ID`: You can follow this [guide](https://support.google.com/analytics/answer/9539598?hl=en) to get your Google Analytics ID and then you will be able to use Google Analytics in this project. - [**Google Analytics Data API**](https://developers.google.com/analytics/devguides/reporting/data/v1): I am using this API to get the analytics of this website so that I can show how many user visit this site in the last 7 days. In this you will need the value of the following properties: - `GA_PROPERTY_ID` - `GA_CLIENT_EMAIL` - `GA_PRIVATE_KEY` I have written a [blog](https://j471n.in/blogs/google-analytics-data-api) that shows how you can get these properties and guides to use them. ### Spotify Integration I have used [Spotify API](https://developer.spotify.com/documentation/web-api/). So, you need three Environment Variable values- - `SPOTIFY_CLIENT_ID` - `SPOTIFY_CLIENT_SECRET` - `SPOTIFY_REFRESH_TOKEN` You need to follow this [blog](https://j471n.in/blogs/spotify-api-nextjs) to get these variables' values. ### Supabase Integration I am using [Supabase](https://supabase.com/) with ISR to store all my projects and certificates for now. It provides an API that helps you to access the data. To access that data you need two things: - `SUPABASE_URL`: Database URL. - `SUPABASE_KEY`: It is safe to be used in a browser context. **Steps-** - To get these go to [Supabase](https://app.supabase.com/sign-in) and log in with your account. - Click on **New Project** and fill all the fields. - Click on **Create New Project**. - Go to the [Settings](https://app.supabase.com/project/_/settings/general) page in the Dashboard. - Click **API** in the sidebar. - Find your API **URL** and **anon** key on this page. - Now you can [Create table](https://app.supabase.com/project/_/editor) and start using it. But before you use this there was one issue I had when I was using this it was returning the empty array ([]). It was because of project policies. By default, no-one has access to the data. To fix that you can do the following: - Go to [Policies](https://app.supabase.com/project/_/auth/policies). - Select your Project. - Click on **New Policy**. ![](https://i.imgur.com/RsGd8oW.png) - You will be presented with two options. You can choose either one. I chose the 1st option: ![](https://i.imgur.com/QDAePUQ.png) - After that, you will have four options as shown in the following image. You can choose according to your need. I only need the read access so I went with 1st option. ![](https://i.imgur.com/h1hSivF.png) - Click on **Use this template**. - Click on **Review**. - Click on **Save Policy** After that, you will be able to access the data using [@supabase/supabase-js](https://www.npmjs.com/package/@supabase/supabase-js). Install it and you just set up your project with Supabase. - `REVALIDATE_SECRET`: As I am using [Supabase](https://supabase.com/), It has a feature called [webhooks](https://supabase.com/docs/guides/database/webhooks) which allow you to send real-time data from your database to another system whenever a table event occurs. So I am using it to revalidate my `projects` and `certificates` page. For that I am providing a custom secret value to verify that request is coming from authenticated source. Let's create webhook: - Go to [webhooks](https://app.supabase.com/project/_/database/hooks) page. - Click on **Create a new hook** - Enter the name of the function hook (example: `update_projects`) ![](https://i.imgur.com/QAYIkKZ.png) - Choose your table from the dropdown list ![](https://i.imgur.com/Hspecbe.png) - Select events which will trigger this function hook ![](https://i.imgur.com/OYq1qcg.png) - Now Choose POST method and enter the revalidate URL (request will be sent to this URL) ![](https://i.imgur.com/lpicIsR.png) - Then add two HTTP Params `secret` and `revalidateUrl` ![](https://i.imgur.com/Mw1Ia0o.png) - Now add this secret to your `env.local` and it will update the page when you made some changes to your supabase database. - `pages/api/revalidate.ts` is using `revalidateUrl` to update the page with new data. ### GitHub Integration To get `GITHUB_TOKEN` Follow these steps to generate a GitHub token that I am using fetch my GitHub details: **Step 1: Accessing GitHub Developer Settings** - Log in to your GitHub account. - Click on your profile picture in the top-right corner of the page. - From the drop-down menu, select Settings. ![](https://i.imgur.com/h7jtNeH.png) **Step 2: Navigating to Developer Settings** In the left sidebar, scroll down and click on Developer settings. ![](https://i.imgur.com/JHFdEhP.png) **Step 3: Creating a New Personal Access Token** - In the Developer settings page, click on Personal access tokens and then Click on Tokens (Classic). ![](https://i.imgur.com/f2eY9vB.png) - Next, click on the Generate new token button. ![](https://i.imgur.com/V7gBKQh.png) - After selecting the necessary permissions, click on the Generate token button at the bottom of the page. - GitHub will generate a new token for you. Make sure to copy the token value. - **Important**: Treat this token like a password and keep it secure. Do not share it publicly or commit it to a version control repository. ### Email Validation Integration To get `EMAIL_VALIDATION_API` follow the following steps to get the `API_KEY` to validate the email address for the newsletter: - You need to have an account on [RapidAPI](https://rapidapi.com/). - If you have an account then you can just [subscribe](https://rapidapi.com/Top-Rated/api/e-mail-check-invalid-or-disposable-domain/pricing) the free version of [E-mail Check Invalid or Disposable Domain](https://rapidapi.com/Top-Rated/api/e-mail-check-invalid-or-disposable-domain/). Which will give you the 1000 request/month. ![Rapid API-1](https://imgur.com/OMFF69O.png) - Then you'll get the `API_KEY`, which you can store in your `.env.local`. ![Rapid API-2](https://imgur.com/REdKVsX.png) ### Sanity Integration - `SANITY_PROJECT_ID`: - Go to the [Sanity.io](<(https://www.sanity.io/)>) website using your web browser. - Login with you account/Create a new account. - After logging in, you'll be redirected to the Sanity.io dashboard. - If you have an existing project, you'll see it listed on the dashboard. Click on the project's name to access it. - Once you're inside the project, look at the browser's address bar. The URL should look something like this: `https://www.sanity.io/manage/project/your-project-id` - The your-project-id in the URL is your Sanity project ID. It's a unique identifier for your specific project. That's it! You've now obtained your Sanity project ID, which you can use for interacting with your Sanity project via its API or other integrations. ### TMDB Integration - `TMDB_ACCOUNT_ID` and `TMDB_ACCESS_TOKEN`: To enable seamless integration of movie and TV show data, we will use the TMDB API, which offers comprehensive information about media content. The following steps will guide you: **1. Overview of TMDB Integration** Previously, movie and TV show data were manually stored using Supabase, requiring tedious manual work. To streamline the process and automatically update ratings, we have switched to TMDB (The Movie Database). **2. Creating or Logging into Your TMDB Account** If you already have a TMDB account, log in with your existing credentials. Otherwise, visit TMDB's website and create a new account. **3. Generating API Key** After logging in, navigate to the API section in your account settings. Here, you can generate a new API key to access TMDB's data and services. ![generate api key](https://i.imgur.com/y0wA21L.png) **Completing the API Key Request Form** Fill in all the required details in the API key request form, and make sure to accept the terms and conditions. ![complete api key request form](https://i.imgur.com/FZ1RdPf.png) **Obtaining API Key and Access Token** Once you have completed the application registration, you will receive an API key and an access token. Assign the access token to the `TMDB_ACCESS_TOKEN` variable. ![API Key and Access Token](https://i.imgur.com/Q6LI6EF.png) **Finding Your TMDB Account ID** To get the `TMDB_ACCOUNT_ID`, log in to the TMDB system and visit the developer website. There, you will find your account ID associated with your account. ![Finding Your TMDB Account ID](https://i.imgur.com/AdEPtb9.png) With the `TMDB_ACCOUNT_ID` and `TMDB_ACCESS_TOKEN` acquired from the steps above, you can now seamlessly access and update movie and TV show data through TMDB's API, automating the process and making it significantly more efficient. Enjoy your improved movie and TV show list management experience! ## Supabase Database Schema ### Table: certificates | Column Name | Data Type | Constraints | | ------------- | --------------------------- | ------------------------------------------------- | | `id` | UUID | NOT NULL, PRIMARY KEY, DEFAULT uuid_generate_v4() | | `title` | TEXT | NOT NULL | | `issued_date` | DATE | - | | `org_name` | TEXT | - | | `org_logo` | TEXT | - | | `url` | TEXT | - | | `pinned` | BOOLEAN | - | | `created_at` | TIMESTAMP WITHOUT TIME ZONE | NOT NULL, DEFAULT now() | ### Table: movies | Column Name | Data Type | Constraints | | ------------ | ------------------------ | ------------------------------------------------------- | | `id` | BIGINT | GENERATED BY DEFAULT AS IDENTITY, NOT NULL, PRIMARY KEY | | `created_at` | TIMESTAMP WITH TIME ZONE | NOT NULL, DEFAULT (now() AT TIME ZONE 'utc'::text) | | `name` | TEXT | - | | `image` | TEXT | - | | `url` | TEXT | - | | `year` | INTEGER | - | | `watched` | BOOLEAN | DEFAULT true | | `rating` | SMALLINT | - | ### Table: projects | Column Name | Data Type | Constraints | | ------------- | --------------------------- | ------------------------------------------------- | | `id` | UUID | NOT NULL, PRIMARY KEY, DEFAULT uuid_generate_v4() | | `created_at` | TIMESTAMP WITHOUT TIME ZONE | - | | `name` | TEXT | - | | `description` | TEXT | - | | `githubURL` | TEXT | - | | `previewURL` | TEXT | - | | `tools` | TEXT[] | - | | `pinned` | BOOLEAN | DEFAULT false | | `coverImage` | TEXT | - | ### Table: views | Column Name | Data Type | Constraints | | ----------- | --------- | --------------------- | | `slug` | TEXT | NOT NULL, PRIMARY KEY | | `views` | BIGINT | - | ### Table: user_data | Column Name | Data Type | Constraints | | ------------ | ------------------------ | ------------------------------------------------ | | `id` | UUID | NOT NULL, PRIMARY KEY, DEFAULT gen_random_uuid() | | `key` | TEXT | - | | `value` | TEXT | - | | `created_at` | TIMESTAMP WITH TIME ZONE | DEFAULT now() | This table is unique because it accommodates various types of miscellaneous data. I'm sharing the current keys in my database, which are utilized in my project. This will guide you on the types of data you need to add to avoid errors during implementation." | Keys | Sample Data | | ---------------------- | ---------------------------------------- | | `linkedin` | [Sample JSON](https://traff.co/K6wi1Q3p) | | `instagram_user_token` | [Sample TEXT](https://traff.co/mgFnU8vN) | | `devto_stats` | [Sample JSON](https://traff.co/Ff99Gv2h) | ## Documentation I have written an in-depth blog on [How I Made My Portfolio with Next.js](https://dev.to/j471n/how-i-made-my-portfolio-with-nextjs-2mn3). You can visit there to look at the detailed guide about this portfolio. ================================================ FILE: components/Blog.tsx ================================================ import { BlogPost } from "@lib/interface/sanity"; import Image from "next/image"; import Link from "next/link"; import { getFormattedDate } from "@utils/date"; import { motion } from "framer-motion"; import { FiArrowRight } from "react-icons/fi"; const cardVariants = { hidden: { opacity: 0, y: 14 }, visible: { opacity: 1, y: 0, transition: { type: "spring" as const, stiffness: 120, damping: 20 }, }, }; export default function Blog({ blog, animate = false, index, }: { blog: BlogPost; animate?: boolean; index?: number; }) { return ( {/* Left accent bar */}
{/* Article index */} {index !== undefined && ( {String(index + 1).padStart(2, "0")} )} {/* Content */}

{blog.title}

{blog.excerpt}

{/* Meta row */}
{blog.author.name}
{blog.author.name} {blog.organization && ( <> · {blog.organization.name} )} · {getFormattedDate(new Date(blog.publishedAt))}
{/* Thumbnail — grayscale at rest, color on hover */} {/*
{blog.title}
*/} {/* Arrow */} ); } ================================================ FILE: components/BookCard.tsx ================================================ import Image from "next/image"; import Link from "next/link"; import { motion } from "framer-motion"; import { HiOutlineExternalLink } from "react-icons/hi"; import { HardcoverBook } from "@lib/types"; const itemVariants = { hidden: { opacity: 0, y: 16 }, visible: { opacity: 1, y: 0, transition: { type: "spring", stiffness: 260, damping: 24 }, }, }; const HARDCOVER_BOOK_URL = "https://hardcover.app/books/"; const FALLBACK_COVER = "https://imgur.com/5dYYce8.png"; const STAR_PATH = "M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z"; function Stars({ rating, size = "w-3 h-3", }: { rating: number; size?: string; }) { const filled = Math.round(rating); return ( <> {[1, 2, 3, 4, 5].map((i) => ( ))} ); } function formatFinishedDate(iso: string): string { const d = new Date(iso); return d.toLocaleDateString("en-US", { month: "short", year: "numeric" }); } export default function BookCard({ book }: { book: HardcoverBook }) { const href = book.slug ? `${HARDCOVER_BOOK_URL}${book.slug}` : "https://hardcover.app"; const authorLine = book.authors.length > 0 ? book.authors.join(", ") : "Unknown author"; const finishedDate = book.statusId === 3 ? book.finishedAt ? formatFinishedDate(book.finishedAt) : book.updatedAt ? formatFinishedDate(book.updatedAt) : null : null; const hasMyRating = book.userRating != null && book.userRating > 0; const hasCommunityRating = book.rating != null && book.rating > 0; return ( {/* External link icon */} {/* Cover */}
{`Cover
{/* Details */}
{/* Author */}

{authorLine}

{/* Title */}

{book.title}

{/* Spacer */}
{/* Bottom meta */}
{/* Ratings row — personal stars + community avg on one line */} {(hasMyRating || hasCommunityRating) && (
{hasMyRating && ( {book.userRating!.toFixed(1)} )} {hasMyRating && hasCommunityRating && ( · )} {hasCommunityRating && ( {book.rating!.toFixed(1)} avg )}
)} {/* Year · pages */}
{book.releaseYear != null && {book.releaseYear}} {book.releaseYear != null && book.pages != null && ( · )} {book.pages != null && ( {book.pages.toLocaleString()} pages )}
{/* Finished date */} {finishedDate && (
Finished · {finishedDate}
)}
); } ================================================ FILE: components/Contact/Contact.tsx ================================================ import React from "react"; import { motion } from "framer-motion"; import ContactForm from "./ContactForm"; import siteConfig from "@content/siteConfig"; const infoRows = [ { label: "Response Time", value: "< 24 hours" }, { label: "Timezone", value: "IST · UTC +5:30" }, { label: "Preferred", value: "Email / LinkedIn" }, { label: "Work Type", value: "Remote / Contract" }, ]; export default function Contact() { const { contact } = siteConfig; return (
{/* Section number watermark */} {/* ── Header ── */}
{contact.eyebrow} {contact.title} {contact.description}
{/* Email quick-link */} Or reach me directly {siteConfig.person.email}
{/* ── Divider ── */}
Send a Message
{/* ── Two-column body ── */}
{/* Form — 3 cols */} {/* Info sidebar — 2 cols */} {/* Status */}
Current Status
Available for Work
{/* Info rows */}
{infoRows.map(({ label, value }, i) => ( {label} {value} ))}
{/* Decorative type */}
); } ================================================ FILE: components/Contact/ContactForm.tsx ================================================ import React, { useRef, useState } from "react"; import { ToastContainer, toast } from "react-toastify"; import { useDarkMode } from "@context/darkModeContext"; import emailjs from "@emailjs/browser"; import { motion, AnimatePresence } from "framer-motion"; import { FormInput } from "@lib/types"; import siteConfig from "@content/siteConfig"; import { FiSend } from "react-icons/fi"; const inputVariants = { hidden: { opacity: 0, x: -20 }, visible: { opacity: 1, x: 0, transition: { duration: 0.5, ease: [0.22, 1, 0.36, 1], }, }, }; export default function Form() { const { isDarkMode } = useDarkMode(); const sendButtonRef = useRef(null!); const formRef = useRef(null!); const [isSubmitting, setIsSubmitting] = useState(false); const [focusedField, setFocusedField] = useState(null); const { contact } = siteConfig; const FailToastId: string = "failed"; function sendEmail(e: React.SyntheticEvent) { e.preventDefault(); const target = e.target as typeof e.target & { first_name: { value: string }; last_name: { value: string }; email: { value: string }; subject: { value: string }; message: { value: string }; }; const emailData = { to_name: siteConfig.person.name, first_name: target.first_name.value.trim(), last_name: target.last_name.value.trim(), email: target.email.value.trim(), subject: target.subject.value.trim(), message: target.message.value.trim(), }; if (!validateForm(emailData) && !toast.isActive(FailToastId)) return toast.error("Please fill in all required fields", { toastId: FailToastId, }); setIsSubmitting(true); sendButtonRef.current.setAttribute("disabled", "true"); const toastId = toast.loading("Sending your message..."); emailjs .send( process.env.NEXT_PUBLIC_YOUR_SERVICE_ID!, process.env.NEXT_PUBLIC_YOUR_TEMPLATE_ID!, emailData!, process.env.NEXT_PUBLIC_YOUR_USER_ID, ) .then(() => { formRef.current.reset(); toast.update(toastId, { render: "Message sent successfully!", type: "success", isLoading: false, autoClose: 4000, }); setIsSubmitting(false); sendButtonRef.current.removeAttribute("disabled"); }) .catch(() => { toast.update(toastId, { render: "Failed to send message. Please try again.", type: "error", isLoading: false, autoClose: 4000, }); setIsSubmitting(false); sendButtonRef.current.removeAttribute("disabled"); }); } function validateForm(data: FormInput): boolean { for (const key in data) { if (data[key as keyof FormInput] === "") return false; } return true; } /* Shared field class — borderless top/sides, only bottom rule */ const fieldCls = (name: string) => `block w-full bg-transparent border-0 border-b py-3 text-gray-900 dark:text-white placeholder-gray-400 dark:placeholder-gray-600 focus:outline-none transition-colors duration-200 ${ focusedField === name ? "border-gray-900 dark:border-white" : "border-gray-300 dark:border-gray-700" }`; return ( <> {/* Name row */}
{/* First Name */} setFocusedField("first_name")} onBlur={() => setFocusedField(null)} className={fieldCls("first_name")} placeholder="John" required /> {/* Last Name */} setFocusedField("last_name")} onBlur={() => setFocusedField(null)} className={fieldCls("last_name")} placeholder="Doe" required />
{/* Email */} setFocusedField("email")} onBlur={() => setFocusedField(null)} className={fieldCls("email")} placeholder="john.doe@example.com" required /> {/* Subject */} setFocusedField("subject")} onBlur={() => setFocusedField(null)} className={fieldCls("subject")} placeholder="Project Discussion" required /> {/* Message */}