├── .eslintrc.json ├── app ├── favicon.ico ├── api │ ├── route.ts │ ├── fetch-umami-stats │ │ └── route.ts │ ├── fetch-umami-events │ │ └── route.ts │ ├── fetch-project-posts │ │ └── route.ts │ └── fetch-github-stars │ │ └── route.ts ├── page.tsx ├── technologies │ └── page.tsx ├── layout.tsx ├── deployments │ └── page.tsx ├── globals.css └── worldwide-reach │ └── page.tsx ├── public ├── images │ ├── thumbnail.png │ └── githubstar.webp ├── vercel.svg └── next.svg ├── postcss.config.mjs ├── lib ├── animations.ts ├── fetchers.ts ├── utils.ts ├── useIntersectionObserver.ts └── data.ts ├── .env.example ├── next.config.mjs ├── components ├── theme-provider.tsx ├── ui │ ├── ripper-card.tsx │ ├── label.tsx │ ├── input.tsx │ ├── toaster.tsx │ ├── input-with-button.tsx │ ├── button.tsx │ ├── card.tsx │ ├── calendar.tsx │ ├── dialog.tsx │ ├── use-toast.ts │ ├── form.tsx │ ├── toast.tsx │ ├── command.tsx │ ├── dropdown-menu.tsx │ └── chart.tsx ├── globe-and-stars.tsx ├── github-stars.tsx ├── contact-button.tsx ├── magicui │ ├── blur-in.tsx │ ├── separate-away.tsx │ ├── retro-grid.tsx │ ├── marquee.tsx │ ├── meteors.tsx │ ├── word-pull-up.tsx │ ├── ripple.tsx │ ├── fade-in.tsx │ ├── number-ticker.tsx │ ├── box-reveal.tsx │ ├── text-reveal.tsx │ ├── animated-subscribe-button.tsx │ ├── orbiting-circles.tsx │ ├── bento-grid.tsx │ ├── shimmer-button.tsx │ ├── scroll-based-velocity.tsx │ ├── animated-grid-pattern.tsx │ ├── animated-beam.tsx │ ├── terminal.tsx │ ├── globe.tsx │ ├── particles.tsx │ └── icon-cloud.tsx ├── project-posts.tsx ├── theme-toggle.tsx ├── call-to-action.tsx ├── technologies.tsx ├── hero.tsx ├── project-showcase.tsx ├── project-showcase-vertical.tsx ├── comments.tsx ├── orbit.tsx ├── email-form.tsx └── stats-chart.tsx ├── components.json ├── .gitignore ├── pb └── pb_schema.json ├── tsconfig.json ├── package.json ├── tailwind.config.ts └── README.md /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/engageintellect/cook/HEAD/app/favicon.ico -------------------------------------------------------------------------------- /public/images/thumbnail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/engageintellect/cook/HEAD/public/images/thumbnail.png -------------------------------------------------------------------------------- /public/images/githubstar.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/engageintellect/cook/HEAD/public/images/githubstar.webp -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | const config = { 3 | plugins: { 4 | tailwindcss: {}, 5 | }, 6 | }; 7 | 8 | export default config; 9 | -------------------------------------------------------------------------------- /lib/animations.ts: -------------------------------------------------------------------------------- 1 | import { gsap } from 'gsap'; 2 | 3 | export const animateMainStagger = () => { 4 | gsap.from('.animate-item', { 5 | opacity: 0, 6 | y: 20, 7 | duration: 1, 8 | delay: 0.1, 9 | stagger: 0.1, 10 | ease: 'power4.out' 11 | }); 12 | }; 13 | -------------------------------------------------------------------------------- /app/api/route.ts: -------------------------------------------------------------------------------- 1 | export const dynamic = 'force-dynamic' // defaults to auto 2 | export async function GET(request: Request) { 3 | return new Response( 4 | JSON.stringify({ 5 | msg: 'hello world', 6 | app: 'cook', 7 | version: '0.5.0', 8 | }), 9 | { 10 | status: 200, 11 | } 12 | ) 13 | } -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_BASE_URL=YOUR_BASE_URL 2 | GITHUB_USERNAME=YOUR_USERNAME 3 | GITHUB_URL=https://github.com/{YOUR_USERNAME} 4 | REPO_NAME=YOUR_REPO_NAME 5 | AVATAR_URL=https://github.com/{YOUR_USERNAME}.png 6 | NEXT_PUBLIC_PORTFOLIO_URL={YOUR_PORTFOLIO_URL} 7 | NEXT_PUBLIC_AVAILABLE_FOR_FREELANCE=true 8 | NEXT_PUBLIC_DISCORD=https://discord.gg/{YOUR_DISCORD_ID} 9 | 10 | -------------------------------------------------------------------------------- /next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | images: { 4 | remotePatterns: [ 5 | { 6 | protocol: 'https', 7 | hostname: 'github.com', 8 | }, 9 | { 10 | protocol: 'https', 11 | hostname: 'cdn.simpleicons.org', 12 | }, 13 | ], 14 | }, 15 | }; 16 | 17 | export default nextConfig; 18 | -------------------------------------------------------------------------------- /components/theme-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/page.tsx: -------------------------------------------------------------------------------- 1 | import { Bento } from "@/components/bento"; 2 | 3 | export default function Home() { 4 | return ( 5 | <> 6 |
7 |
8 |
9 | 10 |
11 |
12 |
13 | 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /components/ui/ripper-card.tsx: -------------------------------------------------------------------------------- 1 | import Ripple from "@/components/magicui/ripple"; 2 | 3 | export function RippleCard() { 4 | return ( 5 |
6 |

7 | bring your ideas to life. 8 |

9 | 10 |
11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /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": "neutral", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "iconLibrary": "lucide", 14 | "aliases": { 15 | "components": "@/components", 16 | "utils": "@/lib/utils" 17 | }, 18 | "registries": { 19 | "@magicui": "https://magicui.design/r/{name}.json" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/fetchers.ts: -------------------------------------------------------------------------------- 1 | // fetchFunctions.ts 2 | 3 | export const fetchStars = async (): Promise => { 4 | const baseUrl = 5 | typeof window !== "undefined" ? "" : process.env.NEXT_PUBLIC_BASE_URL; 6 | const res = await fetch(`${baseUrl}/api/fetch-github-stars`); 7 | const data = await res.json(); 8 | return Number(data?.totalStars); 9 | }; 10 | 11 | export const fetchProjects = async () => { 12 | const baseUrl = 13 | typeof window !== "undefined" ? "" : process.env.NEXT_PUBLIC_BASE_URL; 14 | const res = await fetch(`${baseUrl}/api/fetch-project-posts`); 15 | const data = await res.json(); 16 | return data; 17 | }; 18 | -------------------------------------------------------------------------------- /pb/pb_schema.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "jwvcpkb2f322agd", 4 | "name": "cook_form_submissions", 5 | "type": "base", 6 | "system": false, 7 | "schema": [ 8 | { 9 | "id": "axzcm6o8", 10 | "name": "email", 11 | "type": "email", 12 | "system": false, 13 | "required": true, 14 | "unique": true, 15 | "options": { 16 | "exceptDomains": null, 17 | "onlyDomains": null 18 | } 19 | } 20 | ], 21 | "listRule": null, 22 | "viewRule": null, 23 | "createRule": "", 24 | "updateRule": null, 25 | "deleteRule": null, 26 | "options": {} 27 | } 28 | ] -------------------------------------------------------------------------------- /components/globe-and-stars.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import Particles from "@/components/magicui/particles"; 4 | import { useTheme } from "next-themes"; 5 | import { useEffect, useState } from "react"; 6 | import Globe from "@/components/magicui/globe"; 7 | 8 | export default function GlobeAndStars() { 9 | const { theme } = useTheme(); 10 | const [color, setColor] = useState("#ffffff"); 11 | 12 | useEffect(() => { 13 | setColor(theme === "dark" ? "#ffffff" : "#808080"); 14 | }, [theme]); 15 | 16 | return ( 17 |
18 | 19 | 20 |
21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /components/github-stars.tsx: -------------------------------------------------------------------------------- 1 | // GitHubStars.tsx 2 | 3 | "use client"; 4 | 5 | import { useState, useEffect } from "react"; 6 | import { fetchStars } from "@/lib/fetchers"; 7 | import NumberTicker from "@/components/magicui/number-ticker"; 8 | import { formatLargeNumber } from "@/lib/utils"; 9 | 10 | const GitHubStars = () => { 11 | const [stars, setStars] = useState(null); 12 | 13 | useEffect(() => { 14 | const getStars = async () => { 15 | const totalStars = await fetchStars(); 16 | setStars(totalStars); 17 | }; 18 | 19 | getStars(); 20 | }, []); 21 | 22 | if (stars === null) { 23 | return
0
; 24 | } 25 | 26 | return ; 27 | }; 28 | 29 | export default GitHubStars; 30 | -------------------------------------------------------------------------------- /components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as LabelPrimitive from "@radix-ui/react-label" 5 | import { cva, type VariantProps } from "class-variance-authority" 6 | 7 | import { cn } from "@/lib/utils" 8 | 9 | const labelVariants = cva( 10 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" 11 | ) 12 | 13 | const Label = React.forwardRef< 14 | React.ElementRef, 15 | React.ComponentPropsWithoutRef & 16 | VariantProps 17 | >(({ className, ...props }, ref) => ( 18 | 23 | )) 24 | Label.displayName = LabelPrimitive.Root.displayName 25 | 26 | export { Label } 27 | -------------------------------------------------------------------------------- /components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import { cn } from "@/lib/utils"; 4 | 5 | export interface InputProps 6 | extends React.InputHTMLAttributes {} 7 | 8 | const Input = React.forwardRef( 9 | ({ className, type, ...props }, ref) => { 10 | return ( 11 | 20 | ); 21 | } 22 | ); 23 | Input.displayName = "Input"; 24 | 25 | export { Input }; 26 | -------------------------------------------------------------------------------- /components/contact-button.tsx: -------------------------------------------------------------------------------- 1 | import { AnimatedSubscribeButton } from "@/components/magicui/animated-subscribe-button"; 2 | import { CheckIcon, ChevronRightIcon } from "lucide-react"; 3 | 4 | export function SubscribeButton() { 5 | return ( 6 | 12 | Subscribe{" "} 13 | 14 | 15 | } 16 | changeText={ 17 | 18 | 19 | Subscribed{" "} 20 | 21 | } 22 | /> 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /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 | 8 | 9 | export function formatTagString(tag: string) { 10 | return tag.replaceAll(" ", "-").toLowerCase() 11 | } 12 | 13 | export function sanitizeSlug(slug: string): string { 14 | const replacements: { [key: string]: string } = { 15 | nodedotjs: "nodejs", 16 | nextdotjs: "nextjs", 17 | // Add more replacements as needed 18 | }; 19 | 20 | return replacements[slug] || slug; 21 | } 22 | 23 | 24 | export function formatLargeNumber(num: number): string { 25 | if (num >= 1_000_000) { 26 | return (num / 1_000_000).toFixed(2).replace(/\.?0+$/, '') + 'm'; 27 | } else if (num >= 1_000) { 28 | return (num / 1_000).toFixed(2).replace(/\.?0+$/, '') + 'k'; 29 | } 30 | return num.toString(); 31 | } 32 | 33 | 34 | -------------------------------------------------------------------------------- /lib/useIntersectionObserver.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState, useRef, MutableRefObject } from 'react'; 2 | 3 | export const useIntersectionObserver = (): { 4 | ref: MutableRefObject; 5 | isIntersecting: boolean; 6 | } => { 7 | const [isIntersecting, setIsIntersecting] = useState(false); 8 | const ref = useRef(null); 9 | 10 | useEffect(() => { 11 | if (!ref.current) return; 12 | 13 | const observer = new IntersectionObserver( 14 | ([entry]) => { 15 | console.log('IntersectionObserver entry:', entry); 16 | setIsIntersecting(entry.isIntersecting); 17 | }, 18 | { 19 | root: null, 20 | rootMargin: '0px', 21 | threshold: 0.1, 22 | } 23 | ); 24 | 25 | observer.observe(ref.current); 26 | 27 | return () => { 28 | observer.disconnect(); 29 | }; 30 | }, [ref]); 31 | 32 | return { ref, isIntersecting }; 33 | }; 34 | -------------------------------------------------------------------------------- /components/ui/toaster.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { 4 | Toast, 5 | ToastClose, 6 | ToastDescription, 7 | ToastProvider, 8 | ToastTitle, 9 | ToastViewport, 10 | } from "@/components/ui/toast"; 11 | import { useToast } from "@/components/ui/use-toast"; 12 | 13 | export function Toaster() { 14 | const { toasts } = useToast(); 15 | 16 | return ( 17 | 18 | {toasts.map(function ({ id, title, description, action, ...props }) { 19 | return ( 20 | 21 |
22 | {title && {title}} 23 | {description && ( 24 | {description} 25 | )} 26 |
27 | {action} 28 | 29 |
30 | ); 31 | })} 32 | 33 |
34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /lib/data.ts: -------------------------------------------------------------------------------- 1 | 2 | export const defaultDomains = [ 3 | { 4 | name: "Crypto", 5 | body: "Interface with blockchains, create smart contracts, track market data, manage digital assets.", 6 | slug: "crypto", 7 | image: "crypto", 8 | }, 9 | { 10 | name: "Commerce", 11 | body: "Sell your product or service online.", 12 | slug: "commerce", 13 | image: "crypto", 14 | }, 15 | { 16 | name: "Web", 17 | body: "Create beautiful, responsive, and performant websites.", 18 | slug: "web", 19 | image: "crypto", 20 | }, 21 | { 22 | name: "IOT", 23 | body: "Interface with things around you.", 24 | slug: "iot", 25 | image: "crypto", 26 | }, 27 | { 28 | name: "AI", 29 | body: "Create intelligent, context-aware applications that understand your unique data.", 30 | slug: "ai", 31 | image: "crypto", 32 | }, 33 | { 34 | name: "API", 35 | body: "Create APIs that power your applications.", 36 | slug: "api", 37 | image: "crypto", 38 | }, 39 | ]; 40 | -------------------------------------------------------------------------------- /components/magicui/blur-in.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { motion } from "framer-motion"; 3 | import { cn } from "@/lib/utils"; 4 | import { ReactNode } from "react"; 5 | 6 | interface BlurIntProps { 7 | children: ReactNode; 8 | className?: string; 9 | variant?: { 10 | hidden: { filter: string; opacity: number }; 11 | visible: { filter: string; opacity: number }; 12 | }; 13 | duration?: number; 14 | } 15 | 16 | const BlurIn = ({ 17 | children, 18 | className, 19 | variant, 20 | duration = 0.33, 21 | }: BlurIntProps) => { 22 | const defaultVariants = { 23 | hidden: { filter: "blur(10px)", opacity: 0 }, 24 | visible: { filter: "blur(0px)", opacity: 1 }, 25 | }; 26 | const combinedVariants = variant || defaultVariants; 27 | 28 | return ( 29 | 36 | {children} 37 | 38 | ); 39 | }; 40 | 41 | export default BlurIn; 42 | -------------------------------------------------------------------------------- /components/project-posts.tsx: -------------------------------------------------------------------------------- 1 | // ProjectPosts.tsx 2 | 3 | "use client"; 4 | 5 | import { useState, useEffect } from "react"; 6 | import { fetchProjects } from "@/lib/fetchers"; 7 | import ProjectShowcaseVertical from "@/components/project-showcase-vertical"; 8 | import { defaultDomains } from "@/lib/data"; 9 | 10 | const ProjectPosts = () => { 11 | const [posts, setPosts] = useState(null); 12 | const [files, setFiles] = useState(defaultDomains); 13 | 14 | useEffect(() => { 15 | const getPosts = async () => { 16 | const postsData = await fetchProjects(); 17 | if (postsData) { 18 | const formattedPosts = postsData.postsData.map((post: any) => ({ 19 | name: post.data.title, 20 | body: post.data.description, 21 | slug: post.slug, 22 | image: post.data.image, 23 | })); 24 | setFiles(formattedPosts.slice(0, 10)); 25 | } 26 | setPosts(postsData); 27 | }; 28 | 29 | getPosts(); 30 | }, []); 31 | 32 | return ; 33 | }; 34 | 35 | export default ProjectPosts; 36 | -------------------------------------------------------------------------------- /app/technologies/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { motion } from "framer-motion"; 4 | import Technologies from "@/components/technologies"; 5 | 6 | export default function Tech() { 7 | return ( 8 |
9 | 15 |
Technologies
16 |

