370 lines
22 KiB
TypeScript
370 lines
22 KiB
TypeScript
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>
|
|
);
|