├── .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 |
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 |
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 |
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 |
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 |
22 |
23 |
24 | Toggle theme
25 |
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 | Explore
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 |
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 |
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 |
49 | {/* spark container */}
50 |
56 | {/* spark */}
57 |
58 | {/* spark before */}
59 |
60 |
61 |
62 | {children}
63 |
64 | {/* Highlight */}
65 |
81 |
82 | {/* backdrop */}
83 |
88 |
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 |
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 |
106 |
107 |
115 |
120 |
121 |
122 |
123 |
124 | {squares.map(({ pos: [x, y], id }, index) => (
125 | updateSquarePosition(id)}
135 | key={`${x}-${y}-${index}`}
136 | width={width - 1}
137 | height={height - 1}
138 | x={x * width + 1}
139 | y={y * height + 1}
140 | fill="currentColor"
141 | strokeWidth="0"
142 | />
143 | ))}
144 |
145 |
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 |
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 |
102 | );
103 | });
104 | FormLabel.displayName = "FormLabel";
105 |
106 | const FormControl = React.forwardRef<
107 | React.ElementRef,
108 | React.ComponentPropsWithoutRef
109 | >(({ ...props }, ref) => {
110 | const { error, formItemId, formDescriptionId, formMessageId } =
111 | useFormField();
112 |
113 | return (
114 |
125 | );
126 | });
127 | FormControl.displayName = "FormControl";
128 |
129 | const FormDescription = React.forwardRef<
130 | HTMLParagraphElement,
131 | React.HTMLAttributes
132 | >(({ className, ...props }, ref) => {
133 | const { formDescriptionId } = useFormField();
134 |
135 | return (
136 |
142 | );
143 | });
144 | FormDescription.displayName = "FormDescription";
145 |
146 | const FormMessage = React.forwardRef<
147 | HTMLParagraphElement,
148 | React.HTMLAttributes
149 | >(({ className, children, ...props }, ref) => {
150 | const { error, formMessageId } = useFormField();
151 | const body = error ? String(error?.message) : children;
152 |
153 | if (!body) {
154 | return null;
155 | }
156 |
157 | return (
158 |
159 |
165 | {body}
166 |
167 |
168 | );
169 | });
170 | FormMessage.displayName = "FormMessage";
171 |
172 | export {
173 | useFormField,
174 | Form,
175 | FormItem,
176 | FormLabel,
177 | FormControl,
178 | FormDescription,
179 | FormMessage,
180 | FormField,
181 | };
182 |
--------------------------------------------------------------------------------
/components/ui/toast.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import * as ToastPrimitives from "@radix-ui/react-toast";
5 | import { cva, type VariantProps } from "class-variance-authority";
6 | import { X } from "lucide-react";
7 |
8 | import { cn } from "@/lib/utils";
9 |
10 | const ToastProvider = ToastPrimitives.Provider;
11 |
12 | const ToastViewport = React.forwardRef<
13 | React.ElementRef,
14 | React.ComponentPropsWithoutRef
15 | >(({ className, ...props }, ref) => (
16 |
24 | ));
25 | ToastViewport.displayName = ToastPrimitives.Viewport.displayName;
26 |
27 | const toastVariants = cva(
28 | "group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
29 | {
30 | variants: {
31 | variant: {
32 | default: "border bg-background text-foreground",
33 | success: "group border-emerald-500 bg-emerald-500 text-white",
34 | destructive:
35 | "destructive group border-destructive bg-destructive text-destructive-foreground",
36 | },
37 | },
38 | defaultVariants: {
39 | variant: "default",
40 | },
41 | }
42 | );
43 |
44 | const Toast = React.forwardRef<
45 | React.ElementRef,
46 | React.ComponentPropsWithoutRef &
47 | VariantProps
48 | >(({ className, variant, ...props }, ref) => {
49 | return (
50 |
55 | );
56 | });
57 | Toast.displayName = ToastPrimitives.Root.displayName;
58 |
59 | const ToastAction = React.forwardRef<
60 | React.ElementRef,
61 | React.ComponentPropsWithoutRef
62 | >(({ className, ...props }, ref) => (
63 |
71 | ));
72 | ToastAction.displayName = ToastPrimitives.Action.displayName;
73 |
74 | const ToastClose = React.forwardRef<
75 | React.ElementRef,
76 | React.ComponentPropsWithoutRef
77 | >(({ className, ...props }, ref) => (
78 |
87 |
88 |
89 | ));
90 | ToastClose.displayName = ToastPrimitives.Close.displayName;
91 |
92 | const ToastTitle = React.forwardRef<
93 | React.ElementRef,
94 | React.ComponentPropsWithoutRef
95 | >(({ className, ...props }, ref) => (
96 |
101 | ));
102 | ToastTitle.displayName = ToastPrimitives.Title.displayName;
103 |
104 | const ToastDescription = React.forwardRef<
105 | React.ElementRef,
106 | React.ComponentPropsWithoutRef
107 | >(({ className, ...props }, ref) => (
108 |
113 | ));
114 | ToastDescription.displayName = ToastPrimitives.Description.displayName;
115 |
116 | type ToastProps = React.ComponentPropsWithoutRef;
117 |
118 | type ToastActionElement = React.ReactElement;
119 |
120 | export {
121 | type ToastProps,
122 | type ToastActionElement,
123 | ToastProvider,
124 | ToastViewport,
125 | Toast,
126 | ToastTitle,
127 | ToastDescription,
128 | ToastClose,
129 | ToastAction,
130 | };
131 |
--------------------------------------------------------------------------------
/components/ui/command.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import { type DialogProps } from "@radix-ui/react-dialog"
5 | import { Command as CommandPrimitive } from "cmdk"
6 | import { Search } from "lucide-react"
7 |
8 | import { cn } from "@/lib/utils"
9 | import { Dialog, DialogContent } from "@/components/ui/dialog"
10 |
11 | const Command = React.forwardRef<
12 | React.ElementRef,
13 | React.ComponentPropsWithoutRef
14 | >(({ className, ...props }, ref) => (
15 |
23 | ))
24 | Command.displayName = CommandPrimitive.displayName
25 |
26 | interface CommandDialogProps extends DialogProps {}
27 |
28 | const CommandDialog = ({ children, ...props }: CommandDialogProps) => {
29 | return (
30 |
31 |
32 |
33 | {children}
34 |
35 |
36 |
37 | )
38 | }
39 |
40 | const CommandInput = React.forwardRef<
41 | React.ElementRef,
42 | React.ComponentPropsWithoutRef
43 | >(({ className, ...props }, ref) => (
44 |
45 |
46 |
54 |
55 | ))
56 |
57 | CommandInput.displayName = CommandPrimitive.Input.displayName
58 |
59 | const CommandList = React.forwardRef<
60 | React.ElementRef,
61 | React.ComponentPropsWithoutRef
62 | >(({ className, ...props }, ref) => (
63 |
68 | ))
69 |
70 | CommandList.displayName = CommandPrimitive.List.displayName
71 |
72 | const CommandEmpty = React.forwardRef<
73 | React.ElementRef,
74 | React.ComponentPropsWithoutRef
75 | >((props, ref) => (
76 |
81 | ))
82 |
83 | CommandEmpty.displayName = CommandPrimitive.Empty.displayName
84 |
85 | const CommandGroup = React.forwardRef<
86 | React.ElementRef,
87 | React.ComponentPropsWithoutRef
88 | >(({ className, ...props }, ref) => (
89 |
97 | ))
98 |
99 | CommandGroup.displayName = CommandPrimitive.Group.displayName
100 |
101 | const CommandSeparator = React.forwardRef<
102 | React.ElementRef,
103 | React.ComponentPropsWithoutRef
104 | >(({ className, ...props }, ref) => (
105 |
110 | ))
111 | CommandSeparator.displayName = CommandPrimitive.Separator.displayName
112 |
113 | const CommandItem = React.forwardRef<
114 | React.ElementRef,
115 | React.ComponentPropsWithoutRef
116 | >(({ className, ...props }, ref) => (
117 |
125 | ))
126 |
127 | CommandItem.displayName = CommandPrimitive.Item.displayName
128 |
129 | const CommandShortcut = ({
130 | className,
131 | ...props
132 | }: React.HTMLAttributes) => {
133 | return (
134 |
141 | )
142 | }
143 | CommandShortcut.displayName = "CommandShortcut"
144 |
145 | export {
146 | Command,
147 | CommandDialog,
148 | CommandInput,
149 | CommandList,
150 | CommandEmpty,
151 | CommandGroup,
152 | CommandItem,
153 | CommandShortcut,
154 | CommandSeparator,
155 | }
156 |
--------------------------------------------------------------------------------
/components/magicui/animated-beam.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { cn } from "@/lib/utils";
4 | import { motion } from "framer-motion";
5 | import { RefObject, useEffect, useId, useState } from "react";
6 |
7 | export interface AnimatedBeamProps {
8 | className?: string;
9 | containerRef: RefObject; // Container ref
10 | fromRef: RefObject;
11 | toRef: RefObject;
12 | curvature?: number;
13 | reverse?: boolean;
14 | pathColor?: string;
15 | pathWidth?: number;
16 | pathOpacity?: number;
17 | gradientStartColor?: string;
18 | gradientStopColor?: string;
19 | delay?: number;
20 | duration?: number;
21 | startXOffset?: number;
22 | startYOffset?: number;
23 | endXOffset?: number;
24 | endYOffset?: number;
25 | }
26 |
27 | export const AnimatedBeam: React.FC = ({
28 | className,
29 | containerRef,
30 | fromRef,
31 | toRef,
32 | curvature = 0,
33 | reverse = false, // Include the reverse prop
34 | duration = Math.random() * 3 + 4,
35 | delay = 0,
36 | pathColor = "gray",
37 | pathWidth = 2,
38 | pathOpacity = 0.2,
39 | gradientStartColor = "#ffaa40",
40 | gradientStopColor = "#9c40ff",
41 | startXOffset = 0,
42 | startYOffset = 0,
43 | endXOffset = 0,
44 | endYOffset = 0,
45 | }) => {
46 | const id = useId();
47 | const [pathD, setPathD] = useState("");
48 | const [svgDimensions, setSvgDimensions] = useState({ width: 0, height: 0 });
49 |
50 | // Calculate the gradient coordinates based on the reverse prop
51 | const gradientCoordinates = reverse
52 | ? {
53 | x1: ["90%", "-10%"],
54 | x2: ["100%", "0%"],
55 | y1: ["0%", "0%"],
56 | y2: ["0%", "0%"],
57 | }
58 | : {
59 | x1: ["10%", "110%"],
60 | x2: ["0%", "100%"],
61 | y1: ["0%", "0%"],
62 | y2: ["0%", "0%"],
63 | };
64 |
65 | useEffect(() => {
66 | const updatePath = () => {
67 | if (containerRef.current && fromRef.current && toRef.current) {
68 | const containerRect = containerRef.current.getBoundingClientRect();
69 | const rectA = fromRef.current.getBoundingClientRect();
70 | const rectB = toRef.current.getBoundingClientRect();
71 |
72 | const svgWidth = containerRect.width;
73 | const svgHeight = containerRect.height;
74 | setSvgDimensions({ width: svgWidth, height: svgHeight });
75 |
76 | const startX =
77 | rectA.left - containerRect.left + rectA.width / 2 + startXOffset;
78 | const startY =
79 | rectA.top - containerRect.top + rectA.height / 2 + startYOffset;
80 | const endX =
81 | rectB.left - containerRect.left + rectB.width / 2 + endXOffset;
82 | const endY =
83 | rectB.top - containerRect.top + rectB.height / 2 + endYOffset;
84 |
85 | const controlY = startY - curvature;
86 | const d = `M ${startX},${startY} Q ${
87 | (startX + endX) / 2
88 | },${controlY} ${endX},${endY}`;
89 | setPathD(d);
90 | }
91 | };
92 |
93 | // Initialize ResizeObserver
94 | const resizeObserver = new ResizeObserver((entries) => {
95 | // For all entries, recalculate the path
96 | for (let entry of entries) {
97 | updatePath();
98 | }
99 | });
100 |
101 | // Observe the container element
102 | if (containerRef.current) {
103 | resizeObserver.observe(containerRef.current);
104 | }
105 |
106 | // Call the updatePath initially to set the initial path
107 | updatePath();
108 |
109 | // Clean up the observer on component unmount
110 | return () => {
111 | resizeObserver.disconnect();
112 | };
113 | }, [
114 | containerRef,
115 | fromRef,
116 | toRef,
117 | curvature,
118 | startXOffset,
119 | startYOffset,
120 | endXOffset,
121 | endYOffset,
122 | ]);
123 |
124 | return (
125 |
136 |
143 |
150 |
151 |
175 |
176 |
177 |
178 |
183 |
184 |
185 |
186 | );
187 | };
188 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # cook
2 |
3 | ## Description:
4 |
5 | An ultra-modern, bento-box styled portfolio landing page for developers, designers, and other creatives.
6 |
7 | [](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fengageintellect%2Fcook.git)
8 | [](https://cook.engage-dev.com)
9 |
10 | **NOTE:** If you would like to contribute, please check out the issues tab for a list of tasks that need to be completed.
11 |
12 | ## Current Lighthouse Scores:
13 |
14 | If you would like to help improve the performance, accessibility, best practices, and SEO of this project, please check out the issues tab for a list of tasks that need to be completed.
15 |
16 | | Metric | Score |
17 | | -------------- | -------------------------------------------- |
18 | | Performance | 98% <-- help improve this by submitting a PR |
19 | | Accessibility | 100% |
20 | | Best Practices | 100% |
21 | | SEO | 100% |
22 |
23 | ## Technologies:
24 |
25 | | Name | Description |
26 | | -------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- |
27 | | [next.js](https://nextjs.org/) | React framework |
28 | | [magic-ui](https://magicui.design) | A modern, minimalistic UI library |
29 | | [shadcn/ui](https://ui.shadcn.com/) | A modern, minimalistic UI library |
30 | | [tailwind css](https://tailwindcss.com) | A utility-first CSS framework |
31 | | [zod](https://zod.dev) | TypeScript-first schema declaration and validation |
32 | | [pocketbase](https://pocketbase.io) | A modern, minimalistic database |
33 | | [react-hook-form](https://www.react-hook-form.com/) | Performant, flexible and extensible forms with easy-to-use validation |
34 | | [github public api](https://docs.github.com/en/rest?apiVersion=2022-11-28) | A REST API for accessing public Github repo, star, and user image data |
35 | | [vercel](https://vercel.com) | Deploy web projects with ease |
36 | | [umami analytics](https://umami.is/) | A simple, fast, and privacy-focused website analytics alternative to Google Analytics |
37 |
38 | ## Getting Started
39 |
40 | ### Pocketbase Setup
41 |
42 | First, we need to install Pocketbase. You can download the latest release from the [Pocketbase GitHub releases page](https://github.com/pocketbase/pocketbase/releases)
43 |
44 | ```bash
45 | wget https://github.com/pocketbase/pocketbase/releases/download/v0.8.0/pocketbase_0.8.0_linux_amd64.zip
46 | unzip pocketbase_0.8.0_linux_amd64.zip
47 | ```
48 |
49 | ```bash
50 | chmod +x pocketbase
51 | ```
52 |
53 | ```bash
54 | ./pocketbase serve
55 | ```
56 |
57 | Go to [http://localhost:8080](http://localhost:8080) to see the Pocketbase dashboard. From there, you can import the schema from the `/pb/pb_schema.json` file in the root of this repository by using the "import collections" tab in the settings menu.
58 |
59 | ### Client Setup
60 |
61 | ```bash
62 | git clone https://github.com/engageintellect/cook.git
63 | cd cook
64 | ```
65 |
66 | Now, let's set our environment variables. Copy `/.env.example` to either `.env` (for prod) or `.env.local` (for dev) in the root of the project and add replace the values with your own.:
67 |
68 | Finally, we can install the dependencies and start the development server:
69 |
70 | ### Run the development server:
71 |
72 | ```bash
73 | pnpm i && pnpm run dev
74 | ```
75 |
76 | ### Umami Analytics Setup (Optional)
77 |
78 | If you would like to use Umami Analytics, you can sign up for a free account at [umami.is](https://umami.is/). Once you have signed up, you can add your Umami Analytics tracking code to the `app/layout.tsx` file.
79 |
80 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
81 |
82 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
83 |
84 | This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
85 |
86 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
87 |
88 | ## Learn More
89 |
90 | To learn more about Next.js, take a look at the following resources:
91 |
92 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
93 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
94 |
95 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
96 |
97 | ## Deploy on Vercel
98 |
99 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
100 |
101 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
102 |
--------------------------------------------------------------------------------
/components/stats-chart.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import { Label, Pie, PieChart } from "recharts";
5 | import { useEffect, useState } from "react";
6 | import { LoaderCircle } from "lucide-react";
7 | import NumberTicker from "@/components/magicui/number-ticker";
8 |
9 | import { Card, CardContent } from "@/components/ui/card";
10 | import {
11 | ChartConfig,
12 | ChartContainer,
13 | ChartTooltip,
14 | ChartTooltipContent,
15 | } from "@/components/ui/chart";
16 |
17 | const fetchStats = async (): Promise => {
18 | try {
19 | const baseUrl =
20 | typeof window !== "undefined" ? "" : process.env.NEXT_PUBLIC_BASE_URL;
21 | const res = await fetch(`${baseUrl}/api/fetch-umami-stats`);
22 |
23 | if (!res.ok) {
24 | console.warn("Failed to fetch stats, using dummy data:", res.status, res.statusText);
25 | return getDummyData();
26 | }
27 |
28 | const data = await res.json();
29 | console.log("Stats data received:", data);
30 |
31 | // Only process if we have valid data
32 | if (data && typeof data === 'object' && !data.error) {
33 | // Calculate average visit duration in seconds (with safety checks)
34 | const averageVisitDurationSeconds =
35 | data?.totaltime?.value && data?.visits?.value
36 | ? data.totaltime.value / data.visits.value
37 | : 0;
38 |
39 | // Convert average visit duration to minutes
40 | const averageVisitDurationMinutes = averageVisitDurationSeconds / 60;
41 |
42 | // Store the average visit duration in minutes
43 | if (data?.totaltime) {
44 | data.totaltime.value = averageVisitDurationMinutes;
45 | data.totaltime.prev = (data.totaltime.prev || 0) / 60;
46 | }
47 |
48 | return data;
49 | }
50 |
51 | console.warn("Invalid data structure, using dummy data:", data);
52 | return getDummyData();
53 | } catch (error) {
54 | console.warn("Error fetching stats, using dummy data:", error);
55 | return getDummyData();
56 | }
57 | };
58 |
59 | const getDummyData = (): StatsData => {
60 | return {
61 | pageviews: { value: 150, prev: 120 },
62 | visitors: { value: 120, prev: 95 },
63 | visits: { value: 140, prev: 110 },
64 | bounces: { value: 50, prev: 45 },
65 | totaltime: { value: 29, prev: 25 }, // in minutes
66 | };
67 | };
68 |
69 | type StatsData = {
70 | pageviews: { value: number; prev: number };
71 | visitors: { value: number; prev: number };
72 | visits: { value: number; prev: number };
73 | bounces: { value: number; prev: number };
74 | totaltime: { value: number; prev: number }; // now in minutes
75 | };
76 |
77 | type UmamiStatsProps = {
78 | stats: StatsData;
79 | };
80 |
81 | const UmamiStats: React.FC = ({ stats }) => {
82 | return stats.pageviews.value;
83 | };
84 |
85 | const chartConfig = {
86 | visitor_stats: {
87 | label: "Visitors",
88 | },
89 | pageviews: {
90 | label: "Page Views",
91 | color: "hsl(var(--chart-1))",
92 | },
93 | visitors: {
94 | label: "Users",
95 | color: "hsl(var(--chart-2))",
96 | },
97 | visits: {
98 | label: "Visits",
99 | color: "hsl(var(--chart-3))",
100 | },
101 | bounces: {
102 | label: "Bounces",
103 | color: "hsl(var(--chart-4))",
104 | },
105 | totaltime: {
106 | label: "Avarege Time",
107 | color: "hsl(var(--chart-5))",
108 | },
109 | } satisfies ChartConfig;
110 |
111 | export default function StatsChart() {
112 | const [stats, setStats] = useState(null);
113 |
114 | useEffect(() => {
115 | const getStats = async () => {
116 | const stats = await fetchStats();
117 | setStats(stats);
118 | };
119 |
120 | getStats();
121 | }, []);
122 |
123 | const chartData = React.useMemo(() => {
124 | if (!stats) return [];
125 | return [
126 | {
127 | type: "pageviews",
128 | visitors: stats.pageviews?.value || 0,
129 | fill: "var(--color-pageviews)",
130 | },
131 | {
132 | type: "visitors",
133 | visitors: stats.visitors?.value || 0,
134 | fill: "var(--color-visitors)",
135 | },
136 | {
137 | type: "visits",
138 | visitors: stats.visits?.value || 0,
139 | fill: "var(--color-visits)",
140 | },
141 | {
142 | type: "bounces",
143 | visitors: stats.bounces?.value || 0,
144 | fill: "var(--color-bounces)",
145 | },
146 | {
147 | type: "totaltime",
148 | visitors: stats.totaltime?.value || 0,
149 | fill: "var(--color-totaltime)",
150 | },
151 | ];
152 | }, [stats]);
153 |
154 | if (!stats) {
155 | return
156 |
157 |
;
158 | }
159 |
160 | const totalVisitors = chartData.reduce((acc, curr) => acc + curr.visitors, 0);
161 |
162 | return (
163 |
164 |
165 | }
168 | />
169 |
176 | {
178 | if (viewBox && "cx" in viewBox && "cy" in viewBox) {
179 | return (
180 |
186 |
191 | {totalVisitors.toLocaleString()}
192 |
193 |
198 | Visits
199 |
200 |
201 | );
202 | }
203 | }}
204 | />
205 |
206 |
207 |
208 | );
209 | }
210 |
--------------------------------------------------------------------------------
/components/magicui/terminal.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import {
4 | Children,
5 | createContext,
6 | useContext,
7 | useEffect,
8 | useMemo,
9 | useRef,
10 | useState,
11 | } from "react"
12 | import { motion, useInView } from "framer-motion"
13 | import type { MotionProps } from "framer-motion"
14 |
15 | import { cn } from "@/lib/utils"
16 |
17 | interface SequenceContextValue {
18 | completeItem: (index: number) => void
19 | activeIndex: number
20 | sequenceStarted: boolean
21 | }
22 |
23 | const SequenceContext = createContext(null)
24 |
25 | const useSequence = () => useContext(SequenceContext)
26 |
27 | const ItemIndexContext = createContext(null)
28 | const useItemIndex = () => useContext(ItemIndexContext)
29 |
30 | interface AnimatedSpanProps extends MotionProps {
31 | children: React.ReactNode
32 | delay?: number
33 | className?: string
34 | startOnView?: boolean
35 | }
36 |
37 | export const AnimatedSpan = ({
38 | children,
39 | delay = 0,
40 | className,
41 | startOnView = false,
42 | ...props
43 | }: AnimatedSpanProps) => {
44 | const elementRef = useRef(null)
45 | const isInView = useInView(elementRef as React.RefObject, {
46 | amount: 0.3,
47 | once: true,
48 | })
49 |
50 | const sequence = useSequence()
51 | const itemIndex = useItemIndex()
52 | const [hasStarted, setHasStarted] = useState(false)
53 | useEffect(() => {
54 | if (!sequence || itemIndex === null) return
55 | if (!sequence.sequenceStarted) return
56 | if (hasStarted) return
57 | if (sequence.activeIndex === itemIndex) {
58 | setHasStarted(true)
59 | }
60 | }, [sequence?.activeIndex, sequence?.sequenceStarted, hasStarted, itemIndex])
61 |
62 | const shouldAnimate = sequence ? hasStarted : startOnView ? isInView : true
63 |
64 | return (
65 | {
72 | if (!sequence) return
73 | if (itemIndex === null) return
74 | sequence.completeItem(itemIndex)
75 | }}
76 | {...props}
77 | >
78 | {children}
79 |
80 | )
81 | }
82 |
83 | interface TypingAnimationProps extends MotionProps {
84 | children: string
85 | className?: string
86 | duration?: number
87 | delay?: number
88 | as?: React.ElementType
89 | startOnView?: boolean
90 | }
91 |
92 | export const TypingAnimation = ({
93 | children,
94 | className,
95 | duration = 60,
96 | delay = 0,
97 | as: Component = "span",
98 | startOnView = true,
99 | ...props
100 | }: TypingAnimationProps) => {
101 | if (typeof children !== "string") {
102 | throw new Error("TypingAnimation: children must be a string. Received:")
103 | }
104 |
105 | const [displayedText, setDisplayedText] = useState("")
106 | const [started, setStarted] = useState(false)
107 | const elementRef = useRef(null)
108 | const isInView = useInView(elementRef as React.RefObject, {
109 | amount: 0.3,
110 | once: true,
111 | })
112 |
113 | const sequence = useSequence()
114 | const itemIndex = useItemIndex()
115 |
116 | useEffect(() => {
117 | if (sequence && itemIndex !== null) {
118 | if (!sequence.sequenceStarted) return
119 | if (started) return
120 | if (sequence.activeIndex === itemIndex) {
121 | setStarted(true)
122 | }
123 | return
124 | }
125 |
126 | if (!startOnView) {
127 | const startTimeout = setTimeout(() => setStarted(true), delay)
128 | return () => clearTimeout(startTimeout)
129 | }
130 |
131 | if (!isInView) return
132 |
133 | const startTimeout = setTimeout(() => setStarted(true), delay)
134 | return () => clearTimeout(startTimeout)
135 | }, [
136 | delay,
137 | startOnView,
138 | isInView,
139 | started,
140 | sequence?.activeIndex,
141 | sequence?.sequenceStarted,
142 | itemIndex,
143 | ])
144 |
145 | useEffect(() => {
146 | if (!started) return
147 |
148 | let i = 0
149 | const typingEffect = setInterval(() => {
150 | if (i < children.length) {
151 | setDisplayedText(children.substring(0, i + 1))
152 | i++
153 | } else {
154 | clearInterval(typingEffect)
155 | if (sequence && itemIndex !== null) {
156 | sequence.completeItem(itemIndex)
157 | }
158 | }
159 | }, duration)
160 |
161 | return () => {
162 | clearInterval(typingEffect)
163 | }
164 | }, [children, duration, started, sequence, itemIndex])
165 |
166 | return (
167 |
172 | {displayedText}
173 |
174 | )
175 | }
176 |
177 | interface TerminalProps {
178 | children: React.ReactNode
179 | className?: string
180 | sequence?: boolean
181 | startOnView?: boolean
182 | }
183 |
184 | export const Terminal = ({
185 | children,
186 | className,
187 | sequence = true,
188 | startOnView = true,
189 | }: TerminalProps) => {
190 | const containerRef = useRef(null)
191 | const isInView = useInView(containerRef as React.RefObject, {
192 | amount: 0.3,
193 | once: true,
194 | })
195 |
196 | const [activeIndex, setActiveIndex] = useState(0)
197 | const sequenceHasStarted = sequence ? !startOnView || isInView : false
198 |
199 | const contextValue = useMemo(() => {
200 | if (!sequence) return null
201 | return {
202 | completeItem: (index: number) => {
203 | setActiveIndex((current) => (index === current ? current + 1 : current))
204 | },
205 | activeIndex,
206 | sequenceStarted: sequenceHasStarted,
207 | }
208 | }, [sequence, activeIndex, sequenceHasStarted])
209 |
210 | const wrappedChildren = useMemo(() => {
211 | if (!sequence) return children
212 | const array = Children.toArray(children)
213 | return array.map((child, index) => (
214 |
215 | {child as React.ReactNode}
216 |
217 | ))
218 | }, [children, sequence])
219 |
220 | const content = (
221 |
228 |
235 |
236 | {wrappedChildren}
237 |
238 |
239 | )
240 |
241 | if (!sequence) return content
242 |
243 | return (
244 |
245 | {content}
246 |
247 | )
248 | }
249 |
--------------------------------------------------------------------------------
/components/magicui/globe.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { cn } from "@/lib/utils";
4 | import createGlobe, { COBEOptions } from "cobe";
5 | import { useCallback, useEffect, useRef } from "react";
6 | import { useSpring } from "react-spring";
7 |
8 | const GLOBE_CONFIG: COBEOptions = {
9 | width: 800,
10 | height: 800,
11 | onRender: () => {},
12 | devicePixelRatio: 2,
13 | phi: 0,
14 | theta: 0.41,
15 | dark: 0,
16 | diffuse: 0.4,
17 | mapSamples: 64000,
18 | mapBrightness: 1.2,
19 | baseColor: [1, 1, 1],
20 | markerColor: [251 / 255, 100 / 255, 21 / 255],
21 | glowColor: [0, 0, 0],
22 | markers: [
23 | { location: [14.5995, 120.9842], size: 0.03 }, // Manila
24 | { location: [19.076, 72.8777], size: 0.1 }, // Mumbai
25 | { location: [23.8103, 90.4125], size: 0.05 }, // Dhaka
26 | { location: [30.0444, 31.2357], size: 0.07 }, // Cairo
27 | { location: [39.9042, 116.4074], size: 0.08 }, // Beijing
28 | { location: [-23.5505, -46.6333], size: 0.1 }, // São Paulo
29 | { location: [19.4326, -99.1332], size: 0.1 }, // Mexico City
30 | { location: [40.7128, -74.006], size: 0.1 }, // New York City
31 | { location: [34.6937, 135.5022], size: 0.05 }, // Osaka
32 | { location: [41.0082, 28.9784], size: 0.06 }, // Istanbul
33 | { location: [34.0522, -118.2437], size: 0.08 }, // Los Angeles
34 | { location: [30.2672, -97.7431], size: 0.06 }, // Austin, Texas
35 | { location: [37.7749, -122.4194], size: 0.07 }, // San Francisco
36 | { location: [21.3069, -157.8583], size: 0.05 }, // Honolulu, Hawaii
37 | { location: [35.6895, 139.6917], size: 0.08 }, // Tokyo
38 | { location: [48.8566, 2.3522], size: 0.07 }, // Paris
39 | { location: [59.3293, 18.0686], size: 0.05 }, // Stockholm, Sweden
40 | { location: [46.2044, 6.1432], size: 0.05 }, // Geneva, Switzerland
41 | { location: [22.3193, 114.1694], size: 0.07 }, // Hong Kong
42 | { location: [41.9028, 12.4964], size: 0.06 }, // Rome
43 | { location: [25.7617, -80.1918], size: 0.07 }, // Miami, Florida
44 | { location: [43.6532, -79.3832], size: 0.07 }, // Toronto, Canada
45 | { location: [49.2827, -123.1207], size: 0.07 }, // Vancouver, Canada
46 | { location: [61.2181, -149.9003], size: 0.05 }, // Anchorage, Alaska
47 | { location: [52.52, 13.405], size: 0.06 }, // Berlin
48 | { location: [-33.8688, 151.2093], size: 0.07 }, // Sydney
49 | { location: [40.4173, -82.9071], size: 0.05 }, // Ohio
50 | { location: [38.5976, -80.4549], size: 0.05 }, // West Virginia
51 | { location: [38.9072, -77.0369], size: 0.07 }, // Washington, D.C.
52 | { location: [42.3601, -71.0589], size: 0.06 }, // Boston, Massachusetts
53 | { location: [39.7392, -104.9903], size: 0.06 }, // Denver, Colorado
54 | { location: [47.6062, -122.3321], size: 0.06 }, // Seattle
55 | { location: [37.5665, 126.978], size: 0.07 }, // Seoul, South Korea
56 | { location: [26.8206, 30.8025], size: 0.07 }, // Egypt
57 | { location: [25.276987, 55.296249], size: 0.07 }, // Dubai
58 | { location: [51.5074, -0.1278], size: 0.07 }, // London
59 | { location: [1.3521, 103.8198], size: 0.07 }, // Singapore
60 | { location: [64.9631, -19.0208], size: 0.05 }, // Iceland
61 | { location: [41.8781, -87.6298], size: 0.07 }, // Chicago
62 | { location: [40.4168, -3.7038], size: 0.07 }, // Madrid
63 | { location: [55.7558, 37.6173], size: 0.07 }, // Moscow, Russia
64 | { location: [35.6762, 139.6503], size: 0.07 }, // Tokyo, Japan
65 | { location: [-34.6037, -58.3816], size: 0.07 }, // Buenos Aires, Argentina
66 | { location: [37.9838, 23.7275], size: 0.06 }, // Athens, Greece
67 | { location: [28.6139, 77.209], size: 0.07 }, // New Delhi, India
68 | { location: [45.4642, 9.19], size: 0.07 }, // Milan, Italy
69 | { location: [50.1109, 8.6821], size: 0.07 }, // Frankfurt, Germany
70 | { location: [35.8617, 104.1954], size: 0.07 }, // China
71 | { location: [31.2304, 121.4737], size: 0.08 }, // Shanghai, China
72 | { location: [48.2082, 16.3738], size: 0.06 }, // Vienna, Austria
73 | { location: [50.8503, 4.3517], size: 0.06 }, // Brussels, Belgium
74 | { location: [45.4215, -75.6972], size: 0.07 }, // Ottawa, Canada
75 | { location: [55.8642, -4.2518], size: 0.06 }, // Glasgow, Scotland
76 | { location: [31.7683, 35.2137], size: 0.07 }, // Jerusalem, Israel
77 | { location: [-22.9068, -43.1729], size: 0.07 }, // Rio de Janeiro, Brazil
78 | { location: [35.6895, 139.6917], size: 0.08 }, // Tokyo, Japan
79 | { location: [25.0343, -77.3963], size: 0.06 }, // Nassau, Bahamas
80 | { location: [22.3964, 114.1095], size: 0.07 }, // Hong Kong
81 | { location: [3.139, 101.6869], size: 0.07 }, // Kuala Lumpur, Malaysia
82 | { location: [-1.2921, 36.8219], size: 0.06 }, // Nairobi, Kenya
83 | { location: [39.9042, 116.4074], size: 0.08 }, // Beijing, China
84 | { location: [32.7767, -96.797], size: 0.06 }, // Dallas, Texas
85 | { location: [39.9042, 116.4074], size: 0.08 }, // Beijing, China
86 | { location: [-37.8136, 144.9631], size: 0.07 }, // Melbourne, Australia
87 | { location: [43.7696, 11.2558], size: 0.06 }, // Florence, Italy
88 | { location: [41.9028, 12.4964], size: 0.07 }, // Rome, Italy
89 | ],
90 | };
91 |
92 | export default function Globe({
93 | className,
94 | config = GLOBE_CONFIG,
95 | }: {
96 | className?: string;
97 | config?: COBEOptions;
98 | }) {
99 | let phi = 0;
100 | let width = 0;
101 | const canvasRef = useRef(null);
102 | const pointerInteracting = useRef(null);
103 | const pointerInteractionMovement = useRef(0);
104 | const [{ r }, api] = useSpring(() => ({
105 | r: 0,
106 | config: {
107 | mass: 1,
108 | tension: 280,
109 | friction: 40,
110 | precision: 0.001,
111 | },
112 | }));
113 |
114 | const updatePointerInteraction = (value: any) => {
115 | pointerInteracting.current = value;
116 | canvasRef.current!.style.cursor = value ? "grabbing" : "grab";
117 | };
118 |
119 | const updateMovement = (clientX: any) => {
120 | if (pointerInteracting.current !== null) {
121 | const delta = clientX - pointerInteracting.current;
122 | pointerInteractionMovement.current = delta;
123 | api.start({ r: delta / 200 });
124 | }
125 | };
126 |
127 | const onRender = useCallback(
128 | (state: Record) => {
129 | if (!pointerInteracting.current) phi += 0.0025;
130 | state.phi = phi + r.get();
131 | state.width = width * 2;
132 | state.height = width * 2;
133 | },
134 | [pointerInteracting, phi, r]
135 | );
136 |
137 | const onResize = () => {
138 | if (canvasRef.current) {
139 | width = canvasRef.current.offsetWidth;
140 | }
141 | };
142 |
143 | useEffect(() => {
144 | window.addEventListener("resize", onResize);
145 | onResize();
146 |
147 | const globe = createGlobe(canvasRef.current!, {
148 | ...config,
149 | width: width * 2,
150 | height: width * 2,
151 | onRender,
152 | });
153 |
154 | setTimeout(() => (canvasRef.current!.style.opacity = "1"));
155 | return () => globe.destroy();
156 | }, []);
157 |
158 | return (
159 |
165 |
171 | updatePointerInteraction(
172 | e.clientX - pointerInteractionMovement.current
173 | )
174 | }
175 | onPointerUp={() => updatePointerInteraction(null)}
176 | onPointerOut={() => updatePointerInteraction(null)}
177 | onMouseMove={(e) => updateMovement(e.clientX)}
178 | onTouchMove={(e) =>
179 | e.touches[0] && updateMovement(e.touches[0].clientX)
180 | }
181 | />
182 |
183 | );
184 | }
185 |
--------------------------------------------------------------------------------
/components/ui/dropdown-menu.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
5 | import { Check, ChevronRight, Circle } from "lucide-react";
6 |
7 | import { cn } from "@/lib/utils";
8 |
9 | const DropdownMenu = DropdownMenuPrimitive.Root;
10 |
11 | const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
12 |
13 | const DropdownMenuGroup = DropdownMenuPrimitive.Group;
14 |
15 | const DropdownMenuPortal = DropdownMenuPrimitive.Portal;
16 |
17 | const DropdownMenuSub = DropdownMenuPrimitive.Sub;
18 |
19 | const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
20 |
21 | const DropdownMenuSubTrigger = React.forwardRef<
22 | React.ElementRef,
23 | React.ComponentPropsWithoutRef & {
24 | inset?: boolean;
25 | }
26 | >(({ className, inset, children, ...props }, ref) => (
27 |
36 | {children}
37 |
38 |
39 | ));
40 | DropdownMenuSubTrigger.displayName =
41 | DropdownMenuPrimitive.SubTrigger.displayName;
42 |
43 | const DropdownMenuSubContent = React.forwardRef<
44 | React.ElementRef,
45 | React.ComponentPropsWithoutRef
46 | >(({ className, ...props }, ref) => (
47 |
55 | ));
56 | DropdownMenuSubContent.displayName =
57 | DropdownMenuPrimitive.SubContent.displayName;
58 |
59 | const DropdownMenuContent = React.forwardRef<
60 | React.ElementRef,
61 | React.ComponentPropsWithoutRef
62 | >(({ className, sideOffset = 4, ...props }, ref) => (
63 |
64 |
73 |
74 | ));
75 | DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
76 |
77 | const DropdownMenuItem = React.forwardRef<
78 | React.ElementRef,
79 | React.ComponentPropsWithoutRef & {
80 | inset?: boolean;
81 | }
82 | >(({ className, inset, ...props }, ref) => (
83 |
92 | ));
93 | DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
94 |
95 | const DropdownMenuCheckboxItem = React.forwardRef<
96 | React.ElementRef,
97 | React.ComponentPropsWithoutRef
98 | >(({ className, children, checked, ...props }, ref) => (
99 |
108 |
109 |
110 |
111 |
112 |
113 | {children}
114 |
115 | ));
116 | DropdownMenuCheckboxItem.displayName =
117 | DropdownMenuPrimitive.CheckboxItem.displayName;
118 |
119 | const DropdownMenuRadioItem = React.forwardRef<
120 | React.ElementRef,
121 | React.ComponentPropsWithoutRef
122 | >(({ className, children, ...props }, ref) => (
123 |
131 |
132 |
133 |
134 |
135 |
136 | {children}
137 |
138 | ));
139 | DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;
140 |
141 | const DropdownMenuLabel = React.forwardRef<
142 | React.ElementRef,
143 | React.ComponentPropsWithoutRef & {
144 | inset?: boolean;
145 | }
146 | >(({ className, inset, ...props }, ref) => (
147 |
156 | ));
157 | DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
158 |
159 | const DropdownMenuSeparator = React.forwardRef<
160 | React.ElementRef,
161 | React.ComponentPropsWithoutRef
162 | >(({ className, ...props }, ref) => (
163 |
168 | ));
169 | DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
170 |
171 | const DropdownMenuShortcut = ({
172 | className,
173 | ...props
174 | }: React.HTMLAttributes) => {
175 | return (
176 |
180 | );
181 | };
182 | DropdownMenuShortcut.displayName = "DropdownMenuShortcut";
183 |
184 | export {
185 | DropdownMenu,
186 | DropdownMenuTrigger,
187 | DropdownMenuContent,
188 | DropdownMenuItem,
189 | DropdownMenuCheckboxItem,
190 | DropdownMenuRadioItem,
191 | DropdownMenuLabel,
192 | DropdownMenuSeparator,
193 | DropdownMenuShortcut,
194 | DropdownMenuGroup,
195 | DropdownMenuPortal,
196 | DropdownMenuSub,
197 | DropdownMenuSubContent,
198 | DropdownMenuSubTrigger,
199 | DropdownMenuRadioGroup,
200 | };
201 |
--------------------------------------------------------------------------------
/components/magicui/particles.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import React, { useEffect, useRef, useState } from "react";
4 |
5 | interface MousePosition {
6 | x: number;
7 | y: number;
8 | }
9 |
10 | function MousePosition(): MousePosition {
11 | const [mousePosition, setMousePosition] = useState({
12 | x: 0,
13 | y: 0,
14 | });
15 |
16 | useEffect(() => {
17 | const handleMouseMove = (event: MouseEvent) => {
18 | setMousePosition({ x: event.clientX, y: event.clientY });
19 | };
20 |
21 | window.addEventListener("mousemove", handleMouseMove);
22 |
23 | return () => {
24 | window.removeEventListener("mousemove", handleMouseMove);
25 | };
26 | }, []);
27 |
28 | return mousePosition;
29 | }
30 |
31 | interface ParticlesProps {
32 | className?: string;
33 | quantity?: number;
34 | staticity?: number;
35 | ease?: number;
36 | size?: number;
37 | refresh?: boolean;
38 | color?: string;
39 | vx?: number;
40 | vy?: number;
41 | }
42 | function hexToRgb(hex: string): number[] {
43 | hex = hex.replace("#", "");
44 | const hexInt = parseInt(hex, 16);
45 | const red = (hexInt >> 16) & 255;
46 | const green = (hexInt >> 8) & 255;
47 | const blue = hexInt & 255;
48 | return [red, green, blue];
49 | }
50 |
51 | const Particles: React.FC = ({
52 | className = "",
53 | quantity = 100,
54 | staticity = 50,
55 | ease = 50,
56 | size = 0.4,
57 | refresh = false,
58 | color = "#ffffff",
59 | vx = 0,
60 | vy = 0,
61 | }) => {
62 | const canvasRef = useRef(null);
63 | const canvasContainerRef = useRef(null);
64 | const context = useRef(null);
65 | const circles = useRef([]);
66 | const mousePosition = MousePosition();
67 | const mouse = useRef<{ x: number; y: number }>({ x: 0, y: 0 });
68 | const canvasSize = useRef<{ w: number; h: number }>({ w: 0, h: 0 });
69 | const dpr = typeof window !== "undefined" ? window.devicePixelRatio : 1;
70 |
71 | useEffect(() => {
72 | if (canvasRef.current) {
73 | context.current = canvasRef.current.getContext("2d");
74 | }
75 | initCanvas();
76 | animate();
77 | window.addEventListener("resize", initCanvas);
78 |
79 | return () => {
80 | window.removeEventListener("resize", initCanvas);
81 | };
82 | }, [color]);
83 |
84 | useEffect(() => {
85 | onMouseMove();
86 | }, [mousePosition.x, mousePosition.y]);
87 |
88 | useEffect(() => {
89 | initCanvas();
90 | }, [refresh]);
91 |
92 | const initCanvas = () => {
93 | resizeCanvas();
94 | drawParticles();
95 | };
96 |
97 | const onMouseMove = () => {
98 | if (canvasRef.current) {
99 | const rect = canvasRef.current.getBoundingClientRect();
100 | const { w, h } = canvasSize.current;
101 | const x = mousePosition.x - rect.left - w / 2;
102 | const y = mousePosition.y - rect.top - h / 2;
103 | const inside = x < w / 2 && x > -w / 2 && y < h / 2 && y > -h / 2;
104 | if (inside) {
105 | mouse.current.x = x;
106 | mouse.current.y = y;
107 | }
108 | }
109 | };
110 |
111 | type Circle = {
112 | x: number;
113 | y: number;
114 | translateX: number;
115 | translateY: number;
116 | size: number;
117 | alpha: number;
118 | targetAlpha: number;
119 | dx: number;
120 | dy: number;
121 | magnetism: number;
122 | };
123 |
124 | const resizeCanvas = () => {
125 | if (canvasContainerRef.current && canvasRef.current && context.current) {
126 | circles.current.length = 0;
127 | canvasSize.current.w = canvasContainerRef.current.offsetWidth;
128 | canvasSize.current.h = canvasContainerRef.current.offsetHeight;
129 | canvasRef.current.width = canvasSize.current.w * dpr;
130 | canvasRef.current.height = canvasSize.current.h * dpr;
131 | canvasRef.current.style.width = `${canvasSize.current.w}px`;
132 | canvasRef.current.style.height = `${canvasSize.current.h}px`;
133 | context.current.scale(dpr, dpr);
134 | }
135 | };
136 |
137 | const circleParams = (): Circle => {
138 | const x = Math.floor(Math.random() * canvasSize.current.w);
139 | const y = Math.floor(Math.random() * canvasSize.current.h);
140 | const translateX = 0;
141 | const translateY = 0;
142 | const pSize = Math.floor(Math.random() * 2) + size;
143 | const alpha = 0;
144 | const targetAlpha = parseFloat((Math.random() * 0.6 + 0.1).toFixed(1));
145 | const dx = (Math.random() - 0.5) * 0.1;
146 | const dy = (Math.random() - 0.5) * 0.1;
147 | const magnetism = 0.1 + Math.random() * 4;
148 | return {
149 | x,
150 | y,
151 | translateX,
152 | translateY,
153 | size: pSize,
154 | alpha,
155 | targetAlpha,
156 | dx,
157 | dy,
158 | magnetism,
159 | };
160 | };
161 |
162 | const rgb = hexToRgb(color);
163 |
164 | const drawCircle = (circle: Circle, update = false) => {
165 | if (context.current) {
166 | const { x, y, translateX, translateY, size, alpha } = circle;
167 | context.current.translate(translateX, translateY);
168 | context.current.beginPath();
169 | context.current.arc(x, y, size, 0, 2 * Math.PI);
170 | context.current.fillStyle = `rgba(${rgb.join(", ")}, ${alpha})`;
171 | context.current.fill();
172 | context.current.setTransform(dpr, 0, 0, dpr, 0, 0);
173 |
174 | if (!update) {
175 | circles.current.push(circle);
176 | }
177 | }
178 | };
179 |
180 | const clearContext = () => {
181 | if (context.current) {
182 | context.current.clearRect(
183 | 0,
184 | 0,
185 | canvasSize.current.w,
186 | canvasSize.current.h,
187 | );
188 | }
189 | };
190 |
191 | const drawParticles = () => {
192 | clearContext();
193 | const particleCount = quantity;
194 | for (let i = 0; i < particleCount; i++) {
195 | const circle = circleParams();
196 | drawCircle(circle);
197 | }
198 | };
199 |
200 | const remapValue = (
201 | value: number,
202 | start1: number,
203 | end1: number,
204 | start2: number,
205 | end2: number,
206 | ): number => {
207 | const remapped =
208 | ((value - start1) * (end2 - start2)) / (end1 - start1) + start2;
209 | return remapped > 0 ? remapped : 0;
210 | };
211 |
212 | const animate = () => {
213 | clearContext();
214 | circles.current.forEach((circle: Circle, i: number) => {
215 | // Handle the alpha value
216 | const edge = [
217 | circle.x + circle.translateX - circle.size, // distance from left edge
218 | canvasSize.current.w - circle.x - circle.translateX - circle.size, // distance from right edge
219 | circle.y + circle.translateY - circle.size, // distance from top edge
220 | canvasSize.current.h - circle.y - circle.translateY - circle.size, // distance from bottom edge
221 | ];
222 | const closestEdge = edge.reduce((a, b) => Math.min(a, b));
223 | const remapClosestEdge = parseFloat(
224 | remapValue(closestEdge, 0, 20, 0, 1).toFixed(2),
225 | );
226 | if (remapClosestEdge > 1) {
227 | circle.alpha += 0.02;
228 | if (circle.alpha > circle.targetAlpha) {
229 | circle.alpha = circle.targetAlpha;
230 | }
231 | } else {
232 | circle.alpha = circle.targetAlpha * remapClosestEdge;
233 | }
234 | circle.x += circle.dx + vx;
235 | circle.y += circle.dy + vy;
236 | circle.translateX +=
237 | (mouse.current.x / (staticity / circle.magnetism) - circle.translateX) /
238 | ease;
239 | circle.translateY +=
240 | (mouse.current.y / (staticity / circle.magnetism) - circle.translateY) /
241 | ease;
242 |
243 | drawCircle(circle, true);
244 |
245 | // circle gets out of the canvas
246 | if (
247 | circle.x < -circle.size ||
248 | circle.x > canvasSize.current.w + circle.size ||
249 | circle.y < -circle.size ||
250 | circle.y > canvasSize.current.h + circle.size
251 | ) {
252 | // remove the circle from the array
253 | circles.current.splice(i, 1);
254 | // create a new circle
255 | const newCircle = circleParams();
256 | drawCircle(newCircle);
257 | // update the circle position
258 | }
259 | });
260 | window.requestAnimationFrame(animate);
261 | };
262 |
263 | return (
264 |
265 |
266 |
267 | );
268 | };
269 |
270 | export default Particles;
271 |
--------------------------------------------------------------------------------
/components/magicui/icon-cloud.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import React, { useEffect, useRef, useState } from "react"
4 | import { renderToString } from "react-dom/server"
5 |
6 | interface Icon {
7 | x: number
8 | y: number
9 | z: number
10 | scale: number
11 | opacity: number
12 | id: number
13 | }
14 |
15 | interface IconCloudProps {
16 | icons?: React.ReactNode[]
17 | images?: string[]
18 | onIconClick?: (index: number) => void
19 | }
20 |
21 | function easeOutCubic(t: number): number {
22 | return 1 - Math.pow(1 - t, 3)
23 | }
24 |
25 | export function IconCloud({ icons, images, onIconClick }: IconCloudProps) {
26 | const canvasRef = useRef(null)
27 | const [iconPositions, setIconPositions] = useState([])
28 | const [rotation, setRotation] = useState({ x: 0, y: 0 })
29 | const [isDragging, setIsDragging] = useState(false)
30 | const [lastMousePos, setLastMousePos] = useState({ x: 0, y: 0 })
31 | const [mousePos, setMousePos] = useState({ x: 0, y: 0 })
32 | const [targetRotation, setTargetRotation] = useState<{
33 | x: number
34 | y: number
35 | startX: number
36 | startY: number
37 | distance: number
38 | startTime: number
39 | duration: number
40 | } | null>(null)
41 | const animationFrameRef = useRef(0)
42 | const rotationRef = useRef(rotation)
43 | const iconCanvasesRef = useRef([])
44 | const imagesLoadedRef = useRef([])
45 |
46 | // Create icon canvases once when icons/images change
47 | useEffect(() => {
48 | if (!icons && !images) return
49 |
50 | const items = icons || images || []
51 | imagesLoadedRef.current = new Array(items.length).fill(false)
52 |
53 | const newIconCanvases = items.map((item, index) => {
54 | const offscreen = document.createElement("canvas")
55 | offscreen.width = 40
56 | offscreen.height = 40
57 | const offCtx = offscreen.getContext("2d")
58 |
59 | if (offCtx) {
60 | if (images) {
61 | // Handle image URLs directly
62 | const img = new Image()
63 | img.crossOrigin = "anonymous"
64 | img.src = items[index] as string
65 | img.onload = () => {
66 | offCtx.clearRect(0, 0, offscreen.width, offscreen.height)
67 |
68 | // Draw the image without clipping
69 | offCtx.drawImage(img, 0, 0, 40, 40)
70 |
71 | imagesLoadedRef.current[index] = true
72 | }
73 | } else {
74 | // Handle SVG icons
75 | offCtx.scale(0.4, 0.4)
76 | const svgString = renderToString(item as React.ReactElement)
77 | const img = new Image()
78 | img.src = "data:image/svg+xml;base64," + btoa(svgString)
79 | img.onload = () => {
80 | offCtx.clearRect(0, 0, offscreen.width, offscreen.height)
81 | offCtx.drawImage(img, 0, 0)
82 | imagesLoadedRef.current[index] = true
83 | }
84 | }
85 | }
86 | return offscreen
87 | })
88 |
89 | iconCanvasesRef.current = newIconCanvases
90 | }, [icons, images])
91 |
92 | // Generate initial icon positions on a sphere
93 | useEffect(() => {
94 | const items = icons || images || []
95 | const newIcons: Icon[] = []
96 | const numIcons = items.length || 20
97 |
98 | // Fibonacci sphere parameters
99 | const offset = 2 / numIcons
100 | const increment = Math.PI * (3 - Math.sqrt(5))
101 |
102 | for (let i = 0; i < numIcons; i++) {
103 | const y = i * offset - 1 + offset / 2
104 | const r = Math.sqrt(1 - y * y)
105 | const phi = i * increment
106 |
107 | const x = Math.cos(phi) * r
108 | const z = Math.sin(phi) * r
109 |
110 | newIcons.push({
111 | x: x * 250,
112 | y: y * 250,
113 | z: z * 250,
114 | scale: 1,
115 | opacity: 1,
116 | id: i,
117 | })
118 | }
119 | setIconPositions(newIcons)
120 | }, [icons, images])
121 |
122 | // Handle mouse events
123 | const handleMouseDown = (e: React.MouseEvent) => {
124 | const rect = canvasRef.current?.getBoundingClientRect()
125 | if (!rect || !canvasRef.current) return
126 |
127 | const x = e.clientX - rect.left
128 | const y = e.clientY - rect.top
129 |
130 | const ctx = canvasRef.current.getContext("2d")
131 | if (!ctx) return
132 |
133 | iconPositions.forEach((icon) => {
134 | const cosX = Math.cos(rotationRef.current.x)
135 | const sinX = Math.sin(rotationRef.current.x)
136 | const cosY = Math.cos(rotationRef.current.y)
137 | const sinY = Math.sin(rotationRef.current.y)
138 |
139 | const rotatedX = icon.x * cosY - icon.z * sinY
140 | const rotatedZ = icon.x * sinY + icon.z * cosY
141 | const rotatedY = icon.y * cosX + rotatedZ * sinX
142 |
143 | const screenX = canvasRef.current!.width / 2 + rotatedX
144 | const screenY = canvasRef.current!.height / 2 + rotatedY
145 |
146 | const scale = (rotatedZ + 280) / 400
147 | const radius = 25 * scale
148 | const dx = x - screenX
149 | const dy = y - screenY
150 |
151 | if (dx * dx + dy * dy < radius * radius) {
152 | // If click handler is provided, call it instead of rotating
153 | if (onIconClick) {
154 | onIconClick(icon.id)
155 | return
156 | }
157 |
158 | const targetX = -Math.atan2(
159 | icon.y,
160 | Math.sqrt(icon.x * icon.x + icon.z * icon.z)
161 | )
162 | const targetY = Math.atan2(icon.x, icon.z)
163 |
164 | const currentX = rotationRef.current.x
165 | const currentY = rotationRef.current.y
166 | const distance = Math.sqrt(
167 | Math.pow(targetX - currentX, 2) + Math.pow(targetY - currentY, 2)
168 | )
169 |
170 | const duration = Math.min(2000, Math.max(800, distance * 1000))
171 |
172 | setTargetRotation({
173 | x: targetX,
174 | y: targetY,
175 | startX: currentX,
176 | startY: currentY,
177 | distance,
178 | startTime: performance.now(),
179 | duration,
180 | })
181 | return
182 | }
183 | })
184 |
185 | setIsDragging(true)
186 | setLastMousePos({ x: e.clientX, y: e.clientY })
187 | }
188 |
189 | const handleMouseMove = (e: React.MouseEvent) => {
190 | const rect = canvasRef.current?.getBoundingClientRect()
191 | if (rect) {
192 | const x = e.clientX - rect.left
193 | const y = e.clientY - rect.top
194 | setMousePos({ x, y })
195 | }
196 |
197 | if (isDragging) {
198 | const deltaX = e.clientX - lastMousePos.x
199 | const deltaY = e.clientY - lastMousePos.y
200 |
201 | rotationRef.current = {
202 | x: rotationRef.current.x - deltaY * 0.0005,
203 | y: rotationRef.current.y - deltaX * 0.0005,
204 | }
205 |
206 | setLastMousePos({ x: e.clientX, y: e.clientY })
207 | }
208 | }
209 |
210 | const handleMouseUp = () => {
211 | setIsDragging(false)
212 | }
213 |
214 | // Animation and rendering
215 | useEffect(() => {
216 | const canvas = canvasRef.current
217 | const ctx = canvas?.getContext("2d")
218 | if (!canvas || !ctx) return
219 |
220 | const animate = () => {
221 | ctx.clearRect(0, 0, canvas.width, canvas.height)
222 |
223 | const centerX = canvas.width / 2
224 | const centerY = canvas.height / 2
225 | const maxDistance = Math.sqrt(centerX * centerX + centerY * centerY)
226 | const dx = mousePos.x - centerX
227 | const dy = mousePos.y - centerY
228 | const distance = Math.sqrt(dx * dx + dy * dy)
229 | const speed = 0.0005 + (distance / maxDistance) * 0.002
230 |
231 | if (targetRotation) {
232 | const elapsed = performance.now() - targetRotation.startTime
233 | const progress = Math.min(1, elapsed / targetRotation.duration)
234 | const easedProgress = easeOutCubic(progress)
235 |
236 | rotationRef.current = {
237 | x:
238 | targetRotation.startX +
239 | (targetRotation.x - targetRotation.startX) * easedProgress,
240 | y:
241 | targetRotation.startY +
242 | (targetRotation.y - targetRotation.startY) * easedProgress,
243 | }
244 |
245 | if (progress >= 1) {
246 | setTargetRotation(null)
247 | }
248 | } else if (!isDragging) {
249 | rotationRef.current = {
250 | x: rotationRef.current.x + (dy / canvas.height) * speed,
251 | y: rotationRef.current.y + (dx / canvas.width) * speed,
252 | }
253 | }
254 |
255 | iconPositions.forEach((icon, index) => {
256 | const cosX = Math.cos(rotationRef.current.x)
257 | const sinX = Math.sin(rotationRef.current.x)
258 | const cosY = Math.cos(rotationRef.current.y)
259 | const sinY = Math.sin(rotationRef.current.y)
260 |
261 | const rotatedX = icon.x * cosY - icon.z * sinY
262 | const rotatedZ = icon.x * sinY + icon.z * cosY
263 | const rotatedY = icon.y * cosX + rotatedZ * sinX
264 |
265 | const scale = (rotatedZ + 280) / 400
266 | const opacity = Math.max(0.2, Math.min(1, (rotatedZ + 150) / 200))
267 |
268 | ctx.save()
269 | ctx.translate(canvas.width / 2 + rotatedX, canvas.height / 2 + rotatedY)
270 | ctx.scale(scale, scale)
271 | ctx.globalAlpha = opacity
272 |
273 | if (icons || images) {
274 | // Only try to render icons/images if they exist
275 | if (
276 | iconCanvasesRef.current[index] &&
277 | imagesLoadedRef.current[index]
278 | ) {
279 | ctx.drawImage(iconCanvasesRef.current[index], -20, -20, 40, 40)
280 | }
281 | } else {
282 | // Show numbered circles if no icons/images are provided
283 | ctx.beginPath()
284 | ctx.arc(0, 0, 20, 0, Math.PI * 2)
285 | ctx.fillStyle = "#4444ff"
286 | ctx.fill()
287 | ctx.fillStyle = "white"
288 | ctx.textAlign = "center"
289 | ctx.textBaseline = "middle"
290 | ctx.font = "16px Arial"
291 | ctx.fillText(`${icon.id + 1}`, 0, 0)
292 | }
293 |
294 | ctx.restore()
295 | })
296 | animationFrameRef.current = requestAnimationFrame(animate)
297 | }
298 |
299 | animate()
300 |
301 | return () => {
302 | if (animationFrameRef.current) {
303 | cancelAnimationFrame(animationFrameRef.current)
304 | }
305 | }
306 | }, [icons, images, iconPositions, isDragging, mousePos, targetRotation])
307 |
308 | return (
309 |
321 | )
322 | }
323 |
--------------------------------------------------------------------------------
/components/ui/chart.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import * as RechartsPrimitive from "recharts";
5 |
6 | import { cn } from "@/lib/utils";
7 |
8 | // Format: { THEME_NAME: CSS_SELECTOR }
9 | const THEMES = { light: "", dark: ".dark" } as const;
10 |
11 | export type ChartConfig = {
12 | [k in string]: {
13 | label?: React.ReactNode;
14 | icon?: React.ComponentType;
15 | } & (
16 | | { color?: string; theme?: never }
17 | | { color?: never; theme: Record }
18 | );
19 | };
20 |
21 | type ChartContextProps = {
22 | config: ChartConfig;
23 | };
24 |
25 | const ChartContext = React.createContext(null);
26 |
27 | function useChart() {
28 | const context = React.useContext(ChartContext);
29 |
30 | if (!context) {
31 | throw new Error("useChart must be used within a ");
32 | }
33 |
34 | return context;
35 | }
36 |
37 | const ChartContainer = React.forwardRef<
38 | HTMLDivElement,
39 | React.ComponentProps<"div"> & {
40 | config: ChartConfig;
41 | children: React.ComponentProps<
42 | typeof RechartsPrimitive.ResponsiveContainer
43 | >["children"];
44 | }
45 | >(({ id, className, children, config, ...props }, ref) => {
46 | const uniqueId = React.useId();
47 | const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`;
48 |
49 | return (
50 |
51 |
60 |
61 |
62 | {children}
63 |
64 |
65 |
66 | );
67 | });
68 | ChartContainer.displayName = "Chart";
69 |
70 | const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
71 | const colorConfig = Object.entries(config).filter(
72 | ([_, config]) => config.theme || config.color
73 | );
74 |
75 | if (!colorConfig.length) {
76 | return null;
77 | }
78 |
79 | return (
80 |