├── .eslintrc.json ├── .gitignore ├── .vscode └── settings.json ├── README.md ├── app ├── api │ └── sentry-example-api │ │ └── route.js ├── global-error.jsx ├── globals.css ├── layout.tsx ├── page.tsx ├── provider.tsx └── sentry-example-page │ └── page.jsx ├── components.json ├── components ├── Approach.tsx ├── Clients.tsx ├── Experience.tsx ├── Footer.tsx ├── Grid.tsx ├── Hero.tsx ├── MagicButton.tsx ├── RecentProjects.tsx └── ui │ ├── BentoGrid.tsx │ ├── CanvasRevealEffect.tsx │ ├── FloatingNavbar.tsx │ ├── Globe.tsx │ ├── GradientBg.tsx │ ├── GridGlobe.tsx │ ├── HoverBorder.tsx │ ├── InfiniteCards.tsx │ ├── LayoutGrid.tsx │ ├── MovingBorders.tsx │ ├── Pin.tsx │ ├── Spotlight.tsx │ └── TextGenerateEffect.tsx ├── data ├── confetti.json ├── globe.json └── index.ts ├── lib └── utils.ts ├── next.config.mjs ├── package-lock.json ├── package.json ├── postcss.config.js ├── public ├── app.svg ├── appName.svg ├── b1.svg ├── b4.svg ├── b5.svg ├── bg.png ├── c.svg ├── cloud.svg ├── cloudName.svg ├── confetti.gif ├── dock.svg ├── dockerName.svg ├── exp1.svg ├── exp2.svg ├── exp3.svg ├── exp4.svg ├── fm.svg ├── footer-grid.svg ├── git.svg ├── grid.svg ├── gsap.svg ├── host.svg ├── hostName.svg ├── insta.svg ├── jsm-logo.png ├── link.svg ├── next.svg ├── p1.svg ├── p2.svg ├── p3.svg ├── p4.svg ├── profile.svg ├── re.svg ├── s.svg ├── stream.svg ├── streamName.svg ├── tail.svg ├── three.svg ├── ts.svg ├── twit.svg └── wha.svg ├── sentry.client.config.ts ├── sentry.edge.config.ts ├── sentry.server.config.ts ├── tailwind.config.ts └── tsconfig.json /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | 38 | # Sentry Config File 39 | .sentryclirc 40 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnPaste": true, 3 | "editor.formatOnSave": true, 4 | "[typescriptreact]": { 5 | "editor.defaultFormatter": "esbenp.prettier-vscode" 6 | }, 7 | "[typescript]": { 8 | "editor.defaultFormatter": "esbenp.prettier-vscode" 9 | }, 10 | "editor.codeActionsOnSave": { 11 | "source.fixAll.eslint": "explicit" 12 | }, 13 | "editor.wordWrap": "on" 14 | } 15 | -------------------------------------------------------------------------------- /app/api/sentry-example-api/route.js: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server"; 2 | 3 | export const dynamic = "force-dynamic"; 4 | 5 | // A faulty API route to test Sentry's error monitoring 6 | export function GET() { 7 | throw new Error("Sentry Example API Route Error"); 8 | return NextResponse.json({ data: "Testing Sentry Error..." }); 9 | } 10 | -------------------------------------------------------------------------------- /app/global-error.jsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as Sentry from "@sentry/nextjs"; 4 | import Error from "next/error"; 5 | import { useEffect } from "react"; 6 | 7 | export default function GlobalError({ error }) { 8 | useEffect(() => { 9 | Sentry.captureException(error); 10 | }, [error]); 11 | 12 | return ( 13 | 14 | 15 | 16 | 17 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | :root { 7 | --background: 0 0% 100%; 8 | --foreground: 240 10% 3.9%; 9 | 10 | --card: 0 0% 100%; 11 | --card-foreground: 240 10% 3.9%; 12 | 13 | --popover: 0 0% 100%; 14 | --popover-foreground: 240 10% 3.9%; 15 | 16 | --primary: 240 5.9% 10%; 17 | --primary-foreground: 0 0% 98%; 18 | 19 | --secondary: 240 4.8% 95.9%; 20 | --secondary-foreground: 240 5.9% 10%; 21 | 22 | --muted: 240 4.8% 95.9%; 23 | --muted-foreground: 240 3.8% 46.1%; 24 | 25 | --accent: 240 4.8% 95.9%; 26 | --accent-foreground: 240 5.9% 10%; 27 | 28 | --destructive: 0 84.2% 60.2%; 29 | --destructive-foreground: 0 0% 98%; 30 | 31 | --border: 240 5.9% 90%; 32 | --input: 240 5.9% 90%; 33 | --ring: 240 10% 3.9%; 34 | 35 | --radius: 0.5rem; 36 | } 37 | 38 | .dark { 39 | --background: 240 10% 3.9%; 40 | --foreground: 0 0% 98%; 41 | 42 | --card: 240 10% 3.9%; 43 | --card-foreground: 0 0% 98%; 44 | 45 | --popover: 240 10% 3.9%; 46 | --popover-foreground: 0 0% 98%; 47 | 48 | --primary: 0 0% 98%; 49 | --primary-foreground: 240 5.9% 10%; 50 | 51 | --secondary: 240 3.7% 15.9%; 52 | --secondary-foreground: 0 0% 98%; 53 | 54 | --muted: 240 3.7% 15.9%; 55 | --muted-foreground: 240 5% 64.9%; 56 | 57 | --accent: 240 3.7% 15.9%; 58 | --accent-foreground: 0 0% 98%; 59 | 60 | --destructive: 0 62.8% 30.6%; 61 | --destructive-foreground: 0 0% 98%; 62 | 63 | --border: 240 3.7% 15.9%; 64 | --input: 240 3.7% 15.9%; 65 | --ring: 240 4.9% 83.9%; 66 | } 67 | } 68 | 69 | @layer base { 70 | * { 71 | @apply border-border !scroll-smooth; 72 | } 73 | body { 74 | @apply bg-background text-foreground; 75 | } 76 | button { 77 | @apply active:outline-none; 78 | } 79 | } 80 | 81 | @layer utilities { 82 | .heading { 83 | @apply font-bold text-4xl md:text-5xl text-center; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import { Inter } from "next/font/google"; 3 | 4 | import "./globals.css"; 5 | import { ThemeProvider } from "./provider"; 6 | 7 | const inter = Inter({ subsets: ["latin"] }); 8 | 9 | export const metadata: Metadata = { 10 | title: "Adrian's Portfolio", 11 | description: "Modern & Minimal JS Mastery Portfolio", 12 | }; 13 | 14 | export default function RootLayout({ 15 | children, 16 | }: Readonly<{ 17 | children: React.ReactNode; 18 | }>) { 19 | return ( 20 | 21 | 22 | 23 | 24 | 25 | 31 | {children} 32 | 33 | 34 | 35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /app/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { navItems } from "@/data"; 4 | 5 | import Hero from "@/components/Hero"; 6 | import Grid from "@/components/Grid"; 7 | import Footer from "@/components/Footer"; 8 | import Clients from "@/components/Clients"; 9 | import Approach from "@/components/Approach"; 10 | import Experience from "@/components/Experience"; 11 | import RecentProjects from "@/components/RecentProjects"; 12 | import { FloatingNav } from "@/components/ui/FloatingNavbar"; 13 | 14 | const Home = () => { 15 | return ( 16 |
17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 |
27 |
28 | ); 29 | }; 30 | 31 | export default Home; 32 | -------------------------------------------------------------------------------- /app/provider.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import { ThemeProvider as NextThemesProvider } from "next-themes"; 5 | import { type ThemeProviderProps } from "next-themes/dist/types"; 6 | 7 | export function ThemeProvider({ children, ...props }: ThemeProviderProps) { 8 | return {children}; 9 | } 10 | -------------------------------------------------------------------------------- /app/sentry-example-page/page.jsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import Head from "next/head"; 4 | import * as Sentry from "@sentry/nextjs"; 5 | 6 | export default function Page() { 7 | return ( 8 |
9 | 10 | Sentry Onboarding 11 | 12 | 13 | 14 |
23 |

24 | 31 | 35 | 36 |

37 | 38 |

Get started by sending us a sample error:

39 | 65 | 66 |

67 | Next, look for the error on the{" "} 68 | Issues Page. 69 |

70 |

71 | For more information, see{" "} 72 | 73 | https://docs.sentry.io/platforms/javascript/guides/nextjs/ 74 | 75 |

76 |
77 |
78 | ); 79 | } 80 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "app/globals.css", 9 | "baseColor": "zinc", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils" 16 | } 17 | } -------------------------------------------------------------------------------- /components/Approach.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { AnimatePresence, motion } from "framer-motion"; 3 | 4 | import { CanvasRevealEffect } from "./ui/CanvasRevealEffect"; 5 | 6 | const Approach = () => { 7 | return ( 8 |
9 |

10 | My approach 11 |

12 | {/* remove bg-white dark:bg-black */} 13 |
14 | {/* add des prop */} 15 | } 18 | des="We'll collaborate to map out your website's goals, target audience, 19 | and key functionalities. We'll discuss things like site structure, 20 | navigation, and content requirements." 21 | > 22 | rounded-3xl overflow-hidden 25 | containerClassName="bg-emerald-900 rounded-3xl overflow-hidden" 26 | /> 27 | 28 | } 31 | des="Once we agree on the plan, I cue my lofi playlist and dive into 32 | coding. From initial sketches to polished code, I keep you updated 33 | every step of the way." 34 | > 35 | 46 | {/* Radial gradient for the cute fade */} 47 | {/* remove this one */} 48 | {/*
*/} 49 | 50 | } 53 | des="This is where the magic happens! Based on the approved design, 54 | I'll translate everything into functional code, building your website 55 | from the ground up." 56 | > 57 | 62 | 63 |
64 |
65 | ); 66 | }; 67 | 68 | export default Approach; 69 | 70 | const Card = ({ 71 | title, 72 | icon, 73 | children, 74 | // add this one for the desc 75 | des, 76 | }: { 77 | title: string; 78 | icon: React.ReactNode; 79 | children?: React.ReactNode; 80 | des: string; 81 | }) => { 82 | const [hovered, setHovered] = React.useState(false); 83 | return ( 84 |
setHovered(true)} 86 | onMouseLeave={() => setHovered(false)} 87 | // change h-[30rem] to h-[35rem], add rounded-3xl 88 | className="border border-black/[0.2] group/canvas-card flex items-center justify-center 89 | dark:border-white/[0.2] max-w-sm w-full mx-auto p-4 relative lg:h-[35rem] rounded-3xl " 90 | style={{ 91 | // add these two 92 | // you can generate the color from here https://cssgradient.io/ 93 | background: "rgb(4,7,29)", 94 | backgroundColor: 95 | "linear-gradient(90deg, rgba(4,7,29,1) 0%, rgba(12,14,35,1) 100%)", 96 | }} 97 | > 98 | {/* change to h-10 w-10 , add opacity-30 */} 99 | 100 | 101 | 102 | 103 | 104 | 105 | {hovered && ( 106 | 111 | {children} 112 | 113 | )} 114 | 115 | 116 |
117 |
123 | {icon} 124 |
125 |

131 | {title} 132 |

133 | {/* add this one for the description */} 134 |

140 | {des} 141 |

142 |
143 |
144 | ); 145 | }; 146 | // add order prop for the Phase number change 147 | const AceternityIcon = ({ order }: { order: string }) => { 148 | return ( 149 |
150 | {/* this btn is from https://ui.aceternity.com/components/tailwindcss-buttons border magic */} 151 | {/* change rounded-lg, text-purple px-5 py-2 */} 152 | {/* remove focus:outline-none focus:ring-2 focus:ring-slate-400 focus:ring-offset-2 focus:ring-offset-slate-50 cuz we don't need to focus */} 153 | {/* remove text-sm font-medium h-12 , add font-bold text-2xl */} 154 | 166 |
167 | // remove the svg and add the button 168 | // 176 | // 184 | // 185 | ); 186 | }; 187 | 188 | export const Icon = ({ className, ...rest }: any) => { 189 | return ( 190 | 199 | 200 | 201 | ); 202 | }; 203 | -------------------------------------------------------------------------------- /components/Clients.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React from "react"; 4 | 5 | import { companies, testimonials } from "@/data"; 6 | import { InfiniteMovingCards } from "./ui/InfiniteCards"; 7 | 8 | const Clients = () => { 9 | return ( 10 |
11 |

