├── .env.sample ├── .eslintrc.js ├── .github └── FUNDING.yml ├── .gitignore ├── .nvmrc ├── .prettierignore ├── .prettierrc ├── CNAME ├── LICENSE ├── README.md ├── app ├── about │ └── page.tsx ├── blog │ ├── [slug] │ │ └── page.tsx │ └── page.tsx ├── craft │ ├── android-volume-control-with-framer-motion │ │ └── page.tsx │ ├── evervaults-encrypted-card │ │ └── page.tsx │ ├── jhey-book-a-demo-button │ │ ├── README.md │ │ └── page.tsx │ ├── page.tsx │ └── spotify-now-playing-queue │ │ └── page.tsx ├── layout.tsx ├── page.tsx ├── socials │ └── page.tsx ├── spotify │ └── page.tsx ├── talks │ └── page.tsx ├── wall │ └── page.tsx └── work │ └── page.tsx ├── next-env.d.ts ├── next.config.js ├── package.json ├── pnpm-lock.yaml ├── postcss.config.js ├── public ├── assets │ ├── fonts │ │ ├── Karla │ │ │ └── Karla.woff │ │ └── Mona-Sans │ │ │ └── Mona-Sans.woff2 │ ├── images │ │ └── chev.webp │ └── logos │ │ ├── ga │ │ └── logo.svg │ │ ├── js.svg │ │ ├── json.svg │ │ ├── next │ │ ├── dark.svg │ │ └── light.svg │ │ ├── sanity │ │ └── logo.svg │ │ ├── tailwind │ │ └── logo.svg │ │ ├── ts.svg │ │ └── vercel │ │ ├── dark.svg │ │ └── light.svg ├── favicon.ico └── robots.txt ├── src ├── atoms │ ├── BlogViewIncrement.tsx │ ├── Tabs.tsx │ └── code.tsx ├── components │ ├── BackToTop.tsx │ ├── Banner.tsx │ ├── Blogs │ │ ├── BlogCard.tsx │ │ ├── RecentBlogSection.tsx │ │ └── index.ts │ ├── Footer.tsx │ ├── Header.tsx │ ├── Hero.tsx │ ├── Icons.tsx │ ├── MDXClient.tsx │ ├── NowPlaying.tsx │ ├── Projects │ │ ├── ProjectsCard.tsx │ │ ├── ProjectsSection.tsx │ │ └── index.ts │ ├── SpotifyQueue.tsx │ ├── Talks │ │ ├── TalksCard.tsx │ │ ├── TalksSection.tsx │ │ └── index.tsx │ ├── ThemeToggler.tsx │ ├── craft │ │ └── SpotifyHistoryBar.tsx │ └── index.ts ├── layouts │ ├── BlogLayout.tsx │ ├── ClientMetaLayout.tsx │ ├── PageLayout.tsx │ └── index.ts ├── pages │ ├── _app.tsx │ ├── _document.tsx │ ├── api │ │ ├── og.tsx │ │ ├── revalidate-post.ts │ │ ├── revalidate-social.ts │ │ ├── revalidate-talk.ts │ │ ├── spotify │ │ │ ├── nowPlaying.ts │ │ │ └── recently-played.ts │ │ └── views.ts │ ├── rss.xml.tsx │ └── sitemap.xml.tsx ├── services │ ├── constants.ts │ ├── fetcher.ts │ ├── image-transformer.ts │ ├── rehype-image-blur.ts │ ├── rss.ts │ ├── sanity-config.ts │ ├── sanity-server.ts │ ├── spotify.ts │ └── util.ts ├── store │ └── atoms │ │ └── theme.ts ├── styles │ ├── global.css │ └── tailwind.css └── types │ ├── blogs.types.ts │ ├── books.types.ts │ ├── index.ts │ ├── projects.types.ts │ ├── socials.types.ts │ ├── spotify.types.ts │ ├── talks.types.ts │ └── testimonials.type.ts ├── tailwind.config.js ├── tsconfig.json └── vercel.json /.env.sample: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_UMAMI_UUID= 2 | NEXT_PUBLIC_UMAMI_URI= 3 | SPOTIFY_CLIENT_ID = 4 | SPOTIFY_CLIENT_SECRET = 5 | SPOTIFY_REFRESH_TOKEN = 6 | SANITY_PROJECT_ID = 7 | SANITY_API_TOKEN = 8 | SANITY_DATASET = 9 | SANITY_WEBHOOK_SECRET_HEADER = 10 | SANITY_WEBHOOK_SECRET_TOKEN = 11 | SANITY_PREVIEW_SECRET = 12 | DOMAIN = 13 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es2021: true, 5 | node: true 6 | }, 7 | extends: ['plugin:@next/next/recommended'], 8 | parser: '@typescript-eslint/parser', 9 | parserOptions: { 10 | ecmaFeatures: { 11 | jsx: true 12 | }, 13 | ecmaVersion: 12, 14 | sourceType: 'module' 15 | }, 16 | plugins: ['react', '@typescript-eslint'], 17 | rules: { 18 | // suppress errors for missing 'import React' in files 19 | 'react/react-in-jsx-scope': 'off', 20 | '@typescript-eslint/no-non-null-assertion': 'off', 21 | '@typescript-eslint/no-explicit-any': 'off', 22 | 'react/jsx-key': 'off', 23 | '@typescript-eslint/ban-ts-comment': 'off' 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [shubhamverma1811] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 14 | -------------------------------------------------------------------------------- /.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 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | .env* 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | **/public/workbox-*.js 27 | **/public/workbox-*.js* 28 | **/public/sw.js 29 | **/public/service-*.js* 30 | .vscode 31 | 32 | /public/sitemap*.xml 33 | /public/rss.xml 34 | .vercel 35 | .lighthouseci/* 36 | .sanity 37 | .vercel 38 | .env*.local 39 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v20.11.0 -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | pnpm-lock.yaml -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "bracketSameLine": true, 3 | "endOfLine": "lf", 4 | "jsxSingleQuote": true, 5 | "proseWrap": "always", 6 | "singleQuote": true, 7 | "trailingComma": "none" 8 | } 9 | -------------------------------------------------------------------------------- /CNAME: -------------------------------------------------------------------------------- 1 | shubhamverma.me 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Shubham Verma 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Screenshots 2 | 3 | ### Home Page 4 | 5 | ![image](https://github.com/ShubhamVerma1811/Website/assets/25576658/6258958b-81bd-420c-bbab-4115ac56a3ee#gh-dark-mode-only) 6 | ![image](https://github.com/ShubhamVerma1811/Website/assets/25576658/55eab632-381f-4ee3-a332-d1d4c408a4b8#gh-light-mode-only) 7 | 8 | ### Lighthouse Metrics 9 | 10 | ![image](https://user-images.githubusercontent.com/25576658/180431499-53ef803d-dcc6-4b92-b017-c7606ac540b0.png) 11 | 12 | ### Related Repos 13 | 14 | - [Blogs](https://github.com/ShubhamVerma1811/Blogs) - Runs a cron job that 15 | pulls in the Blogs from Sanity and save them as a markdown files. 16 | 17 | - [shbm.fyi](https://github.com/ShubhamVerma1811/shbm.fyi/) - My 2nd domain that 18 | acts as a URL shortner using Vercel Shortner 19 | -------------------------------------------------------------------------------- /app/about/page.tsx: -------------------------------------------------------------------------------- 1 | import { MDXClient } from 'components/MDXClient'; 2 | import { PageLayout } from 'layouts'; 3 | import { serialize } from 'next-mdx-remote/serialize'; 4 | import rehypeAutolinkHeadings from 'rehype-autolink-headings'; 5 | import rehypeCodeTitles from 'rehype-code-titles'; 6 | import rehypeResizeImage from 'rehype-image-resize'; 7 | import rehypeSlug from 'rehype-slug'; 8 | import remarkGfm from 'remark-gfm'; 9 | import { transformer } from 'services/image-transformer'; 10 | import { getClient } from 'services/sanity-server'; 11 | import { generateMetaData } from 'services/util'; 12 | 13 | export const metadata = generateMetaData({ 14 | title: 'About | Shubham Verma' 15 | }); 16 | 17 | async function getData() { 18 | const about = await getClient().fetch(`*[_type == "about"][0]`); 19 | 20 | const mdxSource = await serialize(about.body, { 21 | mdxOptions: { 22 | remarkPlugins: [remarkGfm], 23 | rehypePlugins: [ 24 | rehypeSlug, 25 | [ 26 | rehypeAutolinkHeadings, 27 | { 28 | behavior: 'wrap', 29 | properties: { 30 | className: 'anchor' 31 | } 32 | } 33 | ], 34 | rehypeCodeTitles, 35 | [rehypeResizeImage, { transformer }] 36 | ] 37 | } 38 | }); 39 | 40 | return { 41 | mdxSource 42 | }; 43 | } 44 | 45 | export default async function AboutPage() { 46 | const { mdxSource } = await getData(); 47 | 48 | return ( 49 | 50 |
51 | 52 |
53 |
54 | ); 55 | } 56 | -------------------------------------------------------------------------------- /app/blog/[slug]/page.tsx: -------------------------------------------------------------------------------- 1 | import BlogViewIncrement from 'atoms/BlogViewIncrement'; 2 | import { DiagonalArrow } from 'components'; 3 | import { MDXClient } from 'components/MDXClient'; 4 | import { BlogLayout } from 'layouts'; 5 | import { Metadata, ResolvingMetadata } from 'next'; 6 | import { serialize } from 'next-mdx-remote/serialize'; 7 | import React from 'react'; 8 | import rehypeAutolinkHeadings from 'rehype-autolink-headings'; 9 | import rehypeCodeTitles from 'rehype-code-titles'; 10 | import rehypeResizeImage from 'rehype-image-resize'; 11 | import rehypeSlug from 'rehype-slug'; 12 | import remarkGfm from 'remark-gfm'; 13 | import { transformer } from 'services/image-transformer'; 14 | import { getClient, urlFor } from 'services/sanity-server'; 15 | import type { Blog as IBlog } from 'types'; 16 | 17 | export const revalidate = 86400; 18 | 19 | export async function generateStaticParams() { 20 | const blogs: Array = await getClient().fetch( 21 | `*[_type == "post"] | order(date desc) {"slug":slug.current}` 22 | ); 23 | 24 | return blogs?.map(({ slug }) => slug); 25 | } 26 | 27 | async function getData(params: { slug: string }) { 28 | if (!params || !params.slug) throw new Error('No slug found in params'); 29 | 30 | const slug = typeof params.slug === 'string' ? params.slug : params.slug[0]; 31 | const blog: IBlog = await getClient().fetch( 32 | `*[_type == "post" && !defined(publicationUrl) && slug.current == "${slug}"][0] {...,"id": _id, "slug": slug.current, "readTime": round(length(body) / 5 / 180 )}` 33 | ); 34 | 35 | if (!blog) { 36 | return { 37 | props: { blog: null } 38 | }; 39 | } 40 | 41 | const mdxSource = await serialize(blog.body, { 42 | mdxOptions: { 43 | remarkPlugins: [remarkGfm], 44 | rehypePlugins: [ 45 | rehypeSlug, 46 | [ 47 | rehypeAutolinkHeadings, 48 | { 49 | behavior: 'wrap', 50 | properties: { 51 | className: 'anchor' 52 | } 53 | } 54 | ], 55 | rehypeCodeTitles, 56 | [rehypeResizeImage, { transformer }] 57 | // rehypeImageBlur 58 | ] 59 | } 60 | }); 61 | 62 | return { 63 | props: { 64 | blog, 65 | mdxSource 66 | } 67 | }; 68 | } 69 | 70 | type Props = { 71 | params: { slug: string }; 72 | searchParams: { [key: string]: string | string[] | undefined }; 73 | }; 74 | 75 | export async function generateMetadata( 76 | { params, searchParams }: Props, 77 | parent: ResolvingMetadata 78 | ): Promise { 79 | const { 80 | props: { blog } 81 | } = await getData(params); 82 | 83 | if (!blog) return {}; 84 | 85 | const d = new Date(blog.date); 86 | const date = `${d.getDate()}/${d.getMonth() + 1}/${d.getFullYear()}`; 87 | 88 | return { 89 | title: blog?.title, 90 | description: blog?.summary, 91 | openGraph: { 92 | title: blog?.title, 93 | description: blog?.summary, 94 | images: { 95 | url: blog?.cover 96 | ? urlFor(blog?.cover).url() 97 | : `${process.env.DOMAIN}/api/og?title=${blog?.title}&date=${date}&readTime=${blog?.readTime}&author=Shubham Verma&desc=${blog?.summary}` 98 | } 99 | } 100 | }; 101 | } 102 | 103 | async function Blog({ params }: { params: { slug: string } }) { 104 | const { 105 | props: { blog, mdxSource } 106 | } = await getData(params); 107 | 108 | if (!blog) return null; 109 | 110 | const d = new Date(blog.date); 111 | const date = `${d.getDate()}/${d.getMonth() + 1}/${d.getFullYear()}`; 112 | 113 | const formatter = new Intl.NumberFormat('en-US', { 114 | notation: 'compact', 115 | compactDisplay: 'short' 116 | }); 117 | 118 | return ( 119 | 120 |

121 | {blog?.title} 122 |

123 | 124 |

125 | {date} 126 | {formatter.format(blog.views)} views 127 | {blog?.readTime} min read 128 | {blog?.canonicalUrl && ( 129 | 130 | Originally published on{' '} 131 | 132 | {new URL(blog.canonicalUrl).hostname} 133 | 134 | 135 | 136 | )} 137 |

138 |
139 |
140 | 141 |
142 | 143 | 144 |
145 | ); 146 | } 147 | 148 | export default Blog; 149 | -------------------------------------------------------------------------------- /app/blog/page.tsx: -------------------------------------------------------------------------------- 1 | import { BlogCard } from 'components/Blogs/BlogCard'; 2 | import { PageLayout } from 'layouts'; 3 | import { getClient } from 'services/sanity-server'; 4 | import { generateMetaData } from 'services/util'; 5 | import type { Blog } from 'types'; 6 | 7 | export const revalidate = 86400; 8 | 9 | export const metadata = generateMetaData({ 10 | title: 'Blogs | Shubham Verma', 11 | description: 12 | 'I blog about open source tools, writing blogs on problems and solutions faced by developers, and other stuff.' 13 | }); 14 | 15 | async function getData() { 16 | const blogs: Array = await getClient().fetch( 17 | `*[_type == "post"] | order(date desc) {...,"slug": slug.current, "readTime": round(length(body) / 5 / 180 )}` 18 | ); 19 | 20 | return { 21 | blogs 22 | }; 23 | } 24 | 25 | export default async function Blog() { 26 | const { blogs } = await getData(); 27 | 28 | return ( 29 | 30 | {blogs.map((blog, index) => { 31 | return ; 32 | })} 33 | 34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /app/craft/android-volume-control-with-framer-motion/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { AnimatePresence, motion } from 'framer-motion'; 4 | import { ClientMetaLayout } from 'layouts/ClientMetaLayout'; 5 | import { useRef, useState } from 'react'; 6 | 7 | function getVolumeIcon(vol: string, active = false) { 8 | switch (vol) { 9 | case 'vibrate': 10 | return ( 11 | 16 | 20 | Vibrate 21 | 22 | ); 23 | case 'silent': 24 | return ( 25 | 30 | 34 | Silent 35 | 36 | ); 37 | default: 38 | return ( 39 | 44 | 48 | Ringer 49 | 50 | ); 51 | } 52 | } 53 | 54 | type VolumeStates = 'vibrate' | 'silent' | 'ringer'; 55 | 56 | export const AndroidVolumeBar = () => { 57 | const [open, setIsOpen] = useState(true); 58 | const [showVolStates, setShowVolStates] = useState(false); 59 | const [activeVolState, setActiveVolState] = useState('ringer'); 60 | // const timer = useRef(null); 61 | 62 | if (typeof window === 'undefined') return null; 63 | 64 | const containerRef = useRef(null); 65 | // useEffect(() => { 66 | // if (!open) return; 67 | 68 | // startTimer(); 69 | // return () => stopTimer(); 70 | // }, [open]); 71 | 72 | function setActiveVolume(vol: VolumeStates) { 73 | setActiveVolState(vol); 74 | //TODO:: HACK FOR THE ANIMATION, NEED TO FIX IT 75 | setTimeout(() => { 76 | setShowVolStates(false); 77 | // setIsOpen(false); 78 | }, 150); 79 | } 80 | 81 | // function startTimer() { 82 | // if (timer.current) return; 83 | 84 | // timer.current = window.setTimeout(() => { 85 | // setIsOpen((p) => !p); 86 | // setShowVolStates(false); 87 | // }, 1000); 88 | // } 89 | 90 | // function stopTimer() { 91 | // timer.current && window.clearTimeout(timer.current); 92 | // } 93 | 94 | return ( 95 |
96 |

97 | Android's Volume Control 98 |

99 | 100 |
101 | 109 |
110 | 111 | {open ? ( 112 |
119 | { 130 | // stopTimer(); 131 | // }} 132 | // onMouseLeave={() => { 133 | // startTimer(); 134 | // }} 135 | className='rounded-full p-1 h-64 w-14 bg-gray-800 flex flex-col relative'> 136 | 137 | {showVolStates ? ( 138 | 155 |
156 | setActiveVolume('vibrate')} 160 | /> 161 | 162 | setActiveVolume('silent')} 166 | /> 167 | 168 | setActiveVolume('ringer')} 172 | /> 173 |
174 |
175 | ) : null} 176 |
177 | 178 | {!showVolStates ? ( 179 | { 188 | setShowVolStates((p) => !p); 189 | }}> 190 | {getVolumeIcon(activeVolState, true)} 191 | 192 | ) : null} 193 |
194 |
195 | { 201 | // setHeight((prevHeight) => -(prevHeight + info.delta.y)); 202 | // }} 203 | // style={{ 204 | // height: height + 'px', 205 | // }} 206 | className='relative bg-blue-200 size-12 rounded-full flex flex-row justify-center items-center'> 207 | 212 | 216 | Music 217 | 218 | 219 |
220 | 221 |
222 | ) : null} 223 | 224 |
225 | ); 226 | }; 227 | 228 | function Icon({ 229 | icon, 230 | onClick, 231 | active 232 | }: { 233 | icon: string; 234 | onClick: () => void; 235 | active: boolean; 236 | }) { 237 | return ( 238 | 242 |
{getVolumeIcon(icon, active)}
243 | {active ? ( 244 | 251 | ) : null} 252 |
253 | ); 254 | } 255 | 256 | export default AndroidVolumeBar; 257 | -------------------------------------------------------------------------------- /app/craft/evervaults-encrypted-card/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { ClientMetaLayout } from 'layouts/ClientMetaLayout'; 4 | import { useRef, useState } from 'react'; 5 | 6 | function generateFakeEncryptedString(length: number) { 7 | const characters = 8 | 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789#@$%&*'; 9 | const charactersLength = characters.length; 10 | let result = ''; 11 | let counter = 0; 12 | 13 | while (counter < length) { 14 | result += characters.charAt(Math.floor(Math.random() * charactersLength)); 15 | counter += 1; 16 | } 17 | 18 | return result; 19 | } 20 | 21 | const Card = () => { 22 | const [text, setText] = useState(generateFakeEncryptedString(10 * 1024)); 23 | 24 | const textRef = useRef(null); 25 | 26 | function applyStyles(bg: string, clip: string, color: string) { 27 | if (!textRef.current) { 28 | return; 29 | } 30 | textRef.current.style.background = bg; 31 | // @ts-ignore 32 | textRef.current.style['-webkit-background-clip'] = clip; 33 | // @ts-ignore 34 | textRef.current.style['-webkit-text-fill-color'] = color; 35 | } 36 | 37 | function relativeCoords(e: any) { 38 | var bounds = e.target.getBoundingClientRect(); 39 | var x = e.clientX - bounds.left; 40 | var y = e.clientY - bounds.top; 41 | return { x: x, y: y }; 42 | } 43 | 44 | function handleMoveMove(e: any) { 45 | if (!textRef.current) { 46 | return; 47 | } 48 | 49 | setText(generateFakeEncryptedString(10 * 1024)); 50 | const { x, y } = relativeCoords(e); 51 | const bounds = e.currentTarget.getBoundingClientRect(); 52 | 53 | // reduce the radius at the edges. 54 | if ( 55 | e.clientX < bounds.left + 10 || 56 | e.clientX > bounds.right - 10 || 57 | e.clientY < bounds.top + 10 || 58 | e.clientY > bounds.bottom - 10 59 | ) { 60 | applyStyles( 61 | `radial-gradient(circle 100px at ${x}px ${y}px, #aff, #5040ac, #00000010)`, 62 | `text`, 63 | `transparent` 64 | ); 65 | return; 66 | } 67 | 68 | if ( 69 | e.clientX < bounds.left + 30 || 70 | e.clientX > bounds.right - 30 || 71 | e.clientY < bounds.top + 30 || 72 | e.clientY > bounds.bottom - 30 73 | ) { 74 | applyStyles( 75 | `radial-gradient(circle 150px at ${x}px ${y}px, #aff, #5040ac, #00000010)`, 76 | `text`, 77 | `transparent` 78 | ); 79 | return; 80 | } 81 | 82 | applyStyles( 83 | `radial-gradient(circle 200px at ${x}px ${y}px, #aff, #5040ac, #00000011)`, 84 | `text`, 85 | `transparent` 86 | ); 87 | } 88 | 89 | return ( 90 | <> 91 |

92 | Evervault's Encrypted Card 93 |

94 | 95 |
96 |
{ 100 | if (!textRef.current) { 101 | return; 102 | } 103 | 104 | textRef.current.style.background = 'initial'; 105 | // @ts-ignore 106 | textRef.current.style['-webkit-background-clip'] = 'initial'; 107 | // @ts-ignore 108 | textRef.current.style['-webkit-text-fill-color'] = 'initial'; 109 | }}> 110 |

115 | {text} 116 |

117 | 118 |
119 |
120 |

121 | Ness 122 |

123 |
124 |
125 |
126 | 127 |
128 |

129 | A points-based health and wellness credit card offering rewards and 130 | benefits with top brands to incentivize members to spend and live 131 | healthily. 132 |

133 | 134 |

135 | PCI Compliance 136 |

137 |
138 |
139 | 140 | ); 141 | }; 142 | 143 | export default Card; 144 | -------------------------------------------------------------------------------- /app/craft/jhey-book-a-demo-button/README.md: -------------------------------------------------------------------------------- 1 | #### HTML 2 | 3 | ```html 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 19 | 20 | 21 | ``` 22 | 23 | --- 24 | 25 | #### CSS 26 | 27 | ```css 28 | button { 29 | cursor: pointer; 30 | border: 4px solid black; 31 | position: relative; 32 | border-radius: 10px; 33 | background: black; 34 | height: 70px; 35 | width: 240px; 36 | color: white; 37 | font-size: 24px; 38 | padding-left: 60px; 39 | } 40 | 41 | .container { 42 | border-radius: 6px; 43 | position: absolute; 44 | left: 0; 45 | top: 0; 46 | width: 60px; 47 | height: 62px; 48 | background-color: #d2ff4d; 49 | box-shadow: 0 10px 10px -5px #00000033; 50 | transition: width 0.2s ease-in-out; 51 | background-repeat: no-repeat; 52 | background-position: center; 53 | } 54 | 55 | .mask { 56 | mask: url('https://assets.codepen.io/605876/chev-mask.png'); 57 | mask-repeat: no-repeat; 58 | position: absolute; 59 | inset: 0px; 60 | left: 6px; 61 | top: 6px; 62 | background: radial-gradient(#000, transparent); 63 | background-size: 500px; 64 | animation: fly 2s infinite; 65 | } 66 | 67 | button:is(:hover, :focus-visible) .container { 68 | width: 100%; 69 | background-repeat: repeat-x; 70 | } 71 | 72 | button:is(:hover, :focus-visible) .container .mask { 73 | mask-repeat: repeat-x; 74 | } 75 | 76 | button:is(:active) .container { 77 | transform: scale(0.95); 78 | } 79 | 80 | @keyframes fly { 81 | 0% { 82 | background-position-x: 100%; 83 | } 84 | 85 | 100% { 86 | background-position-x: 0%; 87 | } 88 | } 89 | ``` 90 | -------------------------------------------------------------------------------- /app/craft/jhey-book-a-demo-button/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { DiagonalArrow } from 'components'; 4 | import { ClientMetaLayout } from 'layouts/ClientMetaLayout'; 5 | import Link from 'next/link'; 6 | 7 | const Page = () => { 8 | return ( 9 |
10 |

