aboutsummaryrefslogtreecommitdiff
path: root/src/components
diff options
context:
space:
mode:
Diffstat (limited to 'src/components')
-rw-r--r--src/components/GlassHeader.tsx11
-rw-r--r--src/components/Spinner.tsx24
-rw-r--r--src/components/backgrounds/Grid.tsx35
-rw-r--r--src/components/backgrounds/Polygons.tsx31
-rw-r--r--src/components/old/bentonbox.tsx165
-rw-r--r--src/components/ui/background-ripple-effect.tsx132
-rw-r--r--src/components/ui/canvas-reveal-effect.tsx308
-rw-r--r--src/components/ui/card-spotlight.tsx74
-rw-r--r--src/components/ui/hover-border-gradient.tsx100
-rw-r--r--src/components/ui/wavy-background.tsx132
-rw-r--r--src/components/ui/wooble-card.tsx78
-rw-r--r--src/components/ui/world-map.tsx167
12 files changed, 1257 insertions, 0 deletions
diff --git a/src/components/GlassHeader.tsx b/src/components/GlassHeader.tsx
new file mode 100644
index 0000000..fd5a5aa
--- /dev/null
+++ b/src/components/GlassHeader.tsx
@@ -0,0 +1,11 @@
+export default function GlassHeader() {
+ return (
+ <div className="w-full bg-white/30 backdrop-blur-lg shadow-md shadow-gray-700/10">
+ <div className="container mx-auto px-6 py-4">
+ <h1 className="text-2xl font-semibold text-gray-900">
+ Sensible Scholars
+ </h1>
+ </div>
+ </div>
+ );
+}
diff --git a/src/components/Spinner.tsx b/src/components/Spinner.tsx
new file mode 100644
index 0000000..96b6b6e
--- /dev/null
+++ b/src/components/Spinner.tsx
@@ -0,0 +1,24 @@
+export default function Spinner() {
+ return (
+ <svg
+ className="animate-spin -ml-1 mr-3 h-5 w-5 text-white"
+ xmlns="http://www.w3.org/2000/svg"
+ fill="none"
+ viewBox="0 0 24 24"
+ >
+ <circle
+ className="opacity-25"
+ cx="12"
+ cy="12"
+ r="10"
+ stroke="currentColor"
+ strokeWidth="4"
+ ></circle>
+ <path
+ className="opacity-75"
+ fill="currentColor"
+ d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
+ ></path>
+ </svg>
+ );
+}
diff --git a/src/components/backgrounds/Grid.tsx b/src/components/backgrounds/Grid.tsx
new file mode 100644
index 0000000..7198933
--- /dev/null
+++ b/src/components/backgrounds/Grid.tsx
@@ -0,0 +1,35 @@
+export default function Grid() {
+ return (
+ <div className="absolute inset-0 -z-10 overflow-hidden">
+ <svg
+ aria-hidden="true"
+ className="absolute top-0 left-[max(50%,25rem)] h-256 w-512 -translate-x-1/2 mask-[radial-gradient(64rem_64rem_at_top,white,transparent)] stroke-gray-200"
+ >
+ <defs>
+ <pattern
+ x="50%"
+ y={-1}
+ id="e813992c-7d03-4cc4-a2bd-151760b470a0"
+ width={200}
+ height={200}
+ patternUnits="userSpaceOnUse"
+ >
+ <path d="M100 200V.5M.5 .5H200" fill="none" />
+ </pattern>
+ </defs>
+ <svg x="50%" y={-1} className="overflow-visible fill-red-50">
+ <path
+ d="M-100.5 0h201v201h-201Z M699.5 0h201v201h-201Z M499.5 400h201v201h-201Z M-300.5 600h201v201h-201Z"
+ strokeWidth={0}
+ />
+ </svg>
+ <rect
+ fill="url(#e813992c-7d03-4cc4-a2bd-151760b470a0)"
+ width="100%"
+ height="100%"
+ strokeWidth={0}
+ />
+ </svg>
+ </div>
+ );
+}
diff --git a/src/components/backgrounds/Polygons.tsx b/src/components/backgrounds/Polygons.tsx
new file mode 100644
index 0000000..c6f8ab6
--- /dev/null
+++ b/src/components/backgrounds/Polygons.tsx
@@ -0,0 +1,31 @@
+export default function Polygons() {
+ return (
+ <>
+ <div
+ aria-hidden="true"
+ className="absolute inset-x-0 -top-40 -z-10 transform-gpu overflow-hidden blur-3xl sm:-top-100"
+ >
+ <div
+ style={{
+ clipPath:
+ "polygon(74.1% 44.1%, 100% 61.6%, 97.5% 26.9%, 85.5% 0.1%, 80.7% 2%, 72.5% 32.5%, 60.2% 62.4%, 52.4% 68.1%, 47.5% 58.3%, 45.2% 34.5%, 27.5% 76.7%, 0.1% 64.9%, 17.9% 100%, 27.6% 76.8%, 76.1% 97.7%, 74.1% 44.1%)",
+ }}
+ className="relative left-[calc(50%-11rem)] aspect-1155/800 w-144.5 -translate-x-1/2 rotate-30 bg-linear-to-tr from-[#ff80b5] to-[#ff0000] opacity-50 sm:left-[calc(50%-30rem)] sm:w-288.75"
+ />
+ </div>
+
+ <div
+ aria-hidden="true"
+ className="absolute inset-x-0 top-[calc(30rem)] -z-10 transform-gpu overflow-hidden blur-3xl sm:top-[calc(20rem)]"
+ >
+ <div
+ style={{
+ clipPath:
+ "polygon(74.1% 44.1%, 100% 61.6%, 97.5% 26.9%, 85.5% 0.1%, 80.7% 2%, 72.5% 32.5%, 60.2% 62.4%, 52.4% 68.1%, 47.5% 58.3%, 45.2% 34.5%, 27.5% 76.7%, 0.1% 64.9%, 17.9% 100%, 27.6% 76.8%, 76.1% 97.7%, 74.1% 44.1%)",
+ }}
+ className="relative left-[calc(50%-15rem)] aspect-1155/900 sm:aspect-1155/700 w-144.5-translate-x-1/2 bg-linear-to-tr from-red-300 to-indigo-400 opacity-50 sm:left-[calc(50%-40rem)] sm:w-288.75"
+ />
+ </div>
+ </>
+ );
+}
diff --git a/src/components/old/bentonbox.tsx b/src/components/old/bentonbox.tsx
new file mode 100644
index 0000000..5a3afca
--- /dev/null
+++ b/src/components/old/bentonbox.tsx
@@ -0,0 +1,165 @@
+"use client";
+
+import { motion } from "motion/react";
+
+export default function OurServices() {
+ return (
+ <div className="pt-24 sm:pt-32">
+ <section className="mx-auto max-w-2xl px-6 lg:max-w-7xl lg:px-8">
+ <motion.h2
+ className="text-center text-base/7 font-semibold text-indigo-700"
+ initial={{ opacity: 0, y: -20 }}
+ whileInView={{ opacity: 1, y: 0 }}
+ transition={{ duration: 0.5 }}
+ >
+ Learn In a Better Way
+ </motion.h2>
+ <motion.p
+ className="mx-auto mt-2 max-w-lg text-center text-4xl font-semibold tracking-tight sm:text-5xl"
+ initial={{ opacity: 0, y: -20 }}
+ whileInView={{ opacity: 1, y: 0 }}
+ transition={{ duration: 0.5, delay: 0.1 }}
+ >
+ Our Services
+ </motion.p>
+ <div className="mt-10 grid gap-4 sm:mt-16 lg:grid-cols-3 lg:grid-rows-2">
+ <motion.div
+ className="relative lg:row-span-2"
+ initial={{ opacity: 0, y: 20 }}
+ whileInView={{ opacity: 1, y: 0 }}
+ transition={{ delay: 0.2, duration: 0.2 }}
+ >
+ <div className="absolute inset-px rounded-lg bg-white/30 outline outline-black/5 lg:rounded-l-4xl" />
+ <div className="relative flex h-full flex-col overflow-hidden rounded-[calc(var(--radius-lg)+1px)] lg:rounded-l-[calc(2rem+1px)]">
+ <div className="px-8 pt-8 pb-3 sm:px-10 sm:pt-10 sm:pb-0">
+ <p className="mt-2 text-lg font-medium tracking-tight max-lg:text-center">
+ Tutoring at All Grade Levels
+ </p>
+ <p className="mt-2 text-sm/6 text-gray-600 max-lg:text-center">
+ Our tutors cover a wide range of subjects and grade levels,
+ </p>
+ </div>
+ <div className="@container relative min-h-120 w-full grow max-lg:mx-auto max-lg:max-w-sm">
+ <div className="absolute inset-x-10 top-10 bottom-0 overflow-hidden rounded-t-[12cqw] border-x-[3cqw] border-t-[3cqw] border-gray-700 bg-gray-900 outline outline-white/20">
+ <img
+ alt=""
+ src="https://tailwindcss.com/plus-assets/img/component-images/bento-03-mobile-friendly.png"
+ className="size-full object-cover object-top"
+ />
+ </div>
+ </div>
+ </div>
+ <div className="pointer-events-none absolute inset-px rounded-lg shadow-sm outline outline-white/15 lg:rounded-l-4xl" />
+ </motion.div>
+ <motion.div
+ className="relative max-lg:row-start-1"
+ initial={{ opacity: 0, y: 20 }}
+ whileInView={{
+ opacity: 1,
+ y: 0,
+ transition: { delay: 0.2, duration: 0.2 },
+ }}
+ >
+ <div className="absolute inset-px rounded-lg bg-white/30 outline outline-black/5 max-lg:rounded-t-4xl" />
+ <div className="relative flex h-full flex-col overflow-hidden rounded-[calc(var(--radius-lg)+1px)] max-lg:rounded-t-[calc(2rem+1px)]">
+ <div className="px-8 pt-8 sm:px-10 sm:pt-10">
+ <p className="mt-2 text-lg font-medium tracking-tight max-lg:text-center">
+ SAT / ACT Prep
+ </p>
+ <p className="mt-2 text-sm/6 text-gray-600 max-lg:text-center">
+ We only hire tutors who have been high-performing students
+ with previous teaching experience. Most of our tutors are
+ currently pursuing PhDs or Masters in their respective fields.
+ </p>
+ </div>
+ <div className="flex flex-1 items-center justify-center px-8 max-lg:pt-10 max-lg:pb-12 sm:px-10 lg:pb-2">
+ <img
+ alt=""
+ src="https://tailwindcss.com/plus-assets/img/component-images/dark-bento-03-performance.png"
+ className="w-full max-lg:max-w-xs"
+ />
+ </div>
+ </div>
+ <div className="pointer-events-none absolute inset-px rounded-lg shadow-sm outline outline-white/15 max-lg:rounded-t-4xl" />
+ </motion.div>
+ <motion.div
+ className="relative max-lg:row-start-3 lg:col-start-2 lg:row-start-2"
+ initial={{ opacity: 0, y: 20 }}
+ whileInView={{
+ opacity: 1,
+ y: 0,
+ transition: { delay: 0.2, duration: 0.2 },
+ }}
+ >
+ <div className="absolute inset-px rounded-lg bg-white/30 outline outline-black/5" />
+ <div className="relative flex h-full flex-col overflow-hidden rounded-[calc(var(--radius-lg)+1px)]">
+ <div className="px-8 pt-8 sm:px-10 sm:pt-10">
+ <p className="mt-2 text-lg font-medium tracking-tight max-lg:text-center">
+ Secure Technological Integration
+ </p>
+ <p className="mt-2 text-sm/6 text-gray-700 max-lg:text-center">
+ While applying technology to make learning the best it can be,
+ we prioritize security and privacy to protect our users' data
+ and ensure a safe learning environment.
+ </p>
+ </div>
+ <div className="@container flex flex-1 items-center max-lg:py-6 lg:pb-2">
+ <img
+ alt=""
+ src="https://tailwindcss.com/plus-assets/img/component-images/dark-bento-03-security.png"
+ className="h-[min(152px,40cqw)] object-cover"
+ />
+ </div>
+ </div>
+ <div className="pointer-events-none absolute inset-px rounded-lg shadow-sm outline outline-white/15" />
+ </motion.div>
+ <motion.div
+ className="relative lg:row-span-2"
+ initial={{ opacity: 0, y: 20 }}
+ whileInView={{
+ opacity: 1,
+ y: 0,
+ transition: { delay: 0.2, duration: 0.2 },
+ }}
+ >
+ <div className="absolute inset-px rounded-lg bg-white/30 outline outline-black/5 max-lg:rounded-b-4xl lg:rounded-r-4xl" />
+ <div className="relative flex h-full flex-col overflow-hidden rounded-[calc(var(--radius-lg)+1px)] max-lg:rounded-b-[calc(2rem+1px)] lg:rounded-r-[calc(2rem+1px)]">
+ <div className="px-8 pt-8 pb-3 sm:px-10 sm:pt-10 sm:pb-0">
+ <p className="mt-2 text-lg font-medium tracking-tight max-lg:text-center">
+ Competitive Math and Coding Counseling
+ </p>
+ <p className="mt-2 text-sm/6 text-gray-600 max-lg:text-center">
+ Our handcrafted AI learns from your sessions and provides
+ personalized summaries, practice problems, and study plans to
+ help you succeed.
+ </p>
+ </div>
+ <div className="relative min-h-120 w-full grow">
+ <div className="absolute top-10 right-0 bottom-0 left-10 overflow-hidden rounded-tl-xl bg-gray-900/60 outline outline-white/10">
+ <div className="flex bg-gray-900 outline outline-white/5">
+ <div className="-mb-px flex text-sm/6 font-medium text-gray-400">
+ <div className="border-r border-b border-r-white/10 border-b-white/20 bg-white/5 px-4 py-2 text-white">
+ Week1_Session_Precalc.md
+ </div>
+ <div className="border-r border-gray-600/10 px-4 py-2">
+ Week2_Session_Precalc.md
+ </div>
+ </div>
+ </div>
+ <div className="px-6 pt-6 pb-14">
+ <p className="text-sm/6 text-gray-400">AI Summary:</p>
+ <p className="text-sm/6 text-gray-400">
+ This is a summary of the AI's findings and recommendations
+ based on the user's input and interactions.
+ </p>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div className="pointer-events-none absolute inset-px rounded-lg shadow-sm outline outline-white/15 max-lg:rounded-b-4xl lg:rounded-r-4xl" />
+ </motion.div>
+ </div>
+ </section>
+ </div>
+ );
+}
diff --git a/src/components/ui/background-ripple-effect.tsx b/src/components/ui/background-ripple-effect.tsx
new file mode 100644
index 0000000..d9d265b
--- /dev/null
+++ b/src/components/ui/background-ripple-effect.tsx
@@ -0,0 +1,132 @@
+"use client";
+import React, { useMemo, useRef, useState } from "react";
+import { cn } from "@/lib/utils";
+
+export const BackgroundRippleEffect = ({
+ rows = 8,
+ cols = 27,
+ cellSize = 56,
+}: {
+ rows?: number;
+ cols?: number;
+ cellSize?: number;
+}) => {
+ const [clickedCell, setClickedCell] = useState<{
+ row: number;
+ col: number;
+ } | null>(null);
+ const [rippleKey, setRippleKey] = useState(0);
+ const ref = useRef<any>(null);
+
+ return (
+ <div
+ ref={ref}
+ className={cn(
+ "absolute inset-0 h-full w-full",
+ "[--cell-border-color:var(--color-neutral-300)] [--cell-fill-color:var(--color-neutral-100)] [--cell-shadow-color:var(--color-neutral-500)]"
+ )}
+ >
+ <div className="relative h-auto w-auto overflow-hidden">
+ <div className="pointer-events-none absolute inset-0 z-[2] h-full w-full overflow-hidden" />
+ <DivGrid
+ key={`base-${rippleKey}`}
+ className="mask-radial-from-20% mask-radial-at-top opacity-600"
+ rows={rows}
+ cols={cols}
+ cellSize={cellSize}
+ borderColor="var(--cell-border-color)"
+ fillColor="var(--cell-fill-color)"
+ clickedCell={clickedCell}
+ onCellClick={(row, col) => {
+ setClickedCell({ row, col });
+ setRippleKey((k) => k + 1);
+ }}
+ interactive
+ />
+ </div>
+ </div>
+ );
+};
+
+type DivGridProps = {
+ className?: string;
+ rows: number;
+ cols: number;
+ cellSize: number; // in pixels
+ borderColor: string;
+ fillColor: string;
+ clickedCell: { row: number; col: number } | null;
+ onCellClick?: (row: number, col: number) => void;
+ interactive?: boolean;
+};
+
+type CellStyle = React.CSSProperties & {
+ ["--delay"]?: string;
+ ["--duration"]?: string;
+};
+
+const DivGrid = ({
+ className,
+ rows = 7,
+ cols = 30,
+ cellSize = 56,
+ borderColor = "#3f3f46",
+ fillColor = "rgba(14,165,233,0.3)",
+ clickedCell = null,
+ onCellClick = () => {},
+ interactive = true,
+}: DivGridProps) => {
+ const cells = useMemo(
+ () => Array.from({ length: rows * cols }, (_, idx) => idx),
+ [rows, cols]
+ );
+
+ const gridStyle: React.CSSProperties = {
+ display: "grid",
+ gridTemplateColumns: `repeat(${cols}, ${cellSize}px)`,
+ gridTemplateRows: `repeat(${rows}, ${cellSize}px)`,
+ width: cols * cellSize,
+ height: rows * cellSize,
+ marginInline: "auto",
+ };
+
+ return (
+ <div className={cn("relative z-[3]", className)} style={gridStyle}>
+ {cells.map((idx) => {
+ const rowIdx = Math.floor(idx / cols);
+ const colIdx = idx % cols;
+ const distance = clickedCell
+ ? Math.hypot(clickedCell.row - rowIdx, clickedCell.col - colIdx)
+ : 0;
+ const delay = clickedCell ? Math.max(0, distance * 55) : 0; // ms
+ const duration = 200 + distance * 80; // ms
+
+ const style: CellStyle = clickedCell
+ ? {
+ "--delay": `${delay}ms`,
+ "--duration": `${duration}ms`,
+ }
+ : {};
+
+ return (
+ <div
+ key={idx}
+ className={cn(
+ "cell relative border-[0.5px] opacity-40 transition-opacity duration-150 will-change-transform hover:opacity-80",
+ clickedCell && "animate-cell-ripple [animation-fill-mode:none]",
+ !interactive && "pointer-events-none"
+ )}
+ style={{
+ backgroundColor: fillColor,
+ borderColor: borderColor,
+ ...style,
+ }}
+ onClick={
+ interactive ? () => onCellClick?.(rowIdx, colIdx) : undefined
+ }
+ />
+ );
+ })}
+ </div>
+ );
+};
diff --git a/src/components/ui/canvas-reveal-effect.tsx b/src/components/ui/canvas-reveal-effect.tsx
new file mode 100644
index 0000000..f84d7a8
--- /dev/null
+++ b/src/components/ui/canvas-reveal-effect.tsx
@@ -0,0 +1,308 @@
+"use client";
+import { cn } from "@/lib/utils";
+import { Canvas, useFrame, useThree } from "@react-three/fiber";
+import React, { useMemo, useRef } from "react";
+import * as THREE from "three";
+
+export const CanvasRevealEffect = ({
+ animationSpeed = 0.4,
+ opacities = [0.3, 0.3, 0.3, 0.5, 0.5, 0.5, 0.8, 0.8, 0.8, 1],
+ colors = [[0, 255, 255]],
+ containerClassName,
+ dotSize,
+ showGradient = false,
+}: {
+ /**
+ * 0.1 - slower
+ * 1.0 - faster
+ */
+ animationSpeed?: number;
+ opacities?: number[];
+ colors?: number[][];
+ containerClassName?: string;
+ dotSize?: number;
+ showGradient?: boolean;
+}) => {
+ return (
+ <div className={cn("h-full relative bg-white w-full", containerClassName)}>
+ <div className="h-full w-full">
+ <DotMatrix
+ colors={colors ?? [[0, 255, 255]]}
+ dotSize={dotSize ?? 3}
+ opacities={
+ opacities ?? [0.3, 0.3, 0.3, 0.5, 0.5, 0.5, 0.8, 0.8, 0.8, 1]
+ }
+ shader={`
+ float animation_speed_factor = ${animationSpeed.toFixed(1)};
+ float intro_offset = distance(u_resolution / 2.0 / u_total_size, st2) * 0.01 + (random(st2) * 0.15);
+ opacity *= step(intro_offset, u_time * animation_speed_factor);
+ opacity *= clamp((1.0 - step(intro_offset + 0.1, u_time * animation_speed_factor)) * 1.25, 1.0, 1.25);
+ `}
+ center={["x", "y"]}
+ />
+ </div>
+ {showGradient && (
+ <div className="absolute inset-0 bg-gradient-to-t from-gray-950 to-[84%]" />
+ )}
+ </div>
+ );
+};
+
+interface DotMatrixProps {
+ colors?: number[][];
+ opacities?: number[];
+ totalSize?: number;
+ dotSize?: number;
+ shader?: string;
+ center?: ("x" | "y")[];
+}
+
+const DotMatrix: React.FC<DotMatrixProps> = ({
+ colors = [[0, 0, 0]],
+ opacities = [0.04, 0.04, 0.04, 0.04, 0.04, 0.08, 0.08, 0.08, 0.08, 0.14],
+ totalSize = 4,
+ dotSize = 2,
+ shader = "",
+ center = ["x", "y"],
+}) => {
+ const uniforms = React.useMemo(() => {
+ let colorsArray = [
+ colors[0],
+ colors[0],
+ colors[0],
+ colors[0],
+ colors[0],
+ colors[0],
+ ];
+ if (colors.length === 2) {
+ colorsArray = [
+ colors[0],
+ colors[0],
+ colors[0],
+ colors[1],
+ colors[1],
+ colors[1],
+ ];
+ } else if (colors.length === 3) {
+ colorsArray = [
+ colors[0],
+ colors[0],
+ colors[1],
+ colors[1],
+ colors[2],
+ colors[2],
+ ];
+ }
+
+ return {
+ u_colors: {
+ value: colorsArray.map((color) => [
+ color[0] / 255,
+ color[1] / 255,
+ color[2] / 255,
+ ]),
+ type: "uniform3fv",
+ },
+ u_opacities: {
+ value: opacities,
+ type: "uniform1fv",
+ },
+ u_total_size: {
+ value: totalSize,
+ type: "uniform1f",
+ },
+ u_dot_size: {
+ value: dotSize,
+ type: "uniform1f",
+ },
+ };
+ }, [colors, opacities, totalSize, dotSize]);
+
+ return (
+ <Shader
+ source={`
+ precision mediump float;
+ in vec2 fragCoord;
+
+ uniform float u_time;
+ uniform float u_opacities[10];
+ uniform vec3 u_colors[6];
+ uniform float u_total_size;
+ uniform float u_dot_size;
+ uniform vec2 u_resolution;
+ out vec4 fragColor;
+ float PHI = 1.61803398874989484820459;
+ float random(vec2 xy) {
+ return fract(tan(distance(xy * PHI, xy) * 0.5) * xy.x);
+ }
+ float map(float value, float min1, float max1, float min2, float max2) {
+ return min2 + (value - min1) * (max2 - min2) / (max1 - min1);
+ }
+ void main() {
+ vec2 st = fragCoord.xy;
+ ${
+ center.includes("x")
+ ? "st.x -= abs(floor((mod(u_resolution.x, u_total_size) - u_dot_size) * 0.5));"
+ : ""
+ }
+ ${
+ center.includes("y")
+ ? "st.y -= abs(floor((mod(u_resolution.y, u_total_size) - u_dot_size) * 0.5));"
+ : ""
+ }
+ float opacity = step(0.0, st.x);
+ opacity *= step(0.0, st.y);
+
+ vec2 st2 = vec2(int(st.x / u_total_size), int(st.y / u_total_size));
+
+ float frequency = 5.0;
+ float show_offset = random(st2);
+ float rand = random(st2 * floor((u_time / frequency) + show_offset + frequency) + 1.0);
+ opacity *= u_opacities[int(rand * 10.0)];
+ opacity *= 1.0 - step(u_dot_size / u_total_size, fract(st.x / u_total_size));
+ opacity *= 1.0 - step(u_dot_size / u_total_size, fract(st.y / u_total_size));
+
+ vec3 color = u_colors[int(show_offset * 6.0)];
+
+ ${shader}
+
+ fragColor = vec4(color, opacity);
+ fragColor.rgb *= fragColor.a;
+ }`}
+ uniforms={uniforms}
+ maxFps={60}
+ />
+ );
+};
+
+type Uniforms = {
+ [key: string]: {
+ value: number[] | number[][] | number;
+ type: string;
+ };
+};
+const ShaderMaterial = ({
+ source,
+ uniforms,
+ maxFps = 60,
+}: {
+ source: string;
+ hovered?: boolean;
+ maxFps?: number;
+ uniforms: Uniforms;
+}) => {
+ const { size } = useThree();
+ const ref = useRef<THREE.Mesh>();
+ let lastFrameTime = 0;
+
+ useFrame(({ clock }) => {
+ if (!ref.current) return;
+ const timestamp = clock.getElapsedTime();
+ if (timestamp - lastFrameTime < 1 / maxFps) {
+ return;
+ }
+ lastFrameTime = timestamp;
+
+ const material: any = ref.current.material;
+ const timeLocation = material.uniforms.u_time;
+ timeLocation.value = timestamp;
+ });
+
+ const getUniforms = () => {
+ const preparedUniforms: any = {};
+
+ for (const uniformName in uniforms) {
+ const uniform: any = uniforms[uniformName];
+
+ switch (uniform.type) {
+ case "uniform1f":
+ preparedUniforms[uniformName] = { value: uniform.value, type: "1f" };
+ break;
+ case "uniform3f":
+ preparedUniforms[uniformName] = {
+ value: new THREE.Vector3().fromArray(uniform.value),
+ type: "3f",
+ };
+ break;
+ case "uniform1fv":
+ preparedUniforms[uniformName] = { value: uniform.value, type: "1fv" };
+ break;
+ case "uniform3fv":
+ preparedUniforms[uniformName] = {
+ value: uniform.value.map((v: number[]) =>
+ new THREE.Vector3().fromArray(v)
+ ),
+ type: "3fv",
+ };
+ break;
+ case "uniform2f":
+ preparedUniforms[uniformName] = {
+ value: new THREE.Vector2().fromArray(uniform.value),
+ type: "2f",
+ };
+ break;
+ default:
+ console.error(`Invalid uniform type for '${uniformName}'.`);
+ break;
+ }
+ }
+
+ preparedUniforms["u_time"] = { value: 0, type: "1f" };
+ preparedUniforms["u_resolution"] = {
+ value: new THREE.Vector2(size.width * 2, size.height * 2),
+ }; // Initialize u_resolution
+ return preparedUniforms;
+ };
+
+ // Shader material
+ const material = useMemo(() => {
+ const materialObject = new THREE.ShaderMaterial({
+ vertexShader: `
+ precision mediump float;
+ in vec2 coordinates;
+ uniform vec2 u_resolution;
+ out vec2 fragCoord;
+ void main(){
+ float x = position.x;
+ float y = position.y;
+ gl_Position = vec4(x, y, 0.0, 1.0);
+ fragCoord = (position.xy + vec2(1.0)) * 0.5 * u_resolution;
+ fragCoord.y = u_resolution.y - fragCoord.y;
+ }
+ `,
+ fragmentShader: source,
+ uniforms: getUniforms(),
+ glslVersion: THREE.GLSL3,
+ blending: THREE.CustomBlending,
+ blendSrc: THREE.SrcAlphaFactor,
+ blendDst: THREE.OneFactor,
+ });
+
+ return materialObject;
+ }, [size.width, size.height, source]);
+
+ return (
+ <mesh ref={ref as any}>
+ <planeGeometry args={[2, 2]} />
+ <primitive object={material} attach="material" />
+ </mesh>
+ );
+};
+
+const Shader: React.FC<ShaderProps> = ({ source, uniforms, maxFps = 60 }) => {
+ return (
+ <Canvas className="absolute inset-0 h-full w-full">
+ <ShaderMaterial source={source} uniforms={uniforms} maxFps={maxFps} />
+ </Canvas>
+ );
+};
+interface ShaderProps {
+ source: string;
+ uniforms: {
+ [key: string]: {
+ value: number[] | number[][] | number;
+ type: string;
+ };
+ };
+ maxFps?: number;
+}
diff --git a/src/components/ui/card-spotlight.tsx b/src/components/ui/card-spotlight.tsx
new file mode 100644
index 0000000..f0b17d7
--- /dev/null
+++ b/src/components/ui/card-spotlight.tsx
@@ -0,0 +1,74 @@
+"use client";
+
+import { useMotionValue, motion, useMotionTemplate } from "motion/react";
+import React, { MouseEvent as ReactMouseEvent, useState } from "react";
+import { CanvasRevealEffect } from "@/components/ui/canvas-reveal-effect";
+import { cn } from "@/lib/utils";
+
+export const CardSpotlight = ({
+ children,
+ radius = 350,
+ color = "#ffffff33",
+ className,
+ ...props
+}: {
+ radius?: number;
+ color?: string;
+ children: React.ReactNode;
+} & React.HTMLAttributes<HTMLDivElement>) => {
+ const mouseX = useMotionValue(0);
+ const mouseY = useMotionValue(0);
+ function handleMouseMove({
+ currentTarget,
+ clientX,
+ clientY,
+ }: ReactMouseEvent<HTMLDivElement>) {
+ let { left, top } = currentTarget.getBoundingClientRect();
+
+ mouseX.set(clientX - left);
+ mouseY.set(clientY - top);
+ }
+
+ const [isHovering, setIsHovering] = useState(false);
+ const handleMouseEnter = () => setIsHovering(true);
+ const handleMouseLeave = () => setIsHovering(false);
+ return (
+ <div
+ className={cn(
+ "group/spotlight p-10 rounded-md relative border border-neutral-800 bg-black dark:border-neutral-800",
+ className
+ )}
+ onMouseMove={handleMouseMove}
+ onMouseEnter={handleMouseEnter}
+ onMouseLeave={handleMouseLeave}
+ {...props}
+ >
+ <motion.div
+ className="pointer-events-none absolute z-0 -inset-px rounded-md opacity-0 transition duration-300 group-hover/spotlight:opacity-100"
+ style={{
+ backgroundColor: color,
+ maskImage: useMotionTemplate`
+ radial-gradient(
+ ${radius}px circle at ${mouseX}px ${mouseY}px,
+ white,
+ transparent 80%
+ )
+ `,
+ }}
+ >
+ {isHovering && (
+ <CanvasRevealEffect
+ animationSpeed={5}
+ containerClassName="bg-transparent absolute inset-0 pointer-events-none"
+ colors={[
+ [59, 130, 246],
+ [139, 92, 246],
+ ]}
+ dotSize={3}
+ />
+ )}
+ </motion.div>
+ {children}
+ </div>
+ );
+};
diff --git a/src/components/ui/hover-border-gradient.tsx b/src/components/ui/hover-border-gradient.tsx
new file mode 100644
index 0000000..898f575
--- /dev/null
+++ b/src/components/ui/hover-border-gradient.tsx
@@ -0,0 +1,100 @@
+"use client";
+import React, { useState, useEffect, useRef } from "react";
+
+import { motion } from "motion/react";
+import { cn } from "@/lib/utils";
+
+type Direction = "TOP" | "LEFT" | "BOTTOM" | "RIGHT";
+
+export function HoverBorderGradient({
+ children,
+ containerClassName,
+ className,
+ as: Tag = "button",
+ duration = 1,
+ clockwise = true,
+ ...props
+}: React.PropsWithChildren<
+ {
+ as?: React.ElementType;
+ containerClassName?: string;
+ className?: string;
+ duration?: number;
+ clockwise?: boolean;
+ } & React.HTMLAttributes<HTMLElement>
+>) {
+ const [hovered, setHovered] = useState<boolean>(false);
+ const [direction, setDirection] = useState<Direction>("TOP");
+
+ const rotateDirection = (currentDirection: Direction): Direction => {
+ const directions: Direction[] = ["TOP", "LEFT", "BOTTOM", "RIGHT"];
+ const currentIndex = directions.indexOf(currentDirection);
+ const nextIndex = clockwise
+ ? (currentIndex - 1 + directions.length) % directions.length
+ : (currentIndex + 1) % directions.length;
+ return directions[nextIndex];
+ };
+
+ const movingMap: Record<Direction, string> = {
+ TOP: "radial-gradient(20.7% 50% at 50% 0%, hsl(0, 0%, 100%) 0%, rgba(255, 255, 255, 0) 100%)",
+ LEFT: "radial-gradient(16.6% 43.1% at 0% 50%, hsl(0, 0%, 100%) 0%, rgba(255, 255, 255, 0) 100%)",
+ BOTTOM:
+ "radial-gradient(20.7% 50% at 50% 100%, hsl(0, 0%, 100%) 0%, rgba(255, 255, 255, 0) 100%)",
+ RIGHT:
+ "radial-gradient(16.2% 41.199999999999996% at 100% 50%, hsl(0, 0%, 100%) 0%, rgba(255, 255, 255, 0) 100%)",
+ };
+
+ const highlight =
+ "radial-gradient(75% 181.15942028985506% at 50% 50%, #3275F8 0%, rgba(255, 255, 255, 0) 100%)";
+
+ useEffect(() => {
+ if (!hovered) {
+ const interval = setInterval(() => {
+ setDirection((prevState) => rotateDirection(prevState));
+ }, duration * 1000);
+ return () => clearInterval(interval);
+ }
+ }, [hovered]);
+ return (
+ <Tag
+ onMouseEnter={(event: React.MouseEvent<HTMLDivElement>) => {
+ setHovered(true);
+ }}
+ onMouseLeave={() => setHovered(false)}
+ className={cn(
+ "relative flex rounded-full border border-indigo-300/50 content-center bg-black/20 hover:bg-black/10 transition duration-500 items-center flex-col flex-nowrap gap-10 h-min justify-center overflow-visible p-px decoration-clone w-fit",
+ containerClassName
+ )}
+ {...props}
+ >
+ <div
+ className={cn(
+ "w-auto text-white z-10 bg-black px-4 py-2 rounded-[inherit]",
+ className
+ )}
+ >
+ {children}
+ </div>
+ <motion.div
+ className={cn(
+ "flex-none inset-0 overflow-hidden absolute z-0 rounded-[inherit]"
+ )}
+ style={{
+ filter: "blur(2px)",
+ position: "absolute",
+ width: "100%",
+ height: "110%",
+ margin: "auto",
+ }}
+ initial={{ background: movingMap[direction] }}
+ animate={{
+ background: hovered
+ ? [movingMap[direction], highlight]
+ : movingMap[direction],
+ }}
+ transition={{ ease: "linear", duration: duration ?? 1 }}
+ />
+ <div className="bg-black absolute z-1 flex-none inset-[2px] rounded-[100px]" />
+ </Tag>
+ );
+}
diff --git a/src/components/ui/wavy-background.tsx b/src/components/ui/wavy-background.tsx
new file mode 100644
index 0000000..4fbc374
--- /dev/null
+++ b/src/components/ui/wavy-background.tsx
@@ -0,0 +1,132 @@
+"use client";
+import { cn } from "@/lib/utils";
+import React, { useEffect, useRef, useState } from "react";
+import { createNoise3D } from "simplex-noise";
+
+export const WavyBackground = ({
+ children,
+ className,
+ containerClassName,
+ colors,
+ waveWidth,
+ backgroundFill,
+ blur = 10,
+ speed = "fast",
+ waveOpacity = 0.5,
+ ...props
+}: {
+ children?: any;
+ className?: string;
+ containerClassName?: string;
+ colors?: string[];
+ waveWidth?: number;
+ backgroundFill?: string;
+ blur?: number;
+ speed?: "slow" | "fast";
+ waveOpacity?: number;
+ [key: string]: any;
+}) => {
+ const noise = createNoise3D();
+ let w: number,
+ h: number,
+ nt: number,
+ i: number,
+ x: number,
+ ctx: any,
+ canvas: any;
+ const canvasRef = useRef<HTMLCanvasElement>(null);
+ const getSpeed = () => {
+ switch (speed) {
+ case "slow":
+ return 0.001;
+ case "fast":
+ return 0.002;
+ default:
+ return 0.001;
+ }
+ };
+
+ const init = () => {
+ canvas = canvasRef.current;
+ ctx = canvas.getContext("2d");
+ w = ctx.canvas.width = window.innerWidth;
+ h = ctx.canvas.height = window.innerHeight;
+ ctx.filter = `blur(${blur}px)`;
+ nt = 0;
+ window.onresize = function () {
+ w = ctx.canvas.width = window.innerWidth;
+ h = ctx.canvas.height = window.innerHeight;
+ ctx.filter = `blur(${blur}px)`;
+ };
+ render();
+ };
+
+ const waveColors = colors ?? [
+ "#38bdf8",
+ "#818cf8",
+ "#c084fc",
+ "#e879f9",
+ "#22d3ee",
+ ];
+ const drawWave = (n: number) => {
+ nt += getSpeed();
+ for (i = 0; i < n; i++) {
+ ctx.beginPath();
+ ctx.lineWidth = waveWidth || 50;
+ ctx.strokeStyle = waveColors[i % waveColors.length];
+ for (x = 0; x < w; x += 5) {
+ var y = noise(x / 800, 0.3 * i, nt) * 100;
+ ctx.lineTo(x, y + h * 0.5); // adjust for height, currently at 50% of the container
+ }
+ ctx.stroke();
+ ctx.closePath();
+ }
+ };
+
+ let animationId: number;
+ const render = () => {
+ ctx.fillStyle = backgroundFill || "black";
+ ctx.globalAlpha = waveOpacity || 0.5;
+ ctx.fillRect(0, 0, w, h);
+ drawWave(5);
+ animationId = requestAnimationFrame(render);
+ };
+
+ useEffect(() => {
+ init();
+ return () => {
+ cancelAnimationFrame(animationId);
+ };
+ }, []);
+
+ const [isSafari, setIsSafari] = useState(false);
+ useEffect(() => {
+ // I'm sorry but i have got to support it on safari.
+ setIsSafari(
+ typeof window !== "undefined" &&
+ navigator.userAgent.includes("Safari") &&
+ !navigator.userAgent.includes("Chrome")
+ );
+ }, []);
+
+ return (
+ <div
+ className={cn(
+ "h-screen flex flex-col items-center justify-center",
+ containerClassName
+ )}
+ >
+ <canvas
+ className="absolute inset-0 z-0"
+ ref={canvasRef}
+ id="canvas"
+ style={{
+ ...(isSafari ? { filter: `blur(${blur}px)` } : {}),
+ }}
+ ></canvas>
+ <div className={cn("relative z-10", className)} {...props}>
+ {children}
+ </div>
+ </div>
+ );
+};
diff --git a/src/components/ui/wooble-card.tsx b/src/components/ui/wooble-card.tsx
new file mode 100644
index 0000000..05c059a
--- /dev/null
+++ b/src/components/ui/wooble-card.tsx
@@ -0,0 +1,78 @@
+"use client";
+import { cn } from "@/lib/utils";
+import { motion } from "motion/react";
+import React, { useState } from "react";
+
+export const WobbleCard = ({
+ children,
+ containerClassName,
+ className,
+}: {
+ children: React.ReactNode;
+ containerClassName?: string;
+ className?: string;
+}) => {
+ const [mousePosition, setMousePosition] = useState({ x: 0, y: 0 });
+ const [isHovering, setIsHovering] = useState(false);
+
+ const handleMouseMove = (event: React.MouseEvent<HTMLElement>) => {
+ const { clientX, clientY } = event;
+ const rect = event.currentTarget.getBoundingClientRect();
+ const x = (clientX - (rect.left + rect.width / 2)) / 20;
+ const y = (clientY - (rect.top + rect.height / 2)) / 20;
+ setMousePosition({ x, y });
+ };
+ return (
+ <motion.section
+ onMouseMove={handleMouseMove}
+ onMouseEnter={() => setIsHovering(true)}
+ onMouseLeave={() => {
+ setIsHovering(false);
+ setMousePosition({ x: 0, y: 0 });
+ }}
+ style={{
+ transform: isHovering
+ ? `translate3d(${mousePosition.x}px, ${mousePosition.y}px, 0) scale3d(1, 1, 1)`
+ : "translate3d(0px, 0px, 0) scale3d(1, 1, 1)",
+ transition: "transform 0.1s ease-out",
+ }}
+ className={cn(
+ "mx-auto w-full bg-indigo-800 relative rounded-2xl overflow-hidden",
+ containerClassName
+ )}
+ >
+ <div
+ className="relative h-full [background-image:radial-gradient(88%_100%_at_top,rgba(255,255,255,0.5),rgba(255,255,255,0))] sm:mx-0 sm:rounded-2xl overflow-hidden"
+ style={{
+ boxShadow:
+ "0 10px 32px rgba(34, 42, 53, 0.12), 0 1px 1px rgba(0, 0, 0, 0.05), 0 0 0 1px rgba(34, 42, 53, 0.05), 0 4px 6px rgba(34, 42, 53, 0.08), 0 24px 108px rgba(47, 48, 55, 0.10)",
+ }}
+ >
+ <motion.div
+ style={{
+ transform: isHovering
+ ? `translate3d(${-mousePosition.x}px, ${-mousePosition.y}px, 0) scale3d(1.03, 1.03, 1)`
+ : "translate3d(0px, 0px, 0) scale3d(1, 1, 1)",
+ transition: "transform 0.1s ease-out",
+ }}
+ className={cn("h-full px-4 py-20 sm:px-10", className)}
+ >
+ <Noise />
+ {children}
+ </motion.div>
+ </div>
+ </motion.section>
+ );
+};
+
+const Noise = () => {
+ return (
+ <div
+ className="absolute inset-0 w-full h-full scale-[1.2] transform opacity-10 [mask-image:radial-gradient(#fff,transparent,75%)]"
+ style={{
+ backgroundImage: "url(/noise.webp)",
+ backgroundSize: "30%",
+ }}
+ ></div>
+ );
+};
diff --git a/src/components/ui/world-map.tsx b/src/components/ui/world-map.tsx
new file mode 100644
index 0000000..d6c9891
--- /dev/null
+++ b/src/components/ui/world-map.tsx
@@ -0,0 +1,167 @@
+"use client";
+
+import { useRef } from "react";
+import { motion } from "motion/react";
+import DottedMap from "dotted-map";
+
+import { useTheme } from "next-themes";
+
+interface MapProps {
+ dots?: Array<{
+ start: { lat: number; lng: number; label?: string };
+ end: { lat: number; lng: number; label?: string };
+ }>;
+ lineColor?: string;
+}
+
+export function WorldMap({ dots = [], lineColor = "#0ea5e9" }: MapProps) {
+ const svgRef = useRef<SVGSVGElement>(null);
+ const map = new DottedMap({ height: 100, grid: "diagonal" });
+
+ const { theme } = useTheme();
+
+ const svgMap = map.getSVG({
+ radius: 0.22,
+ color: theme === "dark" ? "#FFFFFF40" : "#00000040",
+ shape: "circle",
+ backgroundColor: theme === "dark" ? "black" : "white",
+ });
+
+ const projectPoint = (lat: number, lng: number) => {
+ const x = (lng + 180) * (800 / 360);
+ const y = (90 - lat) * (400 / 180);
+ return { x, y };
+ };
+
+ const createCurvedPath = (
+ start: { x: number; y: number },
+ end: { x: number; y: number }
+ ) => {
+ const midX = (start.x + end.x) / 2;
+ const midY = Math.min(start.y, end.y) - 50;
+ return `M ${start.x} ${start.y} Q ${midX} ${midY} ${end.x} ${end.y}`;
+ };
+
+ return (
+ <div className="w-full aspect-[2/1] dark:bg-black bg-white rounded-lg relative font-sans">
+ <img
+ src={`data:image/svg+xml;utf8,${encodeURIComponent(svgMap)}`}
+ className="h-full w-full [mask-image:linear-gradient(to_bottom,transparent,white_10%,white_90%,transparent)] pointer-events-none select-none"
+ alt="world map"
+ height="495"
+ width="1056"
+ draggable={false}
+ />
+ <svg
+ ref={svgRef}
+ viewBox="0 0 800 400"
+ className="w-full h-full absolute inset-0 pointer-events-none select-none"
+ >
+ {dots.map((dot, i) => {
+ const startPoint = projectPoint(dot.start.lat, dot.start.lng);
+ const endPoint = projectPoint(dot.end.lat, dot.end.lng);
+ return (
+ <g key={`path-group-${i}`}>
+ <motion.path
+ d={createCurvedPath(startPoint, endPoint)}
+ fill="none"
+ stroke="url(#path-gradient)"
+ strokeWidth="1"
+ initial={{
+ pathLength: 0,
+ }}
+ animate={{
+ pathLength: 1,
+ }}
+ transition={{
+ duration: 0,
+ delay: 0.5 * i,
+ ease: "easeOut",
+ }}
+ key={`start-upper-${i}`}
+ ></motion.path>
+ </g>
+ );
+ })}
+
+ <defs>
+ <linearGradient id="path-gradient" x1="0%" y1="0%" x2="100%" y2="0%">
+ <stop offset="0%" stopColor="white" stopOpacity="0" />
+ <stop offset="5%" stopColor={lineColor} stopOpacity="1" />
+ <stop offset="95%" stopColor={lineColor} stopOpacity="1" />
+ <stop offset="100%" stopColor="white" stopOpacity="0" />
+ </linearGradient>
+ </defs>
+
+ {dots.map((dot, i) => (
+ <g key={`points-group-${i}`}>
+ <g key={`start-${i}`}>
+ <circle
+ cx={projectPoint(dot.start.lat, dot.start.lng).x}
+ cy={projectPoint(dot.start.lat, dot.start.lng).y}
+ r="2"
+ fill={lineColor}
+ />
+ <circle
+ cx={projectPoint(dot.start.lat, dot.start.lng).x}
+ cy={projectPoint(dot.start.lat, dot.start.lng).y}
+ r="2"
+ fill={lineColor}
+ opacity="0.5"
+ >
+ <animate
+ attributeName="r"
+ from="2"
+ to="8"
+ dur="1.5s"
+ begin="0s"
+ repeatCount="indefinite"
+ />
+ <animate
+ attributeName="opacity"
+ from="0.5"
+ to="0"
+ dur="1.5s"
+ begin="0s"
+ repeatCount="indefinite"
+ />
+ </circle>
+ </g>
+ <g key={`end-${i}`}>
+ <circle
+ cx={projectPoint(dot.end.lat, dot.end.lng).x}
+ cy={projectPoint(dot.end.lat, dot.end.lng).y}
+ r="2"
+ fill={lineColor}
+ />
+ <circle
+ cx={projectPoint(dot.end.lat, dot.end.lng).x}
+ cy={projectPoint(dot.end.lat, dot.end.lng).y}
+ r="2"
+ fill={lineColor}
+ opacity="0.5"
+ >
+ <animate
+ attributeName="r"
+ from="2"
+ to="8"
+ dur="1.5s"
+ begin="0s"
+ repeatCount="indefinite"
+ />
+ <animate
+ attributeName="opacity"
+ from="0.5"
+ to="0"
+ dur="1.5s"
+ begin="0s"
+ repeatCount="indefinite"
+ />
+ </circle>
+ </g>
+ </g>
+ ))}
+ </svg>
+ </div>
+ );
+}