Files
cannaiq/cannaiq/src/pages/Home.tsx
Kelly 5415cac2f3 feat(seo): Add SEO tables to migration and ingress config
- Add seo_pages and seo_page_contents tables to migrate.ts for
  automatic creation on deployment
- Update Home.tsx with minor formatting
- Add ingress configuration updates

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-08 12:58:38 -07:00

530 lines
22 KiB
TypeScript

import { useState, useEffect } from 'react';
import { Link } from 'react-router-dom';
import {
TrendingUp,
BarChart3,
Package,
MapPin,
DollarSign,
Store,
Globe,
Bell,
ArrowRight,
Check,
Zap,
Database,
Code,
Building2,
ShoppingBag,
LineChart
} from 'lucide-react';
const API_URL = import.meta.env.VITE_API_URL || '';
const PLUGIN_DOWNLOAD_URL = `${API_URL}/downloads/cannaiq-menus-1.5.3.zip`;
import { api } from '../lib/api';
interface VersionInfo {
build_version: string;
git_sha: string;
build_time: string;
image_tag: string;
}
// Reusable Logo component matching login page
function Logo({ size = 'md' }: { size?: 'sm' | 'md' | 'lg' }) {
const sizes = {
sm: { box: 'w-8 h-8', icon: 'w-5 h-5', text: 'text-lg' },
md: { box: 'w-10 h-10', icon: 'w-6 h-6', text: 'text-2xl' },
lg: { box: 'w-12 h-12', icon: 'w-8 h-8', text: 'text-3xl' }
};
const s = sizes[size];
return (
<div className="flex items-center gap-3">
<div className={`${s.box} bg-white rounded-lg flex items-center justify-center`}>
<svg viewBox="0 0 24 24" className={`${s.icon} text-emerald-600`} fill="currentColor">
<path d="M12 2C8.5 2 5.5 3.5 3.5 6L12 12L20.5 6C18.5 3.5 15.5 2 12 2Z" />
<path d="M3.5 6C2 8 1 10.5 1 13C1 18.5 6 22 12 22C18 22 23 18.5 23 13C23 10.5 22 8 20.5 6L12 12L3.5 6Z" opacity="0.7" />
</svg>
</div>
<span className={`text-white ${s.text} font-bold`}>Canna IQ</span>
</div>
);
}
// Feature card matching login page style
function FeatureCard({ icon, title, description }: { icon: React.ReactNode; title: string; description: string }) {
return (
<div className="bg-white rounded-xl shadow-lg p-6 hover:shadow-xl transition-shadow">
<div className="w-12 h-12 bg-emerald-100 rounded-xl flex items-center justify-center mb-4">
{icon}
</div>
<h3 className="text-lg font-semibold text-gray-900 mb-2">{title}</h3>
<p className="text-gray-600 text-sm">{description}</p>
</div>
);
}
// Stat card for coverage section
function StatCard({ value, label }: { value: string; label: string }) {
return (
<div className="bg-white/10 backdrop-blur-sm rounded-xl p-6 text-center">
<div className="text-3xl font-bold text-white mb-1">{value}</div>
<div className="text-white/70 text-sm">{label}</div>
</div>
);
}
// Badge pill component
function Badge({ children }: { children: React.ReactNode }) {
return (
<span className="inline-flex items-center gap-1.5 px-3 py-1.5 bg-white/10 backdrop-blur-sm rounded-full text-white/90 text-sm">
<Check className="w-3.5 h-3.5 text-emerald-300" />
{children}
</span>
);
}
// Use case card
function UseCaseCard({ icon, title, bullets }: { icon: React.ReactNode; title: string; bullets: string[] }) {
return (
<div className="bg-white rounded-xl shadow-lg p-6 flex-1">
<div className="flex items-center gap-3 mb-4">
<div className="w-10 h-10 bg-emerald-100 rounded-lg flex items-center justify-center">
{icon}
</div>
<h3 className="text-lg font-semibold text-gray-900">{title}</h3>
</div>
<ul className="space-y-3">
{bullets.map((bullet, i) => (
<li key={i} className="flex items-start gap-2 text-gray-600 text-sm">
<Check className="w-4 h-4 text-emerald-500 mt-0.5 flex-shrink-0" />
{bullet}
</li>
))}
</ul>
</div>
);
}
// Step component for "How it works"
function Step({ number, title, description }: { number: number; title: string; description: string }) {
return (
<div className="flex-1 text-center">
<div className="w-10 h-10 bg-emerald-600 rounded-full flex items-center justify-center text-white font-bold mx-auto mb-3">
{number}
</div>
<h4 className="font-semibold text-gray-900 mb-1">{title}</h4>
<p className="text-gray-600 text-sm">{description}</p>
</div>
);
}
// Mock dashboard preview card
function DashboardPreview() {
return (
<div className="bg-white/10 backdrop-blur-sm rounded-2xl p-4 shadow-2xl">
{/* Header bar */}
<div className="flex items-center gap-2 mb-4">
<div className="w-3 h-3 rounded-full bg-red-400" />
<div className="w-3 h-3 rounded-full bg-yellow-400" />
<div className="w-3 h-3 rounded-full bg-green-400" />
<div className="flex-1 bg-white/10 rounded h-4 ml-2" />
</div>
{/* Stats row */}
<div className="grid grid-cols-3 gap-2 mb-4">
<div className="bg-white/10 rounded-lg p-3">
<div className="text-xs text-white/60 mb-1">Products</div>
<div className="text-lg font-bold text-white">47,832</div>
</div>
<div className="bg-white/10 rounded-lg p-3">
<div className="text-xs text-white/60 mb-1">Stores</div>
<div className="text-lg font-bold text-white">1,248</div>
</div>
<div className="bg-white/10 rounded-lg p-3">
<div className="text-xs text-white/60 mb-1">Brands</div>
<div className="text-lg font-bold text-white">892</div>
</div>
</div>
{/* Mini chart placeholder */}
<div className="bg-white/10 rounded-lg p-3 mb-4">
<div className="text-xs text-white/60 mb-2">Price Trends</div>
<div className="flex items-end gap-1 h-12">
{[40, 55, 45, 60, 50, 70, 65, 75, 68, 80, 72, 85].map((h, i) => (
<div
key={i}
className="flex-1 bg-emerald-400/60 rounded-t"
style={{ height: `${h}%` }}
/>
))}
</div>
</div>
{/* Map placeholder */}
<div className="bg-white/10 rounded-lg p-3">
<div className="text-xs text-white/60 mb-2">Coverage Map</div>
<div className="grid grid-cols-5 gap-1">
{['WA', 'OR', 'CA', 'NV', 'AZ', 'CO', 'MI', 'IL', 'MA', 'NY'].map((state) => (
<div key={state} className="bg-emerald-500/40 rounded text-center py-1 text-xs text-white/80">
{state}
</div>
))}
</div>
</div>
</div>
);
}
export function Home() {
const [versionInfo, setVersionInfo] = useState<VersionInfo | null>(null);
useEffect(() => {
const fetchVersion = async () => {
try {
const data = await api.getVersion();
setVersionInfo(data);
} catch (error) {
console.error('Failed to fetch version info:', error);
}
};
fetchVersion();
}, []);
return (
<div className="min-h-screen bg-gray-50">
{/* Navigation */}
<nav className="bg-gradient-to-r from-emerald-600 via-emerald-700 to-teal-800">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex items-center justify-between h-16">
<Logo size="sm" />
<div className="flex items-center gap-4">
<Link
to="/login"
className="text-white/90 hover:text-white text-sm font-medium transition-colors"
>
Sign in
</Link>
<a
href="mailto:hello@cannaiq.co"
className="bg-white text-emerald-700 px-4 py-2 rounded-lg text-sm font-semibold hover:bg-gray-100 transition-colors"
>
Request a demo
</a>
</div>
</div>
</div>
</nav>
{/* Hero Section */}
<section className="relative bg-gradient-to-br from-emerald-600 via-emerald-700 to-teal-800 overflow-hidden">
{/* Decorative circles matching login page */}
<div className="absolute top-[-100px] right-[-100px] w-[400px] h-[400px] bg-white/5 rounded-full" />
<div className="absolute bottom-[-150px] left-[-100px] w-[500px] h-[500px] bg-white/5 rounded-full" />
<div className="absolute top-[50%] right-[20%] w-[300px] h-[300px] bg-white/5 rounded-full" />
<div className="relative z-10 max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-16 lg:py-24">
<div className="grid lg:grid-cols-2 gap-12 items-center">
{/* Left: Text content */}
<div>
<h1 className="text-4xl lg:text-5xl font-bold text-white leading-tight mb-6">
Real-time cannabis intelligence for the U.S. and Canada.
</h1>
<p className="text-lg text-white/80 mb-8 max-w-xl">
Track products, pricing, availability, and competitor movement across every legal marketpowered by live dispensary menus.
</p>
{/* CTAs */}
<div className="flex flex-wrap gap-4 mb-8">
<Link
to="/login"
className="bg-white text-emerald-700 px-6 py-3 rounded-lg font-semibold hover:bg-gray-100 transition-colors shadow-lg flex items-center gap-2"
>
Log in to CannaiQ
<ArrowRight className="w-4 h-4" />
</Link>
<a
href="mailto:hello@cannaiq.co"
className="border-2 border-white text-white px-6 py-3 rounded-lg font-semibold hover:bg-white hover:text-emerald-700 transition-colors"
>
Request a demo
</a>
</div>
{/* Badges */}
<div className="flex flex-wrap gap-3">
<Badge>U.S. + Canada coverage</Badge>
<Badge>Live menu data</Badge>
<Badge>Brand & retailer views</Badge>
<Badge>Price and promo tracking</Badge>
</div>
</div>
{/* Right: Dashboard preview */}
<div className="hidden lg:block">
<DashboardPreview />
</div>
</div>
</div>
</section>
{/* Feature Grid Section */}
<section className="py-16 lg:py-24 px-4 sm:px-6 lg:px-8">
<div className="max-w-7xl mx-auto">
<div className="text-center mb-12">
<h2 className="text-3xl font-bold text-gray-900 mb-4">What CannaiQ gives you</h2>
<p className="text-gray-600 max-w-2xl mx-auto">
Everything you need to understand and act on cannabis market dynamics.
</p>
</div>
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
<FeatureCard
icon={<Package className="w-6 h-6 text-emerald-600" />}
title="Live product tracking"
description="See what's on the shelf right now—products, SKUs, and stock status pulled from live menus."
/>
<FeatureCard
icon={<DollarSign className="w-6 h-6 text-emerald-600" />}
title="Price & promo monitoring"
description="Compare pricing, discounts, and promos across markets to see who's racing to the bottom and where you're leaving margin on the table."
/>
<FeatureCard
icon={<TrendingUp className="w-6 h-6 text-emerald-600" />}
title="Brand penetration & share"
description="Track where each brand is listed, how deep their presence goes in each store, and how your portfolio compares."
/>
<FeatureCard
icon={<Store className="w-6 h-6 text-emerald-600" />}
title="Store-level intelligence"
description="Drill into individual dispensaries to see assortment, pricing, and who they're favoring in each category."
/>
<FeatureCard
icon={<Globe className="w-6 h-6 text-emerald-600" />}
title="Multi-state coverage (U.S. + Canada)"
description="Flip between U.S. and Canadian markets and compare how brands and categories perform across borders."
/>
<FeatureCard
icon={<Bell className="w-6 h-6 text-emerald-600" />}
title="Alerts & change detection"
description="Get notified when products appear, disappear, or change price in key stores. Coming soon."
/>
</div>
</div>
</section>
{/* Coverage & Scale Section */}
<section className="relative bg-gradient-to-br from-emerald-600 via-emerald-700 to-teal-800 py-16 lg:py-24 overflow-hidden">
<div className="absolute top-[-50px] left-[-50px] w-[300px] h-[300px] bg-white/5 rounded-full" />
<div className="absolute bottom-[-80px] right-[-80px] w-[400px] h-[400px] bg-white/5 rounded-full" />
<div className="relative z-10 max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="text-center mb-12">
<h2 className="text-3xl font-bold text-white mb-4">Built for North American cannabis markets</h2>
<p className="text-white/80 max-w-2xl mx-auto">
CannaiQ continuously monitors licensed cannabis menus across the U.S. and Canada, normalizing brand and product data so you can compare markets, categories, and competitors in one place.
</p>
</div>
<div className="grid sm:grid-cols-2 lg:grid-cols-4 gap-4">
<StatCard value="2 countries" label="U.S. & Canada tracked" />
<StatCard value="Hundreds" label="Live dispensary menus" />
<StatCard value="Tens of thousands" label="Normalized SKUs" />
<StatCard value="Daily updates" label="Fresh crawls & snapshots" />
</div>
</div>
</section>
{/* Use Cases Section */}
<section className="py-16 lg:py-24 px-4 sm:px-6 lg:px-8">
<div className="max-w-7xl mx-auto">
<div className="text-center mb-12">
<h2 className="text-3xl font-bold text-gray-900 mb-4">Built for brands and retailers</h2>
<p className="text-gray-600 max-w-2xl mx-auto">
Whether you're placing products or stocking shelves, CannaiQ gives you the visibility you need.
</p>
</div>
<div className="grid md:grid-cols-2 gap-6">
<UseCaseCard
icon={<Building2 className="w-5 h-5 text-emerald-600" />}
title="For Brands"
bullets={[
"See where your SKUs are listed—and where they're missing.",
"Compare your pricing and promos to competing brands.",
"Find whitespace in categories, formats, and price points."
]}
/>
<UseCaseCard
icon={<ShoppingBag className="w-5 h-5 text-emerald-600" />}
title="For Retailers"
bullets={[
"Benchmark your assortment and pricing against nearby stores.",
"Identify gaps in key categories and formats.",
"Track which brands are gaining or losing shelf space."
]}
/>
</div>
</div>
</section>
{/* How It Works Section */}
<section className="py-16 lg:py-24 px-4 sm:px-6 lg:px-8 bg-gray-100">
<div className="max-w-5xl mx-auto">
<div className="text-center mb-12">
<h2 className="text-3xl font-bold text-gray-900 mb-4">How CannaiQ works</h2>
</div>
<div className="grid sm:grid-cols-2 lg:grid-cols-4 gap-8">
<Step
number={1}
title="We crawl live menus"
description="Automated workers pull product, price, and availability data from dispensary menus."
/>
<Step
number={2}
title="We normalize brands & SKUs"
description="Products are mapped and cleaned so you can compare across stores, states, and platforms."
/>
<Step
number={3}
title="We surface intelligence"
description="Dashboards and APIs highlight trends, penetration, and competitive movement."
/>
<Step
number={4}
title="You act faster"
description="Make confident decisions on pricing, promos, and distribution."
/>
</div>
</div>
</section>
{/* Integration Section */}
<section className="py-16 lg:py-24 px-4 sm:px-6 lg:px-8">
<div className="max-w-4xl mx-auto">
<div className="text-center mb-8">
<h2 className="text-3xl font-bold text-gray-900 mb-4">Data where you need it</h2>
<p className="text-gray-600">
Use CannaiQ in the browser, or plug it into your own tools via API and WordPress.
</p>
</div>
<div className="bg-white rounded-2xl shadow-lg p-8">
<div className="grid md:grid-cols-2 gap-8">
<div>
<h3 className="font-semibold text-gray-900 mb-4 flex items-center gap-2">
<Database className="w-5 h-5 text-emerald-600" />
Available integrations
</h3>
<ul className="space-y-3">
<li className="flex items-start gap-2 text-gray-600">
<Check className="w-4 h-4 text-emerald-500 mt-0.5" />
Admin dashboard at <code className="bg-gray-100 px-1.5 py-0.5 rounded text-sm">/admin</code> for deep drill-downs
</li>
<li className="flex items-start gap-2 text-gray-600">
<Check className="w-4 h-4 text-emerald-500 mt-0.5" />
WordPress plugin for displaying menus on your site
</li>
<li className="flex items-start gap-2 text-gray-600">
<Check className="w-4 h-4 text-emerald-500 mt-0.5" />
API-ready architecture for custom integrations
</li>
</ul>
</div>
<div className="flex flex-col gap-4">
<Link
to="/admin"
className="flex items-center justify-center gap-2 bg-emerald-600 hover:bg-emerald-700 text-white font-semibold py-3 px-6 rounded-lg transition-colors"
>
<BarChart3 className="w-5 h-5" />
Go to /admin
</Link>
<a
href={PLUGIN_DOWNLOAD_URL}
className="flex items-center justify-center gap-2 border-2 border-emerald-600 text-emerald-700 font-semibold py-3 px-6 rounded-lg hover:bg-emerald-50 transition-colors"
download
>
<Code className="w-5 h-5" />
Download WordPress Plugin
</a>
</div>
</div>
</div>
</div>
</section>
{/* Final CTA Section */}
<section className="bg-gradient-to-r from-emerald-600 via-emerald-700 to-teal-800 py-16 lg:py-20">
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
<h2 className="text-3xl font-bold text-white mb-4">Ready to see your market clearly?</h2>
<p className="text-white/80 mb-8 max-w-xl mx-auto">
Log in if you're already onboarded, or request access to start exploring live data.
</p>
<div className="flex flex-wrap justify-center gap-4">
<Link
to="/login"
className="bg-white text-emerald-700 px-8 py-3 rounded-lg font-semibold hover:bg-gray-100 transition-colors shadow-lg flex items-center gap-2"
>
Log in to CannaiQ
<ArrowRight className="w-4 h-4" />
</Link>
<a
href="mailto:hello@cannaiq.co"
className="border-2 border-white text-white px-8 py-3 rounded-lg font-semibold hover:bg-white hover:text-emerald-700 transition-colors"
>
Request a demo
</a>
</div>
</div>
</section>
{/* Footer */}
<footer className="bg-gray-900 py-12 px-4 sm:px-6 lg:px-8">
<div className="max-w-7xl mx-auto">
<div className="flex flex-col md:flex-row items-center justify-between gap-6">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-emerald-600 rounded-lg flex items-center justify-center">
<svg viewBox="0 0 24 24" className="w-6 h-6 text-white" fill="currentColor">
<path d="M12 2C8.5 2 5.5 3.5 3.5 6L12 12L20.5 6C18.5 3.5 15.5 2 12 2Z" />
<path d="M3.5 6C2 8 1 10.5 1 13C1 18.5 6 22 12 22C18 22 23 18.5 23 13C23 10.5 22 8 20.5 6L12 12L3.5 6Z" opacity="0.7" />
</svg>
</div>
<div>
<div className="text-white font-bold text-lg">Canna IQ</div>
<div className="text-gray-400 text-sm">Cannabis Market Intelligence</div>
</div>
</div>
<div className="flex items-center gap-6 text-gray-400 text-sm">
<Link to="/login" className="hover:text-white transition-colors">Sign in</Link>
<a href="mailto:hello@cannaiq.co" className="hover:text-white transition-colors">Contact</a>
<a href={PLUGIN_DOWNLOAD_URL} className="hover:text-white transition-colors" download>WordPress Plugin</a>
</div>
</div>
<div className="mt-8 pt-8 border-t border-gray-800 flex flex-col sm:flex-row items-center justify-between gap-4">
<p className="text-gray-500 text-sm">
&copy; {new Date().getFullYear()} CannaiQ. All rights reserved.
</p>
{versionInfo && (
<div className="text-right">
<p className="text-xs text-gray-500">
{versionInfo.build_version} ({versionInfo.git_sha.slice(0, 7)})
</p>
<p className="text-xs text-gray-600">
{versionInfo.image_tag}
</p>
</div>
)}
</div>
</div>
</footer>
</div>
);
}
export default Home;