17 | Click on an icon to see projects using that technology. 18 |

19 |
20 | 21 | 27 |
28 | 29 |
30 |
31 |
32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import { Inter } from "next/font/google"; 3 | import "./globals.css"; 4 | import { ThemeProvider } from "@/components/theme-provider"; 5 | import { Toaster } from "@/components/ui/toaster"; 6 | 7 | const inter = Inter({ subsets: ["latin"] }); 8 | 9 | export const metadata: Metadata = { 10 | title: "engage-cook", 11 | description: "built using next.js, magic-ui, and tailwindcss", 12 | }; 13 | 14 | export default function RootLayout({ 15 | children, 16 | }: Readonly<{ 17 | children: React.ReactNode; 18 | }>) { 19 | return ( 20 | 21 | 22 | 27 | 28 | 29 | 35 | {children} 36 | 37 | 38 | 39 | 40 | ); 41 | } 42 | -------------------------------------------------------------------------------- /components/magicui/separate-away.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { cn } from "@/lib/utils"; 4 | import { motion } from "framer-motion"; 5 | 6 | interface SeparateAwayProps { 7 | upper_text: string; 8 | lower_text: string; 9 | duration?: number; 10 | hidden_opacity?: number; 11 | visible_opacity?: number; 12 | className?: string; 13 | } 14 | 15 | export function SeparateAway({ 16 | upper_text, 17 | lower_text, 18 | duration = 1.5, 19 | hidden_opacity = 0, 20 | visible_opacity = 1, 21 | className, 22 | }: SeparateAwayProps) { 23 | const separate = { 24 | hidden: { opacity: hidden_opacity, y: 0 }, 25 | visible: (custom: number) => ({ 26 | opacity: visible_opacity, 27 | y: custom * 5, 28 | transition: { duration: duration }, 29 | }), 30 | }; 31 | 32 | return ( 33 |
34 | 41 | {upper_text} 42 | 43 | 50 | {lower_text} 51 | 52 |
53 | ); 54 | } 55 | -------------------------------------------------------------------------------- /components/magicui/retro-grid.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils"; 2 | 3 | export default function RetroGrid({ className }: { className?: string }) { 4 | return ( 5 |
11 | {/* Grid */} 12 |
13 |
26 |
27 | 28 | {/* Background Gradient */} 29 |
30 |
31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/api/fetch-umami-stats/route.ts: -------------------------------------------------------------------------------- 1 | import { getClient } from '@umami/api-client'; 2 | 3 | export const dynamic = 'force-dynamic'; // defaults to auto 4 | 5 | const client = getClient(); 6 | 7 | export async function GET(request: Request) { 8 | try { 9 | // The website ID 10 | const websiteId = 'e2279467-b5f6-4e9d-8b62-869876b433f9'; 11 | 12 | // Get the current time and the Unix epoch time in milliseconds 13 | const now = Date.now(); 14 | const oneDayInMilliseconds = 7 * 24 * 60 * 60 * 1000; // 24 hours in milliseconds 15 | const startAt = now - oneDayInMilliseconds; // 24 hours ago 16 | 17 | // Prepare the data object for getWebsiteMetrics 18 | const metricsData = { 19 | startAt: startAt, 20 | endAt: now, 21 | type: 'url' // Example type, change as needed 22 | }; 23 | 24 | const { ok, data, status, error } = await client.getWebsiteStats(websiteId, metricsData); 25 | 26 | if (!ok) { 27 | return new Response(JSON.stringify({ error: 'Failed to fetch website metrics' }), { status: status }); 28 | } 29 | 30 | return new Response(JSON.stringify(data), { status: 200 }); 31 | } catch (error) { 32 | console.error('Error fetching website metrics:', error); 33 | return new Response(JSON.stringify({ error: 'Failed to fetch website metrics' }), { status: 500 }); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /components/magicui/marquee.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils"; 2 | 3 | interface MarqueeProps { 4 | className?: string; 5 | reverse?: boolean; 6 | pauseOnHover?: boolean; 7 | children?: React.ReactNode; 8 | vertical?: boolean; 9 | repeat?: number; 10 | [key: string]: any; 11 | } 12 | 13 | export default function Marquee({ 14 | className, 15 | reverse, 16 | pauseOnHover = false, 17 | children, 18 | vertical = false, 19 | repeat = 4, 20 | ...props 21 | }: MarqueeProps) { 22 | return ( 23 |
34 | {Array(repeat) 35 | .fill(0) 36 | .map((_, i) => ( 37 |
46 | {children} 47 |
48 | ))} 49 |
50 | ); 51 | } 52 | -------------------------------------------------------------------------------- /components/magicui/meteors.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import clsx from "clsx"; 4 | import { useEffect, useState } from "react"; 5 | 6 | interface MeteorsProps { 7 | number?: number; 8 | } 9 | export const Meteors = ({ number = 20 }: MeteorsProps) => { 10 | const [meteorStyles, setMeteorStyles] = useState>( 11 | [] 12 | ); 13 | 14 | useEffect(() => { 15 | const styles = [...new Array(number)].map(() => ({ 16 | top: -5, 17 | left: Math.floor(Math.random() * window.innerWidth) + "px", 18 | animationDelay: Math.random() * 1 + 0.2 + "s", 19 | animationDuration: Math.floor(Math.random() * 8 + 2) + "s", 20 | })); 21 | setMeteorStyles(styles); 22 | }, [number]); 23 | 24 | return ( 25 | <> 26 | {[...meteorStyles].map((style, idx) => ( 27 | // Meteor Head 28 | 35 | {/* Meteor Tail */} 36 |
37 | 38 | ))} 39 | 40 | ); 41 | }; 42 | 43 | export default Meteors; 44 | -------------------------------------------------------------------------------- /components/magicui/word-pull-up.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { motion, Variants } from "framer-motion"; 4 | import { cn } from "../../lib/utils"; 5 | 6 | interface WordPullUpProps { 7 | words: string; 8 | delayMultiple?: number; 9 | wrapperFramerProps?: Variants; 10 | framerProps?: Variants; 11 | className?: string; 12 | } 13 | 14 | export default function WordPullUp({ 15 | words, 16 | wrapperFramerProps = { 17 | hidden: { opacity: 0 }, 18 | show: { 19 | opacity: 1, 20 | transition: { 21 | staggerChildren: 0.2, 22 | }, 23 | }, 24 | }, 25 | framerProps = { 26 | hidden: { y: 20, opacity: 0 }, 27 | show: { y: 0, opacity: 1 }, 28 | }, 29 | className, 30 | }: WordPullUpProps) { 31 | return ( 32 | 41 | {words.split(" ").map((word, i) => ( 42 | 47 | {word === "" ?   : word} 48 | 49 | ))} 50 | 51 | ); 52 | } 53 | -------------------------------------------------------------------------------- /components/ui/input-with-button.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import { Button } from "@/components/ui/button"; 3 | import { Input } from "@/components/ui/input"; 4 | import PocketBase from "pocketbase"; 5 | 6 | // Initialize PocketBase client 7 | const pb = new PocketBase("https://engage-dev.com/pb"); 8 | 9 | export function InputWithButton() { 10 | const [email, setEmail] = useState(""); 11 | 12 | const handleInputChange = (e: React.ChangeEvent) => { 13 | setEmail(e.target.value); 14 | }; 15 | 16 | const handleSubmit = async (e: React.FormEvent) => { 17 | e.preventDefault(); 18 | try { 19 | const data = { email }; 20 | const record = await pb.collection("cook_form_submissions").create(data); 21 | console.log("Record created:", record); 22 | // Optionally, reset the form 23 | setEmail(""); 24 | alert("Email submitted successfully!"); 25 | } catch (error) { 26 | console.error("Error creating record:", error); 27 | alert("Failed to submit email. Please try again."); 28 | } 29 | }; 30 | 31 | return ( 32 |
36 | 43 | 44 |
45 | ); 46 | } 47 | -------------------------------------------------------------------------------- /app/api/fetch-umami-events/route.ts: -------------------------------------------------------------------------------- 1 | import { getClient } from '@umami/api-client'; 2 | 3 | export const dynamic = 'force-dynamic'; // defaults to auto 4 | 5 | const client = getClient(); 6 | 7 | export async function GET(request: Request) { 8 | try { 9 | // The website ID 10 | const websiteId = 'e2279467-b5f6-4e9d-8b62-869876b433f9'; 11 | 12 | // Get the current time and one year ago in milliseconds 13 | const now = Date.now(); 14 | const oneYearAgo = now - 365 * 24 * 60 * 60 * 1000; // 1 year ago 15 | 16 | // Prepare the data object for getWebsitePageviews 17 | const pageviewsData = { 18 | startAt: oneYearAgo, 19 | endAt: now, 20 | unit: 'day', // Change to 'day' for daily data over the year 21 | timezone: 'America/Los_Angeles', 22 | region: 'US', 23 | }; 24 | 25 | console.log('Fetching pageviews with data:', pageviewsData); 26 | 27 | const response = await client.getWebsitePageviews(websiteId, pageviewsData); 28 | const { ok, data, status, error } = response; // Adjusted to destructure from response 29 | 30 | if (!ok) { 31 | return new Response(JSON.stringify({ error: 'Failed to fetch website pageviews' }), { status: status }); 32 | } 33 | 34 | console.log('Pageviews data:', data); 35 | 36 | return new Response(JSON.stringify(data), { status: 200 }); 37 | } catch (error) { 38 | console.error('Error fetching website pageviews:', error); 39 | return new Response(JSON.stringify({ error: 'Failed to fetch website pageviews' }), { status: 500 }); 40 | } 41 | } -------------------------------------------------------------------------------- /components/theme-toggle.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import { Moon, Sun, Contrast } from "lucide-react"; 5 | import { useTheme } from "next-themes"; 6 | 7 | import { Button } from "@/components/ui/button"; 8 | import { 9 | DropdownMenu, 10 | DropdownMenuContent, 11 | DropdownMenuItem, 12 | DropdownMenuTrigger, 13 | } from "@/components/ui/dropdown-menu"; 14 | 15 | export default function ThemeToggle() { 16 | const { setTheme } = useTheme(); 17 | 18 | return ( 19 | 20 | 21 | 26 | 27 | 28 | setTheme("light")}> 29 | 30 | Light 31 | 32 | setTheme("dark")}> 33 | 34 | Dark 35 | 36 | setTheme("system")}> 37 | 38 | System 39 | 40 | 41 | 42 | ); 43 | } 44 | -------------------------------------------------------------------------------- /app/api/fetch-project-posts/route.ts: -------------------------------------------------------------------------------- 1 | export const dynamic = 'force-dynamic'; // defaults to auto 2 | 3 | export async function GET(request: Request) { 4 | const url = `https://bento.engage-dev.com/api/v1/fetchProjectsFeed.json`; 5 | const headers = { "Accept": "application/json" }; 6 | 7 | try { 8 | let postsData:any = []; 9 | let nextUrl = url; 10 | let pageCount = 0; 11 | 12 | while (nextUrl) { 13 | const response = await fetch(nextUrl, { headers }); 14 | if (!response.ok) { 15 | return new Response(JSON.stringify({ error: 'Failed to fetch projects feed' }), { status: response.status }); 16 | } 17 | 18 | const posts = await response.json(); 19 | postsData = postsData.concat(posts); 20 | 21 | // Logging for debugging 22 | console.log(`Fetched ${posts.length} posts on page ${pageCount + 1}`); 23 | 24 | // Check for pagination 25 | const linkHeader = response.headers.get('link'); 26 | if (linkHeader) { 27 | const nextLink = linkHeader.split(',').find(s => s.includes('rel="next"')); 28 | nextUrl = nextLink ? nextLink.split(';')[0].replace('<', '').replace('>', '').trim() : ""; 29 | } else { 30 | nextUrl = ""; 31 | } 32 | 33 | pageCount++; 34 | } 35 | 36 | console.log(`Total posts: ${postsData.length}`); 37 | return new Response(JSON.stringify({ postsData }), { status: 200 }); 38 | } catch (error) { 39 | console.error('Error fetching projects feed:', error); 40 | return new Response(JSON.stringify({ error: 'Failed to fetch projects feed' }), { status: 500 }); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "magic-ui", 3 | "version": "0.5.1", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@hookform/resolvers": "^3.6.0", 13 | "@motionone/utils": "^10.18.0", 14 | "@radix-ui/react-dialog": "^1.0.5", 15 | "@radix-ui/react-dropdown-menu": "^2.0.6", 16 | "@radix-ui/react-icons": "^1.3.0", 17 | "@radix-ui/react-label": "^2.0.2", 18 | "@radix-ui/react-slot": "^1.0.2", 19 | "@radix-ui/react-toast": "^1.1.5", 20 | "@umami/api-client": "^0.69.0", 21 | "class-variance-authority": "^0.7.0", 22 | "clsx": "^2.1.1", 23 | "cmdk": "^1.0.0", 24 | "date-fns": "^3.6.0", 25 | "framer-motion": "^11.2.10", 26 | "gsap": "^3.12.5", 27 | "lucide-react": "^0.383.0", 28 | "next": "14.2.3", 29 | "next-themes": "^0.3.0", 30 | "pocketbase": "^0.21.3", 31 | "react": "^18", 32 | "react-day-picker": "^8.10.1", 33 | "react-dom": "^18", 34 | "react-hook-form": "^7.51.5", 35 | "react-icon-cloud": "^4.1.4", 36 | "react-icons": "^5.2.1", 37 | "recharts": "^2.12.7", 38 | "tailwind-merge": "^2.3.0", 39 | "tailwindcss-animate": "^1.0.7", 40 | "zod": "^3.23.8" 41 | }, 42 | "devDependencies": { 43 | "@types/node": "^20", 44 | "@types/react": "^18", 45 | "@types/react-dom": "^18", 46 | "cobe": "^0.6.3", 47 | "eslint": "^8", 48 | "eslint-config-next": "14.2.3", 49 | "postcss": "^8", 50 | "react-spring": "^9.7.3", 51 | "tailwindcss": "^3.4.1", 52 | "typescript": "^5" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /components/call-to-action.tsx: -------------------------------------------------------------------------------- 1 | import BoxReveal from "@/components/magicui/box-reveal"; 2 | 3 | import { Button } from "@/components/ui/button"; 4 | 5 | export async function CallToAction() { 6 | return ( 7 |
8 | 9 |

10 | Magic UI. 11 |

12 |
13 | 14 | 15 |

16 | UI library for{" "} 17 | Design Engineers 18 |

19 |
20 | 21 | 22 |
23 |

24 | -> 20+ free and open-source animated components built with 25 | React, 26 | Typescript, 27 | Tailwind CSS, 28 | and 29 | Framer Motion 30 | .
31 | -> 100% open-source, and customizable.
32 |

33 |
34 |
35 | 36 | 37 | 38 | 39 |
40 | ); 41 | } 42 | -------------------------------------------------------------------------------- /components/magicui/ripple.tsx: -------------------------------------------------------------------------------- 1 | import React, { CSSProperties } from "react"; 2 | 3 | interface RippleProps { 4 | mainCircleSize?: number; 5 | mainCircleOpacity?: number; 6 | numCircles?: number; 7 | } 8 | 9 | const Ripple = React.memo(function Ripple({ 10 | mainCircleSize = 210, 11 | mainCircleOpacity = 0.24, 12 | numCircles = 8, 13 | }: RippleProps) { 14 | return ( 15 |
16 | {Array.from({ length: numCircles }, (_, i) => { 17 | const size = mainCircleSize + i * 70; 18 | const opacity = mainCircleOpacity - i * 0.03; 19 | const animationDelay = `${i * 0.06}s`; 20 | const borderStyle = i === numCircles - 1 ? "dashed" : "solid"; 21 | const borderOpacity = 5 + i * 5; 22 | 23 | return ( 24 |
39 | ); 40 | })} 41 |
42 | ); 43 | }); 44 | 45 | Ripple.displayName = "Ripple"; 46 | 47 | export default Ripple; 48 | -------------------------------------------------------------------------------- /components/magicui/fade-in.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { motion, Variants } from "framer-motion"; 4 | import { useMemo, ReactNode } from "react"; 5 | 6 | type FadeTextProps = { 7 | className?: string; 8 | direction?: "up" | "down" | "left" | "right"; 9 | framerProps?: Variants; 10 | children: ReactNode; 11 | }; 12 | 13 | export function FadeIn({ 14 | direction = "up", 15 | className, 16 | framerProps = { 17 | hidden: { opacity: 0 }, 18 | show: { opacity: 1, transition: { type: "spring" } }, 19 | }, 20 | children, 21 | }: FadeTextProps) { 22 | const directionOffset = useMemo(() => { 23 | const map = { up: 20, down: -20, left: -20, right: 20 }; 24 | return map[direction]; 25 | }, [direction]); 26 | 27 | const axis = direction === "up" || direction === "down" ? "y" : "x"; 28 | 29 | const FADE_ANIMATION_VARIANTS = useMemo(() => { 30 | const { hidden, show, ...rest } = framerProps as { 31 | [name: string]: { [name: string]: number; opacity: number }; 32 | }; 33 | 34 | return { 35 | ...rest, 36 | hidden: { 37 | ...(hidden ?? {}), 38 | opacity: hidden?.opacity ?? 0, 39 | [axis]: hidden?.[axis] ?? directionOffset, 40 | }, 41 | show: { 42 | ...(show ?? {}), 43 | opacity: show?.opacity ?? 1, 44 | [axis]: show?.[axis] ?? 0, 45 | }, 46 | }; 47 | }, [directionOffset, axis, framerProps]); 48 | 49 | return ( 50 | 56 | {children} 57 | 58 | ); 59 | } 60 | -------------------------------------------------------------------------------- /components/magicui/number-ticker.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { cn } from "@/lib/utils"; 4 | import { useInView, useMotionValue, useSpring } from "framer-motion"; 5 | import { useEffect, useRef } from "react"; 6 | 7 | export default function NumberTicker({ 8 | value, 9 | direction = "up", 10 | delay = 0, 11 | className, 12 | }: { 13 | value: any; 14 | direction?: "up" | "down"; 15 | className?: string; 16 | delay?: number; // delay in s 17 | }) { 18 | const ref = useRef(null); 19 | const motionValue = useMotionValue(direction === "down" ? value : 0); 20 | const springValue = useSpring(motionValue, { 21 | damping: 60, 22 | stiffness: 100, 23 | }); 24 | const isInView = useInView(ref, { once: true, margin: "0px" }); 25 | 26 | useEffect(() => { 27 | isInView && 28 | setTimeout(() => { 29 | motionValue.set(direction === "down" ? 0 : value); 30 | }, delay * 1000); 31 | }, [motionValue, isInView, delay, value, direction]); 32 | 33 | useEffect( 34 | () => 35 | springValue.on("change", (latest) => { 36 | if (ref.current) { 37 | const num = Number(latest.toFixed(0)); 38 | // Format large numbers (1000+ becomes 1k, 1000000+ becomes 1m) 39 | if (num >= 1_000_000) { 40 | ref.current.textContent = (num / 1_000_000).toFixed(2).replace(/\.?0+$/, '') + 'm'; 41 | } else if (num >= 1_000) { 42 | ref.current.textContent = (num / 1_000).toFixed(2).replace(/\.?0+$/, '') + 'k'; 43 | } else { 44 | ref.current.textContent = Intl.NumberFormat("en-US").format(num); 45 | } 46 | } 47 | }), 48 | [springValue] 49 | ); 50 | 51 | return ( 52 | 53 | ); 54 | } 55 | -------------------------------------------------------------------------------- /components/technologies.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React from "react"; 4 | import { IconCloud } from "@/components/magicui/icon-cloud"; 5 | import { useRouter } from "next/navigation"; 6 | 7 | const slugs = [ 8 | "amazonaws", 9 | "apache", 10 | "apple", 11 | "archlinux", 12 | "astro", 13 | "azuredevops", 14 | "bitcoin", 15 | "digitalocean", 16 | "django", 17 | "docker", 18 | "drizzle", 19 | "ethereum", 20 | "firebase", 21 | "freebsd", 22 | "git", 23 | "github", 24 | "gitlab", 25 | "graphql", 26 | "huggingface", 27 | "jira", 28 | "javascript", 29 | "kalilinux", 30 | "linux", 31 | "linode", 32 | "mongodb", 33 | "mysql", 34 | "nextdotjs", 35 | "nginx", 36 | "nodedotjs", 37 | "numpy", 38 | "openai", 39 | "pandas", 40 | "pocketbase", 41 | "postgresql", 42 | "prisma", 43 | "python", 44 | "pytorch", 45 | "react", 46 | "redis", 47 | "solana", 48 | "square", 49 | "stripe", 50 | "svelte", 51 | "sveltekit", 52 | "tailwindcss", 53 | "tensorflow", 54 | "typescript", 55 | "ubuntu", 56 | "vercel", 57 | "zod", 58 | ]; 59 | 60 | interface TechnologiesProps { 61 | liveLinks?: boolean; 62 | } 63 | 64 | export default function Technologies({ liveLinks = false }: TechnologiesProps) { 65 | const router = useRouter(); 66 | const images = slugs.map(slug => `https://cdn.simpleicons.org/${slug}`); 67 | 68 | const handleIconClick = (index: number) => { 69 | if (!liveLinks) return; 70 | 71 | const slug = slugs[index]; 72 | if (slug) { 73 | router.push(`${process.env.NEXT_PUBLIC_PORTFOLIO_URL}/tags/${slug}`); 74 | } 75 | }; 76 | 77 | return ( 78 |
79 | 83 |
84 | ); 85 | } 86 | -------------------------------------------------------------------------------- /components/magicui/box-reveal.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { motion, useAnimation, useInView } from "framer-motion"; 4 | import { useEffect, useRef } from "react"; 5 | 6 | interface BoxRevealProps { 7 | children: JSX.Element; 8 | width?: "fit-content" | "100%"; 9 | boxColor?: string; 10 | duration?: number; 11 | } 12 | 13 | export const BoxReveal = ({ 14 | children, 15 | width = "fit-content", 16 | boxColor, 17 | duration, 18 | }: BoxRevealProps) => { 19 | const mainControls = useAnimation(); 20 | const slideControls = useAnimation(); 21 | 22 | const ref = useRef(null); 23 | const isInView = useInView(ref, { once: true }); 24 | 25 | useEffect(() => { 26 | if (isInView) { 27 | slideControls.start("visible"); 28 | mainControls.start("visible"); 29 | } else { 30 | slideControls.start("hidden"); 31 | mainControls.start("hidden"); 32 | } 33 | }, [isInView, mainControls, slideControls]); 34 | 35 | return ( 36 |
37 | 46 | {children} 47 | 48 | 49 | 67 |
68 | ); 69 | }; 70 | 71 | export default BoxReveal; 72 | -------------------------------------------------------------------------------- /components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Slot } from "@radix-ui/react-slot"; 3 | import { cva, type VariantProps } from "class-variance-authority"; 4 | 5 | import { cn } from "@/lib/utils"; 6 | 7 | const buttonVariants = cva( 8 | "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none disabled:pointer-events-none disabled:opacity-50", 9 | { 10 | variants: { 11 | variant: { 12 | default: "bg-primary text-primary-foreground hover:bg-primary/90", 13 | destructive: 14 | "bg-destructive text-destructive-foreground hover:bg-destructive/90", 15 | outline: 16 | "border border-input bg-background hover:bg-accent hover:text-accent-foreground", 17 | secondary: 18 | "bg-secondary text-secondary-foreground hover:bg-secondary/80", 19 | ghost: "hover:bg-accent hover:text-accent-foreground", 20 | link: "text-primary underline-offset-4 hover:underline", 21 | none: "text-primary", 22 | }, 23 | size: { 24 | default: "h-10 px-4 py-2", 25 | sm: "h-9 rounded-md px-3", 26 | lg: "h-11 rounded-md px-8", 27 | icon: "h-10 w-10", 28 | }, 29 | }, 30 | defaultVariants: { 31 | variant: "default", 32 | size: "default", 33 | }, 34 | } 35 | ); 36 | 37 | export interface ButtonProps 38 | extends React.ButtonHTMLAttributes, 39 | VariantProps { 40 | asChild?: boolean; 41 | } 42 | 43 | const Button = React.forwardRef( 44 | ({ className, variant, size, asChild = false, ...props }, ref) => { 45 | const Comp = asChild ? Slot : "button"; 46 | return ( 47 | 52 | ); 53 | } 54 | ); 55 | Button.displayName = "Button"; 56 | 57 | export { Button, buttonVariants }; 58 | -------------------------------------------------------------------------------- /components/magicui/text-reveal.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { cn } from "@/lib/utils"; 4 | import { motion, useScroll, useTransform } from "framer-motion"; 5 | import { FC, ReactNode, useRef } from "react"; 6 | 7 | interface TextRevealByWordProps { 8 | text: string; 9 | className?: string; 10 | } 11 | 12 | export const TextRevealByWord: FC = ({ 13 | text, 14 | className, 15 | }) => { 16 | const targetRef = useRef(null); 17 | 18 | const { scrollYProgress } = useScroll({ 19 | target: targetRef, 20 | }); 21 | const words = text.split(" "); 22 | 23 | return ( 24 |
25 |
30 |

36 | {words.map((word, i) => { 37 | const start = i / words.length; 38 | const end = start + 1 / words.length; 39 | return ( 40 | 41 | {word} 42 | 43 | ); 44 | })} 45 |

46 |
47 |
48 | ); 49 | }; 50 | 51 | interface WordProps { 52 | children: ReactNode; 53 | progress: any; 54 | range: [number, number]; 55 | } 56 | 57 | const Word: FC = ({ children, progress, range }) => { 58 | const opacity = useTransform(progress, range, [0, 1]); 59 | return ( 60 | 61 | {children} 62 | 66 | {children} 67 | 68 | 69 | ); 70 | }; 71 | 72 | export default TextRevealByWord; 73 | -------------------------------------------------------------------------------- /components/magicui/animated-subscribe-button.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { AnimatePresence, motion } from "framer-motion"; 4 | import React, { useState } from "react"; 5 | 6 | interface AnimatedSubscribeButtonProps { 7 | brand: string; 8 | subscribeStatus: boolean; 9 | buttonTextColor?: string; 10 | initialText: React.ReactElement | string; 11 | changeText: React.ReactElement | string; 12 | } 13 | 14 | export const AnimatedSubscribeButton: React.FC< 15 | AnimatedSubscribeButtonProps 16 | > = ({ brand, subscribeStatus, buttonTextColor, changeText, initialText }) => { 17 | const [isSubscribed, setIsSubscribed] = useState(subscribeStatus); 18 | 19 | return ( 20 | 21 | {isSubscribed ? ( 22 | setIsSubscribed(false)} 25 | initial={{ opacity: 0 }} 26 | animate={{ opacity: 1 }} 27 | exit={{ opacity: 0 }} 28 | > 29 | 36 | {changeText} 37 | 38 | 39 | ) : ( 40 | setIsSubscribed(true)} 44 | initial={{ opacity: 0 }} 45 | animate={{ opacity: 1 }} 46 | exit={{ opacity: 0 }} 47 | > 48 | 54 | {initialText} 55 | 56 | 57 | )} 58 | 59 | ); 60 | }; 61 | -------------------------------------------------------------------------------- /components/ui/card.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | const Card = React.forwardRef< 6 | HTMLDivElement, 7 | React.HTMLAttributes 8 | >(({ className, ...props }, ref) => ( 9 |
17 | )) 18 | Card.displayName = "Card" 19 | 20 | const CardHeader = React.forwardRef< 21 | HTMLDivElement, 22 | React.HTMLAttributes 23 | >(({ className, ...props }, ref) => ( 24 |
29 | )) 30 | CardHeader.displayName = "CardHeader" 31 | 32 | const CardTitle = React.forwardRef< 33 | HTMLParagraphElement, 34 | React.HTMLAttributes 35 | >(({ className, ...props }, ref) => ( 36 |

44 | )) 45 | CardTitle.displayName = "CardTitle" 46 | 47 | const CardDescription = React.forwardRef< 48 | HTMLParagraphElement, 49 | React.HTMLAttributes 50 | >(({ className, ...props }, ref) => ( 51 |

56 | )) 57 | CardDescription.displayName = "CardDescription" 58 | 59 | const CardContent = React.forwardRef< 60 | HTMLDivElement, 61 | React.HTMLAttributes 62 | >(({ className, ...props }, ref) => ( 63 |

64 | )) 65 | CardContent.displayName = "CardContent" 66 | 67 | const CardFooter = React.forwardRef< 68 | HTMLDivElement, 69 | React.HTMLAttributes 70 | >(({ className, ...props }, ref) => ( 71 |
76 | )) 77 | CardFooter.displayName = "CardFooter" 78 | 79 | export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } 80 | -------------------------------------------------------------------------------- /components/hero.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import MeteorShower from "@/components/magicui/meteors"; 4 | import WordPullUp from "@/components/magicui/word-pull-up"; 5 | import { Button } from "@/components/ui/button"; 6 | import { FadeIn } from "@/components/magicui/fade-in"; 7 | import { Mail, Github } from "lucide-react"; 8 | import BlurIn from "@/components/magicui/blur-in"; 9 | 10 | export default function Hero() { 11 | return ( 12 |
13 |
14 | 15 | 16 |
17 | 18 | I craft sleek, full-stack experiences that users love and developers 19 | enjoy expanding. 20 | 21 | 22 | 23 | 54 | 55 |
56 |
57 | 58 |
59 | ); 60 | } 61 | -------------------------------------------------------------------------------- /components/project-showcase.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils"; 2 | import Marquee from "@/components/magicui/marquee"; 3 | 4 | interface Project { 5 | name: string; 6 | body: string; 7 | slug: string; 8 | } 9 | 10 | interface ProjectShowcaseProps { 11 | projects: Project[]; 12 | } 13 | 14 | const ReviewCard = ({ 15 | name, 16 | body, 17 | slug, 18 | }: { 19 | name: string; 20 | body: string; 21 | slug: string; 22 | }) => { 23 | return ( 24 |
33 | 34 |
35 |
36 |
37 | {name} 38 |
39 |
40 |
41 |
42 | {body} 43 |
44 |
45 |
46 | ); 47 | }; 48 | 49 | const ProjectShowcase = ({ projects }: ProjectShowcaseProps) => { 50 | const firstRow = projects.slice(0, projects.length / 2); 51 | const secondRow = projects.slice(projects.length / 2); 52 | 53 | return ( 54 |
55 | 56 | {firstRow.map((project) => ( 57 | 58 | ))} 59 | 60 | 61 | {secondRow.map((project) => ( 62 | 63 | ))} 64 | 65 |
66 |
67 |
68 | ); 69 | }; 70 | 71 | export default ProjectShowcase; 72 | -------------------------------------------------------------------------------- /components/magicui/orbiting-circles.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef } from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | export interface OrbitingCirclesProps 6 | extends React.HTMLAttributes { 7 | className?: string 8 | children?: React.ReactNode 9 | reverse?: boolean 10 | duration?: number 11 | delay?: number 12 | radius?: number 13 | path?: boolean 14 | iconSize?: number 15 | speed?: number 16 | startAngle?: number 17 | } 18 | 19 | export function OrbitingCircles({ 20 | className, 21 | children, 22 | reverse, 23 | duration = 20, 24 | delay = 0, 25 | radius = 160, 26 | path = true, 27 | iconSize = 30, 28 | speed = 1, 29 | startAngle = 0, 30 | ...props 31 | }: OrbitingCirclesProps) { 32 | const calculatedDuration = duration / speed 33 | const divRef = useRef(null) 34 | 35 | useEffect(() => { 36 | if (!divRef.current) return 37 | 38 | let currentAngle = startAngle 39 | const step = reverse ? -0.25 : 0.25 // degrees per frame (50% slower) 40 | 41 | const animate = () => { 42 | if (!divRef.current) return 43 | currentAngle += step 44 | const transform = `rotate(${currentAngle}deg) translateY(${radius}px) rotate(-${currentAngle}deg)` 45 | divRef.current.style.transform = transform 46 | requestAnimationFrame(animate) 47 | } 48 | 49 | const animationId = requestAnimationFrame(animate) 50 | return () => cancelAnimationFrame(animationId) 51 | }, [startAngle, radius, reverse]) 52 | 53 | return ( 54 | <> 55 | {path && ( 56 | 61 | 68 | 69 | )} 70 |
85 | {children} 86 |
87 | 88 | ) 89 | } 90 | -------------------------------------------------------------------------------- /app/deployments/page.tsx: -------------------------------------------------------------------------------- 1 | import { FadeIn } from "@/components/magicui/fade-in"; 2 | import BlurIn from "@/components/magicui/blur-in"; 3 | import Globe from "@/components/magicui/globe"; 4 | 5 | export default async function DeploymentsPage() { 6 | return ( 7 |
8 | 9 | 10 |
11 |
Deployments
12 |
13 | Click on a deployment to see the project. 14 |
15 |
16 |
17 |
18 | 19 | 20 | 21 |
22 |
Cloud
23 | 24 |
25 | Cloud deployments are the most common way to deploy a project. 26 | They are easy to set up and maintain, and are generally more 27 | secure than other deployment methods. Cloud deployments are also 28 | scalable, meaning that you can easily add more resources to your 29 | project as it grows. Some popular cloud deployment platforms 30 | include AWS, Google Cloud, and Azure. 31 |
32 |
33 | 34 |
35 |
Edge
36 | 37 |
38 | Lorem sint est ipsum excepteur in Lorem occaecat labore 39 | exercitation laboris minim ea. Proident eu consectetur commodo 40 | laborum elit voluptate et adipisicing incididunt amet laboris do. 41 | Pariatur consectetur dolor aliqua labore. Sunt et exercitation 42 | fugiat ullamco non mollit dolor ullamco. 43 |
44 |
45 | 46 |
47 |
On-Prem
48 | 49 |
50 | Lorem sint est ipsum excepteur in Lorem occaecat labore 51 | exercitation laboris minim ea. Proident eu consectetur commodo 52 | laborum elit voluptate et adipisicing incididunt amet laboris do. 53 | Pariatur consectetur dolor aliqua labore. Sunt et exercitation 54 | fugiat ullamco non mollit dolor ullamco. 55 |
56 |
57 |
58 |
59 |
60 | ); 61 | } 62 | -------------------------------------------------------------------------------- /app/api/fetch-github-stars/route.ts: -------------------------------------------------------------------------------- 1 | export const dynamic = 'force-dynamic'; // defaults to auto 2 | 3 | interface Repo { 4 | stargazers_count: number; 5 | name: string; 6 | } 7 | 8 | export async function GET(request: Request): Promise { 9 | const username = "engageintellect"; 10 | const token = process.env.GITHUB_TOKEN; // Ensure you set this in your environment variables 11 | const baseUrl = `https://api.github.com/users/${username}/repos`; 12 | const headers = { 13 | "Accept": "application/vnd.github.v3+json", 14 | "Authorization": `token ${token}` 15 | }; 16 | 17 | try { 18 | if (!token) { 19 | console.error('GitHub token is missing'); 20 | return new Response(JSON.stringify({ error: 'GitHub token is missing' }), { status: 500 }); 21 | } 22 | 23 | let totalStars = 0; 24 | let nextUrl: string | null = baseUrl; 25 | let page = 1; // Start from the first page 26 | 27 | while (nextUrl) { 28 | const response: Response = await fetch(`${nextUrl}?page=${page}×tamp=${Date.now()}`, { headers }); // Cache busting 29 | if (!response.ok) { 30 | console.error(`Failed to fetch URL: ${nextUrl}, Status: ${response.status}`); 31 | return new Response(JSON.stringify({ error: `Failed to fetch repositories, Status: ${response.status}` }), { status: response.status }); 32 | } 33 | 34 | const rateLimitRemaining = response.headers.get('x-ratelimit-remaining'); 35 | const rateLimitReset = response.headers.get('x-ratelimit-reset'); 36 | if (rateLimitRemaining !== null && parseInt(rateLimitRemaining) === 0) { 37 | const resetTime = new Date(parseInt(rateLimitReset!) * 1000); 38 | console.error(`Rate limit exceeded. Try again after ${resetTime}`); 39 | return new Response(JSON.stringify({ error: 'Rate limit exceeded' }), { status: 429 }); 40 | } 41 | 42 | const repos: Repo[] = await response.json(); 43 | repos.forEach(repo => { 44 | totalStars += repo.stargazers_count; 45 | }); 46 | 47 | // Check for pagination 48 | const linkHeader: string | null = response.headers.get('link'); 49 | if (linkHeader) { 50 | const nextLink = linkHeader.split(',').find(s => s.includes('rel="next"')); 51 | if (nextLink) { 52 | const match = nextLink.match(/<([^>]+)>/); 53 | nextUrl = match ? match[1] : null; 54 | page += 1; // Increment the page number 55 | } else { 56 | nextUrl = null; 57 | } 58 | } else { 59 | nextUrl = null; 60 | } 61 | } 62 | 63 | return new Response(JSON.stringify({ totalStars }), { status: 200 }); 64 | } catch (error) { 65 | console.error('Error fetching repositories:', error); 66 | return new Response(JSON.stringify({ error: 'Failed to fetch repositories' }), { status: 500 }); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | :root { 7 | 8 | scroll-behavior: smooth; 9 | --background: 0 0% 100%; 10 | --foreground: 0 0% 3.9%; 11 | 12 | --card: 0 0% 100%; 13 | --card-foreground: 0 0% 3.9%; 14 | 15 | --popover: 0 0% 100%; 16 | --popover-foreground: 0 0% 3.9%; 17 | 18 | --primary: 0 0% 9%; 19 | --primary-foreground: 0 0% 98%; 20 | 21 | --secondary: 0 0% 96.1%; 22 | --secondary-foreground: 0 0% 9%; 23 | 24 | --muted: 0 0% 96.1%; 25 | --muted-foreground: 0 0% 45.1%; 26 | 27 | --accent: 0 0% 96.1%; 28 | --accent-foreground: 0 0% 9%; 29 | 30 | --destructive: 0 84.2% 60.2%; 31 | --destructive-foreground: 0 0% 98%; 32 | 33 | --border: 0 0% 89.8%; 34 | --input: 0 0% 89.8%; 35 | --ring: 0 0% 3.9%; 36 | 37 | --radius: 0.5rem; 38 | 39 | --chart-1: 12 76% 61%; 40 | --chart-2: 173 58% 39%; 41 | --chart-3: 197 37% 24%; 42 | --chart-4: 43 74% 66%; 43 | --chart-5: 27 87% 67%; 44 | } 45 | 46 | .dark { 47 | --background: 0 0% 10%; 48 | --foreground: 0 0% 98%; 49 | 50 | --card: 0 0% 3.9%; 51 | --card-foreground: 0 0% 98%; 52 | 53 | --popover: 0 0% 10%; 54 | --popover-foreground: 0 0% 98%; 55 | 56 | --primary: 0 0% 98%; 57 | --primary-foreground: 0 0% 9%; 58 | 59 | --secondary: 0 0% 14.9%; 60 | --secondary-foreground: 0 0% 98%; 61 | 62 | --muted: 0 0% 14.9%; 63 | --muted-foreground: 0 0% 63.9%; 64 | 65 | --accent: 0 0% 14.9%; 66 | --accent-foreground: 0 0% 98%; 67 | 68 | --destructive: 0 62.8% 30.6%; 69 | --destructive-foreground: 0 0% 98%; 70 | 71 | --border: 0 0% 14.9%; 72 | --input: 0 0% 14.9%; 73 | --ring: 0 0% 83.1%; 74 | 75 | --chart-1: 220 70% 50%; 76 | --chart-2: 160 60% 45%; 77 | --chart-3: 30 80% 55%; 78 | --chart-4: 280 65% 60%; 79 | --chart-5: 340 75% 55%; 80 | } 81 | .theme { 82 | 83 | --animate-orbit: orbit calc(var(--duration)*1s) linear infinite; 84 | 85 | } 86 | 87 | } 88 | 89 | @layer base { 90 | * { 91 | @apply border-border; 92 | } 93 | body { 94 | @apply bg-background text-foreground; 95 | } 96 | } 97 | 98 | @theme inline { 99 | @keyframes orbit { 100 | 0% { 101 | transform: rotate(calc(var(--angle) * 1deg)) translateY(calc(var(--radius) * 1px)) rotate(calc(var(--angle) * -1deg)); 102 | 103 | } 104 | 100% { 105 | transform: rotate(calc(var(--angle) * 1deg + 360deg)) translateY(calc(var(--radius) * 1px)) rotate(calc((var(--angle) * -1deg) - 360deg)); 106 | 107 | } 108 | 109 | } 110 | 111 | } -------------------------------------------------------------------------------- /components/ui/calendar.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import { ChevronLeft, ChevronRight } from "lucide-react" 5 | import { DayPicker } from "react-day-picker" 6 | 7 | import { cn } from "@/lib/utils" 8 | import { buttonVariants } from "@/components/ui/button" 9 | 10 | export type CalendarProps = React.ComponentProps 11 | 12 | function Calendar({ 13 | className, 14 | classNames, 15 | showOutsideDays = true, 16 | ...props 17 | }: CalendarProps) { 18 | return ( 19 | , 58 | IconRight: ({ ...props }) => , 59 | }} 60 | {...props} 61 | /> 62 | ) 63 | } 64 | Calendar.displayName = "Calendar" 65 | 66 | export { Calendar } 67 | -------------------------------------------------------------------------------- /app/worldwide-reach/page.tsx: -------------------------------------------------------------------------------- 1 | import { FadeIn } from "@/components/magicui/fade-in"; 2 | import BlurIn from "@/components/magicui/blur-in"; 3 | 4 | export default async function WorldwideReach() { 5 | return ( 6 |
7 | 8 | 9 |
10 |
11 | Worldwide Reach 12 |
13 |
14 | Click on a deployment to see the project. 15 |
16 |
17 |
18 |
19 | 20 | 21 | 22 |
23 |
Cloud
24 | 25 |
26 | Cloud deployments are the most common way to deploy a project. 27 | include AWS, Google Cloud, and Azure. 28 |
29 | 30 |
    31 |
  • Vercel
  • 32 |
  • AWS
  • 33 |
  • Azure
  • 34 |
  • Docker
  • 35 |
  • Linode
  • 36 |
  • Digital Ocean
  • 37 |
38 |
39 | 40 |
41 |
Edge
42 | 43 |
44 | Edge deployments leverage distributed computing to bring data and 45 | services closer to users, reducing latency and improving response 46 | times. This approach is particularly beneficial for real-time 47 | applications and services that require rapid data processing and 48 | delivery. By minimizing the distance data travels, edge 49 | deployments enhance user experience and can also reduce bandwidth 50 | costs. 51 |
52 |
53 | 54 |
55 |
On-Prem
56 | 57 |
58 | On-premises deployments are hosted locally on a company's own 59 | servers and infrastructure. This approach provides organizations 60 | with greater control over their data and applications, enabling 61 | them to meet specific security and compliance requirements. While 62 | on-premises deployments offer enhanced security and privacy, they 63 | require significant resources to maintain and manage, making them 64 | less flexible and scalable than cloud or edge deployments. 65 |
66 |
67 |
68 |
69 |
70 | ); 71 | } 72 | -------------------------------------------------------------------------------- /components/project-showcase-vertical.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils"; 2 | import Marquee from "@/components/magicui/marquee"; 3 | 4 | interface Project { 5 | name: string; 6 | body: string; 7 | slug: string; 8 | image: string; 9 | } 10 | 11 | interface ProjectShowcaseVerticalProps { 12 | projects: Project[]; 13 | } 14 | 15 | const ReviewCard = ({ 16 | name, 17 | body, 18 | slug, 19 | image, 20 | }: { 21 | name: string; 22 | body: string; 23 | slug: string; 24 | image: string; 25 | }) => { 26 | return ( 27 |
36 | 37 |
38 |
39 |
40 | {name} 45 |
46 | {name} 47 |
48 |
49 |
50 |
51 |
52 | {body} 53 |
54 |
55 |
56 | ); 57 | }; 58 | 59 | const ProjectShowcaseVertical = ({ 60 | projects, 61 | }: ProjectShowcaseVerticalProps) => { 62 | const firstRow = projects.slice(0, 5); // Get first 5 projects 63 | const secondRow = projects.slice(5, 10); // Get next 5 projects 64 | 65 | return ( 66 |
67 | 68 | {firstRow.map((project) => ( 69 | 70 | ))} 71 | 72 | 78 | {secondRow.map((project) => ( 79 | 80 | ))} 81 | 82 |
83 |
84 |
85 | ); 86 | }; 87 | 88 | export default ProjectShowcaseVertical; 89 | -------------------------------------------------------------------------------- /components/magicui/bento-grid.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from "@/components/ui/button"; 2 | import { cn } from "@/lib/utils"; 3 | import { ArrowRightIcon } from "@radix-ui/react-icons"; 4 | import { ReactNode } from "react"; 5 | import BlurIn from "@/components/magicui/blur-in"; 6 | 7 | const BentoGrid = ({ 8 | children, 9 | className, 10 | }: { 11 | children: ReactNode; 12 | className?: string; 13 | }) => { 14 | return ( 15 |
21 | {children} 22 |
23 | ); 24 | }; 25 | 26 | const BentoCard = ({ 27 | name, 28 | className, 29 | background, 30 | Icon, 31 | description, 32 | href, 33 | cta, 34 | }: { 35 | name?: string; 36 | className?: string; 37 | background?: ReactNode; 38 | Icon?: any; 39 | description?: string; 40 | href?: string; 41 | cta?: string; 42 | }) => ( 43 | 55 |
{background}
56 |
57 |
58 |
59 | {Icon !== "" ? ( 60 | 61 | ) : ( 62 | "" 63 | )} 64 |
65 | 66 |
67 | {name} 68 |
69 |
70 |

71 | {description} 72 |

73 |
74 | 75 |
80 | {href !== "" ? ( 81 | 92 | ) : ( 93 | "" 94 | )} 95 |
96 |
97 | 98 | ); 99 | 100 | export { BentoCard, BentoGrid }; 101 | -------------------------------------------------------------------------------- /components/magicui/shimmer-button.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils"; 2 | import React, { CSSProperties } from "react"; 3 | 4 | export interface ShimmerButtonProps 5 | extends React.ButtonHTMLAttributes { 6 | shimmerColor?: string; 7 | shimmerSize?: string; 8 | borderRadius?: string; 9 | shimmerDuration?: string; 10 | background?: string; 11 | className?: string; 12 | children?: React.ReactNode; 13 | } 14 | 15 | const ShimmerButton = React.forwardRef( 16 | ( 17 | { 18 | shimmerColor = "#ffffff", 19 | shimmerSize = "0.05em", 20 | shimmerDuration = "3s", 21 | borderRadius = "100px", 22 | background = "rgba(0, 0, 0, 1)", 23 | className, 24 | children, 25 | ...props 26 | }, 27 | ref 28 | ) => { 29 | return ( 30 | 89 | ); 90 | } 91 | ); 92 | 93 | ShimmerButton.displayName = "ShimmerButton"; 94 | 95 | export default ShimmerButton; 96 | -------------------------------------------------------------------------------- /components/comments.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils"; 2 | import Marquee from "@/components/magicui/marquee"; 3 | 4 | const reviews = [ 5 | { 6 | name: "Jack", 7 | username: "@jack", 8 | body: "I've never seen anything like this before. It's amazing. I love it.", 9 | img: "https://avatar.vercel.sh/jack", 10 | }, 11 | { 12 | name: "Jill", 13 | username: "@jill", 14 | body: "I don't know what to say. I'm speechless. This is amazing.", 15 | img: "https://avatar.vercel.sh/jill", 16 | }, 17 | { 18 | name: "John", 19 | username: "@john", 20 | body: "I'm at a loss for words. This is amazing. I love it.", 21 | img: "https://avatar.vercel.sh/john", 22 | }, 23 | { 24 | name: "Jane", 25 | username: "@jane", 26 | body: "I'm at a loss for words. This is amazing. I love it.", 27 | img: "https://avatar.vercel.sh/jane", 28 | }, 29 | { 30 | name: "Jenny", 31 | username: "@jenny", 32 | body: "I'm at a loss for words. This is amazing. I love it.", 33 | img: "https://avatar.vercel.sh/jenny", 34 | }, 35 | { 36 | name: "James", 37 | username: "@james", 38 | body: "I'm at a loss for words. This is amazing. I love it.", 39 | img: "https://avatar.vercel.sh/james", 40 | }, 41 | ]; 42 | 43 | const firstRow = reviews.slice(0, reviews.length / 2); 44 | const secondRow = reviews.slice(reviews.length / 2); 45 | 46 | const ReviewCard = ({ 47 | img, 48 | name, 49 | username, 50 | body, 51 | }: { 52 | img: string; 53 | name: string; 54 | username: string; 55 | body: string; 56 | }) => { 57 | return ( 58 |
67 |
68 | 69 |
70 |
71 | {name} 72 |
73 |

