first commit
This commit is contained in:
commit
2b251f3ac2
24
.gitignore
vendored
Normal file
24
.gitignore
vendored
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
50
App.tsx
Normal file
50
App.tsx
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Navbar } from './components/Navbar';
|
||||||
|
import { Hero } from './components/Hero';
|
||||||
|
import { ProblemComparison } from './components/ProblemComparison';
|
||||||
|
import { Logic } from './components/Logic';
|
||||||
|
import { Features } from './components/Features';
|
||||||
|
import { AppShowcase } from './components/AppShowcase';
|
||||||
|
import { ChatDemo } from './components/ChatDemo';
|
||||||
|
import { Testimonials } from './components/Testimonials';
|
||||||
|
import { Audience } from './components/Audience';
|
||||||
|
import { Pricing } from './components/Pricing';
|
||||||
|
import { FAQ } from './components/FAQ';
|
||||||
|
import { Footer } from './components/Footer';
|
||||||
|
|
||||||
|
const App: React.FC = () => {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-background text-white selection:bg-primary-500/30 font-sans relative overflow-x-hidden">
|
||||||
|
<div className="bg-noise"></div>
|
||||||
|
<Navbar />
|
||||||
|
<main className="relative z-10 pb-24 md:pb-0">
|
||||||
|
<Hero />
|
||||||
|
<ProblemComparison />
|
||||||
|
<AppShowcase />
|
||||||
|
<Logic />
|
||||||
|
<Features />
|
||||||
|
<ChatDemo />
|
||||||
|
<Testimonials />
|
||||||
|
<Audience />
|
||||||
|
<Pricing />
|
||||||
|
<FAQ />
|
||||||
|
</main>
|
||||||
|
<Footer />
|
||||||
|
|
||||||
|
{/* Mobile Sticky CTA */}
|
||||||
|
<div className="fixed bottom-6 left-4 right-4 z-50 md:hidden">
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
const el = document.getElementById('pricing');
|
||||||
|
if(el) el.scrollIntoView({behavior: 'smooth'});
|
||||||
|
}}
|
||||||
|
className="w-full py-4 rounded-2xl bg-white text-black font-bold text-lg shadow-[0_0_30px_rgba(255,255,255,0.3)] animate-pulse-slow"
|
||||||
|
>
|
||||||
|
Build Your Plan
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default App;
|
||||||
20
README.md
Normal file
20
README.md
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
<div align="center">
|
||||||
|
<img width="1200" height="475" alt="GHBanner" src="https://github.com/user-attachments/assets/0aa67016-6eaf-458a-adb2-6e31a0763ed6" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
# Run and deploy your AI Studio app
|
||||||
|
|
||||||
|
This contains everything you need to run your app locally.
|
||||||
|
|
||||||
|
View your app in AI Studio: https://ai.studio/apps/drive/18Tw8rUWhfHoHaxOJ-dfHFkj8ZkbG1XKL
|
||||||
|
|
||||||
|
## Run Locally
|
||||||
|
|
||||||
|
**Prerequisites:** Node.js
|
||||||
|
|
||||||
|
|
||||||
|
1. Install dependencies:
|
||||||
|
`npm install`
|
||||||
|
2. Set the `GEMINI_API_KEY` in [.env.local](.env.local) to your Gemini API key
|
||||||
|
3. Run the app:
|
||||||
|
`npm run dev`
|
||||||
369
components/AppShowcase.tsx
Normal file
369
components/AppShowcase.tsx
Normal file
@ -0,0 +1,369 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { motion, AnimatePresence } from 'framer-motion';
|
||||||
|
import { PhoneFrame } from './PhoneFrame';
|
||||||
|
import { Activity, Zap, MessageSquare, TrendingUp, ChevronRight, User, BarChart2, Send } from 'lucide-react';
|
||||||
|
|
||||||
|
type Tab = 'dashboard' | 'chat' | 'roi';
|
||||||
|
|
||||||
|
export const AppShowcase: React.FC = () => {
|
||||||
|
const [activeTab, setActiveTab] = useState<Tab>('dashboard');
|
||||||
|
const [isTrainingDay, setIsTrainingDay] = useState(true);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="py-32 bg-black relative overflow-hidden">
|
||||||
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
|
|
||||||
|
<div className="grid lg:grid-cols-2 gap-16 items-center">
|
||||||
|
{/* Left Column: Content & Tabs */}
|
||||||
|
<div>
|
||||||
|
<h2 className="text-4xl md:text-6xl font-bold text-white mb-6 tracking-tighter">
|
||||||
|
See Inside the <br/>
|
||||||
|
<span className="text-primary-400">Black Box</span>.
|
||||||
|
</h2>
|
||||||
|
<p className="text-xl text-gray-400 mb-12 leading-relaxed">
|
||||||
|
Experience how FitMate adapts to your biology in real-time.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* Interactive Tab Controls */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
<button
|
||||||
|
onClick={() => setActiveTab('dashboard')}
|
||||||
|
className={`w-full p-6 text-left rounded-2xl border transition-all duration-300 flex items-center gap-4 group ${
|
||||||
|
activeTab === 'dashboard'
|
||||||
|
? 'bg-white/10 border-primary-500/50 shadow-[0_0_30px_rgba(6,182,212,0.15)]'
|
||||||
|
: 'bg-transparent border-white/5 hover:bg-white/5'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className={`p-3 rounded-xl transition-colors ${activeTab === 'dashboard' ? 'bg-primary-500 text-black' : 'bg-white/5 text-gray-400'}`}>
|
||||||
|
<Activity size={24} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className={`font-bold text-lg ${activeTab === 'dashboard' ? 'text-white' : 'text-gray-400 group-hover:text-white'}`}>Dynamic Dashboard</h3>
|
||||||
|
<p className="text-sm text-gray-500">See how targets shift between Training & Rest days.</p>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => setActiveTab('chat')}
|
||||||
|
className={`w-full p-6 text-left rounded-2xl border transition-all duration-300 flex items-center gap-4 group ${
|
||||||
|
activeTab === 'chat'
|
||||||
|
? 'bg-white/10 border-primary-500/50 shadow-[0_0_30px_rgba(6,182,212,0.15)]'
|
||||||
|
: 'bg-transparent border-white/5 hover:bg-white/5'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className={`p-3 rounded-xl transition-colors ${activeTab === 'chat' ? 'bg-primary-500 text-black' : 'bg-white/5 text-gray-400'}`}>
|
||||||
|
<MessageSquare size={24} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className={`font-bold text-lg ${activeTab === 'chat' ? 'text-white' : 'text-gray-400 group-hover:text-white'}`}>The Co-Pilot</h3>
|
||||||
|
<p className="text-sm text-gray-500">Real-time adjustments for missed meals or slip-ups.</p>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => setActiveTab('roi')}
|
||||||
|
className={`w-full p-6 text-left rounded-2xl border transition-all duration-300 flex items-center gap-4 group ${
|
||||||
|
activeTab === 'roi'
|
||||||
|
? 'bg-white/10 border-primary-500/50 shadow-[0_0_30px_rgba(6,182,212,0.15)]'
|
||||||
|
: 'bg-transparent border-white/5 hover:bg-white/5'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className={`p-3 rounded-xl transition-colors ${activeTab === 'roi' ? 'bg-primary-500 text-black' : 'bg-white/5 text-gray-400'}`}>
|
||||||
|
<TrendingUp size={24} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className={`font-bold text-lg ${activeTab === 'roi' ? 'text-white' : 'text-gray-400 group-hover:text-white'}`}>ROI Graph</h3>
|
||||||
|
<p className="text-sm text-gray-500">Scale weight is noise. Trend weight is truth.</p>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Right Column: Phone Mockup */}
|
||||||
|
<div className="flex justify-center relative">
|
||||||
|
{/* Glow behind phone */}
|
||||||
|
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[120%] h-[80%] bg-primary-500/20 blur-[100px] rounded-full pointer-events-none" />
|
||||||
|
|
||||||
|
<PhoneFrame className="w-[360px] h-[720px]">
|
||||||
|
{/* Persistent Tab Bar */}
|
||||||
|
<div className="absolute bottom-0 left-0 right-0 h-20 bg-black/80 backdrop-blur-xl border-t border-white/10 z-40 flex items-start pt-3 justify-around px-4">
|
||||||
|
{[
|
||||||
|
{ icon: Activity, label: 'Plan', active: activeTab === 'dashboard' },
|
||||||
|
{ icon: MessageSquare, label: 'Chat', active: activeTab === 'chat' },
|
||||||
|
{ icon: TrendingUp, label: 'Trends', active: activeTab === 'roi' },
|
||||||
|
{ icon: User, label: 'Profile', active: false },
|
||||||
|
].map((tab, i) => (
|
||||||
|
<div key={i} className={`flex flex-col items-center gap-1 ${tab.active ? 'text-primary-400' : 'text-gray-500'}`}>
|
||||||
|
<tab.icon size={24} strokeWidth={tab.active ? 2.5 : 2} />
|
||||||
|
<span className="text-[10px] font-medium">{tab.label}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="h-full w-full pt-14 pb-24 px-5 flex flex-col relative z-10 overflow-hidden bg-black">
|
||||||
|
|
||||||
|
{/* --- DASHBOARD VIEW --- */}
|
||||||
|
{activeTab === 'dashboard' && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }}
|
||||||
|
className="h-full flex flex-col"
|
||||||
|
>
|
||||||
|
{/* Header */}
|
||||||
|
<div className="flex justify-between items-end mb-6">
|
||||||
|
<div>
|
||||||
|
<div className="text-gray-400 text-xs font-semibold uppercase tracking-wide">Wednesday, Oct 24</div>
|
||||||
|
<h1 className="text-3xl font-bold text-white tracking-tight">Summary</h1>
|
||||||
|
</div>
|
||||||
|
<div className="w-9 h-9 rounded-full bg-[#2C2C2E] flex items-center justify-center border border-white/10">
|
||||||
|
<User size={16} className="text-gray-400" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* iOS Segmented Control */}
|
||||||
|
<div className="bg-[#1C1C1E] p-1 rounded-lg mb-8 flex relative">
|
||||||
|
<motion.div
|
||||||
|
className="absolute top-1 bottom-1 w-[calc(50%-4px)] bg-[#636366] rounded-[6px] shadow-sm z-0"
|
||||||
|
initial={false}
|
||||||
|
animate={{ x: isTrainingDay ? 0 : '100%' }}
|
||||||
|
transition={{ type: "spring", stiffness: 400, damping: 30 }}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onClick={() => setIsTrainingDay(true)}
|
||||||
|
className={`flex-1 py-1.5 text-[13px] font-semibold rounded-md relative z-10 transition-colors ${isTrainingDay ? 'text-white' : 'text-gray-400'}`}
|
||||||
|
>
|
||||||
|
Training Day
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setIsTrainingDay(false)}
|
||||||
|
className={`flex-1 py-1.5 text-[13px] font-semibold rounded-md relative z-10 transition-colors ${!isTrainingDay ? 'text-white' : 'text-gray-400'}`}
|
||||||
|
>
|
||||||
|
Rest Day
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Apple Watch Style Rings */}
|
||||||
|
<div className="flex justify-center mb-8">
|
||||||
|
<div className="relative w-56 h-56 flex items-center justify-center">
|
||||||
|
{/* Definitions for Gradients */}
|
||||||
|
<svg width="0" height="0">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="ringGradient" x1="0%" y1="0%" x2="100%" y2="0%">
|
||||||
|
<stop offset="0%" stopColor="#06b6d4" />
|
||||||
|
<stop offset="100%" stopColor="#22d3ee" />
|
||||||
|
</linearGradient>
|
||||||
|
<filter id="glow" x="-20%" y="-20%" width="140%" height="140%">
|
||||||
|
<feGaussianBlur stdDeviation="4" result="blur" />
|
||||||
|
<feComposite in="SourceGraphic" in2="blur" operator="over" />
|
||||||
|
</filter>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
{/* Background Track */}
|
||||||
|
<svg className="w-full h-full transform -rotate-90 drop-shadow-2xl">
|
||||||
|
<circle cx="112" cy="112" r="90" stroke="#1C1C1E" strokeWidth="20" fill="none" />
|
||||||
|
{/* Animated Progress Ring */}
|
||||||
|
<motion.circle
|
||||||
|
cx="112" cy="112" r="90"
|
||||||
|
stroke="url(#ringGradient)"
|
||||||
|
strokeWidth="20"
|
||||||
|
fill="none"
|
||||||
|
strokeLinecap="round"
|
||||||
|
initial={{ strokeDasharray: "0 565" }}
|
||||||
|
animate={{ strokeDasharray: isTrainingDay ? "480 565" : "350 565" }}
|
||||||
|
transition={{ duration: 1.2, ease: [0.22, 1, 0.36, 1] }}
|
||||||
|
className="filter drop-shadow-[0_0_10px_rgba(34,211,238,0.3)]"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
{/* Center Stats */}
|
||||||
|
<div className="absolute inset-0 flex flex-col items-center justify-center">
|
||||||
|
<motion.div
|
||||||
|
key={isTrainingDay ? 'train' : 'rest'}
|
||||||
|
initial={{ scale: 0.8, opacity: 0 }}
|
||||||
|
animate={{ scale: 1, opacity: 1 }}
|
||||||
|
className="text-5xl font-bold text-white tracking-tighter font-sans"
|
||||||
|
>
|
||||||
|
{isTrainingDay ? '2,850' : '2,100'}
|
||||||
|
</motion.div>
|
||||||
|
<div className="text-primary-400 text-sm font-semibold uppercase tracking-wide mt-1">kcal Left</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Cards Grid */}
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div className="bg-[#1C1C1E] rounded-2xl p-4 relative overflow-hidden">
|
||||||
|
<div className="absolute top-0 right-0 p-2 opacity-20"><Activity /></div>
|
||||||
|
<div className="text-[13px] font-medium text-gray-400 mb-1">Carbs</div>
|
||||||
|
<div className="text-2xl font-bold text-white mb-2">{isTrainingDay ? '320g' : '150g'}</div>
|
||||||
|
<div className="w-full bg-gray-800 h-1.5 rounded-full overflow-hidden">
|
||||||
|
<motion.div
|
||||||
|
className="h-full bg-primary-500"
|
||||||
|
animate={{ width: isTrainingDay ? '80%' : '40%' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="bg-[#1C1C1E] rounded-2xl p-4 relative overflow-hidden">
|
||||||
|
<div className="absolute top-0 right-0 p-2 opacity-20"><Zap /></div>
|
||||||
|
<div className="text-[13px] font-medium text-gray-400 mb-1">Protein</div>
|
||||||
|
<div className="text-2xl font-bold text-white mb-2">{isTrainingDay ? '200g' : '220g'}</div>
|
||||||
|
<div className="w-full bg-gray-800 h-1.5 rounded-full overflow-hidden">
|
||||||
|
<motion.div
|
||||||
|
className="h-full bg-purple-500"
|
||||||
|
animate={{ width: isTrainingDay ? '90%' : '100%' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* --- CHAT VIEW --- */}
|
||||||
|
{activeTab === 'chat' && (
|
||||||
|
<div className="h-full flex flex-col relative">
|
||||||
|
{/* Header with Blur */}
|
||||||
|
<div className="absolute top-0 left-0 right-0 h-12 flex items-center justify-center z-20 border-b border-white/5 bg-black/50 backdrop-blur-md">
|
||||||
|
<span className="text-[17px] font-semibold text-white">Co-Pilot</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-1 pt-16 space-y-6 overflow-y-auto no-scrollbar pb-20">
|
||||||
|
{/* Timestamp */}
|
||||||
|
<div className="text-center text-xs text-gray-500 font-medium">Today 9:42 AM</div>
|
||||||
|
|
||||||
|
{/* User Message (iMessage style) */}
|
||||||
|
<motion.div initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }} className="flex justify-end px-2">
|
||||||
|
<div className="bg-gradient-to-br from-[#007AFF] to-[#0062CC] text-white px-4 py-2.5 rounded-[20px] rounded-br-md text-[16px] max-w-[80%] shadow-sm leading-snug">
|
||||||
|
I ate a burger and fries. Roughly 1200 cals. Am I screwed?
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
{/* AI Message */}
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 10 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ delay: 0.5 }}
|
||||||
|
className="flex justify-start px-2 items-end gap-2"
|
||||||
|
>
|
||||||
|
<div className="w-6 h-6 rounded-full bg-primary-500 flex-shrink-0 flex items-center justify-center">
|
||||||
|
<Zap size={12} className="text-black fill-current" />
|
||||||
|
</div>
|
||||||
|
<div className="bg-[#2C2C2E] text-white px-4 py-2.5 rounded-[20px] rounded-bl-md text-[16px] max-w-[80%] leading-snug">
|
||||||
|
Not at all. You're just 400 calories over.
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 10 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ delay: 1.2 }}
|
||||||
|
className="flex justify-start px-2 items-end gap-2"
|
||||||
|
>
|
||||||
|
<div className="w-6 h-6 rounded-full bg-transparent flex-shrink-0" />
|
||||||
|
<div className="bg-[#2C2C2E] text-white px-4 py-2.5 rounded-[20px] rounded-tl-md rounded-bl-md text-[16px] max-w-[80%] leading-snug">
|
||||||
|
I've removed <span className="text-primary-400 font-semibold">50g of carbs</span> from tomorrow's plan to balance the weekly load. Enjoy the protein! 🍔
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Input Area */}
|
||||||
|
<div className="absolute bottom-0 left-0 right-0 p-3 bg-[#1C1C1E]/80 backdrop-blur-xl border-t border-white/10 flex items-center gap-3">
|
||||||
|
<div className="w-8 h-8 rounded-full bg-[#2C2C2E] flex items-center justify-center text-gray-400">
|
||||||
|
<PlusIcon />
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 h-9 bg-[#111] border border-white/10 rounded-full flex items-center px-4 text-[15px] text-white">
|
||||||
|
Type a message...
|
||||||
|
</div>
|
||||||
|
<div className="w-8 h-8 rounded-full bg-primary-500 flex items-center justify-center text-black">
|
||||||
|
<Send size={16} className="ml-0.5" fill="currentColor" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* --- ROI VIEW --- */}
|
||||||
|
{activeTab === 'roi' && (
|
||||||
|
<div className="h-full flex flex-col pt-4">
|
||||||
|
<div className="mb-8">
|
||||||
|
<div className="text-sm font-semibold text-gray-400 mb-1">Trend Weight</div>
|
||||||
|
<div className="flex items-baseline gap-2">
|
||||||
|
<span className="text-5xl font-bold text-white tracking-tight">184.2</span>
|
||||||
|
<span className="text-[17px] font-semibold text-primary-500">-1.4 lbs</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-gray-500 font-medium mt-2">Last 30 Days</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Graph Container */}
|
||||||
|
<div className="relative h-64 w-full mb-8">
|
||||||
|
{/* Grid */}
|
||||||
|
<div className="absolute inset-0 flex flex-col justify-between">
|
||||||
|
{[0,1,2,3].map(i => <div key={i} className="w-full h-px bg-white/5 border-t border-dashed border-white/10" />)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<svg className="absolute inset-0 w-full h-full overflow-visible">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="chartGradient" x1="0" y1="0" x2="0" y2="1">
|
||||||
|
<stop offset="0%" stopColor="#22d3ee" stopOpacity="0.2" />
|
||||||
|
<stop offset="100%" stopColor="#22d3ee" stopOpacity="0" />
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
{/* Trend Weight Area */}
|
||||||
|
<motion.path
|
||||||
|
d="M0,170 C50,165 100,160 150,150 C200,140 250,135 320,120 V250 H0 Z"
|
||||||
|
fill="url(#chartGradient)"
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
transition={{ duration: 1 }}
|
||||||
|
/>
|
||||||
|
{/* Trend Weight Line */}
|
||||||
|
<motion.path
|
||||||
|
d="M0,170 C50,165 100,160 150,150 C200,140 250,135 320,120"
|
||||||
|
fill="none"
|
||||||
|
stroke="#22d3ee"
|
||||||
|
strokeWidth="3"
|
||||||
|
initial={{ pathLength: 0 }}
|
||||||
|
animate={{ pathLength: 1 }}
|
||||||
|
transition={{ duration: 1.5, ease: "easeOut" }}
|
||||||
|
/>
|
||||||
|
{/* Scale Weight Dots */}
|
||||||
|
{[
|
||||||
|
{cx: 20, cy: 175}, {cx: 60, cy: 160}, {cx: 100, cy: 168},
|
||||||
|
{cx: 140, cy: 145}, {cx: 180, cy: 155}, {cx: 220, cy: 130}, {cx: 260, cy: 140}, {cx: 300, cy: 115}
|
||||||
|
].map((dot, i) => (
|
||||||
|
<motion.circle
|
||||||
|
key={i} cx={dot.cx} cy={dot.cy} r="3" fill="#525252"
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
transition={{ delay: 0.1 * i + 1 }}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Insight Card */}
|
||||||
|
<div className="bg-[#1C1C1E] rounded-2xl p-4 flex items-center gap-4 border border-white/5">
|
||||||
|
<div className="w-10 h-10 rounded-full bg-green-500/10 flex items-center justify-center text-green-500">
|
||||||
|
<TrendingUp size={20} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-[13px] font-medium text-gray-400">Weekly Adherence</div>
|
||||||
|
<div className="text-lg font-bold text-white">94% <span className="text-gray-600 text-sm font-normal ml-1">on track</span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</PhoneFrame>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const PlusIcon = () => (
|
||||||
|
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M7 0V14M0 7H14" stroke="currentColor" strokeWidth="2" strokeLinecap="round"/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
37
components/Audience.tsx
Normal file
37
components/Audience.tsx
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Cpu, Compass, Target } from 'lucide-react';
|
||||||
|
|
||||||
|
export const Audience: React.FC = () => {
|
||||||
|
return (
|
||||||
|
<section className="py-32 bg-background relative">
|
||||||
|
<div className="max-w-4xl mx-auto px-4 text-center">
|
||||||
|
<div className="flex justify-center mb-12">
|
||||||
|
<div className="p-4 rounded-2xl bg-white/5 border border-white/10">
|
||||||
|
<Cpu className="w-12 h-12 text-gray-400" strokeWidth={1} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<h2 className="text-4xl md:text-7xl font-bold text-white mb-8 tracking-tighter leading-tight">
|
||||||
|
Built for <br/>
|
||||||
|
<span className="text-transparent bg-clip-text bg-gradient-to-r from-white to-gray-600">Systems Thinkers</span>.
|
||||||
|
</h2>
|
||||||
|
<p className="text-xl md:text-2xl text-gray-400 mb-16 max-w-2xl mx-auto leading-relaxed">
|
||||||
|
You treat your body like an engineering project. We handle the fuel calculations so you can just <span className="text-white font-semibold">fly the plane</span>.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-3 gap-6 text-left max-w-3xl mx-auto">
|
||||||
|
{[
|
||||||
|
{ icon: Target, title: "Precision", desc: "No vague advice. Specific macronutrient targets based on exertion." },
|
||||||
|
{ icon: Compass, title: "Direction", desc: "Always know the next move. Remove decision fatigue from your diet." },
|
||||||
|
{ icon: Cpu, title: "Automation", desc: "Algorithms that adapt to your metabolism over time." }
|
||||||
|
].map((item, i) => (
|
||||||
|
<div key={i} className="p-8 rounded-3xl bg-surfaceHighlight border border-white/5 hover:border-white/20 transition-all duration-300 group">
|
||||||
|
<item.icon className="w-8 h-8 text-gray-500 group-hover:text-primary-400 transition-colors mb-6" strokeWidth={1.5} />
|
||||||
|
<h3 className="text-white text-lg font-bold mb-3">{item.title}</h3>
|
||||||
|
<p className="text-sm text-gray-400 leading-relaxed">{item.desc}</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
146
components/ChatDemo.tsx
Normal file
146
components/ChatDemo.tsx
Normal file
@ -0,0 +1,146 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { motion, AnimatePresence } from 'framer-motion';
|
||||||
|
|
||||||
|
const SEQUENCE = [
|
||||||
|
{ type: 'user', text: "I messed up. Ate half a pizza. What now?", delay: 500 },
|
||||||
|
{ type: 'thinking', duration: 1500 },
|
||||||
|
{ type: 'ai', text: "No problem. That happens.", delay: 500 },
|
||||||
|
{ type: 'ai', text: "I've adjusted tomorrow's plan: we'll drop fats slightly (-15g) to balance the weekly average. Enjoy your night! 🌙", delay: 1000 },
|
||||||
|
{ type: 'reset', delay: 4000 }
|
||||||
|
];
|
||||||
|
|
||||||
|
export const ChatDemo: React.FC = () => {
|
||||||
|
const [messages, setMessages] = useState<any[]>([]);
|
||||||
|
const [isTyping, setIsTyping] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Use ReturnType<typeof setTimeout> to ensure compatibility with both browser (number) and Node (Timeout) environments without requiring @types/node
|
||||||
|
let timeoutIds: ReturnType<typeof setTimeout>[] = [];
|
||||||
|
let mounted = true;
|
||||||
|
|
||||||
|
const runSequence = async () => {
|
||||||
|
if (!mounted) return;
|
||||||
|
setMessages([]);
|
||||||
|
setIsTyping(false);
|
||||||
|
|
||||||
|
let accumulatedDelay = 0;
|
||||||
|
|
||||||
|
SEQUENCE.forEach((step) => {
|
||||||
|
accumulatedDelay += (step.delay || 0) + (step.duration || 0);
|
||||||
|
|
||||||
|
const id = setTimeout(() => {
|
||||||
|
if (!mounted) return;
|
||||||
|
|
||||||
|
if (step.type === 'reset') {
|
||||||
|
runSequence();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (step.type === 'thinking') {
|
||||||
|
setIsTyping(true);
|
||||||
|
setTimeout(() => { if(mounted) setIsTyping(false) }, step.duration);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (step.type === 'user' || step.type === 'ai') {
|
||||||
|
setMessages(prev => [...prev, { id: Date.now(), role: step.type, text: step.text }]);
|
||||||
|
}
|
||||||
|
|
||||||
|
}, accumulatedDelay);
|
||||||
|
|
||||||
|
timeoutIds.push(id);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
runSequence();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
mounted = false;
|
||||||
|
timeoutIds.forEach(clearTimeout);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="py-32 bg-background border-y border-white/5 relative overflow-hidden">
|
||||||
|
{/* Ambient Glow */}
|
||||||
|
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[800px] h-[400px] bg-primary-500/5 blur-[120px] rounded-full pointer-events-none" />
|
||||||
|
|
||||||
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
|
<div className="grid lg:grid-cols-2 gap-16 items-center">
|
||||||
|
|
||||||
|
{/* Copy */}
|
||||||
|
<div>
|
||||||
|
<div className="inline-flex items-center gap-2 px-4 py-1.5 rounded-full bg-white/5 border border-white/10 mb-8">
|
||||||
|
<span className="text-xs font-bold text-white tracking-wide uppercase">
|
||||||
|
Judgment-Free Zone
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<h2 className="text-4xl md:text-6xl font-bold text-white mb-8 tracking-tighter">
|
||||||
|
A missed meal isn't a moral failure. <br/>
|
||||||
|
<span className="text-gray-600">It's just data.</span>
|
||||||
|
</h2>
|
||||||
|
<p className="text-lg text-gray-400 mb-8 leading-relaxed max-w-md">
|
||||||
|
Life happens. FitMate's <strong className="text-white">Rolling Adherence Buffer</strong> catches you when you fall.
|
||||||
|
The system simply recalculates the optimal path forward using the remaining days in your week.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Chat Simulation */}
|
||||||
|
<div className="relative mx-auto w-full max-w-md">
|
||||||
|
<div className="bg-surface border border-white/10 rounded-3xl shadow-2xl overflow-hidden min-h-[500px] flex flex-col relative z-10">
|
||||||
|
|
||||||
|
{/* Header */}
|
||||||
|
<div className="px-6 py-5 border-b border-white/5 bg-surfaceHighlight/50 backdrop-blur flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="w-2 h-2 rounded-full bg-green-500 animate-pulse" />
|
||||||
|
<span className="text-sm font-bold text-white">FitMate Co-Pilot</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Messages Area */}
|
||||||
|
<div className="flex-1 p-6 space-y-4 flex flex-col justify-end">
|
||||||
|
<AnimatePresence mode='popLayout'>
|
||||||
|
{messages.map((msg) => (
|
||||||
|
<motion.div
|
||||||
|
key={msg.id}
|
||||||
|
initial={{ opacity: 0, y: 20, scale: 0.9 }}
|
||||||
|
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||||
|
layout
|
||||||
|
className={`flex ${msg.role === 'user' ? 'justify-end' : 'justify-start'}`}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={`max-w-[85%] p-4 rounded-2xl text-sm leading-relaxed shadow-sm ${
|
||||||
|
msg.role === 'user'
|
||||||
|
? 'bg-white text-black rounded-tr-sm font-medium'
|
||||||
|
: 'bg-white/10 text-gray-200 rounded-tl-sm backdrop-blur-md'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{msg.text}
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</AnimatePresence>
|
||||||
|
|
||||||
|
{/* Typing Indicator */}
|
||||||
|
{isTyping && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
className="flex justify-start"
|
||||||
|
>
|
||||||
|
<div className="bg-white/5 px-4 py-3 rounded-2xl rounded-tl-sm flex gap-1">
|
||||||
|
<span className="w-1.5 h-1.5 bg-gray-500 rounded-full animate-bounce" style={{ animationDelay: '0ms' }}/>
|
||||||
|
<span className="w-1.5 h-1.5 bg-gray-500 rounded-full animate-bounce" style={{ animationDelay: '150ms' }}/>
|
||||||
|
<span className="w-1.5 h-1.5 bg-gray-500 rounded-full animate-bounce" style={{ animationDelay: '300ms' }}/>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
66
components/FAQ.tsx
Normal file
66
components/FAQ.tsx
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { motion, AnimatePresence } from 'framer-motion';
|
||||||
|
import { Plus, Minus } from 'lucide-react';
|
||||||
|
|
||||||
|
const QUESTIONS = [
|
||||||
|
{
|
||||||
|
q: "Do I have to track calories forever?",
|
||||||
|
a: "No. You track to learn the system. Many users track strictly for 8-12 weeks to calibrate their eye, then transition to intuitive eating using the app only for high-level guidance or audits."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
q: "Does it work for cutting and bulking?",
|
||||||
|
a: "Yes. FitMate manages the surplus or deficit dynamically based on your rate of weight change (Trend Weight). If you're bulking too fast (gaining fat), it pulls back calories. If you're stalling on a cut, it adjusts down."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
q: "How does the Adherence Buffer work?",
|
||||||
|
a: "If you overeat by 600 calories on Friday, FitMate doesn't shame you. It simply spreads that surplus as a small reduction (-200 kcal) over the next 3 days to keep your weekly average on target."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
q: "Can I use it with intermittent fasting?",
|
||||||
|
a: "Absolutely. The app focuses on daily macronutrient totals. Whether you eat them in a 4-hour window or an 12-hour window is up to your preference."
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
export const FAQ: React.FC = () => {
|
||||||
|
const [openIndex, setOpenIndex] = useState<number | null>(null);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section id="faq" className="py-24 bg-black border-t border-white/5 scroll-mt-32">
|
||||||
|
<div className="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
|
<h2 className="text-3xl md:text-4xl font-bold text-white mb-12 text-center tracking-tight">
|
||||||
|
Frequently Asked Questions
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
{QUESTIONS.map((item, idx) => (
|
||||||
|
<div key={idx} className="border border-white/10 rounded-2xl bg-surfaceHighlight/30 overflow-hidden">
|
||||||
|
<button
|
||||||
|
onClick={() => setOpenIndex(openIndex === idx ? null : idx)}
|
||||||
|
className="w-full flex items-center justify-between p-6 text-left focus:outline-none"
|
||||||
|
>
|
||||||
|
<span className="font-semibold text-white text-lg">{item.q}</span>
|
||||||
|
<span className="text-primary-400">
|
||||||
|
{openIndex === idx ? <Minus size={20} /> : <Plus size={20} />}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
<AnimatePresence>
|
||||||
|
{openIndex === idx && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ height: 0, opacity: 0 }}
|
||||||
|
animate={{ height: "auto", opacity: 1 }}
|
||||||
|
exit={{ height: 0, opacity: 0 }}
|
||||||
|
transition={{ duration: 0.3, ease: "easeInOut" }}
|
||||||
|
>
|
||||||
|
<div className="px-6 pb-6 text-gray-400 leading-relaxed border-t border-white/5 pt-4">
|
||||||
|
{item.a}
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
102
components/Features.tsx
Normal file
102
components/Features.tsx
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
import React, { useRef, useState } from 'react';
|
||||||
|
import { Brain, LineChart, Mic, CalendarClock } from 'lucide-react';
|
||||||
|
import { FeatureItem } from '../types';
|
||||||
|
|
||||||
|
const FEATURES: FeatureItem[] = [
|
||||||
|
{
|
||||||
|
title: "The Performance Co-Pilot",
|
||||||
|
description: "Training-aware guidance. We tell you to eat more carbs on Leg Day and prioritize protein on Rest Days.",
|
||||||
|
icon: Brain,
|
||||||
|
className: "md:col-span-2 md:row-span-2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Day 1 Action Plan",
|
||||||
|
description: "No 'learning phase.' Get an intelligent, customized plan immediately.",
|
||||||
|
icon: CalendarClock,
|
||||||
|
className: "md:col-span-1 md:row-span-1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Purposeful Logging",
|
||||||
|
description: "Log via photo, voice, or text. Inputs act as commands to the Co-Pilot.",
|
||||||
|
icon: Mic,
|
||||||
|
className: "md:col-span-1 md:row-span-1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Forward-Looking Dashboard",
|
||||||
|
description: "See how 92% consistency translates to trend weight changes.",
|
||||||
|
icon: LineChart,
|
||||||
|
className: "md:col-span-3 lg:col-span-2"
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const SpotlightCard: React.FC<{ feature: FeatureItem }> = ({ feature }) => {
|
||||||
|
const divRef = useRef<HTMLDivElement>(null);
|
||||||
|
const [position, setPosition] = useState({ x: 0, y: 0 });
|
||||||
|
const [opacity, setOpacity] = useState(0);
|
||||||
|
|
||||||
|
const handleMouseMove = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||||
|
if (!divRef.current) return;
|
||||||
|
const rect = divRef.current.getBoundingClientRect();
|
||||||
|
setPosition({ x: e.clientX - rect.left, y: e.clientY - rect.top });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseEnter = () => setOpacity(1);
|
||||||
|
const handleMouseLeave = () => setOpacity(0);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={divRef}
|
||||||
|
onMouseMove={handleMouseMove}
|
||||||
|
onMouseEnter={handleMouseEnter}
|
||||||
|
onMouseLeave={handleMouseLeave}
|
||||||
|
className={`relative rounded-3xl border border-white/10 bg-black overflow-hidden group ${feature.className}`}
|
||||||
|
>
|
||||||
|
{/* Spotlight Gradient */}
|
||||||
|
<div
|
||||||
|
className="pointer-events-none absolute -inset-px transition duration-300 opacity-0 group-hover:opacity-100"
|
||||||
|
style={{
|
||||||
|
background: `radial-gradient(600px circle at ${position.x}px ${position.y}px, rgba(255,255,255,0.1), transparent 40%)`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Content Container */}
|
||||||
|
<div className="relative h-full p-8 flex flex-col bg-surface/50 backdrop-blur-xl z-10 m-[1px] rounded-[23px]">
|
||||||
|
<div className="mb-6 w-12 h-12 rounded-2xl bg-white/5 border border-white/10 flex items-center justify-center text-primary-400 group-hover:scale-110 transition-transform duration-300">
|
||||||
|
<feature.icon size={24} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-auto">
|
||||||
|
<h3 className="text-xl font-bold text-white mb-3 tracking-tight">
|
||||||
|
{feature.title}
|
||||||
|
</h3>
|
||||||
|
<p className="text-gray-400 text-sm leading-relaxed">
|
||||||
|
{feature.description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Features: React.FC = () => {
|
||||||
|
return (
|
||||||
|
<section id="features" className="py-32 bg-black relative scroll-mt-32">
|
||||||
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
|
<div className="mb-20 text-center max-w-3xl mx-auto">
|
||||||
|
<h2 className="text-4xl md:text-6xl font-bold text-white mb-6 tracking-tighter">
|
||||||
|
Engineered for <span className="text-transparent bg-clip-text bg-gradient-to-r from-primary-400 to-blue-500">Results</span>
|
||||||
|
</h2>
|
||||||
|
<p className="text-gray-400 text-lg">
|
||||||
|
A suite of tools designed to minimize friction and maximize adherence.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-4 gap-4 lg:gap-6 auto-rows-[minmax(280px,auto)]">
|
||||||
|
{FEATURES.map((feature, idx) => (
|
||||||
|
<SpotlightCard key={idx} feature={feature} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
59
components/Footer.tsx
Normal file
59
components/Footer.tsx
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Activity, Twitter, Instagram, Github } from 'lucide-react';
|
||||||
|
|
||||||
|
export const Footer: React.FC = () => {
|
||||||
|
return (
|
||||||
|
<footer className="bg-surface border-t border-white/5 pt-16 pb-8">
|
||||||
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-8 mb-12">
|
||||||
|
<div className="col-span-2 md:col-span-1">
|
||||||
|
<div className="flex items-center gap-2 mb-4">
|
||||||
|
<Activity className="text-primary-500 w-6 h-6" />
|
||||||
|
<span className="text-xl font-bold text-white">FitMate</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-gray-500 text-sm">
|
||||||
|
The proactive nutrition co-pilot for high performers.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h4 className="text-white font-semibold mb-4">Product</h4>
|
||||||
|
<ul className="space-y-2 text-sm text-gray-400">
|
||||||
|
<li><a href="#" className="hover:text-primary-400 transition-colors">Features</a></li>
|
||||||
|
<li><a href="#" className="hover:text-primary-400 transition-colors">Pricing</a></li>
|
||||||
|
<li><a href="#" className="hover:text-primary-400 transition-colors">Methodology</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h4 className="text-white font-semibold mb-4">Company</h4>
|
||||||
|
<ul className="space-y-2 text-sm text-gray-400">
|
||||||
|
<li><a href="#" className="hover:text-primary-400 transition-colors">About</a></li>
|
||||||
|
<li><a href="#" className="hover:text-primary-400 transition-colors">Blog</a></li>
|
||||||
|
<li><a href="#" className="hover:text-primary-400 transition-colors">Careers</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h4 className="text-white font-semibold mb-4">Legal</h4>
|
||||||
|
<ul className="space-y-2 text-sm text-gray-400">
|
||||||
|
<li><a href="#" className="hover:text-primary-400 transition-colors">Privacy</a></li>
|
||||||
|
<li><a href="#" className="hover:text-primary-400 transition-colors">Terms</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="border-t border-white/5 pt-8 flex flex-col md:flex-row justify-between items-center gap-4">
|
||||||
|
<p className="text-gray-600 text-sm">
|
||||||
|
© {new Date().getFullYear()} FitMate Inc. All rights reserved.
|
||||||
|
</p>
|
||||||
|
<div className="flex gap-6">
|
||||||
|
<a href="#" className="text-gray-500 hover:text-white transition-colors"><Twitter size={20} /></a>
|
||||||
|
<a href="#" className="text-gray-500 hover:text-white transition-colors"><Instagram size={20} /></a>
|
||||||
|
<a href="#" className="text-gray-500 hover:text-white transition-colors"><Github size={20} /></a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
);
|
||||||
|
};
|
||||||
208
components/Hero.tsx
Normal file
208
components/Hero.tsx
Normal file
@ -0,0 +1,208 @@
|
|||||||
|
import React, { useRef } from 'react';
|
||||||
|
import { motion, useScroll, useTransform } from 'framer-motion';
|
||||||
|
import { ChevronRight, Zap, Activity, User } from 'lucide-react';
|
||||||
|
import { PhoneFrame } from './PhoneFrame';
|
||||||
|
|
||||||
|
export const Hero: React.FC = () => {
|
||||||
|
const targetRef = useRef(null);
|
||||||
|
const { scrollYProgress } = useScroll({
|
||||||
|
target: targetRef,
|
||||||
|
offset: ["start start", "end start"]
|
||||||
|
});
|
||||||
|
|
||||||
|
const y = useTransform(scrollYProgress, [0, 1], [0, 150]);
|
||||||
|
const opacity = useTransform(scrollYProgress, [0, 0.5], [1, 0]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section ref={targetRef} className="relative min-h-screen pt-32 pb-20 lg:pt-40 overflow-hidden flex flex-col justify-center items-center">
|
||||||
|
|
||||||
|
{/* Ambient Background */}
|
||||||
|
<div className="absolute top-0 left-1/2 -translate-x-1/2 w-full h-[1000px] bg-gradient-radial from-primary-500/10 via-transparent to-transparent opacity-50 pointer-events-none" />
|
||||||
|
|
||||||
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 relative z-10 w-full">
|
||||||
|
<div className="flex flex-col items-center text-center">
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.6, ease: "easeOut" }}
|
||||||
|
className="mb-8"
|
||||||
|
>
|
||||||
|
<div className="inline-flex items-center gap-2 px-4 py-1.5 rounded-full bg-white/5 border border-white/10 backdrop-blur-md hover:bg-white/10 transition-colors cursor-default">
|
||||||
|
<span className="flex h-2 w-2 rounded-full bg-primary-400 shadow-[0_0_10px_#22d3ee]"></span>
|
||||||
|
<span className="text-sm font-medium text-gray-300 tracking-wide">
|
||||||
|
Now Available on iOS & Android
|
||||||
|
</span>
|
||||||
|
<ChevronRight className="w-3 h-3 text-gray-500" />
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<h1 className="text-6xl sm:text-7xl lg:text-8xl font-extrabold tracking-tighter text-white mb-8 leading-[1] max-w-5xl">
|
||||||
|
<motion.span
|
||||||
|
initial={{ opacity: 0, y: 40 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.8, delay: 0.1 }}
|
||||||
|
className="block"
|
||||||
|
>
|
||||||
|
Make Every
|
||||||
|
</motion.span>
|
||||||
|
<motion.span
|
||||||
|
initial={{ opacity: 0, y: 40 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.8, delay: 0.2 }}
|
||||||
|
className="block text-transparent bg-clip-text bg-gradient-to-r from-white via-gray-200 to-gray-400"
|
||||||
|
>
|
||||||
|
Workout Count.
|
||||||
|
</motion.span>
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<motion.p
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.8, delay: 0.4 }}
|
||||||
|
className="text-xl md:text-2xl text-gray-400 mb-12 leading-relaxed max-w-2xl"
|
||||||
|
>
|
||||||
|
Stop using rearview mirrors. FitMate is your <span className="text-primary-400">GPS</span>.
|
||||||
|
We turn training data into a shame-free game plan for your <em>next</em> meal.
|
||||||
|
</motion.p>
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.8, delay: 0.5 }}
|
||||||
|
className="flex flex-col sm:flex-row items-center gap-4 mb-16"
|
||||||
|
>
|
||||||
|
{/* App Store Button */}
|
||||||
|
<button
|
||||||
|
onClick={() => alert('Redirecting to App Store...')}
|
||||||
|
className="group flex items-center gap-3 bg-white text-black px-6 py-3 rounded-xl hover:scale-105 transition-all duration-300 shadow-[0_0_40px_rgba(255,255,255,0.2)] cursor-pointer"
|
||||||
|
>
|
||||||
|
<svg className="w-6 h-6" viewBox="0 0 24 24" fill="currentColor">
|
||||||
|
<path d="M18.71 19.5c-.83 1.24-1.71 2.45-3.05 2.47-1.34.03-1.77-.79-3.29-.79-1.53 0-2 .77-3.27.82-1.31.05-2.3-1.3-3.14-2.53-2.14-3.08-2.67-7.36-1.97-10.32 1-4.24 3.7-6.76 6.7-1.48.68-1.26.68-1.44.68-1.44.68.85 1.51.85 3.5.7 4.98.82 2.38-.03 3.22-1.33 3.22-1.33.94 2.66 4.41 2.66 4.41s-1.71 3.73-2.62 5.09zM12 5.31c-.77 1.35-2.76 1.78-4.09 1.6-.57-2.02.6-4.24 2.38-5.11 1.78-.87 3.77-.9 4.07 1.61.03.65.03.65-2.36 1.9z"/>
|
||||||
|
</svg>
|
||||||
|
<div className="text-left">
|
||||||
|
<div className="text-[10px] font-medium uppercase tracking-wide opacity-80">Download on the</div>
|
||||||
|
<div className="text-sm font-bold leading-none">App Store</div>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Google Play Button */}
|
||||||
|
<button
|
||||||
|
onClick={() => alert('Redirecting to Play Store...')}
|
||||||
|
className="group flex items-center gap-3 bg-white/5 border border-white/10 text-white px-6 py-3 rounded-xl hover:bg-white/10 hover:scale-105 transition-all duration-300 backdrop-blur-sm cursor-pointer"
|
||||||
|
>
|
||||||
|
<svg className="w-6 h-6" viewBox="0 0 24 24" fill="currentColor">
|
||||||
|
<path d="M3,20.5V3.5C3,2.91,3.34,2.39,3.84,2.15L13.69,12L3.84,21.85C3.34,21.6,3,21.09,3,20.5M16.81,15.12L6.05,21.34L14.54,12.85L16.81,15.12M20.16,10.81C20.5,11.08,20.75,11.5,20.75,12C20.75,12.5,20.53,12.92,20.16,13.19L17.89,14.5L15.39,12L17.89,9.5L20.16,10.81M6.05,2.66L16.81,8.88L14.54,11.15L6.05,2.66Z" />
|
||||||
|
</svg>
|
||||||
|
<div className="text-left">
|
||||||
|
<div className="text-[10px] font-medium uppercase tracking-wide opacity-60">Get it on</div>
|
||||||
|
<div className="text-sm font-bold leading-none">Google Play</div>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Parallax Phone Mockup */}
|
||||||
|
<motion.div
|
||||||
|
style={{ y, opacity }}
|
||||||
|
className="mt-4 relative w-full max-w-[340px] mx-auto perspective-1000"
|
||||||
|
>
|
||||||
|
{/* Glow behind phone */}
|
||||||
|
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[120%] h-[80%] bg-primary-500/30 blur-[120px] rounded-full" />
|
||||||
|
|
||||||
|
<PhoneFrame className="h-[700px]">
|
||||||
|
<div className="h-full w-full pt-14 pb-8 px-6 flex flex-col relative z-10 bg-black">
|
||||||
|
|
||||||
|
{/* Header */}
|
||||||
|
<div className="flex justify-between items-end mb-6">
|
||||||
|
<div>
|
||||||
|
<div className="text-gray-400 text-xs font-semibold uppercase tracking-wide">Wednesday</div>
|
||||||
|
<h1 className="text-3xl font-bold text-white tracking-tight">Leg Day <span className="text-primary-500">.</span></h1>
|
||||||
|
</div>
|
||||||
|
<div className="w-9 h-9 rounded-full bg-[#2C2C2E] flex items-center justify-center border border-white/10">
|
||||||
|
<User size={16} className="text-gray-400" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Segmented Control Lookalike */}
|
||||||
|
<div className="bg-[#1C1C1E] p-1 rounded-lg mb-8 flex relative">
|
||||||
|
<div className="absolute top-1 bottom-1 left-1 w-[calc(50%-4px)] bg-[#636366] rounded-[6px] shadow-sm z-0" />
|
||||||
|
<div className="flex-1 py-1.5 text-[13px] font-semibold rounded-md relative z-10 text-center text-white">
|
||||||
|
Training Day
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 py-1.5 text-[13px] font-semibold rounded-md relative z-10 text-center text-gray-400">
|
||||||
|
Rest Day
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Main Ring */}
|
||||||
|
<div className="flex justify-center mb-8">
|
||||||
|
<div className="relative w-52 h-52 flex items-center justify-center">
|
||||||
|
{/* Static SVG for Hero to save perf */}
|
||||||
|
<svg className="w-full h-full transform -rotate-90 drop-shadow-2xl">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="heroGradient" x1="0%" y1="0%" x2="100%" y2="0%">
|
||||||
|
<stop offset="0%" stopColor="#06b6d4" />
|
||||||
|
<stop offset="100%" stopColor="#22d3ee" />
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<circle cx="104" cy="104" r="84" stroke="#1C1C1E" strokeWidth="18" fill="none" />
|
||||||
|
<circle
|
||||||
|
cx="104" cy="104" r="84"
|
||||||
|
stroke="url(#heroGradient)"
|
||||||
|
strokeWidth="18"
|
||||||
|
fill="none"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeDasharray="420 527"
|
||||||
|
className="filter drop-shadow-[0_0_10px_rgba(34,211,238,0.3)]"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<div className="absolute inset-0 flex flex-col items-center justify-center">
|
||||||
|
<div className="text-5xl font-bold text-white tracking-tighter font-sans">2,850</div>
|
||||||
|
<div className="text-primary-400 text-sm font-semibold uppercase tracking-wide mt-1">kcal Left</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Cards */}
|
||||||
|
<div className="grid grid-cols-2 gap-4 mb-4">
|
||||||
|
<div className="bg-[#1C1C1E] rounded-2xl p-4 relative overflow-hidden border border-white/5">
|
||||||
|
<div className="absolute top-0 right-0 p-2 opacity-20"><Activity /></div>
|
||||||
|
<div className="text-[13px] font-medium text-gray-400 mb-1">Carbs</div>
|
||||||
|
<div className="text-2xl font-bold text-white mb-2">320g</div>
|
||||||
|
<div className="w-full bg-gray-800 h-1.5 rounded-full overflow-hidden">
|
||||||
|
<div className="h-full bg-primary-500 w-[85%]" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="bg-[#1C1C1E] rounded-2xl p-4 relative overflow-hidden border border-white/5">
|
||||||
|
<div className="absolute top-0 right-0 p-2 opacity-20"><Zap /></div>
|
||||||
|
<div className="text-[13px] font-medium text-gray-400 mb-1">Protein</div>
|
||||||
|
<div className="text-2xl font-bold text-white mb-2">200g</div>
|
||||||
|
<div className="w-full bg-gray-800 h-1.5 rounded-full overflow-hidden">
|
||||||
|
<div className="h-full bg-purple-500 w-[65%]" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Insight Card */}
|
||||||
|
<div className="bg-gradient-to-br from-[#1C1C1E] to-[#111] border border-white/5 rounded-2xl p-4 flex-1 min-h-0 relative overflow-hidden">
|
||||||
|
<div className="absolute top-0 left-0 w-full h-1 bg-gradient-to-r from-primary-500 to-transparent" />
|
||||||
|
<div className="flex items-center gap-2 mb-3">
|
||||||
|
<div className="p-1.5 bg-primary-500/10 rounded-lg text-primary-400">
|
||||||
|
<Zap size={14} fill="currentColor" />
|
||||||
|
</div>
|
||||||
|
<span className="text-sm font-bold text-white">Co-Pilot Insight</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-[13px] text-gray-300 leading-relaxed">
|
||||||
|
Squat volume is high. I've increased pre-workout carbs by 40g. Drink 500ml water now.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</PhoneFrame>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
111
components/Logic.tsx
Normal file
111
components/Logic.tsx
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
import React, { useRef } from 'react';
|
||||||
|
import { motion, useScroll, useTransform } from 'framer-motion';
|
||||||
|
import { Dumbbell, Cpu, Utensils, TrendingUp } from 'lucide-react';
|
||||||
|
|
||||||
|
const STEPS = [
|
||||||
|
{
|
||||||
|
icon: Dumbbell,
|
||||||
|
title: "Input",
|
||||||
|
subtitle: "Log Training",
|
||||||
|
description: "You log your workout (e.g., Heavy Leg Day, 90 min).",
|
||||||
|
color: "bg-blue-500"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: Cpu,
|
||||||
|
title: "Process",
|
||||||
|
subtitle: "Analysis",
|
||||||
|
description: "The Co-Pilot analyzes your metabolic state and recovery needs.",
|
||||||
|
color: "bg-purple-500"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: Utensils,
|
||||||
|
title: "Output",
|
||||||
|
subtitle: "The Plan",
|
||||||
|
description: "You get a specific food target: 'Eat 60g Carbs + 40g Protein now.'",
|
||||||
|
color: "bg-primary-500"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: TrendingUp,
|
||||||
|
title: "Result",
|
||||||
|
subtitle: "Optimization",
|
||||||
|
description: "You wake up stronger, not fatter. Performance is prioritized.",
|
||||||
|
color: "bg-green-500"
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
export const Logic: React.FC = () => {
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const { scrollYProgress } = useScroll({
|
||||||
|
target: containerRef,
|
||||||
|
offset: ["start end", "end start"]
|
||||||
|
});
|
||||||
|
|
||||||
|
const lineHeight = useTransform(scrollYProgress, [0, 0.8], ["0%", "100%"]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section id="logic" className="py-32 bg-black relative overflow-hidden scroll-mt-32">
|
||||||
|
{/* Background Effects */}
|
||||||
|
<div className="absolute inset-0 bg-[radial-gradient(circle_at_top_right,_var(--tw-gradient-stops))] from-primary-900/10 via-black to-black pointer-events-none" />
|
||||||
|
|
||||||
|
<div className="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8 relative z-10">
|
||||||
|
<div className="text-center mb-20">
|
||||||
|
<h2 className="text-4xl md:text-6xl font-bold text-white mb-6 tracking-tighter">
|
||||||
|
The Logic
|
||||||
|
</h2>
|
||||||
|
<p className="text-xl text-gray-400 max-w-2xl mx-auto">
|
||||||
|
How FitMate turns your sweat into data, and data into direction.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div ref={containerRef} className="relative">
|
||||||
|
{/* Connecting Line */}
|
||||||
|
<div className="absolute left-[28px] md:left-1/2 top-0 bottom-0 w-0.5 bg-white/10 md:-translate-x-1/2" />
|
||||||
|
<motion.div
|
||||||
|
style={{ height: lineHeight }}
|
||||||
|
className="absolute left-[28px] md:left-1/2 top-0 w-0.5 bg-gradient-to-b from-primary-500 via-purple-500 to-primary-500 md:-translate-x-1/2 shadow-[0_0_15px_rgba(34,211,238,0.5)]"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="space-y-12 md:space-y-24">
|
||||||
|
{STEPS.map((step, idx) => (
|
||||||
|
<motion.div
|
||||||
|
key={idx}
|
||||||
|
initial={{ opacity: 0, y: 50 }}
|
||||||
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
|
viewport={{ once: true, margin: "-100px" }}
|
||||||
|
transition={{ duration: 0.6, delay: idx * 0.1 }}
|
||||||
|
className={`relative flex flex-col md:flex-row gap-8 md:gap-16 items-start md:items-center ${idx % 2 === 0 ? 'md:flex-row-reverse' : ''}`}
|
||||||
|
>
|
||||||
|
{/* Content Card */}
|
||||||
|
<div className="flex-1 ml-16 md:ml-0 w-full md:w-auto">
|
||||||
|
<div className={`bg-surfaceHighlight/50 border border-white/10 p-6 rounded-2xl backdrop-blur-sm hover:border-primary-500/30 transition-colors duration-500 group w-full md:max-w-md ${idx % 2 === 0 ? 'mr-auto' : 'ml-auto'}`}>
|
||||||
|
<div className="flex items-center gap-3 mb-3">
|
||||||
|
<span className="text-xs font-mono text-gray-500 uppercase tracking-widest">Step 0{idx + 1}</span>
|
||||||
|
<span className="h-px flex-1 bg-white/10 group-hover:bg-primary-500/30 transition-colors" />
|
||||||
|
</div>
|
||||||
|
<h3 className="text-2xl font-bold text-white mb-1">{step.subtitle}</h3>
|
||||||
|
<h4 className="text-lg text-gray-400 mb-3">{step.title}</h4>
|
||||||
|
<p className="text-gray-400 leading-relaxed text-sm">
|
||||||
|
{step.description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Icon Marker */}
|
||||||
|
<div className="absolute left-0 md:left-1/2 md:-translate-x-1/2 flex items-center justify-center w-14 h-14 rounded-full bg-black border-4 border-surface z-10">
|
||||||
|
<div className={`w-full h-full rounded-full flex items-center justify-center bg-surfaceHighlight border border-white/10 text-white relative overflow-hidden`}>
|
||||||
|
<div className={`absolute inset-0 opacity-20 ${step.color}`} />
|
||||||
|
<step.icon size={20} className="relative z-10" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Empty spacer for layout balance */}
|
||||||
|
<div className="hidden md:block flex-1" />
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
124
components/Navbar.tsx
Normal file
124
components/Navbar.tsx
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { Menu, X, Activity } from 'lucide-react';
|
||||||
|
import { NavItem } from '../types';
|
||||||
|
|
||||||
|
const NAV_ITEMS: NavItem[] = [
|
||||||
|
{ label: 'Features', href: '#features' },
|
||||||
|
{ label: 'The Logic', href: '#logic' },
|
||||||
|
{ label: 'Reviews', href: '#reviews' },
|
||||||
|
{ label: 'Pricing', href: '#pricing' },
|
||||||
|
{ label: 'FAQ', href: '#faq' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const Navbar: React.FC = () => {
|
||||||
|
const [isScrolled, setIsScrolled] = useState(false);
|
||||||
|
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleScroll = () => {
|
||||||
|
setIsScrolled(window.scrollY > 20);
|
||||||
|
};
|
||||||
|
window.addEventListener('scroll', handleScroll);
|
||||||
|
return () => window.removeEventListener('scroll', handleScroll);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleScrollTo = (id: string) => {
|
||||||
|
setIsMobileMenuOpen(false);
|
||||||
|
const element = document.querySelector(id);
|
||||||
|
if (element) {
|
||||||
|
element.scrollIntoView({ behavior: 'smooth' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<nav
|
||||||
|
className={`fixed top-0 left-0 right-0 z-[100] transition-all duration-500 ${
|
||||||
|
isScrolled
|
||||||
|
? 'bg-black/60 backdrop-blur-xl border-b border-white/5 py-4'
|
||||||
|
: 'bg-transparent border-b border-transparent py-6'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
{/* Logo */}
|
||||||
|
<div className="flex items-center gap-2 cursor-pointer" onClick={() => window.scrollTo({ top: 0, behavior: 'smooth' })}>
|
||||||
|
<div className="w-8 h-8 rounded-lg bg-white text-black flex items-center justify-center shadow-[0_0_15px_rgba(255,255,255,0.3)]">
|
||||||
|
<Activity className="w-5 h-5" strokeWidth={3} />
|
||||||
|
</div>
|
||||||
|
<span className="text-xl font-bold tracking-tight text-white">
|
||||||
|
FitMate
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Desktop Links */}
|
||||||
|
<div className="hidden md:flex items-center gap-8">
|
||||||
|
{NAV_ITEMS.map((item) => (
|
||||||
|
<a
|
||||||
|
key={item.label}
|
||||||
|
href={item.href}
|
||||||
|
className="text-sm font-medium text-gray-400 hover:text-white transition-colors relative group"
|
||||||
|
>
|
||||||
|
{item.label}
|
||||||
|
<span className="absolute -bottom-1 left-0 w-0 h-px bg-primary-400 transition-all duration-300 group-hover:w-full" />
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Desktop CTA */}
|
||||||
|
<div className="hidden md:block">
|
||||||
|
<button
|
||||||
|
onClick={() => handleScrollTo('#pricing')}
|
||||||
|
className="bg-white/10 hover:bg-white text-white hover:text-black border border-white/10 hover:border-white px-6 py-2 rounded-full text-sm font-semibold transition-all duration-300 backdrop-blur-md"
|
||||||
|
>
|
||||||
|
Build Your Plan
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Mobile Toggle */}
|
||||||
|
<div className="md:hidden">
|
||||||
|
<button
|
||||||
|
onClick={() => setIsMobileMenuOpen(!isMobileMenuOpen)}
|
||||||
|
className="text-gray-300 hover:text-white p-2"
|
||||||
|
>
|
||||||
|
{isMobileMenuOpen ? <X size={24} /> : <Menu size={24} />}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Mobile Menu */}
|
||||||
|
{isMobileMenuOpen && (
|
||||||
|
<div className="md:hidden fixed inset-0 bg-black/95 backdrop-blur-2xl z-[101] flex flex-col">
|
||||||
|
<div className="flex justify-end p-6 border-b border-white/10">
|
||||||
|
<button onClick={() => setIsMobileMenuOpen(false)} className="text-white p-2 bg-white/5 rounded-full">
|
||||||
|
<X size={24} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 flex flex-col items-center justify-center space-y-8 px-6">
|
||||||
|
{NAV_ITEMS.map((item) => (
|
||||||
|
<a
|
||||||
|
key={item.label}
|
||||||
|
href={item.href}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
handleScrollTo(item.href);
|
||||||
|
}}
|
||||||
|
className="text-4xl font-bold text-white hover:text-primary-400 transition-colors"
|
||||||
|
>
|
||||||
|
{item.label}
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
<div className="pt-8 w-full max-w-xs">
|
||||||
|
<button
|
||||||
|
onClick={() => handleScrollTo('#pricing')}
|
||||||
|
className="w-full bg-white text-black px-6 py-5 rounded-2xl text-xl font-bold hover:scale-105 transition-transform"
|
||||||
|
>
|
||||||
|
Build Your Plan
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</nav>
|
||||||
|
);
|
||||||
|
};
|
||||||
65
components/PhoneFrame.tsx
Normal file
65
components/PhoneFrame.tsx
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Wifi, Battery, Signal } from 'lucide-react';
|
||||||
|
|
||||||
|
interface PhoneFrameProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
className?: string;
|
||||||
|
isDark?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PhoneFrame: React.FC<PhoneFrameProps> = ({ children, className = '', isDark = true }) => {
|
||||||
|
return (
|
||||||
|
<div className={`relative ${className} select-none`}>
|
||||||
|
{/* Titanium Frame Chassis */}
|
||||||
|
<div className="relative bg-[#2a2a2a] rounded-[3.5rem] p-[2px] shadow-[0_0_0_4px_#1a1a1a,0_20px_50px_-12px_rgba(0,0,0,0.8)] h-full ring-1 ring-white/10">
|
||||||
|
|
||||||
|
{/* Inner Bezel (Black Border) */}
|
||||||
|
<div className="absolute inset-[2px] rounded-[3.3rem] border-[6px] border-black pointer-events-none z-20" />
|
||||||
|
|
||||||
|
{/* Screen Container */}
|
||||||
|
<div className={`bg-black rounded-[3.2rem] overflow-hidden relative h-full flex flex-col w-full mask-image`}>
|
||||||
|
|
||||||
|
{/* iOS Status Bar */}
|
||||||
|
<div className="h-14 w-full absolute top-0 left-0 z-50 px-8 pt-4 flex justify-between items-start text-white pointer-events-none mix-blend-difference">
|
||||||
|
<span className="text-[15px] font-semibold tracking-normal pl-1">9:41</span>
|
||||||
|
|
||||||
|
<div className="flex gap-1.5 items-center pr-1 text-white">
|
||||||
|
<Signal size={15} strokeWidth={2.5} className="fill-current" />
|
||||||
|
<Wifi size={15} strokeWidth={2.5} />
|
||||||
|
<div className="relative ml-1">
|
||||||
|
<Battery size={22} strokeWidth={2} className="text-white/90" />
|
||||||
|
{/* Battery Fill */}
|
||||||
|
<div className="absolute top-1/2 left-[2.5px] -translate-y-1/2 w-[10px] h-[7px] bg-white rounded-[1px]" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Dynamic Island */}
|
||||||
|
<div className="absolute left-1/2 -translate-x-1/2 top-[11px] w-[120px] h-[36px] bg-black rounded-[20px] z-50 flex items-center justify-center pointer-events-none">
|
||||||
|
<div className="flex items-center justify-end w-full h-full relative px-4">
|
||||||
|
{/* Selfie Camera Lens */}
|
||||||
|
<div className="w-3 h-3 rounded-full bg-[#101010] ring-1 ring-white/5 relative overflow-hidden">
|
||||||
|
<div className="absolute inset-0 bg-gradient-to-tr from-blue-900/30 to-transparent" />
|
||||||
|
<div className="absolute top-1 right-1 w-1 h-1 bg-white/10 rounded-full blur-[1px]" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Screen Content */}
|
||||||
|
<div className="relative z-10 h-full w-full flex flex-col bg-black text-white font-sans antialiased">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Home Indicator */}
|
||||||
|
<div className="absolute bottom-2 left-1/2 -translate-x-1/2 w-36 h-1.5 bg-white rounded-full z-50 pointer-events-none mix-blend-exclusion" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Hardware Buttons */}
|
||||||
|
<div className="absolute top-28 -left-[3px] w-[3px] h-8 bg-[#1a1a1a] rounded-l-sm shadow-[-1px_0_2px_rgba(255,255,255,0.1)]" /> {/* Action */}
|
||||||
|
<div className="absolute top-44 -left-[3px] w-[3px] h-16 bg-[#1a1a1a] rounded-l-sm shadow-[-1px_0_2px_rgba(255,255,255,0.1)]" /> {/* Vol Up */}
|
||||||
|
<div className="absolute top-64 -left-[3px] w-[3px] h-16 bg-[#1a1a1a] rounded-l-sm shadow-[-1px_0_2px_rgba(255,255,255,0.1)]" /> {/* Vol Down */}
|
||||||
|
<div className="absolute top-52 -right-[3px] w-[3px] h-24 bg-[#1a1a1a] rounded-r-sm shadow-[1px_0_2px_rgba(255,255,255,0.1)]" /> {/* Power */}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
94
components/Pricing.tsx
Normal file
94
components/Pricing.tsx
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { motion } from 'framer-motion';
|
||||||
|
import { Check } from 'lucide-react';
|
||||||
|
|
||||||
|
export const Pricing: React.FC = () => {
|
||||||
|
return (
|
||||||
|
<section id="pricing" className="py-32 bg-background relative overflow-hidden scroll-mt-32">
|
||||||
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 relative z-10">
|
||||||
|
|
||||||
|
<div className="text-center mb-20">
|
||||||
|
<h2 className="text-4xl md:text-6xl font-bold text-white mb-6 tracking-tighter">
|
||||||
|
Invest in Your System.
|
||||||
|
</h2>
|
||||||
|
<p className="text-xl text-gray-400 max-w-xl mx-auto">
|
||||||
|
Costs less than a single session with a personal trainer.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid md:grid-cols-2 gap-8 max-w-4xl mx-auto">
|
||||||
|
{/* Monthly Plan */}
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, x: -20 }}
|
||||||
|
whileInView={{ opacity: 1, x: 0 }}
|
||||||
|
transition={{ duration: 0.5 }}
|
||||||
|
className="p-8 rounded-3xl bg-surfaceHighlight/30 border border-white/5 backdrop-blur-sm flex flex-col h-full hover:bg-surfaceHighlight/50 transition-all"
|
||||||
|
>
|
||||||
|
<div className="mb-8">
|
||||||
|
<h3 className="text-xl font-medium text-gray-300 mb-2">Monthly</h3>
|
||||||
|
<div className="flex items-baseline gap-1">
|
||||||
|
<span className="text-4xl font-bold text-white">$14.99</span>
|
||||||
|
<span className="text-gray-500">/mo</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ul className="space-y-4 mb-8 flex-1">
|
||||||
|
{["Full AI Co-Pilot Access", "Dynamic Macro Adjustment", "Basic Training Integration", "Community Support"].map((item, i) => (
|
||||||
|
<li key={i} className="flex items-center gap-3 text-gray-400">
|
||||||
|
<Check size={16} className="text-white" />
|
||||||
|
<span>{item}</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
<button
|
||||||
|
onClick={() => alert('Redirecting to checkout...')}
|
||||||
|
className="w-full py-4 rounded-xl bg-white/5 hover:bg-white/10 border border-white/10 text-white font-bold transition-all cursor-pointer"
|
||||||
|
>
|
||||||
|
Start Monthly
|
||||||
|
</button>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
{/* Annual Plan - Highlighted */}
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, x: 20 }}
|
||||||
|
whileInView={{ opacity: 1, x: 0 }}
|
||||||
|
transition={{ duration: 0.5 }}
|
||||||
|
className="relative p-8 rounded-3xl bg-black/40 border border-primary-500/30 backdrop-blur-md flex flex-col h-full shadow-[0_0_40px_rgba(6,182,212,0.1)]"
|
||||||
|
>
|
||||||
|
<div className="absolute top-0 right-0 bg-primary-500 text-black text-xs font-bold px-3 py-1 rounded-bl-xl rounded-tr-2xl">
|
||||||
|
BEST VALUE
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mb-8">
|
||||||
|
<h3 className="text-xl font-medium text-primary-400 mb-2">Annual</h3>
|
||||||
|
<div className="flex items-baseline gap-1">
|
||||||
|
<span className="text-4xl font-bold text-white">$9.99</span>
|
||||||
|
<span className="text-gray-500">/mo</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-gray-500 mt-2">Billed $119.88 yearly</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ul className="space-y-4 mb-8 flex-1">
|
||||||
|
{["Everything in Monthly", "Unlimited Strategy Changes", "Priority Support", "Advanced Trend Analytics"].map((item, i) => (
|
||||||
|
<li key={i} className="flex items-center gap-3 text-gray-300">
|
||||||
|
<div className="bg-primary-500/20 p-0.5 rounded-full">
|
||||||
|
<Check size={14} className="text-primary-400" />
|
||||||
|
</div>
|
||||||
|
<span>{item}</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => alert('Redirecting to checkout...')}
|
||||||
|
className="w-full py-4 rounded-xl bg-primary-500 hover:bg-primary-400 text-black font-bold transition-all shadow-[0_0_20px_rgba(6,182,212,0.2)] cursor-pointer"
|
||||||
|
>
|
||||||
|
Start 14-Day Free Trial
|
||||||
|
</button>
|
||||||
|
<p className="text-center text-xs text-gray-500 mt-4">Cancel anytime.</p>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
118
components/ProblemComparison.tsx
Normal file
118
components/ProblemComparison.tsx
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
import React, { useRef } from 'react';
|
||||||
|
import { motion, useInView } from 'framer-motion';
|
||||||
|
import { XCircle, CheckCircle2 } from 'lucide-react';
|
||||||
|
|
||||||
|
export const ProblemComparison: React.FC = () => {
|
||||||
|
const containerRef = useRef(null);
|
||||||
|
const oldWayRef = useRef(null);
|
||||||
|
const newWayRef = useRef(null);
|
||||||
|
|
||||||
|
// Adjusted margin to make the transition happen exactly when the "New Way" text enters the middle of the screen
|
||||||
|
const isNewWayInView = useInView(newWayRef, { margin: "-40% 0px -40% 0px" });
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section ref={containerRef} className="py-32 bg-background relative border-b border-white/5">
|
||||||
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
|
<div className="grid lg:grid-cols-2 gap-16 lg:gap-24">
|
||||||
|
|
||||||
|
{/* Left Column: Scrollable Text */}
|
||||||
|
<div className="space-y-48 py-12 lg:py-24">
|
||||||
|
{/* Block 1: Old Way */}
|
||||||
|
<div ref={oldWayRef} className={`transition-opacity duration-500 ${isNewWayInView ? 'opacity-30' : 'opacity-100'}`}>
|
||||||
|
<div className="inline-flex items-center gap-2 mb-6 text-red-500 bg-red-500/10 px-4 py-2 rounded-full">
|
||||||
|
<XCircle size={16} />
|
||||||
|
<span className="text-xs font-bold uppercase tracking-wide">The Rearview Mirror</span>
|
||||||
|
</div>
|
||||||
|
<h3 className="text-4xl md:text-5xl font-bold text-white mb-6 tracking-tight">
|
||||||
|
Reactive Logging & <br/>
|
||||||
|
<span className="text-gray-600">Gamified Shame.</span>
|
||||||
|
</h3>
|
||||||
|
<p className="text-xl text-gray-400 leading-relaxed mb-8">
|
||||||
|
Traditional apps tell you what you did wrong after the fact.
|
||||||
|
Seeing red numbers and negative feedback loops kills motivation,
|
||||||
|
turning nutrition into a burden rather than a tool.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Block 2: New Way */}
|
||||||
|
<div ref={newWayRef} className={`transition-opacity duration-500 ${isNewWayInView ? 'opacity-100' : 'opacity-30'}`}>
|
||||||
|
<div className="inline-flex items-center gap-2 mb-6 text-primary-400 bg-primary-500/10 px-4 py-2 rounded-full">
|
||||||
|
<CheckCircle2 size={16} />
|
||||||
|
<span className="text-xs font-bold uppercase tracking-wide">The GPS</span>
|
||||||
|
</div>
|
||||||
|
<h3 className="text-4xl md:text-5xl font-bold text-white mb-6 tracking-tight">
|
||||||
|
Proactive Guidance & <br/>
|
||||||
|
<span className="text-primary-400">Actionable Intelligence.</span>
|
||||||
|
</h3>
|
||||||
|
<p className="text-xl text-gray-400 leading-relaxed mb-8">
|
||||||
|
FitMate tells you what to eat *next*. We use Adherence Buffers to
|
||||||
|
automatically adjust your future meals if you slip up, keeping you
|
||||||
|
on track without the guilt trip.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Right Column: Sticky Visuals */}
|
||||||
|
<div className="hidden lg:block relative h-full">
|
||||||
|
<div className="sticky top-[20vh] h-[60vh] w-full">
|
||||||
|
<div className="relative w-full h-full">
|
||||||
|
|
||||||
|
{/* Old Way Card */}
|
||||||
|
<motion.div
|
||||||
|
animate={{
|
||||||
|
opacity: isNewWayInView ? 0 : 1,
|
||||||
|
scale: isNewWayInView ? 0.9 : 1,
|
||||||
|
filter: isNewWayInView ? "blur(10px)" : "blur(0px)",
|
||||||
|
pointerEvents: isNewWayInView ? "none" : "auto"
|
||||||
|
}}
|
||||||
|
transition={{ duration: 0.5 }}
|
||||||
|
className="absolute inset-0 bg-[#0f0f0f] border border-white/10 rounded-3xl p-8 flex flex-col justify-center items-center text-center overflow-hidden"
|
||||||
|
>
|
||||||
|
<div className="absolute inset-0 bg-red-900/5" />
|
||||||
|
<div className="w-24 h-24 rounded-full bg-red-500/10 flex items-center justify-center mb-8 text-red-500">
|
||||||
|
<XCircle size={48} />
|
||||||
|
</div>
|
||||||
|
<div className="space-y-4 w-full max-w-xs">
|
||||||
|
<div className="h-2 bg-red-900/20 rounded-full w-full overflow-hidden">
|
||||||
|
<div className="h-full bg-red-600 w-[110%]" />
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between text-sm text-red-400">
|
||||||
|
<span>Limit Exceeded</span>
|
||||||
|
<span>+240 kcal</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
{/* New Way Card */}
|
||||||
|
<motion.div
|
||||||
|
animate={{
|
||||||
|
opacity: isNewWayInView ? 1 : 0,
|
||||||
|
scale: isNewWayInView ? 1 : 0.95,
|
||||||
|
y: isNewWayInView ? 0 : 20,
|
||||||
|
filter: isNewWayInView ? "blur(0px)" : "blur(10px)"
|
||||||
|
}}
|
||||||
|
transition={{ duration: 0.5 }}
|
||||||
|
className="absolute inset-0 bg-surfaceHighlight border border-primary-500/20 rounded-3xl p-8 flex flex-col justify-center items-center text-center overflow-hidden shadow-[0_0_50px_rgba(6,182,212,0.15)]"
|
||||||
|
>
|
||||||
|
<div className="absolute inset-0 bg-primary-900/5" />
|
||||||
|
<div className="w-24 h-24 rounded-full bg-primary-500/10 flex items-center justify-center mb-8 text-primary-400">
|
||||||
|
<CheckCircle2 size={48} />
|
||||||
|
</div>
|
||||||
|
<div className="space-y-4 w-full max-w-xs text-left">
|
||||||
|
<div className="bg-black/40 p-4 rounded-xl border border-white/5">
|
||||||
|
<div className="text-xs text-gray-500 mb-1">Recalculating path...</div>
|
||||||
|
<div className="text-sm text-white">Dinner target adjusted:</div>
|
||||||
|
<div className="text-lg font-bold text-primary-400">-15g Fat, +20g Protein</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
81
components/Testimonials.tsx
Normal file
81
components/Testimonials.tsx
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Star, BadgeCheck } from 'lucide-react';
|
||||||
|
|
||||||
|
const REVIEWS = [
|
||||||
|
{
|
||||||
|
name: "Alex Chen",
|
||||||
|
role: "Software Engineer",
|
||||||
|
text: "Finally, an app that understands biology. No red text, just adjustments. I treat my body like a system now, not a moral failing.",
|
||||||
|
rating: 5
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Sarah Jenkins",
|
||||||
|
role: "Product Manager",
|
||||||
|
text: "The Training Day toggle is a game changer for my hypertrophy blocks. I eat 800 calories more on leg days and I'm leaner than ever.",
|
||||||
|
rating: 5
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Marcus Ford",
|
||||||
|
role: "Powerlifter",
|
||||||
|
text: "I used to binge after 'failing' my diet. FitMate just recalculates the route. It's removed the anxiety completely.",
|
||||||
|
rating: 5
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Elena R.",
|
||||||
|
role: "Marathon Runner",
|
||||||
|
text: "The fueling strategies for long run days vs recovery days are spot on. My recovery metrics have never been better.",
|
||||||
|
rating: 5
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "David Kim",
|
||||||
|
role: "Data Analyst",
|
||||||
|
text: "Love the trend weight visualization. Scale weight is so noisy, this app helps me see the signal in the noise.",
|
||||||
|
rating: 5
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Jessica T.",
|
||||||
|
role: "CrossFit Athlete",
|
||||||
|
text: "It doesn't just track; it directs. I wake up and know exactly what the mission is for the day.",
|
||||||
|
rating: 5
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
export const Testimonials: React.FC = () => {
|
||||||
|
return (
|
||||||
|
<section id="reviews" className="py-32 bg-background relative scroll-mt-32">
|
||||||
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
|
<div className="text-center mb-20">
|
||||||
|
<h2 className="text-4xl md:text-6xl font-bold text-white mb-6 tracking-tighter">
|
||||||
|
Trust the <span className="text-transparent bg-clip-text bg-gradient-to-r from-primary-400 to-blue-500">Process</span>.
|
||||||
|
</h2>
|
||||||
|
<p className="text-xl text-gray-400">
|
||||||
|
Join thousands of high-performers who stopped guessing.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
|
{REVIEWS.map((review, idx) => (
|
||||||
|
<div key={idx} className="p-8 rounded-3xl bg-surfaceHighlight border border-white/5 hover:border-primary-500/20 transition-colors duration-300 flex flex-col h-full">
|
||||||
|
<div className="flex gap-1 mb-4">
|
||||||
|
{[...Array(review.rating)].map((_, i) => (
|
||||||
|
<Star key={i} size={16} fill="#22d3ee" className="text-primary-400" strokeWidth={0} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<p className="text-gray-300 leading-relaxed mb-6 flex-1">
|
||||||
|
"{review.text}"
|
||||||
|
</p>
|
||||||
|
<div className="flex items-center justify-between mt-auto pt-6 border-t border-white/5">
|
||||||
|
<div>
|
||||||
|
<div className="font-bold text-white text-sm">{review.name}</div>
|
||||||
|
<div className="text-xs text-gray-500">{review.role}</div>
|
||||||
|
</div>
|
||||||
|
<BadgeCheck className="text-primary-500 w-5 h-5" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
88
index.html
Normal file
88
index.html
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en" class="scroll-smooth">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>FitMate | AI Nutrition Co-Pilot</title>
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
|
||||||
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
|
<script>
|
||||||
|
tailwind.config = {
|
||||||
|
darkMode: 'class',
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
fontFamily: {
|
||||||
|
sans: ['Inter', 'sans-serif'],
|
||||||
|
},
|
||||||
|
colors: {
|
||||||
|
background: '#050505',
|
||||||
|
surface: '#0F0F0F',
|
||||||
|
surfaceHighlight: '#171717',
|
||||||
|
primary: {
|
||||||
|
400: '#22d3ee', // Electric Cyan
|
||||||
|
500: '#06b6d4',
|
||||||
|
600: '#0891b2',
|
||||||
|
glow: 'rgba(34, 211, 238, 0.15)'
|
||||||
|
},
|
||||||
|
secondary: '#64748b',
|
||||||
|
},
|
||||||
|
backgroundImage: {
|
||||||
|
'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))',
|
||||||
|
'hero-glow': 'conic-gradient(from 180deg at 50% 50%, #2a8af6 0deg, #a853ba 180deg, #e92a67 360deg)',
|
||||||
|
},
|
||||||
|
animation: {
|
||||||
|
'float': 'float 8s ease-in-out infinite',
|
||||||
|
'pulse-slow': 'pulse 4s cubic-bezier(0.4, 0, 0.6, 1) infinite',
|
||||||
|
},
|
||||||
|
keyframes: {
|
||||||
|
float: {
|
||||||
|
'0%, 100%': { transform: 'translateY(0)' },
|
||||||
|
'50%': { transform: 'translateY(-20px)' },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
background-color: #050505;
|
||||||
|
color: #f8fafc;
|
||||||
|
}
|
||||||
|
/* Noise Texture */
|
||||||
|
.bg-noise {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 50;
|
||||||
|
opacity: 0.03;
|
||||||
|
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noiseFilter'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.65' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noiseFilter)'/%3E%3C/svg%3E");
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hide scrollbar */
|
||||||
|
.no-scrollbar::-webkit-scrollbar { display: none; }
|
||||||
|
.no-scrollbar { -ms-overflow-style: none; scrollbar-width: none; }
|
||||||
|
</style>
|
||||||
|
<script type="importmap">
|
||||||
|
{
|
||||||
|
"imports": {
|
||||||
|
"lucide-react": "https://aistudiocdn.com/lucide-react@^0.554.0",
|
||||||
|
"framer-motion": "https://aistudiocdn.com/framer-motion@^12.23.24",
|
||||||
|
"react": "https://aistudiocdn.com/react@^19.2.0",
|
||||||
|
"react-dom/": "https://aistudiocdn.com/react-dom@^19.2.0/",
|
||||||
|
"react/": "https://aistudiocdn.com/react@^19.2.0/"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<link rel="stylesheet" href="/index.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/index.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
15
index.tsx
Normal file
15
index.tsx
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom/client';
|
||||||
|
import App from './App';
|
||||||
|
|
||||||
|
const rootElement = document.getElementById('root');
|
||||||
|
if (!rootElement) {
|
||||||
|
throw new Error("Could not find root element to mount to");
|
||||||
|
}
|
||||||
|
|
||||||
|
const root = ReactDOM.createRoot(rootElement);
|
||||||
|
root.render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<App />
|
||||||
|
</React.StrictMode>
|
||||||
|
);
|
||||||
5
metadata.json
Normal file
5
metadata.json
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"name": "FitMate - AI Nutrition Co-Pilot",
|
||||||
|
"description": "A proactive AI nutrition co-pilot designed for systems thinkers. Turns training data into actionable nutrition plans.",
|
||||||
|
"requestFramePermissions": []
|
||||||
|
}
|
||||||
23
package.json
Normal file
23
package.json
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"name": "fitmate---ai-nutrition-co-pilot",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"lucide-react": "^0.554.0",
|
||||||
|
"framer-motion": "^12.23.24",
|
||||||
|
"react": "^19.2.0",
|
||||||
|
"react-dom": "^19.2.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^22.14.0",
|
||||||
|
"@vitejs/plugin-react": "^5.0.0",
|
||||||
|
"typescript": "~5.8.2",
|
||||||
|
"vite": "^6.2.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
1136
pnpm-lock.yaml
Normal file
1136
pnpm-lock.yaml
Normal file
File diff suppressed because it is too large
Load Diff
29
tsconfig.json
Normal file
29
tsconfig.json
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"useDefineForClassFields": false,
|
||||||
|
"module": "ESNext",
|
||||||
|
"lib": [
|
||||||
|
"ES2022",
|
||||||
|
"DOM",
|
||||||
|
"DOM.Iterable"
|
||||||
|
],
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"types": [
|
||||||
|
"node"
|
||||||
|
],
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"isolatedModules": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"allowJs": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"paths": {
|
||||||
|
"@/*": [
|
||||||
|
"./*"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"noEmit": true
|
||||||
|
}
|
||||||
|
}
|
||||||
20
types.ts
Normal file
20
types.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import { LucideIcon } from 'lucide-react';
|
||||||
|
|
||||||
|
export interface FeatureItem {
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
icon: LucideIcon;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NavItem {
|
||||||
|
label: string;
|
||||||
|
href: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ChatMessage {
|
||||||
|
id: string;
|
||||||
|
role: 'user' | 'ai';
|
||||||
|
text: string;
|
||||||
|
timestamp: string;
|
||||||
|
}
|
||||||
23
vite.config.ts
Normal file
23
vite.config.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import path from 'path';
|
||||||
|
import { defineConfig, loadEnv } from 'vite';
|
||||||
|
import react from '@vitejs/plugin-react';
|
||||||
|
|
||||||
|
export default defineConfig(({ mode }) => {
|
||||||
|
const env = loadEnv(mode, '.', '');
|
||||||
|
return {
|
||||||
|
server: {
|
||||||
|
port: 3000,
|
||||||
|
host: '0.0.0.0',
|
||||||
|
},
|
||||||
|
plugins: [react()],
|
||||||
|
define: {
|
||||||
|
'process.env.API_KEY': JSON.stringify(env.GEMINI_API_KEY),
|
||||||
|
'process.env.GEMINI_API_KEY': JSON.stringify(env.GEMINI_API_KEY)
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': path.resolve(__dirname, '.'),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
Loading…
Reference in New Issue
Block a user