diff --git a/public/images/background/FD_bg.png b/public/images/background/FD_bg.png new file mode 100644 index 0000000..b96e12f Binary files /dev/null and b/public/images/background/FD_bg.png differ diff --git a/src/app/globals.css b/src/app/globals.css index a144974..2f5e5b1 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -29,6 +29,35 @@ } } +/* Marquee β€” the track renders its children twice, so travelling exactly -50% + lands the copy where the original started and the loop is seamless. + Defined at top level, not inside @theme: Tailwind only emits keyframes from + @theme when a --animate-* token references them, and these are applied by + plain class instead. */ +@keyframes marquee-left { + from { transform: translate3d(0, 0, 0); } + to { transform: translate3d(-50%, 0, 0); } +} +@keyframes marquee-right { + from { transform: translate3d(-50%, 0, 0); } + to { transform: translate3d(0, 0, 0); } +} + +.marquee-track { + animation: marquee-left var(--marquee-duration, 30s) linear infinite; + will-change: transform; +} +.marquee-track[data-reverse="true"] { + animation-name: marquee-right; +} +/* Pausing on hover lets people actually read a card they're interested in. */ +.marquee-viewport:hover .marquee-track { + animation-play-state: paused; +} +@media (prefers-reduced-motion: reduce) { + .marquee-track { animation: none; } +} + :root { --font-sora: 'Sora', sans-serif; --font-inter: 'Inter', sans-serif; diff --git a/src/components/layout/Header.tsx b/src/components/layout/Header.tsx index ae56b53..3573ec3 100644 --- a/src/components/layout/Header.tsx +++ b/src/components/layout/Header.tsx @@ -196,7 +196,10 @@ export default function Header() {
, color: "text-emerald-400" }, - { name: "Nike", icon: , color: "text-white" }, - { name: "Spotify", icon: , color: "text-green-400" }, - { name: "Netflix", icon: , color: "text-red-500" }, - { name: "Gymshark", icon: , color: "text-amber-500" }, - { name: "Ticketmaster", icon: , color: "text-sky-400" }, - ]; - - return ( -
- - {brands.concat(brands).map((brand, idx) => ( -
- - {brand.icon} - - {brand.name} - -
- ))} -
-
- ); -} diff --git a/src/components/sections/DashboardPreview.tsx b/src/components/sections/DashboardPreview.tsx deleted file mode 100644 index 29ea370..0000000 --- a/src/components/sections/DashboardPreview.tsx +++ /dev/null @@ -1,313 +0,0 @@ -"use client"; - -import { useUser } from "@/lib/context"; -import { User, Flame, Coins, CheckCircle, Smartphone, ArrowRight, Share2, Compass, ChevronRight, Activity, TrendingUp } from "lucide-react"; - -export default function DashboardPreview() { - const { profile } = useUser(); - - // Dynamic calculations based on user's steps - const caloriesBurned = Math.round(profile.stepsToday * 0.04); - const distanceKm = (profile.stepsToday * 0.00076).toFixed(2); - const activeMinutes = Math.round(profile.stepsToday * 0.008); - - const stepPercentage = Math.min(100, Math.round((profile.stepsToday / profile.stepGoal) * 100)); - - // Weekly steps dummy data - const weeklySteps = [ - { day: "Mon", steps: 8200 }, - { day: "Tue", steps: 9400 }, - { day: "Wed", steps: 6100 }, - { day: "Thu", steps: 7800 }, - { day: "Fri", steps: 11200 }, - { day: "Sat", steps: 9000 }, - { day: "Sun", steps: profile.stepsToday }, - ]; - - const maxWeeklySteps = Math.max(...weeklySteps.map((d) => d.steps)); - - // Transaction history dummy data - const transactions = [ - { id: 1, type: "Walk Reward", detail: "6,420 steps synced", amount: "+16 LYT", time: "2 hours ago", isEarn: true }, - { id: 2, type: "Redeemed Brand", detail: "Starbucks $10 Voucher", amount: "-150 LYT", time: "5 hours ago", isEarn: false }, - { id: 3, type: "Referral Bonus", detail: "Invited Marcus Lee", amount: "+50 LYT", time: "1 day ago", isEarn: true }, - { id: 4, type: "Game Catch", detail: "LYT Catcher Score: 45", amount: "+45 LYT", time: "1 day ago", isEarn: true }, - ]; - - return ( -
- {/* Background visual glows */} -
-
- -
- - {/* Section Header */} -
- - Platform Preview - -

- Manage Assets on Your Dashboard -

-

- Track walking logs, check voucher redemption codes, and review points transaction history in your unified portal. -

-
- - {/* Contents Grid */} -
- - {/* Left Block: Weekly Progress Charts (col-span-3) */} -
-
-
- - - Activity Logs - - Weekly -
- - {/* Steps Chart */} -
- {weeklySteps.map((d, idx) => { - const pct = Math.max(15, Math.round((d.steps / maxWeeklySteps) * 100)); - const isToday = idx === weeklySteps.length - 1; - return ( -
-
-
- {/* Tooltip */} -
- {d.steps.toLocaleString()} -
-
- - {d.day} - -
- ); - })} -
- -
- πŸŽ‰ Your weekly walk step frequency has increased by 12.4% since last week. Keep it up! -
-
-
- - {/* Center Block: Mobile iOS Phone Mockup (col-span-5) */} -
- {/* Phone Shell */} -
- - {/* Dynamic Island Notch */} -
-
-
- - {/* Status Header */} -
- 9:41 -
- 5G - 100% -
-
- - {/* App Internal Body (Scrollable mock container) */} -
- - {/* User Header */} -
-
-
-
- AS -
-
-
- {profile.name} - Level {profile.level} Premium Member -
-
- - {/* Streak tag */} -
- - {profile.streak} Days -
-
- - {/* Circular Step Tracker */} -
- - {/* Gauge */} -
- - {/* SVG circular track */} - - - - - - {/* Step values */} -
- - {profile.stepsToday.toLocaleString()} - - - Steps Today - -
-
- - - Goal: {profile.stepGoal.toLocaleString()} steps ({stepPercentage}%) - - - {/* Stats split */} -
-
- {caloriesBurned} - Kcal -
-
- {distanceKm} - Km -
-
- {activeMinutes} - Mins -
-
-
- - {/* Balance & Send Row */} -
-
- Wallet Balance - - - {profile.lytBalance} LYTs - -
- - -
- - {/* Challenges Panel */} -
-

Daily Missions

-
-
-
- - Log 5,000 steps today -
- +10 LYT -
- -
-
- - Check in at Brew & Co -
- +5 LYT -
-
-
- -
- - {/* Bottom Nav Bar */} -
-
- - Track -
-
- - Explore -
-
- - Wallet -
-
- -
-
- - {/* Right Block: Transaction Log Feed (col-span-4) */} -
-
-
- - - Wallet Transaction Ledger - - Real-Time -
- - {/* Transaction list */} -
- {transactions.map((tx) => ( -
-
- {tx.type} - {tx.detail} -
- -
- - {tx.amount} - - {tx.time} -
-
- ))} -
- - -
-
- -
-
-
- ); -} diff --git a/src/components/sections/EarningFeatures.tsx b/src/components/sections/EarningFeatures.tsx deleted file mode 100644 index 63ae61b..0000000 --- a/src/components/sections/EarningFeatures.tsx +++ /dev/null @@ -1,417 +0,0 @@ -"use client"; - -import { useState } from "react"; -import { useUser } from "@/lib/context"; -import { Footprints, ShoppingBag, MapPin, Share2, ClipboardCheck, Sparkles, Check, RefreshCw } from "lucide-react"; - -export default function EarningFeatures() { - const { profile, addLyts, incrementSteps } = useUser(); - - // Walk simulator state - const [simSteps, setSimSteps] = useState(1200); - const [walkClaimed, setWalkClaimed] = useState(false); - const walkReward = Math.floor(simSteps / 400); // 1 LYT per 400 steps for the simulator - - // Shop state - const [selectedItems, setSelectedItems] = useState(["coffee"]); - const [shopClaimed, setShopClaimed] = useState(false); - - const shopItems = [ - { id: "coffee", name: "Starbucks Iced Latte", cost: "$5.80", reward: 8 }, - { id: "sneakers", name: "Nike Air Max Essentials", cost: "$120.00", reward: 150 }, - { id: "gym", name: "Gymshark Fitness Tee", cost: "$35.00", reward: 40 }, - ]; - - const totalShopReward = shopItems - .filter((item) => selectedItems.includes(item.id)) - .reduce((sum, item) => sum + item.reward, 0); - - // Map state - const [visitedStores, setVisitedStores] = useState([]); - const [mapClaimed, setMapClaimed] = useState(null); - - const checkinLocations = [ - { id: "cafe", name: "Brew & Co Cafe", reward: 5, checked: false }, - { id: "apparel", name: "H&M flagship", reward: 12, checked: false }, - { id: "arena", name: "Downtown Arena", reward: 25, checked: false }, - ]; - - // Referral state - const [referralCopied, setReferralCopied] = useState(false); - const referralCode = `lytsup.com/invite/${profile.name.toLowerCase().replace(/\s+/g, "")}-77`; - - const handleSimulateWalk = () => { - incrementSteps(simSteps); - addLyts(walkReward); - setWalkClaimed(true); - setTimeout(() => { - setWalkClaimed(false); - setSimSteps(1200); - }, 4000); - }; - - const handleShopClaim = () => { - if (totalShopReward > 0) { - addLyts(totalShopReward); - setShopClaimed(true); - setTimeout(() => { - setShopClaimed(false); - setSelectedItems([]); - }, 4000); - } - }; - - const handleCheckin = (id: string, name: string, reward: number) => { - if (visitedStores.includes(id)) return; - setVisitedStores((prev) => [...prev, id]); - addLyts(reward); - setMapClaimed(name); - setTimeout(() => setMapClaimed(null), 3000); - }; - - const handleCopyReferral = () => { - navigator.clipboard.writeText(referralCode); - setReferralCopied(true); - addLyts(50); // Referral bonus! - setTimeout(() => setReferralCopied(false), 4000); - }; - - return ( -
- {/* Background glow lines */} -
- -
- - {/* Section Header */} -
- - How it works - -

- Earn LYTs for Living Your Life -

-

- Interact with our live simulations below to see how easy it is to rack up social currency. -

-
- - {/* Feature Grid */} -
- - {/* Card 1: Walk to Earn (Simulator) */} -
-
-
-
- -
- - 1 LYT / 400 Steps - -
- -
-

Walk-to-Earn

-

- Keep your phone in your pocket, walk to class, run errands, or jog. Watch your step count convert to LYTs automatically. -

-
- - {/* Step Simulator Widget */} -
-
-
- Steps to Simulate - - +{simSteps.toLocaleString()} Steps - -
-
- Estimated Earn - - +{walkReward} LYTs - -
-
- - {/* Range Slider */} - setSimSteps(parseInt(e.target.value))} - disabled={walkClaimed} - className="w-full h-1.5 bg-white/10 rounded-lg appearance-none cursor-pointer accent-primary" - /> - -
- 400 Steps - 6,000 Steps - 12,000 Steps -
-
-
- -
- -
-
- - {/* Card 2: Shop & Earn (Receipt Simulator) */} -
-
-
-
- -
- - 10% LYT Cashback - -
- -
-