12 | Kind words from 13 | satisfied clients 14 |

15 | 16 |
17 |
21 | 26 |
27 | 28 |
29 | {companies.map((company) => ( 30 | 31 |
32 | {company.name} 37 | {company.name} 43 |
44 |
45 | ))} 46 |
47 |
48 |
49 | ); 50 | }; 51 | 52 | export default Clients; 53 | -------------------------------------------------------------------------------- /components/Experience.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import { workExperience } from "@/data"; 4 | import { Button } from "./ui/MovingBorders"; 5 | 6 | const Experience = () => { 7 | return ( 8 |
9 |

10 | My work experience 11 |

12 | 13 |
14 | {workExperience.map((card) => ( 15 | 48 | ))} 49 |
50 |
51 | ); 52 | }; 53 | 54 | export default Experience; 55 | -------------------------------------------------------------------------------- /components/Footer.tsx: -------------------------------------------------------------------------------- 1 | import { FaLocationArrow } from "react-icons/fa6"; 2 | 3 | import { socialMedia } from "@/data"; 4 | import MagicButton from "./MagicButton"; 5 | 6 | const Footer = () => { 7 | return ( 8 | 52 | ); 53 | }; 54 | 55 | export default Footer; 56 | -------------------------------------------------------------------------------- /components/Grid.tsx: -------------------------------------------------------------------------------- 1 | import { gridItems } from "@/data"; 2 | import { BentoGrid, BentoGridItem } from "./ui/BentoGrid"; 3 | 4 | const Grid = () => { 5 | return ( 6 |
7 | 8 | {gridItems.map((item, i) => ( 9 | 22 | ))} 23 | 24 |
25 | ); 26 | }; 27 | 28 | export default Grid; 29 | -------------------------------------------------------------------------------- /components/Hero.tsx: -------------------------------------------------------------------------------- 1 | import { FaLocationArrow } from "react-icons/fa6"; 2 | 3 | import MagicButton from "./MagicButton"; 4 | import { Spotlight } from "./ui/Spotlight"; 5 | import { TextGenerateEffect } from "./ui/TextGenerateEffect"; 6 | 7 | const Hero = () => { 8 | return ( 9 |
10 | {/** 11 | * UI: Spotlights 12 | * Link: https://ui.aceternity.com/components/spotlight 13 | */} 14 |
15 | 19 | 23 | 24 |
25 | 26 | {/** 27 | * UI: grid 28 | * change bg color to bg-black-100 and reduce grid color from 29 | * 0.2 to 0.03 30 | */} 31 |
35 | {/* Radial gradient for the container to give a faded look */} 36 |
41 |
42 | 43 |
44 |
45 |

46 | Dynamic Web Magic with Next.js 47 |

48 | 49 | {/** 50 | * Link: https://ui.aceternity.com/components/text-generate-effect 51 | * 52 | * change md:text-6xl, add more responsive code 53 | */} 54 | 58 | 59 |

60 | Hi! I'm Adrian, a Next.js Developer based in Croatia. 61 |

62 | 63 | 64 | } 67 | position="right" 68 | /> 69 | 70 |
71 |
72 |
73 | ); 74 | }; 75 | 76 | export default Hero; 77 | -------------------------------------------------------------------------------- /components/MagicButton.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | /** 4 | * UI: border magic from tailwind css btns 5 | * Link: https://ui.aceternity.com/components/tailwindcss-buttons 6 | * 7 | * change border radius to rounded-lg 8 | * add margin of md:mt-10 9 | * remove focus:ring-2 focus:ring-slate-400 focus:ring-offset-2 focus:ring-offset-slate-50 10 | */ 11 | const MagicButton = ({ 12 | title, 13 | icon, 14 | position, 15 | handleClick, 16 | otherClasses, 17 | }: { 18 | title: string; 19 | icon: React.ReactNode; 20 | position: string; 21 | handleClick?: () => void; 22 | otherClasses?: string; 23 | }) => { 24 | return ( 25 | 41 | ); 42 | }; 43 | 44 | export default MagicButton; 45 | -------------------------------------------------------------------------------- /components/RecentProjects.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { FaLocationArrow } from "react-icons/fa6"; 4 | 5 | import { projects } from "@/data"; 6 | import { PinContainer } from "./ui/Pin"; 7 | 8 | const RecentProjects = () => { 9 | return ( 10 |
11 |

12 | A small selection of{" "} 13 | recent projects 14 |

15 |
16 | {projects.map((item) => ( 17 |
21 | 25 |
26 |
30 | bgimg 31 |
32 | cover 37 |
38 | 39 |

40 | {item.title} 41 |

42 | 43 |

50 | {item.des} 51 |

52 | 53 |
54 |
55 | {item.iconLists.map((icon, index) => ( 56 |
63 | icon5 64 |
65 | ))} 66 |
67 | 68 |
69 |

70 | Check Live Site 71 |

72 | 73 |
74 |
75 |
76 |
77 | ))} 78 |
79 |
80 | ); 81 | }; 82 | 83 | export default RecentProjects; 84 | -------------------------------------------------------------------------------- /components/ui/BentoGrid.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import { IoCopyOutline } from "react-icons/io5"; 3 | 4 | // Also install this npm i --save-dev @types/react-lottie 5 | import Lottie from "react-lottie"; 6 | 7 | import { cn } from "@/lib/utils"; 8 | 9 | 10 | import { BackgroundGradientAnimation } from "./GradientBg"; 11 | import GridGlobe from "./GridGlobe"; 12 | import animationData from "@/data/confetti.json"; 13 | import MagicButton from "../MagicButton"; 14 | 15 | export const BentoGrid = ({ 16 | className, 17 | children, 18 | }: { 19 | className?: string; 20 | children?: React.ReactNode; 21 | }) => { 22 | return ( 23 |
30 | {children} 31 |
32 | ); 33 | }; 34 | 35 | export const BentoGridItem = ({ 36 | className, 37 | id, 38 | title, 39 | description, 40 | // remove unecessary things here 41 | img, 42 | imgClassName, 43 | titleClassName, 44 | spareImg, 45 | }: { 46 | className?: string; 47 | id: number; 48 | title?: string | React.ReactNode; 49 | description?: string | React.ReactNode; 50 | img?: string; 51 | imgClassName?: string; 52 | titleClassName?: string; 53 | spareImg?: string; 54 | }) => { 55 | const leftLists = ["ReactJS", "Express", "Typescript"]; 56 | const rightLists = ["VueJS", "NuxtJS", "GraphQL"]; 57 | 58 | const [copied, setCopied] = useState(false); 59 | 60 | const defaultOptions = { 61 | loop: copied, 62 | autoplay: copied, 63 | animationData: animationData, 64 | rendererSettings: { 65 | preserveAspectRatio: "xMidYMid slice", 66 | }, 67 | }; 68 | 69 | const handleCopy = () => { 70 | const text = "hsu@jsmastery.pro"; 71 | navigator.clipboard.writeText(text); 72 | setCopied(true); 73 | }; 74 | 75 | return ( 76 |
90 | {/* add img divs */} 91 |
92 |
93 | {img && ( 94 | {img} 99 | )} 100 |
101 |
105 | {spareImg && ( 106 | {spareImg} 112 | )} 113 |
114 | {id === 6 && ( 115 | // add background animation , remove the p tag 116 | 117 |
118 |
119 | )} 120 | 121 |
127 | {/* change the order of the title and des, font-extralight, remove text-xs text-neutral-600 dark:text-neutral-300 , change the text-color */} 128 |
129 | {description} 130 |
131 | {/* add text-3xl max-w-96 , remove text-neutral-600 dark:text-neutral-300*/} 132 | {/* remove mb-2 mt-2 */} 133 |
136 | {title} 137 |
138 | 139 | {/* for the github 3d globe */} 140 | {id === 2 && } 141 | 142 | {/* Tech stack list div */} 143 | {id === 3 && ( 144 |
145 | {/* tech stack lists */} 146 |
147 | {leftLists.map((item, i) => ( 148 | 153 | {item} 154 | 155 | ))} 156 | 157 |
158 |
159 | 160 | {rightLists.map((item, i) => ( 161 | 166 | {item} 167 | 168 | ))} 169 |
170 |
171 | )} 172 | {id === 6 && ( 173 |
174 | {/* button border magic from tailwind css buttons */} 175 | {/* add rounded-md h-8 md:h-8, remove rounded-full */} 176 | {/* remove focus:ring-2 focus:ring-slate-400 focus:ring-offset-2 focus:ring-offset-slate-50 */} 177 | {/* add handleCopy() for the copy the text */} 178 |
182 | {/* confetti */} 183 | 184 |
185 | 186 | } 189 | position="left" 190 | handleClick={handleCopy} 191 | otherClasses="!bg-[#161A31]" 192 | /> 193 |
194 | )} 195 |
196 |
197 |
198 | ); 199 | }; 200 | -------------------------------------------------------------------------------- /components/ui/CanvasRevealEffect.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { cn } from "@/lib/utils"; 3 | import { Canvas, useFrame, useThree } from "@react-three/fiber"; 4 | import React, { useMemo, useRef } from "react"; 5 | import * as THREE from "three"; 6 | 7 | export const CanvasRevealEffect = ({ 8 | animationSpeed = 0.4, 9 | opacities = [0.3, 0.3, 0.3, 0.5, 0.5, 0.5, 0.8, 0.8, 0.8, 1], 10 | colors = [[0, 255, 255]], 11 | containerClassName, 12 | dotSize, 13 | showGradient = true, 14 | }: { 15 | /** 16 | * 0.1 - slower 17 | * 1.0 - faster 18 | */ 19 | animationSpeed?: number; 20 | opacities?: number[]; 21 | colors?: number[][]; 22 | containerClassName?: string; 23 | dotSize?: number; 24 | showGradient?: boolean; 25 | }) => { 26 | return ( 27 |
28 |
29 | 43 |
44 | {showGradient && ( 45 |
46 | )} 47 |
48 | ); 49 | }; 50 | 51 | interface DotMatrixProps { 52 | colors?: number[][]; 53 | opacities?: number[]; 54 | totalSize?: number; 55 | dotSize?: number; 56 | shader?: string; 57 | center?: ("x" | "y")[]; 58 | } 59 | 60 | const DotMatrix: React.FC = ({ 61 | colors = [[0, 0, 0]], 62 | opacities = [0.04, 0.04, 0.04, 0.04, 0.04, 0.08, 0.08, 0.08, 0.08, 0.14], 63 | totalSize = 4, 64 | dotSize = 2, 65 | shader = "", 66 | center = ["x", "y"], 67 | }) => { 68 | const uniforms = React.useMemo(() => { 69 | let colorsArray = [ 70 | colors[0], 71 | colors[0], 72 | colors[0], 73 | colors[0], 74 | colors[0], 75 | colors[0], 76 | ]; 77 | if (colors.length === 2) { 78 | colorsArray = [ 79 | colors[0], 80 | colors[0], 81 | colors[0], 82 | colors[1], 83 | colors[1], 84 | colors[1], 85 | ]; 86 | } else if (colors.length === 3) { 87 | colorsArray = [ 88 | colors[0], 89 | colors[0], 90 | colors[1], 91 | colors[1], 92 | colors[2], 93 | colors[2], 94 | ]; 95 | } 96 | 97 | return { 98 | u_colors: { 99 | value: colorsArray.map((color) => [ 100 | color[0] / 255, 101 | color[1] / 255, 102 | color[2] / 255, 103 | ]), 104 | type: "uniform3fv", 105 | }, 106 | u_opacities: { 107 | value: opacities, 108 | type: "uniform1fv", 109 | }, 110 | u_total_size: { 111 | value: totalSize, 112 | type: "uniform1f", 113 | }, 114 | u_dot_size: { 115 | value: dotSize, 116 | type: "uniform1f", 117 | }, 118 | }; 119 | }, [colors, opacities, totalSize, dotSize]); 120 | 121 | return ( 122 | 175 | ); 176 | }; 177 | 178 | type Uniforms = { 179 | [key: string]: { 180 | value: number[] | number[][] | number; 181 | type: string; 182 | }; 183 | }; 184 | const ShaderMaterial = ({ 185 | source, 186 | uniforms, 187 | maxFps = 60, 188 | }: { 189 | source: string; 190 | hovered?: boolean; 191 | maxFps?: number; 192 | uniforms: Uniforms; 193 | }) => { 194 | const { size } = useThree(); 195 | const ref = useRef(); 196 | let lastFrameTime = 0; 197 | 198 | useFrame(({ clock }) => { 199 | if (!ref.current) return; 200 | const timestamp = clock.getElapsedTime(); 201 | if (timestamp - lastFrameTime < 1 / maxFps) { 202 | return; 203 | } 204 | lastFrameTime = timestamp; 205 | 206 | const material: any = ref.current.material; 207 | const timeLocation = material.uniforms.u_time; 208 | timeLocation.value = timestamp; 209 | }); 210 | 211 | const getUniforms = () => { 212 | const preparedUniforms: any = {}; 213 | 214 | for (const uniformName in uniforms) { 215 | const uniform: any = uniforms[uniformName]; 216 | 217 | switch (uniform.type) { 218 | case "uniform1f": 219 | preparedUniforms[uniformName] = { value: uniform.value, type: "1f" }; 220 | break; 221 | case "uniform3f": 222 | preparedUniforms[uniformName] = { 223 | value: new THREE.Vector3().fromArray(uniform.value), 224 | type: "3f", 225 | }; 226 | break; 227 | case "uniform1fv": 228 | preparedUniforms[uniformName] = { value: uniform.value, type: "1fv" }; 229 | break; 230 | case "uniform3fv": 231 | preparedUniforms[uniformName] = { 232 | value: uniform.value.map((v: number[]) => 233 | new THREE.Vector3().fromArray(v) 234 | ), 235 | type: "3fv", 236 | }; 237 | break; 238 | case "uniform2f": 239 | preparedUniforms[uniformName] = { 240 | value: new THREE.Vector2().fromArray(uniform.value), 241 | type: "2f", 242 | }; 243 | break; 244 | default: 245 | console.error(`Invalid uniform type for '${uniformName}'.`); 246 | break; 247 | } 248 | } 249 | 250 | preparedUniforms["u_time"] = { value: 0, type: "1f" }; 251 | preparedUniforms["u_resolution"] = { 252 | value: new THREE.Vector2(size.width * 2, size.height * 2), 253 | }; // Initialize u_resolution 254 | return preparedUniforms; 255 | }; 256 | 257 | // Shader material 258 | const material = useMemo(() => { 259 | const materialObject = new THREE.ShaderMaterial({ 260 | vertexShader: ` 261 | precision mediump float; 262 | in vec2 coordinates; 263 | uniform vec2 u_resolution; 264 | out vec2 fragCoord; 265 | void main(){ 266 | float x = position.x; 267 | float y = position.y; 268 | gl_Position = vec4(x, y, 0.0, 1.0); 269 | fragCoord = (position.xy + vec2(1.0)) * 0.5 * u_resolution; 270 | fragCoord.y = u_resolution.y - fragCoord.y; 271 | } 272 | `, 273 | fragmentShader: source, 274 | uniforms: getUniforms(), 275 | glslVersion: THREE.GLSL3, 276 | blending: THREE.CustomBlending, 277 | blendSrc: THREE.SrcAlphaFactor, 278 | blendDst: THREE.OneFactor, 279 | }); 280 | 281 | return materialObject; 282 | }, [size.width, size.height, source]); 283 | 284 | return ( 285 | 286 | 287 | 288 | 289 | ); 290 | }; 291 | 292 | const Shader: React.FC = ({ source, uniforms, maxFps = 60 }) => { 293 | return ( 294 | 295 | 296 | 297 | ); 298 | }; 299 | interface ShaderProps { 300 | source: string; 301 | uniforms: { 302 | [key: string]: { 303 | value: number[] | number[][] | number; 304 | type: string; 305 | }; 306 | }; 307 | maxFps?: number; 308 | } 309 | -------------------------------------------------------------------------------- /components/ui/FloatingNavbar.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import React, { useState } from "react"; 3 | import { 4 | motion, 5 | AnimatePresence, 6 | useScroll, 7 | useMotionValueEvent, 8 | } from "framer-motion"; 9 | import Link from "next/link"; 10 | import { cn } from "@/lib/utils"; 11 | 12 | export const FloatingNav = ({ 13 | navItems, 14 | className, 15 | }: { 16 | navItems: { 17 | name: string; 18 | link: string; 19 | icon?: JSX.Element; 20 | }[]; 21 | className?: string; 22 | }) => { 23 | const { scrollYProgress } = useScroll(); 24 | 25 | // set true for the initial state so that nav bar is visible in the hero section 26 | const [visible, setVisible] = useState(true); 27 | 28 | useMotionValueEvent(scrollYProgress, "change", (current) => { 29 | // Check if current is not undefined and is a number 30 | if (typeof current === "number") { 31 | let direction = current! - scrollYProgress.getPrevious()!; 32 | 33 | if (scrollYProgress.get() < 0.05) { 34 | // also set true for the initial state 35 | setVisible(true); 36 | } else { 37 | if (direction < 0) { 38 | setVisible(true); 39 | } else { 40 | setVisible(false); 41 | } 42 | } 43 | } 44 | }); 45 | 46 | return ( 47 | 48 | 74 | {navItems.map((navItem: any, idx: number) => ( 75 | 82 | {navItem.icon} 83 | {/* add !cursor-pointer */} 84 | {/* remove hidden sm:block for the mobile responsive */} 85 | {navItem.name} 86 | 87 | ))} 88 | {/* remove this login btn */} 89 | {/* */} 93 | 94 | 95 | ); 96 | }; 97 | -------------------------------------------------------------------------------- /components/ui/Globe.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { useEffect, useRef, useState } from "react"; 3 | import { Color, Scene, Fog, PerspectiveCamera, Vector3 } from "three"; 4 | import ThreeGlobe from "three-globe"; 5 | import { useThree, Object3DNode, Canvas, extend } from "@react-three/fiber"; 6 | import { OrbitControls } from "@react-three/drei"; 7 | import countries from "@/data/globe.json"; 8 | declare module "@react-three/fiber" { 9 | interface ThreeElements { 10 | threeGlobe: Object3DNode; 11 | } 12 | } 13 | 14 | extend({ ThreeGlobe }); 15 | 16 | const RING_PROPAGATION_SPEED = 3; 17 | const aspect = 1.2; 18 | const cameraZ = 300; 19 | 20 | type Position = { 21 | order: number; 22 | startLat: number; 23 | startLng: number; 24 | endLat: number; 25 | endLng: number; 26 | arcAlt: number; 27 | color: string; 28 | }; 29 | 30 | export type GlobeConfig = { 31 | pointSize?: number; 32 | globeColor?: string; 33 | showAtmosphere?: boolean; 34 | atmosphereColor?: string; 35 | atmosphereAltitude?: number; 36 | emissive?: string; 37 | emissiveIntensity?: number; 38 | shininess?: number; 39 | polygonColor?: string; 40 | ambientLight?: string; 41 | directionalLeftLight?: string; 42 | directionalTopLight?: string; 43 | pointLight?: string; 44 | arcTime?: number; 45 | arcLength?: number; 46 | rings?: number; 47 | maxRings?: number; 48 | initialPosition?: { 49 | lat: number; 50 | lng: number; 51 | }; 52 | autoRotate?: boolean; 53 | autoRotateSpeed?: number; 54 | }; 55 | 56 | interface WorldProps { 57 | globeConfig: GlobeConfig; 58 | data: Position[]; 59 | } 60 | 61 | let numbersOfRings = [0]; 62 | 63 | export function Globe({ globeConfig, data }: WorldProps) { 64 | const [globeData, setGlobeData] = useState< 65 | | { 66 | size: number; 67 | order: number; 68 | color: (t: number) => string; 69 | lat: number; 70 | lng: number; 71 | }[] 72 | | null 73 | >(null); 74 | 75 | const globeRef = useRef(null); 76 | 77 | const defaultProps = { 78 | pointSize: 1, 79 | atmosphereColor: "#ffffff", 80 | showAtmosphere: true, 81 | atmosphereAltitude: 0.1, 82 | polygonColor: "rgba(255,255,255,0.7)", 83 | globeColor: "#1d072e", 84 | emissive: "#000000", 85 | emissiveIntensity: 0.1, 86 | shininess: 0.9, 87 | arcTime: 2000, 88 | arcLength: 0.9, 89 | rings: 1, 90 | maxRings: 3, 91 | ...globeConfig, 92 | }; 93 | 94 | useEffect(() => { 95 | if (globeRef.current) { 96 | _buildData(); 97 | _buildMaterial(); 98 | } 99 | }, [globeRef.current]); 100 | 101 | const _buildMaterial = () => { 102 | if (!globeRef.current) return; 103 | 104 | const globeMaterial = globeRef.current.globeMaterial() as unknown as { 105 | color: Color; 106 | emissive: Color; 107 | emissiveIntensity: number; 108 | shininess: number; 109 | }; 110 | globeMaterial.color = new Color(globeConfig.globeColor); 111 | globeMaterial.emissive = new Color(globeConfig.emissive); 112 | globeMaterial.emissiveIntensity = globeConfig.emissiveIntensity || 0.1; 113 | globeMaterial.shininess = globeConfig.shininess || 0.9; 114 | }; 115 | 116 | const _buildData = () => { 117 | const arcs = data; 118 | let points = []; 119 | for (let i = 0; i < arcs.length; i++) { 120 | const arc = arcs[i]; 121 | const rgb = hexToRgb(arc.color) as { r: number; g: number; b: number }; 122 | points.push({ 123 | size: defaultProps.pointSize, 124 | order: arc.order, 125 | color: (t: number) => `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${1 - t})`, 126 | lat: arc.startLat, 127 | lng: arc.startLng, 128 | }); 129 | points.push({ 130 | size: defaultProps.pointSize, 131 | order: arc.order, 132 | color: (t: number) => `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${1 - t})`, 133 | lat: arc.endLat, 134 | lng: arc.endLng, 135 | }); 136 | } 137 | 138 | // remove duplicates for same lat and lng 139 | const filteredPoints = points.filter( 140 | (v, i, a) => 141 | a.findIndex((v2) => 142 | ["lat", "lng"].every( 143 | (k) => v2[k as "lat" | "lng"] === v[k as "lat" | "lng"] 144 | ) 145 | ) === i 146 | ); 147 | 148 | setGlobeData(filteredPoints); 149 | }; 150 | 151 | useEffect(() => { 152 | if (globeRef.current && globeData) { 153 | globeRef.current 154 | .hexPolygonsData(countries.features) 155 | .hexPolygonResolution(3) 156 | .hexPolygonMargin(0.7) 157 | .showAtmosphere(defaultProps.showAtmosphere) 158 | .atmosphereColor(defaultProps.atmosphereColor) 159 | .atmosphereAltitude(defaultProps.atmosphereAltitude) 160 | .hexPolygonColor((e) => { 161 | return defaultProps.polygonColor; 162 | }); 163 | startAnimation(); 164 | } 165 | }, [globeData]); 166 | 167 | const startAnimation = () => { 168 | if (!globeRef.current || !globeData) return; 169 | 170 | globeRef.current 171 | .arcsData(data) 172 | .arcStartLat((d) => (d as { startLat: number }).startLat * 1) 173 | .arcStartLng((d) => (d as { startLng: number }).startLng * 1) 174 | .arcEndLat((d) => (d as { endLat: number }).endLat * 1) 175 | .arcEndLng((d) => (d as { endLng: number }).endLng * 1) 176 | .arcColor((e: any) => (e as { color: string }).color) 177 | .arcAltitude((e) => { 178 | return (e as { arcAlt: number }).arcAlt * 1; 179 | }) 180 | .arcStroke((e) => { 181 | return [0.32, 0.28, 0.3][Math.round(Math.random() * 2)]; 182 | }) 183 | .arcDashLength(defaultProps.arcLength) 184 | .arcDashInitialGap((e) => (e as { order: number }).order * 1) 185 | .arcDashGap(15) 186 | .arcDashAnimateTime((e) => defaultProps.arcTime); 187 | 188 | globeRef.current 189 | .pointsData(data) 190 | .pointColor((e) => (e as { color: string }).color) 191 | .pointsMerge(true) 192 | .pointAltitude(0.0) 193 | .pointRadius(2); 194 | 195 | globeRef.current 196 | .ringsData([]) 197 | .ringColor((e: any) => (t: any) => e.color(t)) 198 | .ringMaxRadius(defaultProps.maxRings) 199 | .ringPropagationSpeed(RING_PROPAGATION_SPEED) 200 | .ringRepeatPeriod( 201 | (defaultProps.arcTime * defaultProps.arcLength) / defaultProps.rings 202 | ); 203 | }; 204 | 205 | useEffect(() => { 206 | if (!globeRef.current || !globeData) return; 207 | 208 | const interval = setInterval(() => { 209 | if (!globeRef.current || !globeData) return; 210 | numbersOfRings = genRandomNumbers( 211 | 0, 212 | data.length, 213 | Math.floor((data.length * 4) / 5) 214 | ); 215 | 216 | globeRef.current.ringsData( 217 | globeData.filter((d, i) => numbersOfRings.includes(i)) 218 | ); 219 | }, 2000); 220 | 221 | return () => { 222 | clearInterval(interval); 223 | }; 224 | }, [globeRef.current, globeData]); 225 | 226 | return ( 227 | <> 228 | 229 | 230 | ); 231 | } 232 | 233 | export function WebGLRendererConfig() { 234 | const { gl, size } = useThree(); 235 | 236 | useEffect(() => { 237 | gl.setPixelRatio(window.devicePixelRatio); 238 | gl.setSize(size.width, size.height); 239 | gl.setClearColor(0xffaaff, 0); 240 | }, []); 241 | 242 | return null; 243 | } 244 | 245 | export function World(props: WorldProps) { 246 | const { globeConfig } = props; 247 | const scene = new Scene(); 248 | scene.fog = new Fog(0xffffff, 400, 2000); 249 | return ( 250 | 251 | 252 | 253 | 257 | 261 | 266 | 267 | 277 | 278 | ); 279 | } 280 | 281 | export function hexToRgb(hex: string) { 282 | var shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i; 283 | hex = hex.replace(shorthandRegex, function (m, r, g, b) { 284 | return r + r + g + g + b + b; 285 | }); 286 | 287 | var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); 288 | return result 289 | ? { 290 | r: parseInt(result[1], 16), 291 | g: parseInt(result[2], 16), 292 | b: parseInt(result[3], 16), 293 | } 294 | : null; 295 | } 296 | 297 | export function genRandomNumbers(min: number, max: number, count: number) { 298 | const arr = []; 299 | while (arr.length < count) { 300 | const r = Math.floor(Math.random() * (max - min)) + min; 301 | if (arr.indexOf(r) === -1) arr.push(r); 302 | } 303 | 304 | return arr; 305 | } 306 | -------------------------------------------------------------------------------- /components/ui/GradientBg.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { cn } from "@/lib/utils"; 3 | import { useEffect, useRef, useState } from "react"; 4 | 5 | export const BackgroundGradientAnimation = ({ 6 | gradientBackgroundStart = "rgb(108, 0, 162)", 7 | gradientBackgroundEnd = "rgb(0, 17, 82)", 8 | firstColor = "18, 113, 255", 9 | secondColor = "221, 74, 255", 10 | thirdColor = "100, 220, 255", 11 | fourthColor = "200, 50, 50", 12 | fifthColor = "180, 180, 50", 13 | pointerColor = "140, 100, 255", 14 | size = "80%", 15 | blendingValue = "hard-light", 16 | children, 17 | className, 18 | interactive = true, 19 | containerClassName, 20 | }: { 21 | gradientBackgroundStart?: string; 22 | gradientBackgroundEnd?: string; 23 | firstColor?: string; 24 | secondColor?: string; 25 | thirdColor?: string; 26 | fourthColor?: string; 27 | fifthColor?: string; 28 | pointerColor?: string; 29 | size?: string; 30 | blendingValue?: string; 31 | children?: React.ReactNode; 32 | className?: string; 33 | interactive?: boolean; 34 | containerClassName?: string; 35 | }) => { 36 | const interactiveRef = useRef(null); 37 | 38 | const [curX, setCurX] = useState(0); 39 | const [curY, setCurY] = useState(0); 40 | const [tgX, setTgX] = useState(0); 41 | const [tgY, setTgY] = useState(0); 42 | useEffect(() => { 43 | document.body.style.setProperty( 44 | "--gradient-background-start", 45 | gradientBackgroundStart 46 | ); 47 | document.body.style.setProperty( 48 | "--gradient-background-end", 49 | gradientBackgroundEnd 50 | ); 51 | document.body.style.setProperty("--first-color", firstColor); 52 | document.body.style.setProperty("--second-color", secondColor); 53 | document.body.style.setProperty("--third-color", thirdColor); 54 | document.body.style.setProperty("--fourth-color", fourthColor); 55 | document.body.style.setProperty("--fifth-color", fifthColor); 56 | document.body.style.setProperty("--pointer-color", pointerColor); 57 | document.body.style.setProperty("--size", size); 58 | document.body.style.setProperty("--blending-value", blendingValue); 59 | }, []); 60 | 61 | useEffect(() => { 62 | function move() { 63 | if (!interactiveRef.current) { 64 | return; 65 | } 66 | setCurX(curX + (tgX - curX) / 20); 67 | setCurY(curY + (tgY - curY) / 20); 68 | interactiveRef.current.style.transform = `translate(${Math.round( 69 | curX 70 | )}px, ${Math.round(curY)}px)`; 71 | } 72 | 73 | move(); 74 | }, [tgX, tgY]); 75 | 76 | const handleMouseMove = (event: React.MouseEvent) => { 77 | if (interactiveRef.current) { 78 | const rect = interactiveRef.current.getBoundingClientRect(); 79 | setTgX(event.clientX - rect.left); 80 | setTgY(event.clientY - rect.top); 81 | } 82 | }; 83 | 84 | const [isSafari, setIsSafari] = useState(false); 85 | useEffect(() => { 86 | setIsSafari(/^((?!chrome|android).)*safari/i.test(navigator.userAgent)); 87 | }, []); 88 | 89 | return ( 90 |
96 | 97 | 98 | 99 | 104 | 110 | 111 | 112 | 113 | 114 |
{children}
115 |
121 |
130 |
139 |
148 |
157 |
166 | 167 | {interactive && ( 168 |
177 | )} 178 |
179 |
180 | ); 181 | }; 182 | -------------------------------------------------------------------------------- /components/ui/GridGlobe.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import React from "react"; 3 | import { motion } from "framer-motion"; 4 | import dynamic from "next/dynamic"; 5 | 6 | const World = dynamic(() => import("./Globe").then((m) => m.World), { 7 | ssr: false, 8 | }); 9 | 10 | const GridGlobe = () => { 11 | const globeConfig = { 12 | pointSize: 4, 13 | globeColor: "#062056", 14 | showAtmosphere: true, 15 | atmosphereColor: "#FFFFFF", 16 | atmosphereAltitude: 0.1, 17 | emissive: "#062056", 18 | emissiveIntensity: 0.1, 19 | shininess: 0.9, 20 | polygonColor: "rgba(255,255,255,0.7)", 21 | ambientLight: "#38bdf8", 22 | directionalLeftLight: "#ffffff", 23 | directionalTopLight: "#ffffff", 24 | pointLight: "#ffffff", 25 | arcTime: 1000, 26 | arcLength: 0.9, 27 | rings: 1, 28 | maxRings: 3, 29 | initialPosition: { lat: 22.3193, lng: 114.1694 }, 30 | autoRotate: true, 31 | autoRotateSpeed: 0.5, 32 | }; 33 | const colors = ["#06b6d4", "#3b82f6", "#6366f1"]; 34 | const sampleArcs = [ 35 | { 36 | order: 1, 37 | startLat: -19.885592, 38 | startLng: -43.951191, 39 | endLat: -22.9068, 40 | endLng: -43.1729, 41 | arcAlt: 0.1, 42 | color: colors[Math.floor(Math.random() * (colors.length - 1))], 43 | }, 44 | { 45 | order: 1, 46 | startLat: 28.6139, 47 | startLng: 77.209, 48 | endLat: 3.139, 49 | endLng: 101.6869, 50 | arcAlt: 0.2, 51 | color: colors[Math.floor(Math.random() * (colors.length - 1))], 52 | }, 53 | { 54 | order: 1, 55 | startLat: -19.885592, 56 | startLng: -43.951191, 57 | endLat: -1.303396, 58 | endLng: 36.852443, 59 | arcAlt: 0.5, 60 | color: colors[Math.floor(Math.random() * (colors.length - 1))], 61 | }, 62 | { 63 | order: 2, 64 | startLat: 1.3521, 65 | startLng: 103.8198, 66 | endLat: 35.6762, 67 | endLng: 139.6503, 68 | arcAlt: 0.2, 69 | color: colors[Math.floor(Math.random() * (colors.length - 1))], 70 | }, 71 | { 72 | order: 2, 73 | startLat: 51.5072, 74 | startLng: -0.1276, 75 | endLat: 3.139, 76 | endLng: 101.6869, 77 | arcAlt: 0.3, 78 | color: colors[Math.floor(Math.random() * (colors.length - 1))], 79 | }, 80 | { 81 | order: 2, 82 | startLat: -15.785493, 83 | startLng: -47.909029, 84 | endLat: 36.162809, 85 | endLng: -115.119411, 86 | arcAlt: 0.3, 87 | color: colors[Math.floor(Math.random() * (colors.length - 1))], 88 | }, 89 | { 90 | order: 3, 91 | startLat: -33.8688, 92 | startLng: 151.2093, 93 | endLat: 22.3193, 94 | endLng: 114.1694, 95 | arcAlt: 0.3, 96 | color: colors[Math.floor(Math.random() * (colors.length - 1))], 97 | }, 98 | { 99 | order: 3, 100 | startLat: 21.3099, 101 | startLng: -157.8581, 102 | endLat: 40.7128, 103 | endLng: -74.006, 104 | arcAlt: 0.3, 105 | color: colors[Math.floor(Math.random() * (colors.length - 1))], 106 | }, 107 | { 108 | order: 3, 109 | startLat: -6.2088, 110 | startLng: 106.8456, 111 | endLat: 51.5072, 112 | endLng: -0.1276, 113 | arcAlt: 0.3, 114 | color: colors[Math.floor(Math.random() * (colors.length - 1))], 115 | }, 116 | { 117 | order: 4, 118 | startLat: 11.986597, 119 | startLng: 8.571831, 120 | endLat: -15.595412, 121 | endLng: -56.05918, 122 | arcAlt: 0.5, 123 | color: colors[Math.floor(Math.random() * (colors.length - 1))], 124 | }, 125 | { 126 | order: 4, 127 | startLat: -34.6037, 128 | startLng: -58.3816, 129 | endLat: 22.3193, 130 | endLng: 114.1694, 131 | arcAlt: 0.7, 132 | color: colors[Math.floor(Math.random() * (colors.length - 1))], 133 | }, 134 | { 135 | order: 4, 136 | startLat: 51.5072, 137 | startLng: -0.1276, 138 | endLat: 48.8566, 139 | endLng: -2.3522, 140 | arcAlt: 0.1, 141 | color: colors[Math.floor(Math.random() * (colors.length - 1))], 142 | }, 143 | { 144 | order: 5, 145 | startLat: 14.5995, 146 | startLng: 120.9842, 147 | endLat: 51.5072, 148 | endLng: -0.1276, 149 | arcAlt: 0.3, 150 | color: colors[Math.floor(Math.random() * (colors.length - 1))], 151 | }, 152 | { 153 | order: 5, 154 | startLat: 1.3521, 155 | startLng: 103.8198, 156 | endLat: -33.8688, 157 | endLng: 151.2093, 158 | arcAlt: 0.2, 159 | color: colors[Math.floor(Math.random() * (colors.length - 1))], 160 | }, 161 | { 162 | order: 5, 163 | startLat: 34.0522, 164 | startLng: -118.2437, 165 | endLat: 48.8566, 166 | endLng: -2.3522, 167 | arcAlt: 0.2, 168 | color: colors[Math.floor(Math.random() * (colors.length - 1))], 169 | }, 170 | { 171 | order: 6, 172 | startLat: -15.432563, 173 | startLng: 28.315853, 174 | endLat: 1.094136, 175 | endLng: -63.34546, 176 | arcAlt: 0.7, 177 | color: colors[Math.floor(Math.random() * (colors.length - 1))], 178 | }, 179 | { 180 | order: 6, 181 | startLat: 37.5665, 182 | startLng: 126.978, 183 | endLat: 35.6762, 184 | endLng: 139.6503, 185 | arcAlt: 0.1, 186 | color: colors[Math.floor(Math.random() * (colors.length - 1))], 187 | }, 188 | { 189 | order: 6, 190 | startLat: 22.3193, 191 | startLng: 114.1694, 192 | endLat: 51.5072, 193 | endLng: -0.1276, 194 | arcAlt: 0.3, 195 | color: colors[Math.floor(Math.random() * (colors.length - 1))], 196 | }, 197 | { 198 | order: 7, 199 | startLat: -19.885592, 200 | startLng: -43.951191, 201 | endLat: -15.595412, 202 | endLng: -56.05918, 203 | arcAlt: 0.1, 204 | color: colors[Math.floor(Math.random() * (colors.length - 1))], 205 | }, 206 | { 207 | order: 7, 208 | startLat: 48.8566, 209 | startLng: -2.3522, 210 | endLat: 52.52, 211 | endLng: 13.405, 212 | arcAlt: 0.1, 213 | color: colors[Math.floor(Math.random() * (colors.length - 1))], 214 | }, 215 | { 216 | order: 7, 217 | startLat: 52.52, 218 | startLng: 13.405, 219 | endLat: 34.0522, 220 | endLng: -118.2437, 221 | arcAlt: 0.2, 222 | color: colors[Math.floor(Math.random() * (colors.length - 1))], 223 | }, 224 | { 225 | order: 8, 226 | startLat: -8.833221, 227 | startLng: 13.264837, 228 | endLat: -33.936138, 229 | endLng: 18.436529, 230 | arcAlt: 0.2, 231 | color: colors[Math.floor(Math.random() * (colors.length - 1))], 232 | }, 233 | { 234 | order: 8, 235 | startLat: 49.2827, 236 | startLng: -123.1207, 237 | endLat: 52.3676, 238 | endLng: 4.9041, 239 | arcAlt: 0.2, 240 | color: colors[Math.floor(Math.random() * (colors.length - 1))], 241 | }, 242 | { 243 | order: 8, 244 | startLat: 1.3521, 245 | startLng: 103.8198, 246 | endLat: 40.7128, 247 | endLng: -74.006, 248 | arcAlt: 0.5, 249 | color: colors[Math.floor(Math.random() * (colors.length - 1))], 250 | }, 251 | { 252 | order: 9, 253 | startLat: 51.5072, 254 | startLng: -0.1276, 255 | endLat: 34.0522, 256 | endLng: -118.2437, 257 | arcAlt: 0.2, 258 | color: colors[Math.floor(Math.random() * (colors.length - 1))], 259 | }, 260 | { 261 | order: 9, 262 | startLat: 22.3193, 263 | startLng: 114.1694, 264 | endLat: -22.9068, 265 | endLng: -43.1729, 266 | arcAlt: 0.7, 267 | color: colors[Math.floor(Math.random() * (colors.length - 1))], 268 | }, 269 | { 270 | order: 9, 271 | startLat: 1.3521, 272 | startLng: 103.8198, 273 | endLat: -34.6037, 274 | endLng: -58.3816, 275 | arcAlt: 0.5, 276 | color: colors[Math.floor(Math.random() * (colors.length - 1))], 277 | }, 278 | { 279 | order: 10, 280 | startLat: -22.9068, 281 | startLng: -43.1729, 282 | endLat: 28.6139, 283 | endLng: 77.209, 284 | arcAlt: 0.7, 285 | color: colors[Math.floor(Math.random() * (colors.length - 1))], 286 | }, 287 | { 288 | order: 10, 289 | startLat: 34.0522, 290 | startLng: -118.2437, 291 | endLat: 31.2304, 292 | endLng: 121.4737, 293 | arcAlt: 0.3, 294 | color: colors[Math.floor(Math.random() * (colors.length - 1))], 295 | }, 296 | { 297 | order: 10, 298 | startLat: -6.2088, 299 | startLng: 106.8456, 300 | endLat: 52.3676, 301 | endLng: 4.9041, 302 | arcAlt: 0.3, 303 | color: colors[Math.floor(Math.random() * (colors.length - 1))], 304 | }, 305 | { 306 | order: 11, 307 | startLat: 41.9028, 308 | startLng: 12.4964, 309 | endLat: 34.0522, 310 | endLng: -118.2437, 311 | arcAlt: 0.2, 312 | color: colors[Math.floor(Math.random() * (colors.length - 1))], 313 | }, 314 | { 315 | order: 11, 316 | startLat: -6.2088, 317 | startLng: 106.8456, 318 | endLat: 31.2304, 319 | endLng: 121.4737, 320 | arcAlt: 0.2, 321 | color: colors[Math.floor(Math.random() * (colors.length - 1))], 322 | }, 323 | { 324 | order: 11, 325 | startLat: 22.3193, 326 | startLng: 114.1694, 327 | endLat: 1.3521, 328 | endLng: 103.8198, 329 | arcAlt: 0.2, 330 | color: colors[Math.floor(Math.random() * (colors.length - 1))], 331 | }, 332 | { 333 | order: 12, 334 | startLat: 34.0522, 335 | startLng: -118.2437, 336 | endLat: 37.7749, 337 | endLng: -122.4194, 338 | arcAlt: 0.1, 339 | color: colors[Math.floor(Math.random() * (colors.length - 1))], 340 | }, 341 | { 342 | order: 12, 343 | startLat: 35.6762, 344 | startLng: 139.6503, 345 | endLat: 22.3193, 346 | endLng: 114.1694, 347 | arcAlt: 0.2, 348 | color: colors[Math.floor(Math.random() * (colors.length - 1))], 349 | }, 350 | { 351 | order: 12, 352 | startLat: 22.3193, 353 | startLng: 114.1694, 354 | endLat: 34.0522, 355 | endLng: -118.2437, 356 | arcAlt: 0.3, 357 | color: colors[Math.floor(Math.random() * (colors.length - 1))], 358 | }, 359 | { 360 | order: 13, 361 | startLat: 52.52, 362 | startLng: 13.405, 363 | endLat: 22.3193, 364 | endLng: 114.1694, 365 | arcAlt: 0.3, 366 | color: colors[Math.floor(Math.random() * (colors.length - 1))], 367 | }, 368 | { 369 | order: 13, 370 | startLat: 11.986597, 371 | startLng: 8.571831, 372 | endLat: 35.6762, 373 | endLng: 139.6503, 374 | arcAlt: 0.3, 375 | color: colors[Math.floor(Math.random() * (colors.length - 1))], 376 | }, 377 | { 378 | order: 13, 379 | startLat: -22.9068, 380 | startLng: -43.1729, 381 | endLat: -34.6037, 382 | endLng: -58.3816, 383 | arcAlt: 0.1, 384 | color: colors[Math.floor(Math.random() * (colors.length - 1))], 385 | }, 386 | { 387 | order: 14, 388 | startLat: -33.936138, 389 | startLng: 18.436529, 390 | endLat: 21.395643, 391 | endLng: 39.883798, 392 | arcAlt: 0.3, 393 | color: colors[Math.floor(Math.random() * (colors.length - 1))], 394 | }, 395 | ]; 396 | 397 | return ( 398 | // remove dark:bg-black bg-white h-screen md:h-auto w-full flex-row py-20 399 | // change absolute -left-5 top-36, add w-full h-full md:top-40 400 |
401 | {/* remove h-full md:h-[40rem] */} 402 |
403 | {/* remove these text divs */} 404 | {/* 418 |

419 | We sell soap worldwide 420 |

421 |

422 | This globe is interactive and customizable. Have fun with it, and 423 | don't forget to share it. 424 |

425 |
*/} 426 |
427 | {/* remove -bottom-20 */} 428 |
429 | 430 |
431 |
432 |
433 | ); 434 | }; 435 | export default GridGlobe; 436 | -------------------------------------------------------------------------------- /components/ui/HoverBorder.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import React, { useState, useEffect, useRef } from "react"; 3 | 4 | import { motion } from "framer-motion"; 5 | import { cn } from "@/lib/utils"; 6 | 7 | type Direction = "TOP" | "LEFT" | "BOTTOM" | "RIGHT"; 8 | 9 | export function HoverBorderGradient({ 10 | children, 11 | containerClassName, 12 | className, 13 | as: Tag = "button", 14 | duration = 1, 15 | clockwise = true, 16 | ...props 17 | }: React.PropsWithChildren< 18 | { 19 | as?: React.ElementType; 20 | containerClassName?: string; 21 | className?: string; 22 | duration?: number; 23 | clockwise?: boolean; 24 | } & React.HTMLAttributes 25 | >) { 26 | const [hovered, setHovered] = useState(false); 27 | const [direction, setDirection] = useState("TOP"); 28 | 29 | const rotateDirection = (currentDirection: Direction): Direction => { 30 | const directions: Direction[] = ["TOP", "LEFT", "BOTTOM", "RIGHT"]; 31 | const currentIndex = directions.indexOf(currentDirection); 32 | const nextIndex = clockwise 33 | ? (currentIndex - 1 + directions.length) % directions.length 34 | : (currentIndex + 1) % directions.length; 35 | return directions[nextIndex]; 36 | }; 37 | 38 | const movingMap: Record = { 39 | TOP: "radial-gradient(20.7% 50% at 50% 0%, hsl(0, 0%, 100%) 0%, rgba(255, 255, 255, 0) 100%)", 40 | LEFT: "radial-gradient(16.6% 43.1% at 0% 50%, hsl(0, 0%, 100%) 0%, rgba(255, 255, 255, 0) 100%)", 41 | BOTTOM: 42 | "radial-gradient(20.7% 50% at 50% 100%, hsl(0, 0%, 100%) 0%, rgba(255, 255, 255, 0) 100%)", 43 | RIGHT: 44 | "radial-gradient(16.2% 41.199999999999996% at 100% 50%, hsl(0, 0%, 100%) 0%, rgba(255, 255, 255, 0) 100%)", 45 | }; 46 | 47 | const highlight = 48 | "radial-gradient(75% 181.15942028985506% at 50% 50%, #3275F8 0%, rgba(255, 255, 255, 0) 100%)"; 49 | 50 | useEffect(() => { 51 | if (!hovered) { 52 | const interval = setInterval(() => { 53 | setDirection((prevState) => rotateDirection(prevState)); 54 | }, duration * 1000); 55 | return () => clearInterval(interval); 56 | } 57 | }, [hovered]); 58 | return ( 59 | ) => { 61 | setHovered(true); 62 | }} 63 | onMouseLeave={() => setHovered(false)} 64 | className={cn( 65 | "relative flex rounded-full border content-center bg-black/20 hover:bg-black/10 transition duration-500 dark:bg-white/20 items-center flex-col flex-nowrap gap-10 h-min justify-center overflow-visible p-px decoration-clone w-fit", 66 | containerClassName 67 | )} 68 | {...props} 69 | > 70 |
76 | {children} 77 |
78 | 96 |
97 | 98 | ); 99 | } 100 | -------------------------------------------------------------------------------- /components/ui/InfiniteCards.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { cn } from "@/lib/utils"; 4 | import React, { useEffect, useState } from "react"; 5 | 6 | export const InfiniteMovingCards = ({ 7 | items, 8 | direction = "left", 9 | speed = "fast", 10 | pauseOnHover = true, 11 | className, 12 | }: { 13 | items: { 14 | quote: string; 15 | name: string; 16 | title: string; 17 | }[]; 18 | direction?: "left" | "right"; 19 | speed?: "fast" | "normal" | "slow"; 20 | pauseOnHover?: boolean; 21 | className?: string; 22 | }) => { 23 | const containerRef = React.useRef(null); 24 | const scrollerRef = React.useRef(null); 25 | 26 | useEffect(() => { 27 | addAnimation(); 28 | }, []); 29 | const [start, setStart] = useState(false); 30 | function addAnimation() { 31 | if (containerRef.current && scrollerRef.current) { 32 | const scrollerContent = Array.from(scrollerRef.current.children); 33 | 34 | scrollerContent.forEach((item) => { 35 | const duplicatedItem = item.cloneNode(true); 36 | if (scrollerRef.current) { 37 | scrollerRef.current.appendChild(duplicatedItem); 38 | } 39 | }); 40 | 41 | getDirection(); 42 | getSpeed(); 43 | setStart(true); 44 | } 45 | } 46 | const getDirection = () => { 47 | if (containerRef.current) { 48 | if (direction === "left") { 49 | containerRef.current.style.setProperty( 50 | "--animation-direction", 51 | "forwards" 52 | ); 53 | } else { 54 | containerRef.current.style.setProperty( 55 | "--animation-direction", 56 | "reverse" 57 | ); 58 | } 59 | } 60 | }; 61 | const getSpeed = () => { 62 | if (containerRef.current) { 63 | if (speed === "fast") { 64 | containerRef.current.style.setProperty("--animation-duration", "20s"); 65 | } else if (speed === "normal") { 66 | containerRef.current.style.setProperty("--animation-duration", "40s"); 67 | } else { 68 | containerRef.current.style.setProperty("--animation-duration", "80s"); 69 | } 70 | } 71 | }; 72 | return ( 73 |
81 |
    90 | {items.map((item, idx) => ( 91 |
  • 107 |
    108 | 112 | {/* change text color, text-lg */} 113 | 114 | {item.quote} 115 | 116 |
    117 | {/* add this div for the profile img */} 118 |
    119 | profile 120 |
    121 | 122 | {/* change text color, font-normal to font-bold, text-xl */} 123 | 124 | {item.name} 125 | 126 | {/* change text color */} 127 | 128 | {item.title} 129 | 130 | 131 |
    132 |
    133 |
  • 134 | ))} 135 |
136 |
137 | ); 138 | }; 139 | -------------------------------------------------------------------------------- /components/ui/LayoutGrid.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import React, { useState, useRef, useEffect } from "react"; 3 | import { motion } from "framer-motion"; 4 | import Image from "next/image"; 5 | import { cn } from "@/lib/utils"; 6 | import { Button } from "./MovingBorders"; 7 | 8 | type Card = { 9 | id: number; 10 | content: JSX.Element | React.ReactNode | string; 11 | className: string; 12 | thumbnail: string; 13 | }; 14 | 15 | export const LayoutGrid = ({ cards }: { cards: Card[] }) => { 16 | const [selected, setSelected] = useState(null); 17 | const [lastSelected, setLastSelected] = useState(null); 18 | 19 | const handleClick = (card: Card) => { 20 | setLastSelected(selected); 21 | setSelected(card); 22 | }; 23 | 24 | const handleOutsideClick = () => { 25 | setLastSelected(selected); 26 | setSelected(null); 27 | }; 28 | 29 | return ( 30 | // change md:grid-cols-3 to md:grid-cols-4, gap-4 to gap-10 31 |
32 | {cards.map((card, i) => ( 33 | 68 | ))} 69 | 77 |
78 | ); 79 | }; 80 | 81 | const BlurImage = ({ card }: { card: Card }) => { 82 | const [loaded, setLoaded] = useState(false); 83 | return ( 84 | setLoaded(true)} 90 | className={cn( 91 | "object-cover object-top absolute inset-0 h-full w-full transition duration-200", 92 | loaded ? "blur-none" : "blur-md" 93 | )} 94 | alt="thumbnail" 95 | /> 96 | ); 97 | }; 98 | 99 | const SelectedCard = ({ selected }: { selected: Card | null }) => { 100 | return ( 101 |
102 | 111 | 126 | {selected?.content} 127 | 128 |
129 | ); 130 | }; 131 | -------------------------------------------------------------------------------- /components/ui/MovingBorders.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import React from "react"; 3 | import { 4 | motion, 5 | useAnimationFrame, 6 | useMotionTemplate, 7 | useMotionValue, 8 | useTransform, 9 | } from "framer-motion"; 10 | import { useRef } from "react"; 11 | import { cn } from "@/lib/utils"; 12 | 13 | export function Button({ 14 | borderRadius = "1.75rem", 15 | children, 16 | as: Component = "button", 17 | containerClassName, 18 | borderClassName, 19 | duration, 20 | className, 21 | ...otherProps 22 | }: { 23 | borderRadius?: string; 24 | children: React.ReactNode; 25 | as?: any; 26 | containerClassName?: string; 27 | borderClassName?: string; 28 | duration?: number; 29 | className?: string; 30 | [key: string]: any; 31 | }) { 32 | return ( 33 | 44 |
48 | 49 |
55 | 56 |
57 | 58 |
67 | {children} 68 |
69 | 70 | ); 71 | } 72 | 73 | export const MovingBorder = ({ 74 | children, 75 | duration = 2000, 76 | rx, 77 | ry, 78 | ...otherProps 79 | }: { 80 | children: React.ReactNode; 81 | duration?: number; 82 | rx?: string; 83 | ry?: string; 84 | [key: string]: any; 85 | }) => { 86 | const pathRef = useRef(); 87 | const progress = useMotionValue(0); 88 | 89 | useAnimationFrame((time) => { 90 | const length = pathRef.current?.getTotalLength(); 91 | if (length) { 92 | const pxPerMillisecond = length / duration; 93 | progress.set((time * pxPerMillisecond) % length); 94 | } 95 | }); 96 | 97 | const x = useTransform( 98 | progress, 99 | (val) => pathRef.current?.getPointAtLength(val).x 100 | ); 101 | const y = useTransform( 102 | progress, 103 | (val) => pathRef.current?.getPointAtLength(val).y 104 | ); 105 | 106 | const transform = useMotionTemplate`translateX(${x}px) translateY(${y}px) translateX(-50%) translateY(-50%)`; 107 | 108 | return ( 109 | <> 110 | 118 | 126 | 127 | 136 | {children} 137 | 138 | 139 | ); 140 | }; 141 | -------------------------------------------------------------------------------- /components/ui/Pin.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import React, { useState } from "react"; 3 | import { motion } from "framer-motion"; 4 | 5 | import { cn } from "@/lib/utils"; 6 | 7 | export const PinContainer = ({ 8 | children, 9 | title, 10 | href, 11 | className, 12 | containerClassName, 13 | }: { 14 | children: React.ReactNode; 15 | title?: string; 16 | href?: string; 17 | className?: string; 18 | containerClassName?: string; 19 | }) => { 20 | const [transform, setTransform] = useState( 21 | "translate(-50%,-50%) rotateX(0deg)" 22 | ); 23 | 24 | const onMouseEnter = () => { 25 | setTransform("translate(-50%,-50%) rotateX(40deg) scale(0.8)"); 26 | }; 27 | const onMouseLeave = () => { 28 | setTransform("translate(-50%,-50%) rotateX(0deg) scale(1)"); 29 | }; 30 | 31 | return ( 32 |
40 |
47 |
54 |
{children}
55 |
56 |
57 | 58 |
59 | ); 60 | }; 61 | 62 | export const PinPerspective = ({ 63 | title, 64 | href, 65 | }: { 66 | title?: string; 67 | href?: string; 68 | }) => { 69 | return ( 70 | // change w-96 to w-full 71 | 72 |
73 | 86 | 87 |
94 | <> 95 | 115 | 135 | 155 | 156 |
157 | 158 | <> 159 | 160 | 161 | 162 | 163 | 164 |
165 |
166 | ); 167 | }; 168 | -------------------------------------------------------------------------------- /components/ui/Spotlight.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils"; 2 | import React from "react"; 3 | 4 | type SpotlightProps = { 5 | className?: string; 6 | fill?: string; 7 | }; 8 | 9 | export const Spotlight = ({ className, fill }: SpotlightProps) => { 10 | return ( 11 | 20 | 21 | 30 | 31 | 32 | 41 | 42 | 48 | 52 | 53 | 54 | 55 | ); 56 | }; 57 | -------------------------------------------------------------------------------- /components/ui/TextGenerateEffect.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { useEffect } from "react"; 3 | import { motion, stagger, useAnimate } from "framer-motion"; 4 | import { cn } from "@/lib/utils"; 5 | 6 | export const TextGenerateEffect = ({ 7 | words, 8 | className, 9 | }: { 10 | words: string; 11 | className?: string; 12 | }) => { 13 | const [scope, animate] = useAnimate(); 14 | let wordsArray = words.split(" "); 15 | useEffect(() => { 16 | console.log(wordsArray); 17 | animate( 18 | "span", 19 | { 20 | opacity: 1, 21 | }, 22 | { 23 | duration: 2, 24 | delay: stagger(0.2), 25 | } 26 | ); 27 | }, [scope.current]); 28 | 29 | const renderWords = () => { 30 | return ( 31 | 32 | {wordsArray.map((word, idx) => { 33 | return ( 34 | 3 ? "text-purple" : "dark:text-white text-black" 38 | } opacity-0`} 39 | > 40 | {word}{" "} 41 | 42 | ); 43 | })} 44 | 45 | ); 46 | }; 47 | 48 | return ( 49 |
50 | {/* mt-4 to my-4 */} 51 |
52 | {/* remove text-2xl from the original */} 53 |
54 | {renderWords()} 55 |
56 |
57 |
58 | ); 59 | }; 60 | -------------------------------------------------------------------------------- /data/index.ts: -------------------------------------------------------------------------------- 1 | export const navItems = [ 2 | { name: "About", link: "#about" }, 3 | { name: "Projects", link: "#projects" }, 4 | { name: "Testimonials", link: "#testimonials" }, 5 | { name: "Contact", link: "#contact" }, 6 | ]; 7 | 8 | export const gridItems = [ 9 | { 10 | id: 1, 11 | title: "I prioritize client collaboration, fostering open communication ", 12 | description: "", 13 | className: "lg:col-span-3 md:col-span-6 md:row-span-4 lg:min-h-[60vh]", 14 | imgClassName: "w-full h-full", 15 | titleClassName: "justify-end", 16 | img: "/b1.svg", 17 | spareImg: "", 18 | }, 19 | { 20 | id: 2, 21 | title: "I'm very flexible with time zone communications", 22 | description: "", 23 | className: "lg:col-span-2 md:col-span-3 md:row-span-2", 24 | imgClassName: "", 25 | titleClassName: "justify-start", 26 | img: "", 27 | spareImg: "", 28 | }, 29 | { 30 | id: 3, 31 | title: "My tech stack", 32 | description: "I constantly try to improve", 33 | className: "lg:col-span-2 md:col-span-3 md:row-span-2", 34 | imgClassName: "", 35 | titleClassName: "justify-center", 36 | img: "", 37 | spareImg: "", 38 | }, 39 | { 40 | id: 4, 41 | title: "Tech enthusiast with a passion for development.", 42 | description: "", 43 | className: "lg:col-span-2 md:col-span-3 md:row-span-1", 44 | imgClassName: "", 45 | titleClassName: "justify-start", 46 | img: "/grid.svg", 47 | spareImg: "/b4.svg", 48 | }, 49 | 50 | { 51 | id: 5, 52 | title: "Currently building a JS Animation library", 53 | description: "The Inside Scoop", 54 | className: "md:col-span-3 md:row-span-2", 55 | imgClassName: "absolute right-0 bottom-0 md:w-96 w-60", 56 | titleClassName: "justify-center md:justify-start lg:justify-center", 57 | img: "/b5.svg", 58 | spareImg: "/grid.svg", 59 | }, 60 | { 61 | id: 6, 62 | title: "Do you want to start a project together?", 63 | description: "", 64 | className: "lg:col-span-2 md:col-span-3 md:row-span-1", 65 | imgClassName: "", 66 | titleClassName: "justify-center md:max-w-full max-w-60 text-center", 67 | img: "", 68 | spareImg: "", 69 | }, 70 | ]; 71 | 72 | export const projects = [ 73 | { 74 | id: 1, 75 | title: "3D Solar System Planets to Explore", 76 | des: "Explore the wonders of our solar system with this captivating 3D simulation of the planets using Three.js.", 77 | img: "/p1.svg", 78 | iconLists: ["/re.svg", "/tail.svg", "/ts.svg", "/three.svg", "/fm.svg"], 79 | link: "/ui.earth.com", 80 | }, 81 | { 82 | id: 2, 83 | title: "Yoom - Video Conferencing App", 84 | des: "Simplify your video conferencing experience with Yoom. Seamlessly connect with colleagues and friends.", 85 | img: "/p2.svg", 86 | iconLists: ["/next.svg", "/tail.svg", "/ts.svg", "/stream.svg", "/c.svg"], 87 | link: "/ui.yoom.com", 88 | }, 89 | { 90 | id: 3, 91 | title: "AI Image SaaS - Canva Application", 92 | des: "A REAL Software-as-a-Service app with AI features and a payments and credits system using the latest tech stack.", 93 | img: "/p3.svg", 94 | iconLists: ["/re.svg", "/tail.svg", "/ts.svg", "/three.svg", "/c.svg"], 95 | link: "/ui.aiimg.com", 96 | }, 97 | { 98 | id: 4, 99 | title: "Animated Apple Iphone 3D Website", 100 | des: "Recreated the Apple iPhone 15 Pro website, combining GSAP animations and Three.js 3D effects..", 101 | img: "/p4.svg", 102 | iconLists: ["/next.svg", "/tail.svg", "/ts.svg", "/three.svg", "/gsap.svg"], 103 | link: "/ui.apple.com", 104 | }, 105 | ]; 106 | 107 | export const testimonials = [ 108 | { 109 | quote: 110 | "Collaborating with Adrian was an absolute pleasure. His professionalism, promptness, and dedication to delivering exceptional results were evident throughout our project. Adrian's enthusiasm for every facet of development truly stands out. If you're seeking to elevate your website and elevate your brand, Adrian is the ideal partner.", 111 | name: "Michael Johnson", 112 | title: "Director of AlphaStream Technologies", 113 | }, 114 | { 115 | quote: 116 | "Collaborating with Adrian was an absolute pleasure. His professionalism, promptness, and dedication to delivering exceptional results were evident throughout our project. Adrian's enthusiasm for every facet of development truly stands out. If you're seeking to elevate your website and elevate your brand, Adrian is the ideal partner.", 117 | name: "Michael Johnson", 118 | title: "Director of AlphaStream Technologies", 119 | }, 120 | { 121 | quote: 122 | "Collaborating with Adrian was an absolute pleasure. His professionalism, promptness, and dedication to delivering exceptional results were evident throughout our project. Adrian's enthusiasm for every facet of development truly stands out. If you're seeking to elevate your website and elevate your brand, Adrian is the ideal partner.", 123 | name: "Michael Johnson", 124 | title: "Director of AlphaStream Technologies", 125 | }, 126 | { 127 | quote: 128 | "Collaborating with Adrian was an absolute pleasure. His professionalism, promptness, and dedication to delivering exceptional results were evident throughout our project. Adrian's enthusiasm for every facet of development truly stands out. If you're seeking to elevate your website and elevate your brand, Adrian is the ideal partner.", 129 | name: "Michael Johnson", 130 | title: "Director of AlphaStream Technologies", 131 | }, 132 | { 133 | quote: 134 | "Collaborating with Adrian was an absolute pleasure. His professionalism, promptness, and dedication to delivering exceptional results were evident throughout our project. Adrian's enthusiasm for every facet of development truly stands out. If you're seeking to elevate your website and elevate your brand, Adrian is the ideal partner.", 135 | name: "Michael Johnson", 136 | title: "Director of AlphaStream Technologies", 137 | }, 138 | ]; 139 | 140 | export const companies = [ 141 | { 142 | id: 1, 143 | name: "cloudinary", 144 | img: "/cloud.svg", 145 | nameImg: "/cloudName.svg", 146 | }, 147 | { 148 | id: 2, 149 | name: "appwrite", 150 | img: "/app.svg", 151 | nameImg: "/appName.svg", 152 | }, 153 | { 154 | id: 3, 155 | name: "HOSTINGER", 156 | img: "/host.svg", 157 | nameImg: "/hostName.svg", 158 | }, 159 | { 160 | id: 4, 161 | name: "stream", 162 | img: "/s.svg", 163 | nameImg: "/streamName.svg", 164 | }, 165 | { 166 | id: 5, 167 | name: "docker.", 168 | img: "/dock.svg", 169 | nameImg: "/dockerName.svg", 170 | }, 171 | ]; 172 | 173 | export const workExperience = [ 174 | { 175 | id: 1, 176 | title: "Frontend Engineer Intern", 177 | desc: "Assisted in the development of a web-based platform using React.js, enhancing interactivity.", 178 | className: "md:col-span-2", 179 | thumbnail: "/exp1.svg", 180 | }, 181 | { 182 | id: 2, 183 | title: "Mobile App Dev - JSM Tech", 184 | desc: "Designed and developed mobile app for both iOS & Android platforms using React Native.", 185 | className: "md:col-span-2", // change to md:col-span-2 186 | thumbnail: "/exp2.svg", 187 | }, 188 | { 189 | id: 3, 190 | title: "Freelance App Dev Project", 191 | desc: "Led the dev of a mobile app for a client, from initial concept to deployment on app stores.", 192 | className: "md:col-span-2", // change to md:col-span-2 193 | thumbnail: "/exp3.svg", 194 | }, 195 | { 196 | id: 4, 197 | title: "Lead Frontend Developer", 198 | desc: "Developed and maintained user-facing features using modern frontend technologies.", 199 | className: "md:col-span-2", 200 | thumbnail: "/exp4.svg", 201 | }, 202 | ]; 203 | 204 | export const socialMedia = [ 205 | { 206 | id: 1, 207 | img: "/git.svg", 208 | }, 209 | { 210 | id: 2, 211 | img: "/twit.svg", 212 | }, 213 | { 214 | id: 3, 215 | img: "/link.svg", 216 | }, 217 | ]; 218 | -------------------------------------------------------------------------------- /lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { type ClassValue, clsx } from "clsx"; 2 | import { twMerge } from "tailwind-merge"; 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)); 6 | } 7 | -------------------------------------------------------------------------------- /next.config.mjs: -------------------------------------------------------------------------------- 1 | import {withSentryConfig} from '@sentry/nextjs'; 2 | /** @type {import('next').NextConfig} */ 3 | const nextConfig = {}; 4 | 5 | export default withSentryConfig(nextConfig, { 6 | // For all available options, see: 7 | // https://github.com/getsentry/sentry-webpack-plugin#options 8 | 9 | // Suppresses source map uploading logs during build 10 | silent: true, 11 | org: "javascript-mastery", 12 | project: "javascript-nextjs", 13 | }, { 14 | // For all available options, see: 15 | // https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/ 16 | 17 | // Upload a larger set of source maps for prettier stack traces (increases build time) 18 | widenClientFileUpload: true, 19 | 20 | // Transpiles SDK to be compatible with IE11 (increases bundle size) 21 | transpileClientSDK: true, 22 | 23 | // Uncomment to route browser requests to Sentry through a Next.js rewrite to circumvent ad-blockers. 24 | // This can increase your server load as well as your hosting bill. 25 | // Note: Check that the configured route will not match with your Next.js middleware, otherwise reporting of client- 26 | // side errors will fail. 27 | // tunnelRoute: "/monitoring", 28 | 29 | // Hides source maps from generated client bundles 30 | hideSourceMaps: true, 31 | 32 | // Automatically tree-shake Sentry logger statements to reduce bundle size 33 | disableLogger: true, 34 | 35 | // Enables automatic instrumentation of Vercel Cron Monitors. 36 | // See the following for more information: 37 | // https://docs.sentry.io/product/crons/ 38 | // https://vercel.com/docs/cron-jobs 39 | automaticVercelMonitors: true, 40 | }); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "portfolio", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint", 10 | "deploy": "vercel --prod" 11 | }, 12 | "dependencies": { 13 | "@react-three/drei": "^9.105.4", 14 | "@react-three/fiber": "^8.16.2", 15 | "@sentry/nextjs": "^7.105.0", 16 | "@tabler/icons-react": "^3.1.0", 17 | "@types/three": "^0.163.0", 18 | "class-variance-authority": "^0.7.0", 19 | "clsx": "^2.1.0", 20 | "framer-motion": "^11.0.25", 21 | "lucide-react": "^0.365.0", 22 | "mini-svg-data-uri": "^1.4.4", 23 | "next": "14.1.4", 24 | "next-themes": "^0.3.0", 25 | "react": "^18", 26 | "react-dom": "^18", 27 | "react-icons": "^5.0.1", 28 | "react-lottie": "^1.2.4", 29 | "tailwind-merge": "^2.2.2", 30 | "tailwindcss-animate": "^1.0.7", 31 | "three": "^0.163.0", 32 | "three-globe": "^2.31.0", 33 | "vercel": "^34.0.0" 34 | }, 35 | "devDependencies": { 36 | "@types/node": "^20", 37 | "@types/react": "^18", 38 | "@types/react-dom": "^18", 39 | "@types/react-lottie": "^1.2.10", 40 | "autoprefixer": "^10.0.1", 41 | "eslint": "^8", 42 | "eslint-config-next": "14.1.4", 43 | "postcss": "^8", 44 | "tailwindcss": "^3.3.0", 45 | "typescript": "^5" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /public/app.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/appName.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /public/b4.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /public/bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adrianhajdin/portfolio/aa3fc6de5066925313070cbb5bdce9d85cc89241/public/bg.png -------------------------------------------------------------------------------- /public/c.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /public/cloud.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /public/cloudName.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /public/confetti.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adrianhajdin/portfolio/aa3fc6de5066925313070cbb5bdce9d85cc89241/public/confetti.gif -------------------------------------------------------------------------------- /public/dock.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/dockerName.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /public/exp1.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 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /public/exp2.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 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /public/git.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/host.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/hostName.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /public/insta.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/jsm-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adrianhajdin/portfolio/aa3fc6de5066925313070cbb5bdce9d85cc89241/public/jsm-logo.png -------------------------------------------------------------------------------- /public/link.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /public/re.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /public/s.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/stream.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /public/streamName.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/tail.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/three.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 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /public/ts.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /public/twit.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/wha.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /sentry.client.config.ts: -------------------------------------------------------------------------------- 1 | // This file configures the initialization of Sentry on the client. 2 | // The config you add here will be used whenever a users loads a page in their browser. 3 | // https://docs.sentry.io/platforms/javascript/guides/nextjs/ 4 | 5 | import * as Sentry from "@sentry/nextjs"; 6 | 7 | Sentry.init({ 8 | dsn: "https://7b883d075caa19b41fd9b00ae313a1c6@o4506813739368448.ingest.us.sentry.io/4507222371729408", 9 | 10 | // Adjust this value in production, or use tracesSampler for greater control 11 | tracesSampleRate: 1, 12 | 13 | // Setting this option to true will print useful information to the console while you're setting up Sentry. 14 | debug: false, 15 | 16 | replaysOnErrorSampleRate: 1.0, 17 | 18 | // This sets the sample rate to be 10%. You may want this to be 100% while 19 | // in development and sample at a lower rate in production 20 | replaysSessionSampleRate: 0.1, 21 | 22 | // You can remove this option if you're not planning to use the Sentry Session Replay feature: 23 | integrations: [ 24 | Sentry.replayIntegration({ 25 | // Additional Replay configuration goes in here, for example: 26 | maskAllText: true, 27 | blockAllMedia: true, 28 | }), 29 | ], 30 | }); 31 | -------------------------------------------------------------------------------- /sentry.edge.config.ts: -------------------------------------------------------------------------------- 1 | // This file configures the initialization of Sentry for edge features (middleware, edge routes, and so on). 2 | // The config you add here will be used whenever one of the edge features is loaded. 3 | // Note that this config is unrelated to the Vercel Edge Runtime and is also required when running locally. 4 | // https://docs.sentry.io/platforms/javascript/guides/nextjs/ 5 | 6 | import * as Sentry from "@sentry/nextjs"; 7 | 8 | Sentry.init({ 9 | dsn: "https://7b883d075caa19b41fd9b00ae313a1c6@o4506813739368448.ingest.us.sentry.io/4507222371729408", 10 | 11 | // Adjust this value in production, or use tracesSampler for greater control 12 | tracesSampleRate: 1, 13 | 14 | // Setting this option to true will print useful information to the console while you're setting up Sentry. 15 | debug: false, 16 | }); 17 | -------------------------------------------------------------------------------- /sentry.server.config.ts: -------------------------------------------------------------------------------- 1 | // This file configures the initialization of Sentry on the server. 2 | // The config you add here will be used whenever the server handles a request. 3 | // https://docs.sentry.io/platforms/javascript/guides/nextjs/ 4 | 5 | import * as Sentry from "@sentry/nextjs"; 6 | 7 | Sentry.init({ 8 | dsn: "https://7b883d075caa19b41fd9b00ae313a1c6@o4506813739368448.ingest.us.sentry.io/4507222371729408", 9 | 10 | // Adjust this value in production, or use tracesSampler for greater control 11 | tracesSampleRate: 1, 12 | 13 | // Setting this option to true will print useful information to the console while you're setting up Sentry. 14 | debug: false, 15 | 16 | // uncomment the line below to enable Spotlight (https://spotlightjs.com) 17 | // spotlight: process.env.NODE_ENV === 'development', 18 | 19 | }); 20 | -------------------------------------------------------------------------------- /tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "tailwindcss"; 2 | 3 | const svgToDataUri = require("mini-svg-data-uri"); 4 | 5 | const colors = require("tailwindcss/colors"); 6 | const { 7 | default: flattenColorPalette, 8 | } = require("tailwindcss/lib/util/flattenColorPalette"); 9 | 10 | const config = { 11 | darkMode: ["class"], 12 | content: [ 13 | "./pages/**/*.{ts,tsx}", 14 | "./components/**/*.{ts,tsx}", 15 | "./app/**/*.{ts,tsx}", 16 | "./src/**/*.{ts,tsx}", 17 | "./data/**/*.{ts,tsx}", 18 | ], 19 | prefix: "", 20 | theme: { 21 | container: { 22 | center: true, 23 | padding: "2rem", 24 | screens: { 25 | "2xl": "1400px", 26 | }, 27 | }, 28 | extend: { 29 | colors: { 30 | black: { 31 | DEFAULT: "#000", 32 | 100: "#000319", 33 | 200: "rgba(17, 25, 40, 0.75)", 34 | 300: "rgba(255, 255, 255, 0.125)", 35 | }, 36 | white: { 37 | DEFAULT: "#FFF", 38 | 100: "#BEC1DD", 39 | 200: "#C1C2D3", 40 | }, 41 | blue: { 42 | "100": "#E4ECFF", 43 | }, 44 | purple: "#CBACF9", 45 | border: "hsl(var(--border))", 46 | input: "hsl(var(--input))", 47 | ring: "hsl(var(--ring))", 48 | background: "hsl(var(--background))", 49 | foreground: "hsl(var(--foreground))", 50 | primary: { 51 | DEFAULT: "hsl(var(--primary))", 52 | foreground: "hsl(var(--primary-foreground))", 53 | }, 54 | secondary: { 55 | DEFAULT: "hsl(var(--secondary))", 56 | foreground: "hsl(var(--secondary-foreground))", 57 | }, 58 | destructive: { 59 | DEFAULT: "hsl(var(--destructive))", 60 | foreground: "hsl(var(--destructive-foreground))", 61 | }, 62 | muted: { 63 | DEFAULT: "hsl(var(--muted))", 64 | foreground: "hsl(var(--muted-foreground))", 65 | }, 66 | accent: { 67 | DEFAULT: "hsl(var(--accent))", 68 | foreground: "hsl(var(--accent-foreground))", 69 | }, 70 | popover: { 71 | DEFAULT: "hsl(var(--popover))", 72 | foreground: "hsl(var(--popover-foreground))", 73 | }, 74 | card: { 75 | DEFAULT: "hsl(var(--card))", 76 | foreground: "hsl(var(--card-foreground))", 77 | }, 78 | }, 79 | borderRadius: { 80 | lg: "var(--radius)", 81 | md: "calc(var(--radius) - 2px)", 82 | sm: "calc(var(--radius) - 4px)", 83 | }, 84 | keyframes: { 85 | "accordion-down": { 86 | from: { height: "0" }, 87 | to: { height: "var(--radix-accordion-content-height)" }, 88 | }, 89 | "accordion-up": { 90 | from: { height: "var(--radix-accordion-content-height)" }, 91 | to: { height: "0" }, 92 | }, 93 | spotlight: { 94 | "0%": { 95 | opacity: "0", 96 | transform: "translate(-72%, -62%) scale(0.5)", 97 | }, 98 | "100%": { 99 | opacity: "1", 100 | transform: "translate(-50%,-40%) scale(1)", 101 | }, 102 | }, 103 | shimmer: { 104 | from: { 105 | backgroundPosition: "0 0", 106 | }, 107 | to: { 108 | backgroundPosition: "-200% 0", 109 | }, 110 | }, 111 | moveHorizontal: { 112 | "0%": { 113 | transform: "translateX(-50%) translateY(-10%)", 114 | }, 115 | "50%": { 116 | transform: "translateX(50%) translateY(10%)", 117 | }, 118 | "100%": { 119 | transform: "translateX(-50%) translateY(-10%)", 120 | }, 121 | }, 122 | moveInCircle: { 123 | "0%": { 124 | transform: "rotate(0deg)", 125 | }, 126 | "50%": { 127 | transform: "rotate(180deg)", 128 | }, 129 | "100%": { 130 | transform: "rotate(360deg)", 131 | }, 132 | }, 133 | moveVertical: { 134 | "0%": { 135 | transform: "translateY(-50%)", 136 | }, 137 | "50%": { 138 | transform: "translateY(50%)", 139 | }, 140 | "100%": { 141 | transform: "translateY(-50%)", 142 | }, 143 | }, 144 | scroll: { 145 | to: { 146 | transform: "translate(calc(-50% - 0.5rem))", 147 | }, 148 | }, 149 | }, 150 | animation: { 151 | "accordion-down": "accordion-down 0.2s ease-out", 152 | "accordion-up": "accordion-up 0.2s ease-out", 153 | spotlight: "spotlight 2s ease .75s 1 forwards", 154 | shimmer: "shimmer 2s linear infinite", 155 | first: "moveVertical 30s ease infinite", 156 | second: "moveInCircle 20s reverse infinite", 157 | third: "moveInCircle 40s linear infinite", 158 | fourth: "moveHorizontal 40s ease infinite", 159 | fifth: "moveInCircle 20s ease infinite", 160 | scroll: 161 | "scroll var(--animation-duration, 40s) var(--animation-direction, forwards) linear infinite", 162 | }, 163 | }, 164 | }, 165 | plugins: [ 166 | require("tailwindcss-animate"), 167 | addVariablesForColors, 168 | function ({ matchUtilities, theme }: any) { 169 | matchUtilities( 170 | { 171 | "bg-grid": (value: any) => ({ 172 | backgroundImage: `url("${svgToDataUri( 173 | `` 174 | )}")`, 175 | }), 176 | "bg-grid-small": (value: any) => ({ 177 | backgroundImage: `url("${svgToDataUri( 178 | `` 179 | )}")`, 180 | }), 181 | "bg-dot": (value: any) => ({ 182 | backgroundImage: `url("${svgToDataUri( 183 | `` 184 | )}")`, 185 | }), 186 | }, 187 | { values: flattenColorPalette(theme("backgroundColor")), type: "color" } 188 | ); 189 | }, 190 | ], 191 | } satisfies Config; 192 | 193 | function addVariablesForColors({ addBase, theme }: any) { 194 | let allColors = flattenColorPalette(theme("colors")); 195 | let newVars = Object.fromEntries( 196 | Object.entries(allColors).map(([key, val]) => [`--${key}`, val]) 197 | ); 198 | 199 | addBase({ 200 | ":root": newVars, 201 | }); 202 | } 203 | 204 | export default config; 205 | -------------------------------------------------------------------------------- /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 | "@/*": ["./*"] 22 | } 23 | }, 24 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 25 | "exclude": ["node_modules"] 26 | } 27 | --------------------------------------------------------------------------------