Feat: Add public landing page for non-authenticated users
- Create /home route with SEO metadata and JSON-LD structured data - Add hero section with dual CTAs (sign-up and PWA install) - Add features section showcasing dual tracking, health timeline, achievements, savings - Add animated phone demo with rotating screens (auto-advance, pause on hover) - Add final CTA section and footer with affiliate disclosure - Extract usePWAInstall hook for reusable PWA install logic - Enhance theme-context with system preference auto-detection Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
d3314c5590
commit
890bdf13e4
65
src/app/home/page.tsx
Normal file
65
src/app/home/page.tsx
Normal file
@ -0,0 +1,65 @@
|
||||
import { Metadata } from 'next';
|
||||
import { LandingPage } from '@/components/landing/LandingPage';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'QuitTraq - Track Your Journey to Quit Smoking & Marijuana',
|
||||
description: 'Free app to track nicotine and marijuana usage. Health recovery timeline, achievements, savings tracker. PWA works offline. Your companion to a healthier life.',
|
||||
keywords: ['quit smoking', 'stop smoking app', 'nicotine tracker', 'marijuana tracker', 'health recovery', 'quit vaping', 'substance tracker'],
|
||||
authors: [{ name: 'QuitTraq' }],
|
||||
openGraph: {
|
||||
type: 'website',
|
||||
locale: 'en_US',
|
||||
url: 'https://quittraq.com/home',
|
||||
title: 'QuitTraq - Track Your Journey to Quit Smoking',
|
||||
description: 'Free app to track nicotine and marijuana usage with health timeline and achievements.',
|
||||
siteName: 'QuitTraq',
|
||||
images: [
|
||||
{
|
||||
url: '/og-image.png',
|
||||
width: 1200,
|
||||
height: 630,
|
||||
alt: 'QuitTraq - Quit Smoking App',
|
||||
},
|
||||
],
|
||||
},
|
||||
twitter: {
|
||||
card: 'summary_large_image',
|
||||
title: 'QuitTraq - Track Your Journey to Quit Smoking',
|
||||
description: 'Free app to track nicotine and marijuana usage with health timeline and achievements.',
|
||||
images: ['/og-image.png'],
|
||||
},
|
||||
robots: {
|
||||
index: true,
|
||||
follow: true,
|
||||
googleBot: {
|
||||
index: true,
|
||||
follow: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default function HomePage() {
|
||||
const jsonLd = {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'WebApplication',
|
||||
name: 'QuitTraq',
|
||||
description: 'Track your journey to quit smoking and marijuana',
|
||||
applicationCategory: 'HealthApplication',
|
||||
operatingSystem: 'Any',
|
||||
offers: {
|
||||
'@type': 'Offer',
|
||||
price: '0',
|
||||
priceCurrency: 'USD',
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<script
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
|
||||
/>
|
||||
<LandingPage />
|
||||
</>
|
||||
);
|
||||
}
|
||||
224
src/components/landing/CTASection.tsx
Normal file
224
src/components/landing/CTASection.tsx
Normal file
@ -0,0 +1,224 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import { ArrowRight, Smartphone, Shield, CreditCard, Wifi, Download, Share, Plus, MoreVertical } from 'lucide-react';
|
||||
import { type PWAInstallState } from '@/hooks/usePWAInstall';
|
||||
|
||||
interface CTASectionProps {
|
||||
pwaInstall: PWAInstallState;
|
||||
}
|
||||
|
||||
export function CTASection({ pwaInstall }: CTASectionProps) {
|
||||
const [showInstructions, setShowInstructions] = useState(false);
|
||||
|
||||
const handleInstallClick = async () => {
|
||||
if (pwaInstall.isInstallable) {
|
||||
await pwaInstall.promptInstall();
|
||||
} else {
|
||||
setShowInstructions(true);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<section
|
||||
id="cta"
|
||||
aria-labelledby="cta-heading"
|
||||
className="py-20 px-4 relative overflow-hidden"
|
||||
>
|
||||
{/* Background gradient */}
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-primary/10 via-purple-500/5 to-pink-500/10 pointer-events-none" />
|
||||
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[800px] h-[800px] bg-primary/10 rounded-full blur-[150px] pointer-events-none" />
|
||||
|
||||
<div className="container mx-auto max-w-4xl relative z-10 text-center">
|
||||
{/* Headline */}
|
||||
<h2
|
||||
id="cta-heading"
|
||||
className="text-3xl sm:text-4xl lg:text-5xl font-black tracking-tight mb-6"
|
||||
>
|
||||
Ready to Start Your{' '}
|
||||
<span className="bg-gradient-to-r from-primary via-purple-500 to-pink-500 bg-clip-text text-transparent">
|
||||
Journey?
|
||||
</span>
|
||||
</h2>
|
||||
|
||||
{/* Subheadline */}
|
||||
<p className="text-lg sm:text-xl text-muted-foreground max-w-2xl mx-auto mb-10">
|
||||
Join thousands of people taking control of their health. Your first step starts here.
|
||||
</p>
|
||||
|
||||
{/* Dual CTAs */}
|
||||
<div className="flex flex-col sm:flex-row gap-4 justify-center items-center mb-12">
|
||||
<Button
|
||||
asChild
|
||||
size="lg"
|
||||
className="h-14 px-8 text-lg rounded-xl bg-gradient-to-r from-primary to-purple-600 hover:from-primary/90 hover:to-purple-600/90 hover:scale-105 transition-all shadow-xl shadow-primary/25 hover-glow"
|
||||
>
|
||||
<Link href="/login">
|
||||
Get Started Free
|
||||
<ArrowRight className="ml-2 h-5 w-5" />
|
||||
</Link>
|
||||
</Button>
|
||||
|
||||
{!pwaInstall.isStandalone && (
|
||||
<Button
|
||||
size="lg"
|
||||
variant="outline"
|
||||
onClick={handleInstallClick}
|
||||
aria-label="Install QuitTraq as an app on your device"
|
||||
className="h-14 px-8 text-lg rounded-xl border-purple-500/30 hover:bg-purple-500/10 hover:scale-105 transition-all"
|
||||
>
|
||||
<Smartphone className="mr-2 h-5 w-5" />
|
||||
Install App
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Trust signals */}
|
||||
<div className="flex flex-wrap justify-center gap-6">
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<CreditCard className="h-4 w-4 text-primary" />
|
||||
<span>No Credit Card Required</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<Shield className="h-4 w-4 text-primary" />
|
||||
<span>Privacy Focused</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<Wifi className="h-4 w-4 text-primary" />
|
||||
<span>Works Offline</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* PWA Install Instructions Dialog */}
|
||||
<Dialog open={showInstructions} onOpenChange={setShowInstructions}>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<Download className="h-5 w-5 text-purple-400" />
|
||||
Add QuitTraq to Home Screen
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
Get quick access to track your progress right from your phone's home screen.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-6 py-4">
|
||||
{pwaInstall.isIOS ? (
|
||||
<div className="space-y-4">
|
||||
<p className="text-sm font-medium text-purple-400">For iPhone / iPad (Safari):</p>
|
||||
<ol className="space-y-4 text-sm">
|
||||
<li className="flex items-start gap-3">
|
||||
<span className="flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-purple-500/20 text-purple-400 text-xs font-bold">
|
||||
1
|
||||
</span>
|
||||
<div>
|
||||
<p>
|
||||
Tap the <strong>Share</strong> button
|
||||
</p>
|
||||
<div className="mt-1 inline-flex items-center gap-1 rounded bg-muted px-2 py-1">
|
||||
<Share className="h-4 w-4" />
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<li className="flex items-start gap-3">
|
||||
<span className="flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-purple-500/20 text-purple-400 text-xs font-bold">
|
||||
2
|
||||
</span>
|
||||
<div>
|
||||
<p>
|
||||
Scroll down and tap <strong>"Add to Home Screen"</strong>
|
||||
</p>
|
||||
<div className="mt-1 inline-flex items-center gap-1 rounded bg-muted px-2 py-1">
|
||||
<Plus className="h-4 w-4" /> Add to Home Screen
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<li className="flex items-start gap-3">
|
||||
<span className="flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-purple-500/20 text-purple-400 text-xs font-bold">
|
||||
3
|
||||
</span>
|
||||
<p>
|
||||
Tap <strong>"Add"</strong> to confirm
|
||||
</p>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
) : pwaInstall.isAndroid ? (
|
||||
<div className="space-y-4">
|
||||
<p className="text-sm font-medium text-purple-400">For Android (Chrome):</p>
|
||||
<ol className="space-y-4 text-sm">
|
||||
<li className="flex items-start gap-3">
|
||||
<span className="flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-purple-500/20 text-purple-400 text-xs font-bold">
|
||||
1
|
||||
</span>
|
||||
<div>
|
||||
<p>
|
||||
Tap the <strong>menu</strong> button (3 dots)
|
||||
</p>
|
||||
<div className="mt-1 inline-flex items-center gap-1 rounded bg-muted px-2 py-1">
|
||||
<MoreVertical className="h-4 w-4" />
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<li className="flex items-start gap-3">
|
||||
<span className="flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-purple-500/20 text-purple-400 text-xs font-bold">
|
||||
2
|
||||
</span>
|
||||
<p>
|
||||
Tap <strong>"Add to Home screen"</strong> or{' '}
|
||||
<strong>"Install app"</strong>
|
||||
</p>
|
||||
</li>
|
||||
<li className="flex items-start gap-3">
|
||||
<span className="flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-purple-500/20 text-purple-400 text-xs font-bold">
|
||||
3
|
||||
</span>
|
||||
<p>
|
||||
Tap <strong>"Add"</strong> or <strong>"Install"</strong> to
|
||||
confirm
|
||||
</p>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
<p className="text-sm font-medium text-purple-400">On mobile device:</p>
|
||||
<ul className="space-y-2 text-sm text-muted-foreground">
|
||||
<li>
|
||||
<strong>iPhone/iPad:</strong> Use Safari, tap Share → Add to Home Screen
|
||||
</li>
|
||||
<li>
|
||||
<strong>Android:</strong> Use Chrome, tap Menu → Add to Home screen
|
||||
</li>
|
||||
</ul>
|
||||
<p className="text-xs text-muted-foreground mt-4">
|
||||
Open this page on your phone to add QuitTraq to your home screen for quick access!
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="pt-2 border-t border-white/10">
|
||||
<p className="text-xs text-muted-foreground text-center">
|
||||
Once added, tap the QuitTraq icon to quickly log your usage!
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button onClick={() => setShowInstructions(false)} className="w-full">
|
||||
Got it!
|
||||
</Button>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
267
src/components/landing/DemoSection.tsx
Normal file
267
src/components/landing/DemoSection.tsx
Normal file
@ -0,0 +1,267 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Cigarette, Leaf, Heart, Trophy, DollarSign, TrendingDown, CheckCircle } from 'lucide-react';
|
||||
|
||||
const DEMO_SCREENS = [
|
||||
{
|
||||
id: 'logging',
|
||||
title: 'Log Your Usage',
|
||||
subtitle: 'Track daily consumption with ease',
|
||||
content: (
|
||||
<div className="space-y-4">
|
||||
<div className="flex justify-between items-center p-3 rounded-xl bg-white/5 border border-white/10">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-8 h-8 rounded-lg bg-red-500/20 flex items-center justify-center">
|
||||
<Cigarette className="h-4 w-4 text-red-400" />
|
||||
</div>
|
||||
<span className="font-medium">Nicotine</span>
|
||||
</div>
|
||||
<div className="text-2xl font-bold">3</div>
|
||||
</div>
|
||||
<div className="flex justify-between items-center p-3 rounded-xl bg-white/5 border border-white/10">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-8 h-8 rounded-lg bg-green-500/20 flex items-center justify-center">
|
||||
<Leaf className="h-4 w-4 text-green-400" />
|
||||
</div>
|
||||
<span className="font-medium">Marijuana</span>
|
||||
</div>
|
||||
<div className="text-2xl font-bold">0</div>
|
||||
</div>
|
||||
<div className="text-center text-xs text-muted-foreground mt-4">
|
||||
<TrendingDown className="h-4 w-4 inline mr-1 text-green-400" />
|
||||
Down 40% from last week
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'health',
|
||||
title: 'Health Recovery',
|
||||
subtitle: 'Watch your body heal',
|
||||
content: (
|
||||
<div className="space-y-3">
|
||||
<div className="space-y-1">
|
||||
<div className="flex justify-between text-xs">
|
||||
<span>Blood Pressure Normalizes</span>
|
||||
<CheckCircle className="h-3 w-3 text-green-400" />
|
||||
</div>
|
||||
<div className="h-2 rounded-full bg-white/10 overflow-hidden">
|
||||
<div className="h-full w-full bg-gradient-to-r from-teal-500 to-cyan-400 rounded-full" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<div className="flex justify-between text-xs">
|
||||
<span>Oxygen Levels Rise</span>
|
||||
<CheckCircle className="h-3 w-3 text-green-400" />
|
||||
</div>
|
||||
<div className="h-2 rounded-full bg-white/10 overflow-hidden">
|
||||
<div className="h-full w-full bg-gradient-to-r from-teal-500 to-cyan-400 rounded-full" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<div className="flex justify-between text-xs">
|
||||
<span>Circulation Improves</span>
|
||||
<span className="text-yellow-400">1 week to go</span>
|
||||
</div>
|
||||
<div className="h-2 rounded-full bg-white/10 overflow-hidden">
|
||||
<div className="h-full w-3/4 bg-gradient-to-r from-teal-500 to-cyan-400 rounded-full" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<div className="flex justify-between text-xs">
|
||||
<span>Heart Attack Risk Drops</span>
|
||||
<span className="text-muted-foreground">2 weeks to go</span>
|
||||
</div>
|
||||
<div className="h-2 rounded-full bg-white/10 overflow-hidden">
|
||||
<div className="h-full w-1/2 bg-gradient-to-r from-teal-500 to-cyan-400 rounded-full" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'achievements',
|
||||
title: 'Achievements',
|
||||
subtitle: 'Celebrate every milestone',
|
||||
content: (
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
{[
|
||||
{ name: 'First Step', unlocked: true },
|
||||
{ name: 'Hat Trick', unlocked: true },
|
||||
{ name: 'Week Warrior', unlocked: true },
|
||||
{ name: 'Fighter', unlocked: false },
|
||||
{ name: 'Monthly Master', unlocked: false },
|
||||
{ name: 'Goal Crusher', unlocked: false },
|
||||
].map((badge) => (
|
||||
<div
|
||||
key={badge.name}
|
||||
className={`aspect-square rounded-xl flex flex-col items-center justify-center p-2 text-center ${
|
||||
badge.unlocked
|
||||
? 'bg-gradient-to-br from-yellow-500/30 to-amber-600/20 border border-yellow-500/50'
|
||||
: 'bg-white/5 border border-white/10 opacity-50'
|
||||
}`}
|
||||
>
|
||||
<Trophy
|
||||
className={`h-5 w-5 mb-1 ${badge.unlocked ? 'text-yellow-400' : 'text-muted-foreground'}`}
|
||||
/>
|
||||
<span className="text-[10px] leading-tight">{badge.name}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'savings',
|
||||
title: 'Money Saved',
|
||||
subtitle: 'Track your financial wins',
|
||||
content: (
|
||||
<div className="text-center space-y-4">
|
||||
<div className="w-16 h-16 rounded-2xl bg-emerald-500/20 flex items-center justify-center mx-auto">
|
||||
<DollarSign className="h-8 w-8 text-emerald-400" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-4xl font-black text-emerald-400">$127.50</div>
|
||||
<div className="text-sm text-muted-foreground">saved this month</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-2 text-xs">
|
||||
<div className="p-2 rounded-lg bg-white/5 border border-white/10">
|
||||
<div className="font-bold">$4.25</div>
|
||||
<div className="text-muted-foreground">per day</div>
|
||||
</div>
|
||||
<div className="p-2 rounded-lg bg-white/5 border border-white/10">
|
||||
<div className="font-bold">$1,530</div>
|
||||
<div className="text-muted-foreground">per year</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
export function DemoSection() {
|
||||
const [activeScreen, setActiveScreen] = useState(0);
|
||||
const [isPaused, setIsPaused] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (isPaused) return;
|
||||
|
||||
const interval = setInterval(() => {
|
||||
setActiveScreen((prev) => (prev + 1) % DEMO_SCREENS.length);
|
||||
}, 4000);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [isPaused]);
|
||||
|
||||
return (
|
||||
<section
|
||||
id="demo"
|
||||
aria-labelledby="demo-heading"
|
||||
className="py-20 px-4 relative overflow-hidden"
|
||||
>
|
||||
{/* Background accents */}
|
||||
<div className="absolute top-1/2 left-0 w-96 h-96 bg-primary/10 rounded-full blur-[120px] -translate-y-1/2 pointer-events-none" />
|
||||
<div className="absolute top-1/2 right-0 w-96 h-96 bg-purple-500/10 rounded-full blur-[120px] -translate-y-1/2 pointer-events-none" />
|
||||
|
||||
<div className="container mx-auto max-w-6xl relative z-10">
|
||||
{/* Section Header */}
|
||||
<div className="text-center mb-12">
|
||||
<h2
|
||||
id="demo-heading"
|
||||
className="text-3xl sm:text-4xl font-bold mb-4"
|
||||
>
|
||||
See It In{' '}
|
||||
<span className="bg-gradient-to-r from-primary via-purple-500 to-pink-500 bg-clip-text text-transparent">
|
||||
Action
|
||||
</span>
|
||||
</h2>
|
||||
<p className="text-muted-foreground max-w-2xl mx-auto">
|
||||
A quick look at how QuitTraq helps you track progress and stay motivated.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col lg:flex-row items-center gap-12 lg:gap-16">
|
||||
{/* Phone Mockup */}
|
||||
<div
|
||||
className="relative mx-auto lg:mx-0"
|
||||
onMouseEnter={() => setIsPaused(true)}
|
||||
onMouseLeave={() => setIsPaused(false)}
|
||||
onTouchStart={() => setIsPaused(true)}
|
||||
onTouchEnd={() => setIsPaused(false)}
|
||||
>
|
||||
{/* Phone frame glow */}
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-primary/30 to-purple-500/30 rounded-[3rem] blur-2xl opacity-50" />
|
||||
|
||||
{/* Phone frame */}
|
||||
<div className="relative w-[280px] sm:w-[320px] aspect-[9/16] rounded-[2.5rem] overflow-hidden border-4 border-white/10 bg-background shadow-2xl shadow-black/30">
|
||||
{/* Notch */}
|
||||
<div className="absolute top-0 left-1/2 -translate-x-1/2 w-24 h-6 bg-black rounded-b-2xl z-20" />
|
||||
|
||||
{/* Screen content */}
|
||||
<div className="absolute inset-0 p-4 pt-10">
|
||||
{/* Screen header */}
|
||||
<div className="text-center mb-4">
|
||||
<h3 className="text-lg font-bold">{DEMO_SCREENS[activeScreen].title}</h3>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{DEMO_SCREENS[activeScreen].subtitle}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Screen content with transition */}
|
||||
<div className="animate-fade-in" key={activeScreen}>
|
||||
{DEMO_SCREENS[activeScreen].content}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Bottom gradient overlay */}
|
||||
<div className="absolute bottom-0 left-0 right-0 h-20 bg-gradient-to-t from-background to-transparent pointer-events-none" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Description and indicators */}
|
||||
<div className="flex-1 text-center lg:text-left">
|
||||
<div className="space-y-6">
|
||||
{/* Screen descriptions */}
|
||||
{DEMO_SCREENS.map((screen, index) => (
|
||||
<button
|
||||
key={screen.id}
|
||||
onClick={() => setActiveScreen(index)}
|
||||
className={`w-full text-left p-4 rounded-xl transition-all duration-300 ${
|
||||
activeScreen === index
|
||||
? 'bg-primary/10 border border-primary/30'
|
||||
: 'hover:bg-white/5 border border-transparent'
|
||||
}`}
|
||||
>
|
||||
<h4
|
||||
className={`font-semibold mb-1 ${
|
||||
activeScreen === index ? 'text-primary' : 'text-foreground'
|
||||
}`}
|
||||
>
|
||||
{screen.title}
|
||||
</h4>
|
||||
<p className="text-sm text-muted-foreground">{screen.subtitle}</p>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Dot indicators (mobile) */}
|
||||
<div className="flex justify-center gap-2 mt-6 lg:hidden">
|
||||
{DEMO_SCREENS.map((_, index) => (
|
||||
<button
|
||||
key={index}
|
||||
onClick={() => setActiveScreen(index)}
|
||||
className={`w-2 h-2 rounded-full transition-all ${
|
||||
activeScreen === index
|
||||
? 'w-6 bg-primary'
|
||||
: 'bg-white/20 hover:bg-white/40'
|
||||
}`}
|
||||
aria-label={`Go to screen ${index + 1}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
78
src/components/landing/FeatureCard.tsx
Normal file
78
src/components/landing/FeatureCard.tsx
Normal file
@ -0,0 +1,78 @@
|
||||
'use client';
|
||||
|
||||
import { useRef, useEffect, useState } from 'react';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { LucideIcon } from 'lucide-react';
|
||||
|
||||
interface FeatureIcon {
|
||||
Icon: LucideIcon;
|
||||
color: string;
|
||||
bg: string;
|
||||
}
|
||||
|
||||
interface FeatureCardProps {
|
||||
title: string;
|
||||
description: string;
|
||||
icons: FeatureIcon[];
|
||||
gradient: string;
|
||||
delay?: number;
|
||||
}
|
||||
|
||||
export function FeatureCard({ title, description, icons, gradient, delay = 0 }: FeatureCardProps) {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const [isInView, setIsInView] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const observer = new IntersectionObserver(
|
||||
([entry]) => {
|
||||
if (entry.isIntersecting) {
|
||||
setIsInView(true);
|
||||
observer.disconnect();
|
||||
}
|
||||
},
|
||||
{ threshold: 0.1 }
|
||||
);
|
||||
|
||||
if (ref.current) {
|
||||
observer.observe(ref.current);
|
||||
}
|
||||
|
||||
return () => observer.disconnect();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={`opacity-0 ${isInView ? 'animate-fade-in-up' : ''}`}
|
||||
style={{ animationDelay: `${delay}ms` }}
|
||||
>
|
||||
<Card
|
||||
className="relative backdrop-blur-xl border border-white/10 shadow-xl hover-lift transition-all duration-300 h-full overflow-hidden group"
|
||||
style={{
|
||||
background: `linear-gradient(135deg, ${gradient})`,
|
||||
}}
|
||||
>
|
||||
{/* Decorative gradient orb */}
|
||||
<div className="absolute top-0 right-0 w-40 h-40 bg-gradient-to-br from-white/10 to-transparent rounded-full -translate-y-1/2 translate-x-1/2 pointer-events-none group-hover:scale-110 transition-transform duration-500" />
|
||||
|
||||
<CardHeader className="relative z-10">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
{icons.map((icon, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={`w-10 h-10 rounded-xl ${icon.bg} flex items-center justify-center`}
|
||||
>
|
||||
<icon.Icon className={`h-5 w-5 ${icon.color}`} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<CardTitle className="text-xl font-bold text-foreground">{title}</CardTitle>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="relative z-10">
|
||||
<p className="text-muted-foreground leading-relaxed">{description}</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
88
src/components/landing/FeaturesSection.tsx
Normal file
88
src/components/landing/FeaturesSection.tsx
Normal file
@ -0,0 +1,88 @@
|
||||
'use client';
|
||||
|
||||
import { FeatureCard } from './FeatureCard';
|
||||
import { Cigarette, Leaf, Heart, Trophy, DollarSign } from 'lucide-react';
|
||||
|
||||
const FEATURES = [
|
||||
{
|
||||
id: 'dual-tracking',
|
||||
title: 'Dual Substance Tracking',
|
||||
description:
|
||||
'Track both nicotine and marijuana independently. Each substance has its own stats, timeline, and achievements to support your unique journey.',
|
||||
icons: [
|
||||
{ Icon: Cigarette, color: 'text-red-400', bg: 'bg-red-500/20' },
|
||||
{ Icon: Leaf, color: 'text-green-400', bg: 'bg-green-500/20' },
|
||||
],
|
||||
gradient: 'rgba(239, 68, 68, 0.15) 0%, rgba(34, 197, 94, 0.15) 100%',
|
||||
},
|
||||
{
|
||||
id: 'health-timeline',
|
||||
title: 'Health Recovery Timeline',
|
||||
description:
|
||||
'Watch your body heal in real-time. From 20 minutes to 1 year, track every health milestone as your body recovers and gets stronger.',
|
||||
icons: [{ Icon: Heart, color: 'text-teal-400', bg: 'bg-teal-500/20' }],
|
||||
gradient: 'rgba(20, 184, 166, 0.15) 0%, rgba(6, 182, 212, 0.1) 100%',
|
||||
},
|
||||
{
|
||||
id: 'achievements',
|
||||
title: 'Achievements & Badges',
|
||||
description:
|
||||
'Unlock badges as you progress. First Step, Week Warrior, Goal Crusher - celebrate every win and stay motivated on your journey.',
|
||||
icons: [{ Icon: Trophy, color: 'text-yellow-400', bg: 'bg-yellow-500/20' }],
|
||||
gradient: 'rgba(234, 179, 8, 0.15) 0%, rgba(245, 158, 11, 0.1) 100%',
|
||||
},
|
||||
{
|
||||
id: 'savings',
|
||||
title: 'Money Savings Tracker',
|
||||
description:
|
||||
'See how much you save by cutting back. Set your goals and watch your savings grow daily - extra motivation to keep going.',
|
||||
icons: [{ Icon: DollarSign, color: 'text-emerald-400', bg: 'bg-emerald-500/20' }],
|
||||
gradient: 'rgba(16, 185, 129, 0.15) 0%, rgba(34, 197, 94, 0.1) 100%',
|
||||
},
|
||||
];
|
||||
|
||||
export function FeaturesSection() {
|
||||
return (
|
||||
<section
|
||||
id="features"
|
||||
aria-labelledby="features-heading"
|
||||
className="py-20 px-4 relative"
|
||||
>
|
||||
{/* Background accent */}
|
||||
<div className="absolute inset-0 bg-gradient-to-b from-transparent via-primary/5 to-transparent pointer-events-none" />
|
||||
|
||||
<div className="container mx-auto max-w-6xl relative z-10">
|
||||
{/* Section Header */}
|
||||
<div className="text-center mb-16">
|
||||
<h2
|
||||
id="features-heading"
|
||||
className="text-3xl sm:text-4xl font-bold mb-4"
|
||||
>
|
||||
Everything You Need to{' '}
|
||||
<span className="bg-gradient-to-r from-primary via-purple-500 to-pink-500 bg-clip-text text-transparent">
|
||||
Succeed
|
||||
</span>
|
||||
</h2>
|
||||
<p className="text-muted-foreground max-w-2xl mx-auto">
|
||||
Built with empathy and designed for your success. Every feature supports your journey
|
||||
to a healthier, happier life.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Features Grid */}
|
||||
<div className="grid sm:grid-cols-2 gap-6">
|
||||
{FEATURES.map((feature, index) => (
|
||||
<FeatureCard
|
||||
key={feature.id}
|
||||
title={feature.title}
|
||||
description={feature.description}
|
||||
icons={feature.icons}
|
||||
gradient={feature.gradient}
|
||||
delay={index * 100}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
233
src/components/landing/HeroSection.tsx
Normal file
233
src/components/landing/HeroSection.tsx
Normal file
@ -0,0 +1,233 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import { ArrowRight, Smartphone, CheckCircle, Shield, Wifi, Download, Share, Plus, MoreVertical } from 'lucide-react';
|
||||
import { type PWAInstallState } from '@/hooks/usePWAInstall';
|
||||
|
||||
interface HeroSectionProps {
|
||||
pwaInstall: PWAInstallState;
|
||||
}
|
||||
|
||||
export function HeroSection({ pwaInstall }: HeroSectionProps) {
|
||||
const [showInstructions, setShowInstructions] = useState(false);
|
||||
|
||||
const handleInstallClick = async () => {
|
||||
if (pwaInstall.isInstallable) {
|
||||
await pwaInstall.promptInstall();
|
||||
} else {
|
||||
setShowInstructions(true);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<section
|
||||
id="hero"
|
||||
aria-labelledby="hero-heading"
|
||||
className="min-h-screen flex items-center justify-center px-4 pt-24 pb-16 relative overflow-hidden"
|
||||
>
|
||||
{/* Background decorative elements */}
|
||||
<div className="absolute top-1/4 left-1/4 w-96 h-96 bg-primary/20 rounded-full blur-[120px] animate-pulse-subtle pointer-events-none" />
|
||||
<div className="absolute bottom-1/4 right-1/4 w-96 h-96 bg-purple-500/15 rounded-full blur-[120px] animate-pulse-subtle delay-500 pointer-events-none" />
|
||||
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[600px] h-[600px] bg-gradient-to-br from-primary/10 to-purple-500/10 rounded-full blur-[150px] pointer-events-none" />
|
||||
|
||||
<div className="container mx-auto max-w-5xl text-center relative z-10">
|
||||
{/* Badge */}
|
||||
<div className="inline-flex items-center gap-2 px-4 py-2 rounded-full bg-primary/10 border border-primary/20 mb-8 animate-fade-in">
|
||||
<CheckCircle className="h-4 w-4 text-primary" />
|
||||
<span className="text-sm font-medium text-primary">Free & Works Offline</span>
|
||||
</div>
|
||||
|
||||
{/* Headline */}
|
||||
<h1
|
||||
id="hero-heading"
|
||||
className="text-4xl sm:text-5xl lg:text-6xl font-black tracking-tight mb-6 animate-fade-in-up"
|
||||
>
|
||||
<span className="bg-gradient-to-r from-primary via-purple-500 to-pink-500 bg-clip-text text-transparent">
|
||||
Take Control
|
||||
</span>
|
||||
<br />
|
||||
<span className="text-foreground">of Your Journey</span>
|
||||
</h1>
|
||||
|
||||
{/* Subheadline */}
|
||||
<p className="text-lg sm:text-xl text-muted-foreground max-w-2xl mx-auto mb-10 animate-fade-in-up delay-200">
|
||||
Track nicotine and marijuana usage, celebrate milestones, and watch your health recover.
|
||||
Your companion to a healthier life.
|
||||
</p>
|
||||
|
||||
{/* Dual CTAs */}
|
||||
<div className="flex flex-col sm:flex-row gap-4 justify-center items-center animate-fade-in-up delay-300">
|
||||
<Button
|
||||
asChild
|
||||
size="lg"
|
||||
className="h-14 px-8 text-lg rounded-xl bg-gradient-to-r from-primary to-purple-600 hover:from-primary/90 hover:to-purple-600/90 hover:scale-105 transition-all shadow-xl shadow-primary/25"
|
||||
>
|
||||
<Link href="/login">
|
||||
Get Started Free
|
||||
<ArrowRight className="ml-2 h-5 w-5" />
|
||||
</Link>
|
||||
</Button>
|
||||
|
||||
{!pwaInstall.isStandalone && (
|
||||
<Button
|
||||
size="lg"
|
||||
variant="outline"
|
||||
onClick={handleInstallClick}
|
||||
aria-label="Install QuitTraq as an app on your device"
|
||||
className="h-14 px-8 text-lg rounded-xl border-purple-500/30 hover:bg-purple-500/10 hover:scale-105 transition-all"
|
||||
>
|
||||
<Smartphone className="mr-2 h-5 w-5" />
|
||||
Install App
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Trust signals */}
|
||||
<div className="flex flex-wrap justify-center gap-6 mt-12 animate-fade-in-up delay-400">
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<Shield className="h-4 w-4 text-primary" />
|
||||
<span>Privacy First</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<Wifi className="h-4 w-4 text-primary" />
|
||||
<span>Works Offline</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<CheckCircle className="h-4 w-4 text-primary" />
|
||||
<span>Always Free</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* PWA Install Instructions Dialog */}
|
||||
<Dialog open={showInstructions} onOpenChange={setShowInstructions}>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<Download className="h-5 w-5 text-purple-400" />
|
||||
Add QuitTraq to Home Screen
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
Get quick access to track your progress right from your phone's home screen.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-6 py-4">
|
||||
{pwaInstall.isIOS ? (
|
||||
<div className="space-y-4">
|
||||
<p className="text-sm font-medium text-purple-400">For iPhone / iPad (Safari):</p>
|
||||
<ol className="space-y-4 text-sm">
|
||||
<li className="flex items-start gap-3">
|
||||
<span className="flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-purple-500/20 text-purple-400 text-xs font-bold">
|
||||
1
|
||||
</span>
|
||||
<div>
|
||||
<p>
|
||||
Tap the <strong>Share</strong> button
|
||||
</p>
|
||||
<div className="mt-1 inline-flex items-center gap-1 rounded bg-muted px-2 py-1">
|
||||
<Share className="h-4 w-4" />
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<li className="flex items-start gap-3">
|
||||
<span className="flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-purple-500/20 text-purple-400 text-xs font-bold">
|
||||
2
|
||||
</span>
|
||||
<div>
|
||||
<p>
|
||||
Scroll down and tap <strong>"Add to Home Screen"</strong>
|
||||
</p>
|
||||
<div className="mt-1 inline-flex items-center gap-1 rounded bg-muted px-2 py-1">
|
||||
<Plus className="h-4 w-4" /> Add to Home Screen
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<li className="flex items-start gap-3">
|
||||
<span className="flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-purple-500/20 text-purple-400 text-xs font-bold">
|
||||
3
|
||||
</span>
|
||||
<p>
|
||||
Tap <strong>"Add"</strong> to confirm
|
||||
</p>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
) : pwaInstall.isAndroid ? (
|
||||
<div className="space-y-4">
|
||||
<p className="text-sm font-medium text-purple-400">For Android (Chrome):</p>
|
||||
<ol className="space-y-4 text-sm">
|
||||
<li className="flex items-start gap-3">
|
||||
<span className="flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-purple-500/20 text-purple-400 text-xs font-bold">
|
||||
1
|
||||
</span>
|
||||
<div>
|
||||
<p>
|
||||
Tap the <strong>menu</strong> button (3 dots)
|
||||
</p>
|
||||
<div className="mt-1 inline-flex items-center gap-1 rounded bg-muted px-2 py-1">
|
||||
<MoreVertical className="h-4 w-4" />
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<li className="flex items-start gap-3">
|
||||
<span className="flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-purple-500/20 text-purple-400 text-xs font-bold">
|
||||
2
|
||||
</span>
|
||||
<p>
|
||||
Tap <strong>"Add to Home screen"</strong> or{' '}
|
||||
<strong>"Install app"</strong>
|
||||
</p>
|
||||
</li>
|
||||
<li className="flex items-start gap-3">
|
||||
<span className="flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-purple-500/20 text-purple-400 text-xs font-bold">
|
||||
3
|
||||
</span>
|
||||
<p>
|
||||
Tap <strong>"Add"</strong> or <strong>"Install"</strong> to
|
||||
confirm
|
||||
</p>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
<p className="text-sm font-medium text-purple-400">On mobile device:</p>
|
||||
<ul className="space-y-2 text-sm text-muted-foreground">
|
||||
<li>
|
||||
<strong>iPhone/iPad:</strong> Use Safari, tap Share → Add to Home Screen
|
||||
</li>
|
||||
<li>
|
||||
<strong>Android:</strong> Use Chrome, tap Menu → Add to Home screen
|
||||
</li>
|
||||
</ul>
|
||||
<p className="text-xs text-muted-foreground mt-4">
|
||||
Open this page on your phone to add QuitTraq to your home screen for quick access!
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="pt-2 border-t border-white/10">
|
||||
<p className="text-xs text-muted-foreground text-center">
|
||||
Once added, tap the QuitTraq icon to quickly log your usage!
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button onClick={() => setShowInstructions(false)} className="w-full">
|
||||
Got it!
|
||||
</Button>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
107
src/components/landing/LandingFooter.tsx
Normal file
107
src/components/landing/LandingFooter.tsx
Normal file
@ -0,0 +1,107 @@
|
||||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
|
||||
const footerLinks = {
|
||||
product: [
|
||||
{ label: 'Features', href: '#features' },
|
||||
{ label: 'Demo', href: '#demo' },
|
||||
{ label: 'Get Started', href: '#cta' },
|
||||
],
|
||||
legal: [
|
||||
{ label: 'Privacy Policy', href: '/privacy' },
|
||||
{ label: 'Terms of Service', href: '/terms' },
|
||||
],
|
||||
};
|
||||
|
||||
export function LandingFooter() {
|
||||
const currentYear = new Date().getFullYear();
|
||||
|
||||
const handleAnchorClick = (href: string) => {
|
||||
if (href.startsWith('#')) {
|
||||
const element = document.querySelector(href);
|
||||
if (element) {
|
||||
element.scrollIntoView({ behavior: 'smooth' });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<footer role="contentinfo" className="border-t border-white/10 bg-background/50 backdrop-blur-sm">
|
||||
<div className="container mx-auto max-w-6xl px-4 py-12">
|
||||
<div className="grid gap-8 sm:grid-cols-2 lg:grid-cols-4">
|
||||
{/* Brand */}
|
||||
<div className="sm:col-span-2 lg:col-span-2">
|
||||
<Link href="/home" className="flex items-center gap-2 mb-4">
|
||||
<div className="w-9 h-9 bg-gradient-to-br from-primary to-purple-600 rounded-xl flex items-center justify-center shadow-lg shadow-primary/20">
|
||||
<span className="text-lg font-black text-white">Q</span>
|
||||
</div>
|
||||
<span className="text-xl font-black tracking-tight">QuitTraq</span>
|
||||
</Link>
|
||||
<p className="text-sm text-muted-foreground max-w-sm mb-4">
|
||||
Your companion to a healthier life. Track your progress, celebrate milestones, and take
|
||||
control of your journey.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Product Links */}
|
||||
<div>
|
||||
<h3 className="font-semibold mb-4">Product</h3>
|
||||
<ul className="space-y-2">
|
||||
{footerLinks.product.map((link) => (
|
||||
<li key={link.href}>
|
||||
{link.href.startsWith('#') ? (
|
||||
<button
|
||||
onClick={() => handleAnchorClick(link.href)}
|
||||
className="text-sm text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
{link.label}
|
||||
</button>
|
||||
) : (
|
||||
<Link
|
||||
href={link.href}
|
||||
className="text-sm text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
{link.label}
|
||||
</Link>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* Legal Links */}
|
||||
<div>
|
||||
<h3 className="font-semibold mb-4">Legal</h3>
|
||||
<ul className="space-y-2">
|
||||
{footerLinks.legal.map((link) => (
|
||||
<li key={link.href}>
|
||||
<Link
|
||||
href={link.href}
|
||||
className="text-sm text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
{link.label}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Affiliate Disclosure */}
|
||||
<div className="mt-12 pt-8 border-t border-white/10">
|
||||
<p className="text-xs text-muted-foreground text-center max-w-2xl mx-auto mb-6">
|
||||
QuitTraq may contain affiliate links. We may earn a small commission when you purchase
|
||||
products through our links, at no extra cost to you. This helps support the free features
|
||||
of the app.
|
||||
</p>
|
||||
|
||||
{/* Copyright */}
|
||||
<p className="text-xs text-muted-foreground text-center">
|
||||
© {currentYear} QuitTraq. All rights reserved.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
142
src/components/landing/LandingNav.tsx
Normal file
142
src/components/landing/LandingNav.tsx
Normal file
@ -0,0 +1,142 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Menu, X, Sun, Moon } from 'lucide-react';
|
||||
|
||||
interface LandingNavProps {
|
||||
theme: 'dark' | 'light';
|
||||
toggleTheme: () => void;
|
||||
}
|
||||
|
||||
const navLinks = [
|
||||
{ href: '#features', label: 'Features' },
|
||||
{ href: '#demo', label: 'Demo' },
|
||||
{ href: '#cta', label: 'Get Started' },
|
||||
];
|
||||
|
||||
export function LandingNav({ theme, toggleTheme }: LandingNavProps) {
|
||||
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
||||
|
||||
const handleNavClick = (href: string) => {
|
||||
setMobileMenuOpen(false);
|
||||
// Smooth scroll handled by CSS (scroll-behavior: smooth)
|
||||
const element = document.querySelector(href);
|
||||
if (element) {
|
||||
element.scrollIntoView({ behavior: 'smooth' });
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<header
|
||||
role="banner"
|
||||
className="fixed top-0 left-0 right-0 z-50 bg-background/80 backdrop-blur-xl border-b border-white/10"
|
||||
>
|
||||
<nav
|
||||
role="navigation"
|
||||
aria-label="Main navigation"
|
||||
className="container mx-auto max-w-6xl px-4 h-16 flex items-center justify-between"
|
||||
>
|
||||
{/* Logo */}
|
||||
<Link
|
||||
href="/home"
|
||||
className="flex items-center gap-2 group"
|
||||
onClick={() => window.scrollTo({ top: 0, behavior: 'smooth' })}
|
||||
>
|
||||
<div className="w-9 h-9 bg-gradient-to-br from-primary to-purple-600 rounded-xl flex items-center justify-center shadow-lg shadow-primary/20 group-hover:scale-105 transition-transform">
|
||||
<span className="text-lg font-black text-white">Q</span>
|
||||
</div>
|
||||
<span className="text-xl font-black tracking-tight bg-gradient-to-br from-foreground to-foreground/70 bg-clip-text text-transparent">
|
||||
QuitTraq
|
||||
</span>
|
||||
</Link>
|
||||
|
||||
{/* Desktop Navigation */}
|
||||
<div className="hidden md:flex items-center gap-6">
|
||||
{navLinks.map((link) => (
|
||||
<button
|
||||
key={link.href}
|
||||
onClick={() => handleNavClick(link.href)}
|
||||
className="text-sm font-medium text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
{link.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex items-center gap-2">
|
||||
{/* Theme Toggle */}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={toggleTheme}
|
||||
aria-label={`Switch to ${theme === 'dark' ? 'light' : 'dark'} mode`}
|
||||
className="text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
{theme === 'dark' ? (
|
||||
<Sun className="h-5 w-5" />
|
||||
) : (
|
||||
<Moon className="h-5 w-5" />
|
||||
)}
|
||||
</Button>
|
||||
|
||||
{/* Login Button - Desktop */}
|
||||
<Button
|
||||
asChild
|
||||
size="sm"
|
||||
className="hidden md:inline-flex bg-gradient-to-r from-primary to-purple-600 hover:from-primary/90 hover:to-purple-600/90"
|
||||
>
|
||||
<Link href="/login">Sign In</Link>
|
||||
</Button>
|
||||
|
||||
{/* Mobile Menu Toggle */}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="md:hidden"
|
||||
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
|
||||
aria-label={mobileMenuOpen ? 'Close menu' : 'Open menu'}
|
||||
aria-expanded={mobileMenuOpen}
|
||||
aria-controls="mobile-menu"
|
||||
>
|
||||
{mobileMenuOpen ? (
|
||||
<X className="h-5 w-5" />
|
||||
) : (
|
||||
<Menu className="h-5 w-5" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
{/* Mobile Menu */}
|
||||
{mobileMenuOpen && (
|
||||
<div
|
||||
id="mobile-menu"
|
||||
className="md:hidden bg-background/95 backdrop-blur-xl border-b border-white/10 animate-fade-in"
|
||||
>
|
||||
<div className="container mx-auto px-4 py-4 space-y-2">
|
||||
{navLinks.map((link) => (
|
||||
<button
|
||||
key={link.href}
|
||||
onClick={() => handleNavClick(link.href)}
|
||||
className="block w-full text-left px-4 py-3 rounded-lg text-sm font-medium text-muted-foreground hover:text-foreground hover:bg-accent transition-colors"
|
||||
>
|
||||
{link.label}
|
||||
</button>
|
||||
))}
|
||||
<div className="pt-2 border-t border-white/10">
|
||||
<Button
|
||||
asChild
|
||||
className="w-full bg-gradient-to-r from-primary to-purple-600"
|
||||
>
|
||||
<Link href="/login">Sign In</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</header>
|
||||
);
|
||||
}
|
||||
38
src/components/landing/LandingPage.tsx
Normal file
38
src/components/landing/LandingPage.tsx
Normal file
@ -0,0 +1,38 @@
|
||||
'use client';
|
||||
|
||||
import { useTheme } from '@/lib/theme-context';
|
||||
import { LandingNav } from './LandingNav';
|
||||
import { HeroSection } from './HeroSection';
|
||||
import { FeaturesSection } from './FeaturesSection';
|
||||
import { DemoSection } from './DemoSection';
|
||||
import { CTASection } from './CTASection';
|
||||
import { LandingFooter } from './LandingFooter';
|
||||
import { usePWAInstall } from '@/hooks/usePWAInstall';
|
||||
|
||||
export function LandingPage() {
|
||||
const { theme, toggleTheme } = useTheme();
|
||||
const pwaInstall = usePWAInstall();
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
{/* Skip link for accessibility */}
|
||||
<a
|
||||
href="#main-content"
|
||||
className="sr-only focus:not-sr-only focus:absolute focus:top-4 focus:left-4 focus:z-50 focus:px-4 focus:py-2 focus:bg-primary focus:text-white focus:rounded-lg"
|
||||
>
|
||||
Skip to main content
|
||||
</a>
|
||||
|
||||
<LandingNav theme={theme} toggleTheme={toggleTheme} />
|
||||
|
||||
<main id="main-content" role="main">
|
||||
<HeroSection pwaInstall={pwaInstall} />
|
||||
<FeaturesSection />
|
||||
<DemoSection />
|
||||
<CTASection pwaInstall={pwaInstall} />
|
||||
</main>
|
||||
|
||||
<LandingFooter />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
78
src/hooks/usePWAInstall.ts
Normal file
78
src/hooks/usePWAInstall.ts
Normal file
@ -0,0 +1,78 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
|
||||
interface BeforeInstallPromptEvent extends Event {
|
||||
prompt: () => Promise<void>;
|
||||
userChoice: Promise<{ outcome: 'accepted' | 'dismissed' }>;
|
||||
}
|
||||
|
||||
export interface PWAInstallState {
|
||||
isInstallable: boolean;
|
||||
isStandalone: boolean;
|
||||
isIOS: boolean;
|
||||
isAndroid: boolean;
|
||||
needsManualInstructions: boolean;
|
||||
promptInstall: () => Promise<{ outcome: 'accepted' | 'dismissed' | 'unavailable' }>;
|
||||
}
|
||||
|
||||
export function usePWAInstall(): PWAInstallState {
|
||||
const [deferredPrompt, setDeferredPrompt] = useState<BeforeInstallPromptEvent | null>(null);
|
||||
const [isIOS, setIsIOS] = useState(false);
|
||||
const [isAndroid, setIsAndroid] = useState(false);
|
||||
const [isStandalone, setIsStandalone] = useState(false);
|
||||
const [isInstallable, setIsInstallable] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
// Check if already installed as PWA
|
||||
const isAppInstalled =
|
||||
window.matchMedia('(display-mode: standalone)').matches ||
|
||||
(window.navigator as Navigator & { standalone?: boolean }).standalone === true;
|
||||
setIsStandalone(isAppInstalled);
|
||||
|
||||
// Detect platform
|
||||
const userAgent = window.navigator.userAgent.toLowerCase();
|
||||
const isIOSDevice = /iphone|ipad|ipod/.test(userAgent);
|
||||
const isAndroidDevice = /android/.test(userAgent);
|
||||
setIsIOS(isIOSDevice);
|
||||
setIsAndroid(isAndroidDevice);
|
||||
|
||||
// Listen for beforeinstallprompt (Android/Chrome)
|
||||
const handleBeforeInstallPrompt = (e: Event) => {
|
||||
e.preventDefault();
|
||||
setDeferredPrompt(e as BeforeInstallPromptEvent);
|
||||
setIsInstallable(true);
|
||||
};
|
||||
|
||||
window.addEventListener('beforeinstallprompt', handleBeforeInstallPrompt);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('beforeinstallprompt', handleBeforeInstallPrompt);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const promptInstall = useCallback(async (): Promise<{ outcome: 'accepted' | 'dismissed' | 'unavailable' }> => {
|
||||
if (!deferredPrompt) {
|
||||
return { outcome: 'unavailable' };
|
||||
}
|
||||
|
||||
await deferredPrompt.prompt();
|
||||
const { outcome } = await deferredPrompt.userChoice;
|
||||
|
||||
if (outcome === 'accepted') {
|
||||
setDeferredPrompt(null);
|
||||
setIsInstallable(false);
|
||||
}
|
||||
|
||||
return { outcome };
|
||||
}, [deferredPrompt]);
|
||||
|
||||
return {
|
||||
isInstallable,
|
||||
isStandalone,
|
||||
isIOS,
|
||||
isAndroid,
|
||||
needsManualInstructions: !deferredPrompt && (isIOS || isAndroid),
|
||||
promptInstall,
|
||||
};
|
||||
}
|
||||
@ -15,10 +15,31 @@ export function ThemeProvider({ children }: { children: ReactNode }) {
|
||||
const [theme, setTheme] = useState<Theme>('dark');
|
||||
|
||||
useEffect(() => {
|
||||
// Check localStorage first
|
||||
const saved = localStorage.getItem('theme') as Theme | null;
|
||||
if (saved) {
|
||||
setTheme(saved);
|
||||
return;
|
||||
}
|
||||
|
||||
// Fall back to system preference
|
||||
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
setTheme(prefersDark ? 'dark' : 'light');
|
||||
}, []);
|
||||
|
||||
// Listen for system preference changes (only if no manual preference set)
|
||||
useEffect(() => {
|
||||
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
|
||||
const handleChange = (e: MediaQueryListEvent) => {
|
||||
// Only update if user hasn't manually set a preference
|
||||
if (!localStorage.getItem('theme')) {
|
||||
setTheme(e.matches ? 'dark' : 'light');
|
||||
}
|
||||
};
|
||||
|
||||
mediaQuery.addEventListener('change', handleChange);
|
||||
return () => mediaQuery.removeEventListener('change', handleChange);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user