Your commit message
This commit is contained in:
parent
818779ab07
commit
9fa8045c26
|
|
@ -762,7 +762,6 @@
|
||||||
"integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==",
|
"integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/code-frame": "^7.28.6",
|
"@babel/code-frame": "^7.28.6",
|
||||||
"@babel/generator": "^7.28.6",
|
"@babel/generator": "^7.28.6",
|
||||||
|
|
@ -3438,7 +3437,6 @@
|
||||||
"integrity": "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==",
|
"integrity": "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "BSD-2-Clause",
|
"license": "BSD-2-Clause",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/scope-manager": "6.21.0",
|
"@typescript-eslint/scope-manager": "6.21.0",
|
||||||
"@typescript-eslint/types": "6.21.0",
|
"@typescript-eslint/types": "6.21.0",
|
||||||
|
|
@ -3621,7 +3619,6 @@
|
||||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"acorn": "bin/acorn"
|
"acorn": "bin/acorn"
|
||||||
},
|
},
|
||||||
|
|
@ -4021,7 +4018,6 @@
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"baseline-browser-mapping": "^2.9.0",
|
"baseline-browser-mapping": "^2.9.0",
|
||||||
"caniuse-lite": "^1.0.30001759",
|
"caniuse-lite": "^1.0.30001759",
|
||||||
|
|
@ -4924,7 +4920,6 @@
|
||||||
"deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.",
|
"deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint-community/eslint-utils": "^4.2.0",
|
"@eslint-community/eslint-utils": "^4.2.0",
|
||||||
"@eslint-community/regexpp": "^4.6.1",
|
"@eslint-community/regexpp": "^4.6.1",
|
||||||
|
|
@ -7575,7 +7570,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/pg/-/pg-8.17.1.tgz",
|
"resolved": "https://registry.npmjs.org/pg/-/pg-8.17.1.tgz",
|
||||||
"integrity": "sha512-EIR+jXdYNSMOrpRp7g6WgQr7SaZNZfS7IzZIO0oTNEeibq956JxeD15t3Jk3zZH0KH8DmOIx38qJfQenoE8bXQ==",
|
"integrity": "sha512-EIR+jXdYNSMOrpRp7g6WgQr7SaZNZfS7IzZIO0oTNEeibq956JxeD15t3Jk3zZH0KH8DmOIx38qJfQenoE8bXQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"pg-connection-string": "^2.10.0",
|
"pg-connection-string": "^2.10.0",
|
||||||
"pg-pool": "^3.11.0",
|
"pg-pool": "^3.11.0",
|
||||||
|
|
@ -8707,7 +8701,6 @@
|
||||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"tsc": "bin/tsc",
|
"tsc": "bin/tsc",
|
||||||
"tsserver": "bin/tsserver"
|
"tsserver": "bin/tsserver"
|
||||||
|
|
@ -8998,7 +8991,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
|
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
|
||||||
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
|
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/colinhacks"
|
"url": "https://github.com/sponsors/colinhacks"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -121,3 +121,15 @@ CREATE TABLE IF NOT EXISTS webhook_logs (
|
||||||
CREATE INDEX IF NOT EXISTS idx_webhook_logs_user_id ON webhook_logs(user_id);
|
CREATE INDEX IF NOT EXISTS idx_webhook_logs_user_id ON webhook_logs(user_id);
|
||||||
CREATE INDEX IF NOT EXISTS idx_webhook_logs_monitor_id ON webhook_logs(monitor_id);
|
CREATE INDEX IF NOT EXISTS idx_webhook_logs_monitor_id ON webhook_logs(monitor_id);
|
||||||
CREATE INDEX IF NOT EXISTS idx_webhook_logs_created_at ON webhook_logs(created_at);
|
CREATE INDEX IF NOT EXISTS idx_webhook_logs_created_at ON webhook_logs(created_at);
|
||||||
|
|
||||||
|
-- Waitlist leads table (pre-launch signups)
|
||||||
|
CREATE TABLE IF NOT EXISTS waitlist_leads (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
email VARCHAR(255) UNIQUE NOT NULL,
|
||||||
|
source VARCHAR(50) DEFAULT 'landing_page',
|
||||||
|
referrer TEXT,
|
||||||
|
created_at TIMESTAMP DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_waitlist_leads_email ON waitlist_leads(email);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_waitlist_leads_created_at ON waitlist_leads(created_at);
|
||||||
|
|
|
||||||
|
|
@ -41,11 +41,13 @@ app.get('/health', async (_req, res) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
import testRoutes from './routes/test';
|
import testRoutes from './routes/test';
|
||||||
|
import waitlistRoutes from './routes/waitlist';
|
||||||
|
|
||||||
// Routes
|
// Routes
|
||||||
app.use('/api/auth', authLimiter, authRoutes);
|
app.use('/api/auth', authLimiter, authRoutes);
|
||||||
app.use('/api/monitors', authMiddleware, monitorRoutes);
|
app.use('/api/monitors', authMiddleware, monitorRoutes);
|
||||||
app.use('/api/settings', authMiddleware, settingsRoutes);
|
app.use('/api/settings', authMiddleware, settingsRoutes);
|
||||||
|
app.use('/api/waitlist', waitlistRoutes); // Public route - no auth required
|
||||||
app.use('/test', testRoutes);
|
app.use('/test', testRoutes);
|
||||||
|
|
||||||
// 404 handler
|
// 404 handler
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,95 @@
|
||||||
|
import { Router } from 'express';
|
||||||
|
import { pool } from '../db';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
const router = Router();
|
||||||
|
|
||||||
|
// Validation schema
|
||||||
|
const waitlistSchema = z.object({
|
||||||
|
email: z.string().email('Invalid email address'),
|
||||||
|
source: z.string().optional().default('landing_page'),
|
||||||
|
referrer: z.string().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// POST /api/waitlist - Add email to waitlist
|
||||||
|
router.post('/', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const data = waitlistSchema.parse(req.body);
|
||||||
|
|
||||||
|
// Check if email already exists
|
||||||
|
const existing = await pool.query(
|
||||||
|
'SELECT id FROM waitlist_leads WHERE email = $1',
|
||||||
|
[data.email.toLowerCase()]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (existing.rows.length > 0) {
|
||||||
|
// Already on waitlist - return success anyway (don't reveal they're already signed up)
|
||||||
|
const countResult = await pool.query('SELECT COUNT(*) FROM waitlist_leads');
|
||||||
|
const position = parseInt(countResult.rows[0].count, 10);
|
||||||
|
|
||||||
|
return res.json({
|
||||||
|
success: true,
|
||||||
|
message: 'You\'re on the list!',
|
||||||
|
position,
|
||||||
|
alreadySignedUp: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert new lead
|
||||||
|
await pool.query(
|
||||||
|
'INSERT INTO waitlist_leads (email, source, referrer) VALUES ($1, $2, $3)',
|
||||||
|
[data.email.toLowerCase(), data.source, data.referrer || null]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Get current position (total count)
|
||||||
|
const countResult = await pool.query('SELECT COUNT(*) FROM waitlist_leads');
|
||||||
|
const position = parseInt(countResult.rows[0].count, 10);
|
||||||
|
|
||||||
|
console.log(`✅ Waitlist signup: ${data.email} (Position #${position})`);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
message: 'You\'re on the list!',
|
||||||
|
position,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof z.ZodError) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
error: 'validation_error',
|
||||||
|
message: error.errors[0].message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error('Waitlist signup error:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
error: 'server_error',
|
||||||
|
message: 'Failed to join waitlist. Please try again.',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// GET /api/waitlist/count - Get current waitlist count (public)
|
||||||
|
router.get('/count', async (_req, res) => {
|
||||||
|
try {
|
||||||
|
const result = await pool.query('SELECT COUNT(*) FROM waitlist_leads');
|
||||||
|
const count = parseInt(result.rows[0].count, 10);
|
||||||
|
|
||||||
|
// Add a base number to make it look more impressive at launch
|
||||||
|
const displayCount = count + 430; // Starting with "430+ waiting"
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
count: displayCount,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Waitlist count error:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
count: 430, // Fallback to base number
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default router;
|
||||||
|
|
@ -76,24 +76,25 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Dark theme following the warm palette aesthetic */
|
/* Dark theme following the warm palette aesthetic */
|
||||||
.dark {
|
/* Dark theme override with higher specificity */
|
||||||
--background: 30 15% 10%;
|
html.dark {
|
||||||
--foreground: 40 14% 92%;
|
--background: 30 15% 4%;
|
||||||
|
--foreground: 40 14% 96%;
|
||||||
|
|
||||||
--card: 30 12% 14%;
|
--card: 30 10% 18%;
|
||||||
--card-foreground: 40 14% 92%;
|
--card-foreground: 40 14% 96%;
|
||||||
|
|
||||||
--popover: 30 12% 14%;
|
--popover: 30 10% 18%;
|
||||||
--popover-foreground: 40 14% 92%;
|
--popover-foreground: 40 14% 96%;
|
||||||
|
|
||||||
--primary: 32 35% 55%;
|
--primary: 32 35% 55%;
|
||||||
--primary-foreground: 30 15% 10%;
|
--primary-foreground: 30 15% 10%;
|
||||||
|
|
||||||
--secondary: 30 12% 20%;
|
--secondary: 30 10% 22%;
|
||||||
--secondary-foreground: 40 14% 92%;
|
--secondary-foreground: 40 14% 96%;
|
||||||
|
|
||||||
--muted: 30 12% 20%;
|
--muted: 30 10% 22%;
|
||||||
--muted-foreground: 35 10% 60%;
|
--muted-foreground: 35 10% 65%;
|
||||||
|
|
||||||
--accent: 32 35% 55%;
|
--accent: 32 35% 55%;
|
||||||
--accent-foreground: 30 15% 10%;
|
--accent-foreground: 30 15% 10%;
|
||||||
|
|
@ -107,9 +108,18 @@
|
||||||
--warning: 38 92% 50%;
|
--warning: 38 92% 50%;
|
||||||
--warning-foreground: 0 0% 100%;
|
--warning-foreground: 0 0% 100%;
|
||||||
|
|
||||||
--border: 30 12% 24%;
|
--border: 30 12% 20%;
|
||||||
--input: 30 12% 24%;
|
--input: 30 12% 20%;
|
||||||
--ring: 32 35% 50%;
|
--ring: 32 35% 50%;
|
||||||
|
|
||||||
|
/* Dark Mode Section Backgrounds */
|
||||||
|
--section-bg-1: 30 15% 8%;
|
||||||
|
--section-bg-2: 30 12% 10%;
|
||||||
|
--section-bg-3: 30 12% 9%;
|
||||||
|
--section-bg-4: 35 15% 11%;
|
||||||
|
--section-bg-5: 25 10% 9%;
|
||||||
|
--section-bg-6: 177 20% 8%;
|
||||||
|
--section-bg-7: 349 20% 9%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -196,6 +206,13 @@
|
||||||
inset 0 1px 0 0 rgba(255, 255, 255, 0.5);
|
inset 0 1px 0 0 rgba(255, 255, 255, 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dark .glass-card {
|
||||||
|
background: rgba(40, 35, 30, 0.7);
|
||||||
|
/* Lighter, warmer dark glass */
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||||
|
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
.glass-card-dark {
|
.glass-card-dark {
|
||||||
background: rgba(0, 0, 0, 0.4);
|
background: rgba(0, 0, 0, 0.4);
|
||||||
backdrop-filter: blur(20px) saturate(180%);
|
backdrop-filter: blur(20px) saturate(180%);
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,8 @@
|
||||||
|
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import { isAuthenticated } from '@/lib/auth'
|
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { ThemeToggle } from '@/components/ui/ThemeToggle'
|
||||||
import { HeroSection, UseCaseShowcase, HowItWorks, Differentiators, SocialProof, FinalCTA } from '@/components/landing/LandingSections'
|
import { HeroSection, UseCaseShowcase, HowItWorks, Differentiators, SocialProof, FinalCTA } from '@/components/landing/LandingSections'
|
||||||
import { LiveStatsBar } from '@/components/landing/LiveStatsBar'
|
import { LiveStatsBar } from '@/components/landing/LiveStatsBar'
|
||||||
import { PricingComparison } from '@/components/landing/PricingComparison'
|
import { PricingComparison } from '@/components/landing/PricingComparison'
|
||||||
|
|
@ -12,20 +12,11 @@ import { motion, AnimatePresence } from 'framer-motion'
|
||||||
import { Check, ChevronDown, Monitor, Globe, Shield, Clock, Zap, Menu } from 'lucide-react'
|
import { Check, ChevronDown, Monitor, Globe, Shield, Clock, Zap, Menu } from 'lucide-react'
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
const [loading, setLoading] = useState(true)
|
|
||||||
const [isAuth, setIsAuth] = useState(false)
|
|
||||||
const [openFaq, setOpenFaq] = useState<number | null>(null)
|
const [openFaq, setOpenFaq] = useState<number | null>(null)
|
||||||
const [billingPeriod, setBillingPeriod] = useState<'monthly' | 'yearly'>('monthly')
|
const [billingPeriod, setBillingPeriod] = useState<'monthly' | 'yearly'>('monthly')
|
||||||
const [mobileMenuOpen, setMobileMenuOpen] = useState(false)
|
const [mobileMenuOpen, setMobileMenuOpen] = useState(false)
|
||||||
const [scrollProgress, setScrollProgress] = useState(0)
|
const [scrollProgress, setScrollProgress] = useState(0)
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// Check auth status but DO NOT redirect
|
|
||||||
const auth = isAuthenticated()
|
|
||||||
setIsAuth(auth)
|
|
||||||
setLoading(false)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
// Scroll progress tracking
|
// Scroll progress tracking
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleScroll = () => {
|
const handleScroll = () => {
|
||||||
|
|
@ -38,16 +29,6 @@ export default function Home() {
|
||||||
return () => window.removeEventListener('scroll', handleScroll)
|
return () => window.removeEventListener('scroll', handleScroll)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
if (loading) {
|
|
||||||
return (
|
|
||||||
<div className="flex min-h-screen items-center justify-center bg-background">
|
|
||||||
<div className="flex flex-col items-center gap-4">
|
|
||||||
<div className="h-8 w-8 animate-spin rounded-full border-2 border-primary border-t-transparent" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const faqs = [
|
const faqs = [
|
||||||
{
|
{
|
||||||
question: 'What is website monitoring?',
|
question: 'What is website monitoring?',
|
||||||
|
|
@ -85,19 +66,14 @@ export default function Home() {
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
{isAuth ? (
|
<ThemeToggle />
|
||||||
<Link href="/dashboard">
|
<Button
|
||||||
<Button size="sm" className="bg-primary hover:bg-primary/90 text-primary-foreground rounded-full px-5 transition-transform hover:scale-105 active:scale-95 shadow-md shadow-primary/20">
|
size="sm"
|
||||||
Dashboard
|
className="bg-primary hover:bg-primary/90 text-primary-foreground rounded-full px-5 transition-transform hover:scale-105 active:scale-95 shadow-md shadow-primary/20"
|
||||||
|
onClick={() => document.getElementById('hero')?.scrollIntoView({ behavior: 'smooth' })}
|
||||||
|
>
|
||||||
|
Join Waitlist
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
|
||||||
) : (
|
|
||||||
<Link href="/register">
|
|
||||||
<Button size="sm" className="bg-primary hover:bg-primary/90 text-primary-foreground rounded-full px-5 transition-transform hover:scale-105 active:scale-95 shadow-md shadow-primary/20">
|
|
||||||
Get Started
|
|
||||||
</Button>
|
|
||||||
</Link>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Mobile Menu Button */}
|
{/* Mobile Menu Button */}
|
||||||
<button
|
<button
|
||||||
|
|
@ -121,11 +97,15 @@ export default function Home() {
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<Link href="#features" onClick={() => setMobileMenuOpen(false)} className="text-sm font-medium text-muted-foreground hover:text-foreground">Features</Link>
|
<Link href="#features" onClick={() => setMobileMenuOpen(false)} className="text-sm font-medium text-muted-foreground hover:text-foreground">Features</Link>
|
||||||
<Link href="#pricing" onClick={() => setMobileMenuOpen(false)} className="text-sm font-medium text-muted-foreground hover:text-foreground">Pricing</Link>
|
<Link href="#pricing" onClick={() => setMobileMenuOpen(false)} className="text-sm font-medium text-muted-foreground hover:text-foreground">Pricing</Link>
|
||||||
{!isAuth && (
|
<button
|
||||||
<>
|
onClick={() => {
|
||||||
<Link href="/register" onClick={() => setMobileMenuOpen(false)} className="text-sm font-medium text-primary font-bold">Get Started</Link>
|
setMobileMenuOpen(false)
|
||||||
</>
|
document.getElementById('hero')?.scrollIntoView({ behavior: 'smooth' })
|
||||||
)}
|
}}
|
||||||
|
className="text-sm font-medium text-primary font-bold text-left"
|
||||||
|
>
|
||||||
|
Join Waitlist
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -140,7 +120,7 @@ export default function Home() {
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Hero Section */}
|
{/* Hero Section */}
|
||||||
<HeroSection isAuthenticated={isAuth} />
|
<HeroSection />
|
||||||
|
|
||||||
{/* Live Stats Bar */}
|
{/* Live Stats Bar */}
|
||||||
<LiveStatsBar />
|
<LiveStatsBar />
|
||||||
|
|
@ -365,7 +345,7 @@ export default function Home() {
|
||||||
</section >
|
</section >
|
||||||
|
|
||||||
{/* Final CTA */}
|
{/* Final CTA */}
|
||||||
<FinalCTA isAuthenticated={isAuth} />
|
<FinalCTA />
|
||||||
|
|
||||||
{/* Footer */}
|
{/* Footer */}
|
||||||
< footer className="border-t border-border bg-background py-12 text-sm" >
|
< footer className="border-t border-border bg-background py-12 text-sm" >
|
||||||
|
|
|
||||||
|
|
@ -34,23 +34,22 @@ export function CompetitorDemoVisual() {
|
||||||
|
|
||||||
{/* Price Card */}
|
{/* Price Card */}
|
||||||
<motion.div
|
<motion.div
|
||||||
className="p-4 rounded-xl border-2 bg-white relative overflow-hidden"
|
className="p-4 rounded-xl border border-zinc-200 dark:border-zinc-800 bg-white dark:bg-zinc-950 relative overflow-hidden shadow-xl"
|
||||||
animate={{
|
animate={{
|
||||||
borderColor: phase === 1 ? 'hsl(var(--teal))' : 'hsl(var(--border))',
|
borderColor: phase === 1 ? '#ef4444' : '#27272a',
|
||||||
boxShadow: phase === 1
|
boxShadow: phase === 1
|
||||||
? '0 0 20px hsl(var(--teal) / 0.3)'
|
? '0 0 20px rgba(239, 68, 68, 0.2)'
|
||||||
: '0 1px 3px rgba(0,0,0,0.1)'
|
: '0 1px 3px rgba(0,0,0,0.5)'
|
||||||
}}
|
}}
|
||||||
transition={{ duration: 0.5 }}
|
transition={{ duration: 0.5 }}
|
||||||
>
|
>
|
||||||
{/* Shine effect on change */}
|
{/* Shine effect on change */}
|
||||||
{phase === 1 && (
|
{phase === 1 && (
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ x: '-100%' }}
|
initial={{ x: '-100%', skewX: -20 }}
|
||||||
animate={{ x: '200%' }}
|
animate={{ x: '200%' }}
|
||||||
transition={{ duration: 0.8, ease: 'easeInOut' }}
|
transition={{ duration: 0.8, ease: 'easeInOut' }}
|
||||||
className="absolute inset-0 bg-gradient-to-r from-transparent via-white/50 to-transparent"
|
className="absolute inset-0 bg-gradient-to-r from-transparent via-red-500/10 to-transparent"
|
||||||
style={{ transform: 'skewX(-20deg)' }}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
@ -58,7 +57,7 @@ export function CompetitorDemoVisual() {
|
||||||
{/* Old Price */}
|
{/* Old Price */}
|
||||||
<motion.div
|
<motion.div
|
||||||
animate={{
|
animate={{
|
||||||
opacity: phase === 1 ? 0.4 : 1,
|
opacity: phase === 1 ? 0.3 : 1,
|
||||||
scale: phase === 1 ? 0.95 : 1
|
scale: phase === 1 ? 0.95 : 1
|
||||||
}}
|
}}
|
||||||
transition={{ duration: 0.3 }}
|
transition={{ duration: 0.3 }}
|
||||||
|
|
@ -68,12 +67,12 @@ export function CompetitorDemoVisual() {
|
||||||
className="text-3xl font-bold"
|
className="text-3xl font-bold"
|
||||||
animate={{
|
animate={{
|
||||||
textDecoration: phase === 1 ? 'line-through' : 'none',
|
textDecoration: phase === 1 ? 'line-through' : 'none',
|
||||||
color: phase === 1 ? 'hsl(var(--muted-foreground))' : 'hsl(var(--foreground))'
|
color: phase === 1 ? '#ef4444' : '#f4f4f5'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
$99
|
$99
|
||||||
</motion.span>
|
</motion.span>
|
||||||
<span className="text-sm text-muted-foreground">/month</span>
|
<span className="text-sm text-zinc-500">/month</span>
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
|
|
@ -82,15 +81,17 @@ export function CompetitorDemoVisual() {
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, x: -10, scale: 0.9 }}
|
initial={{ opacity: 0, x: -10, scale: 0.9 }}
|
||||||
animate={{ opacity: 1, x: 0, scale: 1 }}
|
animate={{ opacity: 1, x: 0, scale: 1 }}
|
||||||
transition={{ delay: 0.2, type: 'spring', stiffness: 300, damping: 20 }}
|
transition={{ delay: 0.1, type: 'spring', stiffness: 300, damping: 20 }}
|
||||||
className="flex items-center gap-2"
|
className="flex items-center gap-3 mt-1"
|
||||||
>
|
>
|
||||||
<ArrowDown className="h-4 w-4 text-[hsl(var(--teal))]" />
|
<div className="flex items-center justify-center h-6 w-6 rounded-full bg-red-500/10">
|
||||||
|
<ArrowDown className="h-4 w-4 text-red-500" strokeWidth={3} />
|
||||||
|
</div>
|
||||||
<div className="flex items-baseline gap-2">
|
<div className="flex items-baseline gap-2">
|
||||||
<span className="text-4xl font-bold text-[hsl(var(--teal))]">
|
<span className="text-5xl font-extrabold text-[#ff0000] tracking-tight">
|
||||||
$79
|
$79
|
||||||
</span>
|
</span>
|
||||||
<span className="text-sm text-muted-foreground">/month</span>
|
<span className="text-sm font-medium text-red-500">/month</span>
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -98,12 +99,12 @@ export function CompetitorDemoVisual() {
|
||||||
{/* Savings Badge */}
|
{/* Savings Badge */}
|
||||||
{phase === 1 && (
|
{phase === 1 && (
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, scale: 0.8, rotate: -5 }}
|
initial={{ opacity: 0, scale: 0.8, rotate: -3 }}
|
||||||
animate={{ opacity: 1, scale: 1, rotate: 0 }}
|
animate={{ opacity: 1, scale: 1, rotate: 0 }}
|
||||||
transition={{ delay: 0.4, type: 'spring' }}
|
transition={{ delay: 0.3, type: 'spring' }}
|
||||||
className="inline-flex items-center gap-1 px-2 py-1 rounded-full bg-[hsl(var(--teal))]/10 border border-[hsl(var(--teal))]/30"
|
className="mt-2 inline-flex items-center gap-1.5 px-3 py-1 rounded-full bg-red-500/10 border border-red-500/20"
|
||||||
>
|
>
|
||||||
<span className="text-[9px] font-bold text-[hsl(var(--teal))] uppercase tracking-wider">
|
<span className="text-[10px] font-extrabold text-red-500 uppercase tracking-wider">
|
||||||
Save $240/year
|
Save $240/year
|
||||||
</span>
|
</span>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
@ -112,26 +113,28 @@ export function CompetitorDemoVisual() {
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
{/* Alert Notification */}
|
{/* Alert Notification */}
|
||||||
{phase === 1 && (
|
{
|
||||||
|
phase === 1 && (
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, y: 10, scale: 0.95 }}
|
initial={{ opacity: 0, y: 10, scale: 0.95 }}
|
||||||
animate={{ opacity: 1, y: 0, scale: 1 }}
|
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||||
transition={{ delay: 0.6 }}
|
transition={{ delay: 0.6 }}
|
||||||
className="flex items-center gap-2 p-2 rounded-lg bg-[hsl(var(--burgundy))]/10 border border-[hsl(var(--burgundy))]/30"
|
className="flex items-center gap-2 p-2 rounded-lg bg-red-500/10 border border-red-500/30"
|
||||||
>
|
>
|
||||||
<div className="relative flex-shrink-0">
|
<div className="relative flex-shrink-0">
|
||||||
<Bell className="h-3 w-3 text-[hsl(var(--burgundy))]" />
|
<Bell className="h-3 w-3 text-red-500" />
|
||||||
<motion.span
|
<motion.span
|
||||||
animate={{ scale: [1, 1.3, 1] }}
|
animate={{ scale: [1, 1.3, 1] }}
|
||||||
transition={{ duration: 1, repeat: Infinity }}
|
transition={{ duration: 1, repeat: Infinity }}
|
||||||
className="absolute -top-0.5 -right-0.5 w-1.5 h-1.5 rounded-full bg-[hsl(var(--burgundy))]"
|
className="absolute -top-0.5 -right-0.5 w-1.5 h-1.5 rounded-full bg-red-500"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-[9px] font-semibold text-[hsl(var(--burgundy))]">
|
<span className="text-[9px] font-semibold text-red-500">
|
||||||
Alert sent to your team
|
Alert sent to your team
|
||||||
</span>
|
</span>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -42,9 +42,9 @@ const scaleIn: Variants = {
|
||||||
// ============================================
|
// ============================================
|
||||||
// 1. HERO SECTION - "Track competitor changes without the noise"
|
// 1. HERO SECTION - "Track competitor changes without the noise"
|
||||||
// ============================================
|
// ============================================
|
||||||
export function HeroSection({ isAuthenticated }: { isAuthenticated: boolean }) {
|
export function HeroSection() {
|
||||||
return (
|
return (
|
||||||
<section className="relative overflow-hidden pt-32 pb-24 lg:pt-40 lg:pb-32 bg-[hsl(var(--section-bg-1))]">
|
<section id="hero" className="relative overflow-hidden pt-32 pb-24 lg:pt-40 lg:pb-32 bg-[hsl(var(--section-bg-1))]">
|
||||||
{/* Background Elements */}
|
{/* Background Elements */}
|
||||||
<div className="absolute inset-0 grain-texture" />
|
<div className="absolute inset-0 grain-texture" />
|
||||||
<div className="absolute right-0 top-20 -z-10 h-[600px] w-[600px] rounded-full bg-[hsl(var(--primary))] opacity-8 blur-[120px]" />
|
<div className="absolute right-0 top-20 -z-10 h-[600px] w-[600px] rounded-full bg-[hsl(var(--primary))] opacity-8 blur-[120px]" />
|
||||||
|
|
@ -109,23 +109,13 @@ export function HeroSection({ isAuthenticated }: { isAuthenticated: boolean }) {
|
||||||
))}
|
))}
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
{/* CTAs */}
|
{/* Waitlist Form */}
|
||||||
<motion.div
|
<motion.div
|
||||||
variants={fadeInUp}
|
variants={fadeInUp}
|
||||||
custom={4}
|
custom={4}
|
||||||
className="flex flex-wrap gap-4"
|
className="w-full max-w-lg"
|
||||||
>
|
>
|
||||||
<MagneticButton strength={0.2}>
|
<WaitlistForm />
|
||||||
<Link href={isAuthenticated ? "/dashboard" : "/register"}>
|
|
||||||
<Button
|
|
||||||
size="lg"
|
|
||||||
className="h-14 rounded-full bg-[hsl(var(--primary))] px-8 text-white hover:bg-[hsl(var(--primary))]/90 shadow-2xl shadow-[hsl(var(--primary))]/25 transition-all hover:scale-105 hover:-translate-y-0.5 font-semibold text-base group"
|
|
||||||
>
|
|
||||||
{isAuthenticated ? 'Go to Dashboard' : 'Get Started Free'}
|
|
||||||
<ArrowRight className="ml-2 h-4 w-4 group-hover:translate-x-1 transition-transform" />
|
|
||||||
</Button>
|
|
||||||
</Link>
|
|
||||||
</MagneticButton>
|
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
{/* Trust Signals */}
|
{/* Trust Signals */}
|
||||||
|
|
@ -352,19 +342,19 @@ function NoiseToSignalVisual() {
|
||||||
className="absolute inset-0 flex items-center justify-center p-8"
|
className="absolute inset-0 flex items-center justify-center p-8"
|
||||||
>
|
>
|
||||||
<motion.div
|
<motion.div
|
||||||
className="w-full p-6 rounded-2xl bg-white border-2 border-[hsl(var(--teal))] shadow-2xl relative overflow-hidden"
|
className="w-full p-6 rounded-2xl bg-white dark:bg-zinc-950 border-2 border-[hsl(var(--teal))] dark:border-zinc-800 shadow-2xl relative overflow-hidden"
|
||||||
animate={{
|
animate={{
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
'0 20px 60px hsl(var(--teal) / 0.2)',
|
'0 20px 60px rgba(20, 184, 166, 0.1)',
|
||||||
'0 20px 80px hsl(var(--teal) / 0.3)',
|
'0 20px 80px rgba(20, 184, 166, 0.2)',
|
||||||
'0 20px 60px hsl(var(--teal) / 0.2)'
|
'0 20px 60px rgba(20, 184, 166, 0.1)'
|
||||||
]
|
]
|
||||||
}}
|
}}
|
||||||
transition={{ duration: 2, repeat: Infinity }}
|
transition={{ duration: 2, repeat: Infinity }}
|
||||||
>
|
>
|
||||||
{/* Animated corner accent */}
|
{/* Animated corner accent */}
|
||||||
<motion.div
|
<motion.div
|
||||||
className="absolute top-0 right-0 w-20 h-20 bg-[hsl(var(--teal))]/10 rounded-bl-full"
|
className="absolute top-0 right-0 w-20 h-20 bg-[hsl(var(--teal))]/5 rounded-bl-full"
|
||||||
animate={{ scale: [1, 1.1, 1] }}
|
animate={{ scale: [1, 1.1, 1] }}
|
||||||
transition={{ duration: 2, repeat: Infinity }}
|
transition={{ duration: 2, repeat: Infinity }}
|
||||||
/>
|
/>
|
||||||
|
|
@ -372,20 +362,20 @@ function NoiseToSignalVisual() {
|
||||||
<div className="relative z-10">
|
<div className="relative z-10">
|
||||||
<div className="flex items-center justify-between mb-2">
|
<div className="flex items-center justify-between mb-2">
|
||||||
<motion.span
|
<motion.span
|
||||||
className="text-xs font-bold uppercase tracking-wider text-[hsl(var(--teal))]"
|
className="text-xs font-bold uppercase tracking-wider text-[hsl(var(--teal))] dark:text-[hsl(var(--teal))]"
|
||||||
animate={{ opacity: [1, 0.7, 1] }}
|
animate={{ opacity: [1, 0.7, 1] }}
|
||||||
transition={{ duration: 1.5, repeat: Infinity }}
|
transition={{ duration: 1.5, repeat: Infinity }}
|
||||||
>
|
>
|
||||||
✓ SIGNAL DETECTED
|
✓ SIGNAL DETECTED
|
||||||
</motion.span>
|
</motion.span>
|
||||||
<div className="flex items-center gap-1.5 text-xs font-medium text-[hsl(var(--teal))]">
|
<div className="flex items-center gap-1.5 text-xs font-medium text-[hsl(var(--teal))] dark:text-[hsl(var(--teal))]">
|
||||||
<Filter className="h-3 w-3" />
|
<Filter className="h-3 w-3" />
|
||||||
Filtered
|
Filtered
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm font-semibold text-muted-foreground mb-3">Enterprise Plan</p>
|
<p className="text-sm font-semibold text-muted-foreground dark:text-zinc-400 mb-3">Enterprise Plan</p>
|
||||||
<div className="flex items-baseline gap-3">
|
<div className="flex items-baseline gap-3">
|
||||||
<p className="text-3xl font-bold text-foreground">$99/mo</p>
|
<p className="text-3xl font-bold text-foreground dark:text-zinc-600/50">$99/mo</p>
|
||||||
<motion.p
|
<motion.p
|
||||||
initial={{ opacity: 0, x: -10, scale: 0.9 }}
|
initial={{ opacity: 0, x: -10, scale: 0.9 }}
|
||||||
animate={{
|
animate={{
|
||||||
|
|
@ -394,12 +384,13 @@ function NoiseToSignalVisual() {
|
||||||
scale: phase >= 2 ? 1 : 0.9
|
scale: phase >= 2 ? 1 : 0.9
|
||||||
}}
|
}}
|
||||||
transition={{ duration: 0.5, ease: [0.22, 1, 0.36, 1] }}
|
transition={{ duration: 0.5, ease: [0.22, 1, 0.36, 1] }}
|
||||||
className="text-lg text-[hsl(var(--burgundy))] font-bold flex items-center gap-1"
|
className="text-lg text-[hsl(var(--burgundy))] dark:text-red-500 font-bold flex items-center gap-1"
|
||||||
>
|
>
|
||||||
<span>→</span>
|
<span>→</span>
|
||||||
<motion.span
|
<motion.span
|
||||||
animate={{ scale: phase === 2 ? [1, 1.1, 1] : 1 }}
|
animate={{ scale: phase === 2 ? [1, 1.1, 1] : 1 }}
|
||||||
transition={{ duration: 0.5 }}
|
transition={{ duration: 0.5 }}
|
||||||
|
className="text-3xl"
|
||||||
>
|
>
|
||||||
$79/mo
|
$79/mo
|
||||||
</motion.span>
|
</motion.span>
|
||||||
|
|
@ -411,10 +402,10 @@ function NoiseToSignalVisual() {
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, y: 10 }}
|
initial={{ opacity: 0, y: 10 }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
className="mt-4 inline-flex items-center gap-2 px-3 py-1.5 rounded-full bg-[hsl(var(--burgundy))]/10 border border-[hsl(var(--burgundy))]/30"
|
className="mt-4 inline-flex items-center gap-2 px-3 py-1.5 rounded-full bg-[hsl(var(--burgundy))]/10 border border-[hsl(var(--burgundy))]/30 dark:bg-red-500/10 dark:border-red-500/20"
|
||||||
>
|
>
|
||||||
<Bell className="h-3 w-3 text-[hsl(var(--burgundy))]" />
|
<Bell className="h-3 w-3 text-[hsl(var(--burgundy))] dark:text-red-500" />
|
||||||
<span className="text-[10px] font-bold text-[hsl(var(--burgundy))] uppercase tracking-wider">
|
<span className="text-[10px] font-bold text-[hsl(var(--burgundy))] dark:text-red-500 uppercase tracking-wider">
|
||||||
Alert Sent
|
Alert Sent
|
||||||
</span>
|
</span>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
@ -424,6 +415,7 @@ function NoiseToSignalVisual() {
|
||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
||||||
{/* Phase Indicator */}
|
{/* Phase Indicator */}
|
||||||
<div className="absolute bottom-4 right-4 flex gap-1.5">
|
<div className="absolute bottom-4 right-4 flex gap-1.5">
|
||||||
{[0, 1, 2, 3].map(i => (
|
{[0, 1, 2, 3].map(i => (
|
||||||
|
|
@ -742,14 +734,12 @@ export function SocialProof() {
|
||||||
]
|
]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="py-32 bg-gradient-to-b from-foreground to-[hsl(var(--foreground))]/95 relative overflow-hidden text-white">
|
<section className="py-32 bg-[hsl(var(--section-bg-2))] relative overflow-hidden">
|
||||||
{/* Background Pattern */}
|
{/* Background Pattern - Subtle dots for light theme */}
|
||||||
<div className="absolute inset-0 opacity-5">
|
<div className="absolute inset-0 opacity-[0.03]" style={{
|
||||||
<div className="absolute inset-0" style={{
|
backgroundImage: `radial-gradient(hsl(var(--foreground)) 1px, transparent 1px)`,
|
||||||
backgroundImage: `url("data:image/svg+xml,%3Csvg width='60' height='60' viewBox='0 0 60 60' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cg fill='%23ffffff' fill-opacity='1'%3E%3Cpath d='M36 34v-4h-2v4h-4v2h4v4h2v-4h4v-2h-4zm0-30V0h-2v4h-4v2h4v4h2V6h4V4h-4zM6 34v-4H4v4H0v2h4v4h2v-4h4v-2H6zM6 4V0H4v4H0v2h4v4h2V6h4V4H6z'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E")`,
|
backgroundSize: '24px 24px'
|
||||||
backgroundSize: '60px 60px'
|
|
||||||
}} />
|
}} />
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mx-auto max-w-7xl px-6 relative z-10">
|
<div className="mx-auto max-w-7xl px-6 relative z-10">
|
||||||
{/* Section Header */}
|
{/* Section Header */}
|
||||||
|
|
@ -759,13 +749,13 @@ export function SocialProof() {
|
||||||
viewport={{ once: true }}
|
viewport={{ once: true }}
|
||||||
className="text-center mb-20"
|
className="text-center mb-20"
|
||||||
>
|
>
|
||||||
<motion.h2 variants={fadeInUp} className="text-4xl lg:text-5xl font-display font-bold mb-6">
|
<motion.h2 variants={fadeInUp} className="text-4xl lg:text-5xl font-display font-bold text-foreground mb-6">
|
||||||
Built for teams who need results,{' '}
|
Built for teams who need results,{' '}
|
||||||
<span className="text-[hsl(var(--primary))]">not demos.</span>
|
<span className="text-[hsl(var(--primary))]">not demos.</span>
|
||||||
</motion.h2>
|
</motion.h2>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
{/* Testimonial Cards - Minimal & Uniform */}
|
{/* Testimonial Cards - Light Theme */}
|
||||||
<div className="grid md:grid-cols-3 gap-8">
|
<div className="grid md:grid-cols-3 gap-8">
|
||||||
{testimonials.map((testimonial, i) => (
|
{testimonials.map((testimonial, i) => (
|
||||||
<motion.div
|
<motion.div
|
||||||
|
|
@ -775,30 +765,28 @@ export function SocialProof() {
|
||||||
whileHover={{ y: -4 }}
|
whileHover={{ y: -4 }}
|
||||||
viewport={{ once: true }}
|
viewport={{ once: true }}
|
||||||
transition={{ delay: i * 0.1, duration: 0.5 }}
|
transition={{ delay: i * 0.1, duration: 0.5 }}
|
||||||
className="relative group"
|
className="relative group h-full"
|
||||||
>
|
>
|
||||||
{/* Subtle gradient border glow */}
|
{/* Card Container */}
|
||||||
<div className="absolute -inset-0.5 bg-gradient-to-br from-[hsl(var(--primary))] to-[hsl(var(--teal))] rounded-3xl blur opacity-15 group-hover:opacity-25 transition-opacity duration-300" />
|
<div className="h-full flex flex-col rounded-3xl bg-white border border-zinc-200 shadow-sm p-8 hover:shadow-xl transition-all duration-300">
|
||||||
|
|
||||||
{/* Main Card - fixed height for uniformity */}
|
{/* Quote Mark */}
|
||||||
<div className="relative h-full flex flex-col rounded-3xl bg-white/10 backdrop-blur-sm border border-white/20 p-8 group-hover:bg-white/12 transition-colors duration-300">
|
<div className="text-5xl font-display text-zinc-300 leading-none mb-4">
|
||||||
{/* Large Quote Mark */}
|
|
||||||
<div className="text-5xl font-display text-[hsl(var(--primary))]/30 leading-none mb-3">
|
|
||||||
"
|
"
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Quote - flex-grow ensures cards align */}
|
{/* Quote */}
|
||||||
<p className="font-body text-base leading-relaxed mb-6 text-white/90 font-medium italic flex-grow">
|
<p className="font-body text-base leading-relaxed mb-8 text-zinc-900 font-medium italic flex-grow">
|
||||||
{testimonial.quote}
|
{testimonial.quote}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{/* Attribution - always at bottom */}
|
{/* Attribution */}
|
||||||
<div className="flex items-start justify-between mt-auto">
|
<div className="flex items-start justify-between mt-auto pt-6 border-t border-zinc-100">
|
||||||
<div>
|
<div>
|
||||||
<p className="font-bold text-white text-sm">{testimonial.author}</p>
|
<p className="font-bold text-zinc-900 text-sm">{testimonial.author}</p>
|
||||||
<p className="text-xs text-white/70">{testimonial.role} at {testimonial.company}</p>
|
<p className="text-xs text-zinc-500">{testimonial.role} at {testimonial.company}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="px-3 py-1 rounded-full bg-[hsl(var(--teal))]/20 border border-[hsl(var(--teal))]/30 text-[10px] font-bold uppercase tracking-wider text-[hsl(var(--teal))]">
|
<div className="px-3 py-1 rounded-full bg-zinc-100 border border-zinc-200 text-[10px] font-bold uppercase tracking-wider text-zinc-600">
|
||||||
{testimonial.useCase}
|
{testimonial.useCase}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -824,7 +812,7 @@ export function SocialProof() {
|
||||||
// ============================================
|
// ============================================
|
||||||
// 6. FINAL CTA - Get Started
|
// 6. FINAL CTA - Get Started
|
||||||
// ============================================
|
// ============================================
|
||||||
export function FinalCTA({ isAuthenticated }: { isAuthenticated: boolean }) {
|
export function FinalCTA() {
|
||||||
return (
|
return (
|
||||||
<section className="relative overflow-hidden py-32">
|
<section className="relative overflow-hidden py-32">
|
||||||
{/* Animated Gradient Mesh Background - More dramatic */}
|
{/* Animated Gradient Mesh Background - More dramatic */}
|
||||||
|
|
@ -869,23 +857,9 @@ export function FinalCTA({ isAuthenticated }: { isAuthenticated: boolean }) {
|
||||||
Join the waitlist and be first to experience monitoring that actually works.
|
Join the waitlist and be first to experience monitoring that actually works.
|
||||||
</motion.p>
|
</motion.p>
|
||||||
|
|
||||||
{/* Waitlist Form - replaces button */}
|
{/* Waitlist Form */}
|
||||||
<motion.div variants={fadeInUp} custom={2} className="pt-4">
|
<motion.div variants={fadeInUp} custom={2} className="pt-4 max-w-lg mx-auto">
|
||||||
{isAuthenticated ? (
|
|
||||||
<MagneticButton strength={0.15}>
|
|
||||||
<Link href="/dashboard">
|
|
||||||
<Button
|
|
||||||
size="lg"
|
|
||||||
className="h-16 rounded-full bg-[hsl(var(--burgundy))] px-12 text-white hover:bg-[hsl(var(--burgundy))]/90 shadow-2xl shadow-[hsl(var(--burgundy))]/30 transition-all hover:scale-105 font-bold text-lg group"
|
|
||||||
>
|
|
||||||
Go to Dashboard
|
|
||||||
<ArrowRight className="ml-2 h-5 w-5 group-hover:translate-x-1 transition-transform" />
|
|
||||||
</Button>
|
|
||||||
</Link>
|
|
||||||
</MagneticButton>
|
|
||||||
) : (
|
|
||||||
<WaitlistForm />
|
<WaitlistForm />
|
||||||
)}
|
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
{/* Social Proof Indicator */}
|
{/* Social Proof Indicator */}
|
||||||
|
|
|
||||||
|
|
@ -84,7 +84,7 @@ export function LiveStatsBar() {
|
||||||
]
|
]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="border-y border-border bg-gradient-to-r from-foreground/95 via-foreground to-foreground/95 py-8 overflow-hidden">
|
<section className="border-y border-border bg-gradient-to-r from-foreground/95 via-foreground to-foreground/95 dark:from-secondary dark:via-secondary dark:to-secondary py-8 overflow-hidden">
|
||||||
<div className="mx-auto max-w-7xl px-6">
|
<div className="mx-auto max-w-7xl px-6">
|
||||||
{/* Desktop: Grid */}
|
{/* Desktop: Grid */}
|
||||||
<div className="hidden lg:grid lg:grid-cols-4 gap-8">
|
<div className="hidden lg:grid lg:grid-cols-4 gap-8">
|
||||||
|
|
|
||||||
|
|
@ -37,23 +37,23 @@ export function PolicyDemoVisual() {
|
||||||
|
|
||||||
{/* Document Content */}
|
{/* Document Content */}
|
||||||
<motion.div
|
<motion.div
|
||||||
className="space-y-2 p-3 rounded-lg border-2 bg-white overflow-hidden"
|
className="space-y-2 p-3 rounded-lg border border-zinc-200 dark:border-zinc-800 bg-white dark:bg-zinc-950 overflow-hidden"
|
||||||
animate={{
|
animate={{
|
||||||
borderColor: phase === 1 ? 'hsl(var(--teal))' : 'hsl(var(--border))',
|
borderColor: phase === 1 ? '#ef4444' : '#27272a',
|
||||||
boxShadow: phase === 1
|
boxShadow: phase === 1
|
||||||
? '0 0 20px hsl(var(--teal) / 0.3)'
|
? '0 0 20px rgba(239, 68, 68, 0.2)'
|
||||||
: '0 1px 3px rgba(0,0,0,0.1)'
|
: '0 1px 3px rgba(0,0,0,0.2)'
|
||||||
}}
|
}}
|
||||||
transition={{ duration: 0.5 }}
|
transition={{ duration: 0.5 }}
|
||||||
>
|
>
|
||||||
{/* Section 4.2 */}
|
{/* Section 4.2 */}
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
<div className="text-[10px] font-bold text-[hsl(var(--primary))]">
|
<div className="text-[10px] font-bold text-zinc-300">
|
||||||
Section 4.2 - Data Retention
|
Section 4.2 - Data Retention
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Text Lines */}
|
{/* Text Lines */}
|
||||||
<div className="space-y-1 text-[9px] text-muted-foreground leading-relaxed">
|
<div className="space-y-1 text-[9px] text-zinc-500 leading-relaxed">
|
||||||
<p>We will retain your personal data for</p>
|
<p>We will retain your personal data for</p>
|
||||||
|
|
||||||
{/* Changing text */}
|
{/* Changing text */}
|
||||||
|
|
@ -63,10 +63,10 @@ export function PolicyDemoVisual() {
|
||||||
>
|
>
|
||||||
<motion.p
|
<motion.p
|
||||||
animate={{
|
animate={{
|
||||||
backgroundColor: phase === 1 ? 'hsl(var(--burgundy) / 0.15)' : 'transparent',
|
backgroundColor: phase === 1 ? 'rgba(239, 68, 68, 0.1)' : 'transparent',
|
||||||
paddingLeft: phase === 1 ? '4px' : '0px',
|
paddingLeft: phase === 1 ? '4px' : '0px',
|
||||||
paddingRight: phase === 1 ? '4px' : '0px',
|
paddingRight: phase === 1 ? '4px' : '0px',
|
||||||
color: phase === 1 ? 'hsl(var(--burgundy))' : 'hsl(var(--muted-foreground))',
|
color: phase === 1 ? '#ef4444' : 'inherit',
|
||||||
fontWeight: phase === 1 ? 600 : 400
|
fontWeight: phase === 1 ? 600 : 400
|
||||||
}}
|
}}
|
||||||
transition={{ duration: 0.4 }}
|
transition={{ duration: 0.4 }}
|
||||||
|
|
@ -91,7 +91,7 @@ export function PolicyDemoVisual() {
|
||||||
initial={{ scaleX: 0 }}
|
initial={{ scaleX: 0 }}
|
||||||
animate={{ scaleX: 1 }}
|
animate={{ scaleX: 1 }}
|
||||||
transition={{ duration: 0.4 }}
|
transition={{ duration: 0.4 }}
|
||||||
className="absolute -left-1 top-0 bottom-0 w-0.5 bg-[hsl(var(--burgundy))] rounded-full origin-left"
|
className="absolute -left-1 top-0 bottom-0 w-0.5 bg-red-500 rounded-full origin-left"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
@ -106,9 +106,9 @@ export function PolicyDemoVisual() {
|
||||||
initial={{ opacity: 0, height: 0 }}
|
initial={{ opacity: 0, height: 0 }}
|
||||||
animate={{ opacity: 1, height: 'auto' }}
|
animate={{ opacity: 1, height: 'auto' }}
|
||||||
transition={{ delay: 0.3 }}
|
transition={{ delay: 0.3 }}
|
||||||
className="pt-2 border-t border-border flex items-center justify-between"
|
className="pt-2 border-t border-zinc-800 flex items-center justify-between"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-3 text-[8px] text-muted-foreground">
|
<div className="flex items-center gap-3 text-[8px] text-zinc-500">
|
||||||
<span className="flex items-center gap-1">
|
<span className="flex items-center gap-1">
|
||||||
<span className="w-2 h-2 rounded bg-green-500/20 border border-green-500" />
|
<span className="w-2 h-2 rounded bg-green-500/20 border border-green-500" />
|
||||||
+18 words
|
+18 words
|
||||||
|
|
@ -128,16 +128,16 @@ export function PolicyDemoVisual() {
|
||||||
initial={{ opacity: 0, y: 5, scale: 0.9 }}
|
initial={{ opacity: 0, y: 5, scale: 0.9 }}
|
||||||
animate={{ opacity: 1, y: 0, scale: 1 }}
|
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||||
transition={{ delay: 0.5 }}
|
transition={{ delay: 0.5 }}
|
||||||
className="mt-3 flex items-center gap-2 p-2 rounded-lg bg-[hsl(var(--teal))]/10 border border-[hsl(var(--teal))]/30"
|
className="mt-3 flex items-center gap-2 p-2 rounded-lg bg-red-500/10 border border-red-500/30"
|
||||||
>
|
>
|
||||||
<div className="flex-shrink-0 flex items-center justify-center w-5 h-5 rounded-full bg-[hsl(var(--teal))] text-white">
|
<div className="flex-shrink-0 flex items-center justify-center w-5 h-5 rounded-full bg-red-500 text-white">
|
||||||
<Check className="h-3 w-3" strokeWidth={3} />
|
<Check className="h-3 w-3" strokeWidth={3} />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<div className="text-[9px] font-bold text-[hsl(var(--teal))]">
|
<div className="text-[9px] font-bold text-red-500">
|
||||||
Audit trail saved
|
Audit trail saved
|
||||||
</div>
|
</div>
|
||||||
<div className="text-[8px] text-muted-foreground">
|
<div className="text-[8px] text-red-500/80">
|
||||||
Snapshot archived for compliance
|
Snapshot archived for compliance
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -57,36 +57,37 @@ export function SEODemoVisual() {
|
||||||
|
|
||||||
{/* SERP Snippet */}
|
{/* SERP Snippet */}
|
||||||
<motion.div
|
<motion.div
|
||||||
className="space-y-2 p-3 rounded-lg bg-white border-2"
|
className="space-y-2 p-3 rounded-lg border border-zinc-200 dark:border-zinc-800 bg-white dark:bg-zinc-950"
|
||||||
animate={{
|
animate={{
|
||||||
borderColor: phase === 0 ? 'hsl(var(--border))' : 'hsl(var(--teal))',
|
borderColor: phase === 0 ? '#27272a' : '#ef4444',
|
||||||
boxShadow: phase === 0
|
boxShadow: phase === 0
|
||||||
? '0 1px 3px rgba(0,0,0,0.1)'
|
? '0 1px 3px rgba(0,0,0,0.2)'
|
||||||
: '0 0 20px hsl(var(--teal) / 0.3)'
|
: '0 0 20px rgba(239, 68, 68, 0.2)'
|
||||||
}}
|
}}
|
||||||
transition={{ duration: 0.5 }}
|
transition={{ duration: 0.5 }}
|
||||||
>
|
>
|
||||||
{/* URL */}
|
{/* URL */}
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="w-4 h-4 rounded-full bg-primary" />
|
<div className="w-4 h-4 rounded-full bg-zinc-800" />
|
||||||
<span className="text-[10px] text-muted-foreground font-mono">
|
<span className="text-[10px] text-zinc-500 font-mono">
|
||||||
competitor.com/product
|
competitor.com/product
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Title */}
|
{/* Title */}
|
||||||
<h4 className="text-sm font-bold text-[hsl(var(--primary))] line-clamp-1">
|
<h4 className="text-sm font-bold text-zinc-300 line-clamp-1">
|
||||||
Best Enterprise Software Solution 2026
|
Best Enterprise Software Solution 2026
|
||||||
</h4>
|
</h4>
|
||||||
|
|
||||||
{/* Meta Description with change highlighting */}
|
{/* Meta Description with change highlighting */}
|
||||||
<motion.p
|
<motion.p
|
||||||
className="text-[11px] text-muted-foreground leading-relaxed relative"
|
className="text-[11px] text-zinc-500 leading-relaxed relative"
|
||||||
layout
|
layout
|
||||||
>
|
>
|
||||||
<motion.span
|
<motion.span
|
||||||
animate={{
|
animate={{
|
||||||
backgroundColor: phase === 1 ? 'hsl(var(--teal) / 0.2)' : 'transparent'
|
backgroundColor: phase === 1 ? 'rgba(239, 68, 68, 0.1)' : 'transparent',
|
||||||
|
color: phase === 1 ? '#ef4444' : 'inherit'
|
||||||
}}
|
}}
|
||||||
transition={{ duration: 0.5 }}
|
transition={{ duration: 0.5 }}
|
||||||
className="inline-block rounded px-0.5"
|
className="inline-block rounded px-0.5"
|
||||||
|
|
@ -99,9 +100,9 @@ export function SEODemoVisual() {
|
||||||
<motion.span
|
<motion.span
|
||||||
initial={{ opacity: 0, x: -5 }}
|
initial={{ opacity: 0, x: -5 }}
|
||||||
animate={{ opacity: 1, x: 0 }}
|
animate={{ opacity: 1, x: 0 }}
|
||||||
className="absolute -right-2 top-0 px-1.5 py-0.5 rounded bg-[hsl(var(--burgundy))] text-[8px] font-bold text-white"
|
className="absolute -right-2 top-0 px-1.5 py-0.5 rounded bg-red-500 text-[8px] font-bold text-white"
|
||||||
>
|
>
|
||||||
NEW
|
Changed
|
||||||
</motion.span>
|
</motion.span>
|
||||||
)}
|
)}
|
||||||
</motion.p>
|
</motion.p>
|
||||||
|
|
|
||||||
|
|
@ -47,16 +47,34 @@ export function WaitlistForm() {
|
||||||
|
|
||||||
setIsSubmitting(true)
|
setIsSubmitting(true)
|
||||||
|
|
||||||
// Simulate API call
|
try {
|
||||||
await new Promise(resolve => setTimeout(resolve, 1500))
|
const apiUrl = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001'
|
||||||
|
const response = await fetch(`${apiUrl}/api/waitlist`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
email,
|
||||||
|
source: 'landing_page',
|
||||||
|
referrer: typeof window !== 'undefined' ? document.referrer : undefined,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
// Generate a random queue position
|
const data = await response.json()
|
||||||
const position = Math.floor(Math.random() * 500) + 400
|
|
||||||
|
|
||||||
setQueuePosition(position)
|
if (data.success) {
|
||||||
|
setQueuePosition(data.position || Math.floor(Math.random() * 500) + 430)
|
||||||
setIsSubmitting(false)
|
setIsSubmitting(false)
|
||||||
setIsSuccess(true)
|
setIsSuccess(true)
|
||||||
triggerConfetti()
|
triggerConfetti()
|
||||||
|
} else {
|
||||||
|
setIsSubmitting(false)
|
||||||
|
setError(data.message || 'Something went wrong. Please try again.')
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Waitlist signup error:', err)
|
||||||
|
setIsSubmitting(false)
|
||||||
|
setError('Connection error. Please try again.')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isSuccess) {
|
if (isSuccess) {
|
||||||
|
|
@ -193,8 +211,7 @@ export function WaitlistForm() {
|
||||||
}}
|
}}
|
||||||
placeholder="Enter your email"
|
placeholder="Enter your email"
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
className={`w-full h-14 rounded-full px-6 text-base border-2 transition-all outline-none ${
|
className={`w-full h-14 rounded-full px-6 text-base border-2 transition-all outline-none ${error
|
||||||
error
|
|
||||||
? 'border-red-500 bg-red-50 focus:border-red-500 focus:ring-4 focus:ring-red-500/20'
|
? 'border-red-500 bg-red-50 focus:border-red-500 focus:ring-4 focus:ring-red-500/20'
|
||||||
: 'border-border bg-background focus:border-[hsl(var(--primary))] focus:ring-4 focus:ring-[hsl(var(--primary))]/20'
|
: 'border-border bg-background focus:border-[hsl(var(--primary))] focus:ring-4 focus:ring-[hsl(var(--primary))]/20'
|
||||||
} disabled:opacity-50 disabled:cursor-not-allowed`}
|
} disabled:opacity-50 disabled:cursor-not-allowed`}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,70 @@
|
||||||
|
'use client'
|
||||||
|
|
||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
import { Moon, Sun } from 'lucide-react'
|
||||||
|
import { motion } from 'framer-motion'
|
||||||
|
|
||||||
|
export function ThemeToggle() {
|
||||||
|
const [theme, setTheme] = useState<'light' | 'dark'>('light')
|
||||||
|
const [mounted, setMounted] = useState(false)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setMounted(true)
|
||||||
|
// Check for stored preference or system preference
|
||||||
|
const stored = localStorage.getItem('theme')
|
||||||
|
if (stored === 'dark' || stored === 'light') {
|
||||||
|
setTheme(stored)
|
||||||
|
} else if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
|
||||||
|
setTheme('dark')
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!mounted) return
|
||||||
|
|
||||||
|
const root = document.documentElement
|
||||||
|
if (theme === 'dark') {
|
||||||
|
root.classList.add('dark')
|
||||||
|
} else {
|
||||||
|
root.classList.remove('dark')
|
||||||
|
}
|
||||||
|
localStorage.setItem('theme', theme)
|
||||||
|
}, [theme, mounted])
|
||||||
|
|
||||||
|
const toggleTheme = () => {
|
||||||
|
setTheme(prev => prev === 'light' ? 'dark' : 'light')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prevent hydration mismatch
|
||||||
|
if (!mounted) {
|
||||||
|
return (
|
||||||
|
<button className="p-2 rounded-lg bg-secondary/50 text-muted-foreground">
|
||||||
|
<Sun className="h-5 w-5" />
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<motion.button
|
||||||
|
onClick={toggleTheme}
|
||||||
|
whileHover={{ scale: 1.05 }}
|
||||||
|
whileTap={{ scale: 0.95 }}
|
||||||
|
className="relative p-2 rounded-lg bg-secondary/50 hover:bg-secondary text-muted-foreground hover:text-foreground transition-colors"
|
||||||
|
aria-label={`Switch to ${theme === 'light' ? 'dark' : 'light'} mode`}
|
||||||
|
>
|
||||||
|
<motion.div
|
||||||
|
initial={false}
|
||||||
|
animate={{ rotate: theme === 'dark' ? 180 : 0 }}
|
||||||
|
transition={{ duration: 0.3, ease: 'easeInOut' }}
|
||||||
|
>
|
||||||
|
{theme === 'light' ? (
|
||||||
|
<Moon className="h-5 w-5" />
|
||||||
|
) : (
|
||||||
|
<Sun className="h-5 w-5" />
|
||||||
|
)}
|
||||||
|
</motion.div>
|
||||||
|
</motion.button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ThemeToggle
|
||||||
|
|
@ -768,7 +768,6 @@
|
||||||
"integrity": "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==",
|
"integrity": "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/prop-types": "*",
|
"@types/prop-types": "*",
|
||||||
"csstype": "^3.2.2"
|
"csstype": "^3.2.2"
|
||||||
|
|
@ -1200,7 +1199,6 @@
|
||||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"acorn": "bin/acorn"
|
"acorn": "bin/acorn"
|
||||||
},
|
},
|
||||||
|
|
@ -1672,7 +1670,6 @@
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"baseline-browser-mapping": "^2.9.0",
|
"baseline-browser-mapping": "^2.9.0",
|
||||||
"caniuse-lite": "^1.0.30001759",
|
"caniuse-lite": "^1.0.30001759",
|
||||||
|
|
@ -2407,7 +2404,6 @@
|
||||||
"deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.",
|
"deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint-community/eslint-utils": "^4.2.0",
|
"@eslint-community/eslint-utils": "^4.2.0",
|
||||||
"@eslint-community/regexpp": "^4.6.1",
|
"@eslint-community/regexpp": "^4.6.1",
|
||||||
|
|
@ -2576,7 +2572,6 @@
|
||||||
"integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
|
"integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@rtsao/scc": "^1.1.0",
|
"@rtsao/scc": "^1.1.0",
|
||||||
"array-includes": "^3.1.9",
|
"array-includes": "^3.1.9",
|
||||||
|
|
@ -3963,7 +3958,6 @@
|
||||||
"integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
|
"integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"jiti": "bin/jiti.js"
|
"jiti": "bin/jiti.js"
|
||||||
}
|
}
|
||||||
|
|
@ -4766,7 +4760,6 @@
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"nanoid": "^3.3.11",
|
"nanoid": "^3.3.11",
|
||||||
"picocolors": "^1.1.1",
|
"picocolors": "^1.1.1",
|
||||||
|
|
@ -4930,7 +4923,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
|
||||||
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
|
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"loose-envify": "^1.1.0"
|
"loose-envify": "^1.1.0"
|
||||||
},
|
},
|
||||||
|
|
@ -4963,7 +4955,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
|
||||||
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
|
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"loose-envify": "^1.1.0",
|
"loose-envify": "^1.1.0",
|
||||||
"scheduler": "^0.23.2"
|
"scheduler": "^0.23.2"
|
||||||
|
|
@ -5745,6 +5736,24 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/tailwindcss/node_modules/yaml": {
|
||||||
|
"version": "2.8.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz",
|
||||||
|
"integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC",
|
||||||
|
"optional": true,
|
||||||
|
"peer": true,
|
||||||
|
"bin": {
|
||||||
|
"yaml": "bin.mjs"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 14.6"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/eemeli"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/text-table": {
|
"node_modules/text-table": {
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
|
||||||
|
|
@ -5816,7 +5825,6 @@
|
||||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
},
|
},
|
||||||
|
|
@ -5986,7 +5994,6 @@
|
||||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"tsc": "bin/tsc",
|
"tsc": "bin/tsc",
|
||||||
"tsserver": "bin/tsserver"
|
"tsserver": "bin/tsserver"
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ const config: Config = {
|
||||||
'./components/**/*.{js,ts,jsx,tsx,mdx}',
|
'./components/**/*.{js,ts,jsx,tsx,mdx}',
|
||||||
'./app/**/*.{js,ts,jsx,tsx,mdx}',
|
'./app/**/*.{js,ts,jsx,tsx,mdx}',
|
||||||
],
|
],
|
||||||
|
darkMode: 'class',
|
||||||
theme: {
|
theme: {
|
||||||
extend: {
|
extend: {
|
||||||
fontFamily: {
|
fontFamily: {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue