diff --git a/web/app/feed/more/page.tsx b/web/app/feed/more/page.tsx
new file mode 100644
index 0000000..f682926
--- /dev/null
+++ b/web/app/feed/more/page.tsx
@@ -0,0 +1,258 @@
+"use client";
+
+import Link from "next/link";
+import { useCurrentUser } from "@/lib/identity";
+import { Avatar, Maxi } from "@/components/ui";
+import {
+ Search,
+ Heart,
+ Gift,
+ Users,
+ Sparkles,
+ ShoppingBag,
+ Calendar,
+ MessageCircle,
+ Bell,
+ Settings,
+ ArrowRight,
+ Zap,
+ TrendingUp,
+ Star,
+} from "lucide-react";
+
+type IconComponent = typeof Search;
+
+interface FeatureCard {
+ title: string;
+ description: string;
+ href: string;
+ icon: IconComponent;
+ color: string;
+ badge?: string;
+}
+
+const PRIMARY_FEATURES: FeatureCard[] = [
+ {
+ title: "Discover Feed",
+ description: "Browse gift finds from friends and taste-matched recommendations.",
+ href: "/feed",
+ icon: Search,
+ color: "#ffc2d1",
+ },
+ {
+ title: "Swipe Challenge",
+ description: "Learn anyone's taste in 60 seconds. Share the link, see what they love.",
+ href: "/feed/swipe",
+ icon: Heart,
+ color: "#d9c2ff",
+ },
+ {
+ title: "Ask Maxi",
+ description: "Your AI gift companion. Tell Maxi who you're shopping for and get instant ideas.",
+ href: "/feed/maxi",
+ icon: Sparkles,
+ color: "#ffe7a0",
+ badge: "AI",
+ },
+ {
+ title: "Gift Pools",
+ description: "Pool money with friends for the big gift nobody could afford solo.",
+ href: "/feed/pools",
+ icon: Users,
+ color: "#bfe3ff",
+ },
+ {
+ title: "Shop",
+ description: "Browse curated collections and trending finds across every budget.",
+ href: "/feed/shop",
+ icon: ShoppingBag,
+ color: "#cde6c5",
+ },
+ {
+ title: "Gift Ideas",
+ description: "AI-powered gift recommendations personalized for every person and occasion.",
+ href: "/feed/ideas",
+ icon: Gift,
+ color: "#ffd3a5",
+ },
+];
+
+const SECONDARY_FEATURES: FeatureCard[] = [
+ {
+ title: "Events",
+ description: "Never miss a birthday or celebration again.",
+ href: "/feed/events",
+ icon: Calendar,
+ color: "#d9c2ff",
+ },
+ {
+ title: "Messages",
+ description: "Chat with friends about gift ideas and coordinate purchases.",
+ href: "/feed/messages",
+ icon: MessageCircle,
+ color: "#bfe3ff",
+ },
+ {
+ title: "Notifications",
+ description: "Friend requests, pool updates, and gift reminders.",
+ href: "/feed/activity",
+ icon: Bell,
+ color: "#ffc2d1",
+ },
+ {
+ title: "Drops",
+ description: "Limited-edition finds and flash deals from curated brands.",
+ href: "/feed/drops",
+ icon: Zap,
+ color: "#ffe7a0",
+ },
+ {
+ title: "Milestones",
+ description: "Track your gifting streaks, saves, and impact.",
+ href: "/feed/milestones",
+ icon: TrendingUp,
+ color: "#cde6c5",
+ },
+ {
+ title: "Recommendations",
+ description: "Personalized picks based on your taste and history.",
+ href: "/feed/recommendations",
+ icon: Star,
+ color: "#ffd3a5",
+ },
+];
+
+function CardGrid({ cards, large }: { cards: FeatureCard[]; large?: boolean }) {
+ return (
+
+ {cards.map((card) => {
+ const Icon = card.icon;
+ return (
+
+
+
+
+
+ {card.badge && (
+
+ {card.badge}
+
+ )}
+
+
+
+ {card.title}
+
+
+ {card.description}
+
+
+
+
+ );
+ })}
+
+ );
+}
+
+export default function MorePage() {
+ const me = useCurrentUser();
+
+ return (
+
+ {/* Header */}
+
+
+
+
+
{me.name}
+
@{me.handle}
+
+
+
+
+
Settings
+
+
+
+ {/* Quick stats */}
+
+ {[
+ { value: "12k+", label: "Finds saved" },
+ { value: "0", label: "Double-buys" },
+ { value: "4.9", label: "Avg rating" },
+ ].map((stat) => (
+
+
{stat.value}
+
{stat.label}
+
+ ))}
+
+
+ {/* Primary features */}
+
+
+ Core Features
+
+
+
+
+ {/* Secondary features */}
+
+
+ More to Explore
+
+
+
+
+ {/* Ask Maxi banner */}
+
+
+
+
Need help finding the perfect gift?
+
+ Ask Maxi — your AI companion that reads their taste and finds the match.
+
+
+
+
+
+ {/* Footer links */}
+
+
+ Privacy
+
+
+ Home
+
+
+ Settings
+
+
+
+ );
+}
diff --git a/web/app/globals.css b/web/app/globals.css
index c28237e..cffb51d 100644
--- a/web/app/globals.css
+++ b/web/app/globals.css
@@ -165,6 +165,41 @@ body {
}
@keyframes line-reveal { to { clip-path: inset(0 0 0 0); } }
+/* ── Interactive hero demo animations ────────────────────────────────── */
+@keyframes panel-in {
+ from { opacity: 0; transform: translateY(8px) scale(0.98); }
+ to { opacity: 1; transform: translateY(0) scale(1); }
+}
+.animate-panel-in {
+ animation: panel-in 0.4s cubic-bezier(0.2, 0.8, 0.2, 1) both;
+}
+
+@keyframes in {
+ from { opacity: 0; transform: translateY(6px); }
+ to { opacity: 1; transform: translateY(0); }
+}
+.animate-in {
+ animation: in 0.5s cubic-bezier(0.2, 0.8, 0.2, 1) both;
+}
+
+/* ── More-screen card hover glow ────────────────────────────────────── */
+.card-glow {
+ position: relative;
+}
+.card-glow::before {
+ content: '';
+ position: absolute;
+ inset: -1px;
+ border-radius: inherit;
+ background: linear-gradient(135deg, rgba(251,111,82,0.3), rgba(255,194,75,0.2), transparent);
+ opacity: 0;
+ transition: opacity 0.4s;
+ z-index: -1;
+}
+.card-glow:hover::before {
+ opacity: 1;
+}
+
.noise-overlay { position: relative; }
.noise-overlay::after {
content: '';
diff --git a/web/components/landing/features-section.tsx b/web/components/landing/features-section.tsx
index 0eeaece..2f6c5dd 100644
--- a/web/components/landing/features-section.tsx
+++ b/web/components/landing/features-section.tsx
@@ -1,243 +1,216 @@
"use client";
import { useEffect, useRef, useState } from "react";
+import Link from "next/link";
+import { Search, Heart, Gift, Users, Sparkles, ArrowRight } from "lucide-react";
+
+interface Feature {
+ tag: string;
+ icon: typeof Search;
+ headline: string;
+ headlineAccent: string;
+ description: string;
+ cta: { text: string; href: string };
+ bullets: { title: string; description: string }[];
+ alternatives?: string[];
+}
-const features = [
+const FEATURES: Feature[] = [
+ {
+ tag: "Discover",
+ icon: Search,
+ headline: "A feed of finds",
+ headlineAccent: "not a search box.",
+ description: "See what friends are saving, gifting and unwrapping. Gift ideas surface naturally from people whose taste you trust.",
+ cta: { text: "Open the feed", href: "/feed" },
+ bullets: [
+ { title: "Taste-matched recommendations", description: "Every item is scored against the recipient's real interests." },
+ { title: "Friend-powered discovery", description: "See saves and shares from your circle before generic ads." },
+ { title: "No doom-scrolling", description: "Curated, finite feed designed around intent, not engagement." },
+ ],
+ alternatives: ["Amazon wish lists", "Google Shopping", "Pinterest boards"],
+ },
{
- number: "01",
- title: "Meet Maxi",
- description:
- "Your AI gift companion reads a person's taste from their Pinterest, Spotify and saved finds — then suggests gifts they'll actually love, inside your budget.",
- stats: { value: "94%", label: "match to their taste" },
+ tag: "Swipe",
+ icon: Heart,
+ headline: "Swipe to learn",
+ headlineAccent: "their taste.",
+ description: "In 60 seconds, Maxi understands what they love. Share a challenge link and let recipients reveal their own style — no guessing required.",
+ cta: { text: "Try a challenge", href: "/challenge" },
+ bullets: [
+ { title: "60-second taste quiz", description: "Swipe on curated items to build a taste profile fast." },
+ { title: "Shareable challenge links", description: "Send a link — they swipe, you see what they actually want." },
+ { title: "Pinterest & Spotify import", description: "Already have saves? Maxi reads them automatically." },
+ ],
},
{
- number: "02",
- title: "A feed of finds",
- description:
- "See what friends are saving, gifting and unwrapping. Discovery that feels like your favorite feed — not a search box.",
- stats: { value: "12k+", label: "finds shared" },
+ tag: "Gift",
+ icon: Gift,
+ headline: "Claim it",
+ headlineAccent: "before someone else does.",
+ description: "Shared wishlists the whole group can see. Claim an item so nobody double-buys, and everyone gets something they actually wanted.",
+ cta: { text: "Browse wishlists", href: "/feed/shop" },
+ bullets: [
+ { title: "Claim to prevent double-buys", description: "Lock an item so the group knows it's handled." },
+ { title: "Budget-aware suggestions", description: "Maxi filters by your budget — not the store's." },
+ { title: "One-tap purchase", description: "Buy directly through affiliate links, no middleman." },
+ ],
},
{
- number: "03",
- title: "Shared wishlists",
- description:
- "Wishlists the whole group can see, so nobody double-buys and everyone gets something they actually wanted.",
- stats: { value: "0", label: "awkward re-gifts" },
+ tag: "Pool",
+ icon: Users,
+ headline: "Go in together",
+ headlineAccent: "on the big one.",
+ description: "Pool money for the gift nobody could afford solo. Split it, track it, and ship it together in a few taps.",
+ cta: { text: "Start a pool", href: "/feed/pools" },
+ bullets: [
+ { title: "Transparent tracking", description: "Everyone sees who chipped in and how much is left." },
+ { title: "Flexible splits", description: "Equal, custom, or pay-what-you-can — you set the rules." },
+ ],
},
{
- number: "04",
- title: "Group gifting",
- description:
- "Pool money for the big one. Split it, track it, and ship it together in a few taps.",
- stats: { value: "3 taps", label: "to chip in" },
+ tag: "Maxi",
+ icon: Sparkles,
+ headline: "Your AI gift companion",
+ headlineAccent: "who actually gets it.",
+ description: "Maxi reads their Pinterest, Spotify and saved finds — then suggests gifts they'll love, inside your budget. Not a search engine. A companion.",
+ cta: { text: "Ask Maxi", href: "/feed/maxi" },
+ bullets: [
+ { title: "Taste-aware matching", description: "94% accuracy against real recipient preferences." },
+ { title: "Budget-locked", description: "Never suggests above what you can spend." },
+ { title: "Context from every signal", description: "Combines social saves, past gifts, and trend data." },
+ ],
},
];
-// Floating dot particles visualization
-function ParticleVisualization() {
- const canvasRef = useRef(null);
- const frameRef = useRef(0);
- const mouseRef = useRef({ x: 0.5, y: 0.5 });
-
- useEffect(() => {
- const canvas = canvasRef.current;
- if (!canvas) return;
- const ctx = canvas.getContext("2d");
- if (!ctx) return;
-
- const resize = () => {
- const rect = canvas.getBoundingClientRect();
- const dpr = Math.min(window.devicePixelRatio || 1, 2);
- canvas.width = rect.width * dpr;
- canvas.height = rect.height * dpr;
- ctx.scale(dpr, dpr);
- };
- resize();
- window.addEventListener("resize", resize);
-
- const handleMouseMove = (e: MouseEvent) => {
- const rect = canvas.getBoundingClientRect();
- mouseRef.current = {
- x: (e.clientX - rect.left) / rect.width,
- y: (e.clientY - rect.top) / rect.height,
- };
- };
- canvas.addEventListener("mousemove", handleMouseMove);
-
- // Generate stable particle positions
- const COUNT = 70;
- const particles = Array.from({ length: COUNT }, (_, i) => {
- const seed = i * 1.618;
- return {
- bx: ((seed * 127.1) % 1),
- by: ((seed * 311.7) % 1),
- phase: seed * Math.PI * 2,
- speed: 0.4 + (seed % 0.4),
- radius: 1.2 + (seed % 2.2),
- };
- });
-
- let time = 0;
- const render = () => {
- const rect = canvas.getBoundingClientRect();
- const w = rect.width;
- const h = rect.height;
-
- ctx.clearRect(0, 0, w, h);
-
- const mx = mouseRef.current.x;
- const my = mouseRef.current.y;
-
- particles.forEach((p) => {
- const flowX = Math.sin(time * p.speed * 0.4 + p.phase) * 38;
- const flowY = Math.cos(time * p.speed * 0.3 + p.phase * 0.7) * 24;
-
- const bx = p.bx * w;
- const by = p.by * h;
- const dx = p.bx - mx;
- const dy = p.by - my;
- const dist = Math.sqrt(dx * dx + dy * dy);
- const influence = Math.max(0, 1 - dist * 2.8);
-
- const x = bx + flowX + influence * Math.cos(time + p.phase) * 36;
- const y = by + flowY + influence * Math.sin(time + p.phase) * 36;
-
- const pulse = Math.sin(time * p.speed + p.phase) * 0.5 + 0.5;
- const alpha = 0.08 + pulse * 0.18 + influence * 0.3;
-
- ctx.beginPath();
- ctx.arc(x, y, p.radius + pulse * 0.8, 0, Math.PI * 2);
- ctx.fillStyle = `rgba(251, 111, 82, ${alpha})`;
- ctx.fill();
- });
-
- time += 0.016;
- frameRef.current = requestAnimationFrame(render);
- };
- render();
-
- return () => {
- window.removeEventListener("resize", resize);
- canvas.removeEventListener("mousemove", handleMouseMove);
- cancelAnimationFrame(frameRef.current);
- };
- }, []);
+const FEATURE_COLORS = ["#ffc2d1", "#d9c2ff", "#cde6c5", "#bfe3ff", "#ffe7a0"];
- return (
-
- );
-}
-
-export function FeaturesSection() {
+function FeatureBlock({ feature, index }: { feature: Feature; index: number }) {
const [isVisible, setIsVisible] = useState(false);
- const sectionRef = useRef(null);
+ const ref = useRef(null);
useEffect(() => {
const observer = new IntersectionObserver(
([entry]) => {
- if (entry.isIntersecting) setIsVisible(true);
+ if (entry.isIntersecting) {
+ setIsVisible(true);
+ observer.disconnect();
+ }
},
- { threshold: 0.1 }
+ { threshold: 0.15 }
);
-
- if (sectionRef.current) observer.observe(sectionRef.current);
+ if (ref.current) observer.observe(ref.current);
return () => observer.disconnect();
}, []);
+ const Icon = feature.icon;
+ const accentColor = FEATURE_COLORS[index % FEATURE_COLORS.length];
+
return (
-
-
- {/* Header - Full width with diagonal layout */}
-
-
-
-
-
- What you get
-
-
- Thoughtful,
-
- on autopilot.
-
-
-
-
- Everything you need to give a gift they'll remember — discovery, taste, and a companion that does the thinking.
-
+
+ {/* Vertical connector line (skip on first) */}
+ {index > 0 && (
+
+ )}
+
+
+ {/* Text column */}
+
- {/* Bento grid */}
-
- {/* Large Maxi card */}
-
+ {feature.headline}
+
+
{feature.headlineAccent}
+
+
+
+ {feature.description}
+
+
+
-
-
-
-
{features[0].number}
-
{features[0].title}
-
{features[0].description}
-
- {features[0].stats.value}
- {features[0].stats.label}
-
-
-
-
-

{
- (e.currentTarget as HTMLImageElement).style.display = "none";
- }}
- className="absolute inset-0 w-full h-full object-cover object-center"
- />
-
-
-
+ {feature.cta.text}
+
+
+
- {/* Three smaller cards */}
- {features.slice(1).map((f, i) => (
+ {/* Bullet cards */}
+
+ {feature.bullets.map((b, i) => (
-
-
{f.number}
-
{f.title}
-
{f.description}
-
-
-
{f.stats.value}
-
{f.stats.label}
+
+
+
+
{b.title}
+
{b.description}
+
))}
+
+ {/* "Alternative to" badges */}
+ {feature.alternatives && (
+
+ Alternative to
+ {feature.alternatives.map((alt) => (
+
+ {alt}
+
+ ))}
+
+ )}
+
+
+
+ );
+}
+
+export function FeaturesSection() {
+ return (
+
+
+ {/* Section header */}
+
+
+
+ Everything you need
+
+
+ From idea
+
+ to unwrap.
+
+
+
+ {/* Feature blocks */}
+
+ {FEATURES.map((feature, i) => (
+
+ ))}
diff --git a/web/components/landing/hero-section.tsx b/web/components/landing/hero-section.tsx
index e3dee97..4d95244 100644
--- a/web/components/landing/hero-section.tsx
+++ b/web/components/landing/hero-section.tsx
@@ -1,131 +1,442 @@
"use client";
-import { useEffect, useState, useRef } from "react";
+import { useEffect, useState, useRef, useCallback } from "react";
import Link from "next/link";
-import { ArrowRight } from "lucide-react";
+import { ArrowRight, Heart, Search, Sparkles, Users, Gift, MessageCircle, Bookmark, Send } from "lucide-react";
-const words = ["love", "treasure", "remember", "show off"];
+/* ── Tab definitions ────────────────────────────────────────────────────── */
+type TabId = "discover" | "swipe" | "gift" | "pool" | "maxi";
-function BlurWord({ word, trigger }: { word: string; trigger: number }) {
- const letters = word.split("");
- const STAGGER = 45; // ms between each letter
- const DURATION = 500; // blur+opacity fade duration per letter
- const GRADIENT_HOLD = STAGGER * letters.length + DURATION + 200;
+interface Tab {
+ id: TabId;
+ label: string;
+ icon: typeof Heart;
+}
+
+const TABS: Tab[] = [
+ { id: "discover", label: "Discover", icon: Search },
+ { id: "swipe", label: "Swipe", icon: Heart },
+ { id: "gift", label: "Gift", icon: Gift },
+ { id: "pool", label: "Pool", icon: Users },
+ { id: "maxi", label: "Maxi", icon: Sparkles },
+];
+
+const AUTO_CYCLE_MS = 5000;
+
+/* ── Mock UI panels ─────────────────────────────────────────────────────── */
+
+function DiscoverPanel() {
+ const [liked, setLiked] = useState
>({});
+
+ const items = [
+ { name: "Instant Film Camera", brand: "Halo", price: "$79", emoji: "📸", color: "#ffc2d1" },
+ { name: "Linen Throw Blanket", brand: "Parachute", price: "$129", emoji: "🧶", color: "#bfe3ff" },
+ { name: "Matcha Starter Kit", brand: "Ippodo", price: "$45", emoji: "🍵", color: "#cde6c5" },
+ { name: "Vinyl Record Player", brand: "Crosley", price: "$89", emoji: "🎵", color: "#d9c2ff" },
+ { name: "Scented Candle Set", brand: "Diptyque", price: "$68", emoji: "🕯️", color: "#ffe7a0" },
+ { name: "Leather Journal", brand: "Moleskine", price: "$32", emoji: "📓", color: "#ffd3a5" },
+ ];
+
+ return (
+
+ {/* Top bar */}
+
+
+
+ Giftmaxxing
+
+
+
+
+
+
+ {/* Search */}
+
+
+
+ Search gifts, friends, ideas...
+
+
+ {/* Grid */}
+
+
+ {items.map((item, i) => (
+
+
+ {item.emoji}
+
+
+
+
{item.name}
+
{item.brand} · {item.price}
+
+
+ ))}
+
+
+
+ );
+}
+
+function SwipePanel() {
+ const [swiped, setSwiped] = useState(false);
+ const [direction, setDirection] = useState<"left" | "right" | null>(null);
+
+ const handleSwipe = (dir: "left" | "right") => {
+ setDirection(dir);
+ setSwiped(true);
+ setTimeout(() => {
+ setSwiped(false);
+ setDirection(null);
+ }, 600);
+ };
+
+ return (
+
+
+ Swipe Challenge
+ 4 of 12
+
+
+
+
+ 📸
+
+
+
Instant Film Camera
+
Halo · $79
+
+
+ 94% match to Maya's taste
+
+
+
+
+ {/* Swipe buttons */}
+
+
+
+
+
+
+ );
+}
+
+function GiftPanel() {
+ const [claimed, setClaimed] = useState(false);
- const [letterStates, setLetterStates] = useState<{ opacity: number; blur: number }[]>(
- letters.map(() => ({ opacity: 0, blur: 20 }))
+ return (
+
+
+ Maya's Wishlist
+ 3 items
+
+
+ {[
+ { name: "Instant Film Camera", price: "$79", emoji: "📸", color: "#ffc2d1", claimed: claimed },
+ { name: "Linen Throw Blanket", price: "$129", emoji: "🧶", color: "#bfe3ff", claimed: false },
+ { name: "Matcha Starter Kit", price: "$45", emoji: "🍵", color: "#cde6c5", claimed: false },
+ ].map((item, i) => (
+
+
+ {item.emoji}
+
+
+
{item.name}
+
{item.price}
+
+ {i === 0 ? (
+
+ ) : (
+
Open
+ )}
+
+ ))}
+
+ {/* Tip */}
+
+
+
+ Maxi tip: Maya saved 3 film cameras on Pinterest last month. This is a lock.
+
+
+
);
- const [showGradient, setShowGradient] = useState(true);
- const framesRef = useRef([]);
- const timersRef = useRef[]>([]);
+}
+
+function PoolPanel() {
+ const [contributed, setContributed] = useState(false);
+ const progress = contributed ? 87 : 62;
+
+ return (
+
+
+ Gift Pool
+ Active
+
+
+ {/* Gift item */}
+
+
+ 🎵
+
+
+
Vinyl Record Player
+
For Maya's birthday
+
+
+ {/* Progress */}
+
+
+ ${Math.round(89 * progress / 100)} raised
+ ${89} goal
+
+
+
+ {/* Contributors */}
+
+
Contributors
+ {[
+ { name: "Jules", amount: "$25", color: "#d9c2ff" },
+ { name: "Theo", amount: "$20", color: "#cde6c5" },
+ { name: "Priya", amount: "$10", color: "#ffc2d1" },
+ ].map((c, i) => (
+
+
+ {c.name[0]}
+
+
{c.name}
+
{c.amount}
+
+ ))}
+ {contributed && (
+
+ )}
+
+
+ {/* CTA */}
+
+
+
+
+ );
+}
+
+function MaxiPanel() {
+ const [msgIndex, setMsgIndex] = useState(0);
+
+ const messages = [
+ { from: "user", text: "What should I get Maya for her birthday?" },
+ { from: "maxi", text: "Based on Maya's Pinterest saves and Spotify playlists, she's really into film photography and cozy aesthetics. Here are my top 3:" },
+ { from: "maxi", text: "1. Instant Film Camera ($79) — 94% match\n2. Linen Throw Blanket ($129) — 88% match\n3. Vinyl Record Player ($89) — 85% match" },
+ ];
useEffect(() => {
- // reset
- framesRef.current.forEach(cancelAnimationFrame);
- timersRef.current.forEach(clearTimeout);
- framesRef.current = [];
- timersRef.current = [];
-
- // eslint-disable-next-line react-hooks/set-state-in-effect -- animation reset requires imperative state init
- setLetterStates(letters.map(() => ({ opacity: 0, blur: 20 })));
- setShowGradient(true);
-
- // stagger each letter
- letters.forEach((_, i) => {
- const t = setTimeout(() => {
- const start = performance.now();
- const tick = (now: number) => {
- const progress = Math.min((now - start) / DURATION, 1);
- const eased = 1 - Math.pow(1 - progress, 3);
- setLetterStates(prev => {
- const next = [...prev];
- next[i] = { opacity: eased, blur: 20 * (1 - eased) };
- return next;
- });
- if (progress < 1) {
- const id = requestAnimationFrame(tick);
- framesRef.current.push(id);
- }
- };
- const id = requestAnimationFrame(tick);
- framesRef.current.push(id);
- }, i * STAGGER);
- timersRef.current.push(t);
- });
-
- // remove gradient once all letters are settled
- const gt = setTimeout(() => setShowGradient(false), GRADIENT_HOLD);
- timersRef.current.push(gt);
-
- return () => {
- framesRef.current.forEach(cancelAnimationFrame);
- timersRef.current.forEach(clearTimeout);
- };
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [trigger]);
-
- // gradient colours cycling across letter positions (Giftmaxxing warm palette)
- const gradientColors = ["#fb6f52", "#ff9a76", "#ffc2d1", "#ffc24b", "#fb6f52"];
+ if (msgIndex < messages.length - 1) {
+ const timer = setTimeout(() => setMsgIndex(prev => prev + 1), 1200);
+ return () => clearTimeout(timer);
+ }
+ }, [msgIndex, messages.length]);
return (
- <>
- {letters.map((char, i) => {
- const colorIndex = (i / Math.max(letters.length - 1, 1)) * (gradientColors.length - 1);
- const lower = Math.floor(colorIndex);
- const upper = Math.min(lower + 1, gradientColors.length - 1);
- const t = colorIndex - lower;
-
- // lerp hex colours
- const hex2rgb = (hex: string) => {
- const r = parseInt(hex.slice(1, 3), 16);
- const g = parseInt(hex.slice(3, 5), 16);
- const b = parseInt(hex.slice(5, 7), 16);
- return [r, g, b];
- };
- const [r1, g1, b1] = hex2rgb(gradientColors[lower]);
- const [r2, g2, b2] = hex2rgb(gradientColors[upper]);
- const r = Math.round(r1 + (r2 - r1) * t);
- const g = Math.round(g1 + (g2 - g1) * t);
- const b = Math.round(b1 + (b2 - b1) * t);
-
- return (
-
+
+
+ {messages.slice(0, msgIndex + 1).map((msg, i) => (
+
- {char}
-
- );
- })}
- >
+ {msg.from === "maxi" && (
+
+
+
+ )}
+
+ {msg.text}
+
+
+ ))}
+ {msgIndex < messages.length - 1 && (
+
+ )}
+
+ {/* Input */}
+
+
+ );
+}
+
+/* ── Maxi icon (small inline version) ───────────────────────────────── */
+function MaxiIcon({ size = 20 }: { size?: number }) {
+ return (
+
);
}
+/* ── Panel renderer (keyed for remount) ─────────────────────────────── */
+const PANELS: Record
React.JSX.Element> = {
+ discover: DiscoverPanel,
+ swipe: SwipePanel,
+ gift: GiftPanel,
+ pool: PoolPanel,
+ maxi: MaxiPanel,
+};
+
+/* ── Hero section ───────────────────────────────────────────────────── */
export function HeroSection() {
const [isVisible, setIsVisible] = useState(false);
- const [wordIndex, setWordIndex] = useState(0);
+ const [activeTab, setActiveTab] = useState("discover");
+ const timerRef = useRef | null>(null);
useEffect(() => {
// eslint-disable-next-line react-hooks/set-state-in-effect -- hydration animation trigger
setIsVisible(true);
}, []);
- useEffect(() => {
- const interval = setInterval(() => {
- setWordIndex((prev) => (prev + 1) % words.length);
- }, 2500);
- return () => clearInterval(interval);
+ // Auto-cycle tabs
+ const startCycle = useCallback(() => {
+ if (timerRef.current) clearInterval(timerRef.current);
+ timerRef.current = setInterval(() => {
+ setActiveTab(prev => {
+ const idx = TABS.findIndex(t => t.id === prev);
+ return TABS[(idx + 1) % TABS.length].id;
+ });
+ }, AUTO_CYCLE_MS);
}, []);
+ useEffect(() => {
+ startCycle();
+ return () => { if (timerRef.current) clearInterval(timerRef.current); };
+ }, [startCycle]);
+
+ const handleTabClick = (id: TabId) => {
+ setActiveTab(id);
+ startCycle(); // restart timer on manual click
+ };
+
+ const Panel = PANELS[activeTab];
+
return (
-
- {/* Background image (Midjourney) with warm gradient fallback */}
+
+ {/* Background — warm gradient with subtle image */}
- {/* Cream overlays keep the left text crisp and blend the image edges */}
-
-
+
+
- {/* Subtle grid lines */}
+ {/* Subtle grid */}
{[...Array(8)].map((_, i) => (
-
+
))}
{[...Array(12)].map((_, i) => (
-
+
))}
-
-
-
- {/* Eyebrow */}
-
-
-
- Social gifting, powered by Maxi
-
-
-
- {/* Main headline */}
-
-
- Know exactly what
-
- they'll{" "}
-
-
+
+
+
+ {/* Left — copy */}
+
+ {/* Eyebrow */}
+
+
+
+ Social gifting, powered by Maxi
-
-
-
+
- {/* Dual CTA */}
-
-
- Try it now
-
-
-
+ Gifting,
+
+
finally figured out.
+
+
+ {/* Subtitle */}
+
+ The all-in-one social gifting app with an AI companion who actually gets their taste.
+
+
+ {/* CTAs */}
+
+
+ Try it now
+
+
+
+ Share a challenge
+
+
+
+
+ {/* Right — Interactive demo (Railway-style) */}
+
- Share a challenge
-
-
+ {/* Device frame */}
+
+ {/* Browser chrome */}
+
+
+
+
+
+ giftmaxxing.app
+
+
+
+ {/* Panel content — fixed height for consistency */}
+
+
+ {/* Tab bar — Railway-style bottom pills */}
+
+ {TABS.map((tab) => {
+ const active = activeTab === tab.id;
+ const Icon = tab.icon;
+ return (
+
+ );
+ })}
+
+
+
-
- {/* Stats — 3 metrics static, no auto-scroll */}
-
@@ -241,16 +589,11 @@ export function HeroSection() {
].map((stat) => (
{stat.value}
-
- {stat.label}
-
+ {stat.label}
))}
-
- {/* Scroll indicator */}
-
);
}