feat: Add Magic UI components including DotPattern, RainbowButton, ShineBorder, WordRotate, and BlurIn, along with corresponding Tailwind configuration updates.
This commit is contained in:
@@ -39,7 +39,7 @@ export async function POST(request: NextRequest) {
|
|||||||
const { analysis } = bodySchema.parse(body)
|
const { analysis } = bodySchema.parse(body)
|
||||||
|
|
||||||
console.log(`🔍 Finding opportunities for: ${analysis.productName}`)
|
console.log(`🔍 Finding opportunities for: ${analysis.productName}`)
|
||||||
|
|
||||||
// Sort queries by priority
|
// Sort queries by priority
|
||||||
const sortedQueries = analysis.dorkQueries
|
const sortedQueries = analysis.dorkQueries
|
||||||
.sort((a, b) => {
|
.sort((a, b) => {
|
||||||
@@ -49,14 +49,14 @@ export async function POST(request: NextRequest) {
|
|||||||
.slice(0, 15) // Limit to top 15 queries
|
.slice(0, 15) // Limit to top 15 queries
|
||||||
|
|
||||||
const allResults: SearchResult[] = []
|
const allResults: SearchResult[] = []
|
||||||
|
|
||||||
// Execute searches
|
// Execute searches
|
||||||
for (const query of sortedQueries) {
|
for (const query of sortedQueries) {
|
||||||
try {
|
try {
|
||||||
console.log(` Searching: ${query.query.substring(0, 60)}...`)
|
console.log(` Searching: ${query.query.substring(0, 60)}...`)
|
||||||
const results = await searchGoogle(query.query, 5)
|
const results = await searchGoogle(query.query, 5)
|
||||||
allResults.push(...results)
|
allResults.push(...results)
|
||||||
|
|
||||||
// Small delay to avoid rate limiting
|
// Small delay to avoid rate limiting
|
||||||
await new Promise(r => setTimeout(r, 500))
|
await new Promise(r => setTimeout(r, 500))
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -85,7 +85,7 @@ export async function POST(request: NextRequest) {
|
|||||||
|
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('❌ Search error:', error)
|
console.error('❌ Search error:', error)
|
||||||
|
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: error.message || 'Failed to find opportunities' },
|
{ error: error.message || 'Failed to find opportunities' },
|
||||||
{ status: 500 }
|
{ status: 500 }
|
||||||
@@ -136,15 +136,15 @@ async function searchDirect(query: string, num: number): Promise<SearchResult[]>
|
|||||||
|
|
||||||
const html = await response.text()
|
const html = await response.text()
|
||||||
const results: SearchResult[] = []
|
const results: SearchResult[] = []
|
||||||
|
|
||||||
// Simple regex parsing
|
// Simple regex parsing
|
||||||
const resultBlocks = html.match(/<div class="g"[^>]*>([\s\S]*?)<\/div>\s*<\/div>/g) || []
|
const resultBlocks = html.match(/<div class="g"[^>]*>([\s\S]*?)<\/div>\s*<\/div>/g) || []
|
||||||
|
|
||||||
for (const block of resultBlocks.slice(0, num)) {
|
for (const block of resultBlocks.slice(0, num)) {
|
||||||
const titleMatch = block.match(/<h3[^>]*>(.*?)<\/h3>/)
|
const titleMatch = block.match(/<h3[^>]*>(.*?)<\/h3>/)
|
||||||
const linkMatch = block.match(/<a href="([^"]+)"/)
|
const linkMatch = block.match(/<a href="([^"]+)"/)
|
||||||
const snippetMatch = block.match(/<div class="VwiC3b[^"]*"[^>]*>(.*?)<\/div>/)
|
const snippetMatch = block.match(/<div class="VwiC3b[^"]*"[^>]*>(.*?)<\/div>/)
|
||||||
|
|
||||||
if (titleMatch && linkMatch) {
|
if (titleMatch && linkMatch) {
|
||||||
results.push({
|
results.push({
|
||||||
title: titleMatch[1].replace(/<[^>]+>/g, ''),
|
title: titleMatch[1].replace(/<[^>]+>/g, ''),
|
||||||
@@ -169,7 +169,7 @@ function getSource(url: string): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function analyzeOpportunities(
|
async function analyzeOpportunities(
|
||||||
results: SearchResult[],
|
results: SearchResult[],
|
||||||
analysis: EnhancedProductAnalysis
|
analysis: EnhancedProductAnalysis
|
||||||
): Promise<Opportunity[]> {
|
): Promise<Opportunity[]> {
|
||||||
const opportunities: Opportunity[] = []
|
const opportunities: Opportunity[] = []
|
||||||
@@ -181,17 +181,17 @@ async function analyzeOpportunities(
|
|||||||
|
|
||||||
// Calculate relevance score
|
// Calculate relevance score
|
||||||
const content = (result.title + ' ' + result.snippet).toLowerCase()
|
const content = (result.title + ' ' + result.snippet).toLowerCase()
|
||||||
|
|
||||||
// Match keywords
|
// Match keywords
|
||||||
const matchedKeywords = analysis.keywords
|
const matchedKeywords = analysis.keywords
|
||||||
.filter(k => content.includes(k.term.toLowerCase()))
|
.filter(k => content.includes(k.term.toLowerCase()))
|
||||||
.map(k => k.term)
|
.map(k => k.term)
|
||||||
|
|
||||||
// Match problems
|
// Match problems
|
||||||
const matchedProblems = analysis.problemsSolved
|
const matchedProblems = analysis.problemsSolved
|
||||||
.filter(p => content.includes(p.problem.toLowerCase()))
|
.filter(p => content.includes(p.problem.toLowerCase()))
|
||||||
.map(p => p.problem)
|
.map(p => p.problem)
|
||||||
|
|
||||||
// Calculate score
|
// Calculate score
|
||||||
const keywordScore = Math.min(matchedKeywords.length * 0.15, 0.6)
|
const keywordScore = Math.min(matchedKeywords.length * 0.15, 0.6)
|
||||||
const problemScore = Math.min(matchedProblems.length * 0.2, 0.4)
|
const problemScore = Math.min(matchedProblems.length * 0.2, 0.4)
|
||||||
@@ -210,7 +210,7 @@ async function analyzeOpportunities(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Find matching persona
|
// Find matching persona
|
||||||
const matchedPersona = analysis.personas.find(p =>
|
const matchedPersona = analysis.personas.find(p =>
|
||||||
p.searchBehavior.some(b => content.includes(b.toLowerCase()))
|
p.searchBehavior.some(b => content.includes(b.toLowerCase()))
|
||||||
)?.name
|
)?.name
|
||||||
|
|
||||||
|
|||||||
@@ -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%;
|
||||||
@@ -54,4 +62,4 @@
|
|||||||
--sidebar-border: 240 3.7% 15.9%;
|
--sidebar-border: 240 3.7% 15.9%;
|
||||||
--sidebar-ring: 217.2 91.2% 59.8%;
|
--sidebar-ring: 217.2 91.2% 59.8%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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"
|
||||||
|
|||||||
274
app/page.tsx
274
app/page.tsx
@@ -1,121 +1,165 @@
|
|||||||
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">
|
||||||
{/* Header */}
|
<DotPattern
|
||||||
<header className="border-b border-border">
|
className={cn(
|
||||||
<div className="container mx-auto max-w-6xl px-4 py-4 flex items-center justify-between">
|
"[mask-image:radial-gradient(1000px_circle_at_center,white,transparent)]",
|
||||||
<div className="flex items-center gap-2">
|
"opacity-50"
|
||||||
<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>
|
<HeroShader />
|
||||||
</div>
|
|
||||||
<nav className="flex items-center gap-4">
|
{/* Header */}
|
||||||
<Link href="/dashboard" className="text-sm text-muted-foreground hover:text-foreground">
|
<header className="border-b border-border/40 backdrop-blur-md sticky top-0 z-50">
|
||||||
Dashboard
|
<div className="container mx-auto max-w-7xl px-4 py-4 flex items-center justify-between">
|
||||||
</Link>
|
<div className="flex items-center gap-2">
|
||||||
<Link href="/auth">
|
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-primary text-primary-foreground">
|
||||||
<Button size="sm">Get Started</Button>
|
<Search className="h-4 w-4" />
|
||||||
</Link>
|
</div>
|
||||||
</nav>
|
<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>
|
</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
120
components/hero-shader.tsx
Normal 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"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
43
components/magicui/blur-in.tsx
Normal file
43
components/magicui/blur-in.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
55
components/magicui/dot-pattern.tsx
Normal file
55
components/magicui/dot-pattern.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
24
components/magicui/rainbow-button.tsx
Normal file
24
components/magicui/rainbow-button.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
62
components/magicui/shine-border.tsx
Normal file
62
components/magicui/shine-border.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
48
components/magicui/word-rotate.tsx
Normal file
48
components/magicui/word-rotate.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
18
lib/types.ts
18
lib/types.ts
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
export type PlatformId = 'reddit' | 'twitter' | 'hackernews' | 'indiehackers' | 'quora' | 'stackoverflow' | 'linkedin'
|
export type PlatformId = 'reddit' | 'twitter' | 'hackernews' | 'indiehackers' | 'quora' | 'stackoverflow' | 'linkedin'
|
||||||
|
|
||||||
export type SearchStrategy =
|
export type SearchStrategy =
|
||||||
| 'direct-keywords'
|
| 'direct-keywords'
|
||||||
| 'problem-pain'
|
| 'problem-pain'
|
||||||
| 'competitor-alternative'
|
| 'competitor-alternative'
|
||||||
@@ -43,30 +43,30 @@ export interface Opportunity {
|
|||||||
snippet: string
|
snippet: string
|
||||||
platform: string
|
platform: string
|
||||||
source: string
|
source: string
|
||||||
|
|
||||||
relevanceScore: number
|
relevanceScore: number
|
||||||
emotionalIntensity: 'low' | 'medium' | 'high'
|
emotionalIntensity: 'low' | 'medium' | 'high'
|
||||||
intent: 'frustrated' | 'looking' | 'comparing' | 'learning' | 'recommending'
|
intent: 'frustrated' | 'looking' | 'comparing' | 'learning' | 'recommending'
|
||||||
|
|
||||||
matchedKeywords: string[]
|
matchedKeywords: string[]
|
||||||
matchedProblems: string[]
|
matchedProblems: string[]
|
||||||
matchedPersona?: string
|
matchedPersona?: string
|
||||||
|
|
||||||
engagement?: {
|
engagement?: {
|
||||||
upvotes?: number
|
upvotes?: number
|
||||||
comments?: number
|
comments?: number
|
||||||
views?: number
|
views?: number
|
||||||
}
|
}
|
||||||
postedAt?: string
|
postedAt?: string
|
||||||
|
|
||||||
status: 'new' | 'viewed' | 'contacted' | 'responded' | 'converted' | 'ignored'
|
status: 'new' | 'viewed' | 'contacted' | 'responded' | 'converted' | 'ignored'
|
||||||
notes?: string
|
notes?: string
|
||||||
tags?: string[]
|
tags?: string[]
|
||||||
|
|
||||||
suggestedApproach: string
|
suggestedApproach: string
|
||||||
replyTemplate?: string
|
replyTemplate?: string
|
||||||
softPitch: boolean
|
softPitch: boolean
|
||||||
|
|
||||||
scoringBreakdown?: {
|
scoringBreakdown?: {
|
||||||
keywordMatches: number
|
keywordMatches: number
|
||||||
problemMatches: number
|
problemMatches: number
|
||||||
@@ -145,7 +145,7 @@ export interface EnhancedProductAnalysis {
|
|||||||
description: string
|
description: string
|
||||||
category: string
|
category: string
|
||||||
positioning: string
|
positioning: string
|
||||||
|
|
||||||
features: Feature[]
|
features: Feature[]
|
||||||
problemsSolved: Problem[]
|
problemsSolved: Problem[]
|
||||||
personas: Persona[]
|
personas: Persona[]
|
||||||
@@ -153,7 +153,7 @@ export interface EnhancedProductAnalysis {
|
|||||||
useCases: UseCase[]
|
useCases: UseCase[]
|
||||||
competitors: Competitor[]
|
competitors: Competitor[]
|
||||||
dorkQueries: DorkQuery[]
|
dorkQueries: DorkQuery[]
|
||||||
|
|
||||||
scrapedAt: string
|
scrapedAt: string
|
||||||
analysisVersion: string
|
analysisVersion: string
|
||||||
}
|
}
|
||||||
|
|||||||
92
package-lock.json
generated
92
package-lock.json
generated
@@ -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",
|
||||||
|
|||||||
11
package.json
11
package.json
@@ -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",
|
||||||
@@ -47,4 +52,4 @@
|
|||||||
"tailwindcss": "^3.4.1",
|
"tailwindcss": "^3.4.1",
|
||||||
"typescript": "^5.3.3"
|
"typescript": "^5.3.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,97 +1,114 @@
|
|||||||
import type { Config } from "tailwindcss"
|
import type { Config } from "tailwindcss"
|
||||||
|
|
||||||
const config: Config = {
|
const config: Config = {
|
||||||
darkMode: ["class"],
|
darkMode: ["class"],
|
||||||
content: [
|
content: [
|
||||||
"./pages/**/*.{ts,tsx}",
|
"./pages/**/*.{ts,tsx}",
|
||||||
"./components/**/*.{ts,tsx}",
|
"./components/**/*.{ts,tsx}",
|
||||||
"./app/**/*.{ts,tsx}",
|
"./app/**/*.{ts,tsx}",
|
||||||
"./src/**/*.{ts,tsx}",
|
"./src/**/*.{ts,tsx}",
|
||||||
],
|
],
|
||||||
theme: {
|
theme: {
|
||||||
container: {
|
container: {
|
||||||
center: true,
|
center: true,
|
||||||
padding: '2rem',
|
padding: '2rem',
|
||||||
screens: {
|
screens: {
|
||||||
'2xl': '1400px'
|
'2xl': '1400px'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
extend: {
|
extend: {
|
||||||
colors: {
|
colors: {
|
||||||
border: 'hsl(var(--border))',
|
border: 'hsl(var(--border))',
|
||||||
input: 'hsl(var(--input))',
|
input: 'hsl(var(--input))',
|
||||||
ring: 'hsl(var(--ring))',
|
ring: 'hsl(var(--ring))',
|
||||||
background: 'hsl(var(--background))',
|
background: 'hsl(var(--background))',
|
||||||
foreground: 'hsl(var(--foreground))',
|
foreground: 'hsl(var(--foreground))',
|
||||||
primary: {
|
primary: {
|
||||||
DEFAULT: 'hsl(var(--primary))',
|
DEFAULT: 'hsl(var(--primary))',
|
||||||
foreground: 'hsl(var(--primary-foreground))'
|
foreground: 'hsl(var(--primary-foreground))'
|
||||||
},
|
},
|
||||||
secondary: {
|
secondary: {
|
||||||
DEFAULT: 'hsl(var(--secondary))',
|
DEFAULT: 'hsl(var(--secondary))',
|
||||||
foreground: 'hsl(var(--secondary-foreground))'
|
foreground: 'hsl(var(--secondary-foreground))'
|
||||||
},
|
},
|
||||||
destructive: {
|
destructive: {
|
||||||
DEFAULT: 'hsl(var(--destructive))',
|
DEFAULT: 'hsl(var(--destructive))',
|
||||||
foreground: 'hsl(var(--destructive-foreground))'
|
foreground: 'hsl(var(--destructive-foreground))'
|
||||||
},
|
},
|
||||||
muted: {
|
muted: {
|
||||||
DEFAULT: 'hsl(var(--muted))',
|
DEFAULT: 'hsl(var(--muted))',
|
||||||
foreground: 'hsl(var(--muted-foreground))'
|
foreground: 'hsl(var(--muted-foreground))'
|
||||||
},
|
},
|
||||||
accent: {
|
accent: {
|
||||||
DEFAULT: 'hsl(var(--accent))',
|
DEFAULT: 'hsl(var(--accent))',
|
||||||
foreground: 'hsl(var(--accent-foreground))'
|
foreground: 'hsl(var(--accent-foreground))'
|
||||||
},
|
},
|
||||||
popover: {
|
popover: {
|
||||||
DEFAULT: 'hsl(var(--popover))',
|
DEFAULT: 'hsl(var(--popover))',
|
||||||
foreground: 'hsl(var(--popover-foreground))'
|
foreground: 'hsl(var(--popover-foreground))'
|
||||||
},
|
},
|
||||||
card: {
|
card: {
|
||||||
DEFAULT: 'hsl(var(--card))',
|
DEFAULT: 'hsl(var(--card))',
|
||||||
foreground: 'hsl(var(--card-foreground))'
|
foreground: 'hsl(var(--card-foreground))'
|
||||||
},
|
},
|
||||||
sidebar: {
|
sidebar: {
|
||||||
DEFAULT: 'hsl(var(--sidebar-background))',
|
DEFAULT: 'hsl(var(--sidebar-background))',
|
||||||
foreground: 'hsl(var(--sidebar-foreground))',
|
foreground: 'hsl(var(--sidebar-foreground))',
|
||||||
primary: 'hsl(var(--sidebar-primary))',
|
primary: 'hsl(var(--sidebar-primary))',
|
||||||
'primary-foreground': 'hsl(var(--sidebar-primary-foreground))',
|
'primary-foreground': 'hsl(var(--sidebar-primary-foreground))',
|
||||||
accent: 'hsl(var(--sidebar-accent))',
|
accent: 'hsl(var(--sidebar-accent))',
|
||||||
'accent-foreground': 'hsl(var(--sidebar-accent-foreground))',
|
'accent-foreground': 'hsl(var(--sidebar-accent-foreground))',
|
||||||
border: 'hsl(var(--sidebar-border))',
|
border: 'hsl(var(--sidebar-border))',
|
||||||
ring: 'hsl(var(--sidebar-ring))'
|
ring: 'hsl(var(--sidebar-ring))'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
borderRadius: {
|
borderRadius: {
|
||||||
lg: 'var(--radius)',
|
lg: 'var(--radius)',
|
||||||
md: 'calc(var(--radius) - 2px)',
|
md: 'calc(var(--radius) - 2px)',
|
||||||
sm: 'calc(var(--radius) - 4px)'
|
sm: 'calc(var(--radius) - 4px)'
|
||||||
},
|
},
|
||||||
keyframes: {
|
keyframes: {
|
||||||
'accordion-down': {
|
'accordion-down': {
|
||||||
from: {
|
from: {
|
||||||
height: '0'
|
height: '0'
|
||||||
},
|
},
|
||||||
to: {
|
to: {
|
||||||
height: 'var(--radix-accordion-content-height)'
|
height: 'var(--radix-accordion-content-height)'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
'accordion-up': {
|
'accordion-up': {
|
||||||
from: {
|
from: {
|
||||||
height: 'var(--radix-accordion-content-height)'
|
height: 'var(--radix-accordion-content-height)'
|
||||||
},
|
},
|
||||||
to: {
|
to: {
|
||||||
height: '0'
|
height: '0'
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
},
|
rainbow: {
|
||||||
animation: {
|
"0%": { "background-position": "0%" },
|
||||||
'accordion-down': 'accordion-down 0.2s ease-out',
|
"100%": { "background-position": "200%" },
|
||||||
'accordion-up': 'accordion-up 0.2s ease-out'
|
},
|
||||||
}
|
shine: {
|
||||||
}
|
"0%": {
|
||||||
},
|
"background-position": "0% 0%",
|
||||||
plugins: [require("tailwindcss-animate")],
|
},
|
||||||
|
"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
|
export default config
|
||||||
|
|||||||
Reference in New Issue
Block a user