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%;
--ring: 240 4.9% 83.9%;
--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 {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
font-feature-settings: "rlig" 1, "calt" 1;
}
:root {
--sidebar-background: 0 0% 98%;
--sidebar-foreground: 240 5.3% 26.1%;
@@ -44,6 +51,7 @@
--sidebar-border: 220 13% 91%;
--sidebar-ring: 217.2 91.2% 59.8%;
}
.dark {
--sidebar-background: 240 5.9% 10%;
--sidebar-foreground: 240 4.8% 95.9%;

View File

@@ -1,11 +1,14 @@
import type { Metadata } from 'next'
import { Inter } from 'next/font/google'
import { Montserrat } from 'next/font/google'
import './globals.css'
import ConvexClientProvider from './ConvexClientProvider'
import { ConvexAuthNextjsServerProvider } from "@convex-dev/auth/nextjs/server";
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 = {
title: 'Sanati - Find Product Opportunities',
@@ -20,7 +23,7 @@ export default function RootLayout({
return (
<ConvexAuthNextjsServerProvider>
<html lang="en" suppressHydrationWarning>
<body className={inter.className}>
<body className={montserrat.className}>
<ThemeProvider
attribute="class"
defaultTheme="dark"

View File

@@ -1,121 +1,165 @@
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 { 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() {
return (
<div className="min-h-screen bg-background">
{/* Header */}
<header className="border-b border-border">
<div className="container mx-auto max-w-6xl 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">Sanati</span>
</div>
<nav className="flex items-center gap-4">
<Link href="/dashboard" className="text-sm text-muted-foreground hover:text-foreground">
Dashboard
</Link>
<Link href="/auth">
<Button size="sm">Get Started</Button>
</Link>
</nav>
return (
<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 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>
</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>
</nav>
</div>
</header>
{/* Hero */}
<section className="container mx-auto max-w-7xl px-4 min-h-[75vh] flex items-center relative z-10">
<div className="grid lg:grid-cols-2 gap-12 items-center w-full">
<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" />
AI-Powered Research
</div>
<h1 className="text-4xl font-light tracking-tight sm:text-5xl lg:text-7xl text-foreground">
Find Your Next
<br />
<span className="font-bold">Customers.</span>
</h1>
<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
who are actively expressing needs that your solution solves.
</p>
<div className="flex flex-col sm:flex-row gap-4 pt-2">
<Link href="/auth">
<Button size="lg" className="gap-2 px-8 text-base">
Start Finding Opportunities
<ArrowRight className="h-4 w-4" />
</Button>
</Link>
</div>
</div>
{/* Right side is reserved for the shader visualization */}
<div className="hidden lg:block h-full min-h-[400px]"></div>
</div>
</section>
{/* Features */}
<section className="border-t border-border/40 bg-muted/20 backdrop-blur-sm relative z-10">
<div className="container mx-auto max-w-7xl px-4 py-24">
<div className="grid md:grid-cols-3 gap-8">
<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 mb-4">
<Zap className="h-5 w-5 text-primary" />
</div>
<h3 className="text-lg font-semibold mb-2">AI Analysis</h3>
<p className="text-muted-foreground text-sm">
We scrape your website and use GPT-4 to extract features, pain points, and keywords automatically.
</p>
</div>
<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 mb-4">
<Search className="h-5 w-5 text-primary" />
</div>
<h3 className="text-lg font-semibold mb-2">Smart Dorking</h3>
<p className="text-muted-foreground text-sm">
Our system generates targeted Google dork queries to find high-intent posts across Reddit, HN, and more.
</p>
</div>
<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 mb-4">
<Target className="h-5 w-5 text-primary" />
</div>
<h3 className="text-lg font-semibold mb-2">Scored Leads</h3>
<p className="text-muted-foreground text-sm">
Each opportunity is scored by relevance and comes with suggested engagement approaches.
</p>
</div>
</div>
</div>
</section>
{/* CTA */}
<section className="container mx-auto max-w-7xl px-4 py-24 relative z-10">
<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">
Ready to find your customers?
</h2>
<p className="text-muted-foreground mb-8 max-w-lg mx-auto">
Stop guessing. Start finding people who are already looking for solutions like yours.
</p>
<Link href="/auth">
<Button size="default" className="px-6">Get Started Free</Button>
</Link>
</div>
<DotPattern
className={cn(
"[mask-image:radial-gradient(400px_circle_at_center,white,transparent)]",
"opacity-50"
)}
/>
</div>
</section>
{/* Footer */}
<footer className="border-t border-border/40 backdrop-blur-sm relative z-10">
<div className="container mx-auto max-w-7xl px-4 py-8">
<p className="text-center text-sm text-muted-foreground">
Sanati Built for indie hackers and founders
</p>
</div>
</footer>
</div>
</header>
{/* Hero */}
<section className="container mx-auto max-w-6xl px-4 py-24 lg:py-32">
<div className="flex flex-col items-center text-center space-y-8">
<Badge variant="secondary" className="px-4 py-1.5">
<Sparkles className="mr-1 h-3 w-3" />
AI-Powered Research
</Badge>
<h1 className="text-4xl font-bold tracking-tight sm:text-5xl lg:text-6xl text-foreground">
Find Your Next Customers
<br />
<span className="text-muted-foreground">Before They Know They Need You</span>
</h1>
<p className="max-w-2xl text-lg text-muted-foreground">
Sanati analyzes your product and finds people on Reddit, Hacker News, and forums
who are actively expressing needs that your solution solves.
</p>
<div className="flex flex-col sm:flex-row gap-4">
<Link href="/auth">
<Button size="lg" className="gap-2">
Start Finding Opportunities
<ArrowRight className="h-4 w-4" />
</Button>
</Link>
</div>
</div>
</section>
{/* Features */}
<section className="border-t border-border bg-muted/50">
<div className="container mx-auto max-w-6xl px-4 py-24">
<div className="grid md:grid-cols-3 gap-8">
<div className="space-y-4">
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-primary/10">
<Zap className="h-5 w-5 text-primary" />
</div>
<h3 className="text-lg font-semibold text-foreground">AI Analysis</h3>
<p className="text-muted-foreground">
We scrape your website and use GPT-4 to extract features, pain points, and keywords automatically.
</p>
</div>
<div className="space-y-4">
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-primary/10">
<Search className="h-5 w-5 text-primary" />
</div>
<h3 className="text-lg font-semibold text-foreground">Smart Dorking</h3>
<p className="text-muted-foreground">
Our system generates targeted Google dork queries to find high-intent posts across Reddit, HN, and more.
</p>
</div>
<div className="space-y-4">
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-primary/10">
<Target className="h-5 w-5 text-primary" />
</div>
<h3 className="text-lg font-semibold text-foreground">Scored Leads</h3>
<p className="text-muted-foreground">
Each opportunity is scored by relevance and comes with suggested engagement approaches.
</p>
</div>
</div>
</div>
</section>
{/* CTA */}
<section className="container mx-auto max-w-6xl px-4 py-24">
<div className="rounded-2xl border border-border bg-muted/50 p-12 text-center">
<h2 className="text-3xl font-bold text-foreground mb-4">
Ready to find your customers?
</h2>
<p className="text-muted-foreground mb-8 max-w-lg mx-auto">
Stop guessing. Start finding people who are already looking for solutions like yours.
</p>
<Link href="/auth">
<Button size="lg">Get Started Free</Button>
</Link>
</div>
</section>
{/* Footer */}
<footer className="border-t border-border">
<div className="container mx-auto max-w-6xl px-4 py-8">
<p className="text-center text-sm text-muted-foreground">
Sanati Built for indie hackers and founders
</p>
</div>
</footer>
</div>
)
)
}

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-dialog": "^1.1.15",
"@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-separator": "^1.1.8",
"@radix-ui/react-slider": "^1.1.2",
@@ -23,21 +24,25 @@
"@radix-ui/react-tooltip": "^1.2.8",
"@supabase/ssr": "^0.8.0",
"@supabase/supabase-js": "^2.93.3",
"animejs": "^4.3.5",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.0",
"clsx": "^2.1.1",
"convex": "^1.31.7",
"framer-motion": "^12.31.0",
"lucide-react": "^0.563.0",
"mini-svg-data-uri": "^1.4.4",
"next": "14.1.0",
"next-themes": "^0.4.6",
"openai": "^4.28.0",
"puppeteer": "^22.0.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"tailwind-merge": "^2.2.0",
"tailwind-merge": "^2.6.1",
"tailwindcss-animate": "^1.0.7",
"zod": "^3.22.4"
},
"devDependencies": {
"@types/animejs": "^3.1.13",
"@types/node": "^20.11.0",
"@types/react": "^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": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz",
@@ -1949,6 +1963,13 @@
"version": "0.23.0",
"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": {
"version": "20.19.30",
"license": "MIT",
@@ -2036,6 +2057,16 @@
"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": {
"version": "5.0.1",
"license": "MIT",
@@ -2488,6 +2519,8 @@
},
"node_modules/clsx": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
"license": "MIT",
"engines": {
"node": ">=6"
@@ -2979,6 +3012,33 @@
"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": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
@@ -3426,10 +3486,34 @@
"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": {
"version": "3.0.1",
"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": {
"version": "2.1.3",
"license": "MIT"
@@ -4360,7 +4444,9 @@
}
},
"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",
"funding": {
"type": "github",

View File

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

View File

@@ -1,97 +1,114 @@
import type { Config } from "tailwindcss"
const config: Config = {
darkMode: ["class"],
content: [
"./pages/**/*.{ts,tsx}",
"./components/**/*.{ts,tsx}",
"./app/**/*.{ts,tsx}",
"./src/**/*.{ts,tsx}",
],
theme: {
container: {
center: true,
padding: '2rem',
screens: {
'2xl': '1400px'
}
},
extend: {
colors: {
border: 'hsl(var(--border))',
input: 'hsl(var(--input))',
ring: 'hsl(var(--ring))',
background: 'hsl(var(--background))',
foreground: 'hsl(var(--foreground))',
primary: {
DEFAULT: 'hsl(var(--primary))',
foreground: 'hsl(var(--primary-foreground))'
},
secondary: {
DEFAULT: 'hsl(var(--secondary))',
foreground: 'hsl(var(--secondary-foreground))'
},
destructive: {
DEFAULT: 'hsl(var(--destructive))',
foreground: 'hsl(var(--destructive-foreground))'
},
muted: {
DEFAULT: 'hsl(var(--muted))',
foreground: 'hsl(var(--muted-foreground))'
},
accent: {
DEFAULT: 'hsl(var(--accent))',
foreground: 'hsl(var(--accent-foreground))'
},
popover: {
DEFAULT: 'hsl(var(--popover))',
foreground: 'hsl(var(--popover-foreground))'
},
card: {
DEFAULT: 'hsl(var(--card))',
foreground: 'hsl(var(--card-foreground))'
},
sidebar: {
DEFAULT: 'hsl(var(--sidebar-background))',
foreground: 'hsl(var(--sidebar-foreground))',
primary: 'hsl(var(--sidebar-primary))',
'primary-foreground': 'hsl(var(--sidebar-primary-foreground))',
accent: 'hsl(var(--sidebar-accent))',
'accent-foreground': 'hsl(var(--sidebar-accent-foreground))',
border: 'hsl(var(--sidebar-border))',
ring: 'hsl(var(--sidebar-ring))'
}
},
borderRadius: {
lg: 'var(--radius)',
md: 'calc(var(--radius) - 2px)',
sm: 'calc(var(--radius) - 4px)'
},
keyframes: {
'accordion-down': {
from: {
height: '0'
},
to: {
height: 'var(--radix-accordion-content-height)'
}
},
'accordion-up': {
from: {
height: 'var(--radix-accordion-content-height)'
},
to: {
height: '0'
}
}
},
animation: {
'accordion-down': 'accordion-down 0.2s ease-out',
'accordion-up': 'accordion-up 0.2s ease-out'
}
}
},
plugins: [require("tailwindcss-animate")],
darkMode: ["class"],
content: [
"./pages/**/*.{ts,tsx}",
"./components/**/*.{ts,tsx}",
"./app/**/*.{ts,tsx}",
"./src/**/*.{ts,tsx}",
],
theme: {
container: {
center: true,
padding: '2rem',
screens: {
'2xl': '1400px'
}
},
extend: {
colors: {
border: 'hsl(var(--border))',
input: 'hsl(var(--input))',
ring: 'hsl(var(--ring))',
background: 'hsl(var(--background))',
foreground: 'hsl(var(--foreground))',
primary: {
DEFAULT: 'hsl(var(--primary))',
foreground: 'hsl(var(--primary-foreground))'
},
secondary: {
DEFAULT: 'hsl(var(--secondary))',
foreground: 'hsl(var(--secondary-foreground))'
},
destructive: {
DEFAULT: 'hsl(var(--destructive))',
foreground: 'hsl(var(--destructive-foreground))'
},
muted: {
DEFAULT: 'hsl(var(--muted))',
foreground: 'hsl(var(--muted-foreground))'
},
accent: {
DEFAULT: 'hsl(var(--accent))',
foreground: 'hsl(var(--accent-foreground))'
},
popover: {
DEFAULT: 'hsl(var(--popover))',
foreground: 'hsl(var(--popover-foreground))'
},
card: {
DEFAULT: 'hsl(var(--card))',
foreground: 'hsl(var(--card-foreground))'
},
sidebar: {
DEFAULT: 'hsl(var(--sidebar-background))',
foreground: 'hsl(var(--sidebar-foreground))',
primary: 'hsl(var(--sidebar-primary))',
'primary-foreground': 'hsl(var(--sidebar-primary-foreground))',
accent: 'hsl(var(--sidebar-accent))',
'accent-foreground': 'hsl(var(--sidebar-accent-foreground))',
border: 'hsl(var(--sidebar-border))',
ring: 'hsl(var(--sidebar-ring))'
}
},
borderRadius: {
lg: 'var(--radius)',
md: 'calc(var(--radius) - 2px)',
sm: 'calc(var(--radius) - 4px)'
},
keyframes: {
'accordion-down': {
from: {
height: '0'
},
to: {
height: 'var(--radix-accordion-content-height)'
}
},
'accordion-up': {
from: {
height: 'var(--radix-accordion-content-height)'
},
to: {
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: {
'accordion-down': 'accordion-down 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",
}
}
},
plugins: [require("tailwindcss-animate")],
}
export default config