diff options
Diffstat (limited to 'src/components')
-rw-r--r-- | src/components/GlassHeader.tsx | 11 | ||||
-rw-r--r-- | src/components/Spinner.tsx | 24 | ||||
-rw-r--r-- | src/components/backgrounds/Grid.tsx | 35 | ||||
-rw-r--r-- | src/components/backgrounds/Polygons.tsx | 31 | ||||
-rw-r--r-- | src/components/old/bentonbox.tsx | 165 | ||||
-rw-r--r-- | src/components/ui/background-ripple-effect.tsx | 132 | ||||
-rw-r--r-- | src/components/ui/canvas-reveal-effect.tsx | 308 | ||||
-rw-r--r-- | src/components/ui/card-spotlight.tsx | 74 | ||||
-rw-r--r-- | src/components/ui/hover-border-gradient.tsx | 100 | ||||
-rw-r--r-- | src/components/ui/wavy-background.tsx | 132 | ||||
-rw-r--r-- | src/components/ui/wooble-card.tsx | 78 | ||||
-rw-r--r-- | src/components/ui/world-map.tsx | 167 |
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> + ); +} |