├── .env.example
├── .eslintrc.json
├── .github
└── ISSUE_TEMPLATE
│ └── feature_request.md
├── .gitignore
├── CODE_OF_CONDUCT.md
├── LICENSE
├── README.md
├── SECURITY.md
├── components.json
├── next.config.mjs
├── package.json
├── postcss.config.mjs
├── prisma
└── schema.prisma
├── public
├── audio
│ └── switch-on.mp3
├── images
│ ├── blog
│ │ └── reduce-cover.webp
│ ├── chain-gpt-logo.svg
│ └── logo.png
└── static
│ ├── og-modulo-operator-a63a03.webp
│ └── publishing-react-package-7c738b.webp
├── src
├── actions
│ ├── mutations.ts
│ └── queries.ts
├── app
│ ├── (main)
│ │ ├── blog
│ │ │ ├── [...slug]
│ │ │ │ └── page.tsx
│ │ │ └── page.tsx
│ │ ├── layout.tsx
│ │ ├── page.tsx
│ │ ├── projects
│ │ │ └── page.tsx
│ │ └── tags
│ │ │ ├── [tag]
│ │ │ └── page.tsx
│ │ │ └── page.tsx
│ ├── api
│ │ ├── contact
│ │ │ └── route.ts
│ │ └── views
│ │ │ └── [slug]
│ │ │ └── route.ts
│ ├── apple-icon.png
│ ├── error.tsx
│ ├── favicon.ico
│ ├── feed.xml
│ │ └── route.ts
│ ├── icon.png
│ ├── layout.tsx
│ ├── not-found.tsx
│ ├── opengraph-image.png
│ ├── robot.ts
│ ├── sitemap.ts
│ └── twitter-image.png
├── assets
│ ├── images
│ │ ├── cover
│ │ │ ├── chain-gpt.png
│ │ │ ├── luma.png
│ │ │ ├── mint-kuto.png
│ │ │ ├── podportal.png
│ │ │ ├── power-up.png
│ │ │ └── world-ranks.png
│ │ ├── f-dubai-police.webp
│ │ ├── mint-kuto.avif
│ │ ├── nft-connect.jpg
│ │ ├── nft-connect.webp
│ │ ├── power-up.webp
│ │ └── world-rank.png
│ └── svg
│ │ ├── chain-gpt.tsx
│ │ ├── index.ts
│ │ └── luma.tsx
├── components
│ ├── about-section.tsx
│ ├── back-btn.tsx
│ ├── contact-us.tsx
│ ├── layout
│ │ ├── footer.tsx
│ │ └── nav
│ │ │ ├── _nav-mock.ts
│ │ │ ├── index.tsx
│ │ │ ├── logo.tsx
│ │ │ ├── mobile-nav.tsx
│ │ │ ├── nav-item.tsx
│ │ │ └── nav-list.tsx
│ ├── mdx
│ │ ├── custom-image.tsx
│ │ ├── custom-link.tsx
│ │ ├── index.ts
│ │ └── mdx-content.tsx
│ ├── post
│ │ ├── index.ts
│ │ ├── post-comments.tsx
│ │ ├── post-item.tsx
│ │ ├── post-json-schema.tsx
│ │ ├── post-list.tsx
│ │ ├── post-metadata.tsx
│ │ ├── post-toc.tsx
│ │ └── post-views.tsx
│ ├── project
│ │ ├── _project-mock.ts
│ │ ├── index.ts
│ │ ├── project-icons.tsx
│ │ ├── project-item.tsx
│ │ └── project-list.tsx
│ ├── scroll-progress.tsx
│ ├── search-input.tsx
│ ├── skills.tsx
│ ├── socials.tsx
│ ├── support-btn.tsx
│ ├── tags.tsx
│ └── ui
│ │ ├── accordion.tsx
│ │ ├── button.tsx
│ │ ├── callout.tsx
│ │ ├── card.tsx
│ │ ├── content-not-found.tsx
│ │ ├── dank-mono.otf
│ │ ├── dropdown-menu.tsx
│ │ ├── fonts.ts
│ │ ├── form.tsx
│ │ ├── input.tsx
│ │ ├── label.tsx
│ │ ├── rss.tsx
│ │ ├── sheet.tsx
│ │ ├── skip-content.tsx
│ │ ├── sonner.tsx
│ │ ├── textarea.tsx
│ │ ├── tooltip.tsx
│ │ ├── top-loader.tsx
│ │ └── typograpghy.tsx
├── config.ts
├── constants
│ ├── anime.ts
│ ├── env.ts
│ └── stack.tsx
├── content
│ └── posts
│ │ ├── images
│ │ └── cover
│ │ │ ├── og-modulo-operator.webp
│ │ │ └── publishing-react-package.webp
│ │ ├── javascript-modulo-operator.mdx
│ │ └── publishing-react-package.mdx
├── hooks
│ ├── index.ts
│ ├── use-client.tsx
│ ├── use-isomorphic.tsx
│ └── use-media.tsx
├── lib
│ ├── axios.ts
│ ├── seo.tsx
│ ├── shadcn-ui.ts
│ └── utils.ts
├── providers
│ ├── index.tsx
│ └── react-query.tsx
├── schema.ts
├── server
│ └── db.ts
├── styles
│ ├── globals.css
│ └── mdx.css
└── types
│ └── config.ts
├── tailwind.config.ts
├── tsconfig.json
├── velite.config.ts
└── yarn.lock
/.env.example:
--------------------------------------------------------------------------------
1 | # https://nocodeapi.com/
2 | NOCODE_API_KEY=""
3 | NOCODE_TAB_ID=""
4 |
5 | # https://vercel.com/storage/postgres
6 | POSTGRES_URL=""
7 | POSTGRES_PRISMA_URL=""
8 | POSTGRES_URL_NO_SSL=""
9 | POSTGRES_URL_NON_POOLING=""
10 | POSTGRES_USER=""
11 | POSTGRES_HOST=""
12 | POSTGRES_PASSWORD=""
13 | POSTGRES_DATABASE=""
14 |
15 | # https://giscus.app/
16 | NEXT_PUBLIC_GISCUS_REPO_ID=""
17 | NEXT_PUBLIC_GISCUS_CATEGORY_ID=""
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "next/core-web-vitals"
3 | }
4 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Is your feature request related to a problem? Please describe.**
11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
12 |
13 | **Describe the solution you'd like**
14 | A clear and concise description of what you want to happen.
15 |
16 | **Describe alternatives you've considered**
17 | A clear and concise description of any alternative solutions or features you've considered.
18 |
19 | **Additional context**
20 | Add any other context or screenshots about the feature request here.
21 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 | .yarn/install-state.gz
8 |
9 |
10 | /prisma/db.sql
11 | /prisma/db.sqlite-journal
12 | .prisma
13 | javascript/**/migrations/
14 | typescript/**/migrations/
15 | /prisma/migrations
16 |
17 | # testing
18 | /coverage
19 |
20 | # next.js
21 | /.next/
22 | /out/
23 |
24 | # production
25 | /build
26 |
27 | # misc
28 | .DS_Store
29 | *.pem
30 |
31 | # debug
32 | npm-debug.log*
33 | yarn-debug.log*
34 | yarn-error.log*
35 |
36 | # local env files
37 | .env*.local
38 | .env
39 |
40 | # vercel
41 | .vercel
42 |
43 | # typescript
44 | *.tsbuildinfo
45 | next-env.d.ts
46 |
47 | # velite files
48 | .velite
49 | .env
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | We as members, contributors, and leaders pledge to make participation in our
6 | community a harassment-free experience for everyone, regardless of age, body
7 | size, visible or invisible disability, ethnicity, sex characteristics, gender
8 | identity and expression, level of experience, education, socio-economic status,
9 | nationality, personal appearance, race, religion, or sexual identity
10 | and orientation.
11 |
12 | We pledge to act and interact in ways that contribute to an open, welcoming,
13 | diverse, inclusive, and healthy community.
14 |
15 | ## Our Standards
16 |
17 | Examples of behavior that contributes to a positive environment for our
18 | community include:
19 |
20 | * Demonstrating empathy and kindness toward other people
21 | * Being respectful of differing opinions, viewpoints, and experiences
22 | * Giving and gracefully accepting constructive feedback
23 | * Accepting responsibility and apologizing to those affected by our mistakes,
24 | and learning from the experience
25 | * Focusing on what is best not just for us as individuals, but for the
26 | overall community
27 |
28 | Examples of unacceptable behavior include:
29 |
30 | * The use of sexualized language or imagery, and sexual attention or
31 | advances of any kind
32 | * Trolling, insulting or derogatory comments, and personal or political attacks
33 | * Public or private harassment
34 | * Publishing others' private information, such as a physical or email
35 | address, without their explicit permission
36 | * Other conduct which could reasonably be considered inappropriate in a
37 | professional setting
38 |
39 | ## Enforcement Responsibilities
40 |
41 | Community leaders are responsible for clarifying and enforcing our standards of
42 | acceptable behavior and will take appropriate and fair corrective action in
43 | response to any behavior that they deem inappropriate, threatening, offensive,
44 | or harmful.
45 |
46 | Community leaders have the right and responsibility to remove, edit, or reject
47 | comments, commits, code, wiki edits, issues, and other contributions that are
48 | not aligned to this Code of Conduct, and will communicate reasons for moderation
49 | decisions when appropriate.
50 |
51 | ## Scope
52 |
53 | This Code of Conduct applies within all community spaces, and also applies when
54 | an individual is officially representing the community in public spaces.
55 | Examples of representing our community include using an official e-mail address,
56 | posting via an official social media account, or acting as an appointed
57 | representative at an online or offline event.
58 |
59 | ## Enforcement
60 |
61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
62 | reported to the community leaders responsible for enforcement at
63 | .
64 | All complaints will be reviewed and investigated promptly and fairly.
65 |
66 | All community leaders are obligated to respect the privacy and security of the
67 | reporter of any incident.
68 |
69 | ## Enforcement Guidelines
70 |
71 | Community leaders will follow these Community Impact Guidelines in determining
72 | the consequences for any action they deem in violation of this Code of Conduct:
73 |
74 | ### 1. Correction
75 |
76 | **Community Impact**: Use of inappropriate language or other behavior deemed
77 | unprofessional or unwelcome in the community.
78 |
79 | **Consequence**: A private, written warning from community leaders, providing
80 | clarity around the nature of the violation and an explanation of why the
81 | behavior was inappropriate. A public apology may be requested.
82 |
83 | ### 2. Warning
84 |
85 | **Community Impact**: A violation through a single incident or series
86 | of actions.
87 |
88 | **Consequence**: A warning with consequences for continued behavior. No
89 | interaction with the people involved, including unsolicited interaction with
90 | those enforcing the Code of Conduct, for a specified period of time. This
91 | includes avoiding interactions in community spaces as well as external channels
92 | like social media. Violating these terms may lead to a temporary or
93 | permanent ban.
94 |
95 | ### 3. Temporary Ban
96 |
97 | **Community Impact**: A serious violation of community standards, including
98 | sustained inappropriate behavior.
99 |
100 | **Consequence**: A temporary ban from any sort of interaction or public
101 | communication with the community for a specified period of time. No public or
102 | private interaction with the people involved, including unsolicited interaction
103 | with those enforcing the Code of Conduct, is allowed during this period.
104 | Violating these terms may lead to a permanent ban.
105 |
106 | ### 4. Permanent Ban
107 |
108 | **Community Impact**: Demonstrating a pattern of violation of community
109 | standards, including sustained inappropriate behavior, harassment of an
110 | individual, or aggression toward or disparagement of classes of individuals.
111 |
112 | **Consequence**: A permanent ban from any sort of public interaction within
113 | the community.
114 |
115 | ## Attribution
116 |
117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage],
118 | version 2.0, available at
119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
120 |
121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct
122 | enforcement ladder](https://github.com/mozilla/diversity).
123 |
124 | [homepage]: https://www.contributor-covenant.org
125 |
126 | For answers to common questions about this code of conduct, see the FAQ at
127 | https://www.contributor-covenant.org/faq. Translations are available at
128 | https://www.contributor-covenant.org/translations.
129 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | faisal-dev
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | Welcome to my personal website! where I share my thoughts, projects, insights & blogs. Feel free to
20 | explore and get inspired
21 |
22 | ## ✨ Features
23 |
24 | - ⚡️ Next.js 14 with App Router (Turbo)
25 | - 📝 MDX powered by velite
26 | - 🎨 Tailwind CSS - for styling
27 | - 🌈 Shadcn UI - accessible UI components
28 | - 🛡 Strict TypeScript and ESLint configuration
29 | - 📱 Responsive design
30 | - 📈 SEO optimized with meta tags and JSON-LD
31 | - 📰 RSS feed
32 | - 🗺 Sitemap
33 | - 📊 Vercel Analytics
34 | - 📝 Blog with comments, likes, and post views
35 | - 🔎 Blog post search
36 | - 📖 Table of contents for blog posts
37 | - 📝 Code syntax highlighting - using Shiki
38 | - 🎨 Animation - using Framer Motion
39 | - 🏠 LightHouse score of nearly 100
40 | - 💄 Prettier - code formatting
41 | - 〰️ Prisma & Vercel Postgres
42 | - 👷🏻♂️ t3-env - validate environment variables before building
43 |
44 | ## 🔨 Requirements
45 |
46 | - Node, recommended `20.x`
47 | - npm, recommended `10.5.0`
48 | - PostgreSQL, recommended `14.x` check
49 | [https://vercel.com/storage/postgres](https://vercel.com/storage/postgres)
50 | - Visual Studio Code [https://code.visualstudio.com/](https://code.visualstudio.com/)
51 | - For the contact section, I'm using NoCode API [https://nocodeapi.com/](https://nocodeapi.com/).
52 | Simply create an account here and under the marketplace, enable the Excel Sheet API. Once enabled,
53 | you will receive a tabId and an API key.
54 | - For the blog comments, I'm using the Giscus component. Set up your configuration
55 | [https://giscus.app/](https://giscus.app/).
56 |
57 | ## 👋 Getting Started
58 |
59 | Follow these steps to run the project locally on your machine:
60 |
61 | ```bash
62 | git clone https://github.com/BinarySenseiii/personal-website.git
63 | cd personal-website
64 | npm install
65 | ```
66 |
67 | Create a `.env.local` file based on the provided `.env.example` file and fill in the necessary
68 | variables.
69 |
70 | OR you can skip this by modifying `src/constants/env.ts`:
71 |
72 | ```ts
73 | export const env = createEnv({
74 | skipValidation: true,
75 |
76 | server: {
77 | // ...
78 | },
79 | })
80 | ```
81 |
82 | It will skip the validation of environment variables. And you may notice that some functionalities
83 | will not work properly. But it's okay for learning.
84 |
85 | Then generate prisma client:
86 |
87 | ```bash
88 | npx prisma generate
89 | npx migrate dev --name any
90 | ```
91 |
92 | To run the app in development mode:
93 |
94 | ```bash
95 | npm run dev
96 | ```
97 |
98 | The app will be available at `localhost:3000`.
99 |
100 | ## ✈️ TODO
101 |
102 | - ESM import { build } from 'velite' may be got a
103 | [webpack.cache.PackFileCacheStrategy/webpack.FileSystemInfo] warning generated during the next
104 | build process, which has little impact, refer to https://github.com/webpack/webpack/pull/15688
105 |
106 | ## ✍🏻 Author
107 |
108 | - [@BinarySenseiii](https://github.com/BinarySenseiii)
109 |
110 | ## 🪪 License
111 |
112 | This project is open source and available under the [GPL3 License](LICENSE).
113 |
114 |
115 |
116 | Design & Developed by ❤️
117 |
118 |
--------------------------------------------------------------------------------
/SECURITY.md:
--------------------------------------------------------------------------------
1 | # Security Policy
2 |
3 | ## Supported Versions
4 |
5 | Use this section to tell people about which versions of your project are
6 | currently being supported with security updates.
7 |
8 | | Version | Supported |
9 | | ------- | ------------------ |
10 | | 5.1.x | :white_check_mark: |
11 | | 5.0.x | :x: |
12 | | 4.0.x | :white_check_mark: |
13 | | < 4.0 | :x: |
14 |
15 | ## Reporting a Vulnerability
16 |
17 | Use this section to tell people how to report a vulnerability.
18 |
19 | Tell them where to go, how often they can expect to get an update on a
20 | reported vulnerability, what to expect if the vulnerability is accepted or
21 | declined, etc.
22 |
--------------------------------------------------------------------------------
/components.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://ui.shadcn.com/schema.json",
3 | "style": "default",
4 | "rsc": true,
5 | "tsx": true,
6 | "tailwind": {
7 | "config": "tailwind.config.ts",
8 | "css": "src/styles/globals.css",
9 | "baseColor": "zinc",
10 | "cssVariables": true,
11 | "prefix": ""
12 | },
13 | "aliases": {
14 | "components": "~/components",
15 | "utils": "~/lib/utils"
16 | }
17 | }
--------------------------------------------------------------------------------
/next.config.mjs:
--------------------------------------------------------------------------------
1 | import {build} from 'velite'
2 |
3 | import {fileURLToPath} from 'node:url'
4 | import createJiti from 'jiti'
5 | const jiti = createJiti(fileURLToPath(import.meta.url))
6 |
7 | jiti('./src/constants/env')
8 |
9 | // Note that this approach uses top-level await, so it only supports next.config.mjs or ESM enabled.
10 | const isDev = process.argv.indexOf('dev') !== -1
11 | const isBuild = process.argv.indexOf('build') !== -1
12 |
13 | if (!process.env.VELITE_STARTED && (isDev || isBuild)) {
14 | process.env.VELITE_STARTED = '1'
15 | const {build} = await import('velite')
16 | await build({watch: isDev, clean: !isDev})
17 | }
18 |
19 | /** @type {import('next').NextConfig} */
20 |
21 | const nextConfig = {}
22 |
23 | export default nextConfig
24 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "personal-website",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev:content": "velite --watch",
7 | "build:content": "velite --clean",
8 | "dev:next": "next dev",
9 | "build:next": "next build",
10 | "dev": "run-p dev:*",
11 | "build": "run-s build:*",
12 | "start": "next start",
13 | "upgrade:latest": "yarn upgrade-interactive --latest"
14 | },
15 | "dependencies": {
16 | "@giscus/react": "^3.0.0",
17 | "@hookform/resolvers": "^3.6.0",
18 | "@next/third-parties": "^14.2.14",
19 | "@paralleldrive/cuid2": "^2.2.2",
20 | "@prisma/client": "^5.20.0",
21 | "@radix-ui/react-accordion": "^1.2.1",
22 | "@radix-ui/react-dialog": "^1.1.2",
23 | "@radix-ui/react-dropdown-menu": "^2.1.2",
24 | "@radix-ui/react-label": "^2.0.2",
25 | "@radix-ui/react-slot": "^1.0.2",
26 | "@radix-ui/react-tooltip": "^1.1.3",
27 | "@t3-oss/env-nextjs": "^0.11.1",
28 | "@tanstack/react-query": "^5.59.0",
29 | "@theme-toggles/react": "^4.1.0",
30 | "@uidotdev/usehooks": "^2.4.1",
31 | "@vercel/analytics": "^1.3.1",
32 | "@vercel/speed-insights": "^1.0.11",
33 | "axios": "^1.7.7",
34 | "class-variance-authority": "^0.7.0",
35 | "clsx": "^2.1.1",
36 | "framer-motion": "^11.11.1",
37 | "jiti": "^2.2.1",
38 | "lucide-react": "^0.447.0",
39 | "next": "14.2.14",
40 | "next-nprogress-bar": "^2.3.12",
41 | "next-themes": "^0.3.0",
42 | "react": "^18",
43 | "react-dom": "^18",
44 | "react-fast-marquee": "^1.6.4",
45 | "react-hook-form": "^7.53.0",
46 | "react-icons": "^5.2.1",
47 | "rehype-autolink-headings": "^7.1.0",
48 | "rehype-code-titles": "^1.2.0",
49 | "rehype-external-links": "^3.0.0",
50 | "rehype-pretty-code": "^0.14.0",
51 | "rehype-slug": "^6.0.0",
52 | "rough-notation": "^0.5.1",
53 | "rss": "^1.2.2",
54 | "sharp": "0.33.5",
55 | "shiki": "^1.21.0",
56 | "sonner": "^1.5.0",
57 | "tailwind-merge": "^2.5.3",
58 | "tailwindcss-animate": "^1.0.7",
59 | "zod": "^3.23.8"
60 | },
61 | "devDependencies": {
62 | "@tailwindcss/typography": "^0.5.15",
63 | "@types/node": "^22.7.4",
64 | "@types/react": "^18.3.11",
65 | "@types/react-dom": "^18",
66 | "@types/rss": "^0.0.32",
67 | "eslint": "^8",
68 | "eslint-config-next": "14.2.14",
69 | "npm-run-all": "^4.1.5",
70 | "postcss": "^8.4.47",
71 | "prisma": "^5.20.0",
72 | "tailwindcss": "^3.4.13",
73 | "typescript": "^5.6.2",
74 | "velite": "^0.1.0"
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/postcss.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('postcss-load-config').Config} */
2 | const config = {
3 | plugins: {
4 | tailwindcss: {},
5 | },
6 | };
7 |
8 | export default config;
9 |
--------------------------------------------------------------------------------
/prisma/schema.prisma:
--------------------------------------------------------------------------------
1 | generator client {
2 | provider = "prisma-client-js"
3 | }
4 |
5 | datasource db {
6 | provider = "postgresql"
7 | url = env("POSTGRES_PRISMA_URL")
8 | directUrl = env("POSTGRES_URL_NON_POOLING")
9 | }
10 |
11 | model Views {
12 | slug String @id
13 | count Int @default(0)
14 | }
15 |
--------------------------------------------------------------------------------
/public/audio/switch-on.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BinarySenseiii/personal-website/6f4f0a97fbb86fa1060453b94a979e9023c0e0f4/public/audio/switch-on.mp3
--------------------------------------------------------------------------------
/public/images/blog/reduce-cover.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BinarySenseiii/personal-website/6f4f0a97fbb86fa1060453b94a979e9023c0e0f4/public/images/blog/reduce-cover.webp
--------------------------------------------------------------------------------
/public/images/chain-gpt-logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/public/images/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BinarySenseiii/personal-website/6f4f0a97fbb86fa1060453b94a979e9023c0e0f4/public/images/logo.png
--------------------------------------------------------------------------------
/public/static/og-modulo-operator-a63a03.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BinarySenseiii/personal-website/6f4f0a97fbb86fa1060453b94a979e9023c0e0f4/public/static/og-modulo-operator-a63a03.webp
--------------------------------------------------------------------------------
/public/static/publishing-react-package-7c738b.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BinarySenseiii/personal-website/6f4f0a97fbb86fa1060453b94a979e9023c0e0f4/public/static/publishing-react-package-7c738b.webp
--------------------------------------------------------------------------------
/src/actions/mutations.ts:
--------------------------------------------------------------------------------
1 | import { useMutation, useQueryClient } from '@tanstack/react-query'
2 | import { toast } from 'sonner'
3 | import { fetchFunc } from '~/lib/axios'
4 | import { contactSchemaType } from '~/schema'
5 |
6 | export const useSendContactData = () =>
7 | useMutation({
8 | mutationFn: (data: contactSchemaType) => fetchFunc('/contact', { method: 'POST', data }),
9 | onError: error => toast.error(error.message),
10 | onSuccess: () => toast.success("I'll be in touch shortly."),
11 | })
12 |
13 | type TViewCount = { message: string }
14 |
15 | export const useIncrementViewCount = () => {
16 | const queryClient = useQueryClient()
17 |
18 | return useMutation({
19 | mutationFn: (slug: string) => fetchFunc(`/views/${slug}`, { method: 'POST' }),
20 | onSuccess: () => queryClient.invalidateQueries({ queryKey: ['POST_VIEWS'] }),
21 | })
22 | }
23 |
--------------------------------------------------------------------------------
/src/actions/queries.ts:
--------------------------------------------------------------------------------
1 | import { useQuery } from '@tanstack/react-query'
2 | import { fetchFunc } from '~/lib/axios'
3 |
4 | type TPostView = { views: { slug: string; count: number } }
5 |
6 | export const usePostViews = (slug: string) =>
7 | useQuery({
8 | queryKey: ['POST_VIEWS'],
9 | queryFn: () => fetchFunc(`/views/${slug}`, { method: 'GET' }),
10 | refetchOnWindowFocus: true,
11 | })
12 |
--------------------------------------------------------------------------------
/src/app/(main)/blog/[...slug]/page.tsx:
--------------------------------------------------------------------------------
1 | import {posts} from '#site/content'
2 | import Image from 'next/image'
3 | import {notFound} from 'next/navigation'
4 | import BackButton from '~/components/back-btn'
5 | import {MDXContent} from '~/components/mdx'
6 | import {
7 | JsonSchemaLD,
8 | PostComments,
9 | PostMetadata,
10 | TableOfContent,
11 | } from '~/components/post'
12 | import Tags from '~/components/tags'
13 | import {getSEOTags} from '~/lib/seo'
14 | import {cn} from '~/lib/utils'
15 | import '~/styles/mdx.css'
16 |
17 | interface BlogPostParams {
18 | params: {
19 | slug: string[]
20 | }
21 | }
22 |
23 | async function getPostFromParams(params: BlogPostParams['params']) {
24 | const slug = params?.slug?.join('/')
25 | const post = posts.find(post => post.slugAsParams === slug)
26 |
27 | if (post === undefined || !post.published) {
28 | return notFound()
29 | }
30 |
31 | return post
32 | }
33 |
34 | export async function generateStaticParams(): Promise<
35 | BlogPostParams['params'][]
36 | > {
37 | return posts.map(post => ({slug: post.slugAsParams.split('/')}))
38 | }
39 |
40 | export async function generateMetadata({params}: BlogPostParams) {
41 | const post = await getPostFromParams(params)
42 |
43 | return getSEOTags({
44 | title: post.title,
45 | description: post.description,
46 | canonicalUrlRelative: `/blog/${post.slugAsParams.split('/')}`,
47 | extraTags: {
48 | openGraph: {
49 | title: post.title,
50 | description: post.description,
51 | url: `/blog/${post.slug.split('/')}`,
52 | images: [
53 | {
54 | url: post.cover.src,
55 | width: 1200,
56 | height: 660,
57 | },
58 | ],
59 | locale: 'en_US',
60 | type: 'website',
61 | },
62 | },
63 | })
64 | }
65 |
66 | export default async function BlogDetail({params}: BlogPostParams) {
67 | const post = await getPostFromParams(params)
68 |
69 | return (
70 | <>
71 | {/* SCHEMA JSON-LD MARKUP FOR GOOGLE */}
72 |
73 |
74 | Back to Posts
75 |
76 |
83 |
84 |
85 |
86 |
87 |
96 |
97 |
98 |
99 |
103 |
104 |
105 |
106 |
107 |
108 | Tags
109 |
110 |
111 |
112 |
113 |
114 | >
115 | )
116 | }
117 |
--------------------------------------------------------------------------------
/src/app/(main)/blog/page.tsx:
--------------------------------------------------------------------------------
1 | import { posts } from '#site/content'
2 | import { PostList } from '~/components/post'
3 | import SearchInput from '~/components/search-input'
4 | import config from '~/config'
5 | import { getSEOTags } from '~/lib/seo'
6 |
7 | export const metadata: ReturnType = getSEOTags({
8 | title: `All Blogs - ${config.appName}`,
9 | description:
10 | "Welcome to my digital garden where I share what I'm learning about shipping great products, becoming a better developer and growing a career in tech.",
11 | canonicalUrlRelative: '/blogs',
12 | keywords: [
13 | 'JavaScript',
14 | 'TypeScript',
15 | 'React',
16 | 'Testing',
17 | 'Career',
18 | 'Software Development',
19 | 'Faisal tariq Blog',
20 | ],
21 | })
22 |
23 | const BlogPage = async ({ searchParams }: { searchParams: { search: string | undefined } }) => {
24 | const filteredPosts = posts.filter(post =>
25 | post.title.toLowerCase().includes(decodeURIComponent(searchParams.search || '')),
26 | )
27 |
28 | return (
29 |
30 |
31 |
All Publications
32 |
33 |
34 |
35 |
36 |
37 | )
38 | }
39 |
40 | export default BlogPage
41 |
--------------------------------------------------------------------------------
/src/app/(main)/layout.tsx:
--------------------------------------------------------------------------------
1 | import {ReactNode} from 'react'
2 | import Footer from '~/components/layout/footer'
3 | import Navbar from '~/components/layout/nav'
4 | import SkipContent from '~/components/ui/skip-content'
5 |
6 | const Layout = ({children}: {children: ReactNode}) => {
7 | return (
8 |
9 |
10 |
11 |
12 | {children}
13 |
14 |
15 |
16 | )
17 | }
18 |
19 | export default Layout
20 |
--------------------------------------------------------------------------------
/src/app/(main)/page.tsx:
--------------------------------------------------------------------------------
1 | import {posts} from '#site/content'
2 | import AboutSection from '~/components/about-section'
3 | import {PostList} from '~/components/post'
4 | import {ProjectList, projects} from '~/components/project'
5 | import Skills from '~/components/skills'
6 | import {sortPosts} from '~/lib/utils'
7 | import ContactUs from '../../components/contact-us'
8 |
9 | const HomePage = () => {
10 | const sortedPosts = sortPosts(posts.filter(post => post.published))
11 | return (
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | )
20 | }
21 |
22 | export default HomePage
23 |
--------------------------------------------------------------------------------
/src/app/(main)/projects/page.tsx:
--------------------------------------------------------------------------------
1 | import { ProjectList, projects } from '~/components/project'
2 | import SearchInput from '~/components/search-input'
3 | import config from '~/config'
4 | import { getSEOTags } from '~/lib/seo'
5 |
6 | export const metadata: ReturnType = getSEOTags({
7 | title: `All Projects - ${config.appName}`,
8 | description:
9 | 'Explore a digital garden of my projects, where I showcase insights on shipping exceptional products, advancing as a developer, and thriving in the tech industry',
10 | canonicalUrlRelative: '/projects',
11 | keywords: [
12 | 'JavaScript',
13 | 'TypeScript',
14 | 'React',
15 | 'Testing',
16 | 'Career',
17 | 'Software Development',
18 | ],
19 | })
20 |
21 | const ProjectsPage = ({ searchParams }: { searchParams: { search: string | undefined } }) => {
22 | const filteredProjects = projects.filter(project =>
23 | project.title.toLowerCase().includes(decodeURIComponent(searchParams.search || '')),
24 | )
25 |
26 | return (
27 |
28 |
29 |
All Projects
30 |
31 |
32 |
33 |
36 |
37 | )
38 | }
39 |
40 | export default ProjectsPage
41 |
--------------------------------------------------------------------------------
/src/app/(main)/tags/[tag]/page.tsx:
--------------------------------------------------------------------------------
1 | import {posts} from '#site/content'
2 | import {slug} from 'github-slugger'
3 | import React, {Fragment} from 'react'
4 | import {CustomLink} from '~/components/mdx'
5 | import {PostList} from '~/components/post'
6 | import SkipContent from '~/components/ui/skip-content'
7 | import config from '~/config'
8 | import {getSEOTags} from '~/lib/seo'
9 | import {getAllTags, getPostsByTagSlug} from '~/lib/utils'
10 |
11 | interface TagPageProps {
12 | params: {
13 | tag: string
14 | }
15 | }
16 |
17 | export async function generateMetadata({
18 | params,
19 | }: TagPageProps): Promise> {
20 | const {tag} = params
21 |
22 | return getSEOTags({
23 | title: `Tagged “${tag}” - ${config.appName}`,
24 | description: `Posts on the topic of ${tag}`,
25 | canonicalUrlRelative: `/tags/${slug(tag)}`,
26 | })
27 | }
28 |
29 | export const generateStaticParams = () => {
30 | const tags = getAllTags(posts)
31 | const paths = Object.keys(tags).map(tag => ({tag: slug(tag)}))
32 | return paths
33 | }
34 |
35 | const TagDetailPage: React.FC = ({params}) => {
36 | const {tag} = params
37 | const title = tag.split('-').join(' ')
38 |
39 | const displayPosts = getPostsByTagSlug(posts, tag)
40 |
41 | return (
42 |
43 |
44 | Tagged [ {title} ]
45 |
46 |
47 |
48 |
49 |
50 | Alternatively, choose from all tags or{' '}
51 | view all posts
52 |
53 |
54 | )
55 | }
56 |
57 | export default TagDetailPage
58 |
--------------------------------------------------------------------------------
/src/app/(main)/tags/page.tsx:
--------------------------------------------------------------------------------
1 | import {Post, posts} from '#site/content'
2 | import {CustomLink} from '~/components/mdx'
3 | import {PostList} from '~/components/post'
4 | import {Tag} from '~/components/tags'
5 | import config from '~/config'
6 | import {getSEOTags} from '~/lib/seo'
7 | import {getAllTags, sortedTagsCount} from '~/lib/utils'
8 |
9 | export const metadata: ReturnType = getSEOTags({
10 | title: `All Tags - ${config.appName}`,
11 | canonicalUrlRelative: '/tags',
12 | })
13 |
14 | type OrganizedPost = Record
15 |
16 | const TagsPage = () => {
17 | const tags = getAllTags(posts)
18 | const sortedTags = sortedTagsCount(tags)
19 |
20 | function organizePostsByTag(posts: Post[]): OrganizedPost {
21 | const organizedPosts: {[key: string]: Post[]} = {}
22 |
23 | posts.forEach(post => {
24 | post.tags.forEach(tag => {
25 | if (!organizedPosts[tag]) {
26 | organizedPosts[tag] = []
27 | }
28 | organizedPosts[tag].push(post)
29 | })
30 | })
31 |
32 | const sortedKeys = Object.keys(organizedPosts).sort()
33 | const result: OrganizedPost = {}
34 |
35 | sortedKeys.forEach(key => {
36 | result[key] = organizedPosts[key]
37 | })
38 |
39 | return result
40 | }
41 |
42 | const result: OrganizedPost = organizePostsByTag(posts)
43 |
44 | return (
45 |
46 |
47 | Posts by Tag (A-Z)
48 |
49 | {Object.keys(result).map(tag => (
50 |
55 |
56 | {tag}
57 |
58 |
59 |
60 | {result[tag].map((post, index) => (
61 |
62 |
63 | {post.title}
64 |
65 |
66 | ))}
67 |
68 |
69 | ))}
70 |
71 |
72 |
73 |
All Tags
74 |
75 | {sortedTags.map((tag, index) => (
76 |
77 | ))}
78 |
79 |
80 |
81 | )
82 | }
83 |
84 | export default TagsPage
85 |
--------------------------------------------------------------------------------
/src/app/api/contact/route.ts:
--------------------------------------------------------------------------------
1 | import { env } from '~/constants/env'
2 | import { ContactSchema, contactSchemaType } from '~/schema'
3 |
4 | async function sendDataToGoogleSheets(data: contactSchemaType): Promise {
5 | const requestOptions = {
6 | method: 'POST',
7 | headers: {
8 | 'Content-Type': 'application/json',
9 | },
10 | body: JSON.stringify([Object.values(data)]),
11 | }
12 |
13 | const response = await fetch(
14 | `https://v1.nocodeapi.com/faisal_dev/google_sheets/${env.NOCODE_API_KEY}?tabId=${env.NOCODE_TAB_ID}`,
15 | requestOptions,
16 | )
17 |
18 | if (!response.ok) {
19 | throw new Error('Failed to send data to Google Sheets')
20 | }
21 |
22 | return await response.json()
23 | }
24 |
25 | async function handlePostRequest(request: Request): Promise {
26 | try {
27 | const data = (await request.json()) as contactSchemaType
28 | const form = ContactSchema.safeParse(data)
29 |
30 | if (form.error) {
31 | return new Response(JSON.stringify(form.error.formErrors.fieldErrors), {
32 | status: 400,
33 | headers: {
34 | 'Content-Type': 'application/json',
35 | },
36 | })
37 | }
38 |
39 | const result = await sendDataToGoogleSheets(data)
40 |
41 | return new Response(JSON.stringify(result), {
42 | headers: {
43 | 'Content-Type': 'application/json',
44 | },
45 | status: 201,
46 | })
47 | } catch (error) {
48 | console.error('Error:', error)
49 | return new Response(JSON.stringify({ error: 'Internal Server Error' }), {
50 | status: 500,
51 | headers: {
52 | 'Content-Type': 'application/json',
53 | },
54 | })
55 | }
56 | }
57 |
58 | export async function POST(request: Request): Promise {
59 | return handlePostRequest(request)
60 | }
61 |
--------------------------------------------------------------------------------
/src/app/api/views/[slug]/route.ts:
--------------------------------------------------------------------------------
1 | import { z } from 'zod'
2 | import { db } from '~/server/db'
3 |
4 | interface Options {
5 | params: {
6 | slug: string
7 | }
8 | }
9 |
10 | export async function GET(_: Request, { params }: Options) {
11 | try {
12 | const slug = z.string().parse(params.slug)
13 | const views = await db.views.findUnique({ where: { slug } })
14 |
15 | return Response.json({ views }, { status: 200 })
16 | } catch (error) {
17 | const message = error instanceof Error ? error.message : 'Unexpected error'
18 | return Response.json({ message }, { status: 400 })
19 | }
20 | }
21 |
22 | export async function POST(_: Request, { params }: Options): Promise {
23 | try {
24 | const slug = z.string().parse(params.slug)
25 |
26 | // Check if the view already exists
27 | const existingView = await db.views.findUnique({ where: { slug } })
28 |
29 | if (existingView) {
30 | // If the view exists, update the count by incrementing it by 1
31 | await db.views.update({
32 | where: { slug },
33 | data: { count: existingView.count + 1 },
34 | })
35 | } else {
36 | // If the view doesn't exist, create a new one with count 1
37 | await db.views.create({
38 | data: { slug, count: 1 },
39 | })
40 | }
41 |
42 | return new Response(JSON.stringify({ message: 'Count incremented successfully' }), {
43 | status: 200,
44 | })
45 | } catch (error) {
46 | console.error('An error occurred while incrementing count:', error)
47 |
48 | return new Response(JSON.stringify({ message: 'Internal server error' }), {
49 | status: 500,
50 | })
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/src/app/apple-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BinarySenseiii/personal-website/6f4f0a97fbb86fa1060453b94a979e9023c0e0f4/src/app/apple-icon.png
--------------------------------------------------------------------------------
/src/app/error.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import {useEffect} from 'react'
4 | import SupportButton from '~/components/support-btn'
5 |
6 | export default function Error({
7 | error,
8 | reset,
9 | }: {
10 | error: Error & {digest?: string}
11 | reset: () => void
12 | }) {
13 | useEffect(() => {
14 | console.error('error::: ', error)
15 | }, [error])
16 |
17 | return (
18 |
19 |
20 |
21 |
27 |
31 |
35 |
39 |
43 |
44 |
45 |
49 |
50 |
54 |
58 |
59 |
63 |
67 |
68 |
69 |
73 |
77 |
81 |
85 |
89 |
93 |
97 |
101 |
102 |
103 | Internal Server Error
104 |
105 |
106 | {error?.message}
107 |
108 |
109 |
110 |
111 |
112 | )
113 | }
114 |
--------------------------------------------------------------------------------
/src/app/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BinarySenseiii/personal-website/6f4f0a97fbb86fa1060453b94a979e9023c0e0f4/src/app/favicon.ico
--------------------------------------------------------------------------------
/src/app/feed.xml/route.ts:
--------------------------------------------------------------------------------
1 | import {posts} from '#site/content'
2 | import RSS from 'rss'
3 | import config from '~/config'
4 | import {BasePath} from '~/lib/utils'
5 |
6 | export async function GET() {
7 | const feed = new RSS({
8 | title: `${config.appName} Personal Website`,
9 | generator: 'RSS for Personal Portfolio',
10 | feed_url: BasePath('/feed.xml'),
11 | site_url: BasePath('/'),
12 | managingEditor: `${config.social.email} (${config.appName})`,
13 | webMaster: `${config.social.email} (${config.appName})`,
14 | copyright: `Copyright ${new Date().getFullYear().toString()}, ${
15 | config.appName
16 | }`,
17 | language: 'en-US',
18 | pubDate: new Date().toUTCString(),
19 | ttl: 60,
20 | })
21 |
22 | posts.forEach(post => {
23 | feed.item({
24 | title: post.title,
25 | url: BasePath(`/blog/${post.slugAsParams.split('/')}`),
26 | date: post.date,
27 | description: post.description,
28 | author: 'Faisal Tariq',
29 | })
30 | })
31 |
32 | return new Response(feed.xml(), {
33 | headers: {
34 | 'Content-Type': 'application/xml; charset=utf-8',
35 | },
36 | })
37 | }
38 |
--------------------------------------------------------------------------------
/src/app/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BinarySenseiii/personal-website/6f4f0a97fbb86fa1060453b94a979e9023c0e0f4/src/app/icon.png
--------------------------------------------------------------------------------
/src/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import { Analytics } from '@vercel/analytics/react'
2 | import { SpeedInsights } from '@vercel/speed-insights/next'
3 | import { dankMono, fontSans, ubuntu } from '~/components/ui/fonts'
4 | import { getSEOTags, renderSchemaTags } from '~/lib/seo'
5 | import { cn } from '~/lib/utils'
6 | import RootProviders from '~/providers'
7 | import '~/styles/globals.css'
8 |
9 | export const viewport = {
10 | viewportFit: 'cover',
11 | width: 'device-width',
12 | initialScale: 1,
13 | maximumScale: 3,
14 | userScalable: true,
15 | themeColor: [
16 | { media: '(prefers-color-scheme: light)', color: 'white' },
17 | { media: '(prefers-color-scheme: dark)', color: 'black' },
18 | ],
19 | }
20 |
21 | export const metadata = getSEOTags()
22 |
23 | export default function RootLayout({
24 | children,
25 | }: Readonly<{
26 | children: React.ReactNode
27 | }>) {
28 | return (
29 |
30 |
37 | {renderSchemaTags()}
38 |
39 | {children}
40 |
41 | {process.env.NODE_ENV === 'production' && (
42 | <>
43 |
44 |
45 | >
46 | )}
47 |
48 |
49 | )
50 | }
51 |
--------------------------------------------------------------------------------
/src/app/not-found.tsx:
--------------------------------------------------------------------------------
1 | import SupportButton from '~/components/support-btn'
2 |
3 | export default function Custom404() {
4 | return (
5 |
6 |
7 |
8 |
13 |
17 |
21 |
25 |
29 |
30 |
34 |
38 |
42 |
46 |
50 |
54 |
58 |
62 |
66 |
70 |
74 |
75 |
76 |
77 |
78 |
79 |
83 |
84 |
88 |
89 |
90 |
94 |
95 |
96 |
97 | This page doesn't exist
98 |
99 |
100 |
101 | Oops! It seems like you've stumbled upon a page that doesn't
102 | exist Don't worry, even the best of us get lost sometimes. Feel
103 | free to navigate back to Home or contact us if you need help
104 |
105 |
106 |
107 |
108 |
109 | )
110 | }
111 |
--------------------------------------------------------------------------------
/src/app/opengraph-image.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BinarySenseiii/personal-website/6f4f0a97fbb86fa1060453b94a979e9023c0e0f4/src/app/opengraph-image.png
--------------------------------------------------------------------------------
/src/app/robot.ts:
--------------------------------------------------------------------------------
1 | import {MetadataRoute} from 'next'
2 | import config from '~/config'
3 |
4 | export default function robots(): MetadataRoute.Robots {
5 | return {
6 | rules: {
7 | userAgent: '*',
8 | allow: '/',
9 | disallow: '/private/',
10 | },
11 | sitemap: `https://${config.domainName}/sitemap.xml`,
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/app/sitemap.ts:
--------------------------------------------------------------------------------
1 | import { posts } from '#site/content'
2 | import { MetadataRoute } from 'next'
3 | import { BasePath } from '~/lib/utils'
4 |
5 | export default async function sitemap(): Promise {
6 | const blogPosts = posts.map(post => ({
7 | url: BasePath(`/blog/${post.slugAsParams.split('/')}`),
8 | lastModified: post.date,
9 | }))
10 |
11 | return [
12 | {
13 | url: BasePath(''),
14 | lastModified: new Date(),
15 | },
16 |
17 | {
18 | url: BasePath('/blog'),
19 | lastModified: new Date(),
20 | },
21 | {
22 | url: BasePath('/projects'),
23 | lastModified: new Date(),
24 | },
25 | {
26 | url: BasePath('/about'),
27 | lastModified: new Date(),
28 | },
29 |
30 | ...blogPosts,
31 | ]
32 | }
33 |
--------------------------------------------------------------------------------
/src/app/twitter-image.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BinarySenseiii/personal-website/6f4f0a97fbb86fa1060453b94a979e9023c0e0f4/src/app/twitter-image.png
--------------------------------------------------------------------------------
/src/assets/images/cover/chain-gpt.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BinarySenseiii/personal-website/6f4f0a97fbb86fa1060453b94a979e9023c0e0f4/src/assets/images/cover/chain-gpt.png
--------------------------------------------------------------------------------
/src/assets/images/cover/luma.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BinarySenseiii/personal-website/6f4f0a97fbb86fa1060453b94a979e9023c0e0f4/src/assets/images/cover/luma.png
--------------------------------------------------------------------------------
/src/assets/images/cover/mint-kuto.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BinarySenseiii/personal-website/6f4f0a97fbb86fa1060453b94a979e9023c0e0f4/src/assets/images/cover/mint-kuto.png
--------------------------------------------------------------------------------
/src/assets/images/cover/podportal.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BinarySenseiii/personal-website/6f4f0a97fbb86fa1060453b94a979e9023c0e0f4/src/assets/images/cover/podportal.png
--------------------------------------------------------------------------------
/src/assets/images/cover/power-up.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BinarySenseiii/personal-website/6f4f0a97fbb86fa1060453b94a979e9023c0e0f4/src/assets/images/cover/power-up.png
--------------------------------------------------------------------------------
/src/assets/images/cover/world-ranks.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BinarySenseiii/personal-website/6f4f0a97fbb86fa1060453b94a979e9023c0e0f4/src/assets/images/cover/world-ranks.png
--------------------------------------------------------------------------------
/src/assets/images/f-dubai-police.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BinarySenseiii/personal-website/6f4f0a97fbb86fa1060453b94a979e9023c0e0f4/src/assets/images/f-dubai-police.webp
--------------------------------------------------------------------------------
/src/assets/images/mint-kuto.avif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BinarySenseiii/personal-website/6f4f0a97fbb86fa1060453b94a979e9023c0e0f4/src/assets/images/mint-kuto.avif
--------------------------------------------------------------------------------
/src/assets/images/nft-connect.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BinarySenseiii/personal-website/6f4f0a97fbb86fa1060453b94a979e9023c0e0f4/src/assets/images/nft-connect.jpg
--------------------------------------------------------------------------------
/src/assets/images/nft-connect.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BinarySenseiii/personal-website/6f4f0a97fbb86fa1060453b94a979e9023c0e0f4/src/assets/images/nft-connect.webp
--------------------------------------------------------------------------------
/src/assets/images/power-up.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BinarySenseiii/personal-website/6f4f0a97fbb86fa1060453b94a979e9023c0e0f4/src/assets/images/power-up.webp
--------------------------------------------------------------------------------
/src/assets/images/world-rank.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BinarySenseiii/personal-website/6f4f0a97fbb86fa1060453b94a979e9023c0e0f4/src/assets/images/world-rank.png
--------------------------------------------------------------------------------
/src/assets/svg/chain-gpt.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import {SVGProps} from 'react'
3 | interface SVGRProps {
4 | title?: string
5 | titleId?: string
6 | }
7 | const ChainGpt = ({title, titleId, ...props}: SVGProps & SVGRProps) => (
8 |
9 |
17 | {title ? {title} : null}
18 |
22 |
26 |
30 |
34 |
38 |
39 |
47 |
48 |
49 |
50 |
58 |
59 |
60 |
61 |
69 |
70 |
71 |
72 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 | )
88 | export default ChainGpt
89 |
--------------------------------------------------------------------------------
/src/assets/svg/index.ts:
--------------------------------------------------------------------------------
1 | export {default as LumaIcon} from './luma'
2 | export {default as ChainGpt} from './chain-gpt'
3 |
--------------------------------------------------------------------------------
/src/assets/svg/luma.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | const LumaIcon = () => {
4 | return (
5 |
19 | )
20 | }
21 |
22 | export default LumaIcon
23 |
--------------------------------------------------------------------------------
/src/components/about-section.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 | import {useEffect, useRef} from 'react'
3 | import {annotate, annotationGroup} from 'rough-notation'
4 | import {
5 | RoughAnnotationConfig,
6 | RoughAnnotationGroup,
7 | } from 'rough-notation/lib/model'
8 |
9 | import config from '~/config'
10 | import {useMediaQuery} from '~/hooks'
11 | import {cn} from '~/lib/utils'
12 | import {typo} from './ui/typograpghy'
13 | import Image from 'next/image'
14 | import dubaiCon from '~/assets/images/f-dubai-police.webp'
15 | import {annotationsConfig} from '~/constants/anime'
16 |
17 | const AboutSection = () => {
18 | const isSmallDevice = useMediaQuery('(max-width: 500px)')
19 | const annotationRefs = annotationsConfig.map(() =>
20 | // eslint-disable-next-line react-hooks/rules-of-hooks
21 | useRef(null),
22 | )
23 |
24 | useEffect(() => {
25 | const annotations = annotationsConfig.map((config, index) => {
26 | const {ref, ...options} = config
27 | return annotate(
28 | annotationRefs[index]!.current!,
29 | options as RoughAnnotationConfig,
30 | )
31 | })
32 |
33 | const annotationGroupInstance: RoughAnnotationGroup =
34 | annotationGroup(annotations)
35 |
36 | if (!isSmallDevice) {
37 | annotationGroupInstance.show()
38 | }
39 |
40 | return () => annotationGroupInstance.hide()
41 | }, [annotationRefs, isSmallDevice])
42 |
43 | return (
44 |
45 |
46 |
47 | Hello , I'm Faisal from UAE.
48 |
49 |
50 |
51 | Experienced self-taught developer with over{' '}
52 |
53 | 5+ years
54 | {' '}
55 | of crafting advanced SaaS products and B2B solutions, specializing in
56 | transforming imaginative designs into robust, scalable web solutions
57 | that set new standards.
58 |
59 |
60 |
61 | I Love building tools that are{' '}
62 |
63 | user friendly, simple
64 | {' '}
65 | and{' '}
66 |
67 | delightful
68 |
69 | .
70 |
71 |
72 |
73 | Through these experiences, I had the opportunity to work with both
74 | small and large companies, as well as specialized and cross-functional
75 | teams across different time zones & developed a working style that
76 | prioritizes{' '}
77 |
78 | flexibility, clarity
79 | {' '}
80 | and{' '}
81 |
82 | collaboration.
83 |
84 |
85 |
86 |
89 | I'm currently looking for a new role as a developer.{' '}
90 | }
92 | href={`mailto:${config.social.email}`}
93 | aria-label="Hire me"
94 | className="text-ring el-focus-styles"
95 | >
96 | Hire me?
97 |
98 |
99 |
100 |
101 |
111 |
112 | )
113 | }
114 |
115 | export default AboutSection
116 |
--------------------------------------------------------------------------------
/src/components/back-btn.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 | import React, {ReactNode} from 'react'
3 | import {Button} from './ui/button'
4 | import {MoveLeft} from 'lucide-react'
5 | import {useRouter} from 'next/navigation'
6 |
7 | const BackButton = ({children}: {children: ReactNode}) => {
8 | const router = useRouter()
9 | return (
10 | router.back()}
14 | >
15 |
16 | {children}
17 |
18 | )
19 | }
20 |
21 | export default BackButton
22 |
--------------------------------------------------------------------------------
/src/components/contact-us.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 | import { zodResolver } from '@hookform/resolvers/zod'
3 | import { useForm } from 'react-hook-form'
4 | import { useSendContactData } from '~/actions/mutations'
5 | import { Form, FormControl, FormField, FormItem, FormMessage } from '~/components/ui/form'
6 | import config from '~/config'
7 | import { ContactSchema, contactSchemaType } from '~/schema'
8 | import { CustomLink } from './mdx'
9 | import Socials from './socials'
10 | import { Button } from './ui/button'
11 | import { Input } from './ui/input'
12 | import { Textarea } from './ui/textarea'
13 | import { typo } from './ui/typograpghy'
14 |
15 | const ContactUs = () => {
16 | const { mutate, isPending } = useSendContactData()
17 | const form = useForm({
18 | resolver: zodResolver(ContactSchema),
19 | defaultValues: {
20 | fullName: '',
21 | phone: '',
22 | email: '',
23 | message: '',
24 | },
25 | })
26 |
27 | function onSubmit(data: contactSchemaType) {
28 | mutate(data, {
29 | onSuccess: () => form.reset(),
30 | })
31 | }
32 |
33 | return (
34 |
35 |
36 |
37 |
Get in Touch
38 |
39 | If you have any inquiries, please feel free to reach out. You can contact us via email
40 | at{' '}
41 |
42 | {config.social.email}
43 | {' '}
44 |
45 |
46 |
47 |
Follow me
48 |
49 |
50 |
51 |
52 |
116 |
117 |
118 |
119 | )
120 | }
121 |
122 | export default ContactUs
123 |
--------------------------------------------------------------------------------
/src/components/layout/footer.tsx:
--------------------------------------------------------------------------------
1 | import config from '~/config'
2 | import { CustomLink } from '../mdx'
3 |
4 | const Footer = () => {
5 | return (
6 |
7 |
8 |
© {new Date().getFullYear()} Design & Developed by ❤️
9 |
10 |
11 |
12 |
13 | RSS FEED
14 |
15 |
16 | /
17 |
18 | SITE MAP
19 |
20 |
21 |
22 |
23 | )
24 | }
25 |
26 | export default Footer
27 |
--------------------------------------------------------------------------------
/src/components/layout/nav/_nav-mock.ts:
--------------------------------------------------------------------------------
1 | import {createId} from '@paralleldrive/cuid2'
2 |
3 | export const navData = [
4 | {
5 | id: createId(),
6 | label: 'Home',
7 | path: '/',
8 | },
9 | {
10 | id: createId(),
11 | label: 'Projects',
12 | path: '/projects',
13 | },
14 | {
15 | id: createId(),
16 | label: 'Blog',
17 | path: '/blog',
18 | },
19 |
20 | // {
21 | // id: createId(),
22 | // label: 'Guests',
23 | // path: '/guests',
24 | // },
25 |
26 | // {
27 | // id: createId(),
28 | // label: 'Contact',
29 | // path: '/contact',
30 | // },
31 | ]
32 |
33 | export type NavType = typeof navData
34 |
--------------------------------------------------------------------------------
/src/components/layout/nav/index.tsx:
--------------------------------------------------------------------------------
1 | import Logo from './logo'
2 | import MobileNav from './mobile-nav'
3 | import NavList from './nav-list'
4 |
5 | const Navbar = () => {
6 | return (
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | )
19 | }
20 |
21 | export default Navbar
22 |
--------------------------------------------------------------------------------
/src/components/layout/nav/logo.tsx:
--------------------------------------------------------------------------------
1 | import Image from 'next/image'
2 | import Link from 'next/link'
3 | import React from 'react'
4 |
5 | const Logo = () => {
6 | return (
7 |
13 |
20 |
21 | )
22 | }
23 |
24 | export default Logo
25 |
--------------------------------------------------------------------------------
/src/components/layout/nav/mobile-nav.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 | import { Menu } from 'lucide-react'
3 | import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger } from '~/components/ui/sheet'
4 | import Logo from './logo'
5 | import NavList from './nav-list'
6 | import { useState } from 'react'
7 |
8 | const MobileNav = () => {
9 | const [open, setOpen] = useState(false)
10 | return (
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 | )
26 | }
27 |
28 | export default MobileNav
29 |
--------------------------------------------------------------------------------
/src/components/layout/nav/nav-item.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 | import Link from 'next/link'
3 | import React, { Dispatch, SetStateAction } from 'react'
4 | import { NavType } from './_nav-mock'
5 | import { usePathname } from 'next/navigation'
6 | import { cn } from '~/lib/utils'
7 | import { motion } from 'framer-motion'
8 |
9 | const NavItem: React.FC> }> = ({
10 | label,
11 | path,
12 | setOpen,
13 | }) => {
14 | const pathname = usePathname()
15 |
16 | const onClickHandler = () => {
17 | if (typeof setOpen === 'function') {
18 | setOpen(false)
19 | }
20 | }
21 |
22 | return (
23 |
33 |
39 | {label}
40 |
41 |
42 | {pathname === path && (
43 |
48 |
49 |
50 | )}
51 |
52 | )
53 | }
54 |
55 | export default NavItem
56 |
--------------------------------------------------------------------------------
/src/components/layout/nav/nav-list.tsx:
--------------------------------------------------------------------------------
1 | import { Dispatch, SetStateAction } from 'react'
2 | import { navData } from './_nav-mock'
3 | import NavItem from './nav-item'
4 |
5 | interface NavProps {
6 | setOpen?: Dispatch>
7 | }
8 |
9 | const NavList: React.FC = ({ setOpen }) => {
10 | return (
11 |
15 | {navData.map(nav => (
16 |
17 | ))}
18 |
19 | )
20 | }
21 |
22 | export default NavList
23 |
--------------------------------------------------------------------------------
/src/components/mdx/custom-image.tsx:
--------------------------------------------------------------------------------
1 | import Image, {ImageProps} from 'next/image'
2 |
3 | const CustomImage: React.FC = ({...props}) => {
4 | return (
5 |
6 |
14 |
15 | )
16 | }
17 |
18 | export default CustomImage
19 |
--------------------------------------------------------------------------------
/src/components/mdx/custom-link.tsx:
--------------------------------------------------------------------------------
1 | import React, {AnchorHTMLAttributes, ReactNode} from 'react'
2 | import Link from 'next/link'
3 | import {ClassValue} from 'clsx'
4 | import {cn} from '~/lib/utils'
5 | import {buttonVariants} from '../ui/button'
6 |
7 | const CustomLink = (
8 | props: {
9 | href: string
10 | children: ReactNode
11 | className?: ClassValue
12 | } & AnchorHTMLAttributes,
13 | ) => {
14 | const href = props.href
15 | const isInternalLink = href && href.startsWith('/')
16 |
17 | const linkVariant = buttonVariants({
18 | variant: 'link',
19 | className: cn(
20 | '!p-0 h-full !inline-block !whitespace-normal !text-base !text-ring',
21 | props.className,
22 | ),
23 | })
24 |
25 | if (isInternalLink) {
26 | return (
27 |
28 | {props.children}
29 |
30 | )
31 | }
32 |
33 | return (
34 |
35 | {props.children}
36 |
37 | )
38 | }
39 |
40 | export default CustomLink
41 |
--------------------------------------------------------------------------------
/src/components/mdx/index.ts:
--------------------------------------------------------------------------------
1 | export {default as MDXContent} from './mdx-content'
2 | export {default as CustomLink} from './custom-link'
3 |
--------------------------------------------------------------------------------
/src/components/mdx/mdx-content.tsx:
--------------------------------------------------------------------------------
1 | import * as runtime from 'react/jsx-runtime'
2 | import Callout from '~/components/ui/callout'
3 | import {YouTubeEmbed} from '@next/third-parties/google'
4 | import CustomImage from './custom-image'
5 | import CustomLink from './custom-link'
6 |
7 | const sharedComponents = {
8 | CustomImage,
9 | CustomLink,
10 | YouTubeEmbed,
11 | Callout,
12 | }
13 |
14 | const useMDXComponent = (code: string) => {
15 | const fn = new Function(code)
16 | return fn({...runtime}).default
17 | }
18 |
19 | interface MDXProps {
20 | code: string
21 | components?: Record
22 | }
23 |
24 | const MDXContent = ({code, components}: MDXProps) => {
25 | const Component = useMDXComponent(code)
26 | return
27 | }
28 |
29 | export default MDXContent
30 |
--------------------------------------------------------------------------------
/src/components/post/index.ts:
--------------------------------------------------------------------------------
1 | export {default as PostList} from './post-list'
2 | export {default as PostComments} from './post-comments'
3 | export {default as TableOfContent} from './post-toc'
4 | export {default as PostMetadata} from './post-metadata'
5 | export {default as JsonSchemaLD} from './post-json-schema'
6 |
--------------------------------------------------------------------------------
/src/components/post/post-comments.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 | import React from 'react'
3 | import Giscus from '@giscus/react'
4 | import {env} from '~/constants/env'
5 |
6 | const PostComments = () => {
7 | return (
8 |
9 |
24 |
25 | )
26 | }
27 |
28 | export default PostComments
29 |
--------------------------------------------------------------------------------
/src/components/post/post-item.tsx:
--------------------------------------------------------------------------------
1 | import {Post} from '#site/content'
2 | import React from 'react'
3 |
4 | import Link from 'next/link'
5 | import {Card, CardContent} from '~/components/ui/card'
6 | import PostMetadata from './post-metadata'
7 | import Tags from '../tags'
8 |
9 | const PostItem: React.FC = ({
10 | date,
11 | title,
12 | description,
13 | metadata,
14 | slugAsParams,
15 | tags,
16 | }) => {
17 | return (
18 |
19 |
20 |
24 |
25 |
26 |
27 |
32 | {description}
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 | )
41 | }
42 |
43 | export default PostItem
44 |
--------------------------------------------------------------------------------
/src/components/post/post-json-schema.tsx:
--------------------------------------------------------------------------------
1 | import {Post} from '#site/content'
2 | import Script from 'next/script'
3 | import React from 'react'
4 | import config from '~/config'
5 |
6 | const JsonSchemaLD = ({post}: {post: Post}) => {
7 | return (
8 |
32 | )
33 | }
34 |
35 | export default JsonSchemaLD
36 |
--------------------------------------------------------------------------------
/src/components/post/post-list.tsx:
--------------------------------------------------------------------------------
1 | import {Post} from '#site/content'
2 | import React from 'react'
3 | import ContentNotFound from '../ui/content-not-found'
4 | import {typo} from '../ui/typograpghy'
5 | import PostItem from './post-item'
6 |
7 | type PostListProps = {
8 | posts: Post[]
9 | showRss?: boolean
10 | }
11 |
12 | const PostList: React.FC = ({posts, showRss}) => {
13 | return (
14 |
15 | {showRss && Most recent posts }
16 |
17 | {posts.length > 0 ? (
18 | posts.map(post => )
19 | ) : (
20 |
21 | )}
22 |
23 |
24 | )
25 | }
26 | export default PostList
27 |
--------------------------------------------------------------------------------
/src/components/post/post-metadata.tsx:
--------------------------------------------------------------------------------
1 | import {Post} from '#site/content'
2 | import {Calendar, Timer} from 'lucide-react'
3 | import React from 'react'
4 | import {cn, formatDate} from '~/lib/utils'
5 | import PostViews from './post-views'
6 |
7 | interface PostMetaProps {
8 | title: string
9 | date: string
10 | metadata: Post['metadata']
11 | isDetailPage?: boolean
12 | slug?: string
13 | }
14 | const PostMetadata: React.FC = ({
15 | title,
16 | date,
17 | metadata,
18 | isDetailPage,
19 | slug,
20 | }) => {
21 | return (
22 |
23 |
31 | {title}
32 |
33 |
34 |
39 |
40 | Published on
41 |
49 | {isDetailPage ? (
50 | Published on
51 | ) : (
52 |
56 | )}
57 |
61 | {formatDate(date)}
62 |
63 |
64 |
65 |
66 |
67 | {isDetailPage &&
}
68 |
69 | Reading time
70 |
78 |
82 | {metadata.readingTime} min read
83 |
84 |
85 |
86 |
87 |
88 | )
89 | }
90 |
91 | export default PostMetadata
92 |
--------------------------------------------------------------------------------
/src/components/post/post-toc.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 | import {Post} from '#site/content'
3 | import {ClassValue} from 'clsx'
4 | import React, {useState} from 'react'
5 | import {
6 | Accordion,
7 | AccordionContent,
8 | AccordionItem,
9 | AccordionTrigger,
10 | } from '~/components/ui/accordion'
11 |
12 | const TOC_STYLES: ClassValue =
13 | 'el-focus-styles rounded-sm hover:underline text-muted-foreground hover:text-ring'
14 |
15 | const PostTableOfContent = ({toc}: {toc: Post['toc']}) => {
16 | const [tocValue, setTocValue] = useState('')
17 |
18 | const renderTocItems = (items: Post['toc']) => {
19 | return (
20 |
21 | {items.map(item => (
22 |
23 |
24 | {item.title}
25 |
26 | {item.items.length > 0 && renderTocItems(item.items)}
27 |
28 | ))}
29 |
30 | )
31 | }
32 |
33 | return (
34 |
35 |
36 |
37 |
38 | Table of Contents{' '}
39 | (Click to {tocValue ? 'Close' : 'Open'})
40 |
41 |
42 |
43 |
44 | {toc.map(tocItem => (
45 |
46 |
47 | {tocItem.title}
48 |
49 | {tocItem.items.length > 0 && renderTocItems(tocItem.items)}
50 |
51 | ))}
52 |
53 |
54 |
55 |
56 | )
57 | }
58 |
59 | export default PostTableOfContent
60 |
--------------------------------------------------------------------------------
/src/components/post/post-views.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 | import {Eye} from 'lucide-react'
3 | import React, {useEffect} from 'react'
4 | import {useIncrementViewCount} from '~/actions/mutations'
5 | import {usePostViews} from '~/actions/queries'
6 |
7 | const PostViews = ({slug}: {slug: string}) => {
8 | const {data, isLoading} = usePostViews(slug)
9 | const incrCount = useIncrementViewCount()
10 |
11 | useEffect(() => {
12 | incrCount.mutate(slug)
13 | // eslint-disable-next-line react-hooks/exhaustive-deps
14 | }, [])
15 |
16 | return (
17 |
18 | Blog Post views
19 |
20 |
21 | {isLoading ? '...' : data?.views?.count ?? 0} Views
22 |
23 |
24 | )
25 | }
26 |
27 | export default PostViews
28 |
--------------------------------------------------------------------------------
/src/components/project/_project-mock.ts:
--------------------------------------------------------------------------------
1 | import {createId} from '@paralleldrive/cuid2'
2 | import {ChainGpt, LumaIcon} from '~/assets/svg'
3 | import {MintKuto, PodPortal, PowerUp, worldRank} from './project-icons'
4 |
5 | // Project cover images
6 | import lumaCover from '~/assets/images/cover/luma.png'
7 | import chainGptCover from '~/assets/images/cover/chain-gpt.png'
8 | import powerUpCover from '~/assets/images/cover/power-up.png'
9 | import worldRankCover from '~/assets/images/cover/world-ranks.png'
10 | import podPortalCover from '~/assets/images/cover/podportal.png'
11 | import mintKutoCover from '~/assets/images/cover/mint-kuto.png'
12 |
13 | const projects = [
14 | {
15 | id: createId(),
16 | Icon: LumaIcon,
17 | title: `Luma - Delightful events start here.`,
18 | description:
19 | 'Find Luma events, join groups, or start your own. Make new friends and connect with like-minded people. Meet people near you who share your interests.',
20 | deployedURL: 'https://lu.ma/',
21 | cover: lumaCover,
22 | stacks: ['Next.js', 'Typescript', 'Stripe', 'CSS-in-JS solution'],
23 | isRepo: false,
24 | },
25 |
26 | {
27 | id: createId(),
28 | Icon: ChainGpt,
29 | title: `ChainGPT - The Power of Blockchain AI`,
30 | description:
31 | 'The premiere AI-powered dashboard for Web3. Featuring advanced Crypto AI tools for traders, NFT creators, developers, and the crypto community.',
32 | deployedURL: 'https://app.chaingpt.org/',
33 | cover: chainGptCover,
34 | stacks: [
35 | 'React',
36 | 'Node.js',
37 | 'Web3',
38 | 'Ethers',
39 | 'GraphQL',
40 | 'TypeScript',
41 | 'Chakra-UI',
42 | ],
43 | isRepo: false,
44 | },
45 | {
46 | id: createId(),
47 | Icon: PowerUp,
48 | title: `PowerUp - Brain Battles`,
49 | description:
50 | 'Thrilling and mind-bending knowledge games offer the perfect opportunity for you to showcase your expertise and win big by competing in fast-paced rounds against other players.',
51 | deployedURL: 'https://nftpowerup.io/',
52 | cover: powerUpCover,
53 | stacks: ['Next.js', 'Ether.js', 'APis', 'TailwindCSS'],
54 | isRepo: false,
55 | },
56 |
57 | {
58 | id: createId(),
59 | Icon: PodPortal,
60 | title: `Pod Portal - Your Gateway to Endless Audio Adventures`,
61 | description:
62 | 'Dive into a world of captivating stories, insightful discussions, and entertaining conversations with our curated selection of podcasts',
63 | deployedURL: 'https://podcast-ten-beta.vercel.app/explore',
64 | cover: podPortalCover,
65 | stacks: ['Next.js', 'TypeScript', 'Itunes APi', 'Emotion JS'],
66 | isRepo: true,
67 | repoUrl: 'https://github.com/BinarySenseiii/podcast',
68 | },
69 |
70 | {
71 | id: createId(),
72 | Icon: MintKuto,
73 | title: `ManyLives - The Potential of Kuto Digital Characters`,
74 | description:
75 | 'Step into the world of Kuto, where you can mint your own digital assets and unlock endless opportunities for creativity and innovation.',
76 | deployedURL: 'https://mintkuto.manylives.io/',
77 | cover: mintKutoCover,
78 | stacks: ['Solidity', 'React', 'Redux', 'Web3.js', 'Sass'],
79 | isRepo: false,
80 | },
81 |
82 | {
83 | id: createId(),
84 | Icon: worldRank,
85 | title: `World Ranks - Ultimate source for country rankings`,
86 | description:
87 | 'Explore comprehensive currency and population data worldwide with World Ranks. Stay informed, make smarter decisions, and broaden your global understanding',
88 | deployedURL: 'https://world-ranks-murex.vercel.app/',
89 | cover: worldRankCover,
90 | stacks: ['React', 'D3.js', 'Node.js', 'Country APi'],
91 | isRepo: true,
92 | repoUrl: 'https://github.com/BinarySenseiii/world-ranks',
93 | },
94 | ]
95 |
96 | export default projects
97 | export type TProject = (typeof projects)[0]
98 |
--------------------------------------------------------------------------------
/src/components/project/index.ts:
--------------------------------------------------------------------------------
1 | export {default as ProjectList} from './project-list'
2 | export {default as projects} from './_project-mock'
3 |
--------------------------------------------------------------------------------
/src/components/project/project-icons.tsx:
--------------------------------------------------------------------------------
1 | import Image from 'next/image'
2 | import React from 'react'
3 | import powerUpLogo from '~/assets/images/power-up.webp'
4 | import mintKutoLogo from '~/assets/images/mint-kuto.avif'
5 | import NftConnectLogo from '~/assets/images/nft-connect.jpg'
6 | import worldRankLogo from '~/assets/images/world-rank.png'
7 | import {cn} from '~/lib/utils'
8 |
9 | const DEFAULT_CLASS =
10 | 'size-16 border bg-[#11117c] grid place-content-center rounded-md p-3'
11 |
12 | export const PowerUp = () => {
13 | return (
14 |
15 |
16 |
17 | )
18 | }
19 | export const MintKuto = () => {
20 | return (
21 |
22 |
27 |
28 | )
29 | }
30 | export const NftConnect = () => {
31 | return (
32 |
33 |
38 |
39 | )
40 | }
41 |
42 | export const PodPortal = () => {
43 | return (
44 |
45 |
P
46 |
47 | )
48 | }
49 |
50 | export const worldRank = () => {
51 | return (
52 |
53 |
58 |
59 | )
60 | }
61 |
--------------------------------------------------------------------------------
/src/components/project/project-item.tsx:
--------------------------------------------------------------------------------
1 | import Image from 'next/image'
2 | import React from 'react'
3 | import {FaExternalLinkAlt} from 'react-icons/fa'
4 | import {FaGithub} from 'react-icons/fa6'
5 | import {cn} from '~/lib/utils'
6 | import {CustomLink} from '../mdx'
7 | import {typo} from '../ui/typograpghy'
8 | import {TProject} from './_project-mock'
9 |
10 | type ProjectItemProps = {} & TProject
11 |
12 | const linkClass =
13 | '!p-0 h-full hover:!text-[#25dde5] !flex items-center gap-2 !text-sm !text-ring'
14 |
15 | const ProjectItem: React.FC = ({
16 | Icon,
17 | title,
18 | description,
19 | deployedURL,
20 | cover,
21 | stacks,
22 | isRepo,
23 | repoUrl,
24 | }) => {
25 | return (
26 |
27 |
28 |
29 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 | {title}
44 |
45 |
46 | {stacks.join(' / ')}
47 |
48 |
49 |
55 | {description}
56 |
57 |
58 |
59 |
64 |
65 | Live Preview
66 |
67 |
68 | {isRepo && (
69 |
74 |
75 | Repo Url
76 |
77 | )}
78 |
79 |
80 |
81 |
82 | )
83 | }
84 | export default ProjectItem
85 |
86 | {
87 | /*
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
*/
96 | }
97 |
--------------------------------------------------------------------------------
/src/components/project/project-list.tsx:
--------------------------------------------------------------------------------
1 | import ContentNotFound from '../ui/content-not-found'
2 | import {typo} from '../ui/typograpghy'
3 | import {TProject} from './_project-mock'
4 |
5 | import ProjectItem from './project-item'
6 |
7 | const ProjectList = ({
8 | projects,
9 | metadata,
10 | }: {
11 | projects: TProject[]
12 | metadata?: boolean
13 | }) => {
14 | return (
15 |
16 | {metadata && Featured Projects }
17 |
18 |
19 | {projects.length > 0 ? (
20 |
21 | {projects.map(project => (
22 |
23 | ))}
24 |
25 | ) : (
26 |
27 | )}
28 |
29 |
30 | )
31 | }
32 |
33 | export default ProjectList
34 |
--------------------------------------------------------------------------------
/src/components/scroll-progress.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 | import React from 'react'
3 | import {motion, useScroll} from 'framer-motion'
4 |
5 | const ScrollProgress = () => {
6 | const {scrollYProgress} = useScroll()
7 | return (
8 |
12 | )
13 | }
14 |
15 | export default ScrollProgress
16 |
--------------------------------------------------------------------------------
/src/components/search-input.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 | import { Search } from 'lucide-react'
3 | import { usePathname, useRouter, useSearchParams } from 'next/navigation'
4 | import React from 'react'
5 | import { Input } from '~/components/ui/input'
6 |
7 | const SearchInput = () => {
8 | const searchParams = useSearchParams();
9 | const pathname = usePathname();
10 | const { replace } = useRouter();
11 |
12 |
13 | const onChangeHandle = (e: React.ChangeEvent) => {
14 | const term = e.target.value
15 | const params = new URLSearchParams(searchParams);
16 |
17 | term ? params.set('search', term) : params.delete('search')
18 |
19 | replace(`${pathname}?${params.toString()}`);
20 | }
21 |
22 | return (
23 |
24 |
25 |
32 |
33 | )
34 | }
35 |
36 | export default SearchInput
37 |
--------------------------------------------------------------------------------
/src/components/skills.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 | import Marquee from 'react-fast-marquee'
3 | import {BACKEND_STACKS, FRONTEND_STACKS, stacksProps} from '~/constants/stack'
4 | import {typo} from './ui/typograpghy'
5 |
6 | const Skills = () => {
7 | return (
8 |
9 | Tools that I have used
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | )
21 | }
22 |
23 | export default Skills
24 |
25 | const SkillsList = ({stacks}: {stacks: stacksProps}) => {
26 | return (
27 |
28 | {Object.keys(stacks).map((stack, index) => {
29 | const Icon = stacks[stack].Icon
30 | const className = stacks[stack].className
31 | return (
32 |
37 | { }
38 | {stack}
39 |
40 | )
41 | })}
42 |
43 | )
44 | }
45 |
--------------------------------------------------------------------------------
/src/components/socials.tsx:
--------------------------------------------------------------------------------
1 | import {ClassValue} from 'clsx'
2 | import {Linkedin} from 'lucide-react'
3 | import {FaInstagram} from 'react-icons/fa6'
4 | import {FiGithub} from 'react-icons/fi'
5 | import {TbBrandDiscord} from 'react-icons/tb'
6 | import config from '~/config'
7 | import {cn} from '~/lib/utils'
8 | import {Tooltip, TooltipContent, TooltipTrigger} from './ui/tooltip'
9 |
10 | const socialsIcons = [
11 | {
12 | id: 1,
13 | label: 'Github',
14 | icon: ,
15 | href: config.social.github,
16 | className: 'hover:bg-gray-400 hover:text-black' as ClassValue,
17 | },
18 | {
19 | id: 2,
20 | label: 'Linkedin',
21 | icon: ,
22 | href: config.social.linkedin,
23 | className: 'hover:bg-blue-700 hover:text-white' as ClassValue,
24 | },
25 | {
26 | id: 3,
27 | label: 'Instagram',
28 | icon: ,
29 | href: config.social.instagram,
30 | className:
31 | ' hover:bg-gradient-to-r from-rose-400 to-red-500 hover:text-white' as ClassValue,
32 | },
33 | {
34 | id: 4,
35 | label: 'Discord',
36 | icon: ,
37 | href: config.social.discord,
38 | className: 'hover:bg-blue-400 hover:text-white' as ClassValue,
39 | },
40 | ]
41 | const Socials = () => {
42 | return (
43 |
44 | {socialsIcons.map(social => (
45 |
46 |
47 |
48 |
58 | {social.icon}
59 |
60 |
61 |
62 | {social.label}
63 |
64 |
65 |
66 | ))}
67 |
68 | )
69 | }
70 |
71 | export default Socials
72 |
--------------------------------------------------------------------------------
/src/components/support-btn.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import config from '~/config'
3 | import {Button} from './ui/button'
4 | import {MdOutlineSupportAgent} from 'react-icons/md'
5 | import Link from 'next/link'
6 | import {AiFillHome} from 'react-icons/ai'
7 | import {GrPowerReset} from 'react-icons/gr'
8 |
9 | const SupportButton = ({onReset}: {onReset?: () => void}) => {
10 | return (
11 |
12 | {typeof onReset === 'undefined' && (
13 |
14 |
20 |
21 |
22 |
23 | Back to Home
24 |
25 |
26 | )}
27 |
28 | {typeof onReset === 'function' && (
29 |
35 |
36 |
37 |
38 | Try again
39 |
40 | )}
41 |
42 |
43 |
50 |
51 | Support
52 |
53 |
54 |
55 | )
56 | }
57 |
58 | export default SupportButton
59 |
--------------------------------------------------------------------------------
/src/components/tags.tsx:
--------------------------------------------------------------------------------
1 | import {slug} from 'github-slugger'
2 | import Link from 'next/link'
3 | import {buttonVariants} from './ui/button'
4 |
5 | const Tags = ({tags}: {tags: string[]}) => {
6 | return (
7 |
8 | {tags.map((tag, index) => (
9 |
10 | ))}
11 |
12 | )
13 | }
14 |
15 | export default Tags
16 |
17 | export const Tag = ({tag, count}: {tag: string; count?: number}) => {
18 | return (
19 |
20 |
28 | #{tag} {count && `(${count})`}
29 |
30 |
31 | )
32 | }
33 |
--------------------------------------------------------------------------------
/src/components/ui/accordion.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import * as React from 'react'
4 | import * as AccordionPrimitive from '@radix-ui/react-accordion'
5 | import {ChevronDown} from 'lucide-react'
6 |
7 | import {cn} from '~/lib/utils'
8 | import {FaCircleChevronDown} from 'react-icons/fa6'
9 |
10 | const Accordion = AccordionPrimitive.Root
11 |
12 | const AccordionItem = React.forwardRef<
13 | React.ElementRef,
14 | React.ComponentPropsWithoutRef
15 | >(({className, ...props}, ref) => (
16 |
21 | ))
22 | AccordionItem.displayName = 'AccordionItem'
23 |
24 | const AccordionTrigger = React.forwardRef<
25 | React.ElementRef,
26 | React.ComponentPropsWithoutRef
27 | >(({className, children, ...props}, ref) => (
28 |
29 | svg]:rotate-180',
33 | className,
34 | )}
35 | {...props}
36 | >
37 |
38 | {children}
39 |
40 |
41 | ))
42 | AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName
43 |
44 | const AccordionContent = React.forwardRef<
45 | React.ElementRef,
46 | React.ComponentPropsWithoutRef
47 | >(({className, children, ...props}, ref) => (
48 |
53 | {children}
54 |
55 | ))
56 |
57 | AccordionContent.displayName = AccordionPrimitive.Content.displayName
58 |
59 | export {Accordion, AccordionItem, AccordionTrigger, AccordionContent}
60 |
--------------------------------------------------------------------------------
/src/components/ui/button.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { Slot } from '@radix-ui/react-slot'
3 | import { cva, type VariantProps } from 'class-variance-authority'
4 |
5 | import { cn } from '~/lib/utils'
6 |
7 | const buttonVariants = cva(
8 | 'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium el-focus-styles transition-colors focus-visible:outline-none disabled:pointer-events-none disabled:opacity-50',
9 | {
10 | variants: {
11 | variant: {
12 | default: 'bg-primary text-primary-foreground hover:bg-primary/90',
13 | destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
14 | outline: 'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
15 | secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
16 | ghost: 'hover:bg-accent hover:text-accent-foreground',
17 | link: 'text-primary underline-offset-4 hover:underline',
18 | },
19 | size: {
20 | default: 'h-10 px-4 py-2',
21 | sm: 'h-9 rounded-md px-3',
22 | lg: 'h-11 rounded-md px-8',
23 | xs: 'h-7 rounded-sm px-2',
24 | icon: 'size-9',
25 | },
26 | },
27 | defaultVariants: {
28 | variant: 'default',
29 | size: 'default',
30 | },
31 | },
32 | )
33 |
34 | export interface ButtonProps
35 | extends React.ButtonHTMLAttributes,
36 | VariantProps {
37 | asChild?: boolean
38 | }
39 |
40 | const Button = React.forwardRef(
41 | ({ className, variant, size, asChild = false, ...props }, ref) => {
42 | const Comp = asChild ? Slot : 'button'
43 | return (
44 |
45 | )
46 | },
47 | )
48 | Button.displayName = 'Button'
49 |
50 | export { Button, buttonVariants }
51 |
--------------------------------------------------------------------------------
/src/components/ui/callout.tsx:
--------------------------------------------------------------------------------
1 | import {cn} from '~/lib/utils'
2 | import {ReactNode} from 'react'
3 |
4 | interface CalloutProps {
5 | children?: ReactNode
6 | type?: 'default' | 'warning' | 'danger'
7 | }
8 |
9 | export default function Callout({children, type = 'default', ...props}: CalloutProps) {
10 | return (
11 |
20 | )
21 | }
22 |
--------------------------------------------------------------------------------
/src/components/ui/card.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 |
3 | import {cn} from '~/lib/utils'
4 |
5 | const Card = React.forwardRef<
6 | HTMLDivElement,
7 | React.HTMLAttributes
8 | >(({className, ...props}, ref) => (
9 |
17 | ))
18 | Card.displayName = 'Card'
19 |
20 | const CardHeader = React.forwardRef<
21 | HTMLDivElement,
22 | React.HTMLAttributes
23 | >(({className, ...props}, ref) => (
24 |
29 | ))
30 | CardHeader.displayName = 'CardHeader'
31 |
32 | const CardTitle = React.forwardRef<
33 | HTMLParagraphElement,
34 | React.HTMLAttributes
35 | >(({className, ...props}, ref) => (
36 |
44 | ))
45 | CardTitle.displayName = 'CardTitle'
46 |
47 | const CardDescription = React.forwardRef<
48 | HTMLParagraphElement,
49 | React.HTMLAttributes
50 | >(({className, ...props}, ref) => (
51 |
56 | ))
57 | CardDescription.displayName = 'CardDescription'
58 |
59 | const CardContent = React.forwardRef<
60 | HTMLDivElement,
61 | React.HTMLAttributes
62 | >(({className, ...props}, ref) => (
63 |
64 | ))
65 | CardContent.displayName = 'CardContent'
66 |
67 | const CardFooter = React.forwardRef<
68 | HTMLDivElement,
69 | React.HTMLAttributes
70 | >(({className, ...props}, ref) => (
71 |
76 | ))
77 | CardFooter.displayName = 'CardFooter'
78 |
79 | export {Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent}
80 |
--------------------------------------------------------------------------------
/src/components/ui/content-not-found.tsx:
--------------------------------------------------------------------------------
1 | import {Frown} from 'lucide-react'
2 | import React from 'react'
3 |
4 | const ContentNotFound = ({text}: {text: string}) => {
5 | return (
6 |
10 |
11 |
12 |
{text}
13 |
14 |
15 | )
16 | }
17 |
18 | export default ContentNotFound
19 |
--------------------------------------------------------------------------------
/src/components/ui/dank-mono.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BinarySenseiii/personal-website/6f4f0a97fbb86fa1060453b94a979e9023c0e0f4/src/components/ui/dank-mono.otf
--------------------------------------------------------------------------------
/src/components/ui/dropdown-menu.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import * as React from 'react'
4 | import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu'
5 | import {Check, ChevronRight, Circle} from 'lucide-react'
6 |
7 | import {cn} from '~/lib/utils'
8 |
9 | const DropdownMenu = DropdownMenuPrimitive.Root
10 |
11 | const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
12 |
13 | const DropdownMenuGroup = DropdownMenuPrimitive.Group
14 |
15 | const DropdownMenuPortal = DropdownMenuPrimitive.Portal
16 |
17 | const DropdownMenuSub = DropdownMenuPrimitive.Sub
18 |
19 | const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
20 |
21 | const DropdownMenuSubTrigger = React.forwardRef<
22 | React.ElementRef,
23 | React.ComponentPropsWithoutRef & {
24 | inset?: boolean
25 | }
26 | >(({className, inset, children, ...props}, ref) => (
27 |
36 | {children}
37 |
38 |
39 | ))
40 | DropdownMenuSubTrigger.displayName =
41 | DropdownMenuPrimitive.SubTrigger.displayName
42 |
43 | const DropdownMenuSubContent = React.forwardRef<
44 | React.ElementRef,
45 | React.ComponentPropsWithoutRef
46 | >(({className, ...props}, ref) => (
47 |
55 | ))
56 | DropdownMenuSubContent.displayName =
57 | DropdownMenuPrimitive.SubContent.displayName
58 |
59 | const DropdownMenuContent = React.forwardRef<
60 | React.ElementRef,
61 | React.ComponentPropsWithoutRef
62 | >(({className, sideOffset = 4, ...props}, ref) => (
63 |
64 |
73 |
74 | ))
75 | DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
76 |
77 | const DropdownMenuItem = React.forwardRef<
78 | React.ElementRef,
79 | React.ComponentPropsWithoutRef & {
80 | inset?: boolean
81 | }
82 | >(({className, inset, ...props}, ref) => (
83 |
92 | ))
93 | DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
94 |
95 | const DropdownMenuCheckboxItem = React.forwardRef<
96 | React.ElementRef,
97 | React.ComponentPropsWithoutRef
98 | >(({className, children, checked, ...props}, ref) => (
99 |
108 |
109 |
110 |
111 |
112 |
113 | {children}
114 |
115 | ))
116 | DropdownMenuCheckboxItem.displayName =
117 | DropdownMenuPrimitive.CheckboxItem.displayName
118 |
119 | const DropdownMenuRadioItem = React.forwardRef<
120 | React.ElementRef,
121 | React.ComponentPropsWithoutRef
122 | >(({className, children, ...props}, ref) => (
123 |
131 |
132 |
133 |
134 |
135 |
136 | {children}
137 |
138 | ))
139 | DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
140 |
141 | const DropdownMenuLabel = React.forwardRef<
142 | React.ElementRef,
143 | React.ComponentPropsWithoutRef & {
144 | inset?: boolean
145 | }
146 | >(({className, inset, ...props}, ref) => (
147 |
156 | ))
157 | DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
158 |
159 | const DropdownMenuSeparator = React.forwardRef<
160 | React.ElementRef,
161 | React.ComponentPropsWithoutRef
162 | >(({className, ...props}, ref) => (
163 |
168 | ))
169 | DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
170 |
171 | const DropdownMenuShortcut = ({
172 | className,
173 | ...props
174 | }: React.HTMLAttributes) => {
175 | return (
176 |
180 | )
181 | }
182 | DropdownMenuShortcut.displayName = 'DropdownMenuShortcut'
183 |
184 | export {
185 | DropdownMenu,
186 | DropdownMenuTrigger,
187 | DropdownMenuContent,
188 | DropdownMenuItem,
189 | DropdownMenuCheckboxItem,
190 | DropdownMenuRadioItem,
191 | DropdownMenuLabel,
192 | DropdownMenuSeparator,
193 | DropdownMenuShortcut,
194 | DropdownMenuGroup,
195 | DropdownMenuPortal,
196 | DropdownMenuSub,
197 | DropdownMenuSubContent,
198 | DropdownMenuSubTrigger,
199 | DropdownMenuRadioGroup,
200 | }
201 |
--------------------------------------------------------------------------------
/src/components/ui/fonts.ts:
--------------------------------------------------------------------------------
1 | import {Bai_Jamjuree, Ubuntu} from 'next/font/google'
2 | import localFont from 'next/font/local'
3 |
4 | export const dankMono = localFont({
5 | src: './dank-mono.otf',
6 | display: 'swap',
7 | variable: '--font-dank',
8 | })
9 |
10 | export const fontSans = Bai_Jamjuree({
11 | subsets: ['latin'],
12 | weight: ['400', '500', '600', '700'],
13 | variable: '--font-sans',
14 | })
15 |
16 | export const ubuntu = Ubuntu({
17 | subsets: ['latin'],
18 | weight: ['400'],
19 | variable: '--font-ubuntu',
20 | })
21 |
--------------------------------------------------------------------------------
/src/components/ui/form.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import * as LabelPrimitive from '@radix-ui/react-label'
3 | import {Slot} from '@radix-ui/react-slot'
4 | import {
5 | Controller,
6 | ControllerProps,
7 | FieldPath,
8 | FieldValues,
9 | FormProvider,
10 | useFormContext,
11 | } from 'react-hook-form'
12 |
13 | import {cn} from '~/lib/utils'
14 | import {Label} from '~/components/ui/label'
15 |
16 | const Form = FormProvider
17 |
18 | type FormFieldContextValue<
19 | TFieldValues extends FieldValues = FieldValues,
20 | TName extends FieldPath = FieldPath,
21 | > = {
22 | name: TName
23 | }
24 |
25 | const FormFieldContext = React.createContext(
26 | {} as FormFieldContextValue,
27 | )
28 |
29 | const FormField = <
30 | TFieldValues extends FieldValues = FieldValues,
31 | TName extends FieldPath = FieldPath,
32 | >({
33 | ...props
34 | }: ControllerProps) => {
35 | return (
36 |
37 |
38 |
39 | )
40 | }
41 |
42 | const useFormField = () => {
43 | const fieldContext = React.useContext(FormFieldContext)
44 | const itemContext = React.useContext(FormItemContext)
45 | const {getFieldState, formState} = useFormContext()
46 |
47 | const fieldState = getFieldState(fieldContext.name, formState)
48 |
49 | if (!fieldContext) {
50 | throw new Error('useFormField should be used within ')
51 | }
52 |
53 | const {id} = itemContext
54 |
55 | return {
56 | id,
57 | name: fieldContext.name,
58 | formItemId: `${id}-form-item`,
59 | formDescriptionId: `${id}-form-item-description`,
60 | formMessageId: `${id}-form-item-message`,
61 | ...fieldState,
62 | }
63 | }
64 |
65 | type FormItemContextValue = {
66 | id: string
67 | }
68 |
69 | const FormItemContext = React.createContext(
70 | {} as FormItemContextValue,
71 | )
72 |
73 | const FormItem = React.forwardRef>(
74 | ({className, ...props}, ref) => {
75 | const id = React.useId()
76 |
77 | return (
78 |
79 |
80 |
81 | )
82 | },
83 | )
84 | FormItem.displayName = 'FormItem'
85 |
86 | const FormLabel = React.forwardRef<
87 | React.ElementRef,
88 | React.ComponentPropsWithoutRef
89 | >(({className, ...props}, ref) => {
90 | const {error, formItemId} = useFormField()
91 |
92 | return (
93 |
99 | )
100 | })
101 | FormLabel.displayName = 'FormLabel'
102 |
103 | const FormControl = React.forwardRef<
104 | React.ElementRef,
105 | React.ComponentPropsWithoutRef
106 | >(({...props}, ref) => {
107 | const {error, formItemId, formDescriptionId, formMessageId} = useFormField()
108 |
109 | return (
110 |
119 | )
120 | })
121 | FormControl.displayName = 'FormControl'
122 |
123 | const FormDescription = React.forwardRef<
124 | HTMLParagraphElement,
125 | React.HTMLAttributes
126 | >(({className, ...props}, ref) => {
127 | const {formDescriptionId} = useFormField()
128 |
129 | return (
130 |
136 | )
137 | })
138 | FormDescription.displayName = 'FormDescription'
139 |
140 | const FormMessage = React.forwardRef<
141 | HTMLParagraphElement,
142 | React.HTMLAttributes
143 | >(({className, children, ...props}, ref) => {
144 | const {error, formMessageId} = useFormField()
145 | const body = error ? String(error?.message) : children
146 |
147 | if (!body) {
148 | return null
149 | }
150 |
151 | return (
152 |
158 | {body}
159 |
160 | )
161 | })
162 | FormMessage.displayName = 'FormMessage'
163 |
164 | export {
165 | useFormField,
166 | Form,
167 | FormItem,
168 | FormLabel,
169 | FormControl,
170 | FormDescription,
171 | FormMessage,
172 | FormField,
173 | }
174 |
--------------------------------------------------------------------------------
/src/components/ui/input.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 |
3 | import {cn} from '~/lib/utils'
4 |
5 | export interface InputProps extends React.InputHTMLAttributes {}
6 |
7 | const Input = React.forwardRef(
8 | ({className, type, ...props}, ref) => {
9 | return (
10 |
20 | )
21 | },
22 | )
23 | Input.displayName = 'Input'
24 |
25 | export {Input}
26 |
--------------------------------------------------------------------------------
/src/components/ui/label.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as LabelPrimitive from "@radix-ui/react-label"
5 | import { cva, type VariantProps } from "class-variance-authority"
6 |
7 | import { cn } from "~/lib/utils"
8 |
9 | const labelVariants = cva(
10 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
11 | )
12 |
13 | const Label = React.forwardRef<
14 | React.ElementRef,
15 | React.ComponentPropsWithoutRef &
16 | VariantProps
17 | >(({ className, ...props }, ref) => (
18 |
23 | ))
24 | Label.displayName = LabelPrimitive.Root.displayName
25 |
26 | export { Label }
27 |
--------------------------------------------------------------------------------
/src/components/ui/rss.tsx:
--------------------------------------------------------------------------------
1 | import {Rss} from 'lucide-react'
2 | import React from 'react'
3 |
4 | const RssFeed = () => {
5 | return (
6 |
12 |
13 |
14 |
15 | RSS Feed
16 |
17 | )
18 | }
19 |
20 | export default RssFeed
21 |
--------------------------------------------------------------------------------
/src/components/ui/sheet.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as SheetPrimitive from "@radix-ui/react-dialog"
5 | import { cva, type VariantProps } from "class-variance-authority"
6 | import { X } from "lucide-react"
7 |
8 | import { cn } from "~/lib/utils"
9 |
10 | const Sheet = SheetPrimitive.Root
11 |
12 | const SheetTrigger = SheetPrimitive.Trigger
13 |
14 | const SheetClose = SheetPrimitive.Close
15 |
16 | const SheetPortal = SheetPrimitive.Portal
17 |
18 | const SheetOverlay = React.forwardRef<
19 | React.ElementRef,
20 | React.ComponentPropsWithoutRef
21 | >(({ className, ...props }, ref) => (
22 |
30 | ))
31 | SheetOverlay.displayName = SheetPrimitive.Overlay.displayName
32 |
33 | const sheetVariants = cva(
34 | "fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
35 | {
36 | variants: {
37 | side: {
38 | top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
39 | bottom:
40 | "inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
41 | left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
42 | right:
43 | "inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
44 | },
45 | },
46 | defaultVariants: {
47 | side: "right",
48 | },
49 | }
50 | )
51 |
52 | interface SheetContentProps
53 | extends React.ComponentPropsWithoutRef,
54 | VariantProps {}
55 |
56 | const SheetContent = React.forwardRef<
57 | React.ElementRef,
58 | SheetContentProps
59 | >(({ side = "right", className, children, ...props }, ref) => (
60 |
61 |
62 |
67 | {children}
68 |
69 |
70 | Close
71 |
72 |
73 |
74 | ))
75 | SheetContent.displayName = SheetPrimitive.Content.displayName
76 |
77 | const SheetHeader = ({
78 | className,
79 | ...props
80 | }: React.HTMLAttributes) => (
81 |
88 | )
89 | SheetHeader.displayName = "SheetHeader"
90 |
91 | const SheetFooter = ({
92 | className,
93 | ...props
94 | }: React.HTMLAttributes) => (
95 |
102 | )
103 | SheetFooter.displayName = "SheetFooter"
104 |
105 | const SheetTitle = React.forwardRef<
106 | React.ElementRef,
107 | React.ComponentPropsWithoutRef
108 | >(({ className, ...props }, ref) => (
109 |
114 | ))
115 | SheetTitle.displayName = SheetPrimitive.Title.displayName
116 |
117 | const SheetDescription = React.forwardRef<
118 | React.ElementRef,
119 | React.ComponentPropsWithoutRef
120 | >(({ className, ...props }, ref) => (
121 |
126 | ))
127 | SheetDescription.displayName = SheetPrimitive.Description.displayName
128 |
129 | export {
130 | Sheet,
131 | SheetPortal,
132 | SheetOverlay,
133 | SheetTrigger,
134 | SheetClose,
135 | SheetContent,
136 | SheetHeader,
137 | SheetFooter,
138 | SheetTitle,
139 | SheetDescription,
140 | }
141 |
--------------------------------------------------------------------------------
/src/components/ui/skip-content.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 | import {usePathname} from 'next/navigation'
3 | import {useEffect, useRef} from 'react'
4 |
5 | const SkipContent = () => {
6 | const pathname = usePathname()
7 | const isTagsPage = pathname.includes('tag')
8 | const skipLinkRef = useRef(null)
9 |
10 | useEffect(() => {
11 | if (skipLinkRef.current) {
12 | skipLinkRef.current.focus()
13 | }
14 | }, [pathname])
15 |
16 | return (
17 | <>
18 |
19 |
25 | Skip to {isTagsPage ? 'navigation' : 'main content'}
26 |
27 | >
28 | )
29 | }
30 |
31 | export default SkipContent
32 |
--------------------------------------------------------------------------------
/src/components/ui/sonner.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { useTheme } from 'next-themes'
4 | import { Toaster as Sonner } from 'sonner'
5 | import { CheckCircle, Info, AlertTriangle, XCircle } from 'lucide-react'
6 |
7 | type ToasterProps = React.ComponentProps
8 |
9 | const Toaster = ({ ...props }: ToasterProps) => {
10 | const { theme = 'system' } = useTheme()
11 |
12 | return (
13 | ,
20 | info: ,
21 | warning: ,
22 | error: ,
23 | }}
24 | toastOptions={{
25 | classNames: {
26 | toast:
27 | 'group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg',
28 | description: 'group-[.toast]:text-muted-foreground',
29 | actionButton: 'group-[.toast]:bg-primary group-[.toast]:text-primary-foreground',
30 | cancelButton: 'group-[.toast]:bg-muted group-[.toast]:text-muted-foreground',
31 | },
32 | }}
33 | {...props}
34 | />
35 | )
36 | }
37 |
38 | export { Toaster }
39 |
--------------------------------------------------------------------------------
/src/components/ui/textarea.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 |
3 | import {cn} from '~/lib/utils'
4 |
5 | export interface TextareaProps
6 | extends React.TextareaHTMLAttributes {}
7 |
8 | const Textarea = React.forwardRef(
9 | ({className, ...props}, ref) => {
10 | return (
11 |
19 | )
20 | },
21 | )
22 | Textarea.displayName = 'Textarea'
23 |
24 | export {Textarea}
25 |
--------------------------------------------------------------------------------
/src/components/ui/tooltip.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as TooltipPrimitive from "@radix-ui/react-tooltip"
5 |
6 | import { cn } from "~/lib/utils"
7 |
8 | const TooltipProvider = TooltipPrimitive.Provider
9 |
10 | const Tooltip = TooltipPrimitive.Root
11 |
12 | const TooltipTrigger = TooltipPrimitive.Trigger
13 |
14 | const TooltipContent = React.forwardRef<
15 | React.ElementRef,
16 | React.ComponentPropsWithoutRef
17 | >(({ className, sideOffset = 4, ...props }, ref) => (
18 |
27 | ))
28 | TooltipContent.displayName = TooltipPrimitive.Content.displayName
29 |
30 | export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
31 |
--------------------------------------------------------------------------------
/src/components/ui/top-loader.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 | import {AppProgressBar as ProgressBar} from 'next-nprogress-bar'
3 |
4 | const TopLoader = () => {
5 | return
6 | }
7 |
8 | export default TopLoader
9 |
--------------------------------------------------------------------------------
/src/components/ui/typograpghy.tsx:
--------------------------------------------------------------------------------
1 | import { cva, type VariantProps } from 'class-variance-authority'
2 | import { cn } from '~/lib/utils'
3 |
4 | const typography = cva(['font-bold inline-block font-ubuntu'], {
5 | variants: {
6 | variant: {
7 | h2: 'text-lg decoration-ring inline-block underline-offset-8 decoration-wavy underline',
8 | paragraph: 'text-muted-foreground font-normal block text-base font-ubuntu',
9 | },
10 | size: {
11 | sm: 'text-sm',
12 | },
13 | font: {
14 | sans: 'font-sans',
15 | dank: 'font-dank',
16 | ubuntu: 'font-ubuntu',
17 | },
18 | },
19 | defaultVariants: {
20 | variant: 'h2',
21 | },
22 | })
23 |
24 | export interface ButtonVariants extends VariantProps {}
25 |
26 | export const typo = (variants: ButtonVariants) => cn(typography(variants))
27 |
--------------------------------------------------------------------------------
/src/config.ts:
--------------------------------------------------------------------------------
1 | import {ConfigProps} from './types/config'
2 |
3 | const config = {
4 | appName: 'Faisal Tariq',
5 | appDesignation: 'Frontend React Specialist',
6 | appDescription: `Hi, I'm Faisal, Frontend developer from the UAE. Skilled in
7 | HTML, CSS, JavaScript, and React. I create pixel-perfect, cross-browser
8 | interfaces with optimal performance`,
9 |
10 | domainName: 'faisal-dev.vercel.app',
11 |
12 | colors: {
13 | theme: 'dark',
14 | },
15 |
16 | social: {
17 | github: 'https://github.com/BinarySenseiii',
18 | linkedin: 'https://www.linkedin.com/in/faisal-tariq1/',
19 | instagram: 'https://www.instagram.com/faisal_griz/',
20 | discord: 'https://discord.gg/cAbzWNQw',
21 | email: 'faisaltariq1812@gmail.com',
22 | phone: '+971503506343',
23 | },
24 |
25 | auth: {
26 | loginUrl: '/api/auth/signin',
27 | callbackUrl: '/dashboard',
28 | },
29 | } as ConfigProps
30 |
31 | export default config
32 |
--------------------------------------------------------------------------------
/src/constants/anime.ts:
--------------------------------------------------------------------------------
1 | import {RoughAnnotationConfig} from 'rough-notation/lib/model'
2 |
3 | interface AnnotateConfig extends RoughAnnotationConfig {
4 | ref: string
5 | }
6 | export const annotationsConfig: AnnotateConfig[] = [
7 | {
8 | ref: 'a1Ref',
9 | type: 'underline',
10 | color: '#fff',
11 | multiline: true,
12 | iterations: 1,
13 | },
14 |
15 | {
16 | ref: 'a2Ref',
17 | type: 'underline',
18 | color: 'white',
19 | multiline: true,
20 | iterations: 1,
21 | },
22 | {
23 | ref: 'a3Ref',
24 | type: 'underline',
25 | color: 'white',
26 | multiline: true,
27 | iterations: 1,
28 | },
29 | {
30 | ref: 'a4Ref',
31 | type: 'underline',
32 | color: 'white',
33 | multiline: true,
34 | iterations: 1,
35 | },
36 | {
37 | ref: 'a5Ref',
38 | type: 'underline',
39 | color: 'white',
40 | multiline: true,
41 | iterations: 1,
42 | },
43 | {
44 | ref: 'a6Ref',
45 | type: 'underline',
46 | color: '#00adb5',
47 | strokeWidth: 2,
48 | padding: 4,
49 | iterations: 8,
50 | },
51 | ]
52 |
--------------------------------------------------------------------------------
/src/constants/env.ts:
--------------------------------------------------------------------------------
1 | import { createEnv } from '@t3-oss/env-nextjs'
2 | import { z } from 'zod'
3 |
4 | export const env = createEnv({
5 | server: {
6 | NODE_ENV: z.enum(['development', 'test', 'production']).default('development'),
7 | NOCODE_API_KEY: z.string().min(1),
8 | NOCODE_TAB_ID: z.string().min(1),
9 | },
10 | client: {
11 | NEXT_PUBLIC_GISCUS_REPO_ID: z.string().min(1),
12 | NEXT_PUBLIC_GISCUS_CATEGORY_ID: z.string().min(1),
13 | },
14 | runtimeEnv: {
15 | NODE_ENV: process.env.NODE_ENV,
16 | NOCODE_API_KEY: process.env.NOCODE_API_KEY,
17 | NOCODE_TAB_ID: process.env.NOCODE_TAB_ID,
18 |
19 | NEXT_PUBLIC_GISCUS_REPO_ID: process.env.NEXT_PUBLIC_GISCUS_REPO_ID,
20 | NEXT_PUBLIC_GISCUS_CATEGORY_ID: process.env.NEXT_PUBLIC_GISCUS_CATEGORY_ID,
21 | },
22 | })
23 |
--------------------------------------------------------------------------------
/src/constants/stack.tsx:
--------------------------------------------------------------------------------
1 | import {BiLogoPostgresql} from 'react-icons/bi'
2 | import {BsFillBootstrapFill, BsRobot} from 'react-icons/bs'
3 | import {IconType} from 'react-icons/lib'
4 | import {
5 | SiApollographql,
6 | SiCss3,
7 | SiExpress,
8 | SiFirebase,
9 | SiGatsby,
10 | SiGraphql,
11 | SiJavascript,
12 | SiJest,
13 | SiMui,
14 | SiNextdotjs,
15 | SiNginx,
16 | SiNodedotjs,
17 | SiPrisma,
18 | SiPwa,
19 | SiReact,
20 | SiRedux,
21 | SiSocketdotio,
22 | SiStorybook,
23 | SiStyledcomponents,
24 | SiTailwindcss,
25 | SiTypescript,
26 | SiWebpack,
27 | } from 'react-icons/si'
28 |
29 | export type stacksProps = Record<
30 | string,
31 | {
32 | Icon: IconType
33 | className: string
34 | }
35 | >
36 |
37 | export const FRONTEND_STACKS: stacksProps = {
38 | JavaScript: {Icon: SiJavascript, className: 'text-yellow-400'},
39 | TypeScript: {Icon: SiTypescript, className: 'text-blue-400'},
40 | 'Next.js': {Icon: SiNextdotjs, className: ''},
41 | 'React.js': {Icon: SiReact, className: 'text-sky-500'},
42 | TailwindCSS: {Icon: SiTailwindcss, className: 'text-cyan-300'},
43 | Bootstrap: {Icon: BsFillBootstrapFill, className: 'text-purple-500'},
44 | 'Material UI': {Icon: SiMui, className: 'text-sky-400'},
45 | Gatsby: {Icon: SiGatsby, className: 'text-purple-600'},
46 | Redux: {Icon: SiRedux, className: 'text-purple-500'},
47 | Webpack: {Icon: SiWebpack, className: 'text-blue-500'},
48 | 'Styled Components': {Icon: SiStyledcomponents, className: 'text-pink-500'},
49 | PWA: {Icon: SiPwa, className: 'text-amber-600'},
50 | Jest: {Icon: SiJest, className: 'text-red-600'},
51 | Storybook: {Icon: SiStorybook, className: 'text-amber-500'},
52 | CSS: {Icon: SiCss3, className: 'text-blue-300'},
53 | // WordPress: { Icon: SiWordpress, className: '' },
54 | // Angular: { Icon: SiAngular, className: 'text-red-500' },
55 | // 'Vue.js': { Icon: SiVuedotjs, className: 'text-green-500' },
56 | // 'Nuxt.js': { Icon: SiNuxtdotjs, className: 'text-green-400' },
57 | }
58 |
59 | export const BACKEND_STACKS = {
60 | postgreSql: {Icon: BiLogoPostgresql, className: 'text-blue-500'},
61 | GraphQL: {Icon: SiGraphql, className: 'text-pink-600'},
62 | Apollo: {Icon: SiApollographql, className: ''},
63 | Prisma: {Icon: SiPrisma, className: 'text-emerald-500'},
64 | 'Node.js': {Icon: SiNodedotjs, className: 'text-green-600'},
65 | Firebase: {Icon: SiFirebase, className: 'text-yellow-500'},
66 | 'Artificial Intelligence': {Icon: BsRobot, className: 'text-rose-500'},
67 | Nginx: {Icon: SiNginx, className: 'text-green-500'},
68 | Socket: {Icon: SiSocketdotio, className: ''},
69 | Express: {Icon: SiExpress, className: ''},
70 | // PHP: {Icon: SiPhp, className: 'text-blue-500'},
71 | // WordPress: {Icon: SiWordpress, className: ''},
72 | // Laravel: {Icon: SiLaravel, className: 'text-red-500'},
73 | }
74 |
--------------------------------------------------------------------------------
/src/content/posts/images/cover/og-modulo-operator.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BinarySenseiii/personal-website/6f4f0a97fbb86fa1060453b94a979e9023c0e0f4/src/content/posts/images/cover/og-modulo-operator.webp
--------------------------------------------------------------------------------
/src/content/posts/images/cover/publishing-react-package.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BinarySenseiii/personal-website/6f4f0a97fbb86fa1060453b94a979e9023c0e0f4/src/content/posts/images/cover/publishing-react-package.webp
--------------------------------------------------------------------------------
/src/content/posts/javascript-modulo-operator.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: 'Understanding the JavaScript Modulo Operator'
3 | description:
4 | One of the most commonly-misunderstood operators is Modulo (%). In this tutorial, we'll unpack
5 | exactly what this little bugger does, and learn how it can help us solve practical problems
6 | tags: ['javascript', 'modules']
7 | date: 2024-04-28
8 | published: true
9 | cover: './images/cover/og-modulo-operator.webp'
10 | ---
11 |
12 | When I was first learning to code, I remember finding the Modulo operator (%) _extremely_
13 | confusing😬
14 |
15 | If you don't understand what it's doing, the values it produces seem completely random:
16 |
17 | ```js
18 | const what = 10 % 4 // 2
19 | const the = 10 % 10 // 0
20 | const heck = 4 % 10 // 4
21 | ```
22 |
23 | In this blog post, we're going to learn how this operator works by refining our mental model for
24 | division. We'll also cover a practical, every-day use case for this curious fella.
25 |
26 | ## Rethinking division
27 |
28 | Suppose we have the following bit of arithmetic:
29 |
30 | ```js
31 | 12 ÷ 4
32 | ```
33 |
34 | Division can often feel pretty abstract or theoretical, but there's a practical way to think about
35 | it: we want to divide a number into equally-sized groups.
36 |
37 | **Drag the slider to see how this operation can be visualized:**
38 |
39 | **TODO: Uncomment DivisionGroupsDemo**
40 |
41 | {/* */}
42 |
43 | `12 ÷ 4` evaluates to `3`, because each group holds exactly 3 items. Essentially, we're figuring out
44 | how many items will be held inside each group.
45 |
46 | In the example widget above, our dividend (the number to be divided) is 12. 12 is a _remarkably_
47 | clean number when it comes to division; it can be split neatly in lots of different ways.
48 |
49 | Suppose we had the following equation instead:
50 |
51 | ```js
52 | 11 ÷ 4
53 | ```
54 |
55 | This equation evaluates to `2.75`. Each group has 2 complete items, and then ¾ths of another item.
56 |
57 | This works if we're dividing up pizzas or cakes… **but what if the items are indestructible?** What
58 | if we _can't_ break each item up into smaller fractions?
59 |
60 | In that case, we'd be able to fit 2 items into each group, and we'd be left with 3 additional items:
61 |
62 | **TODO: Uncomment DivisionGroupsDemo**
63 |
64 | {/* */}
65 |
66 | **This is known as the _remainder_.** It's what the modulo operator produces.
67 |
68 | In cases where the number can be equally divided into groups (eg. `12 ÷ 4`), there is nothing left
69 | over:
70 |
71 | ```js
72 | 12 % 4 // 0
73 | ```
74 |
75 | In situations where the dividend (the number to be divided) _can't_ be split equally into groups,
76 | the modulo operator lets us know how much is left over:
77 |
78 | ```js
79 | 11 % 4 // 3
80 | ```
81 |
82 | ## A real-world use case
83 |
84 | So, I'm not a mathematician, I'm a web developer. All of this math stuff is interesting, but let's
85 | talk about how the modulo operator can come in handy on the web.
86 |
87 | Specifically, there's one sort of problem that I seem to run into a lot, where the modulo operator
88 | offers the perfect solution: _circular arrays._
89 |
90 | For example, suppose we have an array of 3 colors. Each second, we want to switch to the next color
91 | in the list. When we reach the end of the list, we want to jump back to the first item:
92 |
93 | **TODO: Uncomment CircularColorsDemo**
94 |
95 | {/* */}
96 |
97 | This is a surprisingly tricky problem. Suppose we have a variable called `timeElapsed` that starts
98 | at 0 and increments by 1 every second; we have to somehow map this ever-increasing value to an array
99 | with only 3 items.
100 |
101 | Essentially, we need to write a function that produces the following results:
102 |
103 | ```js
104 | const COLORS = ['red', 'yellow', 'blue']
105 |
106 | getColor({ timeElapsed: 0 }) // 'red'
107 | getColor({ timeElapsed: 1 }) // 'yellow'
108 | getColor({ timeElapsed: 2 }) // 'blue'
109 | getColor({ timeElapsed: 3 }) // 'red'
110 | getColor({ timeElapsed: 4 }) // 'yellow'
111 | getColor({ timeElapsed: 5 }) // 'blue'
112 | getColor({ timeElapsed: 6 }) // 'red'
113 | getColor({ timeElapsed: 7 }) // 'yellow'
114 | getColor({ timeElapsed: 8 }) // 'blue'
115 | // ...And so on, forever
116 | ```
117 |
118 | Let's look at how the modulo operator can help us solve this problem:
119 |
120 | ```js
121 | const COLORS = ['red', 'yellow', 'blue']
122 |
123 | function getColor({ timeElapsed }) {
124 | const colorIndex = timeElapsed % COLORS.length
125 |
126 | return COLORS[colorIndex]
127 | }
128 | ```
129 |
130 | Miraculously, this does exactly what we need! This method will always return one of the 3 colors, as
131 | long as `timeElapsed` is an integer. And it'll cycle through the 3 colors as `timeElapsed`
132 | increases.
133 |
134 | `COLORS.length` is equal to `3`, since there are 3 colors in our array. And so, as `timeElapsed`
135 | increments from 0 to 8, this function winds up performing the following sequence of calculations:
136 |
137 | ```js
138 | const colorIndex = 0 % 3 // 0
139 | const colorIndex = 1 % 3 // 1
140 | const colorIndex = 2 % 3 // 2
141 | const colorIndex = 3 % 3 // 0
142 | const colorIndex = 4 % 3 // 1
143 | const colorIndex = 5 % 3 // 2
144 | const colorIndex = 6 % 3 // 0
145 | const colorIndex = 7 % 3 // 1
146 | const colorIndex = 8 % 3 // 2
147 | ```
148 |
149 | We can then use this `colorIndex` to look up the color from the `COLORS` array. It's guaranteed to
150 | always cycle within the range of available indexes for that array.
151 |
152 | To understand why this works, it's worth remembering our new model for division: we're trying to
153 | divide `timeElapsed` into 3 equally-sized groups, without any fractional or decimal values. The
154 | remainder will always be either 0, 1, or 2. It will never be 3+, because if there _was_ 3 left, we
155 | could fit 1 more in each group!
156 |
157 | Essentially, it's as if we had the ability to create a “circular” array. No matter how large our
158 | underlying `timeElapsed` value grows, we can have it cycle indefinitely through the colors in the
159 | `COLORS` array.
160 |
161 | In my opinion, this trick alone makes the modulo operator worth learning! I've used this
162 | circular-array trick dozens of times over the years, and it's just one of several practical use
163 | cases for this handy operator.
164 |
--------------------------------------------------------------------------------
/src/content/posts/publishing-react-package.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: 'Publishing a React component package on npm'
3 | description:
4 | Publishing a React component package on npm involves a series of steps to package, version, and
5 | distribute reusable React components for broader consumption within the JavaScript ecosystem
6 | tags: ['npm', 'react js', 'components']
7 | date: 2024-03-12
8 | published: true
9 | cover: './images/cover/publishing-react-package.webp'
10 | ---
11 |
12 | Recently at work I had to create and publish a React component on npm, which is planned to be used
13 | in multiple projects with little functionality or styles changes.
14 |
15 | Due to being a company project, I can't share the source code, but I will share some important
16 | snippets as needed.
17 |
18 | ### It isn't as easy as expected 😕
19 |
20 | When I was asked to do such task, we already had the component built and working as a
21 | `create-react-app` project, which was also a website and stuff.
22 |
23 | I thought it would be as simple as running `npm publish` and that I was going to be able to import
24 | it in the other project right away.
25 |
26 | Unfortunately it wasn't like that and I ran into compilation issues in the project that implemented
27 | it. So I started looking for the _right_ way to do it.
28 |
29 | ### Meet `create-react-library` 🤓
30 |
31 | While searching how to achieve that, I came across with
32 | [`create-react-library`](https://github.com/transitive-bullshit/create-react-library).
33 |
34 | I was looking for the simplest approach to do what I needed, and didn't want to deal with `webpack`
35 | or `babel` stuff and whatnot, so that project definitely came in handy because it was easy to use
36 | and pretty straight forward.
37 |
38 | ### Using `create-react-library` 👨🏼💻
39 |
40 | Creating a library project is as easy as running
41 |
42 | ```bash
43 | npx create-react-library project-name
44 | ```
45 |
46 | You will be prompted for some config, similar to `create-react-app`, but once done, you get the
47 | project and everything setup to work right away.
48 |
49 | I liked it because I didn't have to do much else, and it also comes with an example project inside,
50 | that uses your library, so you can test everything will be working properly after you've "published"
51 | the package.
52 |
53 | Next thing for me was just migrating the component from the previous project to the library project.
54 | It was pretty much a copy-paste process with some little refactoring, but nothing significant else.
55 |
56 | ### The challenges 😬
57 |
58 | Despite requiring no configuration, I encountered a couple problems during the process...
59 |
60 | ##### 1. React Fragments 🖼
61 |
62 | I was getting compilation errors for using React Fragments like this `<>{...}>`, so I changed that
63 | to `{...} ` and they stopped... well, kinda.
64 |
65 | Then I started getting `ReferenceError: Fragment is not defined` errors. It wasn't easy to solve so
66 | I asked for help in the [Unicorn Utterances](https://discord.gg/FMcvc6T) discord server, and
67 | [Corbin](http://crutchcorn.dev/) found
68 | [an issue](https://github.com/transitive-bullshit/create-react-library/issues/243#issuecomment-653525598)
69 | in the `create-react-library` repo that mentioned adding `--jsxFragment React.Fragment` to the
70 | `build` and `start` scripts.
71 |
72 | And then it was all working again.
73 |
74 | ##### 2. Included SVGs 📐
75 |
76 | The component included an SVG (the company logo), imported and used something like this:
77 |
78 | ```jsx
79 | import CompanyLogo from './company-logo.svg'
80 | ...
81 | return (
82 | ...
83 |
84 | ...
85 | );
86 | ```
87 |
88 | It worked fine locally, even with the local project that imported the component library, but after
89 | published in npm and imported in the other project, compilation failed, because the svg file was
90 | "compiled" with a different name, similar to `company-logo~aBcDe.svg`. Notice the `~` character.
91 | That was the issue, because it can't be used in JS var names.
92 |
93 | The quick solution, was to make it a prop and allow passing it to the component, so it wasn't
94 | compiled by the library but the other project. Again, I didn't want to deal with `webpack` or
95 | `babel` or any extra config, so this was the easiest for me.
96 |
97 | ##### 3. CSS 🎨
98 |
99 | I was using css modules for the library components, but I also needed a little of normal css,
100 | because the component used other components from [`rmwc`](https://rmwc.io), and they use that and I
101 | needed to override a few styles.
102 |
103 | From what I understood `create-react-library` the normal css and the module css files would coexist
104 | and work along normally, but for some reason, it wasn't like that, and the normal css was still
105 | compiled as a module, i.e. the compiled classes names would have some random suffix to make them
106 | unique, and therefore none of the overriden styles were applied.
107 |
108 | At the end, I ended up changing the css modules to normal css, so no suffix was added during
109 | compilation, and then it all worked as expected.
110 |
111 | ### Closing up 👋🏼
112 |
113 | So, after struggling with those few things, I was able to use the component from the library without
114 | any other issues, and everything was working as expected, including react hooks like
115 | `useContext{:.fn}` and `useReducer{:.fn}`.
116 |
117 | Also, `create-react-library` definitely allowed publishing the package by just running `npm publish`
118 | as I wanted it to be.
119 |
120 | There are probably other approaches to build a react component(s) package, but this one worked for
121 | me and was easy to use, so I liked it and wanted to share my experience.
122 |
123 | If you have further questions, I'll try to answer. Find more about me @
124 | [faisal.vercel](https://personal-website-phi-five.vercel.app/)
125 |
126 | Special thanks to [Corbin Crutchley](http://crutchcorn.dev/), [Alex Dueppen](https://ajd.sh/) and
127 | Robert Mennell from the [Unicorn Utterances](https://discord.gg/FMcvc6T) discord server, who helped
128 | me solve the issues and get the library working.
129 |
130 | Cover image is from [unsplash.com](https://unsplash.com/photos/FkjaN-7gWC0)
131 |
--------------------------------------------------------------------------------
/src/hooks/index.ts:
--------------------------------------------------------------------------------
1 | export {default as useMediaQuery} from './use-media'
2 | export {default as useIsomorphicLayoutEffect} from './use-isomorphic'
3 | export {default as useIsClient} from './use-client'
4 |
--------------------------------------------------------------------------------
/src/hooks/use-client.tsx:
--------------------------------------------------------------------------------
1 | import {useEffect, useState} from 'react'
2 |
3 | export default function useIsClient() {
4 | const [isClient, setClient] = useState(false)
5 |
6 | useEffect(() => {
7 | setClient(true)
8 | }, [])
9 |
10 | return isClient
11 | }
12 |
--------------------------------------------------------------------------------
/src/hooks/use-isomorphic.tsx:
--------------------------------------------------------------------------------
1 | import {useEffect, useLayoutEffect} from 'react'
2 |
3 | const useIsomorphicLayoutEffect = typeof window !== 'undefined' ? useLayoutEffect : useEffect
4 |
5 | export default useIsomorphicLayoutEffect
6 |
--------------------------------------------------------------------------------
/src/hooks/use-media.tsx:
--------------------------------------------------------------------------------
1 | import {useState} from 'react'
2 | import useIsomorphicLayoutEffect from './use-isomorphic'
3 |
4 | type UseMediaQueryOptions = {
5 | defaultValue?: boolean
6 | initializeWithValue?: boolean
7 | }
8 |
9 | const IS_SERVER = typeof window === 'undefined'
10 |
11 | export default function useMediaQuery(
12 | query: string,
13 | {defaultValue = false, initializeWithValue = true}: UseMediaQueryOptions = {},
14 | ): boolean {
15 | const getMatches = (query: string): boolean => {
16 | if (IS_SERVER) {
17 | return defaultValue
18 | }
19 | return window.matchMedia(query).matches
20 | }
21 |
22 | const [matches, setMatches] = useState(() => {
23 | if (initializeWithValue) {
24 | return getMatches(query)
25 | }
26 | return defaultValue
27 | })
28 |
29 | // Handles the change event of the media query.
30 | function handleChange() {
31 | setMatches(getMatches(query))
32 | }
33 |
34 | useIsomorphicLayoutEffect(() => {
35 | const matchMedia = window.matchMedia(query)
36 |
37 | // Triggered at the first client-side load and if query changes
38 | handleChange()
39 |
40 | // Use deprecated `addListener` and `removeListener` to support Safari < 14 (#135)
41 | if (matchMedia.addListener) {
42 | matchMedia.addListener(handleChange)
43 | } else {
44 | matchMedia.addEventListener('change', handleChange)
45 | }
46 |
47 | return () => {
48 | if (matchMedia.removeListener) {
49 | matchMedia.removeListener(handleChange)
50 | } else {
51 | matchMedia.removeEventListener('change', handleChange)
52 | }
53 | }
54 | }, [query])
55 |
56 | return matches
57 | }
58 |
--------------------------------------------------------------------------------
/src/lib/axios.ts:
--------------------------------------------------------------------------------
1 | import axios, { AxiosError, AxiosRequestConfig, AxiosResponse } from 'axios'
2 | import config from '~/config'
3 |
4 | export const ORIGIN =
5 | process.env.NODE_ENV === 'development' ? 'http://localhost:3000' : `https://${config.domainName}`
6 |
7 | export const fetchFunc = async (endpoint: string, options?: AxiosRequestConfig): Promise => {
8 | try {
9 | const res: AxiosResponse = await axios(`${ORIGIN}/api${endpoint}`, {
10 | ...options,
11 | })
12 |
13 | return res.data
14 | } catch (error) {
15 | throw axios.isAxiosError(error)
16 | ? new Error(`API request failed: ${(error as AxiosError).message}`)
17 | : error
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/lib/seo.tsx:
--------------------------------------------------------------------------------
1 | import type {Metadata} from 'next'
2 | import Script from 'next/script'
3 | import config from '~/config'
4 |
5 | export const getSEOTags = ({
6 | title,
7 | description,
8 | keywords,
9 | openGraph,
10 | canonicalUrlRelative,
11 | extraTags,
12 | }: Metadata & {
13 | canonicalUrlRelative?: string
14 | extraTags?: Record
15 | } = {}) => {
16 | return {
17 | title: title || config.appName,
18 | description: description || config.appDescription,
19 | keywords: keywords || [
20 | 'portfolio',
21 | 'personal portfolio',
22 | 'web developer',
23 | 'software engineer',
24 | 'front-end developer',
25 | 'creative',
26 | 'coder',
27 | 'programmer',
28 | 'developer',
29 | 'portfolio website',
30 | ],
31 | applicationName: config.appName,
32 |
33 | metadataBase: new URL(
34 | process.env.NODE_ENV === 'development'
35 | ? 'http://localhost:3000/'
36 | : `https://${config.domainName}/`,
37 | ),
38 |
39 | openGraph: {
40 | title: openGraph?.title || config.appName,
41 | description: openGraph?.description || config.appDescription,
42 | url: openGraph?.url || `https://${config.domainName}/`,
43 | siteName: openGraph?.title || config.appName,
44 | locale: 'en_US',
45 | type: 'website',
46 | },
47 |
48 | twitter: {
49 | title: openGraph?.title || config.appName,
50 | description: openGraph?.description || config.appDescription,
51 | card: 'summary_large_image',
52 | creator: '@FaisalTari78554',
53 | },
54 |
55 | ...(canonicalUrlRelative && {
56 | alternates: {canonical: canonicalUrlRelative},
57 | }),
58 |
59 | ...extraTags,
60 | }
61 | }
62 |
63 | export const renderSchemaTags = () => {
64 | return (
65 |
107 | )
108 | }
109 |
--------------------------------------------------------------------------------
/src/lib/shadcn-ui.ts:
--------------------------------------------------------------------------------
1 | import animatePlugin from 'tailwindcss-animate'
2 | import plugin from 'tailwindcss/plugin'
3 | import {fontFamily} from 'tailwindcss/defaultTheme'
4 | import type {Config} from 'tailwindcss'
5 |
6 | const shadcnPlugin = plugin(
7 | function ({addBase}) {
8 | addBase({
9 | ':root': {
10 | '--background': '240 10% 3.9%',
11 | '--foreground': '0 0% 98%',
12 | '--card': '240 10% 3.9%',
13 | '--card-foreground': '0 0% 98%',
14 | '--popover': '240 10% 3.9%',
15 | '--popover-foreground': '0 0% 98%',
16 | '--primary': '0 0% 98%',
17 | '--primary-foreground': '240 5.9% 10%',
18 | '--secondary': '240 3.7% 15.9%',
19 | '--secondary-foreground': '0 0% 98%',
20 | '--muted': '240 3.7% 15.9%',
21 | '--muted-foreground': '240 5% 64.9%',
22 | '--accent': '240 3.7% 15.9%',
23 | '--accent-foreground': '0 0% 98%',
24 | '--destructive': '0 62.8% 30.6%',
25 | '--destructive-foreground': '0 0% 98%',
26 | '--border': '240 3.7% 15.9%',
27 | '--input': '240 3.7% 15.9%',
28 | '--ring': '182.7 100.0% 35.5%',
29 | '--radius': '0.5rem',
30 | },
31 | }),
32 | addBase({
33 | '*': {
34 | '@apply border-border': {},
35 | },
36 | body: {
37 | '@apply bg-background text-foreground': {},
38 | },
39 | })
40 | },
41 |
42 | {
43 | theme: {
44 | container: {
45 | center: true,
46 | padding: '1rem',
47 | screens: {
48 | xl: '57rem',
49 | },
50 | },
51 | extend: {
52 | typography: {
53 | DEFAULT: {
54 | css: {
55 | maxWidth: '100ch',
56 | },
57 | },
58 | },
59 | fontFamily: {
60 | sans: ['var(--font-sans)', ...fontFamily.sans],
61 | ubuntu: 'var(--font-ubuntu)',
62 | dank: 'var(--font-dank)',
63 | },
64 | screens: {
65 | xs: '380px',
66 | sm: '500px',
67 | },
68 | colors: {
69 | border: 'hsl(var(--border))',
70 | input: 'hsl(var(--input))',
71 | ring: 'hsl(var(--ring))',
72 | background: 'hsl(var(--background))',
73 | foreground: 'hsl(var(--foreground))',
74 | primary: {
75 | DEFAULT: 'hsl(var(--primary))',
76 | foreground: 'hsl(var(--primary-foreground))',
77 | },
78 | secondary: {
79 | DEFAULT: 'hsl(var(--secondary))',
80 | foreground: 'hsl(var(--secondary-foreground))',
81 | },
82 | destructive: {
83 | DEFAULT: 'hsl(var(--destructive))',
84 | foreground: 'hsl(var(--destructive-foreground))',
85 | },
86 | muted: {
87 | DEFAULT: 'hsl(var(--muted))',
88 | foreground: 'hsl(var(--muted-foreground))',
89 | },
90 | accent: {
91 | DEFAULT: 'hsl(var(--accent))',
92 | foreground: 'hsl(var(--accent-foreground))',
93 | },
94 | popover: {
95 | DEFAULT: 'hsl(var(--popover))',
96 | foreground: 'hsl(var(--popover-foreground))',
97 | },
98 | card: {
99 | DEFAULT: 'hsl(var(--card))',
100 | foreground: 'hsl(var(--card-foreground))',
101 | },
102 | },
103 | borderRadius: {
104 | lg: 'var(--radius)',
105 | md: 'calc(var(--radius) - 2px)',
106 | sm: 'calc(var(--radius) - 4px)',
107 | },
108 | keyframes: {
109 | 'accordion-down': {
110 | from: {height: '0'},
111 | to: {height: 'var(--radix-accordion-content-height)'},
112 | },
113 | 'accordion-up': {
114 | from: {height: 'var(--radix-accordion-content-height)'},
115 | to: {height: '0'},
116 | },
117 | },
118 | animation: {
119 | 'accordion-down': 'accordion-down 0.2s ease-out',
120 | 'accordion-up': 'accordion-up 0.2s ease-out',
121 | },
122 | },
123 | },
124 | },
125 | )
126 |
127 | export const shadcnPreset = {
128 | prefix: '',
129 | darkMode: ['selector'],
130 | content: [],
131 | plugins: [animatePlugin, shadcnPlugin],
132 | } satisfies Config
133 |
--------------------------------------------------------------------------------
/src/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import {slug} from 'github-slugger'
2 | import {Post} from '#site/content'
3 | import {type ClassValue, clsx} from 'clsx'
4 |
5 | import {twMerge} from 'tailwind-merge'
6 | import config from '~/config'
7 |
8 | export function cn(...inputs: ClassValue[]) {
9 | return twMerge(clsx(inputs))
10 | }
11 |
12 | export function formatDate(input: string | number): string {
13 | const date = new Date(input)
14 | return date.toLocaleDateString('en-US', {
15 | month: 'long',
16 | day: 'numeric',
17 | year: 'numeric',
18 | })
19 | }
20 |
21 | export function sortPosts(posts: Array) {
22 | return posts.sort((a, b) => {
23 | if (a.date > b.date) return -1
24 | if (a.date < b.date) return 1
25 | return 0
26 | })
27 | }
28 |
29 | export const BasePath = (path: string) => `https://${config.domainName}${path}`
30 |
31 | export const getAllTags = (posts: Array) => {
32 | const tags: Record = {}
33 |
34 | posts.forEach(post => {
35 | post.tags.forEach(tag => {
36 | tags[tag] = (tags[tag] ?? 0) + 1
37 | })
38 | })
39 |
40 | return tags
41 | }
42 |
43 | export const sortedTagsCount = (tags: Record) =>
44 | Object.keys(tags).sort((a, b) => tags[b] - tags[a])
45 |
46 | export function getPostsByTagSlug(posts: Array, tag: string) {
47 | return posts.filter(post => {
48 | if (!post.tags) return false
49 | const slugifiedTags = post.tags.map(tag => slug(tag))
50 | return slugifiedTags.includes(tag)
51 | })
52 | }
53 |
--------------------------------------------------------------------------------
/src/providers/index.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 | import { ReactNode } from 'react'
3 |
4 | import ScrollProgress from '~/components/scroll-progress'
5 | import { TooltipProvider } from '~/components/ui/tooltip'
6 | import TopLoader from '~/components/ui/top-loader'
7 | import ReactQueryProvider from './react-query'
8 | import { Toaster } from '~/components/ui/sonner'
9 |
10 | const RootProviders = ({ children }: { children: ReactNode }) => {
11 | return (
12 |
13 |
14 |
15 |
16 | {children}
17 |
18 |
19 |
20 | )
21 | }
22 |
23 | export default RootProviders
24 |
--------------------------------------------------------------------------------
/src/providers/react-query.tsx:
--------------------------------------------------------------------------------
1 | import {QueryClient, QueryClientProvider} from '@tanstack/react-query'
2 | import React from 'react'
3 |
4 | function ReactQueryProvider({children}: React.PropsWithChildren) {
5 | const [client] = React.useState(
6 | new QueryClient({
7 | defaultOptions: {
8 | queries: {staleTime: 5000, refetchOnWindowFocus: false},
9 | },
10 | }),
11 | )
12 |
13 | return {children}
14 | }
15 |
16 | export default ReactQueryProvider
17 |
--------------------------------------------------------------------------------
/src/schema.ts:
--------------------------------------------------------------------------------
1 | import {z} from 'zod'
2 |
3 | const phoneRegex = new RegExp(/^([+]?[\s0-9]+)?(\d{3}|[(]?[0-9]+[)])?([-]?[\s]?[0-9])+$/)
4 |
5 | export const ContactSchema = z.object({
6 | fullName: z.string().min(1, {message: 'Please enter your full name.'}),
7 | phone: z
8 | .string()
9 | .min(1, {message: 'Please enter your phone.'})
10 | .regex(phoneRegex, 'Invalid phone number.'),
11 | email: z
12 | .string()
13 | .min(1, {message: 'Please enter a valid email address.'})
14 | .email({message: 'Invalid email address.'}),
15 | message: z.string().min(1, {message: 'Please enter a message.'}),
16 | })
17 |
18 | export type contactSchemaType = z.infer
19 |
--------------------------------------------------------------------------------
/src/server/db.ts:
--------------------------------------------------------------------------------
1 | import {PrismaClient} from '@prisma/client'
2 | import {env} from '~/constants/env'
3 |
4 | const createPrismaClient = () =>
5 | new PrismaClient({
6 | log:
7 | env.NODE_ENV === 'development' ? ['query', 'error', 'warn'] : ['error'],
8 | })
9 |
10 | const globalForPrisma = globalThis as unknown as {
11 | prisma: ReturnType | undefined
12 | }
13 |
14 | export const db = globalForPrisma.prisma ?? createPrismaClient()
15 |
16 | if (env.NODE_ENV !== 'production') globalForPrisma.prisma = db
17 |
--------------------------------------------------------------------------------
/src/styles/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | @layer components {
6 | .el-focus-styles {
7 | @apply ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2;
8 | }
9 |
10 | ::selection {
11 | @apply bg-ring text-white;
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/styles/mdx.css:
--------------------------------------------------------------------------------
1 | [data-rehype-pretty-code-figure] pre {
2 | @apply px-0;
3 | }
4 |
5 | [data-rehype-pretty-code-figure] code {
6 | @apply text-sm !leading-loose md:text-base border-0 p-0;
7 | }
8 |
9 | [data-rehype-pretty-code-figure] code[data-line-numbers] {
10 | counter-reset: line;
11 | }
12 |
13 | [data-rehype-pretty-code-figure] code[data-line-numbers] > [data-line]::before {
14 | counter-increment: line;
15 | content: counter(line);
16 | @apply mr-4 inline-block w-4 text-right text-gray-500;
17 | }
18 |
19 | [data-rehype-pretty-code-figure] [data-line] {
20 | @apply border-l-2 border-l-transparent px-3;
21 | }
22 |
23 | [data-rehype-pretty-code-figure] [data-highlighted-line] {
24 | background: rgba(21, 21, 205, 0.1);
25 | @apply border-l-blue-400;
26 | }
27 |
28 | [data-rehype-pretty-code-figure] [data-highlighted-chars] {
29 | @apply rounded bg-zinc-600/50;
30 | box-shadow: 0 0 0 4px rgb(82 82 91 / 0.5);
31 | }
32 |
33 | [data-rehype-pretty-code-figure] [data-chars-id] {
34 | @apply border-b-2 p-1 shadow-none;
35 | }
36 |
37 | .subheading-anchor {
38 | @apply no-underline hover:underline text-xl el-focus-styles;
39 | }
40 |
41 | .mdx-content p {
42 | @apply text-base my-0 mt-3 text-muted-foreground font-ubuntu;
43 | }
44 |
45 | .mdx-content p code {
46 | @apply font-ubuntu;
47 | }
48 |
49 | .mdx-content p a {
50 | @apply text-ring el-focus-styles;
51 | }
52 |
53 | .mdx-content p:first-child {
54 | @apply mt-0;
55 | }
56 |
57 | .mdx-content h2,
58 | .mdx-content h3,
59 | .mdx-content h1,
60 | .mdx-content h5 {
61 | @apply mt-3 mb-0;
62 | }
63 |
64 | .mdx-content figure {
65 | @apply my-6;
66 | }
67 |
68 | .mdx-content ol,
69 | .mdx-content ul {
70 | @apply my-3 ps-[14px];
71 | }
72 |
73 | .mdx-content ol a,
74 | .mdx-content ul a {
75 | @apply el-focus-styles hover:text-ring hover:underline;
76 | }
77 |
78 | .mdx-content ol li,
79 | .mdx-content ul li {
80 | @apply ps-[2px];
81 | }
82 |
83 | .mdx-content hr {
84 | @apply my-6;
85 | }
86 |
87 | .mdx-content figure,
88 | .mdx-content code,
89 | .mdx-content pre,
90 | [data-rehype-pretty-code-figure] [data-line] {
91 | @apply !font-dank el-focus-styles;
92 | }
93 |
94 | .mdx-content figure,
95 | .mdx-content pre {
96 | @apply overflow-x-auto;
97 | }
98 |
99 | * {
100 | scroll-behavior: smooth;
101 | }
102 |
103 | .mdx-content div[data-ntpc='YouTubeEmbed'] {
104 | margin-top: 0.75rem;
105 | position: relative;
106 | aspect-ratio: 16/9;
107 | }
108 |
109 | .mdx-content a:hover {
110 | border-bottom: 2px dashed;
111 | }
112 | .mdx-content div[data-ntpc='YouTubeEmbed'] lite-youtube {
113 | border-radius: 6px;
114 | position: absolute;
115 | inset: 0;
116 | height: 100%;
117 | width: 100%;
118 | }
119 |
120 | .mdx-content span[data-line] span[style='color:#6C7086;font-style:italic'],
121 | .mdx-content
122 | span[data-line]
123 | span[style='color: rgb(108, 112, 134); font-style: italic;'] {
124 | @apply !text-[#8286a1];
125 | }
126 |
--------------------------------------------------------------------------------
/src/types/config.ts:
--------------------------------------------------------------------------------
1 | export type Theme = 'light' | 'dark'
2 |
3 | export interface ConfigProps {
4 | appName: string
5 | appDescription: string
6 | appDesignation: string
7 | domainName: string
8 |
9 | social: {
10 | github: string
11 | linkedin: string
12 | instagram: string
13 | discord: string
14 | email: string
15 | phone: string
16 | }
17 |
18 | colors: {
19 | theme: Theme
20 | main: string
21 | }
22 | auth: {
23 | loginUrl: string
24 | callbackUrl: string
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/tailwind.config.ts:
--------------------------------------------------------------------------------
1 | import type {Config} from 'tailwindcss'
2 | import {shadcnPreset} from './src/lib/shadcn-ui'
3 |
4 | const config = {
5 | presets: [shadcnPreset],
6 | content: ['./app/**/*.{ts,tsx}', './src/**/*.{ts,tsx}'],
7 | plugins: [require('@tailwindcss/typography')],
8 | } satisfies Config
9 |
10 | export default config
11 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "lib": ["dom", "dom.iterable", "esnext"],
4 | "allowJs": true,
5 | "skipLibCheck": true,
6 | "strict": true,
7 | "noEmit": true,
8 | "esModuleInterop": true,
9 | "module": "esnext",
10 | "moduleResolution": "bundler",
11 | "resolveJsonModule": true,
12 | "isolatedModules": true,
13 | "jsx": "preserve",
14 | "incremental": true,
15 | "plugins": [
16 | {
17 | "name": "next"
18 | }
19 | ],
20 | "paths": {
21 | "~/*": ["./src/*"],
22 | "#site/content": ["./.velite"]
23 | }
24 | },
25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
26 | "exclude": ["node_modules"]
27 | }
28 |
--------------------------------------------------------------------------------
/velite.config.ts:
--------------------------------------------------------------------------------
1 | import rehypeAutolinkHeadings from 'rehype-autolink-headings'
2 | import rehypeCodeTitles from 'rehype-code-titles'
3 | import rehypePrettyCode from 'rehype-pretty-code'
4 | import rehypeSlug from 'rehype-slug'
5 | import {defineCollection, defineConfig, s} from 'velite'
6 | import rehypeExternalLink from 'rehype-external-links'
7 |
8 | const computedFields = (data: T) => ({
9 | ...data,
10 | slugAsParams: data.slug.split('/').slice(1).join('/'),
11 | })
12 |
13 | const posts = defineCollection({
14 | name: 'Post',
15 | pattern: 'posts/**/*.mdx',
16 | schema: s
17 | .object({
18 | title: s.string().max(99),
19 | slug: s.path(),
20 | description: s.string().max(999),
21 | cover: s.image(),
22 | date: s.isodate(),
23 | published: s.boolean().default(true),
24 | body: s.mdx(),
25 | toc: s.toc(),
26 | tags: s.array(s.string()),
27 | metadata: s.metadata(),
28 | })
29 | .transform(computedFields),
30 | })
31 |
32 | export default defineConfig({
33 | root: 'src/content',
34 | output: {
35 | data: '.velite',
36 | assets: 'public/static',
37 | base: '/static/',
38 | name: '[name]-[hash:6].[ext]',
39 | clean: true,
40 | },
41 | collections: {posts},
42 | mdx: {
43 | rehypePlugins: [
44 | rehypeSlug,
45 | rehypeCodeTitles,
46 | [rehypePrettyCode, {theme: 'catppuccin-mocha'}],
47 | [
48 | rehypeAutolinkHeadings,
49 | {
50 | behavior: 'wrap',
51 | properties: {
52 | className: ['subheading-anchor'],
53 | ariaLabel: 'Link to section',
54 | },
55 | },
56 | ],
57 | [
58 | rehypeExternalLink,
59 | {
60 | properties: {target: '_blank', rel: 'noopener noreferrer'},
61 | },
62 | ],
63 | ],
64 | remarkPlugins: [],
65 | },
66 | })
67 |
--------------------------------------------------------------------------------