feat(hero): CSS-animated 3-screen phone mockup carousel

Replaces the static placeholder with an auto-cycling mockup that shows
three real app screens — Home/Quick Log, Vaccinations (IAP), and
Memories — mirroring the actual app card/row UI patterns.

- 2500ms auto-advance, pauses on hover
- Smooth opacity crossfade (duration-500) between screens
- Active dot indicator stretches to pill shape (w-4)
- Floating label pill above phone changes with active screen
- All pure CSS/Tailwind — zero external assets, static page unchanged

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Manohar Gupta 2026-05-29 10:32:39 +05:30
parent daf6b34281
commit 261a9cbbcb
2 changed files with 337 additions and 26 deletions

View file

@ -1,5 +1,6 @@
import type { Metadata } from "next"; import type { Metadata } from "next";
import Link from "next/link"; import Link from "next/link";
import { PhoneMockup } from "@/components/marketing/PhoneMockup";
export const metadata: Metadata = { export const metadata: Metadata = {
title: "Tia — Your baby's digital heirloom", title: "Tia — Your baby's digital heirloom",
@ -59,33 +60,9 @@ function Hero() {
<p className="mt-4 text-xs text-gray-400">No credit card. No setup fee. Your data is yours.</p> <p className="mt-4 text-xs text-gray-400">No credit card. No setup fee. Your data is yours.</p>
</div> </div>
{/* RIGHT: phone mockup */} {/* RIGHT: animated phone mockup */}
<div className="flex justify-center lg:justify-end"> <div className="flex justify-center lg:justify-end">
<div className="relative" style={{ width: 280 }}> <PhoneMockup />
{/* Device shell */}
<div className="absolute inset-0 -m-3 rounded-[42px] bg-gray-900 shadow-2xl" />
{/* Screen */}
<div className="relative rounded-[34px] overflow-hidden bg-white" style={{ height: 580 }}>
{/* TODO: replace with real export at public/screenshots/home.png (560×1160 PNG, no device frame) */}
<div className="w-full h-full bg-gradient-to-br from-rose-50 via-amber-50 to-rose-100 flex flex-col items-center justify-center gap-4 p-6">
<span className="text-5xl">🌸</span>
<p className="text-xs text-gray-400 text-center leading-relaxed">
App screenshot<br />coming soon
</p>
{/* Mock UI suggestion */}
<div className="w-full space-y-2 mt-2">
{[["🍼","Feed — 90ml","2m ago"],["😴","Sleep — 2h","1h ago"],["🚼","Diaper","3h ago"]].map(([icon,label,time])=>(
<div key={label} className="bg-white/80 rounded-xl px-3 py-2 flex items-center justify-between">
<span className="flex items-center gap-2 text-xs text-gray-700"><span>{icon}</span>{label}</span>
<span className="text-xs text-gray-400">{time}</span>
</div>
))}
</div>
</div>
</div>
{/* Notch */}
<div className="absolute top-0 left-1/2 -translate-x-1/2 w-24 h-6 bg-gray-900 rounded-b-2xl" />
</div>
</div> </div>
</div> </div>

View file

@ -0,0 +1,334 @@
"use client";
import { useState, useEffect, useRef, useCallback } from "react";
const SCREENS = [
{ label: "🏡 Home", key: "home" },
{ label: "💉 Vaccinations", key: "vaccines" },
{ label: "📸 Memories", key: "memories" },
] as const;
// ── Screen 0: Home / Quick Log ──────────────────────────────────
function HomeScreen() {
return (
<div className="w-full h-full bg-gradient-to-br from-rose-50 to-amber-50 flex flex-col overflow-hidden text-[10px]">
{/* Status bar */}
<div className="flex items-center justify-between px-4 pt-3 pb-1">
<span className="font-semibold text-gray-600 text-[9px]">9:41</span>
<div className="flex items-center gap-1 text-gray-500 text-[9px]">
<span></span>
<span>WiFi</span>
<span>🔋</span>
</div>
</div>
{/* Top nav */}
<div className="flex items-center justify-between px-3 pb-1">
<div className="w-6 h-5 flex flex-col justify-between py-0.5">
{[0,1,2].map(i => <div key={i} className="h-0.5 w-4 bg-gray-500 rounded" />)}
</div>
<div className="flex items-center gap-2 text-[11px]">
<span></span>
<span className="text-red-500 font-bold text-[10px]">🆘</span>
</div>
</div>
{/* Greeting */}
<div className="px-3 pb-2">
<p className="font-bold text-[13px] text-gray-900">Good morning 👋</p>
<p className="text-gray-500 text-[9px]">How is Arjun doing today?</p>
</div>
{/* Baby card */}
<div className="mx-3 mb-2 bg-white rounded-2xl shadow-sm px-3 py-2 flex items-center gap-3">
<div className="w-10 h-10 bg-rose-100 rounded-full flex items-center justify-center text-lg flex-shrink-0">
👶
</div>
<div className="flex-1">
<p className="font-semibold text-gray-900 text-[11px]">Arjun</p>
<p className="text-gray-500 text-[9px]">4 months</p>
</div>
<span className="text-gray-400 text-[11px]"></span>
</div>
{/* Today summary */}
<div className="mx-3 mb-2 bg-white rounded-2xl shadow-sm">
<div className="grid grid-cols-3 divide-x divide-gray-100">
{[
{ icon: "🍼", label: "Feeds", count: 4, last: "2m ago" },
{ icon: "🚼", label: "Diapers", count: 3, last: "1h ago" },
{ icon: "😴", label: "Sleep", count: 1, last: "3h ago" },
].map(item => (
<div key={item.label} className="flex flex-col items-center py-2 px-1">
<span className="text-sm mb-0.5">{item.icon}</span>
<span className="text-[13px] font-bold text-gray-800 leading-tight">{item.count}</span>
<span className="text-[8px] text-gray-400">{item.label}</span>
<span className="text-[8px] text-rose-400 mt-0.5">{item.last}</span>
</div>
))}
</div>
</div>
{/* Quick Log */}
<div className="px-3 mb-2">
<p className="font-semibold text-gray-800 text-[10px] mb-1.5">Quick Log</p>
<div className="flex gap-2">
{[["🍼","Feed"],["😴","Sleep"],["🚼","Diaper"]].map(([icon, lbl]) => (
<div key={lbl} className="flex flex-col items-center bg-white rounded-xl shadow-sm py-2 px-3 gap-0.5">
<span className="text-lg">{icon}</span>
<span className="text-[9px] text-gray-600">{lbl}</span>
</div>
))}
</div>
</div>
{/* Recent Activity */}
<div className="px-3 flex-1 overflow-hidden">
<div className="flex justify-between items-center mb-1.5">
<p className="font-semibold text-gray-800 text-[10px]">Recent Activity</p>
<span className="text-rose-500 text-[9px]">See all </span>
</div>
<div className="space-y-1.5">
{[
{ icon: "🍼", label: "Feed", detail: "90 ml", time: "10:42 AM" },
{ icon: "😴", label: "Sleep", detail: "2h", time: "8:30 AM" },
{ icon: "🚼", label: "Diaper",detail: "", time: "7:00 AM" },
].map(row => (
<div key={row.time} className="flex items-center justify-between bg-white rounded-xl px-2.5 py-1.5">
<div className="flex items-center gap-2">
<span className="text-sm">{row.icon}</span>
<div>
<p className="font-medium text-gray-800 text-[10px]">{row.label}</p>
<p className="text-gray-400 text-[8px]">{row.time}</p>
</div>
</div>
{row.detail && <span className="text-gray-400 text-[9px]">{row.detail}</span>}
</div>
))}
</div>
</div>
{/* Bottom tab bar strip */}
<div className="mt-auto border-t border-gray-100 bg-white flex justify-around py-1.5 px-2">
{[["🏡","Home"],["📋","Activity"],["✨","AI"],["☰","Menu"]].map(([icon, lbl]) => (
<div key={lbl} className="flex flex-col items-center gap-0.5">
<span className="text-sm">{icon}</span>
<span className={`text-[7px] ${lbl === "Home" ? "text-rose-500 font-semibold" : "text-gray-400"}`}>{lbl}</span>
</div>
))}
</div>
</div>
);
}
// ── Screen 1: Vaccinations ──────────────────────────────────────
function VaccinationsScreen() {
return (
<div className="w-full h-full bg-gradient-to-br from-rose-50 to-amber-50 flex flex-col overflow-hidden text-[10px]">
{/* Header */}
<div className="flex items-center gap-2 px-3 pt-8 pb-3">
<span className="text-gray-500 text-sm"></span>
<p className="font-bold text-gray-900 text-[13px]">Medical</p>
</div>
{/* Tab bar */}
<div className="px-3 mb-2">
<div className="flex gap-1 bg-white rounded-xl p-1 shadow-sm">
{["Vaccines","Medicine","Allergies"].map((t, i) => (
<div key={t} className={`flex-1 text-center py-1 rounded-lg text-[8px] font-medium ${i === 0 ? "bg-rose-500 text-white" : "text-gray-500"}`}>
{t}
</div>
))}
</div>
</div>
<p className="px-3 font-semibold text-gray-800 text-[10px] mb-2">IAP Schedule</p>
{/* Status tabs */}
<div className="px-3 mb-2">
<div className="flex gap-1">
{[["Upcoming (3)","rose"],["Completed (4)","gray"],["Overdue (0)","gray"]].map(([lbl, col]) => (
<div key={lbl} className={`px-2 py-1 rounded-full text-[8px] font-medium border ${col === "rose" ? "bg-rose-100 text-rose-700 border-rose-200" : "bg-white text-gray-500 border-gray-200"}`}>
{lbl}
</div>
))}
</div>
</div>
{/* Vaccine rows */}
<div className="px-3 space-y-1.5 flex-1 overflow-hidden">
{[
{ name: "OPV-1", due: "15 Jul 2025", done: false },
{ name: "Pentavalent-1", due: "15 Jul 2025", done: false },
{ name: "PCV-1", due: "10 Jul 2025", done: true },
{ name: "Rota-1", due: "15 Jul 2025", done: false },
].map(v => (
<div key={v.name} className="bg-white rounded-xl px-2.5 py-2 flex items-center justify-between shadow-sm">
<div>
<p className="font-medium text-gray-900 text-[10px]">{v.name}</p>
<p className="text-gray-400 text-[8px]">Due: {v.due}</p>
</div>
{v.done
? <span className="text-green-500 font-bold text-sm"></span>
: <div className="px-2 py-1 bg-rose-400 text-white rounded-lg text-[8px] font-medium">Mark Given</div>
}
</div>
))}
</div>
{/* Telegram chip */}
<div className="mx-3 mt-2 mb-3 bg-sky-50 border border-sky-200 rounded-xl px-3 py-2 flex items-center gap-2">
<span className="text-sm">📲</span>
<p className="text-sky-700 text-[9px] font-medium">Telegram reminder in 3 days</p>
</div>
{/* Bottom tab strip */}
<div className="border-t border-gray-100 bg-white flex justify-around py-1.5 px-2">
{[["🏡","Home"],["📋","Activity"],["✨","AI"],["☰","Menu"]].map(([icon, lbl]) => (
<div key={lbl} className="flex flex-col items-center gap-0.5">
<span className="text-sm">{icon}</span>
<span className="text-[7px] text-gray-400">{lbl}</span>
</div>
))}
</div>
</div>
);
}
// ── Screen 2: Memories ──────────────────────────────────────────
function MemoriesScreen() {
return (
<div className="w-full h-full bg-gradient-to-br from-rose-50 to-amber-50 flex flex-col overflow-hidden text-[10px]">
{/* Header */}
<div className="flex items-center gap-2 px-3 pt-8 pb-3">
<span className="text-gray-500 text-sm"></span>
<p className="font-bold text-gray-900 text-[13px]">Memories</p>
</div>
{/* Folder tabs */}
<div className="px-3 mb-3">
<div className="flex gap-1.5 overflow-hidden">
{[["🌟","All"],["👣","First Steps"],["🛁","Bath Time"],["🍼","Feeding"]].map(([emoji, lbl], i) => (
<div key={lbl} className={`flex-shrink-0 flex items-center gap-1 px-2 py-1 rounded-full text-[8px] font-medium border ${i === 0 ? "bg-rose-100 text-rose-700 border-rose-200" : "bg-white text-gray-500 border-gray-200"}`}>
<span>{emoji}</span><span>{lbl}</span>
</div>
))}
</div>
</div>
{/* Photo grid */}
<div className="px-3 grid grid-cols-2 gap-2 flex-1 overflow-hidden">
{[
{ bg: "bg-rose-200", emoji: "😊", caption: "First smile" },
{ bg: "bg-amber-200", emoji: "🛁", caption: "Bath time" },
{ bg: "bg-violet-200", emoji: "🏆", caption: "4 months!" },
{ bg: "bg-green-200", emoji: "👨‍👩‍👧", caption: "With Nani" },
].map(photo => (
<div key={photo.caption} className={`${photo.bg} rounded-xl flex flex-col items-center justify-center aspect-square gap-1`}>
<span className="text-2xl">{photo.emoji}</span>
<span className="text-[8px] text-gray-600 font-medium">{photo.caption}</span>
</div>
))}
</div>
{/* Upload row */}
<div className="mx-3 mt-2 mb-2">
<div className="border-2 border-dashed border-rose-200 rounded-xl flex items-center justify-center py-2 gap-2">
<span className="text-sm">📷</span>
<span className="text-rose-400 text-[9px] font-medium">Add memory</span>
</div>
</div>
{/* Bottom tab strip */}
<div className="border-t border-gray-100 bg-white flex justify-around py-1.5 px-2">
{[["🏡","Home"],["📋","Activity"],["✨","AI"],["☰","Menu"]].map(([icon, lbl]) => (
<div key={lbl} className="flex flex-col items-center gap-0.5">
<span className="text-sm">{icon}</span>
<span className="text-[7px] text-gray-400">{lbl}</span>
</div>
))}
</div>
</div>
);
}
// ── Main component ──────────────────────────────────────────────
export function PhoneMockup() {
const [active, setActive] = useState(0);
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
const start = useCallback(() => {
if (intervalRef.current) clearInterval(intervalRef.current);
intervalRef.current = setInterval(() => {
setActive(prev => (prev + 1) % SCREENS.length);
}, 2500);
}, []);
const stop = useCallback(() => {
if (intervalRef.current) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
}, []);
useEffect(() => {
start();
return stop;
}, [start, stop]);
const screenComponents = [<HomeScreen key="home" />, <VaccinationsScreen key="vax" />, <MemoriesScreen key="mem" />];
return (
<div className="flex flex-col items-center gap-3">
{/* Floating label above phone */}
<div className="h-7 flex items-center">
{SCREENS.map((s, i) => (
<span
key={s.key}
className={`absolute text-xs font-medium px-3 py-1 rounded-full bg-rose-100 text-rose-700 transition-opacity duration-300 ${active === i ? "opacity-100" : "opacity-0 pointer-events-none"}`}
>
{s.label}
</span>
))}
</div>
{/* Phone */}
<div
className="relative"
style={{ width: 280 }}
onMouseEnter={stop}
onMouseLeave={start}
>
{/* Device shell */}
<div className="absolute inset-0 -m-3 rounded-[42px] bg-gray-900 shadow-2xl" />
{/* Screen */}
<div className="relative rounded-[34px] overflow-hidden bg-white" style={{ height: 580 }}>
{screenComponents.map((screen, i) => (
<div
key={i}
className={`absolute inset-0 transition-opacity duration-500 ${active === i ? "opacity-100" : "opacity-0 pointer-events-none"}`}
>
{screen}
</div>
))}
{/* Dot indicators */}
<div className="absolute bottom-3 left-0 right-0 flex justify-center gap-1.5 z-10">
{SCREENS.map((_, i) => (
<button
key={i}
onClick={() => { stop(); setActive(i); }}
className={`w-2 h-2 rounded-full transition-all duration-300 ${active === i ? "bg-rose-500 w-4" : "bg-rose-200"}`}
aria-label={`Show screen ${i + 1}`}
/>
))}
</div>
</div>
{/* Notch */}
<div className="absolute top-0 left-1/2 -translate-x-1/2 w-24 h-6 bg-gray-900 rounded-b-2xl z-10" />
</div>
</div>
);
}