first commit
11
.claude/launch.json
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"version": "0.0.1",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"name": "lytsup-dev",
|
||||||
|
"runtimeExecutable": "npm",
|
||||||
|
"runtimeArgs": ["run", "dev"],
|
||||||
|
"port": 3000
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
29
.dockerignore
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
# Dependency directories
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
# Next.js build output
|
||||||
|
.next/
|
||||||
|
out/
|
||||||
|
|
||||||
|
# Local env files
|
||||||
|
.env*.local
|
||||||
|
|
||||||
|
# Debug logs
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
|
||||||
|
# Git files
|
||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
.gitattributes
|
||||||
|
|
||||||
|
# Editor settings
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
52
Dockerfile
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
# Stage 1: Install dependencies only when needed
|
||||||
|
FROM node:20-alpine AS deps
|
||||||
|
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
|
||||||
|
RUN apk add --no-cache libc6-compat
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Install dependencies based on the preferred package manager
|
||||||
|
COPY package.json package-lock.json* ./
|
||||||
|
RUN npm ci --legacy-peer-deps
|
||||||
|
|
||||||
|
# Stage 2: Rebuild the source code only when needed
|
||||||
|
FROM node:20-alpine AS builder
|
||||||
|
WORKDIR /app
|
||||||
|
COPY --from=deps /app/node_modules ./node_modules
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Disable Next.js telemetry data compilation
|
||||||
|
ENV NEXT_TELEMETRY_DISABLED=1
|
||||||
|
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
# Stage 3: Production image, copy all the files and run next
|
||||||
|
FROM node:20-alpine AS runner
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
ENV NEXT_TELEMETRY_DISABLED=1
|
||||||
|
|
||||||
|
RUN addgroup --system --gid 1001 nodejs
|
||||||
|
RUN adduser --system --uid 1001 nextjs
|
||||||
|
|
||||||
|
COPY --from=builder /app/public ./public
|
||||||
|
|
||||||
|
# Set the correct permission for prerender cache
|
||||||
|
RUN mkdir .next
|
||||||
|
RUN chown nextjs:nodejs .next
|
||||||
|
|
||||||
|
# Automatically leverage output traces to reduce image size
|
||||||
|
# https://nextjs.org/docs/advanced-features/output-file-tracing
|
||||||
|
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
|
||||||
|
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
|
||||||
|
|
||||||
|
USER nextjs
|
||||||
|
|
||||||
|
EXPOSE 3000
|
||||||
|
|
||||||
|
ENV PORT=3000
|
||||||
|
# hostname binds to all network interfaces, allowing container ports to map correctly
|
||||||
|
ENV HOSTNAME="0.0.0.0"
|
||||||
|
|
||||||
|
# server.js is created by next build from the standalone output
|
||||||
|
CMD ["node", "server.js"]
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { NextConfig } from "next";
|
import type { NextConfig } from "next";
|
||||||
|
|
||||||
const nextConfig: NextConfig = {
|
const nextConfig: NextConfig = {
|
||||||
/* config options here */
|
output: "standalone",
|
||||||
};
|
};
|
||||||
|
|
||||||
export default nextConfig;
|
export default nextConfig;
|
||||||
|
|||||||
2100
package-lock.json
generated
30
package.json
@@ -9,9 +9,34 @@
|
|||||||
"lint": "eslint"
|
"lint": "eslint"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@gsap/react": "^2.1.2",
|
||||||
|
"@react-three/drei": "^10.7.7",
|
||||||
|
"@react-three/fiber": "^9.6.1",
|
||||||
|
"@tsparticles/engine": "^4.3.2",
|
||||||
|
"class-variance-authority": "^0.7.1",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"framer-motion": "^12.42.2",
|
||||||
|
"gsap": "^3.15.0",
|
||||||
|
"lenis": "^1.3.25",
|
||||||
|
"lottie-react": "^2.4.1",
|
||||||
|
"lucide-react": "^0.468.0",
|
||||||
|
"motion": "^12.42.2",
|
||||||
"next": "16.2.10",
|
"next": "16.2.10",
|
||||||
|
"next-themes": "^0.4.6",
|
||||||
|
"ogl": "^1.0.11",
|
||||||
"react": "19.2.4",
|
"react": "19.2.4",
|
||||||
"react-dom": "19.2.4"
|
"react-countup": "^6.5.3",
|
||||||
|
"react-dom": "19.2.4",
|
||||||
|
"react-fast-marquee": "^1.6.5",
|
||||||
|
"react-parallax-tilt": "^1.7.333",
|
||||||
|
"react-tsparticles": "^2.12.2",
|
||||||
|
"react-use": "^17.6.1",
|
||||||
|
"split-type": "^0.3.4",
|
||||||
|
"swiper": "^14.0.5",
|
||||||
|
"tailwind-merge": "^3.6.0",
|
||||||
|
"tailwindcss-animate": "^1.0.7",
|
||||||
|
"three": "^0.185.1",
|
||||||
|
"tsparticles": "^4.3.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/postcss": "^4",
|
"@tailwindcss/postcss": "^4",
|
||||||
@@ -20,6 +45,9 @@
|
|||||||
"@types/react-dom": "^19",
|
"@types/react-dom": "^19",
|
||||||
"eslint": "^9",
|
"eslint": "^9",
|
||||||
"eslint-config-next": "16.2.10",
|
"eslint-config-next": "16.2.10",
|
||||||
|
"eslint-config-prettier": "^10.1.8",
|
||||||
|
"prettier": "^3.9.5",
|
||||||
|
"prettier-plugin-tailwindcss": "^0.8.0",
|
||||||
"tailwindcss": "^4",
|
"tailwindcss": "^4",
|
||||||
"typescript": "^5"
|
"typescript": "^5"
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
public/images/1.jpg
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
public/images/10.jpg
Normal file
|
After Width: | Height: | Size: 759 KiB |
BIN
public/images/11.jpg
Normal file
|
After Width: | Height: | Size: 923 KiB |
BIN
public/images/12.jpg
Normal file
|
After Width: | Height: | Size: 778 KiB |
BIN
public/images/13.jpg
Normal file
|
After Width: | Height: | Size: 1017 KiB |
BIN
public/images/14.jpg
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
public/images/15.jpg
Normal file
|
After Width: | Height: | Size: 1018 KiB |
BIN
public/images/16.jpg
Normal file
|
After Width: | Height: | Size: 618 KiB |
BIN
public/images/17.jpg
Normal file
|
After Width: | Height: | Size: 597 KiB |
BIN
public/images/2.jpg
Normal file
|
After Width: | Height: | Size: 743 KiB |
BIN
public/images/3.jpg
Normal file
|
After Width: | Height: | Size: 913 KiB |
BIN
public/images/4.jpg
Normal file
|
After Width: | Height: | Size: 892 KiB |
BIN
public/images/5.jpg
Normal file
|
After Width: | Height: | Size: 814 KiB |
BIN
public/images/6.jpg
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
public/images/7.jpg
Normal file
|
After Width: | Height: | Size: 675 KiB |
BIN
public/images/8.jpg
Normal file
|
After Width: | Height: | Size: 989 KiB |
BIN
public/images/9.jpg
Normal file
|
After Width: | Height: | Size: 731 KiB |
BIN
public/images/lytsup_app.png
Normal file
|
After Width: | Height: | Size: 480 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 426 KiB |
@@ -1,26 +1,240 @@
|
|||||||
@import "tailwindcss";
|
@import "tailwindcss";
|
||||||
|
|
||||||
|
@theme {
|
||||||
|
--color-primary: #FFC72C;
|
||||||
|
--color-dark: #090909;
|
||||||
|
--color-accent-pink: #FF2E88;
|
||||||
|
--color-accent-purple: #8B3DFF;
|
||||||
|
--color-accent-orange: #FF6B35;
|
||||||
|
--color-accent-lime: #C8FF37;
|
||||||
|
|
||||||
|
--font-sora: var(--font-sora), sans-serif;
|
||||||
|
--font-inter: var(--font-inter), sans-serif;
|
||||||
|
|
||||||
|
--animate-float: float 6s ease-in-out infinite;
|
||||||
|
--animate-pulse-glow: pulse-glow 2s ease-in-out infinite;
|
||||||
|
--animate-shimmer: shimmer 2s linear infinite;
|
||||||
|
|
||||||
|
@keyframes float {
|
||||||
|
0%, 100% { transform: translateY(0px) rotate(0deg); }
|
||||||
|
50% { transform: translateY(-10px) rotate(3deg); }
|
||||||
|
}
|
||||||
|
@keyframes pulse-glow {
|
||||||
|
0%, 100% { opacity: 0.6; filter: drop-shadow(0 0 5px rgba(255, 46, 136, 0.4)); }
|
||||||
|
50% { opacity: 1; filter: drop-shadow(0 0 20px rgba(255, 46, 136, 0.8)); }
|
||||||
|
}
|
||||||
|
@keyframes shimmer {
|
||||||
|
0% { background-position: -200% 0; }
|
||||||
|
100% { background-position: 200% 0; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--background: #ffffff;
|
--font-sora: 'Sora', sans-serif;
|
||||||
--foreground: #171717;
|
--font-inter: 'Inter', sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
@theme inline {
|
/* Custom Scrollbar */
|
||||||
--color-background: var(--background);
|
::-webkit-scrollbar {
|
||||||
--color-foreground: var(--foreground);
|
width: 8px;
|
||||||
--font-sans: var(--font-geist-sans);
|
}
|
||||||
--font-mono: var(--font-geist-mono);
|
::-webkit-scrollbar-track {
|
||||||
|
background: #090909;
|
||||||
|
}
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background: #333333;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: #FFC72C;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
/* Base Body Styles */
|
||||||
:root {
|
html, body {
|
||||||
--background: #0a0a0a;
|
background-color: #090909;
|
||||||
--foreground: #ededed;
|
color: #FFFFFF;
|
||||||
}
|
font-family: var(--font-inter), sans-serif;
|
||||||
|
overflow-x: hidden;
|
||||||
|
scroll-behavior: smooth;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
h1, h2, h3, h4, h5, h6 {
|
||||||
background: var(--background);
|
font-family: var(--font-sora), sans-serif;
|
||||||
color: var(--foreground);
|
}
|
||||||
font-family: Arial, Helvetica, sans-serif;
|
|
||||||
|
/* Custom Utilities */
|
||||||
|
.glassmorphism {
|
||||||
|
background: rgba(18, 18, 18, 0.65);
|
||||||
|
backdrop-filter: blur(16px);
|
||||||
|
-webkit-backdrop-filter: blur(16px);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.glassmorphism-light {
|
||||||
|
background: rgba(255, 255, 255, 0.03);
|
||||||
|
backdrop-filter: blur(12px);
|
||||||
|
-webkit-backdrop-filter: blur(12px);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.glassmorphism-yellow {
|
||||||
|
background: rgba(255, 199, 44, 0.08);
|
||||||
|
backdrop-filter: blur(12px);
|
||||||
|
-webkit-backdrop-filter: blur(12px);
|
||||||
|
border: 1px solid rgba(255, 199, 44, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.glassmorphism-pink {
|
||||||
|
background: rgba(255, 46, 136, 0.08);
|
||||||
|
backdrop-filter: blur(12px);
|
||||||
|
-webkit-backdrop-filter: blur(12px);
|
||||||
|
border: 1px solid rgba(255, 46, 136, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.glassmorphism-purple {
|
||||||
|
background: rgba(139, 61, 255, 0.08);
|
||||||
|
backdrop-filter: blur(12px);
|
||||||
|
-webkit-backdrop-filter: blur(12px);
|
||||||
|
border: 1px solid rgba(139, 61, 255, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-glow-yellow {
|
||||||
|
text-shadow: 0 0 15px rgba(255, 199, 44, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-glow-pink {
|
||||||
|
text-shadow: 0 0 15px rgba(255, 46, 136, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-glow-purple {
|
||||||
|
text-shadow: 0 0 15px rgba(139, 61, 255, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Gradient borders */
|
||||||
|
.gradient-border-pink-purple {
|
||||||
|
position: relative;
|
||||||
|
background: #090909;
|
||||||
|
background-clip: padding-box;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
}
|
||||||
|
.gradient-border-pink-purple::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0; right: 0; bottom: 0; left: 0;
|
||||||
|
z-index: -1;
|
||||||
|
margin: -1px;
|
||||||
|
border-radius: inherit;
|
||||||
|
background: linear-gradient(135deg, #FF2E88, #8B3DFF);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gradient-border-yellow-lime {
|
||||||
|
position: relative;
|
||||||
|
background: #090909;
|
||||||
|
background-clip: padding-box;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
}
|
||||||
|
.gradient-border-yellow-lime::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0; right: 0; bottom: 0; left: 0;
|
||||||
|
z-index: -1;
|
||||||
|
margin: -1px;
|
||||||
|
border-radius: inherit;
|
||||||
|
background: linear-gradient(135deg, #FFC72C, #C8FF37);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Neobrutalist helpers (funky Gen Z theme) */
|
||||||
|
.nb {
|
||||||
|
border: 3px solid #0a0a0a;
|
||||||
|
box-shadow: 6px 6px 0 0 #0a0a0a;
|
||||||
|
}
|
||||||
|
.nb-sm {
|
||||||
|
border: 2px solid #0a0a0a;
|
||||||
|
box-shadow: 3px 3px 0 0 #0a0a0a;
|
||||||
|
}
|
||||||
|
.nb-lg {
|
||||||
|
border: 4px solid #0a0a0a;
|
||||||
|
box-shadow: 10px 10px 0 0 #0a0a0a;
|
||||||
|
}
|
||||||
|
.nb-hover {
|
||||||
|
transition: transform 0.15s ease, box-shadow 0.15s ease;
|
||||||
|
}
|
||||||
|
.nb-hover:hover {
|
||||||
|
transform: translate(-2px, -2px);
|
||||||
|
box-shadow: 9px 9px 0 0 #0a0a0a;
|
||||||
|
}
|
||||||
|
.strike-pink {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
.strike-pink::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
left: -2%;
|
||||||
|
right: -2%;
|
||||||
|
top: 52%;
|
||||||
|
height: 6px;
|
||||||
|
background: #FF2E88;
|
||||||
|
transform: rotate(-2deg);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
.bg-cream {
|
||||||
|
background-color: #FBF3DD;
|
||||||
|
}
|
||||||
|
.bg-deep {
|
||||||
|
background: radial-gradient(ellipse at 60% 30%, #7d1fa8 0%, #3a1160 45%, #1a0b2e 100%);
|
||||||
|
}
|
||||||
|
.bg-night {
|
||||||
|
background-color: #120a1f;
|
||||||
|
background-image: radial-gradient(rgba(255, 255, 255, 0.06) 1px, transparent 1px);
|
||||||
|
background-size: 26px 26px;
|
||||||
|
}
|
||||||
|
.text-stroke-dark {
|
||||||
|
-webkit-text-stroke: 2px #0a0a0a;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom Swiper Styles */
|
||||||
|
.swiper-pagination-bullet {
|
||||||
|
background: rgba(255, 255, 255, 0.3) !important;
|
||||||
|
opacity: 1 !important;
|
||||||
|
}
|
||||||
|
.swiper-pagination-bullet-active {
|
||||||
|
background: #FFC72C !important;
|
||||||
|
width: 24px !important;
|
||||||
|
border-radius: 4px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile view media queries for The Vision and For You sections */
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
#the-vision,
|
||||||
|
#for-you {
|
||||||
|
padding-left: 1.25rem !important;
|
||||||
|
padding-right: 1.25rem !important;
|
||||||
|
padding-top: 4rem !important;
|
||||||
|
padding-bottom: 4rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#the-vision h2,
|
||||||
|
#for-you h2 {
|
||||||
|
font-size: 2.5rem !important;
|
||||||
|
line-height: 1.05 !important;
|
||||||
|
letter-spacing: -0.02em !important;
|
||||||
|
word-break: break-word !important;
|
||||||
|
overflow-wrap: break-word !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Optimize card spacing and shadow sizes for mobile neobrutalism style */
|
||||||
|
#the-vision .nb,
|
||||||
|
#for-you .nb {
|
||||||
|
padding: 1rem !important;
|
||||||
|
font-size: 0.95rem !important;
|
||||||
|
box-shadow: 4px 4px 0 0 #0a0a0a !important;
|
||||||
|
border-width: 2px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Decrease vertical spacing inside grid for better layout flow on mobile */
|
||||||
|
#the-vision .grid,
|
||||||
|
#for-you .grid {
|
||||||
|
gap: 1.25rem !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,42 @@
|
|||||||
import type { Metadata } from "next";
|
import type { Metadata, Viewport } from "next";
|
||||||
import { Geist, Geist_Mono } from "next/font/google";
|
import { Sora, Inter } from "next/font/google";
|
||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
|
import Providers from "@/components/common/Providers";
|
||||||
|
|
||||||
const geistSans = Geist({
|
const sora = Sora({
|
||||||
variable: "--font-geist-sans",
|
variable: "--font-sora",
|
||||||
subsets: ["latin"],
|
subsets: ["latin"],
|
||||||
|
weight: ["300", "400", "500", "600", "700", "800"],
|
||||||
|
display: "swap",
|
||||||
});
|
});
|
||||||
|
|
||||||
const geistMono = Geist_Mono({
|
const inter = Inter({
|
||||||
variable: "--font-geist-mono",
|
variable: "--font-inter",
|
||||||
subsets: ["latin"],
|
subsets: ["latin"],
|
||||||
|
weight: ["300", "400", "500", "600", "700", "800"],
|
||||||
|
display: "swap",
|
||||||
});
|
});
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Create Next App",
|
title: "LytsUp | Earn. Spend. Live More. - Gen Z Social Currency Platform",
|
||||||
description: "Generated by create next app",
|
description:
|
||||||
|
"LytsUp is a gamified social currency platform where Gen Z earns LYTs through real-world activities like walking, visiting stores, referrals, playing games, and redeems them for vouchers from premium partner brands.",
|
||||||
|
keywords: [
|
||||||
|
"LytsUp",
|
||||||
|
"Social Currency",
|
||||||
|
"Walk to Earn",
|
||||||
|
"Gen Z rewards",
|
||||||
|
"Loyalty points",
|
||||||
|
"Gamified rewards",
|
||||||
|
"Earn LYTs",
|
||||||
|
"Play to Earn",
|
||||||
|
],
|
||||||
|
authors: [{ name: "LytsUp Team" }],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const viewport: Viewport = {
|
||||||
|
width: "device-width",
|
||||||
|
initialScale: 1.0,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function RootLayout({
|
export default function RootLayout({
|
||||||
@@ -25,9 +47,14 @@ export default function RootLayout({
|
|||||||
return (
|
return (
|
||||||
<html
|
<html
|
||||||
lang="en"
|
lang="en"
|
||||||
className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`}
|
className={`${sora.variable} ${inter.variable} h-full antialiased`}
|
||||||
|
suppressHydrationWarning
|
||||||
>
|
>
|
||||||
<body className="min-h-full flex flex-col">{children}</body>
|
<body className="min-h-full flex flex-col bg-dark text-white selection:bg-primary selection:text-dark">
|
||||||
|
<Providers>
|
||||||
|
{children}
|
||||||
|
</Providers>
|
||||||
|
</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
120
src/app/page.tsx
@@ -1,65 +1,67 @@
|
|||||||
import Image from "next/image";
|
import SideNav, { NavSection } from "@/components/layout/SideNav";
|
||||||
|
import {
|
||||||
|
Intro,
|
||||||
|
TheWhy,
|
||||||
|
TheProblem,
|
||||||
|
TheShift,
|
||||||
|
MeetLyts,
|
||||||
|
OneDay,
|
||||||
|
Feelings,
|
||||||
|
Difference,
|
||||||
|
TheLoop,
|
||||||
|
} from "@/components/story/SectionsA";
|
||||||
|
import {
|
||||||
|
ForYou,
|
||||||
|
ForBusiness,
|
||||||
|
WhyNow,
|
||||||
|
HowItWorks,
|
||||||
|
OldVsNew,
|
||||||
|
TheVision,
|
||||||
|
OurBelief,
|
||||||
|
ThePromise,
|
||||||
|
} from "@/components/story/SectionsB";
|
||||||
|
|
||||||
|
const sections: NavSection[] = [
|
||||||
|
{ id: "intro", label: "Intro" },
|
||||||
|
{ id: "the-why", label: "The Why" },
|
||||||
|
{ id: "the-problem", label: "The Problem" },
|
||||||
|
{ id: "the-shift", label: "The Shift" },
|
||||||
|
{ id: "lyts", label: "Meet LYTs" },
|
||||||
|
{ id: "one-day", label: "One Day" },
|
||||||
|
{ id: "feelings", label: "Feelings" },
|
||||||
|
{ id: "difference", label: "Difference" },
|
||||||
|
{ id: "the-loop", label: "The Loop" },
|
||||||
|
{ id: "for-you", label: "For You" },
|
||||||
|
{ id: "for-business", label: "For Business" },
|
||||||
|
{ id: "why-now", label: "Why Now" },
|
||||||
|
{ id: "how-it-works", label: "How It Works" },
|
||||||
|
{ id: "old-vs-new", label: "Old vs New" },
|
||||||
|
{ id: "the-vision", label: "The Vision" },
|
||||||
|
{ id: "our-belief", label: "Our Belief" },
|
||||||
|
{ id: "the-promise", label: "The Promise" },
|
||||||
|
];
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col flex-1 items-center justify-center bg-zinc-50 font-sans dark:bg-black">
|
<main className="relative w-full">
|
||||||
<main className="flex flex-1 w-full max-w-3xl flex-col items-center justify-between py-32 px-16 bg-white dark:bg-black sm:items-start">
|
<SideNav sections={sections} />
|
||||||
<Image
|
<Intro />
|
||||||
className="dark:invert"
|
<TheWhy />
|
||||||
src="/next.svg"
|
<TheProblem />
|
||||||
alt="Next.js logo"
|
<TheShift />
|
||||||
width={100}
|
<MeetLyts />
|
||||||
height={20}
|
<OneDay />
|
||||||
priority
|
<Feelings />
|
||||||
/>
|
<Difference />
|
||||||
<div className="flex flex-col items-center gap-6 text-center sm:items-start sm:text-left">
|
<TheLoop />
|
||||||
<h1 className="max-w-xs text-3xl font-semibold leading-10 tracking-tight text-black dark:text-zinc-50">
|
<ForYou />
|
||||||
To get started, edit the page.tsx file.
|
<ForBusiness />
|
||||||
</h1>
|
<WhyNow />
|
||||||
<p className="max-w-md text-lg leading-8 text-zinc-600 dark:text-zinc-400">
|
<HowItWorks />
|
||||||
Looking for a starting point or more instructions? Head over to{" "}
|
<OldVsNew />
|
||||||
<a
|
<TheVision />
|
||||||
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
<OurBelief />
|
||||||
className="font-medium text-zinc-950 dark:text-zinc-50"
|
<ThePromise />
|
||||||
>
|
|
||||||
Templates
|
|
||||||
</a>{" "}
|
|
||||||
or the{" "}
|
|
||||||
<a
|
|
||||||
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
|
||||||
className="font-medium text-zinc-950 dark:text-zinc-50"
|
|
||||||
>
|
|
||||||
Learning
|
|
||||||
</a>{" "}
|
|
||||||
center.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col gap-4 text-base font-medium sm:flex-row">
|
|
||||||
<a
|
|
||||||
className="flex h-12 w-full items-center justify-center gap-2 rounded-full bg-foreground px-5 text-background transition-colors hover:bg-[#383838] dark:hover:bg-[#ccc] md:w-[158px]"
|
|
||||||
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
<Image
|
|
||||||
className="dark:invert"
|
|
||||||
src="/vercel.svg"
|
|
||||||
alt="Vercel logomark"
|
|
||||||
width={16}
|
|
||||||
height={16}
|
|
||||||
/>
|
|
||||||
Deploy Now
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
className="flex h-12 w-full items-center justify-center rounded-full border border-solid border-black/[.08] px-5 transition-colors hover:border-transparent hover:bg-black/[.04] dark:border-white/[.145] dark:hover:bg-[#1a1a1a] md:w-[158px]"
|
|
||||||
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
Documentation
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</main>
|
</main>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
227
src/components/animations/Hero3D.tsx
Normal file
@@ -0,0 +1,227 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useRef } from "react";
|
||||||
|
import * as THREE from "three";
|
||||||
|
|
||||||
|
export default function Hero3D() {
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!containerRef.current) return;
|
||||||
|
|
||||||
|
const container = containerRef.current;
|
||||||
|
const width = container.clientWidth;
|
||||||
|
const height = container.clientHeight;
|
||||||
|
|
||||||
|
// Scene setup
|
||||||
|
const scene = new THREE.Scene();
|
||||||
|
|
||||||
|
// Camera setup
|
||||||
|
const camera = new THREE.PerspectiveCamera(45, width / height, 0.1, 100);
|
||||||
|
camera.position.z = 8;
|
||||||
|
|
||||||
|
// Renderer setup
|
||||||
|
const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
|
||||||
|
renderer.setSize(width, height);
|
||||||
|
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
|
||||||
|
container.appendChild(renderer.domElement);
|
||||||
|
|
||||||
|
// Lights
|
||||||
|
const ambientLight = new THREE.AmbientLight(0xffffff, 0.4);
|
||||||
|
scene.add(ambientLight);
|
||||||
|
|
||||||
|
const keyLight = new THREE.DirectionalLight(0xffffff, 1.5);
|
||||||
|
keyLight.position.set(5, 5, 5);
|
||||||
|
scene.add(keyLight);
|
||||||
|
|
||||||
|
const pinkLight = new THREE.PointLight(0xff2e88, 5, 15);
|
||||||
|
pinkLight.position.set(-4, -2, 2);
|
||||||
|
scene.add(pinkLight);
|
||||||
|
|
||||||
|
const goldLight = new THREE.PointLight(0xffc72c, 8, 15);
|
||||||
|
goldLight.position.set(4, 3, 2);
|
||||||
|
scene.add(goldLight);
|
||||||
|
|
||||||
|
// Creating the LYT Coin (an Octahedron + Cylinder compound shape for a stylized look)
|
||||||
|
const coinGroup = new THREE.Group();
|
||||||
|
|
||||||
|
// Core Crystal (Diamond/Octahedron)
|
||||||
|
const crystalGeom = new THREE.OctahedronGeometry(1.6, 0);
|
||||||
|
const crystalMat = new THREE.MeshStandardMaterial({
|
||||||
|
color: 0xffc72c,
|
||||||
|
roughness: 0.1,
|
||||||
|
metalness: 0.9,
|
||||||
|
emissive: 0xffa500,
|
||||||
|
emissiveIntensity: 0.3,
|
||||||
|
flatShading: true,
|
||||||
|
});
|
||||||
|
const crystal = new THREE.Mesh(crystalGeom, crystalMat);
|
||||||
|
coinGroup.add(crystal);
|
||||||
|
|
||||||
|
// Outer Ring
|
||||||
|
const ringGeom = new THREE.TorusGeometry(2.3, 0.12, 16, 100);
|
||||||
|
const ringMat = new THREE.MeshStandardMaterial({
|
||||||
|
color: 0xff2e88,
|
||||||
|
roughness: 0.2,
|
||||||
|
metalness: 0.8,
|
||||||
|
emissive: 0x8b3dff,
|
||||||
|
emissiveIntensity: 0.2,
|
||||||
|
});
|
||||||
|
const ring = new THREE.Mesh(ringGeom, ringMat);
|
||||||
|
ring.rotation.x = Math.PI / 2;
|
||||||
|
coinGroup.add(ring);
|
||||||
|
|
||||||
|
// Floating minor energy crystals
|
||||||
|
const smallCrystals: THREE.Mesh[] = [];
|
||||||
|
const smallCrystalGeom = new THREE.OctahedronGeometry(0.3, 0);
|
||||||
|
const smallCrystalMat = new THREE.MeshStandardMaterial({
|
||||||
|
color: 0xc8ff37,
|
||||||
|
roughness: 0.1,
|
||||||
|
metalness: 0.8,
|
||||||
|
flatShading: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
for (let i = 0; i < 4; i++) {
|
||||||
|
const smallCrys = new THREE.Mesh(smallCrystalGeom, smallCrystalMat);
|
||||||
|
const angle = (i / 4) * Math.PI * 2;
|
||||||
|
smallCrys.position.set(Math.cos(angle) * 3.2, Math.sin(angle) * 3.2, 0);
|
||||||
|
coinGroup.add(smallCrys);
|
||||||
|
smallCrystals.push(smallCrys);
|
||||||
|
}
|
||||||
|
|
||||||
|
scene.add(coinGroup);
|
||||||
|
|
||||||
|
// Floating particles (energy dust)
|
||||||
|
const particleCount = 120;
|
||||||
|
const particleGeometry = new THREE.BufferGeometry();
|
||||||
|
const particlePositions = new Float32Array(particleCount * 3);
|
||||||
|
|
||||||
|
for (let i = 0; i < particleCount * 3; i += 3) {
|
||||||
|
// Circle layout with random spreads
|
||||||
|
const radius = 2 + Math.random() * 8;
|
||||||
|
const theta = Math.random() * Math.PI * 2;
|
||||||
|
const phi = Math.acos(2 * Math.random() - 1);
|
||||||
|
|
||||||
|
particlePositions[i] = radius * Math.sin(phi) * Math.cos(theta);
|
||||||
|
particlePositions[i + 1] = radius * Math.sin(phi) * Math.sin(theta);
|
||||||
|
particlePositions[i + 2] = radius * Math.cos(phi);
|
||||||
|
}
|
||||||
|
|
||||||
|
particleGeometry.setAttribute(
|
||||||
|
"position",
|
||||||
|
new THREE.BufferAttribute(particlePositions, 3)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Dynamic glowing points
|
||||||
|
const pMaterial = new THREE.PointsMaterial({
|
||||||
|
color: 0xffffff,
|
||||||
|
size: 0.05,
|
||||||
|
transparent: true,
|
||||||
|
opacity: 0.8,
|
||||||
|
blending: THREE.AdditiveBlending,
|
||||||
|
});
|
||||||
|
|
||||||
|
const particleSystem = new THREE.Points(particleGeometry, pMaterial);
|
||||||
|
scene.add(particleSystem);
|
||||||
|
|
||||||
|
// Mouse interaction states
|
||||||
|
let mouseX = 0;
|
||||||
|
let mouseY = 0;
|
||||||
|
let targetX = 0;
|
||||||
|
let targetY = 0;
|
||||||
|
|
||||||
|
const handleMouseMove = (event: MouseEvent) => {
|
||||||
|
const rect = container.getBoundingClientRect();
|
||||||
|
const x = event.clientX - rect.left;
|
||||||
|
const y = event.clientY - rect.top;
|
||||||
|
|
||||||
|
mouseX = (x / width) * 2 - 1;
|
||||||
|
mouseY = -(y / height) * 2 + 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener("mousemove", handleMouseMove);
|
||||||
|
|
||||||
|
// Animation Loop
|
||||||
|
let animationFrameId: number;
|
||||||
|
let clock = new THREE.Clock();
|
||||||
|
|
||||||
|
const animate = () => {
|
||||||
|
const elapsedTime = clock.getElapsedTime();
|
||||||
|
|
||||||
|
// Slow passive rotation
|
||||||
|
coinGroup.rotation.y = elapsedTime * 0.4;
|
||||||
|
coinGroup.rotation.x = Math.sin(elapsedTime * 0.5) * 0.2;
|
||||||
|
|
||||||
|
// Outer ring counter-rotation
|
||||||
|
ring.rotation.z = -elapsedTime * 0.6;
|
||||||
|
|
||||||
|
// Small crystals float around
|
||||||
|
smallCrystals.forEach((crys, idx) => {
|
||||||
|
const offset = idx * (Math.PI / 2);
|
||||||
|
crys.position.y = Math.sin(elapsedTime * 1.5 + offset) * 3.4;
|
||||||
|
crys.position.x = Math.cos(elapsedTime * 1.5 + offset) * 3.4;
|
||||||
|
crys.rotation.x += 0.01;
|
||||||
|
crys.rotation.y += 0.02;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Drifting particles
|
||||||
|
particleSystem.rotation.y = elapsedTime * 0.05;
|
||||||
|
particleSystem.rotation.x = Math.sin(elapsedTime * 0.02) * 0.1;
|
||||||
|
|
||||||
|
// Smooth mouse follow (LERP)
|
||||||
|
targetX += (mouseX - targetX) * 0.05;
|
||||||
|
targetY += (mouseY - targetY) * 0.05;
|
||||||
|
|
||||||
|
coinGroup.position.x = targetX * 1.5;
|
||||||
|
coinGroup.position.y = targetY * 1.5;
|
||||||
|
|
||||||
|
// Dynamic lighting follow
|
||||||
|
goldLight.position.x = 4 + targetX * 2;
|
||||||
|
goldLight.position.y = 3 + targetY * 2;
|
||||||
|
|
||||||
|
renderer.render(scene, camera);
|
||||||
|
animationFrameId = requestAnimationFrame(animate);
|
||||||
|
};
|
||||||
|
|
||||||
|
animate();
|
||||||
|
|
||||||
|
// Resize Handler
|
||||||
|
const handleResize = () => {
|
||||||
|
if (!container) return;
|
||||||
|
const newWidth = container.clientWidth;
|
||||||
|
const newHeight = container.clientHeight;
|
||||||
|
|
||||||
|
camera.aspect = newWidth / newHeight;
|
||||||
|
camera.updateProjectionMatrix();
|
||||||
|
|
||||||
|
renderer.setSize(newWidth, newHeight);
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener("resize", handleResize);
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
return () => {
|
||||||
|
cancelAnimationFrame(animationFrameId);
|
||||||
|
window.removeEventListener("mousemove", handleMouseMove);
|
||||||
|
window.removeEventListener("resize", handleResize);
|
||||||
|
container.removeChild(renderer.domElement);
|
||||||
|
|
||||||
|
// Dispose materials/geometries to prevent memory leaks
|
||||||
|
crystalGeom.dispose();
|
||||||
|
crystalMat.dispose();
|
||||||
|
ringGeom.dispose();
|
||||||
|
ringMat.dispose();
|
||||||
|
smallCrystalGeom.dispose();
|
||||||
|
smallCrystalMat.dispose();
|
||||||
|
particleGeometry.dispose();
|
||||||
|
pMaterial.dispose();
|
||||||
|
renderer.dispose();
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={containerRef} className="w-full h-full relative">
|
||||||
|
<div className="absolute inset-0 bg-radial-[circle_at_center,_transparent_60%,_var(--color-dark)_100%] pointer-events-none" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
44
src/components/common/Providers.tsx
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import { ThemeProvider } from "next-themes";
|
||||||
|
import Lenis from "lenis";
|
||||||
|
import { UserProvider } from "@/lib/context";
|
||||||
|
|
||||||
|
export default function Providers({ children }: { children: React.ReactNode }) {
|
||||||
|
useEffect(() => {
|
||||||
|
// Initialize Lenis smooth scroll (client-side only)
|
||||||
|
const lenis = new Lenis({
|
||||||
|
duration: 1.2,
|
||||||
|
easing: (t) => Math.min(1, 1.001 - Math.pow(2, -10 * t)),
|
||||||
|
orientation: "vertical",
|
||||||
|
gestureOrientation: "vertical",
|
||||||
|
smoothWheel: true,
|
||||||
|
wheelMultiplier: 1,
|
||||||
|
touchMultiplier: 2,
|
||||||
|
});
|
||||||
|
|
||||||
|
function raf(time: number) {
|
||||||
|
lenis.raf(time);
|
||||||
|
requestAnimationFrame(raf);
|
||||||
|
}
|
||||||
|
|
||||||
|
requestAnimationFrame(raf);
|
||||||
|
|
||||||
|
// Expose instance so components can trigger programmatic smooth scroll
|
||||||
|
(window as unknown as { lenis?: Lenis }).lenis = lenis;
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
lenis.destroy();
|
||||||
|
delete (window as unknown as { lenis?: Lenis }).lenis;
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ThemeProvider attribute="class" defaultTheme="dark" enableSystem={false}>
|
||||||
|
<UserProvider>
|
||||||
|
{children}
|
||||||
|
</UserProvider>
|
||||||
|
</ThemeProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
391
src/components/games/CoinCatcher.tsx
Normal file
@@ -0,0 +1,391 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
import { useUser } from "@/lib/context";
|
||||||
|
import { Play, RotateCcw, Coins, ShieldAlert, Award, Heart } from "lucide-react";
|
||||||
|
|
||||||
|
interface Particle {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
vx: number;
|
||||||
|
vy: number;
|
||||||
|
color: string;
|
||||||
|
alpha: number;
|
||||||
|
size: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GameObject {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
size: number;
|
||||||
|
speed: number;
|
||||||
|
type: "coin" | "obstacle";
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function CoinCatcher() {
|
||||||
|
const { addLyts } = useUser();
|
||||||
|
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||||
|
|
||||||
|
// Game states
|
||||||
|
const [gameState, setGameState] = useState<"idle" | "playing" | "gameover">("idle");
|
||||||
|
const [score, setScore] = useState(0);
|
||||||
|
const [lives, setLives] = useState(3);
|
||||||
|
const [claimed, setClaimed] = useState(false);
|
||||||
|
|
||||||
|
// References for mutable game loop variables
|
||||||
|
const requestRef = useRef<number>(0);
|
||||||
|
const objectsRef = useRef<GameObject[]>([]);
|
||||||
|
const particlesRef = useRef<Particle[]>([]);
|
||||||
|
const playerXRef = useRef<number>(200);
|
||||||
|
const livesRef = useRef<number>(3);
|
||||||
|
const scoreRef = useRef<number>(0);
|
||||||
|
const spawnTimerRef = useRef<number>(0);
|
||||||
|
|
||||||
|
// Start the game
|
||||||
|
const startGame = () => {
|
||||||
|
setGameState("playing");
|
||||||
|
setScore(0);
|
||||||
|
setLives(3);
|
||||||
|
setClaimed(false);
|
||||||
|
livesRef.current = 3;
|
||||||
|
scoreRef.current = 0;
|
||||||
|
objectsRef.current = [];
|
||||||
|
particlesRef.current = [];
|
||||||
|
playerXRef.current = 200;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Claim score as LYTs
|
||||||
|
const claimEarnings = () => {
|
||||||
|
if (score > 0 && !claimed) {
|
||||||
|
addLyts(score);
|
||||||
|
setClaimed(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const canvas = canvasRef.current;
|
||||||
|
if (!canvas) return;
|
||||||
|
const ctx = canvas.getContext("2d");
|
||||||
|
if (!ctx) return;
|
||||||
|
|
||||||
|
// Track mouse coordinates to control player position
|
||||||
|
const handleMouseMove = (e: MouseEvent) => {
|
||||||
|
const rect = canvas.getBoundingClientRect();
|
||||||
|
const relativeX = e.clientX - rect.left;
|
||||||
|
// Clamp player position within canvas bounds
|
||||||
|
playerXRef.current = Math.max(30, Math.min(canvas.width - 30, relativeX));
|
||||||
|
};
|
||||||
|
|
||||||
|
// Track touch events for mobile support
|
||||||
|
const handleTouchMove = (e: TouchEvent) => {
|
||||||
|
if (e.touches.length === 0) return;
|
||||||
|
const rect = canvas.getBoundingClientRect();
|
||||||
|
const relativeX = e.touches[0].clientX - rect.left;
|
||||||
|
playerXRef.current = Math.max(30, Math.min(canvas.width - 30, relativeX));
|
||||||
|
};
|
||||||
|
|
||||||
|
canvas.addEventListener("mousemove", handleMouseMove);
|
||||||
|
canvas.addEventListener("touchmove", handleTouchMove);
|
||||||
|
|
||||||
|
// Particle emitter for explosion effects
|
||||||
|
const createExplosion = (x: number, y: number, color: string) => {
|
||||||
|
for (let i = 0; i < 15; i++) {
|
||||||
|
particlesRef.current.push({
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
vx: (Math.random() - 0.5) * 6,
|
||||||
|
vy: (Math.random() - 0.5) * 6 - 2,
|
||||||
|
color,
|
||||||
|
alpha: 1,
|
||||||
|
size: Math.random() * 3 + 2,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Main physics and rendering game loop
|
||||||
|
const gameLoop = () => {
|
||||||
|
if (gameState !== "playing") return;
|
||||||
|
|
||||||
|
// Clear Canvas
|
||||||
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||||
|
|
||||||
|
// 1. Draw Catcher Bucket (Player)
|
||||||
|
const pX = playerXRef.current;
|
||||||
|
const pY = canvas.height - 35;
|
||||||
|
|
||||||
|
// Outer glow for player
|
||||||
|
ctx.shadowBlur = 15;
|
||||||
|
ctx.shadowColor = "#ffc72c";
|
||||||
|
|
||||||
|
ctx.fillStyle = "#ffc72c";
|
||||||
|
ctx.beginPath();
|
||||||
|
// Draw a sleek curved basket shape
|
||||||
|
ctx.roundRect(pX - 35, pY, 70, 15, [4, 4, 10, 10]);
|
||||||
|
ctx.fill();
|
||||||
|
|
||||||
|
// Inside mesh glow
|
||||||
|
ctx.shadowBlur = 5;
|
||||||
|
ctx.shadowColor = "#ff2e88";
|
||||||
|
ctx.fillStyle = "#ff2e88";
|
||||||
|
ctx.fillRect(pX - 25, pY + 4, 50, 2);
|
||||||
|
ctx.shadowBlur = 0; // Reset shadow
|
||||||
|
|
||||||
|
// 2. Spawn Items
|
||||||
|
spawnTimerRef.current += 1;
|
||||||
|
if (spawnTimerRef.current > 40) {
|
||||||
|
spawnTimerRef.current = 0;
|
||||||
|
const type = Math.random() > 0.35 ? "coin" : "obstacle";
|
||||||
|
objectsRef.current.push({
|
||||||
|
x: Math.random() * (canvas.width - 40) + 20,
|
||||||
|
y: -20,
|
||||||
|
size: type === "coin" ? 12 : 14,
|
||||||
|
speed: Math.random() * 2.5 + 3 + scoreRef.current * 0.05, // Speed increases as score goes up
|
||||||
|
type,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Update and Draw Falling Items
|
||||||
|
const activeObjects: GameObject[] = [];
|
||||||
|
objectsRef.current.forEach((obj) => {
|
||||||
|
obj.y += obj.speed;
|
||||||
|
|
||||||
|
// Collision Check (AABB / Circle collision against basket)
|
||||||
|
const hitPlayer =
|
||||||
|
obj.y + obj.size >= pY &&
|
||||||
|
obj.y - obj.size <= pY + 15 &&
|
||||||
|
obj.x >= pX - 45 &&
|
||||||
|
obj.x <= pX + 45;
|
||||||
|
|
||||||
|
if (hitPlayer) {
|
||||||
|
if (obj.type === "coin") {
|
||||||
|
scoreRef.current += 1;
|
||||||
|
setScore(scoreRef.current);
|
||||||
|
createExplosion(obj.x, obj.y, "#ffc72c");
|
||||||
|
} else {
|
||||||
|
livesRef.current -= 1;
|
||||||
|
setLives(livesRef.current);
|
||||||
|
createExplosion(obj.x, obj.y, "#ff2e88");
|
||||||
|
|
||||||
|
// Screen shake effect on bomb hit
|
||||||
|
ctx.translate((Math.random() - 0.5) * 15, (Math.random() - 0.5) * 15);
|
||||||
|
|
||||||
|
if (livesRef.current <= 0) {
|
||||||
|
setGameState("gameover");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (obj.y < canvas.height + 20) {
|
||||||
|
activeObjects.push(obj);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
objectsRef.current = activeObjects;
|
||||||
|
|
||||||
|
// Draw active falling objects
|
||||||
|
objectsRef.current.forEach((obj) => {
|
||||||
|
ctx.shadowBlur = 12;
|
||||||
|
if (obj.type === "coin") {
|
||||||
|
// Yellow LYT Coin
|
||||||
|
ctx.shadowColor = "#ffc72c";
|
||||||
|
ctx.fillStyle = "#ffc72c";
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(obj.x, obj.y, obj.size, 0, Math.PI * 2);
|
||||||
|
ctx.fill();
|
||||||
|
|
||||||
|
// Outer yellow shine ring
|
||||||
|
ctx.strokeStyle = "#ffffff";
|
||||||
|
ctx.lineWidth = 1.5;
|
||||||
|
ctx.stroke();
|
||||||
|
|
||||||
|
// Core details
|
||||||
|
ctx.fillStyle = "#ffffff";
|
||||||
|
ctx.font = "bold 9px Arial";
|
||||||
|
ctx.textAlign = "center";
|
||||||
|
ctx.textBaseline = "middle";
|
||||||
|
ctx.fillText("L", obj.x, obj.y);
|
||||||
|
} else {
|
||||||
|
// Purple Obstacle Spike
|
||||||
|
ctx.shadowColor = "#8b3dff";
|
||||||
|
ctx.fillStyle = "#8b3dff";
|
||||||
|
|
||||||
|
ctx.beginPath();
|
||||||
|
// Draw simple triangle obstacle
|
||||||
|
ctx.moveTo(obj.x, obj.y - obj.size);
|
||||||
|
ctx.lineTo(obj.x + obj.size, obj.y + obj.size);
|
||||||
|
ctx.lineTo(obj.x - obj.size, obj.y + obj.size);
|
||||||
|
ctx.closePath();
|
||||||
|
ctx.fill();
|
||||||
|
|
||||||
|
// Spiky border
|
||||||
|
ctx.strokeStyle = "#ff2e88";
|
||||||
|
ctx.lineWidth = 1;
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
ctx.shadowBlur = 0;
|
||||||
|
|
||||||
|
// 4. Update and Draw Particle Explosions
|
||||||
|
particlesRef.current.forEach((p, idx) => {
|
||||||
|
p.x += p.vx;
|
||||||
|
p.y += p.vy;
|
||||||
|
p.alpha -= 0.025;
|
||||||
|
|
||||||
|
ctx.fillStyle = p.color;
|
||||||
|
ctx.globalAlpha = Math.max(0, p.alpha);
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2);
|
||||||
|
ctx.fill();
|
||||||
|
});
|
||||||
|
ctx.globalAlpha = 1.0;
|
||||||
|
particlesRef.current = particlesRef.current.filter((p) => p.alpha > 0);
|
||||||
|
|
||||||
|
// Loop
|
||||||
|
requestRef.current = requestAnimationFrame(gameLoop);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (gameState === "playing") {
|
||||||
|
requestRef.current = requestAnimationFrame(gameLoop);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
cancelAnimationFrame(requestRef.current);
|
||||||
|
canvas.removeEventListener("mousemove", handleMouseMove);
|
||||||
|
canvas.removeEventListener("touchmove", handleTouchMove);
|
||||||
|
};
|
||||||
|
}, [gameState]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section id="play" className="py-24 bg-[#060606] relative overflow-hidden">
|
||||||
|
{/* Background glow */}
|
||||||
|
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[600px] h-[600px] rounded-full bg-accent-pink/5 blur-[120px] pointer-events-none" />
|
||||||
|
|
||||||
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 relative z-10">
|
||||||
|
|
||||||
|
{/* Header */}
|
||||||
|
<div className="text-center max-w-3xl mx-auto mb-16 space-y-4">
|
||||||
|
<span className="text-accent-lime font-extrabold text-xs tracking-widest uppercase">
|
||||||
|
Play & Earn
|
||||||
|
</span>
|
||||||
|
<h2 className="text-3xl sm:text-5xl font-extrabold font-sora text-white leading-tight">
|
||||||
|
The <span className="text-glow-yellow text-primary">LYT Catcher</span> Mini-Game
|
||||||
|
</h2>
|
||||||
|
<p className="text-white/60 text-base sm:text-lg">
|
||||||
|
Use your mouse or touch screen to catch the falling yellow LYT coins. Avoid the purple hazard spikes! Points are credited straight to your wallet.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Game Box Container */}
|
||||||
|
<div className="max-w-xl mx-auto">
|
||||||
|
<div className="relative rounded-3xl overflow-hidden bg-[#0a0a0a] border border-white/5 shadow-[0_20px_50px_rgba(0,0,0,0.8)] aspect-[4/5] sm:aspect-[4/5] w-full">
|
||||||
|
|
||||||
|
{/* Top Game UI Bar (While Playing) */}
|
||||||
|
{gameState === "playing" && (
|
||||||
|
<div className="absolute top-4 left-4 right-4 flex justify-between items-center z-10">
|
||||||
|
{/* Score */}
|
||||||
|
<div className="flex items-center space-x-1.5 px-3 py-1.5 rounded-full bg-black/50 border border-white/10 text-primary font-bold text-sm">
|
||||||
|
<Coins className="w-4 h-4 text-primary animate-pulse" />
|
||||||
|
<span>Score: {score}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Lives */}
|
||||||
|
<div className="flex items-center space-x-1 px-3 py-1.5 rounded-full bg-black/50 border border-white/10 text-red-500 font-semibold text-sm">
|
||||||
|
{Array.from({ length: 3 }).map((_, i) => (
|
||||||
|
<Heart
|
||||||
|
key={i}
|
||||||
|
className={`w-4 h-4 fill-current ${
|
||||||
|
i < lives ? "text-red-500" : "text-white/20"
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Canvas */}
|
||||||
|
<canvas
|
||||||
|
ref={canvasRef}
|
||||||
|
width={480}
|
||||||
|
height={600}
|
||||||
|
className="w-full h-full block bg-radial-gradient"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Screens overlays */}
|
||||||
|
|
||||||
|
{/* 1. Idle Overlay */}
|
||||||
|
{gameState === "idle" && (
|
||||||
|
<div className="absolute inset-0 bg-dark/90 flex flex-col items-center justify-center p-6 text-center space-y-6">
|
||||||
|
<div className="w-16 h-16 rounded-3xl bg-primary/10 border border-primary/20 flex items-center justify-center text-primary shadow-[0_0_20px_rgba(255,199,44,0.1)]">
|
||||||
|
<Coins className="w-8 h-8 text-primary animate-bounce" />
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<h3 className="text-2xl font-bold font-sora text-white">Ready to Catch?</h3>
|
||||||
|
<p className="text-white/60 text-xs max-w-xs leading-relaxed">
|
||||||
|
Move your mouse or finger horizontally to control the catcher basket. Grab coins, avoid spikes!
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={startGame}
|
||||||
|
className="px-8 py-3.5 rounded-2xl bg-primary text-dark font-extrabold text-sm tracking-wider flex items-center justify-center gap-2 shadow-[0_0_25px_rgba(255,199,44,0.3)] hover:scale-105 transition-transform cursor-pointer"
|
||||||
|
>
|
||||||
|
<Play className="w-4.5 h-4.5 fill-current" />
|
||||||
|
<span>Start Game</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 2. Gameover Overlay */}
|
||||||
|
{gameState === "gameover" && (
|
||||||
|
<div className="absolute inset-0 bg-dark/95 flex flex-col items-center justify-center p-6 text-center space-y-8 animate-in fade-in duration-300">
|
||||||
|
<div className="w-16 h-16 rounded-3xl bg-red-500/10 border border-red-500/20 flex items-center justify-center text-red-500">
|
||||||
|
<ShieldAlert className="w-8 h-8" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<h3 className="text-3xl font-extrabold font-sora text-white">Game Over</h3>
|
||||||
|
<p className="text-white/60 text-sm">
|
||||||
|
You caught <span className="text-primary font-bold text-lg">{score}</span> LYTs!
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Claim details */}
|
||||||
|
<div className="w-full max-w-xs space-y-3">
|
||||||
|
<button
|
||||||
|
onClick={claimEarnings}
|
||||||
|
disabled={score === 0 || claimed}
|
||||||
|
className={`w-full py-4 rounded-2xl font-extrabold text-sm tracking-wide flex items-center justify-center gap-2 cursor-pointer transition-all ${
|
||||||
|
claimed
|
||||||
|
? "bg-accent-lime text-dark shadow-[0_0_20px_rgba(200,255,55,0.3)]"
|
||||||
|
: score === 0
|
||||||
|
? "bg-white/5 text-white/20 border border-white/5 cursor-not-allowed"
|
||||||
|
: "bg-primary text-dark shadow-[0_0_25px_rgba(255,199,44,0.3)]"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{claimed ? (
|
||||||
|
<>
|
||||||
|
<Award className="w-4.5 h-4.5" />
|
||||||
|
<span>+{score} LYTs Claimed!</span>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Coins className="w-4.5 h-4.5" />
|
||||||
|
<span>Claim Reward (+{score} LYTs)</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={startGame}
|
||||||
|
className="w-full py-3.5 rounded-2xl bg-white/5 hover:bg-white/10 border border-white/10 text-white font-bold text-sm tracking-wide cursor-pointer flex items-center justify-center gap-2 transition-all"
|
||||||
|
>
|
||||||
|
<RotateCcw className="w-4 h-4" />
|
||||||
|
<span>Play Again</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
152
src/components/layout/Footer.tsx
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
import { Send, Instagram, Twitter, MessageSquare, Heart } from "lucide-react";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
|
export default function Footer() {
|
||||||
|
const [email, setEmail] = useState("");
|
||||||
|
const [subscribed, setSubscribed] = useState(false);
|
||||||
|
|
||||||
|
const handleSubscribe = (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (email) {
|
||||||
|
setSubscribed(true);
|
||||||
|
setEmail("");
|
||||||
|
setTimeout(() => setSubscribed(false), 5000);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<footer className="relative bg-[#060606] border-t border-white/5 pt-16 pb-8 overflow-hidden">
|
||||||
|
{/* Ambient background light glows */}
|
||||||
|
<div className="absolute top-0 left-1/4 -translate-y-1/2 w-80 h-80 rounded-full bg-accent-purple/10 blur-[80px] pointer-events-none" />
|
||||||
|
<div className="absolute top-0 right-1/4 -translate-y-1/2 w-80 h-80 rounded-full bg-accent-pink/10 blur-[80px] pointer-events-none" />
|
||||||
|
|
||||||
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 relative z-10">
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-12 mb-16">
|
||||||
|
{/* Brand Info */}
|
||||||
|
<div className="space-y-4 col-span-1 md:col-span-1">
|
||||||
|
<Link href="/" className="inline-block">
|
||||||
|
<span className="text-2xl font-extrabold tracking-wider font-sora text-white">
|
||||||
|
Lyts<span className="text-primary text-glow-yellow">Up</span>
|
||||||
|
</span>
|
||||||
|
</Link>
|
||||||
|
<p className="text-white/60 text-sm leading-relaxed max-w-xs">
|
||||||
|
The premier social currency app for Gen Z. Earn rewards for walking, playing, and living your best life.
|
||||||
|
</p>
|
||||||
|
<p className="text-primary font-semibold text-xs tracking-wider uppercase">
|
||||||
|
Earn. Spend. Live More.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Site Navigation */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h4 className="text-white font-bold text-sm tracking-wider uppercase font-sora">Navigate</h4>
|
||||||
|
<ul className="space-y-2.5">
|
||||||
|
<li>
|
||||||
|
<a href="#earn" className="text-white/60 hover:text-primary transition-colors text-sm">Earn Points</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="#spend" className="text-white/60 hover:text-primary transition-colors text-sm">Redeem Rewards</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="#play" className="text-white/60 hover:text-primary transition-colors text-sm">Mini-Game</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="#leaderboard" className="text-white/60 hover:text-primary transition-colors text-sm">Leaderboard</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Corporate Links */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h4 className="text-white font-bold text-sm tracking-wider uppercase font-sora">Partnerships</h4>
|
||||||
|
<ul className="space-y-2.5">
|
||||||
|
<li>
|
||||||
|
<Link href="#" className="text-white/60 hover:text-primary transition-colors text-sm">Become a Partner</Link>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<Link href="#" className="text-white/60 hover:text-primary transition-colors text-sm">Brands Portal</Link>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<Link href="#" className="text-white/60 hover:text-primary transition-colors text-sm">API Integration</Link>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<Link href="#" className="text-white/60 hover:text-primary transition-colors text-sm">Support Hub</Link>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Newsletter subscription */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h4 className="text-white font-bold text-sm tracking-wider uppercase font-sora">Get Secret Drops</h4>
|
||||||
|
<p className="text-white/60 text-xs">
|
||||||
|
Subscribe to get notified about ultra-rare brand voucher drops.
|
||||||
|
</p>
|
||||||
|
<form onSubmit={handleSubscribe} className="relative flex">
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
placeholder="your.email@gmail.com"
|
||||||
|
value={email}
|
||||||
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
|
required
|
||||||
|
className="w-full bg-white/5 border border-white/10 rounded-xl px-4 py-2.5 text-sm text-white focus:outline-none focus:border-primary/50 transition-colors pr-10"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="absolute right-1 top-1 bottom-1 bg-primary text-dark font-bold px-3.5 rounded-lg hover:bg-primary/95 transition-colors flex items-center justify-center cursor-pointer"
|
||||||
|
aria-label="Subscribe"
|
||||||
|
>
|
||||||
|
<Send className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
{subscribed && (
|
||||||
|
<p className="text-accent-lime text-xs font-semibold animate-pulse">
|
||||||
|
You're in! Check your inbox for secret codes.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr className="border-white/5 mb-8" />
|
||||||
|
|
||||||
|
{/* Socials & Copyright */}
|
||||||
|
<div className="flex flex-col sm:flex-row justify-between items-center space-y-4 sm:space-y-0">
|
||||||
|
<div className="flex items-center space-x-2 text-xs text-white/40">
|
||||||
|
<span>© {new Date().getFullYear()} LytsUp.com. All rights reserved.</span>
|
||||||
|
<span className="hidden sm:inline">•</span>
|
||||||
|
<span className="flex items-center">
|
||||||
|
Made with <Heart className="w-3.5 h-3.5 text-accent-pink fill-current mx-1" /> for Gen Z
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Social Icons */}
|
||||||
|
<div className="flex space-x-4">
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
className="w-8 h-8 rounded-full bg-white/5 flex items-center justify-center text-white/60 hover:bg-accent-pink hover:text-white transition-all scale-95 hover:scale-100"
|
||||||
|
aria-label="Instagram"
|
||||||
|
>
|
||||||
|
<Instagram className="w-4 h-4" />
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
className="w-8 h-8 rounded-full bg-white/5 flex items-center justify-center text-white/60 hover:bg-accent-purple hover:text-white transition-all scale-95 hover:scale-100"
|
||||||
|
aria-label="Twitter"
|
||||||
|
>
|
||||||
|
<Twitter className="w-4 h-4" />
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
className="w-8 h-8 rounded-full bg-white/5 flex items-center justify-center text-white/60 hover:bg-primary hover:text-dark transition-all scale-95 hover:scale-100"
|
||||||
|
aria-label="Discord"
|
||||||
|
>
|
||||||
|
<MessageSquare className="w-4 h-4" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
);
|
||||||
|
}
|
||||||
146
src/components/layout/Navbar.tsx
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import { useUser } from "@/lib/context";
|
||||||
|
import { Menu, X, Coins, Flame, Award, Smartphone, User, BarChart2 } from "lucide-react";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
|
export default function Navbar() {
|
||||||
|
const { profile } = useUser();
|
||||||
|
const [isScrolled, setIsScrolled] = useState(false);
|
||||||
|
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleScroll = () => {
|
||||||
|
if (window.scrollY > 20) {
|
||||||
|
setIsScrolled(true);
|
||||||
|
} else {
|
||||||
|
setIsScrolled(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
window.addEventListener("scroll", handleScroll);
|
||||||
|
return () => window.removeEventListener("scroll", handleScroll);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const navLinks = [
|
||||||
|
{ name: "Earn", href: "#earn" },
|
||||||
|
{ name: "Spend", href: "#spend" },
|
||||||
|
{ name: "Play & Win", href: "#play" },
|
||||||
|
{ name: "Leaderboard", href: "#leaderboard" },
|
||||||
|
{ name: "Dashboard", href: "#dashboard" },
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<nav
|
||||||
|
className={`fixed top-0 left-0 right-0 z-50 transition-all duration-300 ${
|
||||||
|
isScrolled
|
||||||
|
? "glassmorphism py-3 shadow-[0_8px_32px_0_rgba(0,0,0,0.4)] border-b border-white/5"
|
||||||
|
: "bg-transparent py-5"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
{/* Logo */}
|
||||||
|
<Link href="/" className="flex items-center space-x-2.5 group">
|
||||||
|
<img src="/images/lytsup_app.png" alt="LytsUp Logo" className="h-8 w-auto object-contain rounded-lg" />
|
||||||
|
<span className="text-xl font-extrabold tracking-wider font-sora text-white flex items-center">
|
||||||
|
Lyts<span className="text-primary text-glow-yellow">Up</span>
|
||||||
|
<span className="h-1.5 w-1.5 rounded-full bg-accent-pink ml-0.5 animate-ping" />
|
||||||
|
</span>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
{/* Desktop Navigation Links */}
|
||||||
|
<div className="hidden md:flex items-center space-x-8">
|
||||||
|
{navLinks.map((link) => (
|
||||||
|
<a
|
||||||
|
key={link.name}
|
||||||
|
href={link.href}
|
||||||
|
className="text-sm font-semibold tracking-wide text-white/80 hover:text-primary transition-colors duration-200"
|
||||||
|
>
|
||||||
|
{link.name}
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* User Currency and Profile Stats */}
|
||||||
|
<div className="hidden md:flex items-center space-x-4">
|
||||||
|
{/* Streak Counter */}
|
||||||
|
<div className="flex items-center space-x-1.5 px-3 py-1.5 rounded-full bg-white/5 border border-white/10 text-accent-orange font-bold text-sm">
|
||||||
|
<Flame className="w-4.5 h-4.5 fill-current animate-bounce" />
|
||||||
|
<span>{profile.streak} Days</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* LYT Balance Widget */}
|
||||||
|
<div className="flex items-center space-x-2 px-4 py-1.5 rounded-full bg-primary/10 border border-primary/20 text-primary font-extrabold text-sm shadow-[0_0_15px_rgba(255,199,44,0.1)]">
|
||||||
|
<Coins className="w-4.5 h-4.5 text-primary animate-pulse" />
|
||||||
|
<span className="font-sora">{profile.lytBalance} LYTs</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* User Profile Avatar */}
|
||||||
|
<div className="flex items-center space-x-2 pl-2">
|
||||||
|
<div className="relative">
|
||||||
|
<div className="w-9 h-9 rounded-full bg-gradient-to-tr from-accent-pink to-accent-purple p-0.5 flex items-center justify-center cursor-pointer hover:scale-105 transition-transform">
|
||||||
|
<div className="w-full h-full rounded-full bg-dark flex items-center justify-center">
|
||||||
|
<User className="w-4.5 h-4.5 text-white" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span className="absolute -bottom-1 -right-1 flex h-4 w-4 items-center justify-center rounded-full bg-accent-lime text-[10px] font-extrabold text-dark ring-2 ring-dark">
|
||||||
|
L{profile.level}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Mobile Menu Button */}
|
||||||
|
<div className="flex items-center space-x-3 md:hidden">
|
||||||
|
{/* Mobile LYT widget */}
|
||||||
|
<div className="flex items-center space-x-1 px-2.5 py-1 rounded-full bg-primary/10 border border-primary/20 text-primary font-bold text-xs">
|
||||||
|
<Coins className="w-3.5 h-3.5" />
|
||||||
|
<span>{profile.lytBalance}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => setIsMobileMenuOpen(!isMobileMenuOpen)}
|
||||||
|
className="text-white hover:text-primary transition-colors focus:outline-none"
|
||||||
|
aria-label="Toggle Menu"
|
||||||
|
>
|
||||||
|
{isMobileMenuOpen ? <X className="w-6 h-6" /> : <Menu className="w-6 h-6" />}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Mobile Menu Overlay */}
|
||||||
|
{isMobileMenuOpen && (
|
||||||
|
<div className="md:hidden glassmorphism border-b border-white/10 animate-in fade-in slide-in-from-top-4 duration-300">
|
||||||
|
<div className="px-4 pt-2 pb-6 space-y-3">
|
||||||
|
{navLinks.map((link) => (
|
||||||
|
<a
|
||||||
|
key={link.name}
|
||||||
|
href={link.href}
|
||||||
|
onClick={() => setIsMobileMenuOpen(false)}
|
||||||
|
className="block px-3 py-2.5 rounded-xl text-base font-medium text-white/80 hover:bg-white/5 hover:text-primary transition-all"
|
||||||
|
>
|
||||||
|
{link.name}
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<hr className="border-white/10 my-4" />
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between px-3">
|
||||||
|
<div className="flex items-center space-x-1.5 text-accent-orange font-bold text-sm">
|
||||||
|
<Flame className="w-4.5 h-4.5 fill-current" />
|
||||||
|
<span>{profile.streak} Day Streak</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center space-x-2 text-primary font-bold text-sm">
|
||||||
|
<Award className="w-4.5 h-4.5 text-accent-lime" />
|
||||||
|
<span>Level {profile.level}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</nav>
|
||||||
|
);
|
||||||
|
}
|
||||||
106
src/components/layout/SideNav.tsx
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useState, useCallback } from "react";
|
||||||
|
import type Lenis from "lenis";
|
||||||
|
|
||||||
|
export interface NavSection {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function SideNav({ sections }: { sections: NavSection[] }) {
|
||||||
|
const [active, setActive] = useState(sections[0]?.id ?? "");
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
|
// Track which section is in view
|
||||||
|
useEffect(() => {
|
||||||
|
const observer = new IntersectionObserver(
|
||||||
|
(entries) => {
|
||||||
|
entries.forEach((entry) => {
|
||||||
|
if (entry.isIntersecting) setActive(entry.target.id);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
{ rootMargin: "-45% 0px -45% 0px", threshold: 0 }
|
||||||
|
);
|
||||||
|
|
||||||
|
sections.forEach(({ id }) => {
|
||||||
|
const el = document.getElementById(id);
|
||||||
|
if (el) observer.observe(el);
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => observer.disconnect();
|
||||||
|
}, [sections]);
|
||||||
|
|
||||||
|
const scrollTo = useCallback((id: string) => {
|
||||||
|
const el = document.getElementById(id);
|
||||||
|
if (!el) return;
|
||||||
|
const lenis = (window as unknown as { lenis?: Lenis }).lenis;
|
||||||
|
if (lenis) {
|
||||||
|
lenis.scrollTo(el, { offset: 0, duration: 1.2 });
|
||||||
|
} else {
|
||||||
|
el.scrollIntoView({ behavior: "smooth", block: "start" });
|
||||||
|
}
|
||||||
|
setOpen(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* Mobile toggle */}
|
||||||
|
<button
|
||||||
|
aria-label="Toggle navigation"
|
||||||
|
onClick={() => setOpen((o) => !o)}
|
||||||
|
className="fixed right-4 top-4 z-50 rounded-full bg-black/70 p-3 backdrop-blur-md ring-1 ring-white/15 lg:hidden"
|
||||||
|
>
|
||||||
|
<span className="block h-0.5 w-5 bg-white" />
|
||||||
|
<span className="mt-1 block h-0.5 w-5 bg-white" />
|
||||||
|
<span className="mt-1 block h-0.5 w-5 bg-white" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Right-side navigation */}
|
||||||
|
<nav
|
||||||
|
aria-label="Section navigation"
|
||||||
|
className={`fixed right-0 top-0 z-40 flex h-screen flex-col justify-center gap-1 pr-5 pl-6 transition-transform duration-300 lg:translate-x-0 ${
|
||||||
|
open
|
||||||
|
? "translate-x-0 bg-black/80 backdrop-blur-xl"
|
||||||
|
: "translate-x-full lg:bg-transparent"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<ul className="flex flex-col gap-1">
|
||||||
|
{sections.map(({ id, label }, i) => {
|
||||||
|
const isActive = active === id;
|
||||||
|
return (
|
||||||
|
<li key={id} className="group flex items-center justify-end gap-3">
|
||||||
|
<button
|
||||||
|
onClick={() => scrollTo(id)}
|
||||||
|
className="flex items-center gap-3 text-right"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className={`whitespace-nowrap text-[11px] font-semibold uppercase tracking-wider transition-all duration-200 ${
|
||||||
|
isActive
|
||||||
|
? "text-primary opacity-100"
|
||||||
|
: "text-white/50 opacity-0 group-hover:opacity-100 lg:opacity-0 lg:group-hover:opacity-100"
|
||||||
|
} ${open ? "opacity-100" : ""}`}
|
||||||
|
>
|
||||||
|
<span className="mr-2 tabular-nums text-white/30">
|
||||||
|
{String(i + 1).padStart(2, "0")}
|
||||||
|
</span>
|
||||||
|
{label}
|
||||||
|
</span>
|
||||||
|
<span className="relative flex h-3 w-3 items-center justify-center">
|
||||||
|
<span
|
||||||
|
className={`rounded-full transition-all duration-300 ${
|
||||||
|
isActive
|
||||||
|
? "h-3 w-3 bg-primary shadow-[0_0_12px_2px_rgba(255,199,44,0.7)]"
|
||||||
|
: "h-2 w-2 bg-white/30 group-hover:bg-white/70"
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
31
src/components/sections/BrandMarquee.tsx
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import Marquee from "react-fast-marquee";
|
||||||
|
import { Coffee, ShoppingBag, Music, Film, Dumbbell, Ticket, Sparkles } from "lucide-react";
|
||||||
|
|
||||||
|
export default function BrandMarquee() {
|
||||||
|
const brands = [
|
||||||
|
{ name: "Starbucks", icon: <Coffee className="w-5 h-5 text-emerald-400" />, color: "text-emerald-400" },
|
||||||
|
{ name: "Nike", icon: <ShoppingBag className="w-5 h-5 text-white" />, color: "text-white" },
|
||||||
|
{ name: "Spotify", icon: <Music className="w-5 h-5 text-green-400" />, color: "text-green-400" },
|
||||||
|
{ name: "Netflix", icon: <Film className="w-5 h-5 text-red-500" />, color: "text-red-500" },
|
||||||
|
{ name: "Gymshark", icon: <Dumbbell className="w-5 h-5 text-amber-500" />, color: "text-amber-500" },
|
||||||
|
{ name: "Ticketmaster", icon: <Ticket className="w-5 h-5 text-sky-400" />, color: "text-sky-400" },
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="w-full bg-[#070707] border-y border-white/5 py-8 relative z-10 overflow-hidden">
|
||||||
|
<Marquee gradient={false} speed={40} className="font-sora text-sm font-extrabold tracking-wider">
|
||||||
|
{brands.concat(brands).map((brand, idx) => (
|
||||||
|
<div key={idx} className="flex items-center mx-12 space-x-3 text-white/70 hover:text-white transition-colors duration-200">
|
||||||
|
<span className="p-2 bg-white/5 rounded-xl border border-white/10 flex items-center justify-center">
|
||||||
|
{brand.icon}
|
||||||
|
</span>
|
||||||
|
<span className="text-base font-bold">{brand.name}</span>
|
||||||
|
<Sparkles className="w-3 h-3 text-primary animate-pulse" />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</Marquee>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
313
src/components/sections/DashboardPreview.tsx
Normal file
@@ -0,0 +1,313 @@
|
|||||||
|
"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 (
|
||||||
|
<section id="dashboard" className="py-24 bg-dark relative overflow-hidden">
|
||||||
|
{/* Background visual glows */}
|
||||||
|
<div className="absolute top-1/2 left-1/4 -translate-y-1/2 w-96 h-96 rounded-full bg-accent-purple/10 blur-[130px] pointer-events-none" />
|
||||||
|
<div className="absolute bottom-0 right-1/4 w-96 h-96 rounded-full bg-accent-pink/5 blur-[130px] pointer-events-none" />
|
||||||
|
|
||||||
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 relative z-10">
|
||||||
|
|
||||||
|
{/* Section Header */}
|
||||||
|
<div className="text-center max-w-3xl mx-auto mb-20 space-y-4">
|
||||||
|
<span className="text-accent-lime font-extrabold text-xs tracking-widest uppercase">
|
||||||
|
Platform Preview
|
||||||
|
</span>
|
||||||
|
<h2 className="text-3xl sm:text-5xl font-extrabold font-sora text-white leading-tight">
|
||||||
|
Manage Assets on <span className="text-transparent bg-clip-text bg-gradient-to-r from-primary to-accent-pink text-glow-yellow">Your Dashboard</span>
|
||||||
|
</h2>
|
||||||
|
<p className="text-white/60 text-base sm:text-lg">
|
||||||
|
Track walking logs, check voucher redemption codes, and review points transaction history in your unified portal.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Contents Grid */}
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-12 gap-12 items-center">
|
||||||
|
|
||||||
|
{/* Left Block: Weekly Progress Charts (col-span-3) */}
|
||||||
|
<div className="lg:col-span-3 space-y-6 hidden lg:block">
|
||||||
|
<div className="glassmorphism rounded-3xl p-6 space-y-6">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-sm font-bold text-white font-sora flex items-center gap-1.5">
|
||||||
|
<Activity className="w-4 h-4 text-accent-lime" />
|
||||||
|
<span>Activity Logs</span>
|
||||||
|
</span>
|
||||||
|
<span className="text-[10px] text-white/40 font-bold uppercase">Weekly</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Steps Chart */}
|
||||||
|
<div className="h-44 flex items-end justify-between gap-2.5 pt-4">
|
||||||
|
{weeklySteps.map((d, idx) => {
|
||||||
|
const pct = Math.max(15, Math.round((d.steps / maxWeeklySteps) * 100));
|
||||||
|
const isToday = idx === weeklySteps.length - 1;
|
||||||
|
return (
|
||||||
|
<div key={idx} className="flex-1 flex flex-col items-center gap-2">
|
||||||
|
<div className="w-full relative group">
|
||||||
|
<div
|
||||||
|
style={{ height: `${pct}%` }}
|
||||||
|
className={`w-full rounded-t-lg transition-all duration-500 min-h-[15px] ${
|
||||||
|
isToday
|
||||||
|
? "bg-gradient-to-t from-accent-pink to-accent-purple shadow-[0_0_10px_rgba(255,46,136,0.2)]"
|
||||||
|
: "bg-white/10 hover:bg-white/20"
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
{/* Tooltip */}
|
||||||
|
<div className="absolute bottom-full left-1/2 -translate-x-1/2 mb-1 hidden group-hover:block bg-dark border border-white/10 px-2 py-0.5 rounded text-[9px] font-mono text-white z-10 whitespace-nowrap">
|
||||||
|
{d.steps.toLocaleString()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span className={`text-[10px] font-bold ${isToday ? "text-accent-pink" : "text-white/40"}`}>
|
||||||
|
{d.day}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="border-t border-white/5 pt-4 text-xs text-white/50 leading-relaxed">
|
||||||
|
🎉 Your weekly walk step frequency has increased by <span className="text-accent-lime font-bold">12.4%</span> since last week. Keep it up!
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Center Block: Mobile iOS Phone Mockup (col-span-5) */}
|
||||||
|
<div className="lg:col-span-5 flex justify-center">
|
||||||
|
{/* Phone Shell */}
|
||||||
|
<div className="relative w-[320px] h-[640px] rounded-[48px] border-[8px] border-white/10 bg-[#090909] p-3.5 shadow-[0_25px_60px_-15px_rgba(0,0,0,0.9)] overflow-hidden flex flex-col justify-between">
|
||||||
|
|
||||||
|
{/* Dynamic Island Notch */}
|
||||||
|
<div className="absolute top-3.5 left-1/2 -translate-x-1/2 w-28 h-6 rounded-full bg-black z-30 flex items-center justify-center">
|
||||||
|
<div className="w-2.5 h-2.5 rounded-full bg-zinc-900 ml-auto mr-4" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Status Header */}
|
||||||
|
<div className="flex justify-between items-center text-[10px] text-white/70 font-semibold px-4 pt-1.5 z-20">
|
||||||
|
<span>9:41</span>
|
||||||
|
<div className="flex space-x-1.5">
|
||||||
|
<span>5G</span>
|
||||||
|
<span>100%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* App Internal Body (Scrollable mock container) */}
|
||||||
|
<div className="flex-grow overflow-y-auto px-1.5 py-4 space-y-5 scrollbar-none mt-2">
|
||||||
|
|
||||||
|
{/* User Header */}
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center space-x-2.5">
|
||||||
|
<div className="w-8 h-8 rounded-full bg-gradient-to-tr from-accent-pink to-accent-purple p-0.5 flex items-center justify-center">
|
||||||
|
<div className="w-full h-full rounded-full bg-dark flex items-center justify-center text-xs font-bold text-white">
|
||||||
|
AS
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-white font-bold text-xs block">{profile.name}</span>
|
||||||
|
<span className="text-[9px] text-white/40 block">Level {profile.level} Premium Member</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Streak tag */}
|
||||||
|
<div className="flex items-center space-x-1 px-2.5 py-1 rounded-full bg-white/5 border border-white/10 text-accent-orange font-bold text-[10px]">
|
||||||
|
<Flame className="w-3 h-3 fill-current" />
|
||||||
|
<span>{profile.streak} Days</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Circular Step Tracker */}
|
||||||
|
<div className="bg-white/3 border border-white/5 rounded-3xl p-5 flex flex-col items-center justify-center text-center relative overflow-hidden">
|
||||||
|
|
||||||
|
{/* Gauge */}
|
||||||
|
<div className="relative w-36 h-36 flex items-center justify-center">
|
||||||
|
|
||||||
|
{/* SVG circular track */}
|
||||||
|
<svg className="w-full h-full transform -rotate-90">
|
||||||
|
<circle
|
||||||
|
cx="72"
|
||||||
|
cy="72"
|
||||||
|
r="60"
|
||||||
|
className="stroke-white/5 fill-transparent"
|
||||||
|
strokeWidth="8"
|
||||||
|
/>
|
||||||
|
<circle
|
||||||
|
cx="72"
|
||||||
|
cy="72"
|
||||||
|
r="60"
|
||||||
|
className="stroke-primary fill-transparent"
|
||||||
|
strokeWidth="8"
|
||||||
|
strokeDasharray={377}
|
||||||
|
strokeDashoffset={377 - (377 * stepPercentage) / 100}
|
||||||
|
strokeLinecap="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
{/* Step values */}
|
||||||
|
<div className="absolute inset-0 flex flex-col items-center justify-center">
|
||||||
|
<span className="text-2xl font-extrabold font-sora text-white">
|
||||||
|
{profile.stepsToday.toLocaleString()}
|
||||||
|
</span>
|
||||||
|
<span className="text-[9px] text-white/40 font-bold uppercase tracking-wider">
|
||||||
|
Steps Today
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span className="text-[10px] text-white/50 font-bold mt-2">
|
||||||
|
Goal: {profile.stepGoal.toLocaleString()} steps ({stepPercentage}%)
|
||||||
|
</span>
|
||||||
|
|
||||||
|
{/* Stats split */}
|
||||||
|
<div className="grid grid-cols-3 gap-2 w-full pt-4 mt-4 border-t border-white/5">
|
||||||
|
<div>
|
||||||
|
<span className="text-white font-extrabold text-xs block">{caloriesBurned}</span>
|
||||||
|
<span className="text-[8px] text-white/40 block">Kcal</span>
|
||||||
|
</div>
|
||||||
|
<div className="border-x border-white/5">
|
||||||
|
<span className="text-white font-extrabold text-xs block">{distanceKm}</span>
|
||||||
|
<span className="text-[8px] text-white/40 block">Km</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-white font-extrabold text-xs block">{activeMinutes}</span>
|
||||||
|
<span className="text-[8px] text-white/40 block">Mins</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Balance & Send Row */}
|
||||||
|
<div className="bg-gradient-to-tr from-accent-purple/20 to-accent-pink/10 border border-accent-purple/20 rounded-2xl p-4 flex items-center justify-between">
|
||||||
|
<div className="space-y-1">
|
||||||
|
<span className="text-[9px] text-white/40 font-bold uppercase tracking-wider block">Wallet Balance</span>
|
||||||
|
<span className="text-lg font-extrabold font-sora text-primary flex items-center gap-1">
|
||||||
|
<Coins className="w-4.5 h-4.5" />
|
||||||
|
<span>{profile.lytBalance} LYTs</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button className="px-3.5 py-1.5 bg-primary text-dark font-extrabold text-[10px] tracking-wide rounded-xl">
|
||||||
|
Redeem
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Challenges Panel */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<h4 className="text-[10px] font-bold text-white/40 uppercase tracking-widest px-1">Daily Missions</h4>
|
||||||
|
<div className="bg-white/3 border border-white/5 rounded-2xl p-3 space-y-2">
|
||||||
|
<div className="flex items-center justify-between p-2 hover:bg-white/2 rounded-lg text-xs">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<CheckCircle className="w-4 h-4 text-accent-lime" />
|
||||||
|
<span className="text-white/80">Log 5,000 steps today</span>
|
||||||
|
</div>
|
||||||
|
<span className="text-accent-lime font-bold text-[10px]">+10 LYT</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between p-2 hover:bg-white/2 rounded-lg text-xs">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<CheckCircle className="w-4 h-4 text-accent-lime" />
|
||||||
|
<span className="text-white/80">Check in at Brew & Co</span>
|
||||||
|
</div>
|
||||||
|
<span className="text-accent-lime font-bold text-[10px]">+5 LYT</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Bottom Nav Bar */}
|
||||||
|
<div className="border-t border-white/5 pt-2 pb-1.5 flex justify-around items-center z-20">
|
||||||
|
<div className="flex flex-col items-center text-primary">
|
||||||
|
<Activity className="w-4.5 h-4.5" />
|
||||||
|
<span className="text-[8px] font-bold mt-0.5">Track</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col items-center text-white/40 hover:text-white transition-colors">
|
||||||
|
<Compass className="w-4.5 h-4.5" />
|
||||||
|
<span className="text-[8px] font-bold mt-0.5">Explore</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col items-center text-white/40 hover:text-white transition-colors">
|
||||||
|
<Coins className="w-4.5 h-4.5" />
|
||||||
|
<span className="text-[8px] font-bold mt-0.5">Wallet</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Right Block: Transaction Log Feed (col-span-4) */}
|
||||||
|
<div className="lg:col-span-4 space-y-6">
|
||||||
|
<div className="glassmorphism rounded-3xl p-6 space-y-6">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-sm font-bold text-white font-sora flex items-center gap-1.5">
|
||||||
|
<TrendingUp className="w-4 h-4 text-accent-pink" />
|
||||||
|
<span>Wallet Transaction Ledger</span>
|
||||||
|
</span>
|
||||||
|
<span className="text-[10px] text-white/40 font-bold uppercase">Real-Time</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Transaction list */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
{transactions.map((tx) => (
|
||||||
|
<div key={tx.id} className="flex justify-between items-center p-3 rounded-xl bg-white/2 border border-white/5">
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
<span className="text-xs font-bold text-white block">{tx.type}</span>
|
||||||
|
<span className="text-[10px] text-white/40 block">{tx.detail}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="text-right">
|
||||||
|
<span className={`text-xs font-bold block ${tx.isEarn ? "text-accent-lime" : "text-red-400"}`}>
|
||||||
|
{tx.amount}
|
||||||
|
</span>
|
||||||
|
<span className="text-[8px] text-white/30">{tx.time}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="pt-2">
|
||||||
|
<a
|
||||||
|
href="#spend"
|
||||||
|
className="w-full py-3 rounded-xl bg-white/5 hover:bg-white/10 text-white font-extrabold text-xs tracking-wider flex items-center justify-center gap-1 border border-white/10 hover:border-white/20 transition-all cursor-pointer"
|
||||||
|
>
|
||||||
|
<span>Redeem Store Vouchers</span>
|
||||||
|
<ChevronRight className="w-4 h-4" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
417
src/components/sections/EarningFeatures.tsx
Normal file
@@ -0,0 +1,417 @@
|
|||||||
|
"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<string[]>(["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<string[]>([]);
|
||||||
|
const [mapClaimed, setMapClaimed] = useState<string | null>(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 (
|
||||||
|
<section id="earn" className="py-24 bg-[#090909] relative overflow-hidden">
|
||||||
|
{/* Background glow lines */}
|
||||||
|
<div className="absolute top-1/2 left-0 right-0 h-0.5 bg-gradient-to-r from-transparent via-white/5 to-transparent pointer-events-none" />
|
||||||
|
|
||||||
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 relative z-10">
|
||||||
|
|
||||||
|
{/* Section Header */}
|
||||||
|
<div className="text-center max-w-3xl mx-auto mb-20 space-y-4">
|
||||||
|
<span className="text-accent-pink font-extrabold text-xs tracking-widest uppercase">
|
||||||
|
How it works
|
||||||
|
</span>
|
||||||
|
<h2 className="text-3xl sm:text-5xl font-extrabold font-sora text-white leading-tight">
|
||||||
|
Earn LYTs for <span className="text-glow-yellow text-primary">Living Your Life</span>
|
||||||
|
</h2>
|
||||||
|
<p className="text-white/60 text-base sm:text-lg">
|
||||||
|
Interact with our live simulations below to see how easy it is to rack up social currency.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Feature Grid */}
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
||||||
|
|
||||||
|
{/* Card 1: Walk to Earn (Simulator) */}
|
||||||
|
<div className="glassmorphism rounded-3xl p-8 flex flex-col justify-between group hover:border-primary/20 transition-all duration-300">
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="w-12 h-12 rounded-2xl bg-primary/10 border border-primary/20 flex items-center justify-center text-primary">
|
||||||
|
<Footprints className="w-6 h-6 animate-pulse" />
|
||||||
|
</div>
|
||||||
|
<span className="text-xs font-bold px-3 py-1 rounded-full bg-white/5 text-white/50 border border-white/5">
|
||||||
|
1 LYT / 400 Steps
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<h3 className="text-2xl font-bold font-sora text-white">Walk-to-Earn</h3>
|
||||||
|
<p className="text-white/60 text-sm">
|
||||||
|
Keep your phone in your pocket, walk to class, run errands, or jog. Watch your step count convert to LYTs automatically.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Step Simulator Widget */}
|
||||||
|
<div className="bg-white/3 border border-white/5 rounded-2xl p-6 space-y-4">
|
||||||
|
<div className="flex justify-between items-end">
|
||||||
|
<div>
|
||||||
|
<span className="text-white/40 text-[10px] font-bold tracking-wider uppercase block">Steps to Simulate</span>
|
||||||
|
<span className="text-2xl font-extrabold font-sora text-white">
|
||||||
|
+{simSteps.toLocaleString()} <span className="text-xs text-white/40">Steps</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-right">
|
||||||
|
<span className="text-white/40 text-[10px] font-bold tracking-wider uppercase block">Estimated Earn</span>
|
||||||
|
<span className="text-xl font-extrabold text-primary font-sora flex items-center justify-end gap-1">
|
||||||
|
+{walkReward} <span className="text-xs font-medium text-white/60">LYTs</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Range Slider */}
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min="400"
|
||||||
|
max="12000"
|
||||||
|
step="400"
|
||||||
|
value={simSteps}
|
||||||
|
onChange={(e) => setSimSteps(parseInt(e.target.value))}
|
||||||
|
disabled={walkClaimed}
|
||||||
|
className="w-full h-1.5 bg-white/10 rounded-lg appearance-none cursor-pointer accent-primary"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="flex justify-between text-[10px] text-white/40 font-bold">
|
||||||
|
<span>400 Steps</span>
|
||||||
|
<span>6,000 Steps</span>
|
||||||
|
<span>12,000 Steps</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-8">
|
||||||
|
<button
|
||||||
|
onClick={handleSimulateWalk}
|
||||||
|
disabled={walkClaimed}
|
||||||
|
className={`w-full py-4 rounded-xl font-extrabold text-sm tracking-wider flex items-center justify-center gap-2 cursor-pointer transition-all ${
|
||||||
|
walkClaimed
|
||||||
|
? "bg-accent-lime text-dark shadow-[0_0_20px_rgba(200,255,55,0.3)]"
|
||||||
|
: "bg-white/5 hover:bg-primary hover:text-dark text-white border border-white/10 hover:border-primary"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{walkClaimed ? (
|
||||||
|
<>
|
||||||
|
<Check className="w-5 h-5" />
|
||||||
|
<span>Steps Synced! +{walkReward} LYTs Claimed</span>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<RefreshCw className="w-4 h-4 animate-spin-slow" />
|
||||||
|
<span>Simulate Walk & Sync Steps</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Card 2: Shop & Earn (Receipt Simulator) */}
|
||||||
|
<div className="glassmorphism rounded-3xl p-8 flex flex-col justify-between group hover:border-accent-pink/20 transition-all duration-300">
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="w-12 h-12 rounded-2xl bg-accent-pink/10 border border-accent-pink/20 flex items-center justify-center text-accent-pink">
|
||||||
|
<ShoppingBag className="w-6 h-6" />
|
||||||
|
</div>
|
||||||
|
<span className="text-xs font-bold px-3 py-1 rounded-full bg-white/5 text-white/50 border border-white/5">
|
||||||
|
10% LYT Cashback
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<h3 className="text-2xl font-bold font-sora text-white">Shop-to-Earn</h3>
|
||||||
|
<p className="text-white/60 text-sm">
|
||||||
|
Shop from partner merchants (food, clothing, fitness centers) and scan your digital receipt to claim instant LYT cashback.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Receipt Widget */}
|
||||||
|
<div className="bg-[#0b0b0b] border border-white/5 rounded-2xl p-5 space-y-3.5 relative overflow-hidden">
|
||||||
|
<div className="absolute top-0 right-0 bg-accent-pink/10 text-accent-pink px-2.5 py-0.5 text-[9px] font-bold tracking-wider rounded-bl-lg uppercase">
|
||||||
|
Pending Receipt
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h4 className="text-[10px] font-bold text-white/40 uppercase tracking-widest">Select items purchased:</h4>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{shopItems.map((item) => (
|
||||||
|
<div
|
||||||
|
key={item.id}
|
||||||
|
onClick={() => {
|
||||||
|
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"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-center space-x-3">
|
||||||
|
<div
|
||||||
|
className={`w-4 h-4 rounded flex items-center justify-center border transition-all ${
|
||||||
|
selectedItems.includes(item.id)
|
||||||
|
? "bg-accent-pink border-accent-pink"
|
||||||
|
: "border-white/20"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{selectedItems.includes(item.id) && <Check className="w-3 h-3 text-white" />}
|
||||||
|
</div>
|
||||||
|
<div className="text-xs font-semibold">{item.name}</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-right">
|
||||||
|
<div className="text-xs font-extrabold text-white">{item.cost}</div>
|
||||||
|
<div className="text-[10px] text-accent-pink font-bold">+{item.reward} LYTs</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-8">
|
||||||
|
<button
|
||||||
|
onClick={handleShopClaim}
|
||||||
|
disabled={shopClaimed || totalShopReward === 0}
|
||||||
|
className={`w-full py-4 rounded-xl font-extrabold text-sm tracking-wider flex items-center justify-center gap-2 cursor-pointer transition-all ${
|
||||||
|
shopClaimed
|
||||||
|
? "bg-accent-lime text-dark shadow-[0_0_20px_rgba(200,255,55,0.3)]"
|
||||||
|
: totalShopReward === 0
|
||||||
|
? "bg-white/5 text-white/20 border border-white/5 cursor-not-allowed"
|
||||||
|
: "bg-white/5 hover:bg-accent-pink hover:text-white text-white border border-white/10 hover:border-accent-pink"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{shopClaimed ? (
|
||||||
|
<>
|
||||||
|
<Check className="w-5 h-5" />
|
||||||
|
<span>Receipt Scanned! +{totalShopReward} LYTs Claimed</span>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<span>Claim Receipt Reward (+{totalShopReward} LYTs)</span>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Card 3: Visit to Earn (Geofence Checkins) */}
|
||||||
|
<div className="glassmorphism rounded-3xl p-8 flex flex-col justify-between group hover:border-accent-purple/20 transition-all duration-300">
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="w-12 h-12 rounded-2xl bg-accent-purple/10 border border-accent-purple/20 flex items-center justify-center text-accent-purple">
|
||||||
|
<MapPin className="w-6 h-6" />
|
||||||
|
</div>
|
||||||
|
<span className="text-xs font-bold px-3 py-1 rounded-full bg-white/5 text-white/50 border border-white/5">
|
||||||
|
Location Check-Ins
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<h3 className="text-2xl font-bold font-sora text-white">Visit-to-Earn</h3>
|
||||||
|
<p className="text-white/60 text-sm">
|
||||||
|
Explore your city! Check in at partner gyms, parks, retail shops, or cafes. Each location visit awards you instant points.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Map Simulator Widget */}
|
||||||
|
<div className="bg-[#0b0b0b] border border-white/5 rounded-2xl p-5 space-y-4 relative overflow-hidden">
|
||||||
|
{mapClaimed && (
|
||||||
|
<div className="absolute inset-0 bg-dark/95 flex items-center justify-center z-10 p-4 text-center animate-in fade-in duration-200">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="w-10 h-10 rounded-full bg-accent-purple/10 border border-accent-purple/30 mx-auto flex items-center justify-center text-accent-purple">
|
||||||
|
<Sparkles className="w-5 h-5 animate-spin-slow" />
|
||||||
|
</div>
|
||||||
|
<p className="text-sm font-semibold text-white">Checked in at {mapClaimed}!</p>
|
||||||
|
<p className="text-xs text-accent-lime font-bold">LYTs Added directly to wallet</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<h4 className="text-[10px] font-bold text-white/40 uppercase tracking-widest">Active nodes near you:</h4>
|
||||||
|
<div className="space-y-2.5">
|
||||||
|
{checkinLocations.map((loc) => {
|
||||||
|
const visited = visitedStores.includes(loc.id);
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={loc.id}
|
||||||
|
className={`flex items-center justify-between p-3 rounded-xl border transition-all ${
|
||||||
|
visited
|
||||||
|
? "bg-white/2 border-white/5 opacity-50"
|
||||||
|
: "bg-white/3 border-white/5 hover:border-accent-purple/30"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-center space-x-3">
|
||||||
|
<MapPin className={`w-4 h-4 ${visited ? "text-white/40" : "text-accent-purple animate-bounce"}`} />
|
||||||
|
<div className="text-xs font-semibold text-white">{loc.name}</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => handleCheckin(loc.id, loc.name, loc.reward)}
|
||||||
|
disabled={visited}
|
||||||
|
className={`px-3 py-1.5 rounded-lg text-xs font-bold transition-all cursor-pointer ${
|
||||||
|
visited
|
||||||
|
? "bg-white/5 text-white/40 cursor-not-allowed"
|
||||||
|
: "bg-accent-purple text-white hover:bg-accent-purple/90"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{visited ? "Visited" : `Check In (+${loc.reward} LYT)`}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-8 text-center text-xs text-white/40">
|
||||||
|
<span className="font-semibold text-accent-purple">Glow tips:</span> Allow location permissions in-app to auto check-in.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Card 4: Refer to Earn (Social Loop) */}
|
||||||
|
<div className="glassmorphism rounded-3xl p-8 flex flex-col justify-between group hover:border-accent-orange/20 transition-all duration-300">
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="w-12 h-12 rounded-2xl bg-accent-orange/10 border border-accent-orange/20 flex items-center justify-center text-accent-orange">
|
||||||
|
<Share2 className="w-6 h-6" />
|
||||||
|
</div>
|
||||||
|
<span className="text-xs font-bold px-3 py-1 rounded-full bg-white/5 text-white/50 border border-white/5">
|
||||||
|
50 LYTs / Referral
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<h3 className="text-2xl font-bold font-sora text-white">Refer-to-Earn</h3>
|
||||||
|
<p className="text-white/60 text-sm">
|
||||||
|
Everything is better with friends. Send your personal link to your friends. Get 50 LYTs the second they verify their profile!
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Referral Widget */}
|
||||||
|
<div className="bg-[#0b0b0b] border border-white/5 rounded-2xl p-5 space-y-4">
|
||||||
|
<div>
|
||||||
|
<label className="text-[10px] font-bold text-white/40 uppercase tracking-widest block mb-2">
|
||||||
|
Your Invite Link:
|
||||||
|
</label>
|
||||||
|
<div className="relative flex">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
readOnly
|
||||||
|
value={referralCode}
|
||||||
|
className="w-full bg-white/3 border border-white/10 rounded-xl px-4 py-3 text-xs text-white/80 font-mono focus:outline-none select-all pr-12"
|
||||||
|
/>
|
||||||
|
<div className="absolute right-3 top-1/2 -translate-y-1/2 text-white/40">
|
||||||
|
<Share2 className="w-4 h-4" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="text-xs text-white/50 leading-relaxed bg-white/3 p-3 rounded-xl border border-white/5">
|
||||||
|
🔥 <span className="text-white font-semibold">Bonus:</span> Your friends also start with <span className="text-primary font-bold">50 LYTs</span> in their wallet when they sign up!
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-8">
|
||||||
|
<button
|
||||||
|
onClick={handleCopyReferral}
|
||||||
|
className={`w-full py-4 rounded-xl font-extrabold text-sm tracking-wider flex items-center justify-center gap-2 cursor-pointer transition-all ${
|
||||||
|
referralCopied
|
||||||
|
? "bg-accent-lime text-dark shadow-[0_0_20px_rgba(200,255,55,0.3)]"
|
||||||
|
: "bg-white/5 hover:bg-accent-orange hover:text-white text-white border border-white/10 hover:border-accent-orange"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{referralCopied ? (
|
||||||
|
<>
|
||||||
|
<ClipboardCheck className="w-5 h-5 animate-bounce" />
|
||||||
|
<span>Copied! +50 LYTs Added to wallet</span>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<span>Copy Invite Link & Earn</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
103
src/components/sections/FlashDrops.tsx
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
"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 className="py-24 bg-dark relative overflow-hidden">
|
||||||
|
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 relative z-10">
|
||||||
|
|
||||||
|
<div className="glassmorphism rounded-3xl p-8 sm:p-12 border border-accent-lime/10 shadow-[0_0_50px_rgba(200,255,55,0.05)]">
|
||||||
|
<div className="text-center space-y-6 max-w-xl mx-auto mb-10">
|
||||||
|
<div className="w-12 h-12 rounded-2xl bg-accent-lime/10 border border-accent-lime/20 flex items-center justify-center text-accent-lime mx-auto">
|
||||||
|
<Clock className="w-6 h-6 animate-pulse" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<span className="text-accent-lime font-bold text-xs uppercase tracking-wider">Section 12 • Flash Drops</span>
|
||||||
|
<h3 className="text-2xl sm:text-4xl font-extrabold font-sora text-white">
|
||||||
|
Ultra-Rare <br />
|
||||||
|
<span className="text-glow-yellow text-primary">Limited Vouchers</span>
|
||||||
|
</h3>
|
||||||
|
<p className="text-white/60 text-sm leading-relaxed">
|
||||||
|
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.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Time & Drops display */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 items-stretch">
|
||||||
|
|
||||||
|
{/* Timer card */}
|
||||||
|
<div className="bg-[#0b0b0b] border border-white/5 rounded-2xl p-6 flex flex-col justify-center items-center text-center space-y-4">
|
||||||
|
<span className="text-white/40 text-[10px] font-bold tracking-widest uppercase block">Next Secret Drop Unlocks In:</span>
|
||||||
|
<span className="text-3xl sm:text-4xl font-mono font-black text-accent-lime tracking-widest">
|
||||||
|
{formatTime(timeLeft)}
|
||||||
|
</span>
|
||||||
|
<div className="flex items-center space-x-1.5 text-xs text-white/50 bg-white/5 border border-white/10 px-3 py-1 rounded-full">
|
||||||
|
<Zap className="w-3.5 h-3.5 text-accent-lime fill-current" />
|
||||||
|
<span>Notify me when drops open</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Locked drops listing */}
|
||||||
|
<div className="space-y-3">
|
||||||
|
{drops.map((drop, idx) => (
|
||||||
|
<div key={idx} className="bg-white/3 border border-white/5 p-4 rounded-xl flex items-center justify-between opacity-80 hover:opacity-100 transition-opacity">
|
||||||
|
<div className="space-y-1">
|
||||||
|
<span className="text-sm font-bold text-white block">{drop.brand}</span>
|
||||||
|
<span className="text-[10px] text-white/40 block">{drop.detail}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="text-right space-y-1">
|
||||||
|
<span className="text-xs font-mono font-bold text-white/50 bg-white/5 px-2 py-0.5 rounded border border-white/10 block">
|
||||||
|
Locked
|
||||||
|
</span>
|
||||||
|
<span className="text-[9px] text-accent-lime font-bold block">Cost: {drop.cost}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
223
src/components/sections/FooterFAQ.tsx
Normal file
@@ -0,0 +1,223 @@
|
|||||||
|
"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<number | null>(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 (
|
||||||
|
<section className="bg-[#050505] border-t border-white/5 relative overflow-hidden">
|
||||||
|
{/* Glow lights */}
|
||||||
|
<div className="absolute top-0 left-1/4 -translate-y-1/2 w-90 h-90 rounded-full bg-accent-purple/5 blur-[120px] pointer-events-none" />
|
||||||
|
<div className="absolute top-0 right-1/4 -translate-y-1/2 w-90 h-90 rounded-full bg-accent-pink/5 blur-[120px] pointer-events-none" />
|
||||||
|
|
||||||
|
{/* Part 1: FAQ Accordions */}
|
||||||
|
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-20 border-b border-white/5">
|
||||||
|
<div className="text-center max-w-2xl mx-auto mb-12 space-y-4">
|
||||||
|
<span className="text-accent-lime font-extrabold text-xs tracking-widest uppercase">
|
||||||
|
Support Hub
|
||||||
|
</span>
|
||||||
|
<h3 className="text-2xl sm:text-4xl font-extrabold font-sora text-white">
|
||||||
|
Frequently Asked <span className="text-glow-yellow text-primary">Questions</span>
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
{faqItems.map((faq, idx) => {
|
||||||
|
const isOpen = openFaq === idx;
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={idx}
|
||||||
|
className="bg-white/3 border border-white/5 rounded-2xl overflow-hidden transition-all duration-300"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
onClick={() => toggleFaq(idx)}
|
||||||
|
className="w-full p-5 text-left flex justify-between items-center font-semibold font-sora text-sm text-white hover:bg-white/5 transition-colors cursor-pointer"
|
||||||
|
>
|
||||||
|
<span>{faq.question}</span>
|
||||||
|
<ChevronDown
|
||||||
|
className={`w-4.5 h-4.5 text-white/50 transition-transform duration-300 shrink-0 ml-4 ${
|
||||||
|
isOpen ? "transform rotate-180 text-primary" : ""
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{isOpen && (
|
||||||
|
<div className="p-5 border-t border-white/5 text-xs text-white/60 leading-relaxed bg-[#070707] animate-in slide-in-from-top-2 duration-300">
|
||||||
|
{faq.answer}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Part 2: Footer layout */}
|
||||||
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 pt-16 pb-8 relative z-10">
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-12 mb-16">
|
||||||
|
|
||||||
|
{/* Brand Info */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
<Link href="/" className="flex items-center space-x-2.5">
|
||||||
|
<img src="/images/lytsup_app.png" alt="LytsUp Logo" className="h-8 w-auto object-contain rounded-lg" />
|
||||||
|
<span className="text-xl font-extrabold tracking-wider font-sora text-white">
|
||||||
|
Lyts<span className="text-primary text-glow-yellow">Up</span>
|
||||||
|
</span>
|
||||||
|
</Link>
|
||||||
|
<p className="text-white/60 text-xs leading-relaxed max-w-xs">
|
||||||
|
The premier social currency app for Gen Z. Earn rewards for walking, playing, and living your best life.
|
||||||
|
</p>
|
||||||
|
<p className="text-primary font-semibold text-[10px] tracking-widest uppercase">
|
||||||
|
Earn. Spend. Live More.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Site Navigation */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h4 className="text-white font-bold text-xs tracking-wider uppercase font-sora">Navigate</h4>
|
||||||
|
<ul className="space-y-2.5 text-xs">
|
||||||
|
<li>
|
||||||
|
<a href="#earn" className="text-white/60 hover:text-primary transition-colors">Earn Points</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="#spend" className="text-white/60 hover:text-primary transition-colors">Redeem Vouchers</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="#play" className="text-white/60 hover:text-primary transition-colors">Mini-Game</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="#leaderboard" className="text-white/60 hover:text-primary transition-colors">Leaderboard</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Corporate Links */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h4 className="text-white font-bold text-xs tracking-wider uppercase font-sora">Partnerships</h4>
|
||||||
|
<ul className="space-y-2.5 text-xs">
|
||||||
|
<li>
|
||||||
|
<Link href="#" className="text-white/60 hover:text-primary transition-colors">Become a Partner</Link>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<Link href="#" className="text-white/60 hover:text-primary transition-colors">Brands Portal</Link>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<Link href="#" className="text-white/60 hover:text-primary transition-colors">API Integration</Link>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Newsletter subscription */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h4 className="text-white font-bold text-xs tracking-wider uppercase font-sora">Get Secret Drops</h4>
|
||||||
|
<p className="text-white/60 text-xs leading-relaxed">
|
||||||
|
Subscribe to get notified about ultra-rare brand voucher drops.
|
||||||
|
</p>
|
||||||
|
<form onSubmit={handleSubscribe} className="relative flex">
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
placeholder="your.email@gmail.com"
|
||||||
|
value={email}
|
||||||
|
onChange={(e) => 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"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="absolute right-1 top-1 bottom-1 bg-primary text-dark font-bold px-3 rounded-lg hover:bg-primary/95 transition-colors flex items-center justify-center cursor-pointer"
|
||||||
|
aria-label="Subscribe"
|
||||||
|
>
|
||||||
|
<Send className="w-3.5 h-3.5" />
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
{subscribed && (
|
||||||
|
<p className="text-accent-lime text-[10px] font-semibold animate-pulse">
|
||||||
|
You're in! Check your inbox for secret codes.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr className="border-white/5 mb-8" />
|
||||||
|
|
||||||
|
{/* Socials & Copyright */}
|
||||||
|
<div className="flex flex-col sm:flex-row justify-between items-center space-y-4 sm:space-y-0">
|
||||||
|
<div className="flex items-center space-x-2 text-[10px] text-white/40">
|
||||||
|
<span>© {new Date().getFullYear()} LytsUp.com. All rights reserved.</span>
|
||||||
|
<span className="hidden sm:inline">•</span>
|
||||||
|
<span className="flex items-center">
|
||||||
|
Made with <Heart className="w-3 h-3 text-accent-pink fill-current mx-1" /> for Gen Z
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Social Icons */}
|
||||||
|
<div className="flex space-x-4">
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
className="w-7 h-7 rounded-full bg-white/5 flex items-center justify-center text-white/60 hover:bg-accent-pink hover:text-white transition-all scale-95 hover:scale-100"
|
||||||
|
aria-label="Instagram"
|
||||||
|
>
|
||||||
|
<Instagram className="w-3.5 h-3.5" />
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
className="w-7 h-7 rounded-full bg-white/5 flex items-center justify-center text-white/60 hover:bg-accent-purple hover:text-white transition-all scale-95 hover:scale-100"
|
||||||
|
aria-label="Twitter"
|
||||||
|
>
|
||||||
|
<Twitter className="w-3.5 h-3.5" />
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
className="w-7 h-7 rounded-full bg-white/5 flex items-center justify-center text-white/60 hover:bg-primary hover:text-dark transition-all scale-95 hover:scale-100"
|
||||||
|
aria-label="Discord"
|
||||||
|
>
|
||||||
|
<MessageSquare className="w-3.5 h-3.5" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
145
src/components/sections/Hero.tsx
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
"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: () => (
|
||||||
|
<div className="w-full h-full flex items-center justify-center">
|
||||||
|
<div className="relative w-24 h-24">
|
||||||
|
<div className="absolute inset-0 rounded-full border-4 border-primary/20" />
|
||||||
|
<div className="absolute inset-0 rounded-full border-4 border-t-primary animate-spin" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<section className="relative min-h-screen flex flex-col justify-between pt-24 overflow-hidden bg-dark">
|
||||||
|
{/* Background Gradients & Glows */}
|
||||||
|
<div className="absolute top-1/4 left-1/10 w-96 h-96 rounded-full bg-accent-pink/10 blur-[120px] pointer-events-none" />
|
||||||
|
<div className="absolute bottom-1/4 right-1/10 w-96 h-96 rounded-full bg-accent-purple/10 blur-[120px] pointer-events-none" />
|
||||||
|
|
||||||
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 w-full flex-grow flex items-center">
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12 items-center w-full py-12">
|
||||||
|
|
||||||
|
{/* Left Column: Copy & Actions */}
|
||||||
|
<div className="space-y-8 text-left z-10">
|
||||||
|
{/* Promo Tag */}
|
||||||
|
<div className="inline-flex items-center space-x-2 px-3 py-1.5 rounded-full bg-white/5 border border-white/10 text-xs font-semibold tracking-wider text-accent-lime uppercase animate-pulse">
|
||||||
|
<Sparkles className="w-3.5 h-3.5" />
|
||||||
|
<span>Next Gen Social Currency</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Main Headline */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h1 className="text-4xl sm:text-6xl font-extrabold tracking-tight font-sora leading-[1.1] text-white">
|
||||||
|
Earn. Spend. <br />
|
||||||
|
<span className="text-transparent bg-clip-text bg-gradient-to-r from-primary via-accent-pink to-accent-purple text-glow-pink">
|
||||||
|
Live More.
|
||||||
|
</span>
|
||||||
|
</h1>
|
||||||
|
<p className="text-white/60 text-lg sm:text-xl font-medium max-w-lg leading-relaxed font-inter">
|
||||||
|
Turn your everyday activities into social capital. Walk, play games, and shop to earn <span className="text-primary font-bold">LYTs</span>. Spend them at the brands you love.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* CTAs */}
|
||||||
|
<div className="flex flex-col sm:flex-row items-stretch sm:items-center gap-4">
|
||||||
|
<a
|
||||||
|
href="#play"
|
||||||
|
className="group relative px-8 py-4 rounded-2xl bg-primary text-dark font-extrabold text-center text-base tracking-wide shadow-[0_0_30px_rgba(255,199,44,0.35)] hover:shadow-[0_0_40px_rgba(255,199,44,0.5)] transition-all transform hover:-translate-y-0.5 cursor-pointer flex items-center justify-center gap-2"
|
||||||
|
>
|
||||||
|
<span>Earn LYTs Now</span>
|
||||||
|
<ArrowRight className="w-5 h-5 group-hover:translate-x-1.5 transition-transform" />
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a
|
||||||
|
href="#spend"
|
||||||
|
className="px-8 py-4 rounded-2xl bg-white/5 hover:bg-white/10 border border-white/10 text-white font-extrabold text-center text-base tracking-wide transition-all transform hover:-translate-y-0.5 cursor-pointer flex items-center justify-center gap-2"
|
||||||
|
>
|
||||||
|
<span>Spend Vouchers</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Live Counters / Stats */}
|
||||||
|
<div className="grid grid-cols-3 gap-6 pt-6 border-t border-white/5">
|
||||||
|
<div className="space-y-1">
|
||||||
|
<div className="text-2xl sm:text-3xl font-extrabold text-white flex items-center gap-1 font-sora">
|
||||||
|
<CountUp end={850} duration={3} />
|
||||||
|
<span>K+</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-white/40 text-xs font-semibold flex items-center gap-1.5">
|
||||||
|
<Users className="w-3.5 h-3.5 text-accent-pink" />
|
||||||
|
<span>Active Users</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-1">
|
||||||
|
<div className="text-2xl sm:text-3xl font-extrabold text-white flex items-center gap-1 font-sora">
|
||||||
|
<CountUp end={120} duration={3} />
|
||||||
|
<span>+</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-white/40 text-xs font-semibold flex items-center gap-1.5">
|
||||||
|
<ShoppingBag className="w-3.5 h-3.5 text-accent-purple" />
|
||||||
|
<span>Brand Partners</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-1">
|
||||||
|
<div className="text-2xl sm:text-3xl font-extrabold text-white flex items-center gap-1 font-sora">
|
||||||
|
<CountUp end={45} duration={3} />
|
||||||
|
<span>M+</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-white/40 text-xs font-semibold flex items-center gap-1.5">
|
||||||
|
<TrendingUp className="w-3.5 h-3.5 text-accent-lime" />
|
||||||
|
<span>Steps Tracked</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Right Column: Interactive 3D Canvas */}
|
||||||
|
<div className="relative h-[450px] sm:h-[600px] w-full flex items-center justify-center">
|
||||||
|
{/* Hologram Circle Rings behind 3D Object */}
|
||||||
|
<div className="absolute w-[300px] sm:w-[450px] h-[300px] sm:h-[450px] rounded-full border border-white/5 animate-pulse" />
|
||||||
|
<div className="absolute w-[200px] sm:w-[350px] h-[200px] sm:h-[350px] rounded-full border border-dashed border-primary/10 animate-spin [animation-duration:40s]" />
|
||||||
|
<div className="absolute w-[100px] sm:w-[250px] h-[100px] sm:h-[250px] rounded-full border border-accent-pink/10 animate-spin [animation-duration:20s] [animation-direction:reverse]" />
|
||||||
|
|
||||||
|
<div className="w-full h-full">
|
||||||
|
<Hero3D />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Highlights Marquee Ticker */}
|
||||||
|
<div className="w-full bg-white/3 border-y border-white/5 py-4 relative z-10 overflow-hidden">
|
||||||
|
<Marquee gradient={false} speed={55} className="font-sora text-sm font-extrabold tracking-widest">
|
||||||
|
{highlights.map((text, idx) => (
|
||||||
|
<div key={idx} className="flex items-center mx-8 space-x-3 text-white">
|
||||||
|
<span className="h-2 w-2 rounded-full bg-primary" />
|
||||||
|
<span>{text}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</Marquee>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
85
src/components/sections/HowItWorks.tsx
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
"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: <Zap className="w-6 h-6 text-accent-pink" />,
|
||||||
|
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: <Coins className="w-6 h-6 text-primary" />,
|
||||||
|
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: <Gift className="w-6 h-6 text-accent-purple" />,
|
||||||
|
bg: "rgba(139, 61, 255, 0.05)",
|
||||||
|
border: "border-accent-purple/15",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="py-24 bg-dark relative overflow-hidden">
|
||||||
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 relative z-10">
|
||||||
|
|
||||||
|
{/* Section Header */}
|
||||||
|
<div className="text-center max-w-3xl mx-auto mb-16 space-y-4">
|
||||||
|
<span className="text-primary font-extrabold text-xs tracking-widest uppercase">
|
||||||
|
Platform Workflow
|
||||||
|
</span>
|
||||||
|
<h2 className="text-3xl sm:text-5xl font-extrabold font-sora text-white leading-tight">
|
||||||
|
The Social Currency <span className="text-transparent bg-clip-text bg-gradient-to-r from-primary to-accent-pink">Lifecycle</span>
|
||||||
|
</h2>
|
||||||
|
<p className="text-white/60 text-base sm:text-lg">
|
||||||
|
A three-step gamified loop designed to reward you for living an active, social, and healthy lifestyle.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Step Cards Grid */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
|
||||||
|
{steps.map((step, idx) => (
|
||||||
|
<motion.div
|
||||||
|
key={step.num}
|
||||||
|
initial={{ opacity: 0, y: 30 }}
|
||||||
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
transition={{ duration: 0.5, delay: idx * 0.15 }}
|
||||||
|
style={{ backgroundColor: step.bg }}
|
||||||
|
className={`glassmorphism rounded-3xl p-8 border ${step.border} relative flex flex-col justify-between group hover:scale-[1.02] transition-transform`}
|
||||||
|
>
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<div className="w-12 h-12 rounded-2xl bg-white/5 border border-white/10 flex items-center justify-center">
|
||||||
|
{step.icon}
|
||||||
|
</div>
|
||||||
|
<span className="text-3xl font-black font-sora text-white/10 group-hover:text-white/20 transition-colors">
|
||||||
|
{step.num}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<h3 className="text-xl font-bold font-sora text-white">{step.title}</h3>
|
||||||
|
<p className="text-white/60 text-sm leading-relaxed">{step.description}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
113
src/components/sections/Leaderboard.tsx
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
"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 className="py-24 bg-dark relative overflow-hidden">
|
||||||
|
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 relative z-10">
|
||||||
|
|
||||||
|
<div className="glassmorphism rounded-3xl p-8 sm:p-12 border border-accent-pink/10 space-y-8 shadow-[0_0_50px_rgba(255,46,136,0.02)]">
|
||||||
|
<div className="text-left space-y-4">
|
||||||
|
<div className="w-12 h-12 rounded-2xl bg-accent-pink/10 border border-accent-pink/20 flex items-center justify-center text-accent-pink">
|
||||||
|
<Trophy className="w-6 h-6 animate-pulse" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<span className="text-accent-pink font-bold text-xs uppercase tracking-wider">Section 14 • Leaderboard</span>
|
||||||
|
<h3 className="text-2xl sm:text-4xl font-extrabold font-sora text-white">
|
||||||
|
Daily Ranks & <br />
|
||||||
|
<span className="text-glow-pink text-accent-pink">Community Standings</span>
|
||||||
|
</h3>
|
||||||
|
<p className="text-white/60 text-sm leading-relaxed">
|
||||||
|
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.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Leaderboard panel list */}
|
||||||
|
<div className="bg-white/3 border border-white/5 rounded-2xl p-4 sm:p-6 space-y-4">
|
||||||
|
<div className="flex items-center justify-between text-[10px] text-white/40 font-bold uppercase tracking-wider px-3 border-b border-white/5 pb-3">
|
||||||
|
<span>User Roster</span>
|
||||||
|
<div className="flex space-x-12">
|
||||||
|
<span>Steps Logged</span>
|
||||||
|
<span>Est. LYTs</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2.5">
|
||||||
|
{leaderboardUsers.map((user) => (
|
||||||
|
<div
|
||||||
|
key={user.rank}
|
||||||
|
className={`flex items-center justify-between p-3 rounded-xl transition-all ${
|
||||||
|
user.highlight
|
||||||
|
? "bg-accent-pink/10 border border-accent-pink/20 shadow-[0_0_15px_rgba(255,46,136,0.08)]"
|
||||||
|
: "bg-white/2 border border-transparent hover:border-white/5"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-center space-x-3">
|
||||||
|
<span
|
||||||
|
className={`w-6 text-center font-sora font-extrabold text-sm ${
|
||||||
|
user.rank === 1
|
||||||
|
? "text-primary text-glow-yellow"
|
||||||
|
: user.rank === 2
|
||||||
|
? "text-slate-300"
|
||||||
|
: user.rank === 3
|
||||||
|
? "text-amber-600"
|
||||||
|
: "text-white/60"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
#{user.rank}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<div className="relative">
|
||||||
|
<div className="w-8 h-8 rounded-full bg-white/10 flex items-center justify-center font-bold text-xs text-white">
|
||||||
|
{user.avatar}
|
||||||
|
</div>
|
||||||
|
<span className="absolute -bottom-1 -right-1 flex h-4 w-4 items-center justify-center rounded-full bg-[#181818] text-[8px] font-bold text-white/80 border border-white/10">
|
||||||
|
L{user.level}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<span className="text-xs font-bold text-white block">{user.name}</span>
|
||||||
|
{user.highlight && (
|
||||||
|
<span className="text-[9px] text-accent-pink font-semibold flex items-center gap-1 mt-0.5">
|
||||||
|
<Flame className="w-3 h-3 fill-current animate-pulse" />
|
||||||
|
<span>{profile.streak} days active streak</span>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center space-x-12 text-right">
|
||||||
|
<span className="text-xs font-semibold text-white/80 font-mono">
|
||||||
|
{user.steps.toLocaleString()}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span className="text-sm font-bold text-primary font-mono">
|
||||||
|
+{user.points}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
270
src/components/sections/LeaderboardSection.tsx
Normal file
@@ -0,0 +1,270 @@
|
|||||||
|
"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<TeamChallenge[]>([
|
||||||
|
{
|
||||||
|
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: <Footprints className="w-5 h-5 text-accent-lime" />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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: <Landmark className="w-5 h-5 text-emerald-400" />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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: <Users className="w-5 h-5 text-accent-pink" />,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<section id="leaderboard" className="py-24 bg-[#090909] relative overflow-hidden">
|
||||||
|
{/* Background ambient lighting */}
|
||||||
|
<div className="absolute top-1/3 left-0 w-80 h-80 rounded-full bg-accent-lime/5 blur-[100px] pointer-events-none" />
|
||||||
|
<div className="absolute bottom-1/3 right-0 w-80 h-80 rounded-full bg-accent-purple/5 blur-[100px] pointer-events-none" />
|
||||||
|
|
||||||
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 relative z-10">
|
||||||
|
|
||||||
|
{/* Section Header */}
|
||||||
|
<div className="text-center max-w-3xl mx-auto mb-20 space-y-4">
|
||||||
|
<span className="text-accent-pink font-extrabold text-xs tracking-widest uppercase">
|
||||||
|
Community Hub
|
||||||
|
</span>
|
||||||
|
<h2 className="text-3xl sm:text-5xl font-extrabold font-sora text-white leading-tight">
|
||||||
|
Play Together. <span className="text-glow-yellow text-primary">Earn Together.</span>
|
||||||
|
</h2>
|
||||||
|
<p className="text-white/60 text-base sm:text-lg">
|
||||||
|
Join squad goals to multiply your LYT earnings, or compete for the top spots on the daily leaderboard.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Contents Grid */}
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12 items-start">
|
||||||
|
|
||||||
|
{/* Left Column: Team Challenges */}
|
||||||
|
<div className="space-y-6">
|
||||||
|
<h3 className="text-2xl font-bold font-sora text-white flex items-center gap-2">
|
||||||
|
<Trophy className="w-6 h-6 text-primary" />
|
||||||
|
<span>Active Squad Challenges</span>
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
{challenges.map((ch) => {
|
||||||
|
const percentage = Math.min(100, Math.round((ch.current / ch.goal) * 100));
|
||||||
|
const isCompleted = ch.current >= ch.goal;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={ch.id}
|
||||||
|
className={`glassmorphism rounded-3xl p-6 border transition-colors ${
|
||||||
|
ch.joined ? "border-primary/20 bg-primary/2" : "border-white/5"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex justify-between items-start gap-4 mb-4">
|
||||||
|
<div className="flex items-center space-x-3">
|
||||||
|
<div className="w-10 h-10 rounded-xl bg-white/5 border border-white/10 flex items-center justify-center">
|
||||||
|
{ch.icon}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h4 className="text-base font-bold text-white font-sora">{ch.title}</h4>
|
||||||
|
<p className="text-white/60 text-xs mt-0.5 leading-relaxed">{ch.description}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="text-right">
|
||||||
|
<span className="text-xs text-white/40 font-bold uppercase tracking-wider block">Reward</span>
|
||||||
|
<span className="text-sm font-extrabold text-primary">+{ch.reward} LYTs</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Progress Slider */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex justify-between text-xs font-semibold">
|
||||||
|
<span className="text-white/50">{ch.current.toLocaleString()} / {ch.goal.toLocaleString()}</span>
|
||||||
|
<span className="text-primary">{percentage}%</span>
|
||||||
|
</div>
|
||||||
|
<div className="w-full h-2 bg-white/5 rounded-full overflow-hidden">
|
||||||
|
<div
|
||||||
|
style={{ width: `${percentage}%` }}
|
||||||
|
className="h-full bg-gradient-to-r from-primary to-accent-lime rounded-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Button */}
|
||||||
|
<div className="mt-6 flex justify-end">
|
||||||
|
<button
|
||||||
|
onClick={() => handleJoinChallenge(ch.id, ch.reward)}
|
||||||
|
disabled={isCompleted}
|
||||||
|
className={`px-5 py-2 rounded-xl text-xs font-bold transition-all flex items-center gap-1.5 cursor-pointer ${
|
||||||
|
isCompleted
|
||||||
|
? "bg-accent-lime/10 border border-accent-lime/20 text-accent-lime cursor-not-allowed"
|
||||||
|
: ch.joined
|
||||||
|
? "bg-primary text-dark hover:bg-primary/90"
|
||||||
|
: "bg-white/5 border border-white/10 text-white hover:bg-white/10"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{isCompleted ? (
|
||||||
|
<>
|
||||||
|
<Check className="w-3.5 h-3.5" />
|
||||||
|
<span>Claimed</span>
|
||||||
|
</>
|
||||||
|
) : ch.joined ? (
|
||||||
|
<>
|
||||||
|
<span>Complete & Claim</span>
|
||||||
|
<ArrowUpRight className="w-3.5 h-3.5" />
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<span>Join Challenge</span>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Right Column: Leaderboard list */}
|
||||||
|
<div className="space-y-6">
|
||||||
|
<h3 className="text-2xl font-bold font-sora text-white flex items-center gap-2">
|
||||||
|
<Users className="w-6 h-6 text-accent-pink" />
|
||||||
|
<span>Daily Leaderboard</span>
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div className="glassmorphism rounded-3xl p-6 space-y-4">
|
||||||
|
<div className="flex items-center justify-between text-xs text-white/40 font-bold uppercase tracking-wider px-3 border-b border-white/5 pb-3">
|
||||||
|
<span>User</span>
|
||||||
|
<div className="flex space-x-12">
|
||||||
|
<span>Steps</span>
|
||||||
|
<span>Est. LYTs</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-3">
|
||||||
|
{leaderboardUsers.map((user) => (
|
||||||
|
<div
|
||||||
|
key={user.rank}
|
||||||
|
className={`flex items-center justify-between p-3 rounded-2xl transition-all ${
|
||||||
|
user.highlight
|
||||||
|
? "bg-accent-pink/10 border border-accent-pink/20 shadow-[0_0_15px_rgba(255,46,136,0.1)]"
|
||||||
|
: "bg-white/3 border border-transparent hover:border-white/5"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-center space-x-3">
|
||||||
|
{/* Rank badge */}
|
||||||
|
<span
|
||||||
|
className={`w-6 text-center font-sora font-extrabold text-sm ${
|
||||||
|
user.rank === 1
|
||||||
|
? "text-primary text-glow-yellow"
|
||||||
|
: user.rank === 2
|
||||||
|
? "text-slate-300"
|
||||||
|
: user.rank === 3
|
||||||
|
? "text-amber-600"
|
||||||
|
: "text-white/60"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
#{user.rank}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
{/* Avatar */}
|
||||||
|
<div className="relative">
|
||||||
|
<div className="w-9 h-9 rounded-full bg-white/10 flex items-center justify-center font-bold text-sm text-white font-sora">
|
||||||
|
{user.avatar}
|
||||||
|
</div>
|
||||||
|
<span className="absolute -bottom-1 -right-1 flex h-4 w-4 items-center justify-center rounded-full bg-[#181818] text-[9px] font-bold text-white/80 border border-white/10">
|
||||||
|
L{user.level}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Profile Details */}
|
||||||
|
<div>
|
||||||
|
<span className="text-sm font-bold text-white block">{user.name}</span>
|
||||||
|
{user.highlight && (
|
||||||
|
<span className="text-[10px] text-accent-pink font-semibold flex items-center gap-1 mt-0.5">
|
||||||
|
<Flame className="w-3 h-3 fill-current animate-pulse" />
|
||||||
|
<span>{profile.streak} days active streak</span>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center space-x-12 text-right">
|
||||||
|
{/* Steps */}
|
||||||
|
<span className="text-xs font-semibold text-white/80 font-mono">
|
||||||
|
{user.steps.toLocaleString()}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
{/* Points */}
|
||||||
|
<span className="text-sm font-bold text-primary font-mono">
|
||||||
|
+{user.points}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
12
src/components/sections/PlayToEarn.tsx
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import CoinCatcher from "../games/CoinCatcher";
|
||||||
|
|
||||||
|
export default function PlayToEarn() {
|
||||||
|
return (
|
||||||
|
<div className="w-full">
|
||||||
|
{/* Renders the fully functional canvas coin-catcher mini-game */}
|
||||||
|
<CoinCatcher />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
92
src/components/sections/ReferToEarn.tsx
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
"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 (
|
||||||
|
<section className="py-24 bg-dark relative overflow-hidden">
|
||||||
|
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 relative z-10">
|
||||||
|
|
||||||
|
<div className="glassmorphism rounded-3xl p-8 sm:p-12 border border-accent-orange/10 shadow-[0_0_50px_rgba(255,107,53,0.05)]">
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 items-center">
|
||||||
|
|
||||||
|
{/* Referral Widget */}
|
||||||
|
<div className="bg-[#0b0b0b] border border-white/5 rounded-2xl p-6 space-y-4 order-2 md:order-1">
|
||||||
|
<div>
|
||||||
|
<label className="text-[9px] font-bold text-white/40 uppercase tracking-widest block mb-2">
|
||||||
|
Your Referral Link:
|
||||||
|
</label>
|
||||||
|
<div className="relative flex">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
readOnly
|
||||||
|
value={referralCode}
|
||||||
|
className="w-full bg-white/3 border border-white/10 rounded-xl px-3 py-3 text-xs text-white/80 font-mono focus:outline-none select-all pr-10"
|
||||||
|
/>
|
||||||
|
<div className="absolute right-3 top-1/2 -translate-y-1/2 text-white/40">
|
||||||
|
<Share2 className="w-4 h-4" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="text-xs text-white/50 leading-relaxed bg-white/3 p-3.5 rounded-xl border border-white/5">
|
||||||
|
🔥 <span className="text-white font-semibold">Invite Code Bonus:</span> Friends start with <span className="text-primary font-bold">50 LYTs</span> and you get <span className="text-primary font-bold">50 LYTs</span> immediately on activation!
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={handleCopyReferral}
|
||||||
|
className={`w-full py-3.5 rounded-xl font-extrabold text-xs tracking-wider flex items-center justify-center gap-2 cursor-pointer transition-all ${
|
||||||
|
referralCopied
|
||||||
|
? "bg-accent-lime text-dark shadow-[0_0_20px_rgba(200,255,55,0.3)]"
|
||||||
|
: "bg-white/5 hover:bg-accent-orange hover:text-white text-white border border-white/10 hover:border-accent-orange"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{referralCopied ? (
|
||||||
|
<>
|
||||||
|
<ClipboardCheck className="w-4.5 h-4.5 animate-bounce" />
|
||||||
|
<span>Invite Copied! +50 LYT</span>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<span>Copy Invitation Link</span>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Information */}
|
||||||
|
<div className="space-y-6 text-left order-1 md:order-2 md:pl-6">
|
||||||
|
<div className="w-12 h-12 rounded-2xl bg-accent-orange/10 border border-accent-orange/20 flex items-center justify-center text-accent-orange">
|
||||||
|
<Share2 className="w-6 h-6" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<span className="text-accent-orange font-bold text-xs uppercase tracking-wider">Section 09 • Refer-to-Earn</span>
|
||||||
|
<h3 className="text-2xl sm:text-4xl font-extrabold font-sora text-white">
|
||||||
|
Squad Invites & <br />
|
||||||
|
<span className="text-glow-purple text-accent-orange">Growth Loops</span>
|
||||||
|
</h3>
|
||||||
|
<p className="text-white/60 text-sm leading-relaxed">
|
||||||
|
Earn bonuses by inviting your friends. Create shared earning pools, unlock group level multipliers, and dominate leaderboards together.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
127
src/components/sections/ShopToEarn.tsx
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
"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<string[]>(["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 (
|
||||||
|
<section className="py-24 bg-dark relative overflow-hidden">
|
||||||
|
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 relative z-10">
|
||||||
|
|
||||||
|
<div className="glassmorphism rounded-3xl p-8 sm:p-12 border border-accent-pink/10 shadow-[0_0_50px_rgba(255,46,136,0.05)]">
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 items-center">
|
||||||
|
|
||||||
|
{/* Simulator box */}
|
||||||
|
<div className="bg-[#0b0b0b] border border-white/5 rounded-2xl p-6 space-y-4 order-2 md:order-1">
|
||||||
|
<h4 className="text-[10px] font-bold text-white/40 uppercase tracking-widest">Select items purchased:</h4>
|
||||||
|
<div className="space-y-2.5">
|
||||||
|
{shopItems.map((item) => (
|
||||||
|
<div
|
||||||
|
key={item.id}
|
||||||
|
onClick={() => {
|
||||||
|
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"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-center space-x-3">
|
||||||
|
<div
|
||||||
|
className={`w-4 h-4 rounded flex items-center justify-center border transition-all ${
|
||||||
|
selectedItems.includes(item.id)
|
||||||
|
? "bg-accent-pink border-accent-pink"
|
||||||
|
: "border-white/20"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{selectedItems.includes(item.id) && <Check className="w-3.5 h-3.5 text-white" />}
|
||||||
|
</div>
|
||||||
|
<span className="text-xs font-semibold">{item.name}</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-right">
|
||||||
|
<span className="text-xs font-extrabold text-white block">{item.cost}</span>
|
||||||
|
<span className="text-[9px] text-accent-pink font-bold">+{item.reward} LYTs</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={handleShopClaim}
|
||||||
|
disabled={shopClaimed || totalReward === 0}
|
||||||
|
className={`w-full py-3 rounded-xl font-extrabold text-xs tracking-wider flex items-center justify-center gap-2 cursor-pointer transition-all ${
|
||||||
|
shopClaimed
|
||||||
|
? "bg-accent-lime text-dark shadow-[0_0_20px_rgba(200,255,55,0.3)]"
|
||||||
|
: totalReward === 0
|
||||||
|
? "bg-white/5 text-white/20 border border-white/5 cursor-not-allowed"
|
||||||
|
: "bg-white/5 hover:bg-accent-pink hover:text-white text-white border border-white/10 hover:border-accent-pink"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{shopClaimed ? (
|
||||||
|
<>
|
||||||
|
<Check className="w-4 h-4" />
|
||||||
|
<span>Cashback Claimed! +{totalReward} LYTs</span>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<span>Claim Receipt Cashback (+{totalReward} LYTs)</span>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Information */}
|
||||||
|
<div className="space-y-6 text-left order-1 md:order-2 md:pl-6">
|
||||||
|
<div className="w-12 h-12 rounded-2xl bg-accent-pink/10 border border-accent-pink/20 flex items-center justify-center text-accent-pink">
|
||||||
|
<ShoppingBag className="w-6 h-6" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<span className="text-accent-pink font-bold text-xs uppercase tracking-wider">Section 06 • Shop-to-Earn</span>
|
||||||
|
<h3 className="text-2xl sm:text-4xl font-extrabold font-sora text-white">
|
||||||
|
Cashback & <br />
|
||||||
|
<span className="text-glow-pink text-accent-pink">Instant Rebates</span>
|
||||||
|
</h3>
|
||||||
|
<p className="text-white/60 text-sm leading-relaxed">
|
||||||
|
Earn high cashback rates when shopping with retail partners. Submit digital checks or verify loyalty IDs to earn LYT social tokens.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
96
src/components/sections/SocialWall.tsx
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
"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 (
|
||||||
|
<section className="py-24 bg-[#090909] relative overflow-hidden">
|
||||||
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 relative z-10">
|
||||||
|
|
||||||
|
{/* Header */}
|
||||||
|
<div className="text-center max-w-3xl mx-auto mb-20 space-y-4">
|
||||||
|
<span className="text-accent-purple font-extrabold text-xs tracking-widest uppercase">
|
||||||
|
Gen Z Social Wall
|
||||||
|
</span>
|
||||||
|
<h2 className="text-3xl sm:text-5xl font-extrabold font-sora text-white leading-tight">
|
||||||
|
Hear it from the <span className="text-transparent bg-clip-text bg-gradient-to-r from-accent-pink via-accent-purple to-accent-lime">Squad</span>
|
||||||
|
</h2>
|
||||||
|
<p className="text-white/60 text-base sm:text-lg">
|
||||||
|
See how real users are converting their daily distance and plays into social currency vouchers.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Social Cards Grid */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
|
||||||
|
{posts.map((post, idx) => (
|
||||||
|
<motion.div
|
||||||
|
key={idx}
|
||||||
|
initial={{ opacity: 0, y: 30 }}
|
||||||
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
transition={{ duration: 0.5, delay: idx * 0.15 }}
|
||||||
|
style={{ boxShadow: `0 10px 30px -15px ${post.bgGlow}` }}
|
||||||
|
className="glassmorphism rounded-3xl p-6 border border-white/5 flex flex-col justify-between hover:border-white/10 transition-colors"
|
||||||
|
>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex justify-between items-center border-b border-white/5 pb-3">
|
||||||
|
<span className="font-bold text-sm text-white">{post.user}</span>
|
||||||
|
<span className="text-[10px] font-bold text-accent-lime uppercase bg-white/5 px-2 py-0.5 rounded border border-white/10">
|
||||||
|
Redeemed {post.brand}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-white/80 text-sm leading-relaxed font-medium">
|
||||||
|
"{post.content}"
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Feed Actions Mock */}
|
||||||
|
<div className="flex items-center space-x-6 text-white/40 mt-6 pt-3 border-t border-white/5 text-xs font-semibold">
|
||||||
|
<button className="flex items-center space-x-1.5 hover:text-accent-pink transition-colors">
|
||||||
|
<Heart className="w-4 h-4" />
|
||||||
|
<span>{post.likes}</span>
|
||||||
|
</button>
|
||||||
|
<button className="flex items-center space-x-1.5 hover:text-accent-purple transition-colors">
|
||||||
|
<MessageCircle className="w-4 h-4" />
|
||||||
|
<span>{post.comments}</span>
|
||||||
|
</button>
|
||||||
|
<button className="flex items-center space-x-1.5 hover:text-primary transition-colors ml-auto">
|
||||||
|
<Share className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
268
src/components/sections/SpendSection.tsx
Normal file
@@ -0,0 +1,268 @@
|
|||||||
|
"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<string | null>(null);
|
||||||
|
const [copiedId, setCopiedId] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const availableVouchers: BrandVoucher[] = [
|
||||||
|
{
|
||||||
|
id: "v-sbux",
|
||||||
|
brand: "Starbucks",
|
||||||
|
discount: "$10 Gift Card",
|
||||||
|
cost: 150,
|
||||||
|
icon: <Coffee className="w-8 h-8 text-emerald-400" />,
|
||||||
|
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: <ShoppingBag className="w-8 h-8 text-white" />,
|
||||||
|
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: <Music className="w-8 h-8 text-green-400" />,
|
||||||
|
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: <Film className="w-8 h-8 text-red-500" />,
|
||||||
|
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: <Dumbbell className="w-8 h-8 text-amber-500" />,
|
||||||
|
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: <Ticket className="w-8 h-8 text-sky-400" />,
|
||||||
|
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 (
|
||||||
|
<section id="spend" className="py-24 bg-dark relative overflow-hidden">
|
||||||
|
{/* Visual background glows */}
|
||||||
|
<div className="absolute top-1/4 right-0 w-[500px] h-[500px] rounded-full bg-primary/5 blur-[150px] pointer-events-none" />
|
||||||
|
<div className="absolute bottom-1/4 left-0 w-[500px] h-[500px] rounded-full bg-accent-pink/5 blur-[150px] pointer-events-none" />
|
||||||
|
|
||||||
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 relative z-10">
|
||||||
|
|
||||||
|
{/* Section Header */}
|
||||||
|
<div className="text-center max-w-3xl mx-auto mb-16 space-y-4">
|
||||||
|
<span className="text-accent-purple font-extrabold text-xs tracking-widest uppercase">
|
||||||
|
Spend Your LYTs
|
||||||
|
</span>
|
||||||
|
<h2 className="text-3xl sm:text-5xl font-extrabold font-sora text-white leading-tight">
|
||||||
|
Claim Vouchers at <span className="text-transparent bg-clip-text bg-gradient-to-r from-accent-pink via-accent-purple to-accent-lime">Your Favorite Brands</span>
|
||||||
|
</h2>
|
||||||
|
<p className="text-white/60 text-base sm:text-lg">
|
||||||
|
Redeem your accumulated LYT coins for gift cards, subscriptions, and discounts. Click redeem to generate a checkout code.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Dynamic Warning Alert */}
|
||||||
|
{errorMessage && (
|
||||||
|
<div className="max-w-xl mx-auto mb-8 bg-red-950/40 border border-red-500/20 text-red-200 px-5 py-4 rounded-2xl flex items-center justify-between text-sm animate-bounce">
|
||||||
|
<span>{errorMessage}</span>
|
||||||
|
<a href="#play" className="text-primary font-bold underline hover:no-underline">Earn LYTs fast</a>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Success Modal Popup */}
|
||||||
|
{redeemedCode && (
|
||||||
|
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-dark/80 backdrop-blur-sm animate-in fade-in duration-200">
|
||||||
|
<div className="glassmorphism max-w-md w-full p-8 rounded-3xl border border-accent-lime/30 text-center space-y-6 shadow-[0_0_50px_rgba(200,255,55,0.15)]">
|
||||||
|
<div className="w-16 h-16 rounded-full bg-accent-lime/10 border border-accent-lime/30 mx-auto flex items-center justify-center text-accent-lime">
|
||||||
|
<ShieldCheck className="w-8 h-8 animate-pulse" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<h3 className="text-2xl font-bold font-sora text-white">Redemption Successful!</h3>
|
||||||
|
<p className="text-white/60 text-sm">
|
||||||
|
You successfully claimed your <span className="text-white font-bold">{redeemedCode.brand}</span> voucher code.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Code Shelf */}
|
||||||
|
<div className="bg-white/5 border border-white/10 rounded-2xl p-4 flex items-center justify-between">
|
||||||
|
<code className="text-xl font-mono font-bold tracking-wider text-accent-lime">{redeemedCode.code}</code>
|
||||||
|
<button
|
||||||
|
onClick={() => copyToClipboard("modal", redeemedCode.code)}
|
||||||
|
className="p-2.5 rounded-xl bg-white/5 hover:bg-white/10 text-white/80 hover:text-white transition-colors cursor-pointer"
|
||||||
|
>
|
||||||
|
{copiedId === "modal" ? <Check className="w-5 h-5 text-accent-lime" /> : <Copy className="w-5 h-5" />}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<button
|
||||||
|
onClick={() => setRedeemedCode(null)}
|
||||||
|
className="w-full py-3 rounded-xl bg-primary text-dark font-extrabold text-sm tracking-wider cursor-pointer"
|
||||||
|
>
|
||||||
|
Awesome, Thanks!
|
||||||
|
</button>
|
||||||
|
<span className="text-[10px] text-white/40">Code is also saved to your Active Vouchers panel below.</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Voucher Grid */}
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6 mb-20">
|
||||||
|
{availableVouchers.map((voucher) => {
|
||||||
|
const isAffordable = profile.lytBalance >= voucher.cost;
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={voucher.id}
|
||||||
|
style={{
|
||||||
|
boxShadow: `0 10px 30px -15px ${voucher.bgGlow}`,
|
||||||
|
}}
|
||||||
|
className={`glassmorphism rounded-3xl p-6 flex flex-col justify-between border transition-all duration-300 transform hover:-translate-y-1.5 ${voucher.color}`}
|
||||||
|
>
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* Brand Header */}
|
||||||
|
<div className="flex justify-between items-start">
|
||||||
|
<div className="p-3.5 bg-white/5 rounded-2xl border border-white/5 flex items-center justify-center">
|
||||||
|
{voucher.icon}
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-1.5 px-3 py-1 rounded-full bg-white/5 border border-white/10 font-sora font-extrabold text-xs text-white">
|
||||||
|
<Coins className="w-3.5 h-3.5 text-primary" />
|
||||||
|
<span>{voucher.cost} LYTs</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Info */}
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<h3 className="text-xl font-bold font-sora text-white">{voucher.brand}</h3>
|
||||||
|
<p className="text-white/60 text-sm font-semibold">{voucher.discount}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Bottom action */}
|
||||||
|
<div className="mt-8">
|
||||||
|
<button
|
||||||
|
onClick={() => handleRedeem(voucher)}
|
||||||
|
className={`w-full py-3.5 rounded-xl font-extrabold text-xs tracking-wider uppercase transition-all flex items-center justify-center gap-1.5 cursor-pointer ${
|
||||||
|
isAffordable
|
||||||
|
? "bg-white/5 hover:bg-primary hover:text-dark text-white border border-white/10 hover:border-primary shadow-sm"
|
||||||
|
: "bg-white/2 text-white/20 border border-white/5 cursor-not-allowed"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<span>Redeem Voucher</span>
|
||||||
|
<ArrowRight className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Claimed Vouchers Display */}
|
||||||
|
{vouchers.length > 0 && (
|
||||||
|
<div className="border-t border-white/5 pt-16">
|
||||||
|
<h3 className="text-2xl font-bold font-sora text-white mb-6 flex items-center gap-2">
|
||||||
|
<span>Your Claimed Vouchers</span>
|
||||||
|
<span className="text-xs px-2.5 py-1 rounded-full bg-accent-lime/10 border border-accent-lime/20 text-accent-lime">
|
||||||
|
{vouchers.length} Total
|
||||||
|
</span>
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
|
{vouchers.map((v) => (
|
||||||
|
<div
|
||||||
|
key={v.id}
|
||||||
|
className="bg-[#0b0b0b] border border-white/5 rounded-2xl p-5 flex items-center justify-between gap-4 animate-in slide-in-from-bottom-2 duration-300"
|
||||||
|
>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<span className="text-white font-extrabold text-sm">{v.brand}</span>
|
||||||
|
<p className="text-white/60 text-xs">{v.discount}</p>
|
||||||
|
<span className="text-[10px] text-white/40 block">Claimed on {v.claimedAt}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="text-right space-y-1.5">
|
||||||
|
<span className="inline-block px-2.5 py-1 bg-white/5 border border-white/10 rounded-lg text-xs font-mono font-extrabold text-accent-lime">
|
||||||
|
{v.code}
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
onClick={() => copyToClipboard(v.id, v.code)}
|
||||||
|
className="text-[10px] font-bold text-primary hover:underline flex items-center justify-end gap-1 ml-auto cursor-pointer"
|
||||||
|
>
|
||||||
|
{copiedId === v.id ? (
|
||||||
|
<span className="text-accent-lime">Copied!</span>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Copy className="w-3 h-3" />
|
||||||
|
<span>Copy Code</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
12
src/components/sections/SpendShop.tsx
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import SpendSection from "./SpendSection";
|
||||||
|
|
||||||
|
export default function SpendShop() {
|
||||||
|
return (
|
||||||
|
<div className="w-full">
|
||||||
|
{/* Renders the fully functional vouchers shop store catalog */}
|
||||||
|
<SpendSection />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
157
src/components/sections/SquadChallenges.tsx
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
"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<TeamChallenge[]>([
|
||||||
|
{
|
||||||
|
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: <Footprints className="w-5 h-5 text-accent-lime" />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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: <Landmark className="w-5 h-5 text-emerald-400" />,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
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 className="py-24 bg-[#090909] relative overflow-hidden">
|
||||||
|
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 relative z-10">
|
||||||
|
|
||||||
|
<div className="glassmorphism rounded-3xl p-8 sm:p-12 border border-primary/10 space-y-8">
|
||||||
|
<div className="text-left space-y-4">
|
||||||
|
<div className="w-12 h-12 rounded-2xl bg-primary/10 border border-primary/20 flex items-center justify-center text-primary">
|
||||||
|
<Users className="w-6 h-6" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<span className="text-primary font-bold text-xs uppercase tracking-wider">Section 13 • Squad Goals</span>
|
||||||
|
<h3 className="text-2xl sm:text-4xl font-extrabold font-sora text-white">
|
||||||
|
Cooperative Team <br />
|
||||||
|
<span className="text-glow-yellow text-primary">Active Challenges</span>
|
||||||
|
</h3>
|
||||||
|
<p className="text-white/60 text-sm leading-relaxed">
|
||||||
|
Connect with friends or match with nearby community squads. Log steps, complete visits, and unlock bulk LYT currency bonuses together.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Challenges listing */}
|
||||||
|
<div className="space-y-4 pt-4">
|
||||||
|
{challenges.map((ch) => {
|
||||||
|
const percentage = Math.min(100, Math.round((ch.current / ch.goal) * 100));
|
||||||
|
const isCompleted = ch.current >= ch.goal;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={ch.id}
|
||||||
|
className={`glassmorphism rounded-2xl p-6 border transition-colors ${
|
||||||
|
ch.joined ? "border-primary/20 bg-primary/2" : "border-white/5"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex justify-between items-start gap-4 mb-4">
|
||||||
|
<div className="flex items-center space-x-3">
|
||||||
|
<div className="w-10 h-10 rounded-xl bg-white/5 border border-white/10 flex items-center justify-center">
|
||||||
|
{ch.icon}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h4 className="text-sm font-bold text-white font-sora">{ch.title}</h4>
|
||||||
|
<p className="text-white/60 text-xs mt-0.5 leading-relaxed">{ch.description}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="text-right">
|
||||||
|
<span className="text-[9px] text-white/40 font-bold uppercase tracking-wider block">Reward</span>
|
||||||
|
<span className="text-xs font-extrabold text-primary">+{ch.reward} LYTs</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex justify-between text-xs font-semibold">
|
||||||
|
<span className="text-white/50">{ch.current.toLocaleString()} / {ch.goal.toLocaleString()}</span>
|
||||||
|
<span className="text-primary">{percentage}%</span>
|
||||||
|
</div>
|
||||||
|
<div className="w-full h-1.5 bg-white/5 rounded-full overflow-hidden">
|
||||||
|
<div
|
||||||
|
style={{ width: `${percentage}%` }}
|
||||||
|
className="h-full bg-gradient-to-r from-primary to-accent-lime rounded-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-4 flex justify-end">
|
||||||
|
<button
|
||||||
|
onClick={() => handleJoinChallenge(ch.id, ch.reward)}
|
||||||
|
disabled={isCompleted}
|
||||||
|
className={`px-4 py-2 rounded-xl text-xs font-bold transition-all flex items-center gap-1.5 cursor-pointer ${
|
||||||
|
isCompleted
|
||||||
|
? "bg-accent-lime/10 border border-accent-lime/20 text-accent-lime cursor-not-allowed"
|
||||||
|
: ch.joined
|
||||||
|
? "bg-primary text-dark hover:bg-primary/90"
|
||||||
|
: "bg-white/5 border border-white/10 text-white hover:bg-white/10"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{isCompleted ? (
|
||||||
|
<>
|
||||||
|
<Check className="w-3.5 h-3.5" />
|
||||||
|
<span>Claimed</span>
|
||||||
|
</>
|
||||||
|
) : ch.joined ? (
|
||||||
|
<span>Complete & Claim</span>
|
||||||
|
) : (
|
||||||
|
<span>Join Squad Goal</span>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
31
src/components/sections/StorySection.tsx
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
"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 (
|
||||||
|
<section
|
||||||
|
id={id}
|
||||||
|
className="relative flex w-full items-center justify-center scroll-mt-0"
|
||||||
|
>
|
||||||
|
<div className="w-full">
|
||||||
|
<Image
|
||||||
|
src={src}
|
||||||
|
alt={alt}
|
||||||
|
width={3000}
|
||||||
|
height={1688}
|
||||||
|
priority={priority}
|
||||||
|
sizes="100vw"
|
||||||
|
className="h-auto w-full select-none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
72
src/components/sections/ValueProp.tsx
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
"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: <Award className="w-5 h-5 text-primary" />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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: <ShieldCheck className="w-5 h-5 text-accent-lime" />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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: <Zap className="w-5 h-5 text-accent-pink" />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Health & Well-being Loop",
|
||||||
|
desc: "Earn currency while prioritizing your physical health, mental focus, and social community connections.",
|
||||||
|
icon: <Heart className="w-5 h-5 text-accent-purple" />,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="py-24 bg-[#090909] relative overflow-hidden">
|
||||||
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 relative z-10">
|
||||||
|
|
||||||
|
{/* Header */}
|
||||||
|
<div className="text-center max-w-3xl mx-auto mb-16 space-y-4">
|
||||||
|
<span className="text-accent-pink font-extrabold text-xs tracking-widest uppercase">
|
||||||
|
Platform Benefits
|
||||||
|
</span>
|
||||||
|
<h2 className="text-3xl sm:text-5xl font-extrabold font-sora text-white leading-tight">
|
||||||
|
Designed for the <span className="text-glow-yellow text-primary">Connected Generation</span>
|
||||||
|
</h2>
|
||||||
|
<p className="text-white/60 text-base sm:text-lg">
|
||||||
|
LytsUp shifts the loyalty points equation. Discover why thousands of users sync daily.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Grid */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
|
||||||
|
{points.map((item, idx) => (
|
||||||
|
<motion.div
|
||||||
|
key={idx}
|
||||||
|
initial={{ opacity: 0, x: idx % 2 === 0 ? -30 : 30 }}
|
||||||
|
whileInView={{ opacity: 1, x: 0 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
transition={{ duration: 0.5 }}
|
||||||
|
className="glassmorphism rounded-3xl p-8 border border-white/5 flex gap-5 hover:border-white/10 transition-colors"
|
||||||
|
>
|
||||||
|
<div className="w-10 h-10 rounded-xl bg-white/5 border border-white/10 flex items-center justify-center shrink-0">
|
||||||
|
{item.icon}
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<h3 className="text-lg font-bold font-sora text-white">{item.title}</h3>
|
||||||
|
<p className="text-white/60 text-sm leading-relaxed">{item.desc}</p>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
103
src/components/sections/VisitToEarn.tsx
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
"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<string[]>([]);
|
||||||
|
const [mapClaimed, setMapClaimed] = useState<string | null>(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 (
|
||||||
|
<section className="py-24 bg-[#090909] relative overflow-hidden">
|
||||||
|
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 relative z-10">
|
||||||
|
|
||||||
|
<div className="glassmorphism rounded-3xl p-8 sm:p-12 border border-accent-purple/10 shadow-[0_0_50px_rgba(139,61,255,0.05)]">
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 items-center">
|
||||||
|
|
||||||
|
{/* Information */}
|
||||||
|
<div className="space-y-6 text-left">
|
||||||
|
<div className="w-12 h-12 rounded-2xl bg-accent-purple/10 border border-accent-purple/20 flex items-center justify-center text-accent-purple">
|
||||||
|
<MapPin className="w-6 h-6" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<span className="text-accent-purple font-bold text-xs uppercase tracking-wider">Section 08 • Visit-to-Earn</span>
|
||||||
|
<h3 className="text-2xl sm:text-4xl font-extrabold font-sora text-white">
|
||||||
|
Location Check-Ins & <br />
|
||||||
|
<span className="text-glow-purple text-accent-purple">GPS Rewards</span>
|
||||||
|
</h3>
|
||||||
|
<p className="text-white/60 text-sm leading-relaxed">
|
||||||
|
Earn points simply by visiting local spaces. Confirm your location status via GPS check-ins at restaurants, parks, concerts, and malls.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Map Simulator */}
|
||||||
|
<div className="bg-[#0b0b0b] border border-white/5 rounded-2xl p-6 space-y-4 relative overflow-hidden min-h-[220px]">
|
||||||
|
{mapClaimed && (
|
||||||
|
<div className="absolute inset-0 bg-dark/95 flex items-center justify-center z-10 p-4 text-center animate-in fade-in duration-200">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Sparkles className="w-8 h-8 text-accent-purple mx-auto animate-bounce" />
|
||||||
|
<p className="text-sm font-semibold text-white">Checked in at {mapClaimed}!</p>
|
||||||
|
<p className="text-xs text-accent-lime font-bold">LYT reward added directly to wallet</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<h4 className="text-[10px] font-bold text-white/40 uppercase tracking-widest">Active nodes near you:</h4>
|
||||||
|
<div className="space-y-2.5">
|
||||||
|
{checkinLocations.map((loc) => {
|
||||||
|
const visited = visitedStores.includes(loc.id);
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={loc.id}
|
||||||
|
className={`flex items-center justify-between p-3 rounded-xl border transition-all ${
|
||||||
|
visited
|
||||||
|
? "bg-white/2 border-white/5 opacity-55"
|
||||||
|
: "bg-white/3 border-white/5 hover:border-accent-purple/35"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-center space-x-3">
|
||||||
|
<MapPin className={`w-4 h-4 ${visited ? "text-white/40" : "text-accent-purple animate-bounce"}`} />
|
||||||
|
<span className="text-xs font-semibold text-white">{loc.name}</span>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => handleCheckin(loc.id, loc.name, loc.reward)}
|
||||||
|
disabled={visited}
|
||||||
|
className={`px-3 py-1.5 rounded-lg text-[10px] font-bold transition-all cursor-pointer ${
|
||||||
|
visited
|
||||||
|
? "bg-white/5 text-white/40 cursor-not-allowed"
|
||||||
|
: "bg-accent-purple text-white hover:bg-accent-purple/90"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{visited ? "Visited" : `Check In (+${loc.reward} LYT)`}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
113
src/components/sections/WalkToEarn.tsx
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
"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 (
|
||||||
|
<section className="py-24 bg-[#090909] relative overflow-hidden">
|
||||||
|
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 relative z-10">
|
||||||
|
|
||||||
|
<div className="glassmorphism rounded-3xl p-8 sm:p-12 border border-primary/10 shadow-[0_0_50px_rgba(255,199,44,0.05)]">
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 items-center">
|
||||||
|
|
||||||
|
{/* Left side: Information */}
|
||||||
|
<div className="space-y-6 text-left">
|
||||||
|
<div className="w-12 h-12 rounded-2xl bg-primary/10 border border-primary/20 flex items-center justify-center text-primary">
|
||||||
|
<Footprints className="w-6 h-6 animate-pulse" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<span className="text-primary font-bold text-xs uppercase tracking-wider">Section 05 • Walk-to-Earn</span>
|
||||||
|
<h3 className="text-2xl sm:text-4xl font-extrabold font-sora text-white">
|
||||||
|
Step Sync & <br />
|
||||||
|
<span className="text-glow-yellow text-primary">Points Generation</span>
|
||||||
|
</h3>
|
||||||
|
<p className="text-white/60 text-sm leading-relaxed">
|
||||||
|
Go about your day. Next.js integration syncs steps in the background from your phone's Health API. Convert them to tradeable LYTs.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Right side: Step Simulator Panel */}
|
||||||
|
<div className="bg-white/3 border border-white/5 rounded-2xl p-6 space-y-6">
|
||||||
|
<div className="flex justify-between items-end">
|
||||||
|
<div>
|
||||||
|
<span className="text-white/40 text-[10px] font-bold tracking-wider uppercase block">Simulate Distance</span>
|
||||||
|
<span className="text-2xl font-extrabold font-sora text-white">
|
||||||
|
+{simSteps.toLocaleString()} <span className="text-xs text-white/40 font-medium">Steps</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-right">
|
||||||
|
<span className="text-white/40 text-[10px] font-bold tracking-wider uppercase block">Rewards Wallet</span>
|
||||||
|
<span className="text-xl font-extrabold text-primary font-sora flex items-center justify-end gap-1">
|
||||||
|
+{walkReward} <span className="text-xs font-semibold text-white/60">LYTs</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Slider */}
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min="500"
|
||||||
|
max="10000"
|
||||||
|
step="500"
|
||||||
|
value={simSteps}
|
||||||
|
onChange={(e) => setSimSteps(parseInt(e.target.value))}
|
||||||
|
disabled={walkClaimed}
|
||||||
|
className="w-full h-1.5 bg-white/10 rounded-lg appearance-none cursor-pointer accent-primary"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="flex justify-between text-[10px] text-white/40 font-bold">
|
||||||
|
<span>500 Steps</span>
|
||||||
|
<span>5,000 Steps</span>
|
||||||
|
<span>10,000 Steps</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={handleSimulateWalk}
|
||||||
|
disabled={walkClaimed}
|
||||||
|
className={`w-full py-3.5 rounded-xl font-extrabold text-sm tracking-wider flex items-center justify-center gap-2 cursor-pointer transition-all ${
|
||||||
|
walkClaimed
|
||||||
|
? "bg-accent-lime text-dark shadow-[0_0_20px_rgba(200,255,55,0.3)]"
|
||||||
|
: "bg-white/5 hover:bg-primary hover:text-dark text-white border border-white/10 hover:border-primary"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{walkClaimed ? (
|
||||||
|
<>
|
||||||
|
<Check className="w-4 h-4" />
|
||||||
|
<span>Reward Added! +{walkReward} LYTs</span>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<RefreshCw className="w-4 h-4 animate-spin-slow" />
|
||||||
|
<span>Sync Walk Distance</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
341
src/components/story/SectionsA.tsx
Normal file
@@ -0,0 +1,341 @@
|
|||||||
|
import { Section, Pill, Card, Chip, Heading, Logo, Group, Reveal } from "./ui";
|
||||||
|
import { C } from "./palette";
|
||||||
|
|
||||||
|
/* 01 — INTRO / HERO */
|
||||||
|
export function Intro() {
|
||||||
|
return (
|
||||||
|
<Section id="intro" variant="deep">
|
||||||
|
{/* giant watermark */}
|
||||||
|
<span className="pointer-events-none absolute right-0 top-1/2 -z-0 -translate-y-1/2 select-none font-sora text-[28vw] font-extrabold leading-none text-white/5">
|
||||||
|
LYTS
|
||||||
|
</span>
|
||||||
|
<div className="relative z-10">
|
||||||
|
<Card bg="#fff" className="mb-8 w-fit px-6 py-4" rounded="rounded-2xl">
|
||||||
|
<Logo />
|
||||||
|
</Card>
|
||||||
|
<div className="mt-8 space-y-1 font-bold text-[#FFD21E] sm:text-lg">
|
||||||
|
<p>we built this for you.</p>
|
||||||
|
<p>
|
||||||
|
social currency is value earned for <em className="italic">participating</em> in life.
|
||||||
|
</p>
|
||||||
|
<p>a LYTIAN believes every meaningful interaction has value.</p>
|
||||||
|
</div>
|
||||||
|
<h1 className="mt-8 font-sora text-6xl font-extrabold leading-[0.9] tracking-tight text-white sm:text-7xl lg:text-8xl">
|
||||||
|
social
|
||||||
|
<br />
|
||||||
|
currency
|
||||||
|
<br />
|
||||||
|
for real life.
|
||||||
|
</h1>
|
||||||
|
<div className="mt-8">
|
||||||
|
<Chip bg="#0a0a0a" text="#fff">
|
||||||
|
<span className="text-[#C8FF37]">"earn. spend. live more."</span>
|
||||||
|
<span className="ml-2 font-semibold text-white/80">
|
||||||
|
a quiet belief that everyday moments matter.
|
||||||
|
</span>
|
||||||
|
</Chip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="absolute right-8 top-24 hidden h-40 w-40 rotate-6 items-center justify-center rounded-full bg-[#FFD21E] text-center font-sora text-lg font-extrabold text-[#0a0a0a] nb lg:flex">
|
||||||
|
<div>
|
||||||
|
<div className="text-xs font-bold text-[#FF2E88]">psst</div>
|
||||||
|
make everyday count.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 02 — THE WHY */
|
||||||
|
export function TheWhy() {
|
||||||
|
return (
|
||||||
|
<Section id="the-why" variant="cream">
|
||||||
|
<div className="flex flex-col gap-8">
|
||||||
|
<Pill bg="#0a0a0a" text="#FFD21E">💌 hey. quick story before we dive in</Pill>
|
||||||
|
<div className="grid gap-8 lg:grid-cols-[1.4fr_1fr]">
|
||||||
|
<div>
|
||||||
|
<Card bg="#0a0a0a" text="#fff" className="p-6">
|
||||||
|
<div className="font-sora text-6xl font-extrabold text-[#C8FF37]">30+ yrs</div>
|
||||||
|
<div className="mt-2 text-sm font-bold tracking-wide">
|
||||||
|
OF LOYALTY = SPEND · EARN POINTS · REDEEM REWARDS{" "}
|
||||||
|
<span className="text-white/60">(the same old loop)</span>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
<Heading className="mt-8">
|
||||||
|
<span className="strike-pink">loyalty began after you swiped.</span>
|
||||||
|
<br />
|
||||||
|
<span className="text-[#FF2E88]">the world moved on.</span>
|
||||||
|
</Heading>
|
||||||
|
<div className="mt-8 flex flex-wrap items-center gap-2">
|
||||||
|
<Chip bg={C.orange} text="#fff">SPEND MONEY</Chip>
|
||||||
|
<span>→</span>
|
||||||
|
<Chip bg={C.yellow}>EARN POINTS</Chip>
|
||||||
|
<span>→</span>
|
||||||
|
<Chip bg={C.lime}>REDEEM</Chip>
|
||||||
|
<span>→</span>
|
||||||
|
<Chip bg="#0a0a0a" text="#fff">⏳ POINTS EXPIRE</Chip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
<Card bg="#fff" className="-rotate-1 p-5">
|
||||||
|
<div className="font-sora font-extrabold">🎬 DISCOVER ON TIKTOK</div>
|
||||||
|
<div className="text-sm font-semibold text-black/70">brand relationships start before purchase</div>
|
||||||
|
</Card>
|
||||||
|
<Card bg={C.pink} text="#fff" className="rotate-1 p-5">
|
||||||
|
<div className="font-sora font-extrabold">👯 COMMUNITIES DECIDE</div>
|
||||||
|
<div className="text-sm font-semibold text-white/80">influence > paid advertising</div>
|
||||||
|
</Card>
|
||||||
|
<Card bg={C.lime} className="-rotate-1 p-5">
|
||||||
|
<div className="font-sora font-extrabold">✨ REWARD PARTICIPATION</div>
|
||||||
|
<div className="text-sm font-semibold text-black/70">recognition, not coupons</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 03 — THE PROBLEM */
|
||||||
|
export function TheProblem() {
|
||||||
|
const cards = [
|
||||||
|
{ n: "01", t: "starts too late.", bg: "#fff", tc: "#0a0a0a", items: ["Visits a store", "Discovers a brand", "Refers a friend", "Attends an event", "Joins a community"], foot: "= $0 value today" },
|
||||||
|
{ n: "02", t: "points = useless.", bg: C.pink, tc: "#fff", items: ["Expiring numbers", "Transactional incentives", "Isolated brand currencies", "Complex redemptions", "Zero emotional bond"], foot: "no flex · no vibe." },
|
||||||
|
{ 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 (
|
||||||
|
<Section id="the-problem" variant="night">
|
||||||
|
<Pill>⚡ the problem</Pill>
|
||||||
|
<Heading className="mt-6">
|
||||||
|
traditional loyalty is <span className="strike-pink">broken.</span>
|
||||||
|
<br />
|
||||||
|
<span className="text-[#FFD21E]">here's why.</span>
|
||||||
|
</Heading>
|
||||||
|
<Group className="mt-10 grid gap-5 md:grid-cols-3">
|
||||||
|
{cards.map((c) => (
|
||||||
|
<Card key={c.n} group bg={c.bg} text={c.tc} className="flex flex-col p-6">
|
||||||
|
<div className="mb-2 w-fit rounded-md bg-[#0a0a0a] px-2 py-0.5 text-xs font-extrabold text-white">{c.n}</div>
|
||||||
|
<div className="font-sora text-2xl font-extrabold">{c.t}</div>
|
||||||
|
<ul className="mt-3 flex-1 space-y-1.5 text-sm font-bold">
|
||||||
|
{c.items.map((i) => (
|
||||||
|
<li key={i}>✗ {i}</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
<div className="mt-4 rounded-lg bg-[#0a0a0a] py-2 text-center text-sm font-extrabold text-[#FFD21E]">{c.foot}</div>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</Group>
|
||||||
|
<div className="mt-6">
|
||||||
|
<Chip bg={C.yellow}>❗ customers create value before spending. then traditional loyalty assigns zero credit.</Chip>
|
||||||
|
</div>
|
||||||
|
</Section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 04 — THE SHIFT */
|
||||||
|
export function TheShift() {
|
||||||
|
return (
|
||||||
|
<Section id="the-shift" variant="plain" className="!px-0">
|
||||||
|
<div className="mx-auto max-w-6xl px-6 sm:px-10 lg:px-20">
|
||||||
|
<Pill bg="#0a0a0a" text="#FFD21E">⚡ the shift</Pill>
|
||||||
|
<Heading className="mt-6">
|
||||||
|
from <span className="strike-pink">loyalty</span> to <span className="text-[#FF2E88]">engagement.</span>
|
||||||
|
</Heading>
|
||||||
|
<div className="mt-10 grid gap-8 md:grid-cols-2">
|
||||||
|
<div>
|
||||||
|
<div className="font-sora text-2xl font-extrabold">📺 old world</div>
|
||||||
|
<div className="text-sm font-semibold text-white/50">rewards spending only</div>
|
||||||
|
<div className="mt-5 space-y-3">
|
||||||
|
{[["🛒 OFFLINE COMMERCE", "still rewards only purchases"], ["📺 TV ADS · BANNERS", "one-way broadcast · no loop"], ["💳 SPEND-BASED POINTS", "transaction, no participation"]].map(([t, s]) => (
|
||||||
|
<Card key={t} bg="#fff" className="p-4">
|
||||||
|
<div className="font-sora font-extrabold">{t}</div>
|
||||||
|
<div className="text-sm font-semibold text-black/60">{s}</div>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="font-sora text-2xl font-extrabold">✨ new world</div>
|
||||||
|
<div className="text-sm font-semibold text-[#C8FF37]">rewards participation</div>
|
||||||
|
<div className="mt-5 space-y-3">
|
||||||
|
{[["📱 SOCIAL MEDIA", "rewards engagement + time", C.purple], ["🎮 GAMING", "rewards achievements + grind", C.pink], ["🎬 CREATOR PLATFORMS", "rewards contribution + community", C.orange]].map(([t, s, bg]) => (
|
||||||
|
<Card key={t as string} bg={bg as string} text="#fff" className="p-4">
|
||||||
|
<div className="font-sora font-extrabold">{t}</div>
|
||||||
|
<div className="text-sm font-semibold text-white/80">{s}</div>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-8">
|
||||||
|
<Chip bg={C.yellow}>🔥 biggest miss in consumer tech right now: offline commerce still rewards only spending. that changes with LYTs.</Chip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 05 — MEET LYTs */
|
||||||
|
export function MeetLyts() {
|
||||||
|
return (
|
||||||
|
<Section id="lyts" variant="deep">
|
||||||
|
<Pill bg={C.yellow} text="#0a0a0a">✦ introducing</Pill>
|
||||||
|
<h2 className="mt-6 font-sora text-8xl font-extrabold tracking-tight text-white">LYTs.</h2>
|
||||||
|
<Card bg="#fff" className="mt-6 max-w-3xl p-6">
|
||||||
|
<p className="text-lg font-bold">
|
||||||
|
a LYT is <span className="text-[#FF2E88]">a unit of digital social currency</span> you earn by participating in real life. one LYT = one moment of showing up.
|
||||||
|
</p>
|
||||||
|
</Card>
|
||||||
|
<div className="mt-8 flex flex-wrap items-center gap-2">
|
||||||
|
<Chip>walk 🚶</Chip>
|
||||||
|
<Chip bg={C.yellow}>visit 📍</Chip>
|
||||||
|
<Chip bg={C.lime}>friend 🤝</Chip>
|
||||||
|
<Chip bg={C.orange} text="#fff">shop 🛍️</Chip>
|
||||||
|
<Chip bg={C.purple} text="#fff">event 🎉</Chip>
|
||||||
|
<span>→</span>
|
||||||
|
<Chip bg="#0a0a0a" text={C.lime}>your LYTs balance</Chip>
|
||||||
|
<span>→</span>
|
||||||
|
<Chip bg={C.orange} text="#fff">spend</Chip>
|
||||||
|
<span>→</span>
|
||||||
|
<Chip bg={C.purple} text="#fff">live more</Chip>
|
||||||
|
</div>
|
||||||
|
</Section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 06 — ONE ORDINARY DAY */
|
||||||
|
export function OneDay() {
|
||||||
|
const rows = [
|
||||||
|
["🚶", "you walked", "10,000 steps.", C.lime],
|
||||||
|
["☕", "met a friend", "for coffee.", C.yellow],
|
||||||
|
["📍", "visited", "a new place.", C.pink],
|
||||||
|
["🏪", "supported", "a local business.", C.orange],
|
||||||
|
["🌱", "showed up", "for life.", C.purple],
|
||||||
|
] as const;
|
||||||
|
return (
|
||||||
|
<Section id="one-day" variant="deep">
|
||||||
|
<Pill bg={C.lime} text="#0a0a0a">💚 one ordinary day</Pill>
|
||||||
|
<Heading className="mt-6">
|
||||||
|
here is what one ordinary day with <span className="text-[#C8FF37]">LYTs</span> looks like.
|
||||||
|
</Heading>
|
||||||
|
<div className="mt-8 max-w-3xl space-y-3">
|
||||||
|
{rows.map(([e, a, b, c], i) => (
|
||||||
|
<Reveal key={a} delay={i * 0.08} y={18}>
|
||||||
|
<div className="nb flex items-center gap-3 rounded-xl px-5 py-3 font-bold text-[#0a0a0a]" style={{ background: c }}>
|
||||||
|
<span className="text-xl">{e}</span>
|
||||||
|
<span>{a}</span>
|
||||||
|
<span className="font-extrabold">{b}</span>
|
||||||
|
</div>
|
||||||
|
</Reveal>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<Card bg="#fff" className="mt-6 w-fit p-5">
|
||||||
|
<div className="font-sora text-xl font-extrabold">you earned LYTs</div>
|
||||||
|
<div className="mt-1 w-fit bg-[#FFD21E] px-2 font-sora text-xl font-extrabold">because your life has value. 🤍</div>
|
||||||
|
</Card>
|
||||||
|
</Section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 07 — FEELINGS */
|
||||||
|
export function Feelings() {
|
||||||
|
const cards = [
|
||||||
|
["🏆", "Recognition", "be seen, be credited", C.pink, "#fff"],
|
||||||
|
["🌴", "Experiences", "real life, not receipts", C.yellow, "#0a0a0a"],
|
||||||
|
["🫂", "Belonging", "community = currency", C.lime, "#0a0a0a"],
|
||||||
|
["🪞", "Identity", "who you are matters", C.orange, "#fff"],
|
||||||
|
["💬", "Community", "tribes win", "#0a0a0a", C.lime],
|
||||||
|
] as const;
|
||||||
|
return (
|
||||||
|
<Section id="feelings" variant="cream">
|
||||||
|
<Pill bg={C.purple} text="#fff">💗 why social currency</Pill>
|
||||||
|
<Heading className="mt-6">
|
||||||
|
people don't want points. they want <span className="text-[#FF2E88]">feelings.</span>
|
||||||
|
</Heading>
|
||||||
|
<Group className="mt-10 grid gap-4 sm:grid-cols-3 lg:grid-cols-5">
|
||||||
|
{cards.map(([e, t, s, bg, tc]) => (
|
||||||
|
<Card key={t} group bg={bg} text={tc} className="p-5">
|
||||||
|
<div className="text-2xl">{e}</div>
|
||||||
|
<div className="mt-6 font-sora text-lg font-extrabold">{t}</div>
|
||||||
|
<div className="text-xs font-bold opacity-80">{s}</div>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</Group>
|
||||||
|
<Card bg={C.purple} text="#fff" className="mt-6 flex flex-col items-start gap-2 p-6 sm:flex-row sm:items-center sm:gap-6">
|
||||||
|
<div className="font-sora text-6xl font-extrabold text-[#FFD21E]">0/5</div>
|
||||||
|
<div>
|
||||||
|
<div className="font-sora font-extrabold tracking-wide">TRADITIONAL LOYALTY SCORES</div>
|
||||||
|
<div className="text-sm font-semibold text-white/85">On every emotional need above, old-school points-based loyalty = 0%. LYTs is designed to reward human participation itself.</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</Section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 08 — THE DIFFERENCE */
|
||||||
|
export function Difference() {
|
||||||
|
return (
|
||||||
|
<Section id="difference" variant="night">
|
||||||
|
<Pill>⚡ the fundamental difference</Pill>
|
||||||
|
<Heading className="mt-6 text-[#FFD21E]">
|
||||||
|
loyalty starts at spend.
|
||||||
|
<br />
|
||||||
|
LYTs starts at engage.
|
||||||
|
</Heading>
|
||||||
|
<div className="mt-10 grid items-start gap-4 md:grid-cols-2">
|
||||||
|
<div>
|
||||||
|
<Pill bg={C.yellow} text="#0a0a0a">💻 OLD WORLD</Pill>
|
||||||
|
<div className="mt-3 font-sora text-3xl font-extrabold">TRADITIONAL LOYALTY</div>
|
||||||
|
<div className="mt-4 space-y-2">
|
||||||
|
<div className="nb rounded-lg bg-white px-4 py-2 text-center font-extrabold text-[#0a0a0a]">PURCHASE ↓</div>
|
||||||
|
<div className="nb rounded-lg px-4 py-2 text-center font-extrabold text-[#0a0a0a]" style={{ background: C.yellow }}>EARN POINTS ↓</div>
|
||||||
|
<div className="nb rounded-lg px-4 py-2 text-center font-extrabold text-white" style={{ background: C.orange }}>REDEEM ↓</div>
|
||||||
|
<div className="font-extrabold text-[#C8FF37]">⏳ POINTS EXPIRE</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-4"><Chip bg={C.lime}>🔥 not a better coupon. a whole new currency. 🌱</Chip></div>
|
||||||
|
</div>
|
||||||
|
<div className="nb-lg rounded-2xl bg-gradient-to-br from-[#FF2E88] to-[#A020F0] p-5">
|
||||||
|
<Pill bg={C.yellow} text="#0a0a0a">✨ LYTS WAY</Pill>
|
||||||
|
<div className="mt-3 font-sora text-3xl font-extrabold text-white">LYTS SOCIAL CURRENCY</div>
|
||||||
|
<div className="mt-4 space-y-2">
|
||||||
|
{[["DISCOVER", C.lime, "#0a0a0a"], ["ENGAGE", C.yellow, "#0a0a0a"], ["EARN LYTS", C.orange, "#fff"], ["SPEND", C.pink, "#fff"], ["LIVE MORE", "#fff", "#0a0a0a"], ["💞 LYTIAN", "#0a0a0a", C.lime]].map(([t, bg, tc]) => (
|
||||||
|
<div key={t} className="nb rounded-lg px-4 py-2 text-center font-extrabold" style={{ background: bg, color: tc }}>{t} {t !== "💞 LYTIAN" && "↓"}</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 09 — THE LOOP */
|
||||||
|
export function TheLoop() {
|
||||||
|
const cards = [
|
||||||
|
["01", "DISCOVER", "brands, places, people worth meeting.", C.pink, "#fff"],
|
||||||
|
["02", "ENGAGE", "visit, share, take the small challenge.", C.yellow, "#0a0a0a"],
|
||||||
|
["03", "EARN LYTS", "every interaction turns into value.", C.lime, "#0a0a0a"],
|
||||||
|
["04", "SPEND LYTS", "moments, access, experiences that matter.", C.orange, "#fff"],
|
||||||
|
["05", "LIVE MORE", "explore, connect, feel alive.", C.purple, "#fff"],
|
||||||
|
["06", "BECOME A LYTIAN", "the community you always wanted. 💞", "#0a0a0a", C.lime],
|
||||||
|
] as const;
|
||||||
|
return (
|
||||||
|
<Section id="the-loop" variant="night">
|
||||||
|
<Pill bg={C.lime} text="#0a0a0a">🚀 your 6-step loop</Pill>
|
||||||
|
<Heading className="mt-6">
|
||||||
|
six small moments.
|
||||||
|
<br />
|
||||||
|
one big life that <span className="text-[#FFD21E]">keeps going.</span>
|
||||||
|
</Heading>
|
||||||
|
<Group className="mt-10 grid gap-4 sm:grid-cols-2 lg:grid-cols-6">
|
||||||
|
{cards.map(([n, t, s, bg, tc]) => (
|
||||||
|
<Card key={n} group bg={bg} text={tc} className="p-4">
|
||||||
|
<div className="mb-2 w-fit rounded bg-[#0a0a0a] px-1.5 text-xs font-extrabold text-white">{n}</div>
|
||||||
|
<div className="font-sora text-lg font-extrabold leading-tight">{t}</div>
|
||||||
|
<div className="mt-2 text-xs font-bold opacity-80">{s}</div>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</Group>
|
||||||
|
<div className="mt-6"><Chip bg={C.purple} text="#fff">♻️ every interaction creates value. every LYTIAN invites another.</Chip></div>
|
||||||
|
</Section>
|
||||||
|
);
|
||||||
|
}
|
||||||
294
src/components/story/SectionsB.tsx
Normal file
@@ -0,0 +1,294 @@
|
|||||||
|
import { Section, Pill, Card, Chip, Heading, Logo, Group, Reveal } from "./ui";
|
||||||
|
import { C } from "./palette";
|
||||||
|
|
||||||
|
/* 10 — FOR YOU */
|
||||||
|
export function ForYou() {
|
||||||
|
const cards = [
|
||||||
|
["🚶", "earn before spending", "value starts before purchase. discovery, visits, sharing.", C.pink, "#fff"],
|
||||||
|
["🌅", "everyday life = value", "walking. exploring. visiting. sharing. everything matters.", C.yellow, "#0a0a0a"],
|
||||||
|
["🌐", "one connected currency", "one LYTs balance across experiences and brands. no silos.", C.lime, "#0a0a0a"],
|
||||||
|
["📢", "real recognition", "rewarded for contribution and engagement, not just transactions.", C.purple, "#fff"],
|
||||||
|
] as const;
|
||||||
|
return (
|
||||||
|
<Section id="for-you" variant="cream">
|
||||||
|
<div className="grid gap-8 lg:grid-cols-[1.3fr_1fr] lg:items-start">
|
||||||
|
<div>
|
||||||
|
<Pill>🙋 for you</Pill>
|
||||||
|
<h2 className="mt-6 font-sora text-4xl sm:text-6xl md:text-7xl font-extrabold leading-[0.9] tracking-tight">
|
||||||
|
your life
|
||||||
|
<br />
|
||||||
|
is <span className="text-[#A020F0]">worth</span>
|
||||||
|
<br />
|
||||||
|
something.
|
||||||
|
</h2>
|
||||||
|
<p className="mt-4 font-bold">every step. every visit. every selfie. it's all worth it here. 💗</p>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<Card bg="#0a0a0a" text={C.lime} className="p-5">
|
||||||
|
<div className="font-sora text-lg sm:text-xl font-extrabold">value begins before purchase. 🌱</div>
|
||||||
|
</Card>
|
||||||
|
<Card bg={C.orange} text="#fff" className="p-5">
|
||||||
|
<div className="font-sora text-xl sm:text-2xl font-extrabold">walk in. win more.</div>
|
||||||
|
<div className="mt-1 text-sm font-bold">
|
||||||
|
10k steps? <span className="rounded bg-[#FFD21E] px-1 text-[#0a0a0a]">earn.</span> coffee with a friend?{" "}
|
||||||
|
<span className="rounded bg-[#FFD21E] px-1 text-[#0a0a0a]">earn.</span> sharing a vibe?{" "}
|
||||||
|
<span className="rounded bg-[#FFD21E] px-1 text-[#0a0a0a]">earn.</span>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Group className="mt-8 grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
||||||
|
{cards.map(([e, t, s, bg, tc]) => (
|
||||||
|
<Card key={t} group bg={bg} text={tc} className="p-5">
|
||||||
|
<div className="text-2xl">{e}</div>
|
||||||
|
<div className="mt-4 font-sora text-lg font-extrabold leading-tight">{t}</div>
|
||||||
|
<div className="mt-1 text-xs font-bold opacity-85">{s}</div>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</Group>
|
||||||
|
</Section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 11 — FOR BUSINESS */
|
||||||
|
export function ForBusiness() {
|
||||||
|
const cards = [
|
||||||
|
["1", "🤝 ACQUIRE", "CUSTOMERS EARLIER", "Engage them BEFORE the transaction.", C.pink, "#fff"],
|
||||||
|
["2", "👟 FOOTFALL", "INCREASE VISITS", "Reward the showing up.", C.lime, "#0a0a0a"],
|
||||||
|
["3", "📁 CAC", "SLASH ACQUISITION COSTS", "Organic loops replace paid ads.", C.yellow, "#0a0a0a"],
|
||||||
|
["4", "🔁 KEEP", "STICKIER CUSTOMERS", "Continuous engagement = love.", C.orange, "#fff"],
|
||||||
|
["5", "👥 TRIBE", "BUILD ADVOCATES", "Customers turn into LYTIANS.", C.purple, "#fff"],
|
||||||
|
["6", "🧠 INSIGHT", "BETTER INTENT SIGNALS", "See them before the receipt.", "#fff", "#0a0a0a"],
|
||||||
|
] as const;
|
||||||
|
return (
|
||||||
|
<Section id="for-business" variant="night">
|
||||||
|
<Pill bg={C.yellow} text="#0a0a0a">🤝 for businesses</Pill>
|
||||||
|
<Heading className="mt-6">
|
||||||
|
acquire earlier.
|
||||||
|
<br />
|
||||||
|
retain <span className="text-[#FFD21E]">forever.</span>
|
||||||
|
</Heading>
|
||||||
|
<p className="mt-4 font-bold text-[#C8FF37]">meet people before they buy. keep them warm after. 💞</p>
|
||||||
|
<Group className="mt-8 grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||||
|
{cards.map(([n, tag, t, s, bg, tc]) => (
|
||||||
|
<Card key={n} group bg={bg} text={tc} className="relative overflow-hidden p-5">
|
||||||
|
<span className="absolute right-3 top-2 font-sora text-4xl font-extrabold opacity-20">{n}</span>
|
||||||
|
<div className="w-fit rounded bg-[#0a0a0a] px-2 py-0.5 text-xs font-extrabold text-[#FFD21E]">{tag}</div>
|
||||||
|
<div className="mt-3 font-sora text-xl font-extrabold leading-tight">{t}</div>
|
||||||
|
<div className="mt-1 text-sm font-bold opacity-85">{s}</div>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</Group>
|
||||||
|
</Section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 12 — WHY NOW */
|
||||||
|
export function WhyNow() {
|
||||||
|
const cards = [
|
||||||
|
["🌴", "EXPERIENCE ECONOMY", "Experiences > possessions.", "↑↑", "#fff", "#0a0a0a"],
|
||||||
|
["👯", "COMMUNITY COMMERCE", "Tribes > ads.", "↑", C.yellow, "#0a0a0a"],
|
||||||
|
["🤲", "PARTICIPATION REWARDED", "Attention = the new spend.", "↑↑", C.lime, "#0a0a0a"],
|
||||||
|
["🎮", "GAMIFIED LIFE", "Systems make it real.", "↑", C.orange, "#fff"],
|
||||||
|
["📉", "OLD LOYALTY FADING", "No real bond anymore.", "↓↓", C.pink, "#fff"],
|
||||||
|
] as const;
|
||||||
|
return (
|
||||||
|
<Section id="why-now" variant="deep">
|
||||||
|
<Pill bg={C.lime} text="#0a0a0a">⏰ why this matters</Pill>
|
||||||
|
<Heading className="mt-6">
|
||||||
|
5 currents
|
||||||
|
<br />
|
||||||
|
are converging <span className="text-[#FFD21E]">right now.</span>
|
||||||
|
</Heading>
|
||||||
|
<Group className="mt-10 grid gap-4 sm:grid-cols-2 lg:grid-cols-5">
|
||||||
|
{cards.map(([e, t, s, a, bg, tc]) => (
|
||||||
|
<Card key={t} group bg={bg} text={tc} className="flex flex-col p-5">
|
||||||
|
<div className="text-2xl">{e}</div>
|
||||||
|
<div className="mt-4 font-sora text-lg font-extrabold leading-tight">{t}</div>
|
||||||
|
<div className="mt-1 text-xs font-bold opacity-80">{s}</div>
|
||||||
|
<div className="mt-4 text-2xl font-extrabold">{a}</div>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</Group>
|
||||||
|
</Section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 13 — HOW IT WORKS */
|
||||||
|
export function HowItWorks() {
|
||||||
|
const cards = [
|
||||||
|
["🚶", "Walk", "turn every step into social currency", C.pink, "#fff"],
|
||||||
|
["📍", "Visit", "explore local spots, earn for showing up", C.yellow, "#0a0a0a"],
|
||||||
|
["🤳", "Selfie", "turn memories into rewards", C.lime, "#0a0a0a"],
|
||||||
|
["🎡", "Spin", "surprise LYTs + experiences daily", C.orange, "#fff"],
|
||||||
|
["🎟️", "Scratch", "instant wins, hidden surprises", C.purple, "#fff"],
|
||||||
|
["🏆", "Challenge", "missions, goals, achievements", C.lime, "#0a0a0a"],
|
||||||
|
["🤝", "Friend", "invite + grow community together", C.purple, "#fff"],
|
||||||
|
["🛍️", "Shop", "purchases still matter · earn more", "#0a0a0a", C.lime],
|
||||||
|
["🎉", "Event", "show up, get rewarded for the moment", C.pink, "#fff"],
|
||||||
|
["🏷️", "Brand", "campaigns + unlock exclusive LYTs", C.orange, "#fff"],
|
||||||
|
] as const;
|
||||||
|
return (
|
||||||
|
<Section id="how-it-works" variant="cream">
|
||||||
|
<Pill bg="#0a0a0a" text={C.lime}>⚡ how it works</Pill>
|
||||||
|
<h2 className="mt-6 font-sora text-5xl font-extrabold leading-[0.95] tracking-tight sm:text-6xl">
|
||||||
|
every action earns points{" "}
|
||||||
|
<span className="nb-sm bg-[#C8FF37] px-2 text-[#0a0a0a]">LYTs.</span>
|
||||||
|
</h2>
|
||||||
|
<p className="mt-4 font-bold">
|
||||||
|
no app-opening rituals. no complicated loyalty rules.{" "}
|
||||||
|
<span className="bg-[#FFD21E] px-1">no waiting until you spend.</span> just live your life, engage the world, and watch your LYTs grow. 🌱
|
||||||
|
</p>
|
||||||
|
<Group className="mt-8 grid gap-4 grid-cols-2 sm:grid-cols-3 lg:grid-cols-5">
|
||||||
|
{cards.map(([e, t, s, bg, tc]) => (
|
||||||
|
<Card key={t} group bg={bg} text={tc} className="relative p-4">
|
||||||
|
<span className="absolute right-2 top-2 rounded bg-[#0a0a0a] px-1.5 py-0.5 text-[10px] font-extrabold text-[#FFD21E]">+LYTs</span>
|
||||||
|
<div className="text-2xl">{e}</div>
|
||||||
|
<div className="mt-4 font-sora text-lg font-extrabold">{t}</div>
|
||||||
|
<div className="text-xs font-bold opacity-80">{s}</div>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</Group>
|
||||||
|
</Section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 14 — OLD vs NEW */
|
||||||
|
export function OldVsNew() {
|
||||||
|
return (
|
||||||
|
<Section id="old-vs-new" variant="deep">
|
||||||
|
<Pill bg={C.yellow} text="#0a0a0a">💞 a new kind of loyalty</Pill>
|
||||||
|
<p className="mt-4 font-bold text-[#C8FF37]">traditional loyalty said one thing.</p>
|
||||||
|
<Heading className="mt-1">
|
||||||
|
<span className="text-[#FFD21E]">LYTs says</span> another.
|
||||||
|
</Heading>
|
||||||
|
<div className="mt-10 grid items-stretch gap-4 md:grid-cols-[1fr_auto_1fr]">
|
||||||
|
<Card bg="#0a0a0a" text="#fff" className="p-6">
|
||||||
|
<Pill bg={C.yellow} text="#0a0a0a">💻 TRADITIONAL LOYALTY</Pill>
|
||||||
|
<div className="mt-4 font-sora text-3xl font-extrabold leading-tight text-[#FFD21E]">
|
||||||
|
the old rule<br /><span className="text-white">spend first.</span><br /><span className="text-white">earn later.</span>
|
||||||
|
</div>
|
||||||
|
<div className="mt-4 text-sm font-bold text-white/70">value is created only when you buy. participation = invisible. 🫥</div>
|
||||||
|
</Card>
|
||||||
|
<div className="flex items-center justify-center">
|
||||||
|
<span className="nb flex h-16 w-16 items-center justify-center rounded-full bg-[#FFD21E] font-sora text-xl font-extrabold text-[#0a0a0a]">VS</span>
|
||||||
|
</div>
|
||||||
|
<div className="nb-lg rounded-2xl bg-gradient-to-br from-[#FF2E88] to-[#A020F0] p-6">
|
||||||
|
<Pill bg="#0a0a0a" text="#fff">✨ THE LYTS WAY</Pill>
|
||||||
|
<div className="mt-4 font-sora text-3xl font-extrabold leading-tight text-white">
|
||||||
|
the new rule<br />live first.<br />earn everywhere.
|
||||||
|
</div>
|
||||||
|
<div className="mt-4 text-sm font-bold text-white/85">value is created when you participate. everyday life is the unlock. 🎉</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-6">
|
||||||
|
<Chip bg="#0a0a0a" text="#fff">
|
||||||
|
discover · engage · <span className="text-[#C8FF37]">earn LYTs</span> · spend LYTs · live more · <span className="text-[#FFD21E]">become a LYTIAN.</span>
|
||||||
|
</Chip>
|
||||||
|
</div>
|
||||||
|
</Section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 15 — THE VISION */
|
||||||
|
export function TheVision() {
|
||||||
|
const items = [
|
||||||
|
["every discovery has value", "#fff", "#0a0a0a"],
|
||||||
|
["every experience has value", C.yellow, "#0a0a0a"],
|
||||||
|
["every interaction has value", C.lime, "#0a0a0a"],
|
||||||
|
["every community has value", C.orange, "#fff"],
|
||||||
|
["every person can live more", C.purple, "#fff"],
|
||||||
|
] as const;
|
||||||
|
return (
|
||||||
|
<Section id="the-vision" variant="deep">
|
||||||
|
<div className="grid gap-8 lg:grid-cols-[1.2fr_1fr] lg:items-center">
|
||||||
|
<div>
|
||||||
|
<Pill bg={C.yellow} text="#0a0a0a">🌈 the vision</Pill>
|
||||||
|
<h2 className="mt-6 font-sora text-4xl sm:text-6xl md:text-7xl font-extrabold leading-[0.95] tracking-tight">
|
||||||
|
world's largest <span className="text-[#A020F0]">social</span> currency ecosystem.
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-3">
|
||||||
|
{items.map(([t, bg, tc], i) => (
|
||||||
|
<Reveal key={t} delay={i * 0.09} y={16}>
|
||||||
|
<div className="nb flex items-center gap-3 rounded-xl px-4 py-3 font-extrabold" style={{ background: bg, color: tc }}>
|
||||||
|
<span className="flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-[#C8FF37] text-sm text-[#0a0a0a]">✓</span>
|
||||||
|
{t}
|
||||||
|
</div>
|
||||||
|
</Reveal>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-8">
|
||||||
|
<Chip bg="#0a0a0a" text={C.lime}>every meaningful interaction has value. 🤝</Chip>
|
||||||
|
</div>
|
||||||
|
</Section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 16 — OUR BELIEF */
|
||||||
|
export function OurBelief() {
|
||||||
|
return (
|
||||||
|
<Section id="our-belief" variant="night">
|
||||||
|
<div className="mx-auto flex flex-col items-center text-center">
|
||||||
|
<span className="nb flex h-20 w-20 items-center justify-center rounded-full bg-[#0a0a0a] font-sora text-sm font-extrabold text-[#FFD21E] ring-2 ring-[#FF2E88]">
|
||||||
|
🌱 our belief
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="mt-12 space-y-6">
|
||||||
|
<div className="flex flex-wrap items-center gap-3">
|
||||||
|
<span className="flex h-8 w-8 items-center justify-center rounded bg-[#0a0a0a] font-extrabold text-white ring-2 ring-white/20">1</span>
|
||||||
|
<h3 className="font-sora text-4xl font-extrabold leading-tight sm:text-5xl">
|
||||||
|
<span className="strike-pink">traditional loyalty</span> begins when you SPEND.
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-wrap items-center gap-3">
|
||||||
|
<span className="flex h-8 w-8 items-center justify-center rounded bg-[#C8FF37] font-extrabold text-[#0a0a0a]">2</span>
|
||||||
|
<div className="nb-lg w-full rounded-2xl bg-gradient-to-r from-[#FF2E88] to-[#A020F0] p-6">
|
||||||
|
<h3 className="font-sora text-4xl font-extrabold leading-tight text-white sm:text-5xl">
|
||||||
|
<span className="nb-sm bg-[#C8FF37] px-2 text-[#0a0a0a]">LYTs</span> begins when you ENGAGE.
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 17 — THE PROMISE */
|
||||||
|
export function ThePromise() {
|
||||||
|
const chips = [
|
||||||
|
["✦ Discover.", C.pink, "#fff"],
|
||||||
|
["🌐 Engage.", C.yellow, "#0a0a0a"],
|
||||||
|
["💰 Earn LYTs.", C.lime, "#0a0a0a"],
|
||||||
|
["🛍️ Spend LYTs.", C.orange, "#fff"],
|
||||||
|
["🌱 Live More.", C.purple, "#fff"],
|
||||||
|
["💞 LYTIAN.", "#0a0a0a", C.lime],
|
||||||
|
] as const;
|
||||||
|
return (
|
||||||
|
<Section id="the-promise" variant="cream">
|
||||||
|
<div className="flex items-start justify-between">
|
||||||
|
<Pill bg="#0a0a0a" text={C.yellow}>⚡ the promise</Pill>
|
||||||
|
<Logo />
|
||||||
|
</div>
|
||||||
|
<h2 className="mt-8 font-sora text-6xl font-extrabold leading-[0.9] tracking-tight sm:text-7xl">
|
||||||
|
the social currency
|
||||||
|
<br />
|
||||||
|
for real life.
|
||||||
|
</h2>
|
||||||
|
<div className="mt-8 flex flex-wrap gap-3">
|
||||||
|
{chips.map(([t, bg, tc]) => (
|
||||||
|
<Chip key={t} bg={bg} text={tc}>{t}</Chip>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="mt-10 flex flex-col items-start justify-between gap-6 sm:flex-row sm:items-center">
|
||||||
|
<Chip bg={C.purple} text="#fff">
|
||||||
|
👉 Earn. Spend. Live More. <span className="ml-1 text-[#FFD21E]">— the rest is just coupons.</span>
|
||||||
|
</Chip>
|
||||||
|
<span className="nb flex h-28 w-28 shrink-0 rotate-6 items-center justify-center rounded-full bg-[#C8FF37] text-center font-sora text-sm font-extrabold text-[#0a0a0a]">
|
||||||
|
join the LYTIANS 🤝
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</Section>
|
||||||
|
);
|
||||||
|
}
|
||||||
12
src/components/story/palette.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
/* Shared color palette — plain module (no "use client") so it can be
|
||||||
|
imported as real values by both Server and Client components. */
|
||||||
|
export const C = {
|
||||||
|
pink: "#FF2E88",
|
||||||
|
hotpink: "#FF1B6D",
|
||||||
|
purple: "#A020F0",
|
||||||
|
lime: "#C8FF37",
|
||||||
|
yellow: "#FFD21E",
|
||||||
|
orange: "#FF6B1A",
|
||||||
|
black: "#0a0a0a",
|
||||||
|
cream: "#FBF3DD",
|
||||||
|
};
|
||||||
228
src/components/story/ui.tsx
Normal file
@@ -0,0 +1,228 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { ReactNode } from "react";
|
||||||
|
import Image from "next/image";
|
||||||
|
import { motion, Variants } from "framer-motion";
|
||||||
|
import { C } from "./palette";
|
||||||
|
|
||||||
|
export { C };
|
||||||
|
|
||||||
|
const viewport = { once: true, amount: 0.25 } as const;
|
||||||
|
|
||||||
|
/* stagger container variants */
|
||||||
|
export const stagger: Variants = {
|
||||||
|
hidden: {},
|
||||||
|
show: { transition: { staggerChildren: 0.09, delayChildren: 0.05 } },
|
||||||
|
};
|
||||||
|
export const riseItem: Variants = {
|
||||||
|
hidden: { opacity: 0, y: 30, scale: 0.96 },
|
||||||
|
show: {
|
||||||
|
opacity: 1,
|
||||||
|
y: 0,
|
||||||
|
scale: 1,
|
||||||
|
transition: { type: "spring", stiffness: 130, damping: 16 },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/* ------- Section ------- */
|
||||||
|
type SectionProps = {
|
||||||
|
id: string;
|
||||||
|
children: ReactNode;
|
||||||
|
variant?: "cream" | "deep" | "night" | "plain";
|
||||||
|
className?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function Section({ id, children, variant = "deep", className = "" }: SectionProps) {
|
||||||
|
const bg =
|
||||||
|
variant === "cream"
|
||||||
|
? "bg-cream text-[#0a0a0a]"
|
||||||
|
: variant === "night"
|
||||||
|
? "bg-night text-white"
|
||||||
|
: variant === "plain"
|
||||||
|
? "bg-[#120a1f] text-white"
|
||||||
|
: "bg-deep text-white";
|
||||||
|
return (
|
||||||
|
<section
|
||||||
|
id={id}
|
||||||
|
className={`relative flex min-h-screen w-full flex-col justify-center overflow-hidden px-6 py-20 sm:px-10 lg:px-20 ${bg} ${className}`}
|
||||||
|
>
|
||||||
|
<div className="mx-auto w-full max-w-6xl">{children}</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------- Reveal (fade/slide up on scroll) ------- */
|
||||||
|
export function Reveal({
|
||||||
|
children,
|
||||||
|
className = "",
|
||||||
|
delay = 0,
|
||||||
|
y = 26,
|
||||||
|
}: {
|
||||||
|
children: ReactNode;
|
||||||
|
className?: string;
|
||||||
|
delay?: number;
|
||||||
|
y?: number;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
className={className}
|
||||||
|
initial={{ opacity: 0, y }}
|
||||||
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
|
viewport={viewport}
|
||||||
|
transition={{ type: "spring", stiffness: 120, damping: 18, delay }}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------- Group (stagger children that use riseItem) ------- */
|
||||||
|
export function Group({
|
||||||
|
children,
|
||||||
|
className = "",
|
||||||
|
}: {
|
||||||
|
children: ReactNode;
|
||||||
|
className?: string;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
className={className}
|
||||||
|
variants={stagger}
|
||||||
|
initial="hidden"
|
||||||
|
whileInView="show"
|
||||||
|
viewport={{ once: true, amount: 0.15 }}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------- Pill ------- */
|
||||||
|
export function Pill({
|
||||||
|
children,
|
||||||
|
bg = C.pink,
|
||||||
|
text = "#fff",
|
||||||
|
}: {
|
||||||
|
children: ReactNode;
|
||||||
|
bg?: string;
|
||||||
|
text?: string;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<motion.span
|
||||||
|
className="nb-sm inline-flex items-center gap-2 rounded-full px-4 py-1.5 text-xs font-extrabold uppercase tracking-wide sm:text-sm"
|
||||||
|
style={{ background: bg, color: text }}
|
||||||
|
initial={{ opacity: 0, scale: 0.8, y: 12 }}
|
||||||
|
whileInView={{ opacity: 1, scale: 1, y: 0 }}
|
||||||
|
viewport={viewport}
|
||||||
|
transition={{ type: "spring", stiffness: 320, damping: 15 }}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</motion.span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------- Card (scroll-reveal + neobrutalist hover) ------- */
|
||||||
|
export function Card({
|
||||||
|
children,
|
||||||
|
bg = "#fff",
|
||||||
|
text = "#0a0a0a",
|
||||||
|
className = "",
|
||||||
|
rounded = "rounded-2xl",
|
||||||
|
delay = 0,
|
||||||
|
group = false,
|
||||||
|
}: {
|
||||||
|
children: ReactNode;
|
||||||
|
bg?: string;
|
||||||
|
text?: string;
|
||||||
|
className?: string;
|
||||||
|
rounded?: string;
|
||||||
|
delay?: number;
|
||||||
|
/** set true when rendered inside a <Group> so stagger drives the entrance */
|
||||||
|
group?: boolean;
|
||||||
|
}) {
|
||||||
|
const entrance = group
|
||||||
|
? { variants: riseItem }
|
||||||
|
: {
|
||||||
|
initial: { opacity: 0, y: 30, scale: 0.96 },
|
||||||
|
whileInView: { opacity: 1, y: 0, scale: 1 },
|
||||||
|
viewport,
|
||||||
|
transition: { type: "spring" as const, stiffness: 130, damping: 16, delay },
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
className={`nb ${rounded} ${className}`}
|
||||||
|
style={{ background: bg, color: text }}
|
||||||
|
{...entrance}
|
||||||
|
whileHover={{
|
||||||
|
y: -6,
|
||||||
|
x: -3,
|
||||||
|
boxShadow: "11px 13px 0 0 #0a0a0a",
|
||||||
|
transition: { type: "spring", stiffness: 400, damping: 15 },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------- Chip ------- */
|
||||||
|
export function Chip({
|
||||||
|
children,
|
||||||
|
bg = C.lime,
|
||||||
|
text = "#0a0a0a",
|
||||||
|
delay = 0,
|
||||||
|
}: {
|
||||||
|
children: ReactNode;
|
||||||
|
bg?: string;
|
||||||
|
text?: string;
|
||||||
|
delay?: number;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<motion.span
|
||||||
|
className="nb-sm inline-flex items-center gap-1.5 rounded-xl px-3 py-2 text-sm font-extrabold sm:px-4"
|
||||||
|
style={{ background: bg, color: text }}
|
||||||
|
initial={{ opacity: 0, scale: 0.85, y: 10 }}
|
||||||
|
whileInView={{ opacity: 1, scale: 1, y: 0 }}
|
||||||
|
viewport={viewport}
|
||||||
|
transition={{ type: "spring", stiffness: 300, damping: 16, delay }}
|
||||||
|
whileHover={{ scale: 1.06, rotate: -1.5 }}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</motion.span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------- Heading (self-animating) ------- */
|
||||||
|
export function Heading({
|
||||||
|
children,
|
||||||
|
className = "",
|
||||||
|
}: {
|
||||||
|
children: ReactNode;
|
||||||
|
className?: string;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<motion.h2
|
||||||
|
className={`font-sora text-4xl font-extrabold leading-[0.95] tracking-tight sm:text-5xl lg:text-6xl ${className}`}
|
||||||
|
initial={{ opacity: 0, y: 28 }}
|
||||||
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
|
viewport={viewport}
|
||||||
|
transition={{ type: "spring", stiffness: 120, damping: 18 }}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</motion.h2>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* LytsUp wordmark */
|
||||||
|
export function Logo({ className = "" }: { className?: string }) {
|
||||||
|
return (
|
||||||
|
<Image
|
||||||
|
src="/images/lytsup_app.png"
|
||||||
|
alt="LytsUp"
|
||||||
|
width={2048}
|
||||||
|
height={554}
|
||||||
|
priority
|
||||||
|
className={`h-10 w-auto sm:h-12 ${className}`}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
160
src/lib/context.tsx
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import React, { createContext, useContext, useState, useEffect } from "react";
|
||||||
|
|
||||||
|
interface UserProfile {
|
||||||
|
name: string;
|
||||||
|
level: number;
|
||||||
|
xp: number;
|
||||||
|
xpToNextLevel: number;
|
||||||
|
streak: number;
|
||||||
|
stepsToday: number;
|
||||||
|
stepGoal: number;
|
||||||
|
lytBalance: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Voucher {
|
||||||
|
id: string;
|
||||||
|
brand: string;
|
||||||
|
logo: string;
|
||||||
|
discount: string;
|
||||||
|
lytCost: number;
|
||||||
|
code: string;
|
||||||
|
claimedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UserContextType {
|
||||||
|
profile: UserProfile;
|
||||||
|
vouchers: Voucher[];
|
||||||
|
addLyts: (amount: number) => void;
|
||||||
|
spendLyts: (amount: number) => boolean;
|
||||||
|
incrementSteps: (amount: number) => void;
|
||||||
|
claimVoucher: (brand: string, discount: string, cost: number) => { success: boolean; code?: string };
|
||||||
|
}
|
||||||
|
|
||||||
|
const UserContext = createContext<UserContextType | undefined>(undefined);
|
||||||
|
|
||||||
|
export function UserProvider({ children }: { children: React.ReactNode }) {
|
||||||
|
const [profile, setProfile] = useState<UserProfile>({
|
||||||
|
name: "Alex Stark",
|
||||||
|
level: 4,
|
||||||
|
xp: 680,
|
||||||
|
xpToNextLevel: 1000,
|
||||||
|
streak: 5,
|
||||||
|
stepsToday: 6420,
|
||||||
|
stepGoal: 10000,
|
||||||
|
lytBalance: 450,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [vouchers, setVouchers] = useState<Voucher[]>([]);
|
||||||
|
|
||||||
|
// Function to add LYTs and award XP
|
||||||
|
const addLyts = (amount: number) => {
|
||||||
|
setProfile((prev) => {
|
||||||
|
const newBalance = prev.lytBalance + amount;
|
||||||
|
const addedXp = amount * 2;
|
||||||
|
let newXp = prev.xp + addedXp;
|
||||||
|
let newLevel = prev.level;
|
||||||
|
let newXpToNext = prev.xpToNextLevel;
|
||||||
|
|
||||||
|
if (newXp >= newXpToNext) {
|
||||||
|
newLevel += 1;
|
||||||
|
newXp = newXp - newXpToNext;
|
||||||
|
newXpToNext = Math.round(newXpToNext * 1.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...prev,
|
||||||
|
lytBalance: newBalance,
|
||||||
|
xp: newXp,
|
||||||
|
level: newLevel,
|
||||||
|
xpToNextLevel: newXpToNext,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Function to spend LYTs
|
||||||
|
const spendLyts = (amount: number): boolean => {
|
||||||
|
let success = false;
|
||||||
|
setProfile((prev) => {
|
||||||
|
if (prev.lytBalance >= amount) {
|
||||||
|
success = true;
|
||||||
|
return {
|
||||||
|
...prev,
|
||||||
|
lytBalance: prev.lytBalance - amount,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return prev;
|
||||||
|
});
|
||||||
|
return success;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Function to update steps
|
||||||
|
const incrementSteps = (amount: number) => {
|
||||||
|
setProfile((prev) => {
|
||||||
|
const newSteps = Math.min(25000, prev.stepsToday + amount);
|
||||||
|
// Earn 1 LYT for every 500 steps taken
|
||||||
|
const stepDiff = newSteps - prev.stepsToday;
|
||||||
|
const lytReward = Math.floor(stepDiff / 500);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...prev,
|
||||||
|
stepsToday: newSteps,
|
||||||
|
lytBalance: prev.lytBalance + (lytReward > 0 ? lytReward : 0),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Function to claim brand vouchers
|
||||||
|
const claimVoucher = (brand: string, discount: string, cost: number) => {
|
||||||
|
if (profile.lytBalance < cost) {
|
||||||
|
return { success: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
const claimSuccess = spendLyts(cost);
|
||||||
|
if (!claimSuccess) return { success: false };
|
||||||
|
|
||||||
|
// Generate a random alphanumeric code
|
||||||
|
const generatedCode = Math.random().toString(36).substring(2, 8).toUpperCase() + "-" + cost;
|
||||||
|
|
||||||
|
const newVoucher: Voucher = {
|
||||||
|
id: Math.random().toString(36).substring(2, 9),
|
||||||
|
brand,
|
||||||
|
logo: `/images/logo-${brand.toLowerCase().replace(/\s+/g, "")}.png`,
|
||||||
|
discount,
|
||||||
|
lytCost: cost,
|
||||||
|
code: generatedCode,
|
||||||
|
claimedAt: new Date().toLocaleDateString("en-US", {
|
||||||
|
month: "short",
|
||||||
|
day: "numeric",
|
||||||
|
year: "numeric",
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
setVouchers((prev) => [newVoucher, ...prev]);
|
||||||
|
return { success: true, code: generatedCode };
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<UserContext.Provider
|
||||||
|
value={{
|
||||||
|
profile,
|
||||||
|
vouchers,
|
||||||
|
addLyts,
|
||||||
|
spendLyts,
|
||||||
|
incrementSteps,
|
||||||
|
claimVoucher,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</UserContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useUser() {
|
||||||
|
const context = useContext(UserContext);
|
||||||
|
if (context === undefined) {
|
||||||
|
throw new Error("useUser must be used within a UserProvider");
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
}
|
||||||