{username}

74 |
75 |
76 |
{body}
77 |
78 | ); 79 | }; 80 | 81 | export default function MarqueeDemo() { 82 | return ( 83 |
84 | 85 | {firstRow.map((review) => ( 86 | 87 | ))} 88 | 89 | 90 | {secondRow.map((review) => ( 91 | 92 | ))} 93 | 94 |
95 |
96 |
97 | ); 98 | } 99 | -------------------------------------------------------------------------------- /components/magicui/scroll-based-velocity.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { cn } from "@/lib/utils"; 4 | import { wrap } from "@motionone/utils"; 5 | import { 6 | motion, 7 | useAnimationFrame, 8 | useMotionValue, 9 | useScroll, 10 | useSpring, 11 | useTransform, 12 | useVelocity, 13 | } from "framer-motion"; 14 | import React, { useEffect, useRef, useState } from "react"; 15 | 16 | interface VelocityScrollProps { 17 | text: string; 18 | default_velocity?: number; 19 | className?: string; 20 | } 21 | 22 | interface ParallaxProps { 23 | children: string; 24 | baseVelocity: number; 25 | className?: string; 26 | } 27 | 28 | export function VelocityScroll({ 29 | text, 30 | default_velocity = 5, 31 | className, 32 | }: VelocityScrollProps) { 33 | function ParallaxText({ 34 | children, 35 | baseVelocity = 100, 36 | className, 37 | }: ParallaxProps) { 38 | const baseX = useMotionValue(0); 39 | const { scrollY } = useScroll(); 40 | const scrollVelocity = useVelocity(scrollY); 41 | const smoothVelocity = useSpring(scrollVelocity, { 42 | damping: 50, 43 | stiffness: 400, 44 | }); 45 | 46 | const velocityFactor = useTransform(smoothVelocity, [0, 1000], [0, 5], { 47 | clamp: false, 48 | }); 49 | 50 | const [repetitions, setRepetitions] = useState(1); 51 | const containerRef = useRef(null); 52 | const textRef = useRef(null); 53 | 54 | useEffect(() => { 55 | const calculateRepetitions = () => { 56 | if (containerRef.current && textRef.current) { 57 | const containerWidth = containerRef.current.offsetWidth; 58 | const textWidth = textRef.current.offsetWidth; 59 | const newRepetitions = Math.ceil(containerWidth / textWidth) + 2; 60 | setRepetitions(newRepetitions); 61 | } 62 | }; 63 | 64 | calculateRepetitions(); 65 | 66 | window.addEventListener("resize", calculateRepetitions); 67 | return () => window.removeEventListener("resize", calculateRepetitions); 68 | }, [children]); 69 | 70 | const x = useTransform(baseX, (v) => `${wrap(-100 / repetitions, 0, v)}%`); 71 | 72 | const directionFactor = React.useRef(1); 73 | useAnimationFrame((t, delta) => { 74 | let moveBy = directionFactor.current * baseVelocity * (delta / 1000); 75 | 76 | if (velocityFactor.get() < 0) { 77 | directionFactor.current = -1; 78 | } else if (velocityFactor.get() > 0) { 79 | directionFactor.current = 1; 80 | } 81 | 82 | moveBy += directionFactor.current * moveBy * velocityFactor.get(); 83 | 84 | baseX.set(baseX.get() + moveBy); 85 | }); 86 | 87 | return ( 88 |
92 | 93 | {Array.from({ length: repetitions }).map((_, i) => ( 94 | 95 | {children}{" "} 96 | 97 | ))} 98 | 99 |
100 | ); 101 | } 102 | 103 | return ( 104 |
105 | 106 | {text} 107 | 108 | 109 | {text} 110 | 111 |
112 | ); 113 | } 114 | -------------------------------------------------------------------------------- /components/orbit.tsx: -------------------------------------------------------------------------------- 1 | import { OrbitingCircles } from "@/components/magicui/orbiting-circles"; 2 | import { motion } from "framer-motion"; 3 | 4 | export default function Orbit() { 5 | return ( 6 |
7 | 13 | {/* Center Icon */} 14 | 15 | GitHub 16 | 17 | 18 | {/* Inner Ring - 3 icons spread out */} 19 | 20 | Python 21 | 22 | 23 | Svelte 24 | 25 | 26 | TypeScript 27 | 28 | 29 | {/* Middle Ring (reverse) - 4 icons spread out */} 30 | 31 | Vercel 32 | 33 | 34 | Ubuntu 35 | 36 | 37 | Docker 38 | 39 | 40 | Nginx 41 | 42 | 43 | {/* Outer Ring - 4 icons spread out */} 44 | 45 | FastAPI 46 | 47 | 48 | OpenAI 49 | 50 | 51 | Pocketbase 52 | 53 | 54 | React 55 | 56 | 57 |
58 | ); 59 | } 60 | -------------------------------------------------------------------------------- /components/magicui/animated-grid-pattern.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { cn } from "@/lib/utils"; 4 | import { motion } from "framer-motion"; 5 | import { useEffect, useId, useRef, useState } from "react"; 6 | 7 | interface GridPatternProps { 8 | width?: number; 9 | height?: number; 10 | x?: number; 11 | y?: number; 12 | strokeDasharray?: any; 13 | numSquares?: number; 14 | className?: string; 15 | maxOpacity?: number; 16 | duration?: number; 17 | repeatDelay?: number; 18 | } 19 | 20 | export function GridPattern({ 21 | width = 40, 22 | height = 40, 23 | x = -1, 24 | y = -1, 25 | strokeDasharray = 0, 26 | numSquares = 50, 27 | className, 28 | maxOpacity = 0.5, 29 | duration = 4, 30 | repeatDelay = 0.5, 31 | ...props 32 | }: GridPatternProps) { 33 | const id = useId(); 34 | const containerRef = useRef(null); 35 | const [dimensions, setDimensions] = useState({ width: 0, height: 0 }); 36 | const [squares, setSquares] = useState(() => generateSquares(numSquares)); 37 | 38 | function getPos() { 39 | return [ 40 | Math.floor((Math.random() * dimensions.width) / width), 41 | Math.floor((Math.random() * dimensions.height) / height), 42 | ]; 43 | } 44 | 45 | // Adjust the generateSquares function to return objects with an id, x, and y 46 | function generateSquares(count: number) { 47 | return Array.from({ length: count }, (_, i) => ({ 48 | id: i, 49 | pos: getPos(), 50 | })); 51 | } 52 | 53 | // Function to update a single square's position 54 | const updateSquarePosition = (id: number) => { 55 | setSquares((currentSquares) => 56 | currentSquares.map((sq) => 57 | sq.id === id 58 | ? { 59 | ...sq, 60 | pos: getPos(), 61 | } 62 | : sq 63 | ) 64 | ); 65 | }; 66 | 67 | // Update squares to animate in 68 | useEffect(() => { 69 | if (dimensions.width && dimensions.height) { 70 | setSquares(generateSquares(numSquares)); 71 | } 72 | }, [dimensions, numSquares]); 73 | 74 | // Resize observer to update container dimensions 75 | useEffect(() => { 76 | const resizeObserver = new ResizeObserver((entries) => { 77 | for (let entry of entries) { 78 | setDimensions({ 79 | width: entry.contentRect.width, 80 | height: entry.contentRect.height, 81 | }); 82 | } 83 | }); 84 | 85 | if (containerRef.current) { 86 | resizeObserver.observe(containerRef.current); 87 | } 88 | 89 | return () => { 90 | if (containerRef.current) { 91 | resizeObserver.unobserve(containerRef.current); 92 | } 93 | }; 94 | }, [containerRef]); 95 | 96 | return ( 97 | 146 | ); 147 | } 148 | 149 | export default GridPattern; 150 | -------------------------------------------------------------------------------- /components/ui/dialog.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as DialogPrimitive from "@radix-ui/react-dialog" 5 | import { X } from "lucide-react" 6 | 7 | import { cn } from "@/lib/utils" 8 | 9 | const Dialog = DialogPrimitive.Root 10 | 11 | const DialogTrigger = DialogPrimitive.Trigger 12 | 13 | const DialogPortal = DialogPrimitive.Portal 14 | 15 | const DialogClose = DialogPrimitive.Close 16 | 17 | const DialogOverlay = React.forwardRef< 18 | React.ElementRef, 19 | React.ComponentPropsWithoutRef 20 | >(({ className, ...props }, ref) => ( 21 | 29 | )) 30 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName 31 | 32 | const DialogContent = React.forwardRef< 33 | React.ElementRef, 34 | React.ComponentPropsWithoutRef 35 | >(({ className, children, ...props }, ref) => ( 36 | 37 | 38 | 46 | {children} 47 | 48 | 49 | Close 50 | 51 | 52 | 53 | )) 54 | DialogContent.displayName = DialogPrimitive.Content.displayName 55 | 56 | const DialogHeader = ({ 57 | className, 58 | ...props 59 | }: React.HTMLAttributes) => ( 60 |
67 | ) 68 | DialogHeader.displayName = "DialogHeader" 69 | 70 | const DialogFooter = ({ 71 | className, 72 | ...props 73 | }: React.HTMLAttributes) => ( 74 |
81 | ) 82 | DialogFooter.displayName = "DialogFooter" 83 | 84 | const DialogTitle = React.forwardRef< 85 | React.ElementRef, 86 | React.ComponentPropsWithoutRef 87 | >(({ className, ...props }, ref) => ( 88 | 96 | )) 97 | DialogTitle.displayName = DialogPrimitive.Title.displayName 98 | 99 | const DialogDescription = React.forwardRef< 100 | React.ElementRef, 101 | React.ComponentPropsWithoutRef 102 | >(({ className, ...props }, ref) => ( 103 | 108 | )) 109 | DialogDescription.displayName = DialogPrimitive.Description.displayName 110 | 111 | export { 112 | Dialog, 113 | DialogPortal, 114 | DialogOverlay, 115 | DialogClose, 116 | DialogTrigger, 117 | DialogContent, 118 | DialogHeader, 119 | DialogFooter, 120 | DialogTitle, 121 | DialogDescription, 122 | } 123 | -------------------------------------------------------------------------------- /components/email-form.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { zodResolver } from "@hookform/resolvers/zod"; 4 | import { useForm } from "react-hook-form"; 5 | import { z } from "zod"; 6 | import { Button } from "@/components/ui/button"; 7 | import { 8 | Form, 9 | FormControl, 10 | FormField, 11 | FormItem, 12 | FormMessage, 13 | } from "@/components/ui/form"; 14 | import { Input } from "@/components/ui/input"; 15 | import { toast } from "@/components/ui/use-toast"; 16 | import PocketBase from "pocketbase"; 17 | import { SendHorizonal, LoaderCircle } from "lucide-react"; 18 | 19 | import { useState } from "react"; 20 | 21 | // Initialize PocketBase client 22 | const pb = new PocketBase(process.env.NEXT_PUBLIC_POCKETBASE_URL); 23 | 24 | // Define the form schema with email validation 25 | const FormSchema = z.object({ 26 | email: z 27 | .string() 28 | .email({ message: "Please enter a valid email address." }) 29 | .transform((email) => email.toLowerCase()), 30 | }); 31 | 32 | export function EmailForm() { 33 | const [isLoading, setIsLoading] = useState(false); 34 | const form = useForm>({ 35 | resolver: zodResolver(FormSchema), 36 | defaultValues: { 37 | email: "", 38 | }, 39 | }); 40 | 41 | async function onSubmit(data: z.infer) { 42 | setIsLoading(true); // Set loading state to true 43 | try { 44 | // wait for half a second to show loading animation 45 | await new Promise((resolve) => setTimeout(resolve, 500)); 46 | const record = await pb.collection("cook_form_submissions").create(data); 47 | toast({ 48 | variant: "success", 49 | title: "Email submitted successfully!", 50 | description: ( 51 | <> 52 |
 53 |               
 54 |                 {record.email}
 55 |               
 56 |             
