From 494acebeb3f71f545acbd50003e0cfacbfd8d599 Mon Sep 17 00:00:00 2001 From: Matiss Jurevics Date: Tue, 3 Feb 2026 16:23:28 +0000 Subject: [PATCH] feat: Add Magic UI components including DotPattern, RainbowButton, ShineBorder, WordRotate, and BlurIn, along with corresponding Tailwind configuration updates. --- app/api/search/route.ts | 24 +-- app/globals.css | 10 +- app/layout.tsx | 9 +- app/page.tsx | 274 +++++++++++++++----------- components/hero-shader.tsx | 120 +++++++++++ components/magicui/blur-in.tsx | 43 ++++ components/magicui/dot-pattern.tsx | 55 ++++++ components/magicui/rainbow-button.tsx | 24 +++ components/magicui/shine-border.tsx | 62 ++++++ components/magicui/word-rotate.tsx | 48 +++++ lib/types.ts | 18 +- package-lock.json | 92 ++++++++- package.json | 11 +- tailwind.config.ts | 199 ++++++++++--------- 14 files changed, 752 insertions(+), 237 deletions(-) create mode 100644 components/hero-shader.tsx create mode 100644 components/magicui/blur-in.tsx create mode 100644 components/magicui/dot-pattern.tsx create mode 100644 components/magicui/rainbow-button.tsx create mode 100644 components/magicui/shine-border.tsx create mode 100644 components/magicui/word-rotate.tsx diff --git a/app/api/search/route.ts b/app/api/search/route.ts index 1d89bd4..7be3723 100644 --- a/app/api/search/route.ts +++ b/app/api/search/route.ts @@ -39,7 +39,7 @@ export async function POST(request: NextRequest) { const { analysis } = bodySchema.parse(body) console.log(`🔍 Finding opportunities for: ${analysis.productName}`) - + // Sort queries by priority const sortedQueries = analysis.dorkQueries .sort((a, b) => { @@ -49,14 +49,14 @@ export async function POST(request: NextRequest) { .slice(0, 15) // Limit to top 15 queries const allResults: SearchResult[] = [] - + // Execute searches for (const query of sortedQueries) { try { console.log(` Searching: ${query.query.substring(0, 60)}...`) const results = await searchGoogle(query.query, 5) allResults.push(...results) - + // Small delay to avoid rate limiting await new Promise(r => setTimeout(r, 500)) } catch (e) { @@ -85,7 +85,7 @@ export async function POST(request: NextRequest) { } catch (error: any) { console.error('❌ Search error:', error) - + return NextResponse.json( { error: error.message || 'Failed to find opportunities' }, { status: 500 } @@ -136,15 +136,15 @@ async function searchDirect(query: string, num: number): Promise const html = await response.text() const results: SearchResult[] = [] - + // Simple regex parsing const resultBlocks = html.match(/
]*>([\s\S]*?)<\/div>\s*<\/div>/g) || [] - + for (const block of resultBlocks.slice(0, num)) { const titleMatch = block.match(/]*>(.*?)<\/h3>/) const linkMatch = block.match(/]*>(.*?)<\/div>/) - + if (titleMatch && linkMatch) { results.push({ title: titleMatch[1].replace(/<[^>]+>/g, ''), @@ -169,7 +169,7 @@ function getSource(url: string): string { } async function analyzeOpportunities( - results: SearchResult[], + results: SearchResult[], analysis: EnhancedProductAnalysis ): Promise { const opportunities: Opportunity[] = [] @@ -181,17 +181,17 @@ async function analyzeOpportunities( // Calculate relevance score const content = (result.title + ' ' + result.snippet).toLowerCase() - + // Match keywords const matchedKeywords = analysis.keywords .filter(k => content.includes(k.term.toLowerCase())) .map(k => k.term) - + // Match problems const matchedProblems = analysis.problemsSolved .filter(p => content.includes(p.problem.toLowerCase())) .map(p => p.problem) - + // Calculate score const keywordScore = Math.min(matchedKeywords.length * 0.15, 0.6) const problemScore = Math.min(matchedProblems.length * 0.2, 0.4) @@ -210,7 +210,7 @@ async function analyzeOpportunities( } // Find matching persona - const matchedPersona = analysis.personas.find(p => + const matchedPersona = analysis.personas.find(p => p.searchBehavior.some(b => content.includes(b.toLowerCase())) )?.name diff --git a/app/globals.css b/app/globals.css index 95a6a46..ecc1e59 100644 --- a/app/globals.css +++ b/app/globals.css @@ -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%; @@ -54,4 +62,4 @@ --sidebar-border: 240 3.7% 15.9%; --sidebar-ring: 217.2 91.2% 59.8%; } -} +} \ No newline at end of file diff --git a/app/layout.tsx b/app/layout.tsx index 4f4c036..3a835d3 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -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 ( - + - {/* Header */} -
-
-
-
- -
- Sanati -
- + return ( +
+ + + + + {/* Header */} +
+
+
+
+ +
+ Sanati +
+ +
+
+ + {/* Header */} +
+
+
+
+ +
+ Sanati +
+ +
+
+ + {/* Hero */} +
+
+
+
+ + AI-Powered Research +
+ +

+ Find Your Next +
+ Customers. +

+ +

+ Sanati analyzes your product and finds people on Reddit, Hacker News, and forums + who are actively expressing needs that your solution solves. +

+ +
+ + + +
+
+ + {/* Right side is reserved for the shader visualization */} +
+
+
+ + {/* Features */} +
+
+
+
+
+ +
+

AI Analysis

+

+ We scrape your website and use GPT-4 to extract features, pain points, and keywords automatically. +

+
+ +
+
+ +
+

Smart Dorking

+

+ Our system generates targeted Google dork queries to find high-intent posts across Reddit, HN, and more. +

+
+ +
+
+ +
+

Scored Leads

+

+ Each opportunity is scored by relevance and comes with suggested engagement approaches. +

+
+
+
+
+ + {/* CTA */} +
+
+
+

+ Ready to find your customers? +

+

+ Stop guessing. Start finding people who are already looking for solutions like yours. +

+ + + +
+ +
+
+ + {/* Footer */} +
+
+

+ Sanati — Built for indie hackers and founders +

+
+
-
- - {/* Hero */} -
-
- - - AI-Powered Research - - -

- Find Your Next Customers -
- Before They Know They Need You -

- -

- Sanati analyzes your product and finds people on Reddit, Hacker News, and forums - who are actively expressing needs that your solution solves. -

- -
- - - -
-
-
- - {/* Features */} -
-
-
-
-
- -
-

AI Analysis

-

- We scrape your website and use GPT-4 to extract features, pain points, and keywords automatically. -

-
- -
-
- -
-

Smart Dorking

-

- Our system generates targeted Google dork queries to find high-intent posts across Reddit, HN, and more. -

-
- -
-
- -
-

Scored Leads

-

- Each opportunity is scored by relevance and comes with suggested engagement approaches. -

-
-
-
-
- - {/* CTA */} -
-
-

- Ready to find your customers? -

-

- Stop guessing. Start finding people who are already looking for solutions like yours. -

- - - -
-
- - {/* Footer */} -
-
-

- Sanati — Built for indie hackers and founders -

-
-
-
- ) + ) } diff --git a/components/hero-shader.tsx b/components/hero-shader.tsx new file mode 100644 index 0000000..d9caff7 --- /dev/null +++ b/components/hero-shader.tsx @@ -0,0 +1,120 @@ +"use client"; + +import { useEffect, useRef } from "react"; + +export function HeroShader() { + const canvasRef = useRef(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 ( + + ); +} diff --git a/components/magicui/blur-in.tsx b/components/magicui/blur-in.tsx new file mode 100644 index 0000000..75f7a67 --- /dev/null +++ b/components/magicui/blur-in.tsx @@ -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 ( + + {word} + + ); +} diff --git a/components/magicui/dot-pattern.tsx b/components/magicui/dot-pattern.tsx new file mode 100644 index 0000000..e03a8b7 --- /dev/null +++ b/components/magicui/dot-pattern.tsx @@ -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 ( + + ); +} diff --git a/components/magicui/rainbow-button.tsx b/components/magicui/rainbow-button.tsx new file mode 100644 index 0000000..122ce46 --- /dev/null +++ b/components/magicui/rainbow-button.tsx @@ -0,0 +1,24 @@ +import React from "react"; + +import { cn } from "@/lib/utils"; + +interface RainbowButtonProps + extends React.ButtonHTMLAttributes { } + +export function RainbowButton({ + children, + className, + ...props +}: RainbowButtonProps) { + return ( + + ); +} diff --git a/components/magicui/shine-border.tsx b/components/magicui/shine-border.tsx new file mode 100644 index 0000000..53e1174 --- /dev/null +++ b/components/magicui/shine-border.tsx @@ -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 ( +
+
+ {children} +
+ ); +} diff --git a/components/magicui/word-rotate.tsx b/components/magicui/word-rotate.tsx new file mode 100644 index 0000000..c8e654b --- /dev/null +++ b/components/magicui/word-rotate.tsx @@ -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 ( +
+ + + {words[index]} + + +
+ ); +} diff --git a/lib/types.ts b/lib/types.ts index a1d0ce2..158eee3 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -2,7 +2,7 @@ export type PlatformId = 'reddit' | 'twitter' | 'hackernews' | 'indiehackers' | 'quora' | 'stackoverflow' | 'linkedin' -export type SearchStrategy = +export type SearchStrategy = | 'direct-keywords' | 'problem-pain' | 'competitor-alternative' @@ -43,30 +43,30 @@ export interface Opportunity { snippet: string platform: string source: string - + relevanceScore: number emotionalIntensity: 'low' | 'medium' | 'high' intent: 'frustrated' | 'looking' | 'comparing' | 'learning' | 'recommending' - + matchedKeywords: string[] matchedProblems: string[] matchedPersona?: string - + engagement?: { upvotes?: number comments?: number views?: number } postedAt?: string - + status: 'new' | 'viewed' | 'contacted' | 'responded' | 'converted' | 'ignored' notes?: string tags?: string[] - + suggestedApproach: string replyTemplate?: string softPitch: boolean - + scoringBreakdown?: { keywordMatches: number problemMatches: number @@ -145,7 +145,7 @@ export interface EnhancedProductAnalysis { description: string category: string positioning: string - + features: Feature[] problemsSolved: Problem[] personas: Persona[] @@ -153,7 +153,7 @@ export interface EnhancedProductAnalysis { useCases: UseCase[] competitors: Competitor[] dorkQueries: DorkQuery[] - + scrapedAt: string analysisVersion: string } diff --git a/package-lock.json b/package-lock.json index 224f47f..a600ccc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index d57eb88..0ccbe91 100644 --- a/package.json +++ b/package.json @@ -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", @@ -47,4 +52,4 @@ "tailwindcss": "^3.4.1", "typescript": "^5.3.3" } -} \ No newline at end of file +} diff --git a/tailwind.config.ts b/tailwind.config.ts index 96e6940..16577c9 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -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