Shop-to-Earn

-

- Shop from partner merchants (food, clothing, fitness centers) and scan your digital receipt to claim instant LYT cashback. -

-
- - {/* Receipt Widget */} -
-
- Pending Receipt -
- -

Select items purchased:

-
- {shopItems.map((item) => ( -
{ - if (shopClaimed) return; - setSelectedItems((prev) => - prev.includes(item.id) - ? prev.filter((id) => id !== item.id) - : [...prev, item.id] - ); - }} - className={`flex items-center justify-between p-3 rounded-xl border cursor-pointer transition-all ${ - selectedItems.includes(item.id) - ? "bg-accent-pink/5 border-accent-pink/30 text-white" - : "bg-white/3 border-white/5 text-white/60 hover:border-white/10" - }`} - > -
-
- {selectedItems.includes(item.id) && } -
-
{item.name}
-
-
-
{item.cost}
-
+{item.reward} LYTs
-
-
- ))} -
-
-
- -
- -
-
- - {/* Card 3: Visit to Earn (Geofence Checkins) */} -
-
-
-
- -
- - Location Check-Ins - -
- -
-

Visit-to-Earn

-

- Explore your city! Check in at partner gyms, parks, retail shops, or cafes. Each location visit awards you instant points. -

-
- - {/* Map Simulator Widget */} -
- {mapClaimed && ( -
-
-
- -
-

Checked in at {mapClaimed}!

-

LYTs Added directly to wallet

-
-
- )} - -

Active nodes near you:

-
- {checkinLocations.map((loc) => { - const visited = visitedStores.includes(loc.id); - return ( -
-
- -
{loc.name}
-
- -
- ); - })} -
-
-
- -
- Glow tips: Allow location permissions in-app to auto check-in. -
-
- - {/* Card 4: Refer to Earn (Social Loop) */} -
-
-
-
- -
- - 50 LYTs / Referral - -
- -
-

Refer-to-Earn

-

- Everything is better with friends. Send your personal link to your friends. Get 50 LYTs the second they verify their profile! -