57 |

58 | We'll be in touch soon. 59 |

60 | 61 | ), 62 | }); 63 | form.reset(); // Reset form on successful submission 64 | } catch (error) { 65 | console.error("Error creating record:", error); 66 | toast({ 67 | variant: "destructive", 68 | title: "Failed to submit email. It may already exist.", 69 | description: "Please try again.", 70 | }); 71 | } finally { 72 | setIsLoading(false); // Set loading state to false after the request is complete 73 | } 74 | } 75 | 76 | return ( 77 |
78 | 79 |
80 | ( 84 | 85 | 86 | 91 | 92 |
93 | 94 |
95 |
96 | )} 97 | /> 98 | 99 | 117 |
118 |
119 | 120 | ); 121 | } 122 | -------------------------------------------------------------------------------- /components/ui/use-toast.ts: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | // Inspired by react-hot-toast library 4 | import * as React from "react" 5 | 6 | import type { 7 | ToastActionElement, 8 | ToastProps, 9 | } from "@/components/ui/toast" 10 | 11 | const TOAST_LIMIT = 1 12 | const TOAST_REMOVE_DELAY = 1000000 13 | 14 | type ToasterToast = ToastProps & { 15 | id: string 16 | title?: React.ReactNode 17 | description?: React.ReactNode 18 | action?: ToastActionElement 19 | } 20 | 21 | const actionTypes = { 22 | ADD_TOAST: "ADD_TOAST", 23 | UPDATE_TOAST: "UPDATE_TOAST", 24 | DISMISS_TOAST: "DISMISS_TOAST", 25 | REMOVE_TOAST: "REMOVE_TOAST", 26 | } as const 27 | 28 | let count = 0 29 | 30 | function genId() { 31 | count = (count + 1) % Number.MAX_SAFE_INTEGER 32 | return count.toString() 33 | } 34 | 35 | type ActionType = typeof actionTypes 36 | 37 | type Action = 38 | | { 39 | type: ActionType["ADD_TOAST"] 40 | toast: ToasterToast 41 | } 42 | | { 43 | type: ActionType["UPDATE_TOAST"] 44 | toast: Partial 45 | } 46 | | { 47 | type: ActionType["DISMISS_TOAST"] 48 | toastId?: ToasterToast["id"] 49 | } 50 | | { 51 | type: ActionType["REMOVE_TOAST"] 52 | toastId?: ToasterToast["id"] 53 | } 54 | 55 | interface State { 56 | toasts: ToasterToast[] 57 | } 58 | 59 | const toastTimeouts = new Map>() 60 | 61 | const addToRemoveQueue = (toastId: string) => { 62 | if (toastTimeouts.has(toastId)) { 63 | return 64 | } 65 | 66 | const timeout = setTimeout(() => { 67 | toastTimeouts.delete(toastId) 68 | dispatch({ 69 | type: "REMOVE_TOAST", 70 | toastId: toastId, 71 | }) 72 | }, TOAST_REMOVE_DELAY) 73 | 74 | toastTimeouts.set(toastId, timeout) 75 | } 76 | 77 | export const reducer = (state: State, action: Action): State => { 78 | switch (action.type) { 79 | case "ADD_TOAST": 80 | return { 81 | ...state, 82 | toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT), 83 | } 84 | 85 | case "UPDATE_TOAST": 86 | return { 87 | ...state, 88 | toasts: state.toasts.map((t) => 89 | t.id === action.toast.id ? { ...t, ...action.toast } : t 90 | ), 91 | } 92 | 93 | case "DISMISS_TOAST": { 94 | const { toastId } = action 95 | 96 | // ! Side effects ! - This could be extracted into a dismissToast() action, 97 | // but I'll keep it here for simplicity 98 | if (toastId) { 99 | addToRemoveQueue(toastId) 100 | } else { 101 | state.toasts.forEach((toast) => { 102 | addToRemoveQueue(toast.id) 103 | }) 104 | } 105 | 106 | return { 107 | ...state, 108 | toasts: state.toasts.map((t) => 109 | t.id === toastId || toastId === undefined 110 | ? { 111 | ...t, 112 | open: false, 113 | } 114 | : t 115 | ), 116 | } 117 | } 118 | case "REMOVE_TOAST": 119 | if (action.toastId === undefined) { 120 | return { 121 | ...state, 122 | toasts: [], 123 | } 124 | } 125 | return { 126 | ...state, 127 | toasts: state.toasts.filter((t) => t.id !== action.toastId), 128 | } 129 | } 130 | } 131 | 132 | const listeners: Array<(state: State) => void> = [] 133 | 134 | let memoryState: State = { toasts: [] } 135 | 136 | function dispatch(action: Action) { 137 | memoryState = reducer(memoryState, action) 138 | listeners.forEach((listener) => { 139 | listener(memoryState) 140 | }) 141 | } 142 | 143 | type Toast = Omit 144 | 145 | function toast({ ...props }: Toast) { 146 | const id = genId() 147 | 148 | const update = (props: ToasterToast) => 149 | dispatch({ 150 | type: "UPDATE_TOAST", 151 | toast: { ...props, id }, 152 | }) 153 | const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id }) 154 | 155 | dispatch({ 156 | type: "ADD_TOAST", 157 | toast: { 158 | ...props, 159 | id, 160 | open: true, 161 | onOpenChange: (open) => { 162 | if (!open) dismiss() 163 | }, 164 | }, 165 | }) 166 | 167 | return { 168 | id: id, 169 | dismiss, 170 | update, 171 | } 172 | } 173 | 174 | function useToast() { 175 | const [state, setState] = React.useState(memoryState) 176 | 177 | React.useEffect(() => { 178 | listeners.push(setState) 179 | return () => { 180 | const index = listeners.indexOf(setState) 181 | if (index > -1) { 182 | listeners.splice(index, 1) 183 | } 184 | } 185 | }, [state]) 186 | 187 | return { 188 | ...state, 189 | toast, 190 | dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }), 191 | } 192 | } 193 | 194 | export { useToast, toast } 195 | -------------------------------------------------------------------------------- /tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "tailwindcss" 2 | 3 | const config = { 4 | darkMode: ["class"], 5 | content: [ 6 | './pages/**/*.{ts,tsx}', 7 | './components/**/*.{ts,tsx}', 8 | './app/**/*.{ts,tsx}', 9 | './src/**/*.{ts,tsx}', 10 | ], 11 | prefix: "", 12 | theme: { 13 | container: { 14 | center: true, 15 | padding: "2rem", 16 | screens: { 17 | "2xl": "1400px", 18 | }, 19 | }, 20 | extend: { 21 | colors: { 22 | border: "hsl(var(--border))", 23 | input: "hsl(var(--input))", 24 | ring: "hsl(var(--ring))", 25 | background: "hsl(var(--background))", 26 | foreground: "hsl(var(--foreground))", 27 | primary: { 28 | DEFAULT: "hsl(var(--primary))", 29 | foreground: "hsl(var(--primary-foreground))", 30 | }, 31 | secondary: { 32 | DEFAULT: "hsl(var(--secondary))", 33 | foreground: "hsl(var(--secondary-foreground))", 34 | }, 35 | destructive: { 36 | DEFAULT: "hsl(var(--destructive))", 37 | foreground: "hsl(var(--destructive-foreground))", 38 | }, 39 | muted: { 40 | DEFAULT: "hsl(var(--muted))", 41 | foreground: "hsl(var(--muted-foreground))", 42 | }, 43 | accent: { 44 | DEFAULT: "hsl(var(--accent))", 45 | foreground: "hsl(var(--accent-foreground))", 46 | }, 47 | popover: { 48 | DEFAULT: "hsl(var(--popover))", 49 | foreground: "hsl(var(--popover-foreground))", 50 | }, 51 | card: { 52 | DEFAULT: "hsl(var(--card))", 53 | foreground: "hsl(var(--card-foreground))", 54 | }, 55 | }, 56 | borderRadius: { 57 | lg: "var(--radius)", 58 | md: "calc(var(--radius) - 2px)", 59 | sm: "calc(var(--radius) - 4px)", 60 | }, 61 | keyframes: { 62 | "accordion-down": { 63 | from: { height: "0" }, 64 | to: { height: "var(--radix-accordion-content-height)" }, 65 | }, 66 | "accordion-up": { 67 | from: { height: "var(--radix-accordion-content-height)" }, 68 | to: { height: "0" }, 69 | }, 70 | 71 | "spin-around": { 72 | "0%": { 73 | transform: "translateZ(0) rotate(0)", 74 | }, 75 | "15%, 35%": { 76 | transform: "translateZ(0) rotate(90deg)", 77 | }, 78 | "65%, 85%": { 79 | transform: "translateZ(0) rotate(270deg)", 80 | }, 81 | "100%": { 82 | transform: "translateZ(0) rotate(360deg)", 83 | }, 84 | }, 85 | slide: { 86 | to: { 87 | transform: "translate(calc(100cqw - 100%), 0)", 88 | }, 89 | }, 90 | 91 | marquee: { 92 | from: { transform: "translateX(0)" }, 93 | to: { transform: "translateX(calc(-100% - var(--gap)))" }, 94 | }, 95 | "marquee-vertical": { 96 | from: { transform: "translateY(0)" }, 97 | to: { transform: "translateY(calc(-100% - var(--gap)))" }, 98 | }, 99 | 100 | meteor: { 101 | "0%": { transform: "rotate(215deg) translateX(0)", opacity: '1' }, 102 | "70%": { opacity: '1' }, 103 | "100%": { 104 | transform: "rotate(215deg) translateX(-500px)", 105 | opacity: '0', 106 | }, 107 | }, 108 | 109 | orbit: { 110 | "0%": { 111 | transform: "rotate(calc(var(--angle) * 1deg)) translateY(calc(var(--radius) * 1px)) rotate(calc(var(--angle) * -1deg))", 112 | }, 113 | "100%": { 114 | transform: "rotate(calc(var(--angle) * 1deg + 360deg)) translateY(calc(var(--radius) * 1px)) rotate(calc(var(--angle) * -1deg - 360deg))", 115 | }, 116 | }, 117 | 118 | grid: { 119 | "0%": { transform: "translateY(-50%)" }, 120 | "100%": { transform: "translateY(0)" }, 121 | }, 122 | 123 | ripple: { 124 | "0%, 100%": { 125 | transform: "translate(-50%, -50%) scale(1)", 126 | }, 127 | "50%": { 128 | transform: "translate(-50%, -50%) scale(0.9)", 129 | }, 130 | }, 131 | }, 132 | animation: { 133 | "accordion-down": "accordion-down 0.2s ease-out", 134 | "accordion-up": "accordion-up 0.2s ease-out", 135 | "spin-around": "spin-around calc(var(--speed) * 2) infinite linear", 136 | slide: "slide var(--speed) ease-in-out infinite alternate", 137 | marquee: "marquee var(--duration) linear infinite", 138 | "marquee-vertical": "marquee-vertical var(--duration) linear infinite", 139 | meteor: "meteor 5s linear infinite", 140 | orbit: "orbit calc(var(--duration)*1s) linear infinite", 141 | grid: "grid 15s linear infinite", 142 | ripple: "ripple 3400ms ease infinite", 143 | 144 | 145 | 146 | 147 | 148 | }, 149 | }, 150 | }, 151 | plugins: [require("tailwindcss-animate")], 152 | } satisfies Config 153 | 154 | export default config -------------------------------------------------------------------------------- /components/ui/form.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import * as LabelPrimitive from "@radix-ui/react-label"; 3 | import { Slot } from "@radix-ui/react-slot"; 4 | import { 5 | Controller, 6 | ControllerProps, 7 | FieldPath, 8 | FieldValues, 9 | FormProvider, 10 | useFormContext, 11 | } from "react-hook-form"; 12 | 13 | import { cn } from "@/lib/utils"; 14 | import { Label } from "@/components/ui/label"; 15 | import BlurIn from "@/components/magicui/blur-in"; 16 | import { FadeIn } from "@/components/magicui/fade-in"; 17 | 18 | const Form = FormProvider; 19 | 20 | type FormFieldContextValue< 21 | TFieldValues extends FieldValues = FieldValues, 22 | TName extends FieldPath = FieldPath 23 | > = { 24 | name: TName; 25 | }; 26 | 27 | const FormFieldContext = React.createContext( 28 | {} as FormFieldContextValue 29 | ); 30 | 31 | const FormField = < 32 | TFieldValues extends FieldValues = FieldValues, 33 | TName extends FieldPath = FieldPath 34 | >({ 35 | ...props 36 | }: ControllerProps) => { 37 | return ( 38 | 39 | 40 | 41 | ); 42 | }; 43 | 44 | const useFormField = () => { 45 | const fieldContext = React.useContext(FormFieldContext); 46 | const itemContext = React.useContext(FormItemContext); 47 | const { getFieldState, formState } = useFormContext(); 48 | 49 | const fieldState = getFieldState(fieldContext.name, formState); 50 | 51 | if (!fieldContext) { 52 | throw new Error("useFormField should be used within "); 53 | } 54 | 55 | const { id } = itemContext; 56 | 57 | return { 58 | id, 59 | name: fieldContext.name, 60 | formItemId: `${id}-form-item`, 61 | formDescriptionId: `${id}-form-item-description`, 62 | formMessageId: `${id}-form-item-message`, 63 | ...fieldState, 64 | }; 65 | }; 66 | 67 | type FormItemContextValue = { 68 | id: string; 69 | }; 70 | 71 | const FormItemContext = React.createContext( 72 | {} as FormItemContextValue 73 | ); 74 | 75 | const FormItem = React.forwardRef< 76 | HTMLDivElement, 77 | React.HTMLAttributes 78 | >(({ className, ...props }, ref) => { 79 | const id = React.useId(); 80 | 81 | return ( 82 | 83 |
84 | 85 | ); 86 | }); 87 | FormItem.displayName = "FormItem"; 88 | 89 | const FormLabel = React.forwardRef< 90 | React.ElementRef, 91 | React.ComponentPropsWithoutRef 92 | >(({ className, ...props }, ref) => { 93 | const { error, formItemId } = useFormField(); 94 | 95 | return ( 96 |