From 9a1363119367a24eba7e27cc161a556c68abce98 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 1 Jul 2026 16:58:42 +0000 Subject: [PATCH 1/2] feat: Railway-style interactive hero demo + More screen MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace hero section with 5-tab interactive demo (Discover → Swipe → Gift → Pool → Maxi) showing simulated app UI with auto-cycling and click-override - Rebuild features section with Railway-style narrative arc and connector lines - Add /feed/more page with feature hub, quick stats, and Maxi CTA banner - Add CSS animations for panel transitions and card hover effects Co-Authored-By: Saksham --- web/app/feed/more/page.tsx | 258 +++++++ web/app/globals.css | 35 + web/components/landing/features-section.tsx | 378 +++++------ web/components/landing/hero-section.tsx | 705 +++++++++++++++----- 4 files changed, 989 insertions(+), 387 deletions(-) create mode 100644 web/app/feed/more/page.tsx 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..ff28845 100644 --- a/web/components/landing/features-section.tsx +++ b/web/components/landing/features-section.tsx @@ -1,243 +1,211 @@ "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); - }, - { threshold: 0.1 } + ([entry]) => { if (entry.isIntersecting) setIsVisible(true); }, + { 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 */} +
+ {/* Tag */} +
+
+
+ {feature.tag}
-
- {/* 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..3bb4247 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 && ( +
+
+ Y +
+ You + $22 +
+ )} +
+
+ {/* 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 ( - +
+ +
+ Maxi + AI +
+
+
+ {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 +587,11 @@ export function HeroSection() { ].map((stat) => (
{stat.value} - - {stat.label} - + {stat.label}
))}
- - {/* Scroll indicator */} -
); } From f6990760feca4ac5525ce54f5860b09954cd112a Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 1 Jul 2026 17:07:00 +0000 Subject: [PATCH 2/2] fix: add aria-label to tab buttons + disconnect observer after trigger Co-Authored-By: Saksham --- web/components/landing/features-section.tsx | 7 ++++++- web/components/landing/hero-section.tsx | 2 ++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/web/components/landing/features-section.tsx b/web/components/landing/features-section.tsx index ff28845..2f6c5dd 100644 --- a/web/components/landing/features-section.tsx +++ b/web/components/landing/features-section.tsx @@ -91,7 +91,12 @@ function FeatureBlock({ feature, index }: { feature: Feature; index: number }) { useEffect(() => { const observer = new IntersectionObserver( - ([entry]) => { if (entry.isIntersecting) setIsVisible(true); }, + ([entry]) => { + if (entry.isIntersecting) { + setIsVisible(true); + observer.disconnect(); + } + }, { threshold: 0.15 } ); if (ref.current) observer.observe(ref.current); diff --git a/web/components/landing/hero-section.tsx b/web/components/landing/hero-section.tsx index 3bb4247..4d95244 100644 --- a/web/components/landing/hero-section.tsx +++ b/web/components/landing/hero-section.tsx @@ -556,6 +556,8 @@ export function HeroSection() { key={tab.id} type="button" onClick={() => handleTabClick(tab.id)} + aria-label={tab.label} + aria-pressed={active} className={`flex items-center gap-1.5 px-3 py-1.5 rounded-full text-[11px] font-medium transition-all duration-300 ${ active ? "bg-[#fb6f52] text-white shadow-sm shadow-[#fb6f52]/30"