-
- - {/* Referral Widget */} -
-
- -
- -
- -
-
-
- -
- πŸ”₯ Bonus: Your friends also start with 50 LYTs in their wallet when they sign up! -
-
-
- -
- -
-
- -
-
-
- ); -} diff --git a/src/components/sections/FlashDrops.tsx b/src/components/sections/FlashDrops.tsx deleted file mode 100644 index d0360ff..0000000 --- a/src/components/sections/FlashDrops.tsx +++ /dev/null @@ -1,103 +0,0 @@ -"use client"; - -import { useEffect, useState } from "react"; -import { Zap, Clock, ShieldAlert, Award } from "lucide-react"; -import { motion } from "framer-motion"; - -export default function FlashDrops() { - const [timeLeft, setTimeLeft] = useState(7920); // 2 hours 12 minutes in seconds - - useEffect(() => { - const timer = setInterval(() => { - setTimeLeft((prev) => (prev > 0 ? prev - 1 : 7920)); - }, 1000); - return () => clearInterval(timer); - }, []); - - const formatTime = (secs: number) => { - const h = Math.floor(secs / 3600); - const m = Math.floor((secs % 3600) / 60); - const s = secs % 60; - return `${h.toString().padStart(2, "0")}:${m.toString().padStart(2, "0")}:${s.toString().padStart(2, "0")}`; - }; - - const drops = [ - { - brand: "Starbucks Concert", - detail: "VIP Pass Ticket Drop", - cost: "800 LYTs", - locked: true, - time: "2h 12m", - }, - { - brand: "Nike Air Jordan", - detail: "$100 Gift Voucher Drop", - cost: "1200 LYTs", - locked: true, - time: "5h 40m", - }, - ]; - - return ( -
-
- -
-
-
- -
- -
- Section 12 β€’ Flash Drops -

- Ultra-Rare
- Limited Vouchers -

-

- Check back periodically. High-value partner rewards drop at set intervals. When the timer hits zero, vouchers unlock for instant claims. First come, first served. -

-
-
- - {/* Time & Drops display */} -
- - {/* Timer card */} -
- Next Secret Drop Unlocks In: - - {formatTime(timeLeft)} - -
- - Notify me when drops open -
-
- - {/* Locked drops listing */} -
- {drops.map((drop, idx) => ( -
-
- {drop.brand} - {drop.detail} -
- -
- - Locked - - Cost: {drop.cost} -
-
- ))} -
- -
-
- -
-
- ); -} diff --git a/src/components/sections/FooterFAQ.tsx b/src/components/sections/FooterFAQ.tsx deleted file mode 100644 index b56addf..0000000 --- a/src/components/sections/FooterFAQ.tsx +++ /dev/null @@ -1,223 +0,0 @@ -"use client"; - -import { useState } from "react"; -import { ChevronDown, Send, Instagram, Twitter, MessageSquare, Heart, Sparkles } from "lucide-react"; -import Link from "next/link"; - -interface FaqItem { - question: string; - answer: string; -} - -export default function FooterFAQ() { - const [openFaq, setOpenFaq] = useState(null); - const [email, setEmail] = useState(""); - const [subscribed, setSubscribed] = useState(false); - - const faqItems: FaqItem[] = [ - { - question: "How do I sync my walk steps to earn LYTs?", - answer: "LytsUp requests standard health API permissions during app registration. It automatically reads your daily distance in the background and converts every 500 steps taken into 1 LYT coin. You don't need to keep the app open!", - }, - { - question: "Are there any fees or hidden charges?", - answer: "No, LytsUp is 100% free for users. Our partner brands sponsor the cashback rewards and vouchers to encourage physical activity, foot traffic, and community engagement.", - }, - { - question: "How do I redeem my vouchers at partner stores?", - answer: "When you click 'Redeem' on a voucher card in the marketplace, LytsUp validates your balance, deducts the points, and generates an active checkout code. Copy the code and apply it during checkout on the partner brand's site.", - }, - { - question: "Can I transfer LYT coins to my friends?", - answer: "Yes! The mobile app contains a peer-to-peer wallet system where you can send coins to friends to pool assets, complete squad challenges, or gift rewards.", - }, - ]; - - const handleSubscribe = (e: React.FormEvent) => { - e.preventDefault(); - if (email) { - setSubscribed(true); - setEmail(""); - setTimeout(() => setSubscribed(false), 5000); - } - }; - - const toggleFaq = (idx: number) => { - setOpenFaq((prev) => (prev === idx ? null : idx)); - }; - - return ( -
- {/* Glow lights */} -
-
- - {/* Part 1: FAQ Accordions */} -
-
- - Support Hub - -

- Frequently Asked Questions -

-
- -
- {faqItems.map((faq, idx) => { - const isOpen = openFaq === idx; - return ( -
- - - {isOpen && ( -
- {faq.answer} -
- )} -
- ); - })} -
-
- - {/* Part 2: Footer layout */} -
-
- - {/* Brand Info */} -
- - LytsUp Logo - - LytsUp - - -

- The premier social currency app for Gen Z. Earn rewards for walking, playing, and living your best life. -

-

- Earn. Spend. Live More. -

-
- - {/* Site Navigation */} -
-

Navigate

- -
- - {/* Corporate Links */} -
-

Partnerships

-
    -
  • - Become a Partner -
  • -
  • - Brands Portal -
  • -
  • - API Integration -
  • -
-
- - {/* Newsletter subscription */} -
-

Get Secret Drops

-

- Subscribe to get notified about ultra-rare brand voucher drops. -

-
- setEmail(e.target.value)} - required - className="w-full bg-white/5 border border-white/10 rounded-xl px-4 py-2 text-xs text-white focus:outline-none focus:border-primary/50 transition-colors pr-10" - /> - -
- {subscribed && ( -

- You're in! Check your inbox for secret codes. -

- )} -
-
- -
- - {/* Socials & Copyright */} -
-
- Β© {new Date().getFullYear()} LytsUp.com. All rights reserved. - β€’ - - Made with for Gen Z - -
- - {/* Social Icons */} - -
-
-
- ); -} diff --git a/src/components/sections/Hero.tsx b/src/components/sections/Hero.tsx deleted file mode 100644 index c4a315c..0000000 --- a/src/components/sections/Hero.tsx +++ /dev/null @@ -1,145 +0,0 @@ -"use client"; - -import dynamic from "next/dynamic"; -import { ArrowRight, Sparkles, TrendingUp, Users, ShoppingBag } from "lucide-react"; -import CountUp from "react-countup"; -import Marquee from "react-fast-marquee"; - -// Dynamically import Three.js Hero3D to prevent SSR issues -const Hero3D = dynamic(() => import("../animations/Hero3D"), { - ssr: false, - loading: () => ( -
-
-
-
-
-
- ), -}); - -export default function Hero() { - const highlights = [ - "WALK & EARN", - "PLAY & WIN", - "SHOP & REDEEM", - "SHARE WITH FRIENDS", - "DRINK FREE COFFEE", - "LEVEL UP", - "GEN Z SOCIAL CURRENCY", - ]; - - return ( -
- {/* Background Gradients & Glows */} -
-
- -
-
- - {/* Left Column: Copy & Actions */} -
- {/* Promo Tag */} -
- - Next Gen Social Currency -
- - {/* Main Headline */} -
-

- Earn. Spend.
- - Live More. - -

-

- Turn your everyday activities into social capital. Walk, play games, and shop to earn LYTs. Spend them at the brands you love. -

-
- - {/* CTAs */} - - - {/* Live Counters / Stats */} -
-
-
- - K+ -
-
- - Active Users -
-
- -
-
- - + -
-
- - Brand Partners -
-
- -
-
- - M+ -
-
- - Steps Tracked -
-
-
-
- - {/* Right Column: Interactive 3D Canvas */} -
- {/* Hologram Circle Rings behind 3D Object */} -
-
-
- -
- -
-
- -
-
- - {/* Highlights Marquee Ticker */} -
- - {highlights.map((text, idx) => ( -
- - {text} -
- ))} -
-
-
- ); -} diff --git a/src/components/sections/HowItWorks.tsx b/src/components/sections/HowItWorks.tsx deleted file mode 100644 index 4c9410f..0000000 --- a/src/components/sections/HowItWorks.tsx +++ /dev/null @@ -1,85 +0,0 @@ -"use client"; - -import { motion } from "framer-motion"; -import { Zap, Coins, Gift } from "lucide-react"; - -export default function HowItWorks() { - const steps = [ - { - num: "01", - title: "Walk, Play & Shop", - description: "Keep doing your daily activities. Walk to school, play browser games, check-in, or scan shopping receipts.", - icon: , - bg: "rgba(255, 46, 136, 0.05)", - border: "border-accent-pink/15", - }, - { - num: "02", - title: "Earn LYT Coins", - description: "Earn LYTs directly into your wallet. Watch your points and account experience points (XP) scale as you walk.", - icon: , - bg: "rgba(255, 199, 44, 0.05)", - border: "border-primary/15", - }, - { - num: "03", - title: "Redeem Brand Vouchers", - description: "Spend your earned LYTs on our checkout catalog for gift cards, subscription tokens, and exclusive brand discounts.", - icon: , - bg: "rgba(139, 61, 255, 0.05)", - border: "border-accent-purple/15", - }, - ]; - - return ( -
-
- - {/* Section Header */} -
- - Platform Workflow - -

- The Social Currency Lifecycle -

-

- A three-step gamified loop designed to reward you for living an active, social, and healthy lifestyle. -

-
- - {/* Step Cards Grid */} -
- {steps.map((step, idx) => ( - -
-
-
- {step.icon} -
- - {step.num} - -
- -
-

{step.title}

-

{step.description}

-
-
-
- ))} -
- -
-
- ); -} diff --git a/src/components/sections/Leaderboard.tsx b/src/components/sections/Leaderboard.tsx deleted file mode 100644 index 463a051..0000000 --- a/src/components/sections/Leaderboard.tsx +++ /dev/null @@ -1,113 +0,0 @@ -"use client"; - -import { useUser } from "@/lib/context"; -import { Users, Flame, Trophy } from "lucide-react"; -import { motion } from "framer-motion"; - -export default function Leaderboard() { - const { profile } = useUser(); - - const leaderboardUsers = [ - { rank: 1, name: "Zara Cole", level: 6, steps: 14200, points: 285, avatar: "Z" }, - { rank: 2, name: "Marcus Lee", level: 5, steps: 12100, points: 240, avatar: "M" }, - { rank: 3, name: "Chloe Vane", level: 4, steps: 9400, points: 188, avatar: "C" }, - { rank: 4, name: "Alex Stark (You)", level: profile.level, steps: profile.stepsToday, points: Math.round(profile.stepsToday / 15), avatar: "A", highlight: true }, - { rank: 5, name: "Devon Shaw", level: 3, steps: 5900, points: 118, avatar: "D" }, - ]; - - return ( -
-
- -
-
-
- -
- -
- Section 14 β€’ Leaderboard -

- Daily Ranks &
- Community Standings -

-

- Check out who is logging the most activity today. Walk, play, refer, and climb to the top to unlock premium avatar banners and multiplier rewards. -

-
-
- - {/* Leaderboard panel list */} -
-
- User Roster -
- Steps Logged - Est. LYTs -
-
- -
- {leaderboardUsers.map((user) => ( -
-
- - #{user.rank} - - -
-
- {user.avatar} -
- - L{user.level} - -
- -
- {user.name} - {user.highlight && ( - - - {profile.streak} days active streak - - )} -
-
- -
- - {user.steps.toLocaleString()} - - - - +{user.points} - -
-
- ))} -
-
-
- -
-
- ); -} diff --git a/src/components/sections/LeaderboardSection.tsx b/src/components/sections/LeaderboardSection.tsx deleted file mode 100644 index d00038a..0000000 --- a/src/components/sections/LeaderboardSection.tsx +++ /dev/null @@ -1,270 +0,0 @@ -"use client"; - -import { useState } from "react"; -import { useUser } from "@/lib/context"; -import { Users, Flame, Award, ArrowUpRight, Check, Trophy, Footprints, Landmark } from "lucide-react"; - -interface TeamChallenge { - id: string; - title: string; - description: string; - reward: number; - current: number; - goal: number; - joined: boolean; - icon: React.ReactNode; -} - -export default function LeaderboardSection() { - const { profile, addLyts } = useUser(); - const [challenges, setChallenges] = useState([ - { - id: "ch-steps", - title: "Weekend Step Sprint", - description: "Collectively log 50,000 steps with your team this weekend.", - reward: 100, - current: 38400, - goal: 50000, - joined: false, - icon: , - }, - { - id: "ch-stores", - title: "Starbucks Double Drop", - description: "Check in at any two Starbucks locations within 7 days.", - reward: 40, - current: 1, - goal: 2, - joined: true, - icon: , - }, - { - id: "ch-ref", - title: "Referral Rush", - description: "Invite 3 friends to join LytsUp. Earn bonus points.", - reward: 150, - current: 2, - goal: 3, - joined: false, - icon: , - }, - ]); - - const leaderboardUsers = [ - { rank: 1, name: "Zara Cole", level: 6, steps: 14200, points: 285, avatar: "Z" }, - { rank: 2, name: "Marcus Lee", level: 5, steps: 12100, points: 240, avatar: "M" }, - { rank: 3, name: "Chloe Vane", level: 4, steps: 9400, points: 188, avatar: "C" }, - { rank: 4, name: "Alex Stark (You)", level: profile.level, steps: profile.stepsToday, points: Math.round(profile.stepsToday / 15), avatar: "A", highlight: true }, - { rank: 5, name: "Devon Shaw", level: 3, steps: 5900, points: 118, avatar: "D" }, - ]; - - const handleJoinChallenge = (id: string, reward: number) => { - setChallenges((prev) => - prev.map((ch) => { - if (ch.id === id) { - // If already joined, complete it to give points for the simulation - if (ch.joined) { - addLyts(reward); - return { ...ch, current: ch.goal, completed: true }; - } - return { ...ch, joined: true }; - } - return ch; - }) - ); - }; - - return ( -
- {/* Background ambient lighting */} -
-
- -
- - {/* Section Header */} -
- - Community Hub - -

- Play Together. Earn Together. -

-

- Join squad goals to multiply your LYT earnings, or compete for the top spots on the daily leaderboard. -

-
- - {/* Contents Grid */} -
- - {/* Left Column: Team Challenges */} -
-

- - Active Squad Challenges -

- -
- {challenges.map((ch) => { - const percentage = Math.min(100, Math.round((ch.current / ch.goal) * 100)); - const isCompleted = ch.current >= ch.goal; - - return ( -
-
-
-
- {ch.icon} -
-
-

{ch.title}

-

{ch.description}

-
-
- -
- Reward - +{ch.reward} LYTs -
-
- - {/* Progress Slider */} -
-
- {ch.current.toLocaleString()} / {ch.goal.toLocaleString()} - {percentage}% -
-
-
-
-
- - {/* Button */} -
- -
-
- ); - })} -
-
- - {/* Right Column: Leaderboard list */} -
-

- - Daily Leaderboard -

- -
-
- User -
- Steps - Est. LYTs -
-
- -
- {leaderboardUsers.map((user) => ( -
-
- {/* Rank badge */} - - #{user.rank} - - - {/* Avatar */} -
-
- {user.avatar} -
- - L{user.level} - -
- - {/* Profile Details */} -
- {user.name} - {user.highlight && ( - - - {profile.streak} days active streak - - )} -
-
- -
- {/* Steps */} - - {user.steps.toLocaleString()} - - - {/* Points */} - - +{user.points} - -
-
- ))} -
-
-
- -
-
-
- ); -} diff --git a/src/components/sections/PlayToEarn.tsx b/src/components/sections/PlayToEarn.tsx deleted file mode 100644 index 172533c..0000000 --- a/src/components/sections/PlayToEarn.tsx +++ /dev/null @@ -1,12 +0,0 @@ -"use client"; - -import CoinCatcher from "../games/CoinCatcher"; - -export default function PlayToEarn() { - return ( -
- {/* Renders the fully functional canvas coin-catcher mini-game */} - -
- ); -} diff --git a/src/components/sections/ReferToEarn.tsx b/src/components/sections/ReferToEarn.tsx deleted file mode 100644 index ac10783..0000000 --- a/src/components/sections/ReferToEarn.tsx +++ /dev/null @@ -1,92 +0,0 @@ -"use client"; - -import { useState } from "react"; -import { useUser } from "@/lib/context"; -import { Share2, ClipboardCheck, Sparkles } from "lucide-react"; - -export default function ReferToEarn() { - const { profile, addLyts } = useUser(); - const [referralCopied, setReferralCopied] = useState(false); - const referralCode = `lytsup.com/invite/${profile.name.toLowerCase().replace(/\s+/g, "")}-77`; - - const handleCopyReferral = () => { - navigator.clipboard.writeText(referralCode); - setReferralCopied(true); - addLyts(50); // Give user 50 LYTs bonus for checking/testing referral copy! - setTimeout(() => setReferralCopied(false), 3000); - }; - - return ( -
-
- -
-
- - {/* Referral Widget */} -
-
- -
- -
- -
-
-
- -
- πŸ”₯ Invite Code Bonus: Friends start with 50 LYTs and you get 50 LYTs immediately on activation! -
- - -
- - {/* Information */} -
-
- -
- -
- Section 09 β€’ Refer-to-Earn -

- Squad Invites &
- Growth Loops -

-

- Earn bonuses by inviting your friends. Create shared earning pools, unlock group level multipliers, and dominate leaderboards together. -

-
-
- -
-
- -
-
- ); -} diff --git a/src/components/sections/ShopToEarn.tsx b/src/components/sections/ShopToEarn.tsx deleted file mode 100644 index 87183e9..0000000 --- a/src/components/sections/ShopToEarn.tsx +++ /dev/null @@ -1,127 +0,0 @@ -"use client"; - -import { useState } from "react"; -import { useUser } from "@/lib/context"; -import { ShoppingBag, Check } from "lucide-react"; - -export default function ShopToEarn() { - const { addLyts } = useUser(); - const [selectedItems, setSelectedItems] = useState(["coffee"]); - const [shopClaimed, setShopClaimed] = useState(false); - - const shopItems = [ - { id: "coffee", name: "Starbucks Iced Caramel Macchiato", cost: "$6.20", reward: 12 }, - { id: "sneakers", name: "Nike Pegasus Running Shoes", cost: "$130.00", reward: 260 }, - { id: "tee", name: "Gymshark Seamless Tee", cost: "$38.00", reward: 76 }, - ]; - - const totalReward = shopItems - .filter((item) => selectedItems.includes(item.id)) - .reduce((sum, item) => sum + item.reward, 0); - - const handleShopClaim = () => { - if (totalReward > 0) { - addLyts(totalReward); - setShopClaimed(true); - setTimeout(() => { - setShopClaimed(false); - setSelectedItems([]); - }, 3000); - } - }; - - return ( -
-
- -
-
- - {/* Simulator box */} -
-

Select items purchased:

-
- {shopItems.map((item) => ( -
{ - if (shopClaimed) return; - setSelectedItems((prev) => - prev.includes(item.id) - ? prev.filter((id) => id !== item.id) - : [...prev, item.id] - ); - }} - className={`flex items-center justify-between p-3.5 rounded-xl border cursor-pointer transition-all ${ - selectedItems.includes(item.id) - ? "bg-accent-pink/5 border-accent-pink/30 text-white" - : "bg-white/3 border-white/5 text-white/60 hover:border-white/10" - }`} - > -
-
- {selectedItems.includes(item.id) && } -
- {item.name} -
-
- {item.cost} - +{item.reward} LYTs -
-
- ))} -
- - -
- - {/* Information */} -
-
- -
- -
- Section 06 β€’ Shop-to-Earn -

- Cashback &
- Instant Rebates -

-

- Earn high cashback rates when shopping with retail partners. Submit digital checks or verify loyalty IDs to earn LYT social tokens. -

-
-
- -
-
- -
-
- ); -} diff --git a/src/components/sections/SocialWall.tsx b/src/components/sections/SocialWall.tsx deleted file mode 100644 index d3a942a..0000000 --- a/src/components/sections/SocialWall.tsx +++ /dev/null @@ -1,96 +0,0 @@ -"use client"; - -import { motion } from "framer-motion"; -import { Heart, MessageCircle, Share, Star } from "lucide-react"; - -export default function SocialWall() { - const posts = [ - { - user: "@charlie_walks", - content: "Just cleared my daily step goal and claimed a $10 Starbucks gift card. Literally paid to walk to class! β˜•πŸšΆβ€β™‚οΈ", - likes: 242, - comments: 18, - brand: "Starbucks", - bgGlow: "rgba(16, 185, 129, 0.08)", - }, - { - user: "@kicks_n_lyts", - content: "Redeemed my LYTs for a 20% discount code at Nike. Ripped these Air Max 90s for absolute dirt. Walk simulator goes crazy! πŸ”₯πŸ‘Ÿ", - likes: 512, - comments: 44, - brand: "Nike", - bgGlow: "rgba(255, 255, 255, 0.08)", - }, - { - user: "@gamer_earn", - content: "Hit score 75 on the Coin Catcher mini-game, converted it directly and unlocked Spotify Premium. This app is essential. 🎧✨", - likes: 189, - comments: 12, - brand: "Spotify", - bgGlow: "rgba(34, 197, 94, 0.08)", - }, - ]; - - return ( -
-
- - {/* Header */} -
- - Gen Z Social Wall - -

- Hear it from the Squad -

-

- See how real users are converting their daily distance and plays into social currency vouchers. -

-
- - {/* Social Cards Grid */} -
- {posts.map((post, idx) => ( - -
-
- {post.user} - - Redeemed {post.brand} - -
-

- "{post.content}" -

-
- - {/* Feed Actions Mock */} -
- - - -
-
- ))} -
- -
-
- ); -} diff --git a/src/components/sections/SpendSection.tsx b/src/components/sections/SpendSection.tsx deleted file mode 100644 index ed2f92c..0000000 --- a/src/components/sections/SpendSection.tsx +++ /dev/null @@ -1,268 +0,0 @@ -"use client"; - -import { useState } from "react"; -import { useUser } from "@/lib/context"; -import { Coffee, ShoppingBag, Music, Film, Dumbbell, Ticket, Check, Coins, Copy, ArrowRight, ShieldCheck } from "lucide-react"; - -interface BrandVoucher { - id: string; - brand: string; - discount: string; - cost: number; - icon: React.ReactNode; - color: string; - bgGlow: string; -} - -export default function SpendSection() { - const { profile, claimVoucher, vouchers } = useUser(); - const [redeemedCode, setRedeemedCode] = useState<{ brand: string; code: string } | null>(null); - const [errorMessage, setErrorMessage] = useState(null); - const [copiedId, setCopiedId] = useState(null); - - const availableVouchers: BrandVoucher[] = [ - { - id: "v-sbux", - brand: "Starbucks", - discount: "$10 Gift Card", - cost: 150, - icon: , - color: "border-emerald-500/20 text-emerald-400 hover:border-emerald-500/50", - bgGlow: "rgba(16, 185, 129, 0.15)", - }, - { - id: "v-nike", - brand: "Nike", - discount: "20% Discount Code", - cost: 300, - icon: , - color: "border-white/10 text-white hover:border-white/30", - bgGlow: "rgba(255, 255, 255, 0.15)", - }, - { - id: "v-spotify", - brand: "Spotify", - discount: "1 Month Premium", - cost: 200, - icon: , - color: "border-green-500/20 text-green-400 hover:border-green-500/50", - bgGlow: "rgba(34, 197, 94, 0.15)", - }, - { - id: "v-netflix", - brand: "Netflix", - discount: "1 Month Free UltraHD", - cost: 450, - icon: , - color: "border-red-500/20 text-red-500 hover:border-red-500/50", - bgGlow: "rgba(239, 68, 68, 0.15)", - }, - { - id: "v-gymshark", - brand: "Gymshark", - discount: "30% Storewide Voucher", - cost: 250, - icon: , - color: "border-amber-500/20 text-amber-500 hover:border-amber-500/50", - bgGlow: "rgba(245, 158, 11, 0.15)", - }, - { - id: "v-tickets", - brand: "Ticketmaster", - discount: "$25 Concert Coupon", - cost: 400, - icon: , - color: "border-sky-500/20 text-sky-400 hover:border-sky-500/50", - bgGlow: "rgba(56, 189, 248, 0.15)", - }, - ]; - - const handleRedeem = (voucher: BrandVoucher) => { - if (profile.lytBalance < voucher.cost) { - setErrorMessage(`Insufficient LYT balance. You need ${voucher.cost - profile.lytBalance} more LYTs for this voucher.`); - setTimeout(() => setErrorMessage(null), 5000); - return; - } - - const res = claimVoucher(voucher.brand, voucher.discount, voucher.cost); - if (res.success && res.code) { - setRedeemedCode({ brand: voucher.brand, code: res.code }); - // Clear popup after a while - setTimeout(() => setRedeemedCode(null), 10000); - } - }; - - const copyToClipboard = (id: string, text: string) => { - navigator.clipboard.writeText(text); - setCopiedId(id); - setTimeout(() => setCopiedId(null), 3000); - }; - - return ( -
- {/* Visual background glows */} -
-
- -
- - {/* Section Header */} -
- - Spend Your LYTs - -

- Claim Vouchers at Your Favorite Brands -

-

- Redeem your accumulated LYT coins for gift cards, subscriptions, and discounts. Click redeem to generate a checkout code. -

-
- - {/* Dynamic Warning Alert */} - {errorMessage && ( -
- {errorMessage} - Earn LYTs fast -
- )} - - {/* Success Modal Popup */} - {redeemedCode && ( -
-
-
- -
- -
-

Redemption Successful!

-

- You successfully claimed your {redeemedCode.brand} voucher code. -

-
- - {/* Code Shelf */} -
- {redeemedCode.code} - -
- -
- - Code is also saved to your Active Vouchers panel below. -
-
-
- )} - - {/* Voucher Grid */} -
- {availableVouchers.map((voucher) => { - const isAffordable = profile.lytBalance >= voucher.cost; - return ( -
-
- {/* Brand Header */} -
-
- {voucher.icon} -
-
- - {voucher.cost} LYTs -
-
- - {/* Info */} -
-

{voucher.brand}

-

{voucher.discount}

-
-
- - {/* Bottom action */} -
- -
-
- ); - })} -
- - {/* Claimed Vouchers Display */} - {vouchers.length > 0 && ( -
-

- Your Claimed Vouchers - - {vouchers.length} Total - -

- -
- {vouchers.map((v) => ( -
-
- {v.brand} -

{v.discount}

- Claimed on {v.claimedAt} -
- -
- - {v.code} - - -
-
- ))} -
-
- )} - -
-
- ); -} diff --git a/src/components/sections/SpendShop.tsx b/src/components/sections/SpendShop.tsx deleted file mode 100644 index 55d644f..0000000 --- a/src/components/sections/SpendShop.tsx +++ /dev/null @@ -1,12 +0,0 @@ -"use client"; - -import SpendSection from "./SpendSection"; - -export default function SpendShop() { - return ( -
- {/* Renders the fully functional vouchers shop store catalog */} - -
- ); -} diff --git a/src/components/sections/SquadChallenges.tsx b/src/components/sections/SquadChallenges.tsx deleted file mode 100644 index 5c4b1f8..0000000 --- a/src/components/sections/SquadChallenges.tsx +++ /dev/null @@ -1,157 +0,0 @@ -"use client"; - -import { useState } from "react"; -import { useUser } from "@/lib/context"; -import { Users, Footprints, Check, Zap, Landmark } from "lucide-react"; -import { motion } from "framer-motion"; - -interface TeamChallenge { - id: string; - title: string; - description: string; - reward: number; - current: number; - goal: number; - joined: boolean; - icon: React.ReactNode; -} - -export default function SquadChallenges() { - const { addLyts } = useUser(); - const [challenges, setChallenges] = useState([ - { - id: "ch-steps", - title: "Weekend Step Sprint", - description: "Collectively log 50,000 steps with your team this weekend.", - reward: 100, - current: 41200, - goal: 50000, - joined: false, - icon: , - }, - { - id: "ch-stores", - title: "Starbucks Double Drop", - description: "Check in at any two Starbucks locations within 7 days.", - reward: 40, - current: 1, - goal: 2, - joined: true, - icon: , - }, - ]); - - const handleJoinChallenge = (id: string, reward: number) => { - setChallenges((prev) => - prev.map((ch) => { - if (ch.id === id) { - if (ch.joined) { - addLyts(reward); - return { ...ch, current: ch.goal }; - } - return { ...ch, joined: true }; - } - return ch; - }) - ); - }; - - return ( -
-
- -
-
-
- -
- -
- Section 13 β€’ Squad Goals -

- Cooperative Team
- Active Challenges -

-

- Connect with friends or match with nearby community squads. Log steps, complete visits, and unlock bulk LYT currency bonuses together. -

-
-
- - {/* Challenges listing */} -
- {challenges.map((ch) => { - const percentage = Math.min(100, Math.round((ch.current / ch.goal) * 100)); - const isCompleted = ch.current >= ch.goal; - - return ( -
-
-
-
- {ch.icon} -
-
-

{ch.title}

-

{ch.description}

-
-
- -
- Reward - +{ch.reward} LYTs -
-
- -
-
- {ch.current.toLocaleString()} / {ch.goal.toLocaleString()} - {percentage}% -
-
-
-
-
- -
- -
-
- ); - })} -
-
- -
-
- ); -} diff --git a/src/components/sections/StorySection.tsx b/src/components/sections/StorySection.tsx deleted file mode 100644 index c5d5c60..0000000 --- a/src/components/sections/StorySection.tsx +++ /dev/null @@ -1,31 +0,0 @@ -"use client"; - -import Image from "next/image"; - -interface StorySectionProps { - id: string; - src: string; - alt: string; - priority?: boolean; -} - -export default function StorySection({ id, src, alt, priority }: StorySectionProps) { - return ( -
-
- {alt} -
-
- ); -} diff --git a/src/components/sections/ValueProp.tsx b/src/components/sections/ValueProp.tsx deleted file mode 100644 index 2c189f8..0000000 --- a/src/components/sections/ValueProp.tsx +++ /dev/null @@ -1,72 +0,0 @@ -"use client"; - -import { motion } from "framer-motion"; -import { ShieldCheck, Award, Zap, Heart } from "lucide-react"; - -export default function ValueProp() { - const points = [ - { - title: "Real Brands, Instant Codes", - desc: "No points conversions or delays. Claim digital voucher codes instantly at Starbucks, Nike, Netflix, Spotify.", - icon: , - }, - { - title: "100% Free & Transparent", - desc: "Zero fees, zero limits. Earn by living your active, healthy life. Your steps and plays are your only capital.", - icon: , - }, - { - title: "Gen Z Social Ecosystem", - desc: "Form squad step goals, battle in real-time catcher leaderboards, and trade vouchers with friends in our app.", - icon: , - }, - { - title: "Health & Well-being Loop", - desc: "Earn currency while prioritizing your physical health, mental focus, and social community connections.", - icon: , - }, - ]; - - return ( -
-
- - {/* Header */} -
- - Platform Benefits - -

- Designed for the Connected Generation -

-

- LytsUp shifts the loyalty points equation. Discover why thousands of users sync daily. -

-
- - {/* Grid */} -
- {points.map((item, idx) => ( - -
- {item.icon} -
-
-

{item.title}

-

{item.desc}

-
-
- ))} -
- -
-
- ); -} diff --git a/src/components/sections/VisitToEarn.tsx b/src/components/sections/VisitToEarn.tsx deleted file mode 100644 index 2783d00..0000000 --- a/src/components/sections/VisitToEarn.tsx +++ /dev/null @@ -1,103 +0,0 @@ -"use client"; - -import { useState } from "react"; -import { useUser } from "@/lib/context"; -import { MapPin, Sparkles } from "lucide-react"; - -export default function VisitToEarn() { - const { addLyts } = useUser(); - const [visitedStores, setVisitedStores] = useState([]); - const [mapClaimed, setMapClaimed] = useState(null); - - const checkinLocations = [ - { id: "cafe", name: "Brew & Co Cafe", reward: 8 }, - { id: "hm", name: "H&Mζ——θˆ°εΊ— Retail Store", reward: 15 }, - { id: "gym", name: "Core Fitness Gym", reward: 20 }, - ]; - - const handleCheckin = (id: string, name: string, reward: number) => { - if (visitedStores.includes(id)) return; - setVisitedStores((prev) => [...prev, id]); - addLyts(reward); - setMapClaimed(name); - setTimeout(() => setMapClaimed(null), 3000); - }; - - return ( -
-
- -
-
- - {/* Information */} -
-
- -
- -
- Section 08 β€’ Visit-to-Earn -

- Location Check-Ins &
- GPS Rewards -

-

- Earn points simply by visiting local spaces. Confirm your location status via GPS check-ins at restaurants, parks, concerts, and malls. -

-
-
- - {/* Map Simulator */} -
- {mapClaimed && ( -
-
- -

Checked in at {mapClaimed}!

-

LYT reward added directly to wallet

-
-
- )} - -

Active nodes near you:

-
- {checkinLocations.map((loc) => { - const visited = visitedStores.includes(loc.id); - return ( -
-
- - {loc.name} -
- -
- ); - })} -
-
- -
-
- -
-
- ); -} diff --git a/src/components/sections/WalkToEarn.tsx b/src/components/sections/WalkToEarn.tsx deleted file mode 100644 index 5f25f17..0000000 --- a/src/components/sections/WalkToEarn.tsx +++ /dev/null @@ -1,113 +0,0 @@ -"use client"; - -import { useState } from "react"; -import { useUser } from "@/lib/context"; -import { Footprints, Check, RefreshCw } from "lucide-react"; -import { motion } from "framer-motion"; - -export default function WalkToEarn() { - const { profile, addLyts, incrementSteps } = useUser(); - const [simSteps, setSimSteps] = useState(2500); - const [walkClaimed, setWalkClaimed] = useState(false); - const walkReward = Math.floor(simSteps / 500); // 1 LYT / 500 steps - - const handleSimulateWalk = () => { - incrementSteps(simSteps); - addLyts(walkReward); - setWalkClaimed(true); - setTimeout(() => { - setWalkClaimed(false); - setSimSteps(2500); - }, 3000); - }; - - return ( -
-
- -
-
- - {/* Left side: Information */} -
-
- -
- -
- Section 05 β€’ Walk-to-Earn -

- Step Sync &
- Points Generation -

-

- Go about your day. Next.js integration syncs steps in the background from your phone's Health API. Convert them to tradeable LYTs. -

-
-
- - {/* Right side: Step Simulator Panel */} -
-
-
- Simulate Distance - - +{simSteps.toLocaleString()} Steps - -
-
- Rewards Wallet - - +{walkReward} LYTs - -
-
- - {/* Slider */} - setSimSteps(parseInt(e.target.value))} - disabled={walkClaimed} - className="w-full h-1.5 bg-white/10 rounded-lg appearance-none cursor-pointer accent-primary" - /> - -
- 500 Steps - 5,000 Steps - 10,000 Steps -
- - -
- -
-
- -
-
- ); -} diff --git a/src/components/story/SectionsA.tsx b/src/components/story/SectionsA.tsx index 97fab30..74a7a0a 100644 --- a/src/components/story/SectionsA.tsx +++ b/src/components/story/SectionsA.tsx @@ -1,19 +1,35 @@ -import { Section, Pill, Card, Chip, Heading, Group, Reveal } from "./ui"; +import { + Section, + Pill, + Card, + Chip, + Heading, + Group, + Reveal, + SplitWords, + Magnetic, + Parallax, +} from "./ui"; import { C } from "./palette"; /* 01 β€” INTRO / HERO */ export function Intro() { return ( -
+
{/* animated ambient glows β€” soft washes, heavily blurred so no hard edge shows */} - {/* giant drifting watermark */} - - LYTS - + {/* giant watermark β€” drifts against the scroll for depth */} + + + LYTS + +
{/* eyebrow badge */} @@ -40,34 +56,40 @@ export function Intro() { {/* headline β€” line-by-line reveal, shimmering last line */}

- social - currency - - - for{" "} - - real life. - + + {" "} + + + {" "} + + + {" "} + + - +

{/* CTA buttons */} @@ -113,7 +135,7 @@ export function Intro() { /* 02 β€” THE WHY */ export function TheWhy() { return ( -
+
πŸ’Œ hey. quick story before we dive in
@@ -168,7 +190,7 @@ export function TheProblem() { { n: "03", t: "businesses stuck.", bg: C.lime, tc: "#0a0a0a", items: ["CAC is thru the roof", "Engagement nosedives", "Retention is melting", "Communities = missing", "Zero pre-purchase insight"], foot: "spend $$$ Β· earn $0." }, ]; return ( -
+
⚑ the problem traditional loyalty is broken. @@ -242,7 +264,7 @@ export function TheShift() { /* 05 β€” MEET LYTs */ export function MeetLyts() { return ( -
+
✦ introducing

LYTs.

@@ -277,7 +299,7 @@ export function OneDay() { ["🌱", "showed up", "for life.", C.purple], ] as const; return ( -
+
πŸ’š one ordinary day here is what one ordinary day with LYTs looks like. @@ -311,7 +333,7 @@ export function Feelings() { ["πŸ’¬", "Community", "tribes win", "#0a0a0a", C.lime], ] as const; return ( -
+
πŸ’— why social currency people don't want points. they want feelings. @@ -339,7 +361,7 @@ export function Feelings() { /* 08 β€” THE DIFFERENCE */ export function Difference() { return ( -
+
⚑ the fundamental difference loyalty starts at spend. @@ -383,7 +405,7 @@ export function TheLoop() { ["06", "BECOME A LYTIAN", "the community you always wanted. πŸ’ž", "#0a0a0a", C.lime], ] as const; return ( -
+
πŸš€ your 6-step loop six small moments. diff --git a/src/components/story/SectionsB.tsx b/src/components/story/SectionsB.tsx index 22d7e24..c650296 100644 --- a/src/components/story/SectionsB.tsx +++ b/src/components/story/SectionsB.tsx @@ -1,4 +1,15 @@ -import { Section, Pill, Card, Chip, Heading, Logo, Group, Reveal } from "./ui"; +import { + Section, + Pill, + Card, + Chip, + Heading, + Logo, + Group, + Reveal, + Marquee, + ScrollLitText, +} from "./ui"; import { C } from "./palette"; /* 10 β€” FOR YOU */ @@ -10,7 +21,7 @@ export function ForYou() { ["πŸ“’", "real recognition", "rewarded for contribution and engagement, not just transactions.", C.purple, "#fff"], ] as const; return ( -
+
πŸ™‹ for you @@ -61,7 +72,7 @@ export function ForBusiness() { ["6", "🧠 INSIGHT", "BETTER INTENT SIGNALS", "See them before the receipt.", "#fff", "#0a0a0a"], ] as const; return ( -
+
🀝 for businesses acquire earlier. @@ -93,7 +104,7 @@ export function WhyNow() { ["πŸ“‰", "OLD LOYALTY FADING", "No real bond anymore.", "↓↓", C.pink, "#fff"], ] as const; return ( -
+
⏰ why this matters 5 currents @@ -128,8 +139,10 @@ export function HowItWorks() { ["πŸŽ‰", "Event", "show up, get rewarded for the moment", C.pink, "#fff"], ["🏷️", "Brand", "campaigns + unlock exclusive LYTs", C.orange, "#fff"], ] as const; + const top = cards.slice(0, 5); + const bottom = cards.slice(5); return ( -
+
⚑ how it works

every action earns points{" "} @@ -139,24 +152,59 @@ export function HowItWorks() { no app-opening rituals. no complicated loyalty rules.{" "} no waiting until you spend. just live your life, engage the world, and watch your LYTs grow. 🌱

- - {cards.map(([e, t, s, bg, tc]) => ( - - +LYTs -
{e}
-
{t}
-
{s}
-
- ))} -
+ + {/* Two counter-scrolling tracks. The opposing directions read as a + machine that never stops running β€” which is the point of the copy. */} +
+ + {top.map(([e, t, s, bg, tc], i) => ( + + ))} + + + {bottom.map(([e, t, s, bg, tc], i) => ( + + ))} + +

); } +/* One tile on the HowItWorks marquee tracks. Fixed width so the loop reads as + an even cadence rather than a ragged queue. */ +function ActionTile({ + emoji, + title, + sub, + bg, + text, +}: { + emoji: string; + title: string; + sub: string; + bg: string; + text: string; +}) { + return ( +
+ + +LYTs + +
{emoji}
+
{title}
+
{sub}
+
+ ); +} + /* 14 β€” OLD vs NEW */ export function OldVsNew() { return ( -
+
πŸ’ž a new kind of loyalty

traditional loyalty said one thing.

@@ -200,7 +248,7 @@ export function TheVision() { ["every person can live more", C.purple, "#fff"], ] as const; return ( -
+
🌈 the vision @@ -229,11 +277,20 @@ export function TheVision() { /* 16 β€” OUR BELIEF */ export function OurBelief() { return ( -
+
🌱 our belief
-
+ + {/* The thesis, lit word by word as it scrolls past. */} + + +
1

@@ -264,7 +321,7 @@ export function ThePromise() { ["πŸ’ž LYTIAN.", "#0a0a0a", C.lime], ] as const; return ( -
+
⚑ the promise diff --git a/src/components/story/ui.tsx b/src/components/story/ui.tsx index 0f52a81..021efae 100644 --- a/src/components/story/ui.tsx +++ b/src/components/story/ui.tsx @@ -1,14 +1,32 @@ "use client"; -import { ReactNode } from "react"; +import { ReactNode, useEffect, useRef, useSyncExternalStore } from "react"; import Image from "next/image"; -import { motion, Variants } from "framer-motion"; +import { motion, useSpring, Variants } from "framer-motion"; import { C } from "./palette"; export { C }; const viewport = { once: true, amount: 0.2 } as const; +/* Respect the OS "reduce motion" setting β€” entrances collapse to a plain fade + so the page stays usable for anyone who gets motion sickness. */ +const REDUCE_QUERY = "(prefers-reduced-motion: reduce)"; + +function subscribeReduced(onChange: () => void) { + const mql = window.matchMedia(REDUCE_QUERY); + mql.addEventListener("change", onChange); + return () => mql.removeEventListener("change", onChange); +} + +export function useReducedMotion() { + return useSyncExternalStore( + subscribeReduced, + () => window.matchMedia(REDUCE_QUERY).matches, + () => false, // server render: assume motion is fine, corrected on hydrate + ); +} + /* Shared smooth easing β€” a soft, premium ease-out curve reused across reveals. */ const smoothEase = [0.22, 1, 0.36, 1] as const; @@ -33,9 +51,49 @@ type SectionProps = { children: ReactNode; variant?: "cream" | "deep" | "night" | "plain"; className?: string; + /** optional full-bleed background image layered over the variant colour */ + bgImage?: string; + /** Vertical weight. Drives the page's pacing β€” not every beat deserves a + full screen. `punch` is a short interstitial, `epic` is an over-tall + statement moment. */ + size?: "punch" | "normal" | "full" | "epic"; + /** Content width. `bleed` lets a section run edge-to-edge instead of sitting + in the same 6xl column as everything else. */ + width?: "narrow" | "default" | "wide" | "bleed"; + /** Vertical anchoring of the content within the section. */ + align?: "center" | "start" | "end"; }; -export function Section({ id, children, variant = "deep", className = "" }: SectionProps) { +const SIZE = { + punch: "min-h-[42vh] py-14", + normal: "min-h-[70vh] py-16", + full: "min-h-[calc(100vh-40px)] py-20", + epic: "min-h-[125vh] py-28", +} as const; + +const WIDTH = { + narrow: "max-w-3xl", + default: "max-w-6xl", + wide: "max-w-[1600px]", + bleed: "max-w-none", +} as const; + +const ALIGN = { + center: "justify-center", + start: "justify-start", + end: "justify-end", +} as const; + +export function Section({ + id, + children, + variant = "deep", + className = "", + bgImage, + size = "full", + width = "default", + align = "center", +}: SectionProps) { const bg = variant === "cream" ? "bg-cream text-[#0a0a0a]" @@ -47,9 +105,29 @@ export function Section({ id, children, variant = "deep", className = "" }: Sect return (
-
{children}
+ {bgImage && ( +
); } @@ -79,6 +157,266 @@ export function Reveal({ ); } +/* ------- useScrollPass ------- + Calls back with 0..1 as an element travels from entering the bottom of the + viewport to leaving the top. + + Deliberately NOT framer-motion's useScroll: this app drives scrolling with + Lenis, and useScroll's progress stays pinned at 0 the whole way through + (verified β€” a Parallax on it never left its initial offset). A rAF loop is + agnostic to how scroll is driven. Gated on an IntersectionObserver so the + loop only spins while the element is actually near the viewport. */ +function useScrollPass( + ref: React.RefObject, + onProgress: (p: number) => void, + enabled = true, +) { + const cb = useRef(onProgress); + useEffect(() => { + cb.current = onProgress; + }, [onProgress]); + + useEffect(() => { + const el = ref.current; + if (!el || !enabled) return; + let frame = 0; + let last = -1; + + const paint = () => { + const r = el.getBoundingClientRect(); + const span = window.innerHeight + r.height; + const p = span > 0 ? (window.innerHeight - r.top) / span : 0; + const clamped = Math.min(1, Math.max(0, p)); + if (clamped !== last) { + last = clamped; + cb.current(clamped); + } + frame = requestAnimationFrame(paint); + }; + + const io = new IntersectionObserver( + ([entry]) => { + if (entry.isIntersecting && !frame) frame = requestAnimationFrame(paint); + else if (!entry.isIntersecting && frame) { + cancelAnimationFrame(frame); + frame = 0; + } + }, + { rootMargin: "200px 0px" }, + ); + io.observe(el); + return () => { + io.disconnect(); + if (frame) cancelAnimationFrame(frame); + }; + }, [ref, enabled]); +} + +/* ------- Parallax (scroll-linked drift) ------- + Unlike Reveal, this stays tied to scroll position the whole time the element + is on screen, so the page has depth rather than just fading things in. + `speed` is how far it travels, in px, across the full pass. */ +export function Parallax({ + children, + className = "", + speed = 80, +}: { + children: ReactNode; + className?: string; + speed?: number; +}) { + const outer = useRef(null); + const inner = useRef(null); + const reduced = useReducedMotion(); + + useScrollPass( + outer, + (p) => { + const el = inner.current; + if (el) el.style.transform = `translate3d(0, ${(0.5 - p) * 2 * speed}px, 0)`; + }, + !reduced, + ); + + return ( +
+
+ {children} +
+
+ ); +} + +/* ------- Marquee (continuous horizontal scroll) ------- + A funky, always-moving band. Doubles its children so the loop is seamless. */ +export function Marquee({ + children, + className = "", + duration = 22, + reverse = false, +}: { + children: ReactNode; + className?: string; + duration?: number; + reverse?: boolean; +}) { + return ( +
+
+ {/* Rendered twice β€” the second copy is what the loop scrolls into. */} + {children} + + {children} + +
+
+ ); +} + +/* ------- SplitWords ------- + Headline that assembles word by word, each one rising and unblurring from + its own mask. The mask is what sells it β€” words emerge from behind a hard + edge rather than just fading. */ +export function SplitWords({ + text, + className = "", + delay = 0, + stagger = 0.055, +}: { + text: string; + className?: string; + delay?: number; + stagger?: number; +}) { + const reduced = useReducedMotion(); + const words = text.split(" "); + return ( + + {words.map((w, i) => ( + // Real space between words so the text stays one readable string for + // screen readers, copy-paste and crawlers β€” the mask is presentation. + + + + {w} + + + {i < words.length - 1 ? " " : null} + + ))} + + ); +} + +/* ------- ScrollLitText ------- + Long statement where each word lights up as it passes the middle of the + viewport β€” the copy reads itself as you scroll. */ +export function ScrollLitText({ + text, + className = "", + dim = "rgba(255,255,255,0.16)", + lit = "#ffffff", +}: { + text: string; + className?: string; + dim?: string; + lit?: string; +}) { + const ref = useRef(null); + const reduced = useReducedMotion(); + const words = text.split(" "); + + /* The pass is remapped so the line is fully lit by the time it reaches the + middle of the screen, rather than only at the very top. */ + useScrollPass( + ref, + (p) => { + const el = ref.current; + if (!el) return; + const spans = el.querySelectorAll("[data-lit-word]"); + const eased = Math.min(1, Math.max(0, (p - 0.15) / 0.45)); + spans.forEach((s, i) => { + const t = Math.min(1, Math.max(0, eased * spans.length - i)); + s.style.color = t > 0.5 ? lit : dim; + s.style.opacity = String(0.35 + t * 0.65); + }); + }, + !reduced, + ); + + return ( +

+ {words.map((w, i) => ( + + + {w} + + {i < words.length - 1 ? " " : null} + + ))} +

+ ); +} + +/* ------- Magnetic ------- + Pulls toward the cursor on hover and springs back on leave. Pointer-driven, + so it's skipped on touch and under reduced-motion. */ +export function Magnetic({ + children, + className = "", + strength = 0.35, +}: { + children: ReactNode; + className?: string; + strength?: number; +}) { + const ref = useRef(null); + const reduced = useReducedMotion(); + const x = useSpring(0, { stiffness: 260, damping: 18 }); + const y = useSpring(0, { stiffness: 260, damping: 18 }); + + const onMove = (e: React.PointerEvent) => { + if (reduced || e.pointerType !== "mouse" || !ref.current) return; + const r = ref.current.getBoundingClientRect(); + x.set((e.clientX - (r.left + r.width / 2)) * strength); + y.set((e.clientY - (r.top + r.height / 2)) * strength); + }; + const reset = () => { + x.set(0); + y.set(0); + }; + + return ( + + {children} + + ); +} + /* ------- Group (stagger children that use riseItem) ------- */ export function Group({ children,