11 | Jhey's Book A Demo Button 12 |

13 | 14 |
15 | 23 |
24 |

25 | This was my attempt at recreating this{' '} 26 | 31 | tweet 32 | 33 | {' '} 34 | by Jhey. The source code can be viewed{' '} 35 | 40 | here 41 | 42 | {' '} 43 | . 44 |

45 | 46 | 77 |
78 | ); 79 | }; 80 | 81 | export default Page; 82 | -------------------------------------------------------------------------------- /app/craft/page.tsx: -------------------------------------------------------------------------------- 1 | import { PageLayout } from 'layouts'; 2 | import Link from 'next/link'; 3 | import fs from 'node:fs'; 4 | import path from 'node:path'; 5 | import { generateMetaData } from 'services/util'; 6 | 7 | export const metadata = generateMetaData({ 8 | title: 'Crafts | Shubham Verma', 9 | description: 'Crafts/Experienents I have made.' 10 | }); 11 | 12 | function getCrafts() { 13 | const craftDir = path.join(process.cwd(), 'app/craft'); 14 | const craftFolders = fs 15 | .readdirSync(craftDir, { withFileTypes: true }) 16 | .filter((dirent) => dirent.isDirectory()) 17 | .map((dirent) => dirent.name); 18 | 19 | const craftsWithTime = craftFolders.map((slug) => { 20 | const folderPath = path.join(craftDir, slug); 21 | const stats = fs.statSync(folderPath); 22 | return { 23 | title: slug 24 | .split('-') 25 | .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) 26 | .join(' '), 27 | slug, 28 | birthtime: stats.birthtime 29 | }; 30 | }); 31 | 32 | craftsWithTime.sort((a, b) => a.birthtime.getTime() - b.birthtime.getTime()); 33 | 34 | return craftsWithTime.map(({ title, slug }) => ({ title, slug })); 35 | } 36 | 37 | export default function Craft() { 38 | const crafts = getCrafts(); 39 | 40 | return ( 41 | 42 |
    43 | {crafts.map((craft) => ( 44 |
  • 47 | 50 | {craft.title} 51 | 52 |
  • 53 | ))} 54 |
55 |
56 | ); 57 | } 58 | -------------------------------------------------------------------------------- /app/craft/spotify-now-playing-queue/page.tsx: -------------------------------------------------------------------------------- 1 | import { ClientMetaLayout } from 'layouts/ClientMetaLayout'; 2 | import dynamic from 'next/dynamic'; 3 | 4 | const Comp = dynamic( 5 | () => 6 | import('../../../src/components/SpotifyQueue').then((mod) => mod.default), 7 | { 8 | ssr: false 9 | } 10 | ); 11 | 12 | const Page = () => { 13 | return ( 14 |
15 |

16 | Spotify's Now Playing with Framer Motion 17 |

18 | 19 | 20 |
21 | ); 22 | }; 23 | 24 | export default Page; 25 | -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import { BackToTop, Banner, Footer, Header } from 'components'; 2 | import '../src/styles/global.css'; 3 | import '../src/styles/tailwind.css'; 4 | 5 | export default function RootLayout({ 6 | children 7 | }: { 8 | children: React.ReactNode; 9 | }) { 10 | return ( 11 | 12 | 13 | 17 | 59 | 60 | 61 | 65 |
66 | 67 | 68 | 69 | ); 70 | } 71 | } 72 | 73 | export default MyDocument; 74 | -------------------------------------------------------------------------------- /src/pages/api/og.tsx: -------------------------------------------------------------------------------- 1 | import { ImageResponse } from '@vercel/og'; 2 | import { NextRequest } from 'next/server'; 3 | 4 | export const config = { 5 | runtime: 'edge' 6 | }; 7 | 8 | const font = fetch( 9 | new URL('../../../public/assets/fonts/Karla/Karla.woff', import.meta.url) 10 | ).then((res) => res.arrayBuffer()); 11 | 12 | export default async function handler(req: NextRequest) { 13 | const fontData = await font; 14 | 15 | const { searchParams } = new URL(req.url.replaceAll('&%3B', '&')); 16 | 17 | const title = searchParams.has('title') 18 | ? searchParams.get('title') 19 | : 'My default title'; 20 | 21 | const desc = searchParams.has('desc') ? searchParams.get('desc') : null; 22 | const img = searchParams.has('img') ? searchParams.get('img') : null; 23 | 24 | const readTime = searchParams.has('readTime') 25 | ? searchParams.get('readTime') 26 | : null; 27 | const date = searchParams.has('date') ? searchParams.get('date') : null; 28 | 29 | const author = searchParams.has('author') ? searchParams.get('author') : null; 30 | 31 | return new ImageResponse( 32 | ( 33 |
34 |

{title}

35 | {desc &&

{desc}

} 36 |

37 | {author && `By ${author}`} {date && `• ${date}`}{' '} 38 | {readTime && `• ${readTime} min read`} 39 |

40 |
41 | {'og-image'} 49 |
50 |
51 | ), 52 | { 53 | width: 1600, 54 | height: 840, 55 | fonts: [ 56 | { 57 | name: 'Karla', 58 | data: fontData, 59 | style: 'normal' 60 | } 61 | ] 62 | } 63 | ); 64 | } 65 | -------------------------------------------------------------------------------- /src/pages/api/revalidate-post.ts: -------------------------------------------------------------------------------- 1 | import type { NextApiRequest, NextApiResponse } from 'next'; 2 | 3 | const handler = async (req: NextApiRequest, res: NextApiResponse) => { 4 | if (req.method === 'POST') { 5 | try { 6 | const { slug } = req.body; 7 | const token = req.headers[process.env.SANITY_WEBHOOK_SECRET_HEADER!]; 8 | if (token !== process.env.SANITY_WEBHOOK_SECRET_TOKEN) { 9 | return res.status(401).json({ message: 'Unauthorized' }); 10 | } 11 | await Promise.all([ 12 | res.revalidate('/'), 13 | res.revalidate('/blog'), 14 | res.revalidate(`/blog/${slug}`) 15 | ]); 16 | return res.status(200).json({ message: `Updated ${slug}` }); 17 | } catch (error) { 18 | console.error(error); 19 | res.status(500).send('Server Error'); 20 | } 21 | } else res.status(400).send('Request method is not supported'); 22 | }; 23 | 24 | export default handler; 25 | -------------------------------------------------------------------------------- /src/pages/api/revalidate-social.ts: -------------------------------------------------------------------------------- 1 | import type { NextApiRequest, NextApiResponse } from 'next'; 2 | 3 | const handler = async (req: NextApiRequest, res: NextApiResponse) => { 4 | if (req.method === 'POST') { 5 | try { 6 | const token = req.headers[process.env.SANITY_WEBHOOK_SECRET_HEADER!]; 7 | if (token !== process.env.SANITY_WEBHOOK_SECRET_TOKEN) { 8 | return res.status(401).json({ message: 'Unauthorized' }); 9 | } 10 | await Promise.all([res.revalidate('/socials')]); 11 | return res.status(200).json({ message: `Updated Socials` }); 12 | } catch (error) { 13 | console.error(error); 14 | res.status(500).send('Server Error'); 15 | } 16 | } else res.status(400).send('Request method is not supported'); 17 | }; 18 | 19 | export default handler; 20 | -------------------------------------------------------------------------------- /src/pages/api/revalidate-talk.ts: -------------------------------------------------------------------------------- 1 | import type { NextApiRequest, NextApiResponse } from 'next'; 2 | 3 | const handler = async (req: NextApiRequest, res: NextApiResponse) => { 4 | if (req.method === 'POST') { 5 | try { 6 | const token = req.headers[process.env.SANITY_WEBHOOK_SECRET_HEADER!]; 7 | if (token !== process.env.SANITY_WEBHOOK_SECRET_TOKEN) { 8 | return res.status(401).json({ message: 'Unauthorized' }); 9 | } 10 | await Promise.all([res.revalidate('/')]); 11 | return res.status(200).json({ message: `Updated Talks on Index` }); 12 | } catch (error) { 13 | console.error(error); 14 | res.status(500).send('Server Error'); 15 | } 16 | } else res.status(400).send('Request method is not supported'); 17 | }; 18 | 19 | export default handler; 20 | -------------------------------------------------------------------------------- /src/pages/api/spotify/nowPlaying.ts: -------------------------------------------------------------------------------- 1 | import { NextApiRequest, NextApiResponse } from 'next'; 2 | import { type } from 'os'; 3 | import { getNowPlaying } from 'services/spotify'; 4 | 5 | export default async function handler( 6 | req: NextApiRequest, 7 | res: NextApiResponse 8 | ) { 9 | const song = await getNowPlaying(); 10 | 11 | if (!song || song.is_playing === false) { 12 | return res.status(200).json({ isPlaying: false }); 13 | } 14 | 15 | const isPlaying = song.is_playing; 16 | const title = song.item.name; 17 | // @ts-ignore 18 | const artist = song.item.artists 19 | .map((_artist: any) => _artist.name) 20 | .join(', '); 21 | // @ts-ignore 22 | const album = song.item.album.name; 23 | const songUrl = song.item.external_urls.spotify; 24 | const id = song.item.id; 25 | // @ts-ignore 26 | const previewUrl = song.item.preview_url; 27 | 28 | return res.status(200).json({ 29 | id, 30 | album, 31 | artist, 32 | isPlaying, 33 | songUrl, 34 | title, 35 | previewUrl 36 | }); 37 | } 38 | -------------------------------------------------------------------------------- /src/pages/api/spotify/recently-played.ts: -------------------------------------------------------------------------------- 1 | import { NextApiRequest, NextApiResponse } from 'next'; 2 | import { getRecentlyPlayed } from 'services/spotify'; 3 | 4 | export default async function handler( 5 | req: NextApiRequest, 6 | res: NextApiResponse 7 | ) { 8 | if (req.method !== 'GET') { 9 | return res.status(405).json({ message: 'Method not allowed' }); 10 | } 11 | 12 | try { 13 | const tracks = await getRecentlyPlayed(); 14 | res.status(200).json(tracks.slice(0, 10)); 15 | } catch (error) { 16 | console.error('Error in recently-played API:', error); 17 | res.status(500).json({ message: 'Failed to fetch recently played tracks' }); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/pages/api/views.ts: -------------------------------------------------------------------------------- 1 | import type { NextApiRequest, NextApiResponse } from 'next'; 2 | import { getClient } from 'services/sanity-server'; 3 | 4 | const handler = async (req: NextApiRequest, res: NextApiResponse) => { 5 | if (req.method === 'POST') { 6 | try { 7 | if (req.preview) { 8 | res.status(403).send('Forbidden for Preview Mode'); 9 | return; 10 | } 11 | 12 | const { page_id } = JSON.parse(req.body); 13 | const doc = await getClient(false).mutate([ 14 | { 15 | patch: { 16 | id: page_id, 17 | inc: { 18 | views: 1 19 | } 20 | } 21 | } 22 | ]); 23 | res.status(200).json(doc); 24 | } catch (error) { 25 | console.error(error); 26 | res.status(500).send('Server Error'); 27 | } 28 | } else res.send('Request method is not supported'); 29 | }; 30 | 31 | export default handler; 32 | -------------------------------------------------------------------------------- /src/pages/rss.xml.tsx: -------------------------------------------------------------------------------- 1 | import type { GetServerSideProps } from 'next'; 2 | import { generateRSSFeed } from 'services/rss'; 3 | import { getClient } from 'services/sanity-server'; 4 | import type { Blog } from 'types'; 5 | 6 | const RSS = () => { 7 | return null; 8 | }; 9 | 10 | export default RSS; 11 | 12 | export const getServerSideProps: GetServerSideProps = async ({ 13 | res, 14 | preview = false 15 | }) => { 16 | const blogs: Array = await getClient(preview).fetch( 17 | `*[_type == "post"] | order(date desc) {...,"slug": slug.current, "id": _id, "readTime": round(length(body) / 5 / 180 )}` 18 | ); 19 | const rss = generateRSSFeed(blogs); 20 | res.setHeader('Content-Type', 'text/xml'); 21 | res.write(rss); 22 | res.end(); 23 | 24 | return { props: {} }; 25 | }; 26 | -------------------------------------------------------------------------------- /src/pages/sitemap.xml.tsx: -------------------------------------------------------------------------------- 1 | import type { GetServerSideProps } from 'next'; 2 | import prettier from 'prettier'; 3 | import { DOMAIN } from 'services/constants'; 4 | import { getClient } from 'services/sanity-server'; 5 | import type { Blog } from 'types'; 6 | 7 | const generate = async (preview: boolean) => { 8 | const prettierConfig = await prettier.resolveConfig('../../.prettierrc'); 9 | const pages = [ 10 | '', 11 | 'about', 12 | 'blog', 13 | 'craft', 14 | 'socials', 15 | 'spotify', 16 | 'talks', 17 | 'wall', 18 | 'work' 19 | ]; 20 | 21 | const blogs: Array = await getClient(preview).fetch( 22 | `*[_type == "post"] | order(date desc) {"slug": slug.current}` 23 | ); 24 | 25 | const sitemap = ` 26 | 27 | 28 | ${blogs 29 | .map((blog) => { 30 | return ` 31 | 32 | ${`${DOMAIN}/blog/${blog.slug}`} 33 | 34 | `; 35 | }) 36 | .join('')} 37 | ${pages 38 | .map((page) => { 39 | return ` 40 | 41 | ${`${DOMAIN}/${page}`} 42 | 43 | `; 44 | }) 45 | .join('')} 46 | 47 | 48 | 49 | `; 50 | 51 | const formatted = prettier.format(sitemap, { 52 | ...prettierConfig, 53 | parser: 'html' 54 | }); 55 | 56 | // eslint-disable-next-line no-sync 57 | return formatted; 58 | }; 59 | 60 | const Sitemap = () => { 61 | return null; 62 | }; 63 | 64 | export default Sitemap; 65 | 66 | export const getServerSideProps: GetServerSideProps = async ({ 67 | res, 68 | preview = false 69 | }) => { 70 | res.setHeader('Content-Type', 'text/xml'); 71 | const posts = await generate(preview); 72 | res.write(posts); 73 | res.end(); 74 | 75 | return { props: {} }; 76 | }; 77 | -------------------------------------------------------------------------------- /src/services/constants.ts: -------------------------------------------------------------------------------- 1 | export const DOMAIN = 'https://shubhamverma.me'; 2 | export const SHORT_DOMAIN = 'https://shbm.fyi'; 3 | 4 | export const TWITTER_HANDLE = '@verma__shubham'; 5 | export const TWITTER_URL = `${SHORT_DOMAIN}/tw`; 6 | export const LINKEDIN_HANDLE = 'shubhamverma1811'; 7 | export const LINKEDIN_URL = `${SHORT_DOMAIN}/li`; 8 | export const GITHUB_HANDLE = 'shubhamverma1811'; 9 | export const GITHUB_URL = `${SHORT_DOMAIN}/gh`; 10 | export const INSTAGRAM_URL = `${SHORT_DOMAIN}/ig`; 11 | export const SPOTIFY_URL = `${SHORT_DOMAIN}/sp`; 12 | export const CAL_URL = `${SHORT_DOMAIN}/cal`; 13 | export const CURRENT_ORGANIZATION = 'GeekyAnts'; 14 | export const CURRENT_TITLE = 'Senior Software Engineer - II'; 15 | export const RESUME_URL = `${SHORT_DOMAIN}/resume`; 16 | export const HIRE_MAIL = "hi@shubhamverma.me"; 17 | export const CITY = "Hyderabad"; 18 | export const COUNTRY = "India" -------------------------------------------------------------------------------- /src/services/fetcher.ts: -------------------------------------------------------------------------------- 1 | export const fetcher = (...args: any) => 2 | // @ts-ignore 3 | fetch(...args).then((res) => res.json()); 4 | -------------------------------------------------------------------------------- /src/services/image-transformer.ts: -------------------------------------------------------------------------------- 1 | import type { TransformerArgs, TransformerResult } from 'rehype-image-resize'; 2 | 3 | export const transformer = ({ 4 | src, 5 | alt 6 | }: TransformerArgs): TransformerResult => { 7 | if (!src || !alt) return null; 8 | const dimensionsRegex = /\[\[(.*?)\]\]/; 9 | const dimensions = alt?.match(dimensionsRegex); 10 | if (dimensions) { 11 | if (!dimensions?.[1].includes(' x ')) return; 12 | const [width, height] = dimensions?.[1]?.split(' x '); 13 | return { 14 | width, 15 | height, 16 | alt: alt.replace(dimensionsRegex, '').trim() 17 | }; 18 | } 19 | }; 20 | -------------------------------------------------------------------------------- /src/services/rehype-image-blur.ts: -------------------------------------------------------------------------------- 1 | import { getPlaiceholder } from 'plaiceholder'; 2 | // @ts-ignore 3 | import { visit } from 'unist-util-visit'; 4 | 5 | // TODO:: fix types 6 | export default function rehypeImageBlur() { 7 | // @ts-ignore 8 | return async (tree) => { 9 | // @ts-ignore 10 | const nodes = []; 11 | 12 | // @ts-ignore 13 | visit(tree, (node) => { 14 | if (node.tagName === 'img') { 15 | nodes.push(node); 16 | } 17 | }); 18 | 19 | await Promise.all( 20 | // @ts-ignore 21 | nodes.map(async (node) => { 22 | if (node.properties.src) { 23 | node.properties.hash = ( 24 | await getPlaiceholder(node.properties.src) 25 | ).base64; 26 | } 27 | }) 28 | ); 29 | 30 | return tree; 31 | }; 32 | } 33 | -------------------------------------------------------------------------------- /src/services/rss.ts: -------------------------------------------------------------------------------- 1 | import { Feed, Item } from 'feed'; 2 | import { remark } from 'remark'; 3 | import remarkHTML from 'remark-html'; 4 | import type { Blog } from 'types'; 5 | import { HIRE_MAIL, TWITTER_URL } from './constants'; 6 | 7 | export const generateRSSFeed = (blogs: Array) => { 8 | const baseURL = process.env.DOMAIN!; 9 | 10 | const author = { 11 | name: 'Shubham Verma', 12 | link: TWITTER_URL, 13 | email: HIRE_MAIL 14 | }; 15 | 16 | const feed = new Feed({ 17 | title: 'Blogs by Shubham Verma', 18 | description: 19 | 'I blog about open source tools, writing blogs on problems and solutions faced by developers, and other stuff.', 20 | id: baseURL, 21 | link: baseURL, 22 | language: 'en', 23 | author, 24 | copyright: `Copyright © ${new Date().getFullYear()} Shubham Verma`, 25 | feedLinks: { 26 | rss2: `${baseURL}/rss.xml` 27 | } 28 | }); 29 | 30 | blogs?.forEach((blog) => { 31 | const d = new Date(blog.date); 32 | const date = `${d.getDate()}/${d.getMonth() + 1}/${d.getFullYear()}`; 33 | const cover = `${baseURL}/api/og?title=${blog.title}&readTime=${blog.readTime}&date=${date}`; 34 | 35 | const html = remark() 36 | .use(remarkHTML) 37 | .processSync(blog.body ?? '') 38 | .toString(); 39 | 40 | const item: Item = { 41 | title: blog.title, 42 | id: blog.id, 43 | date: new Date(blog.date), 44 | link: `${baseURL}/blog/${blog.slug}`, 45 | author: [{ ...author }], 46 | description: blog.summary, 47 | // image: cover, 48 | content: html, 49 | published: new Date(blog.date) 50 | }; 51 | 52 | feed.addItem(item); 53 | }); 54 | 55 | return feed.rss2(); 56 | }; 57 | -------------------------------------------------------------------------------- /src/services/sanity-config.ts: -------------------------------------------------------------------------------- 1 | export const sanityConfig = { 2 | dataset: process.env.SANITY_DATASET || 'dev', 3 | projectId: process.env.SANITY_PROJECT_ID!, 4 | useCdn: process.env.NODE_ENV === 'production', 5 | apiVersion: '2021-03-25', 6 | token: process.env.SANITY_API_TOKEN 7 | }; 8 | -------------------------------------------------------------------------------- /src/services/sanity-server.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Server-side Sanity utilities. By having these in a separate file from the 3 | * utilities we use on the client side, we are able to tree-shake (remove) 4 | * code that is not used on the client side. 5 | */ 6 | import { createClient } from 'next-sanity'; 7 | import { sanityConfig } from './sanity-config'; 8 | import imageUrlBuilder from '@sanity/image-url'; 9 | 10 | export const sanityClient = createClient(sanityConfig); 11 | 12 | export const previewClient = createClient({ 13 | ...sanityConfig, 14 | useCdn: false 15 | }); 16 | 17 | export const getClient = (preview?: boolean) => 18 | preview ? previewClient : sanityClient; 19 | 20 | const builder = imageUrlBuilder(getClient()); 21 | 22 | //@ts-ignore 23 | export const urlFor = (source) => { 24 | return builder.image(source); 25 | }; 26 | -------------------------------------------------------------------------------- /src/services/spotify.ts: -------------------------------------------------------------------------------- 1 | import { SpotifyApi } from '@spotify/web-api-ts-sdk'; 2 | 3 | const client_id = process.env.SPOTIFY_CLIENT_ID!; 4 | const client_secret = process.env.SPOTIFY_CLIENT_SECRET!; 5 | const refresh_token = process.env.SPOTIFY_REFRESH_TOKEN!; 6 | 7 | if (!client_id || !client_secret || !refresh_token) { 8 | throw new Error('Missing required Spotify environment variables'); 9 | } 10 | 11 | export type Song = { 12 | idx: number; 13 | id: string; 14 | artist: string; 15 | title: string; 16 | imageUrl: string; 17 | previewUrl: string; 18 | }; 19 | 20 | let accessToken: string | null = null; 21 | let tokenExpirationTime: number | null = null; 22 | 23 | async function refreshAccessToken(): Promise { 24 | const response = await fetch('https://accounts.spotify.com/api/token', { 25 | method: 'POST', 26 | headers: { 27 | 'Content-Type': 'application/x-www-form-urlencoded', 28 | Authorization: `Basic ${Buffer.from( 29 | `${client_id}:${client_secret}` 30 | ).toString('base64')}` 31 | }, 32 | body: new URLSearchParams({ 33 | grant_type: 'refresh_token', 34 | refresh_token: refresh_token 35 | }) 36 | }); 37 | 38 | if (!response.ok) { 39 | throw new Error('Failed to refresh access token'); 40 | } 41 | 42 | const data = await response.json(); 43 | accessToken = data.access_token; 44 | tokenExpirationTime = Date.now() + data.expires_in * 1000; 45 | return accessToken; 46 | } 47 | 48 | async function getValidAccessToken(): Promise { 49 | if ( 50 | !accessToken || 51 | !tokenExpirationTime || 52 | Date.now() >= tokenExpirationTime 53 | ) { 54 | return refreshAccessToken(); 55 | } 56 | return accessToken; 57 | } 58 | 59 | async function getSpotifyApi(): Promise { 60 | const token = await getValidAccessToken(); 61 | return SpotifyApi.withAccessToken(client_id, { 62 | access_token: token, 63 | token_type: 'Bearer', 64 | expires_in: 3600 65 | }); 66 | } 67 | 68 | export const getNowPlaying = async () => { 69 | try { 70 | const spotify = await getSpotifyApi(); 71 | const response = await spotify.player.getCurrentlyPlayingTrack(); 72 | 73 | return response; 74 | } catch (error) { 75 | console.error('Error getting now playing:', error); 76 | throw error; 77 | } 78 | }; 79 | 80 | export const getTopTracks = async () => { 81 | try { 82 | const spotify = await getSpotifyApi(); 83 | const response = await spotify.currentUser.topItems( 84 | 'tracks', 85 | 'short_term', 86 | 10 87 | ); 88 | return response; 89 | } catch (error) { 90 | console.error('Error getting top tracks:', error); 91 | throw error; 92 | } 93 | }; 94 | 95 | export const getTopArtists = async () => { 96 | try { 97 | const spotify = await getSpotifyApi(); 98 | const response = await spotify.currentUser.topItems( 99 | 'artists', 100 | 'short_term', 101 | 10 102 | ); 103 | return response; 104 | } catch (error) { 105 | console.error('Error getting top artists:', error); 106 | throw error; 107 | } 108 | }; 109 | 110 | export const getRecentlyPlayed = async () => { 111 | try { 112 | const spotify = await getSpotifyApi(); 113 | const response = await spotify.player.getRecentlyPlayedTracks(); 114 | const tracks = response.items.map((item, idx) => ({ 115 | idx, 116 | id: item.track.id, 117 | artist: item.track.artists.map((artist) => artist.name).join(', '), 118 | title: item.track.name, 119 | imageUrl: item.track.album.images[0].url, 120 | previewUrl: item.track.preview_url, 121 | time: new Date(item.played_at) 122 | })); 123 | return tracks; 124 | } catch (error) { 125 | console.error('Error getting recently played:', error); 126 | return []; 127 | } 128 | }; 129 | -------------------------------------------------------------------------------- /src/services/util.ts: -------------------------------------------------------------------------------- 1 | import type { Metadata } from 'next'; 2 | 3 | export function generateMetaData({ 4 | title, 5 | description 6 | }: { 7 | title: string; 8 | description?: string; 9 | }): Metadata { 10 | return { 11 | metadataBase: new URL(process.env.DOMAIN!), 12 | title: title, 13 | description: description, 14 | openGraph: { 15 | title: title, 16 | description: description, 17 | images: [ 18 | { 19 | url: `${process.env.DOMAIN}/api/og?title=${title}&desc=${description}` 20 | } 21 | ] 22 | } 23 | }; 24 | } 25 | -------------------------------------------------------------------------------- /src/store/atoms/theme.ts: -------------------------------------------------------------------------------- 1 | import { atom } from 'jotai'; 2 | 3 | export const isDarkModeAtom = atom(true); 4 | -------------------------------------------------------------------------------- /src/styles/global.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: Karla; 3 | font-display: swap; 4 | src: local('Karla'), 5 | url('../../public/assets/fonts/Karla/Karla.woff') format('woff'); 6 | } 7 | 8 | @font-face { 9 | font-family: 'Mona-Sans'; 10 | font-display: swap; 11 | src: local('Mona-Sans'), 12 | url('../../public/assets/fonts/Mona-Sans/Mona-Sans.woff2') format('woff2'); 13 | } 14 | 15 | :root { 16 | font-family: 'Karla'; 17 | } 18 | 19 | html { 20 | scroll-behavior: smooth; 21 | } 22 | -------------------------------------------------------------------------------- /src/styles/tailwind.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | :root { 7 | --color-primary: #f9fafb; 8 | --color-secondary: #111827; 9 | --color-primary-muted: #a3a3a3; 10 | --color-secondary-muted: #f0f2f4; 11 | --color-accent: #60a5fa; 12 | } 13 | 14 | .dark { 15 | --color-primary: #111; 16 | --color-secondary: #f9fafb; 17 | --color-primary-muted: #6b7280; 18 | --color-secondary-muted: #1f2021; 19 | --color-accent: #60a5fa; 20 | } 21 | } 22 | 23 | .anchor { 24 | color: var(--color-secondary) !important; 25 | text-decoration: none; 26 | font-weight: bold; 27 | } 28 | 29 | .anchor:hover { 30 | text-decoration: underline; 31 | text-underline-offset: 4px; 32 | } 33 | -------------------------------------------------------------------------------- /src/types/blogs.types.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | 3 | const BlogScheme = z.object({ 4 | id: z.string(), 5 | title: z.string(), 6 | summary: z.string(), 7 | slug: z.string(), 8 | date: z.date(), 9 | publicationUrl: z.string().optional(), 10 | canonicalUrl: z.string().optional(), 11 | readTime: z.number(), 12 | views: z.number(), 13 | cover: z.string().optional(), 14 | body: z.string() 15 | }); 16 | 17 | export type Blog = z.infer; 18 | -------------------------------------------------------------------------------- /src/types/books.types.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | 3 | const BookScheme = z.object({ 4 | title: z.string(), 5 | author: z.string(), 6 | progress: z.union([ 7 | z.literal('read'), 8 | z.literal('reading'), 9 | z.literal('wishlist'), 10 | z.literal('favorite') 11 | ]), 12 | link: z.string(), 13 | cover: z.string() 14 | }); 15 | 16 | export type Book = z.infer; 17 | -------------------------------------------------------------------------------- /src/types/index.ts: -------------------------------------------------------------------------------- 1 | export * from './blogs.types'; 2 | export * from './books.types'; 3 | export * from './projects.types'; 4 | export * from './socials.types'; 5 | export * from './spotify.types'; 6 | export * from './talks.types'; 7 | -------------------------------------------------------------------------------- /src/types/projects.types.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | 3 | const ProjectScheme = z.object({ 4 | title: z.string(), 5 | summary: z.string(), 6 | live: z.string(), 7 | repo: z.string() 8 | }); 9 | 10 | export type Project = z.infer; 11 | -------------------------------------------------------------------------------- /src/types/socials.types.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | 3 | const SocialScheme = z.object({ 4 | id: z.string(), 5 | name: z.string(), 6 | url: z.string(), 7 | color: z.string().optional(), 8 | handle: z.string() 9 | }); 10 | 11 | export type Social = z.infer; 12 | -------------------------------------------------------------------------------- /src/types/spotify.types.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | 3 | const NowPlayingScheme = z.object({ 4 | id: z.string(), 5 | album: z.string(), 6 | artist: z.string(), 7 | isPlaying: z.string(), 8 | songUrl: z.string(), 9 | title: z.string(), 10 | previewUrl: z.string() 11 | }); 12 | 13 | export type NowPlaying = z.infer; 14 | -------------------------------------------------------------------------------- /src/types/talks.types.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | 3 | const TalkScheme = z.object({ 4 | id: z.string(), 5 | title: z.string(), 6 | description: z.string().optional(), 7 | date: z.string(), 8 | time: z.string().optional(), 9 | url: z.string() 10 | }); 11 | 12 | export type Talk = z.infer; 13 | -------------------------------------------------------------------------------- /src/types/testimonials.type.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | 3 | const TestimonialScheme = z.object({ 4 | id: z.number(), 5 | author: z.string(), 6 | role: z.string(), 7 | company: z.string(), 8 | quote: z.string(), 9 | avatar: z.string().url() 10 | }); 11 | 12 | export type Testimonial = z.infer; 13 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @type {import('tailwindcss/tailwind-config').TailwindConfig} 3 | */ 4 | module.exports = { 5 | content: ['./src/**/*.{ts,tsx,html}', './app/**/*.{ts,tsx,html}'], 6 | darkMode: 'class', 7 | theme: { 8 | extend: { 9 | textColor: { 10 | skin: { 11 | primary: 'var(--color-primary)', 12 | secondary: 'var(--color-secondary)', 13 | accent: 'var(--color-accent)', 14 | 'primary-muted': 'var(--color-primary-muted)', 15 | 'secondary-muted': 'var(--color-secondary-muted)' 16 | } 17 | }, 18 | backgroundColor: (theme) => { 19 | return { 20 | skin: { 21 | primary: 'var(--color-primary)', 22 | secondary: 'var(--color-secondary)', 23 | accent: 'var(--color-accent)', 24 | 'primary-muted': (props) => { 25 | return 'var(--color-primary-muted)'; 26 | }, 27 | 'secondary-muted': 'var(--color-secondary-muted)' 28 | } 29 | }; 30 | }, 31 | borderColor: (theme) => { 32 | return { 33 | skin: { 34 | primary: 'var(--color-primary)', 35 | secondary: 'var(--color-secondary)', 36 | accent: 'var(--color-accent)', 37 | 'primary-muted': (props) => { 38 | return 'var(--color-primary-muted)'; 39 | }, 40 | 'secondary-muted': 'var(--color-secondary-muted)' 41 | } 42 | }; 43 | }, 44 | fontFamily: { 45 | primary: ['Karla'], 46 | secondary: ['Mona-Sans'] 47 | } 48 | } 49 | }, 50 | 51 | plugins: [ 52 | require('@tailwindcss/typography'), 53 | require('prettier-plugin-tailwindcss') 54 | ] 55 | }; 56 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "noUnusedLocals": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "noEmit": true, 11 | "esModuleInterop": true, 12 | "module": "esnext", 13 | "moduleResolution": "node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "jsx": "preserve", 17 | "incremental": true, 18 | "baseUrl": "./src", 19 | "plugins": [ 20 | { 21 | "name": "next" 22 | } 23 | ] 24 | }, 25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 26 | "exclude": ["node_modules"] 27 | } 28 | -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://openapi.vercel.sh/vercel.json", 3 | "redirects": [ 4 | { 5 | "destination": "https://shbm.fyi/:path*", 6 | "source": "/s/:path*" 7 | } 8 | ] 9 | } 10 | --------------------------------------------------------------------------------