feat: Add Magic UI components including DotPattern, RainbowButton, ShineBorder, WordRotate, and BlurIn, along with corresponding Tailwind configuration updates.

This commit is contained in:
2026-02-03 16:23:28 +00:00
parent 8d1923203d
commit 494acebeb3
14 changed files with 752 additions and 237 deletions

View File

@@ -24,16 +24,23 @@
--input: 240 3.7% 15.9%; --input: 240 3.7% 15.9%;
--ring: 240 4.9% 83.9%; --ring: 240 4.9% 83.9%;
--radius: 0.5rem; --radius: 0.5rem;
--color-1: 0 100% 63%;
--color-2: 270 100% 63%;
--color-3: 210 100% 63%;
--color-4: 195 100% 63%;
--color-5: 90 100% 63%;
} }
@layer base { @layer base {
* { * {
@apply border-border; @apply border-border;
} }
body { body {
@apply bg-background text-foreground; @apply bg-background text-foreground;
font-feature-settings: "rlig" 1, "calt" 1; font-feature-settings: "rlig" 1, "calt" 1;
} }
:root { :root {
--sidebar-background: 0 0% 98%; --sidebar-background: 0 0% 98%;
--sidebar-foreground: 240 5.3% 26.1%; --sidebar-foreground: 240 5.3% 26.1%;
@@ -44,6 +51,7 @@
--sidebar-border: 220 13% 91%; --sidebar-border: 220 13% 91%;
--sidebar-ring: 217.2 91.2% 59.8%; --sidebar-ring: 217.2 91.2% 59.8%;
} }
.dark { .dark {
--sidebar-background: 240 5.9% 10%; --sidebar-background: 240 5.9% 10%;
--sidebar-foreground: 240 4.8% 95.9%; --sidebar-foreground: 240 4.8% 95.9%;

View File

@@ -1,11 +1,14 @@
import type { Metadata } from 'next' import type { Metadata } from 'next'
import { Inter } from 'next/font/google' import { Montserrat } from 'next/font/google'
import './globals.css' import './globals.css'
import ConvexClientProvider from './ConvexClientProvider' import ConvexClientProvider from './ConvexClientProvider'
import { ConvexAuthNextjsServerProvider } from "@convex-dev/auth/nextjs/server"; import { ConvexAuthNextjsServerProvider } from "@convex-dev/auth/nextjs/server";
import { ThemeProvider } from "@/components/theme-provider"; import { ThemeProvider } from "@/components/theme-provider";
const inter = Inter({ subsets: ['latin'] }) const montserrat = Montserrat({
subsets: ['latin'],
weight: ['300', '400', '500', '600', '700'],
})
export const metadata: Metadata = { export const metadata: Metadata = {
title: 'Sanati - Find Product Opportunities', title: 'Sanati - Find Product Opportunities',
@@ -20,7 +23,7 @@ export default function RootLayout({
return ( return (
<ConvexAuthNextjsServerProvider> <ConvexAuthNextjsServerProvider>
<html lang="en" suppressHydrationWarning> <html lang="en" suppressHydrationWarning>
<body className={inter.className}> <body className={montserrat.className}>
<ThemeProvider <ThemeProvider
attribute="class" attribute="class"
defaultTheme="dark" defaultTheme="dark"

View File

@@ -1,91 +1,127 @@
import Link from 'next/link' import Link from 'next/link'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
import { ArrowRight, Search, Zap, Target, Sparkles } from 'lucide-react' import { ArrowRight, Search, Zap, Target, Sparkles } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { DotPattern } from '@/components/magicui/dot-pattern'
import { HeroShader } from '@/components/hero-shader'
import { cn } from '@/lib/utils'
export default function LandingPage() { export default function LandingPage() {
return ( return (
<div className="min-h-screen bg-background"> <div className="relative min-h-screen bg-background overflow-hidden font-sans">
<DotPattern
className={cn(
"[mask-image:radial-gradient(1000px_circle_at_center,white,transparent)]",
"opacity-50"
)}
/>
<HeroShader />
{/* Header */} {/* Header */}
<header className="border-b border-border"> <header className="border-b border-border/40 backdrop-blur-md sticky top-0 z-50">
<div className="container mx-auto max-w-6xl px-4 py-4 flex items-center justify-between"> <div className="container mx-auto max-w-7xl px-4 py-4 flex items-center justify-between">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-primary text-primary-foreground"> <div className="flex h-8 w-8 items-center justify-center rounded-lg bg-primary text-primary-foreground">
<Search className="h-4 w-4" /> <Search className="h-4 w-4" />
</div> </div>
<span className="font-semibold text-foreground">Sanati</span> <span className="font-semibold text-foreground tracking-tight">Sanati</span>
</div> </div>
<nav className="flex items-center gap-4"> <nav className="flex items-center gap-4">
<Link href="/dashboard" className="text-sm text-muted-foreground hover:text-foreground"> <Link href="/dashboard" className="text-sm font-medium text-muted-foreground hover:text-foreground transition-colors">
Dashboard Dashboard
</Link> </Link>
<Link href="/auth"> <Link href="/auth">
<Button size="sm">Get Started</Button> <Button size="sm" className="font-medium">Get Started</Button>
</Link>
</nav>
</div>
</header>
{/* Header */}
<header className="border-b border-border/40 backdrop-blur-md sticky top-0 z-50">
<div className="container mx-auto max-w-7xl px-4 py-4 flex items-center justify-between">
<div className="flex items-center gap-2">
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-primary text-primary-foreground">
<Search className="h-4 w-4" />
</div>
<span className="font-semibold text-foreground tracking-tight">Sanati</span>
</div>
<nav className="flex items-center gap-4">
<Link href="/dashboard" className="text-sm font-medium text-muted-foreground hover:text-foreground transition-colors">
Dashboard
</Link>
<Link href="/auth">
<Button size="sm" className="font-medium">Get Started</Button>
</Link> </Link>
</nav> </nav>
</div> </div>
</header> </header>
{/* Hero */} {/* Hero */}
<section className="container mx-auto max-w-6xl px-4 py-24 lg:py-32"> <section className="container mx-auto max-w-7xl px-4 min-h-[75vh] flex items-center relative z-10">
<div className="flex flex-col items-center text-center space-y-8"> <div className="grid lg:grid-cols-2 gap-12 items-center w-full">
<Badge variant="secondary" className="px-4 py-1.5"> <div className="flex flex-col items-start text-left space-y-8">
<div className="inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80">
<Sparkles className="mr-1 h-3 w-3" /> <Sparkles className="mr-1 h-3 w-3" />
AI-Powered Research AI-Powered Research
</Badge> </div>
<h1 className="text-4xl font-bold tracking-tight sm:text-5xl lg:text-6xl text-foreground"> <h1 className="text-4xl font-light tracking-tight sm:text-5xl lg:text-7xl text-foreground">
Find Your Next Customers Find Your Next
<br /> <br />
<span className="text-muted-foreground">Before They Know They Need You</span> <span className="font-bold">Customers.</span>
</h1> </h1>
<p className="max-w-2xl text-lg text-muted-foreground"> <p className="max-w-xl text-lg text-muted-foreground font-light leading-relaxed">
Sanati analyzes your product and finds people on Reddit, Hacker News, and forums Sanati analyzes your product and finds people on Reddit, Hacker News, and forums
who are actively expressing needs that your solution solves. who are actively expressing needs that your solution solves.
</p> </p>
<div className="flex flex-col sm:flex-row gap-4"> <div className="flex flex-col sm:flex-row gap-4 pt-2">
<Link href="/auth"> <Link href="/auth">
<Button size="lg" className="gap-2"> <Button size="lg" className="gap-2 px-8 text-base">
Start Finding Opportunities Start Finding Opportunities
<ArrowRight className="h-4 w-4" /> <ArrowRight className="h-4 w-4" />
</Button> </Button>
</Link> </Link>
</div> </div>
</div> </div>
{/* Right side is reserved for the shader visualization */}
<div className="hidden lg:block h-full min-h-[400px]"></div>
</div>
</section> </section>
{/* Features */} {/* Features */}
<section className="border-t border-border bg-muted/50"> <section className="border-t border-border/40 bg-muted/20 backdrop-blur-sm relative z-10">
<div className="container mx-auto max-w-6xl px-4 py-24"> <div className="container mx-auto max-w-7xl px-4 py-24">
<div className="grid md:grid-cols-3 gap-8"> <div className="grid md:grid-cols-3 gap-8">
<div className="space-y-4"> <div className="bg-card text-card-foreground p-6 rounded-xl border h-full w-full flex flex-col items-start text-left">
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-primary/10"> <div className="flex h-10 w-10 items-center justify-center rounded-lg bg-primary/10 mb-4">
<Zap className="h-5 w-5 text-primary" /> <Zap className="h-5 w-5 text-primary" />
</div> </div>
<h3 className="text-lg font-semibold text-foreground">AI Analysis</h3> <h3 className="text-lg font-semibold mb-2">AI Analysis</h3>
<p className="text-muted-foreground"> <p className="text-muted-foreground text-sm">
We scrape your website and use GPT-4 to extract features, pain points, and keywords automatically. We scrape your website and use GPT-4 to extract features, pain points, and keywords automatically.
</p> </p>
</div> </div>
<div className="space-y-4"> <div className="bg-card text-card-foreground p-6 rounded-xl border h-full w-full flex flex-col items-start text-left">
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-primary/10"> <div className="flex h-10 w-10 items-center justify-center rounded-lg bg-primary/10 mb-4">
<Search className="h-5 w-5 text-primary" /> <Search className="h-5 w-5 text-primary" />
</div> </div>
<h3 className="text-lg font-semibold text-foreground">Smart Dorking</h3> <h3 className="text-lg font-semibold mb-2">Smart Dorking</h3>
<p className="text-muted-foreground"> <p className="text-muted-foreground text-sm">
Our system generates targeted Google dork queries to find high-intent posts across Reddit, HN, and more. Our system generates targeted Google dork queries to find high-intent posts across Reddit, HN, and more.
</p> </p>
</div> </div>
<div className="space-y-4"> <div className="bg-card text-card-foreground p-6 rounded-xl border h-full w-full flex flex-col items-start text-left">
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-primary/10"> <div className="flex h-10 w-10 items-center justify-center rounded-lg bg-primary/10 mb-4">
<Target className="h-5 w-5 text-primary" /> <Target className="h-5 w-5 text-primary" />
</div> </div>
<h3 className="text-lg font-semibold text-foreground">Scored Leads</h3> <h3 className="text-lg font-semibold mb-2">Scored Leads</h3>
<p className="text-muted-foreground"> <p className="text-muted-foreground text-sm">
Each opportunity is scored by relevance and comes with suggested engagement approaches. Each opportunity is scored by relevance and comes with suggested engagement approaches.
</p> </p>
</div> </div>
@@ -94,8 +130,9 @@ export default function LandingPage() {
</section> </section>
{/* CTA */} {/* CTA */}
<section className="container mx-auto max-w-6xl px-4 py-24"> <section className="container mx-auto max-w-7xl px-4 py-24 relative z-10">
<div className="rounded-2xl border border-border bg-muted/50 p-12 text-center"> <div className="rounded-3xl border border-border bg-gradient-to-b from-muted/50 to-muted/10 p-12 text-center relative overflow-hidden">
<div className="relative z-10">
<h2 className="text-3xl font-bold text-foreground mb-4"> <h2 className="text-3xl font-bold text-foreground mb-4">
Ready to find your customers? Ready to find your customers?
</h2> </h2>
@@ -103,14 +140,21 @@ export default function LandingPage() {
Stop guessing. Start finding people who are already looking for solutions like yours. Stop guessing. Start finding people who are already looking for solutions like yours.
</p> </p>
<Link href="/auth"> <Link href="/auth">
<Button size="lg">Get Started Free</Button> <Button size="default" className="px-6">Get Started Free</Button>
</Link> </Link>
</div> </div>
<DotPattern
className={cn(
"[mask-image:radial-gradient(400px_circle_at_center,white,transparent)]",
"opacity-50"
)}
/>
</div>
</section> </section>
{/* Footer */} {/* Footer */}
<footer className="border-t border-border"> <footer className="border-t border-border/40 backdrop-blur-sm relative z-10">
<div className="container mx-auto max-w-6xl px-4 py-8"> <div className="container mx-auto max-w-7xl px-4 py-8">
<p className="text-center text-sm text-muted-foreground"> <p className="text-center text-sm text-muted-foreground">
Sanati Built for indie hackers and founders Sanati Built for indie hackers and founders
</p> </p>

120
components/hero-shader.tsx Normal file
View File

@@ -0,0 +1,120 @@
"use client";
import { useEffect, useRef } from "react";
export function HeroShader() {
const canvasRef = useRef<HTMLCanvasElement>(null);
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext("2d");
if (!ctx) return;
let t = 0;
let animationFrameId: number;
const resize = () => {
canvas.width = canvas.offsetWidth;
canvas.height = canvas.offsetHeight;
};
resize();
window.addEventListener("resize", resize);
const draw = () => {
// Clear canvas with some transparency for trails?
// Or just clear completely given the math looks like it redraws?
// Tweet says background(9), which is very dark.
// Tweetcarts usually redraw fully or rely on trails.
// Given complexity, let's clear fully first.
ctx.fillStyle = "#090909"; // background(9) approximation
// ctx.fillRect(0, 0, canvas.width, canvas.height);
// Actually, let's make it transparent so it blends with our site
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = "rgba(255, 255, 255, 0.5)"; // stroke(w, 116) approx white with alpha
// Center of drawing - offset to the right
const cx = canvas.width * 0.75;
const cy = canvas.height / 2;
// Loop for points
// for(t+=PI/90,i=1e4;i--;)a()
t += Math.PI / 90;
for (let i = 10000; i > 0; i--) {
// y = i / 790
let y = i / 790;
// k = (y<8 ? 9+sin(y^9)*6 : 4+cos(y)) * cos(i+t/4)
// Note: processing sin(y^9) is bitwise XOR in JS, but usually in math it's power?
// "y^9" in many tweetcarts (JS) is XOR if it's straight JS evaluation,
// but if it's GLSL it might mean pow.
// Given "tweetcart", it's likely JS/Processing, where ^ is XOR in standard JS but often used as Pow in math context?
// Processing language: ^ is bitwise XOR. pow(n, e) is power.
// Let's assume XOR as it's common in condensed code.
// Wait, standard JS `^` is XOR.
let k_term1 = (y < 8)
? (9 + Math.sin(y ** 9) * 6) // Trying Power first as it produces curves
: (4 + Math.cos(y));
// Wait, dweet/tweetcart usually use JS syntax.
// Let's try to replicate exact syntax logic.
// Logic: k = (condition) * cos(i + t/4)
// Re-evaluating y^9. If y is float, XOR converts to int.
// y = i / 790, which is float.
// Let's stick to Math.pow for smoother graphs if intended.
// But let's try standard JS behavior for ^ (XOR) might be intended for glitchy look?
// Let's try power first.
const k = ((y < 8) ? (9 + Math.sin(Math.pow(y, 9)) * 6) : (4 + Math.cos(y))) * Math.cos(i + t / 4);
// e = y/3 - 13 + cos(e ?? no, that was likely comma operator)
// Original: d=mag(k=(...), e=y/3-13) + cos(e+t*2+i%2*4)
// mag(a, b) = sqrt(a*a + b*b)
// So args to mag are k and e.
const e = y / 3 - 13;
const mag_ke = Math.sqrt(k * k + e * e);
// d = mag(...) + cos(e + t*2 + i%2*4)
const d = mag_ke + Math.cos(e + t * 2 + (i % 2) * 4);
// c = d/4 - t/2 + i%2*3
const c = d / 4 - t / 2 + (i % 2) * 3;
// q = y * k / 5 * (2 + sin(d*2 + y - t*4)) + 80
const q = y * k / 5 * (2 + Math.sin(d * 2 + y - t * 4)) + 80;
// x = q * cos(c) + 200
// y_out = q * sin(c) + d * 9 + 60
// 200 and 60 are likely offsets for 400x400 canvas.
// We should center it.
const x = (q * Math.cos(c)) + cx;
const y_out = (q * Math.sin(c) + d * 9) + cy;
ctx.fillRect(x, y_out, 1.5, 1.5);
}
animationFrameId = requestAnimationFrame(draw);
};
draw();
return () => {
window.removeEventListener("resize", resize);
cancelAnimationFrame(animationFrameId);
};
}, []);
return (
<canvas
ref={canvasRef}
className="absolute inset-0 h-full w-full opacity-60 mix-blend-screen pointer-events-none"
/>
);
}

View File

@@ -0,0 +1,43 @@
"use client";
import { motion } from "framer-motion";
import { cn } from "@/lib/utils";
interface BlurInProps {
word: string;
className?: string;
variant?: {
hidden: { filter: string; opacity: number };
visible: { filter: string; opacity: number };
};
duration?: number;
}
export function BlurIn({
word,
className,
variant,
duration = 1,
}: BlurInProps) {
const defaultVariants = {
hidden: { filter: "blur(10px)", opacity: 0 },
visible: { filter: "blur(0px)", opacity: 1 },
};
const combinedVariants = variant || defaultVariants;
return (
<motion.h1
initial="hidden"
animate="visible"
transition={{ duration }}
variants={combinedVariants}
className={cn(
"font-display text-center text-4xl font-bold tracking-[-0.02em] drop-shadow-sm md:text-7xl md:leading-[5rem]",
className,
)}
>
{word}
</motion.h1>
);
}

View File

@@ -0,0 +1,55 @@
import { useId } from "react";
import { cn } from "@/lib/utils";
interface DotPatternProps {
width?: any;
height?: any;
x?: any;
y?: any;
cx?: any;
cy?: any;
cr?: any;
className?: string;
[key: string]: any;
}
export function DotPattern({
width = 16,
height = 16,
x = 0,
y = 0,
cx = 1,
cy = 1,
cr = 1,
className,
...props
}: DotPatternProps) {
const id = useId();
return (
<svg
aria-hidden="true"
className={cn(
"pointer-events-none absolute inset-0 h-full w-full fill-neutral-400/80",
className,
)}
{...props}
>
<defs>
<pattern
id={id}
width={width}
height={height}
patternUnits="userSpaceOnUse"
patternContentUnits="userSpaceOnUse"
x={x}
y={y}
>
<circle id="pattern-circle" cx={cx} cy={cy} r={cr} />
</pattern>
</defs>
<rect width="100%" height="100%" strokeWidth={0} fill={`url(#${id})`} />
</svg>
);
}

View File

@@ -0,0 +1,24 @@
import React from "react";
import { cn } from "@/lib/utils";
interface RainbowButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement> { }
export function RainbowButton({
children,
className,
...props
}: RainbowButtonProps) {
return (
<button
className={cn(
"group relative inline-flex h-11 animate-rainbow cursor-pointer items-center justify-center rounded-xl border-0 bg-[length:200%] px-8 py-2 font-medium text-primary-foreground transition-colors [background-image:linear-gradient(90deg,hsl(var(--color-1)),hsl(var(--color-5)),hsl(var(--color-3)),hsl(var(--color-4)),hsl(var(--color-2)))] focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50",
className,
)}
{...props}
>
{children}
</button>
);
}

View File

@@ -0,0 +1,62 @@
"use client";
import { cn } from "@/lib/utils";
type TColorProp = string | string[];
interface ShineBorderProps {
borderRadius?: number;
borderWidth?: number;
duration?: number;
color?: TColorProp;
className?: string;
children: React.ReactNode;
}
/**
* @name Shine Border
* @description It is an animated background border effect component with easy to use and configurable props.
* @param borderRadius defines the radius of the border.
* @param borderWidth defines the width of the border.
* @param duration defines the animation duration to be applied on the shining border
* @param color a string or string array to define border color.
* @param className defines the class name to be applied to the component
* @param children nodes to be rendered in the center of the component
*/
export function ShineBorder({
borderRadius = 8,
borderWidth = 1,
duration = 14,
color = "#000000",
className,
children,
}: ShineBorderProps) {
return (
<div
style={
{
"--border-radius": `${borderRadius}px`,
} as React.CSSProperties
}
className={cn(
"relative min-h-[60px] w-fit min-w-[300px] place-items-center rounded-[--border-radius] bg-white p-3 text-black dark:bg-black dark:text-white",
className,
)}
>
<div
style={
{
"--border-width": `${borderWidth}px`,
"--border-radius": `${borderRadius}px`,
"--duration": `${duration}s`,
"--mask-linear-gradient": `linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0)`,
"--background-radial-gradient": `radial-gradient(transparent,transparent, ${Array.isArray(color) ? color.join(",") : color
},transparent,transparent)`,
} as React.CSSProperties
}
className={`before:bg-shine-size before:absolute before:inset-0 before:aspect-square before:size-full before:rounded-[--border-radius] before:p-[--border-width] before:will-change-[background-position] before:content-[""] before:![-webkit-mask-composite:xor] before:![mask-composite:exclude] before:[background-image:--background-radial-gradient] before:[background-size:300%_300%] before:[mask:--mask-linear-gradient] motion-safe:before:animate-shine`}
></div>
{children}
</div>
);
}

View File

@@ -0,0 +1,48 @@
"use client";
import { cn } from "@/lib/utils";
import { AnimatePresence, HTMLMotionProps, motion } from "framer-motion";
import { useEffect, useState } from "react";
interface WordRotateProps {
words: string[];
duration?: number;
framerProps?: HTMLMotionProps<"h1">;
className?: string;
}
export function WordRotate({
words,
duration = 2500,
framerProps = {
initial: { opacity: 0, y: -50 },
animate: { opacity: 1, y: 0 },
exit: { opacity: 0, y: 50 },
transition: { duration: 0.25, ease: "easeOut" },
},
className,
}: WordRotateProps) {
const [index, setIndex] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setIndex((prevIndex) => (prevIndex + 1) % words.length);
}, duration);
return () => clearInterval(interval);
}, [words, duration]);
return (
<div className="overflow-hidden py-2">
<AnimatePresence mode="wait">
<motion.h1
key={words[index]}
className={cn(className)}
{...framerProps}
>
{words[index]}
</motion.h1>
</AnimatePresence>
</div>
);
}

92
package-lock.json generated
View File

@@ -15,6 +15,7 @@
"@radix-ui/react-collapsible": "^1.0.3", "@radix-ui/react-collapsible": "^1.0.3",
"@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-icons": "^1.3.2",
"@radix-ui/react-scroll-area": "^1.0.5", "@radix-ui/react-scroll-area": "^1.0.5",
"@radix-ui/react-separator": "^1.1.8", "@radix-ui/react-separator": "^1.1.8",
"@radix-ui/react-slider": "^1.1.2", "@radix-ui/react-slider": "^1.1.2",
@@ -23,21 +24,25 @@
"@radix-ui/react-tooltip": "^1.2.8", "@radix-ui/react-tooltip": "^1.2.8",
"@supabase/ssr": "^0.8.0", "@supabase/ssr": "^0.8.0",
"@supabase/supabase-js": "^2.93.3", "@supabase/supabase-js": "^2.93.3",
"animejs": "^4.3.5",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.0", "clsx": "^2.1.1",
"convex": "^1.31.7", "convex": "^1.31.7",
"framer-motion": "^12.31.0",
"lucide-react": "^0.563.0", "lucide-react": "^0.563.0",
"mini-svg-data-uri": "^1.4.4",
"next": "14.1.0", "next": "14.1.0",
"next-themes": "^0.4.6", "next-themes": "^0.4.6",
"openai": "^4.28.0", "openai": "^4.28.0",
"puppeteer": "^22.0.0", "puppeteer": "^22.0.0",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"tailwind-merge": "^2.2.0", "tailwind-merge": "^2.6.1",
"tailwindcss-animate": "^1.0.7", "tailwindcss-animate": "^1.0.7",
"zod": "^3.22.4" "zod": "^3.22.4"
}, },
"devDependencies": { "devDependencies": {
"@types/animejs": "^3.1.13",
"@types/node": "^20.11.0", "@types/node": "^20.11.0",
"@types/react": "^18.2.0", "@types/react": "^18.2.0",
"@types/react-dom": "^18.2.0", "@types/react-dom": "^18.2.0",
@@ -1239,6 +1244,15 @@
} }
} }
}, },
"node_modules/@radix-ui/react-icons": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-icons/-/react-icons-1.3.2.tgz",
"integrity": "sha512-fyQIhGDhzfc9pK2kH6Pl9c4BDJGfMkPqkyIgYDthyNYoNg3wVhoJMMh19WS4Up/1KMPFVpNsT2q3WmXn2N1m6g==",
"license": "MIT",
"peerDependencies": {
"react": "^16.x || ^17.x || ^18.x || ^19.0.0 || ^19.0.0-rc"
}
},
"node_modules/@radix-ui/react-id": { "node_modules/@radix-ui/react-id": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz",
@@ -1949,6 +1963,13 @@
"version": "0.23.0", "version": "0.23.0",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/animejs": {
"version": "3.1.13",
"resolved": "https://registry.npmjs.org/@types/animejs/-/animejs-3.1.13.tgz",
"integrity": "sha512-yWg9l1z7CAv/TKpty4/vupEh24jDGUZXv4r26StRkpUPQm04ztJaftgpto8vwdFs8SiTq6XfaPKCSI+wjzNMvQ==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "20.19.30", "version": "20.19.30",
"license": "MIT", "license": "MIT",
@@ -2036,6 +2057,16 @@
"node": ">= 8.0.0" "node": ">= 8.0.0"
} }
}, },
"node_modules/animejs": {
"version": "4.3.5",
"resolved": "https://registry.npmjs.org/animejs/-/animejs-4.3.5.tgz",
"integrity": "sha512-yuQo/r97TCE+DDu3dTRKjyhBKSEGBcZorWeRW7WCE7EkAQpBoNd2E82dAAD/MDdrbREv7qsw/u7MAqiUX544WQ==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/juliangarnier"
}
},
"node_modules/ansi-regex": { "node_modules/ansi-regex": {
"version": "5.0.1", "version": "5.0.1",
"license": "MIT", "license": "MIT",
@@ -2488,6 +2519,8 @@
}, },
"node_modules/clsx": { "node_modules/clsx": {
"version": "2.1.1", "version": "2.1.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=6" "node": ">=6"
@@ -2979,6 +3012,33 @@
"url": "https://github.com/sponsors/rawify" "url": "https://github.com/sponsors/rawify"
} }
}, },
"node_modules/framer-motion": {
"version": "12.31.0",
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.31.0.tgz",
"integrity": "sha512-Tnd0FU05zGRFI3JJmBegXonF1rfuzYeuXd1QSdQ99Ysnppk0yWBWSW2wUsqzRpS5nv0zPNx+y0wtDj4kf0q5RQ==",
"license": "MIT",
"dependencies": {
"motion-dom": "^12.30.1",
"motion-utils": "^12.29.2",
"tslib": "^2.4.0"
},
"peerDependencies": {
"@emotion/is-prop-valid": "*",
"react": "^18.0.0 || ^19.0.0",
"react-dom": "^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@emotion/is-prop-valid": {
"optional": true
},
"react": {
"optional": true
},
"react-dom": {
"optional": true
}
}
},
"node_modules/fsevents": { "node_modules/fsevents": {
"version": "2.3.3", "version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
@@ -3426,10 +3486,34 @@
"node": ">= 0.6" "node": ">= 0.6"
} }
}, },
"node_modules/mini-svg-data-uri": {
"version": "1.4.4",
"resolved": "https://registry.npmjs.org/mini-svg-data-uri/-/mini-svg-data-uri-1.4.4.tgz",
"integrity": "sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg==",
"license": "MIT",
"bin": {
"mini-svg-data-uri": "cli.js"
}
},
"node_modules/mitt": { "node_modules/mitt": {
"version": "3.0.1", "version": "3.0.1",
"license": "MIT" "license": "MIT"
}, },
"node_modules/motion-dom": {
"version": "12.30.1",
"resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.30.1.tgz",
"integrity": "sha512-QXB+iFJRzZTqL+Am4a1CRoHdH+0Nq12wLdqQQZZsfHlp9AMt6PA098L/61oVZsDA+Ep3QSGudzpViyRrhYhGcQ==",
"license": "MIT",
"dependencies": {
"motion-utils": "^12.29.2"
}
},
"node_modules/motion-utils": {
"version": "12.29.2",
"resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.29.2.tgz",
"integrity": "sha512-G3kc34H2cX2gI63RqU+cZq+zWRRPSsNIOjpdl9TN4AQwC4sgwYPl/Q/Obf/d53nOm569T0fYK+tcoSV50BWx8A==",
"license": "MIT"
},
"node_modules/ms": { "node_modules/ms": {
"version": "2.1.3", "version": "2.1.3",
"license": "MIT" "license": "MIT"
@@ -4360,7 +4444,9 @@
} }
}, },
"node_modules/tailwind-merge": { "node_modules/tailwind-merge": {
"version": "2.6.0", "version": "2.6.1",
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.6.1.tgz",
"integrity": "sha512-Oo6tHdpZsGpkKG88HJ8RR1rg/RdnEkQEfMoEk2x1XRI3F1AxeU+ijRXpiVUF4UbLfcxxRGw6TbUINKYdWVsQTQ==",
"license": "MIT", "license": "MIT",
"funding": { "funding": {
"type": "github", "type": "github",

View File

@@ -16,6 +16,7 @@
"@radix-ui/react-collapsible": "^1.0.3", "@radix-ui/react-collapsible": "^1.0.3",
"@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-icons": "^1.3.2",
"@radix-ui/react-scroll-area": "^1.0.5", "@radix-ui/react-scroll-area": "^1.0.5",
"@radix-ui/react-separator": "^1.1.8", "@radix-ui/react-separator": "^1.1.8",
"@radix-ui/react-slider": "^1.1.2", "@radix-ui/react-slider": "^1.1.2",
@@ -24,21 +25,25 @@
"@radix-ui/react-tooltip": "^1.2.8", "@radix-ui/react-tooltip": "^1.2.8",
"@supabase/ssr": "^0.8.0", "@supabase/ssr": "^0.8.0",
"@supabase/supabase-js": "^2.93.3", "@supabase/supabase-js": "^2.93.3",
"animejs": "^4.3.5",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.0", "clsx": "^2.1.1",
"convex": "^1.31.7", "convex": "^1.31.7",
"framer-motion": "^12.31.0",
"lucide-react": "^0.563.0", "lucide-react": "^0.563.0",
"mini-svg-data-uri": "^1.4.4",
"next": "14.1.0", "next": "14.1.0",
"next-themes": "^0.4.6", "next-themes": "^0.4.6",
"openai": "^4.28.0", "openai": "^4.28.0",
"puppeteer": "^22.0.0", "puppeteer": "^22.0.0",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"tailwind-merge": "^2.2.0", "tailwind-merge": "^2.6.1",
"tailwindcss-animate": "^1.0.7", "tailwindcss-animate": "^1.0.7",
"zod": "^3.22.4" "zod": "^3.22.4"
}, },
"devDependencies": { "devDependencies": {
"@types/animejs": "^3.1.13",
"@types/node": "^20.11.0", "@types/node": "^20.11.0",
"@types/react": "^18.2.0", "@types/react": "^18.2.0",
"@types/react-dom": "^18.2.0", "@types/react-dom": "^18.2.0",

View File

@@ -83,11 +83,28 @@ const config: Config = {
to: { to: {
height: '0' height: '0'
} }
} },
rainbow: {
"0%": { "background-position": "0%" },
"100%": { "background-position": "200%" },
},
shine: {
"0%": {
"background-position": "0% 0%",
},
"50%": {
"background-position": "100% 100%",
},
to: {
"background-position": "0% 0%",
},
},
}, },
animation: { animation: {
'accordion-down': 'accordion-down 0.2s ease-out', 'accordion-down': 'accordion-down 0.2s ease-out',
'accordion-up': 'accordion-up 0.2s ease-out' 'accordion-up': 'accordion-up 0.2s ease-out',
rainbow: "rainbow var(--speed, 2s) infinite linear",
shine: "shine var(--duration) infinite linear",
} }
} }
}, },