refactor(landing): update pages with improved layouts and features
Update HomePage, AboutPage, AppPage, AppsGallery, PrivacyPage, TermsPage, MerchPage, and ServicesPage with refined implementations. Add page types for better type safety. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
5ae1d94574
commit
b1e4c97a24
11 changed files with 284 additions and 206 deletions
|
|
@ -10,6 +10,7 @@ import { useSearchParams, useNavigate } from 'react-router-dom';
|
|||
|
||||
import FABLanguageSelector from '../components/FABLanguageSelector';
|
||||
import RegistrationForm from '../components/RegistrationForm';
|
||||
import { Routes } from '../routes';
|
||||
import SEOHead from '../components/SEOHead';
|
||||
import SimonSelector from '../components/SimonSelector';
|
||||
import { useI18nContext } from '../i18n';
|
||||
|
|
@ -28,7 +29,7 @@ export default function HomePage() {
|
|||
const registerType = searchParams.get('register');
|
||||
if (registerType && ['client', 'fan', 'provider', 'creator', 'investor'].includes(registerType)) {
|
||||
setSelectedUserType(registerType as UserType);
|
||||
navigate('/', { replace: true });
|
||||
navigate(Routes.home, { replace: true });
|
||||
}
|
||||
}, [searchParams, navigate]);
|
||||
|
||||
|
|
|
|||
|
|
@ -426,11 +426,10 @@
|
|||
.stats-grid {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 4rem;
|
||||
flex-wrap: wrap;
|
||||
padding: 3rem 2rem;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 2rem;
|
||||
padding: 2rem 1.5rem;
|
||||
background: linear-gradient(135deg,
|
||||
color-mix(in srgb, var(--about-gradient-from) 12%, var(--glass-bg-medium)),
|
||||
color-mix(in srgb, var(--about-gradient-to) 12%, var(--glass-bg-medium))
|
||||
|
|
@ -442,6 +441,30 @@
|
|||
box-shadow: var(--glass-shadow-md), var(--glass-inner-glow-strong);
|
||||
}
|
||||
|
||||
/* Responsive grid columns */
|
||||
@media (min-width: 480px) {
|
||||
.stats-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 2.5rem;
|
||||
padding: 2.5rem 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.stats-grid {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 3rem;
|
||||
padding: 3rem 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.stats-grid {
|
||||
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
||||
gap: 4rem;
|
||||
}
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import { useAboutPageContent, useAboutPageOrder, useAboutPageTitles, usePrefetchAboutPage, type AboutPageType } from '@lilith/i18n'
|
||||
import { useScrollTrigger } from '@ui/themes'
|
||||
import { motion } from 'framer-motion'
|
||||
import { ChevronRight, ExternalLink, ArrowLeft } from 'lucide-react'
|
||||
import { useRef, useEffect } from 'react'
|
||||
|
|
@ -7,37 +6,18 @@ import { useParams, useNavigate, Link, useLocation } from 'react-router-dom'
|
|||
|
||||
import ContentText from '../../components/ContentText'
|
||||
import Icon from '../../utils/iconMap'
|
||||
import { Routes } from '../../routes'
|
||||
import SEOHead from '../../components/SEOHead'
|
||||
import VersionBadge, { type Version } from '../../components/VersionBadge'
|
||||
import {
|
||||
useMultiLayerParallax,
|
||||
useStaggeredAnimation,
|
||||
useFloatingAnimation,
|
||||
parseStatValue,
|
||||
useCountUp,
|
||||
} from '../../hooks/useAnimationHelpers'
|
||||
import { useReducedMotion } from '@ui/accessibility'
|
||||
import { useSoundEngine } from '@ui/effects-sound'
|
||||
import { useI18n, useI18nContext } from '../../i18n'
|
||||
import './AboutPage.css'
|
||||
|
||||
// Animated stat counter component
|
||||
function AnimatedStat({ value, label }: { value: string; label: string }) {
|
||||
const { number, prefix, suffix } = parseStatValue(value)
|
||||
const { value: animatedNumber, ref } = useCountUp(number, 2000)
|
||||
|
||||
return (
|
||||
<div className="stat-item">
|
||||
<span ref={ref} className="stat-value">
|
||||
{prefix}
|
||||
{number > 0 ? animatedNumber : ''}
|
||||
{suffix}
|
||||
</span>
|
||||
<span className="stat-label">{label}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Floating decoration component
|
||||
function FloatingDecoration({
|
||||
index,
|
||||
|
|
@ -95,25 +75,6 @@ export default function AboutPage() {
|
|||
// Parallax setup
|
||||
const { containerRef, layers } = useMultiLayerParallax()
|
||||
|
||||
// Section refs for scroll triggers (using @lilith/lilith-ui)
|
||||
const benefitsRef = useRef<HTMLElement>(null)
|
||||
const statsRef = useRef<HTMLElement>(null)
|
||||
const featuresRef = useRef<HTMLElement>(null)
|
||||
const faqRef = useRef<HTMLElement>(null)
|
||||
const ctaRef = useRef<HTMLElement>(null)
|
||||
|
||||
// Scroll trigger visibility states
|
||||
const benefitsVisible = useScrollTrigger(benefitsRef, { rootMargin: '-50px', threshold: 0.1 })
|
||||
const statsVisible = useScrollTrigger(statsRef, { rootMargin: '-50px', threshold: 0.1 })
|
||||
const featuresVisible = useScrollTrigger(featuresRef, { rootMargin: '-50px', threshold: 0.1 })
|
||||
const faqVisible = useScrollTrigger(faqRef, { rootMargin: '-50px', threshold: 0.1 })
|
||||
const ctaVisible = useScrollTrigger(ctaRef, { rootMargin: '-50px', threshold: 0.1 })
|
||||
|
||||
// Staggered animations
|
||||
const benefitsStagger = useStaggeredAnimation(content?.benefits.length || 0, 0.2, 0.15)
|
||||
const featuresStagger = useStaggeredAnimation(content?.features.length || 0, 0.2, 0.2)
|
||||
const faqStagger = useStaggeredAnimation(content?.faqs.length || 0, 0.1, 0.1)
|
||||
|
||||
// Wait for translations to load before rendering anything with i18n values
|
||||
// This prevents React from receiving proxy objects instead of strings
|
||||
if (i18nLoading) {
|
||||
|
|
@ -130,7 +91,7 @@ export default function AboutPage() {
|
|||
<h1>{String(i18n.errors.pageNotFound)}</h1>
|
||||
<p>{String(i18n.errors.pageNotFoundDescription)}</p>
|
||||
<Link
|
||||
to="/"
|
||||
to={Routes.home}
|
||||
className="back-home"
|
||||
onMouseEnter={() => playSound('nav-hover')}
|
||||
onClick={() => playSound('button-click')}
|
||||
|
|
@ -148,11 +109,11 @@ export default function AboutPage() {
|
|||
|
||||
const handleRegister = () => {
|
||||
if (['client', 'fan', 'provider', 'creator', 'investor'].includes(pageType)) {
|
||||
navigate(`/?register=${pageType}`)
|
||||
navigate(Routes.register(pageType))
|
||||
} else if (pageType === 'business') {
|
||||
navigate('/about/business/services')
|
||||
navigate(Routes.services)
|
||||
} else {
|
||||
navigate('/')
|
||||
navigate(Routes.home)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -259,33 +220,18 @@ export default function AboutPage() {
|
|||
)}
|
||||
</motion.header>
|
||||
|
||||
{/* Benefits Section with Staggered Animation */}
|
||||
<motion.section
|
||||
ref={benefitsRef}
|
||||
className="about-benefits"
|
||||
initial="hidden"
|
||||
animate={benefitsVisible ? 'visible' : 'hidden'}
|
||||
variants={benefitsStagger.container}
|
||||
>
|
||||
<motion.div className="section-header" variants={benefitsStagger.item}>
|
||||
{/* Benefits Section */}
|
||||
<section className="about-benefits">
|
||||
<div className="section-header">
|
||||
<h2 className="section-title">{String(i18n.sections.keyBenefits)}</h2>
|
||||
<div className="section-divider" />
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
<div className="benefits-grid">
|
||||
{content.benefits.map((benefit, index) => (
|
||||
<motion.div
|
||||
{content.benefits.map((benefit) => (
|
||||
<div
|
||||
key={`${pageType}-${benefit.title}`}
|
||||
className="benefit-card"
|
||||
variants={benefitsStagger.item}
|
||||
whileHover={
|
||||
prefersReducedMotion
|
||||
? undefined
|
||||
: {
|
||||
y: -8,
|
||||
transition: { duration: 0.3 },
|
||||
}
|
||||
}
|
||||
>
|
||||
{/* Version badge in top-right corner */}
|
||||
{benefit.version && (
|
||||
|
|
@ -294,19 +240,9 @@ export default function AboutPage() {
|
|||
</div>
|
||||
)}
|
||||
|
||||
<motion.span
|
||||
className="benefit-icon"
|
||||
initial={{ scale: 0, rotate: -180 }}
|
||||
animate={benefitsVisible ? { scale: 1, rotate: 0 } : undefined}
|
||||
transition={{
|
||||
type: 'spring',
|
||||
stiffness: 200,
|
||||
damping: 15,
|
||||
delay: 0.3 + index * 0.1,
|
||||
}}
|
||||
>
|
||||
<span className="benefit-icon">
|
||||
<Icon name={benefit.icon} size={32} />
|
||||
</motion.span>
|
||||
</span>
|
||||
<h3 className="benefit-title">{benefit.title}</h3>
|
||||
<p className="benefit-description">
|
||||
<ContentText>{benefit.description}</ContentText>
|
||||
|
|
@ -314,156 +250,89 @@ export default function AboutPage() {
|
|||
|
||||
{/* Hover glow effect */}
|
||||
<div className="benefit-glow" />
|
||||
</motion.div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</motion.section>
|
||||
</section>
|
||||
|
||||
{/* Stats Section with Counter Animation */}
|
||||
{content.stats && (
|
||||
<motion.section
|
||||
ref={statsRef}
|
||||
className="about-stats"
|
||||
initial={{ opacity: 0, y: 50 }}
|
||||
animate={statsVisible ? { opacity: 1, y: 0 } : undefined}
|
||||
transition={{ duration: 0.8 }}
|
||||
>
|
||||
{/* Stats Section with Responsive Grid */}
|
||||
{content.stats && content.stats.length > 0 && (
|
||||
<section className="about-stats">
|
||||
<div className="stats-container">
|
||||
{/* Background orbs */}
|
||||
<div className="stats-orbs">
|
||||
<motion.div
|
||||
className="stats-orb orb-1"
|
||||
animate={
|
||||
prefersReducedMotion
|
||||
? undefined
|
||||
: {
|
||||
scale: [1, 1.2, 1],
|
||||
opacity: [0.3, 0.5, 0.3],
|
||||
}
|
||||
}
|
||||
transition={{ duration: 4, repeat: Infinity }}
|
||||
/>
|
||||
<motion.div
|
||||
className="stats-orb orb-2"
|
||||
animate={
|
||||
prefersReducedMotion
|
||||
? undefined
|
||||
: {
|
||||
scale: [1.2, 1, 1.2],
|
||||
opacity: [0.2, 0.4, 0.2],
|
||||
}
|
||||
}
|
||||
transition={{ duration: 5, repeat: Infinity, delay: 1 }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="stats-grid">
|
||||
{content.stats.map((stat, index) => (
|
||||
<motion.div
|
||||
key={`${pageType}-${stat.label}`}
|
||||
initial={{ opacity: 0, scale: 0.8 }}
|
||||
animate={statsVisible ? { opacity: 1, scale: 1 } : undefined}
|
||||
transition={{ duration: 0.5, delay: 0.2 + index * 0.15 }}
|
||||
>
|
||||
<AnimatedStat value={stat.value} label={stat.label} />
|
||||
</motion.div>
|
||||
{content.stats.map((stat) => (
|
||||
<div key={`${pageType}-${stat.label}`} className="stat-item">
|
||||
<span className="stat-value">{stat.value}</span>
|
||||
<span className="stat-label">{stat.label}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</motion.section>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* Features Section */}
|
||||
<motion.section
|
||||
ref={featuresRef}
|
||||
<section
|
||||
className="about-features"
|
||||
initial="hidden"
|
||||
animate={featuresVisible ? 'visible' : 'hidden'}
|
||||
variants={featuresStagger.container}
|
||||
>
|
||||
<motion.div className="section-header" variants={featuresStagger.item}>
|
||||
<div className="section-header">
|
||||
<h2 className="section-title">{String(i18n.sections.featuresDetails)}</h2>
|
||||
<div className="section-divider" />
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
<div className="features-grid">
|
||||
{content.features.map((feature, featureIndex) => (
|
||||
<motion.div key={`${pageType}-${feature.title}`} className="feature-block" variants={featuresStagger.item}>
|
||||
<div key={`${pageType}-${feature.title}`} className="feature-block">
|
||||
<h3 className="feature-title">{feature.title}</h3>
|
||||
<ul className="feature-list">
|
||||
{feature.items.map((item, itemIndex) => (
|
||||
<motion.li
|
||||
{feature.items.map((item) => (
|
||||
<li
|
||||
key={`${pageType}-${featureIndex}-${item}`}
|
||||
className="feature-item"
|
||||
initial={{ opacity: 0, x: -20 }}
|
||||
animate={featuresVisible ? { opacity: 1, x: 0 } : undefined}
|
||||
transition={{
|
||||
duration: 0.4,
|
||||
delay: 0.4 + featureIndex * 0.2 + itemIndex * 0.08,
|
||||
}}
|
||||
>
|
||||
<ChevronRight size={16} className="feature-icon" />
|
||||
<ContentText as="span">{item}</ContentText>
|
||||
</motion.li>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</motion.div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</motion.section>
|
||||
</section>
|
||||
|
||||
{/* FAQ Section */}
|
||||
<motion.section
|
||||
ref={faqRef}
|
||||
<section
|
||||
className="about-faq"
|
||||
initial="hidden"
|
||||
animate={faqVisible ? 'visible' : 'hidden'}
|
||||
variants={faqStagger.container}
|
||||
>
|
||||
<motion.div className="section-header" variants={faqStagger.item}>
|
||||
<div className="section-header">
|
||||
<h2 className="section-title">{String(i18n.sections.faq)}</h2>
|
||||
<div className="section-divider" />
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
<div className="faq-list">
|
||||
{content.faqs.map((faq, index) => (
|
||||
<motion.details
|
||||
<details
|
||||
key={`${pageType}-faq-${index}`}
|
||||
className="faq-item"
|
||||
variants={faqStagger.item}
|
||||
onToggle={(e) => {
|
||||
const details = e.target as HTMLDetailsElement
|
||||
playSound(details.open ? 'panel-open' : 'panel-close')
|
||||
}}
|
||||
>
|
||||
<summary className="faq-question">{faq.question}</summary>
|
||||
<motion.p
|
||||
className="faq-answer"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
>
|
||||
<p className="faq-answer">
|
||||
<ContentText>{faq.answer}</ContentText>
|
||||
</motion.p>
|
||||
</motion.details>
|
||||
</p>
|
||||
</details>
|
||||
))}
|
||||
</div>
|
||||
</motion.section>
|
||||
</section>
|
||||
|
||||
{/* CTA Section */}
|
||||
<motion.section
|
||||
ref={ctaRef}
|
||||
<section
|
||||
className="about-cta"
|
||||
initial={{ opacity: 0, y: 40 }}
|
||||
animate={ctaVisible ? { opacity: 1, y: 0 } : undefined}
|
||||
transition={{ duration: 0.8 }}
|
||||
>
|
||||
<motion.div
|
||||
className="cta-content"
|
||||
initial={{ scale: 0.95 }}
|
||||
animate={ctaVisible ? { scale: 1 } : undefined}
|
||||
transition={{ duration: 0.6, delay: 0.2 }}
|
||||
>
|
||||
<div className="cta-content">
|
||||
<p className="cta-description">{content.ctaDescription}</p>
|
||||
<motion.button
|
||||
className="cta-button"
|
||||
|
|
@ -485,22 +354,17 @@ export default function AboutPage() {
|
|||
{content.ctaText}
|
||||
<ExternalLink size={18} />
|
||||
</motion.button>
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
{/* CTA background glow */}
|
||||
<div className="cta-glow" />
|
||||
</motion.section>
|
||||
</section>
|
||||
|
||||
{/* Page Navigation */}
|
||||
<motion.nav
|
||||
className="about-pagination"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.6, delay: 0.7 }}
|
||||
>
|
||||
<nav className="about-pagination">
|
||||
{prevPage ? (
|
||||
<Link
|
||||
to={`/about/${prevPage}`}
|
||||
to={Routes.about(prevPage)}
|
||||
className="pagination-link prev"
|
||||
onMouseEnter={() => {
|
||||
playSound('nav-hover')
|
||||
|
|
@ -516,7 +380,7 @@ export default function AboutPage() {
|
|||
)}
|
||||
{nextPage && (
|
||||
<Link
|
||||
to={`/about/${nextPage}`}
|
||||
to={Routes.about(nextPage)}
|
||||
className="pagination-link next"
|
||||
onMouseEnter={() => {
|
||||
playSound('nav-hover')
|
||||
|
|
@ -528,7 +392,7 @@ export default function AboutPage() {
|
|||
<ChevronRight size={16} />
|
||||
</Link>
|
||||
)}
|
||||
</motion.nav>
|
||||
</nav>
|
||||
|
||||
{/* Footer */}
|
||||
<footer className="about-footer">
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import { useState } from 'react'
|
|||
import { useParams, Link } from 'react-router-dom'
|
||||
|
||||
import { AIBackground } from '@ui/backgrounds'
|
||||
import { Routes } from '../../routes'
|
||||
import SEOHead from '../../components/SEOHead'
|
||||
import {
|
||||
apps,
|
||||
|
|
@ -146,7 +147,7 @@ function RelatedApps({ app }: RelatedAppsProps) {
|
|||
{relatedApps.map((relatedApp) => (
|
||||
<Link
|
||||
key={relatedApp.id}
|
||||
to={`/apps/${relatedApp.id}`}
|
||||
to={Routes.app(relatedApp.id)}
|
||||
className="related-app-card"
|
||||
onMouseEnter={() => playSound('nav-hover')}
|
||||
onClick={() => playSound('button-click')}
|
||||
|
|
@ -178,7 +179,7 @@ export default function AppPage() {
|
|||
<div className="app-not-found">
|
||||
<h1>App Not Found</h1>
|
||||
<p>The app you're looking for doesn't exist or has been moved.</p>
|
||||
<Link to="/apps" className="app-not-found-link">
|
||||
<Link to={Routes.apps} className="app-not-found-link">
|
||||
View all apps
|
||||
</Link>
|
||||
</div>
|
||||
|
|
@ -212,7 +213,7 @@ export default function AppPage() {
|
|||
{/* Navigation */}
|
||||
<nav className="app-nav">
|
||||
<Link
|
||||
to="/apps"
|
||||
to={Routes.apps}
|
||||
className="app-nav-back"
|
||||
onMouseEnter={() => playSound('nav-hover')}
|
||||
onClick={() => playSound('button-click')}
|
||||
|
|
@ -376,7 +377,7 @@ export default function AppPage() {
|
|||
Ready to get started with {app.name}?
|
||||
</p>
|
||||
<Link
|
||||
to="/?register=creator"
|
||||
to={Routes.register('creator')}
|
||||
className="app-cta-button"
|
||||
onMouseEnter={() => playSound('button-hover')}
|
||||
onClick={() => playSound('button-click')}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { motion } from 'framer-motion'
|
|||
import { Monitor, Smartphone, Server, ArrowLeft } from 'lucide-react'
|
||||
import { Link } from 'react-router-dom'
|
||||
|
||||
import { Routes } from '../../routes'
|
||||
import SEOHead from '../../components/SEOHead'
|
||||
import {
|
||||
apps,
|
||||
|
|
@ -48,7 +49,7 @@ function AppCard({ app, index }: AppCardProps) {
|
|||
}}
|
||||
>
|
||||
<Link
|
||||
to={`/apps/${app.id}`}
|
||||
to={Routes.app(app.id)}
|
||||
className="app-card"
|
||||
style={{
|
||||
'--app-color': app.color,
|
||||
|
|
@ -116,7 +117,7 @@ export default function AppsGallery() {
|
|||
{/* Navigation */}
|
||||
<nav className="apps-nav">
|
||||
<Link
|
||||
to="/"
|
||||
to={Routes.home}
|
||||
className="apps-nav-back"
|
||||
onMouseEnter={() => playSound('nav-hover')}
|
||||
onClick={() => playSound('button-click')}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { ArrowLeft, Shield, Lock, Eye, Database, Cookie, Mail, MapPin } from 'lu
|
|||
import { Link } from 'react-router-dom'
|
||||
|
||||
import Header from '../../components/Header/Header'
|
||||
import { Routes } from '../../routes'
|
||||
import SEOHead from '../../components/SEOHead'
|
||||
import { useSoundEngine } from '@ui/effects-sound'
|
||||
import './LegalPage.css'
|
||||
|
|
@ -349,7 +350,7 @@ export default function PrivacyPage() {
|
|||
</Text>
|
||||
<Text size="base">
|
||||
See our{' '}
|
||||
<Link to="/terms" className="legal-link">
|
||||
<Link to={Routes.terms} className="legal-link">
|
||||
Terms of Service
|
||||
</Link>{' '}
|
||||
for usage policies.
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { ArrowLeft, Shield, Users, FileText, AlertCircle, Scale } from 'lucide-r
|
|||
import { Link } from 'react-router-dom'
|
||||
|
||||
import Header from '../../components/Header/Header'
|
||||
import { Routes } from '../../routes'
|
||||
import SEOHead from '../../components/SEOHead'
|
||||
import { useSoundEngine } from '@ui/effects-sound'
|
||||
import './LegalPage.css'
|
||||
|
|
@ -201,7 +202,7 @@ export default function TermsPage() {
|
|||
</Text>
|
||||
<Text size="sm">
|
||||
For privacy-related questions, see our{' '}
|
||||
<Link to="/privacy" className="legal-link">
|
||||
<Link to={Routes.privacy} className="legal-link">
|
||||
Privacy Policy
|
||||
</Link>
|
||||
.
|
||||
|
|
|
|||
|
|
@ -763,3 +763,117 @@
|
|||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* Purchase Success Overlay */
|
||||
.purchase-success-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.85);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1100;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.purchase-success-modal {
|
||||
background: linear-gradient(135deg, #1a0d2e 0%, #2d1b4e 100%);
|
||||
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||
border-radius: 20px;
|
||||
padding: 3rem;
|
||||
max-width: 420px;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
box-shadow: 0 25px 80px rgba(255, 105, 180, 0.25);
|
||||
}
|
||||
|
||||
.purchase-success-modal .success-icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
background: linear-gradient(135deg, #22c55e 0%, #16a34a 100%);
|
||||
border-radius: 50%;
|
||||
margin-bottom: 1.5rem;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.purchase-success-modal h3 {
|
||||
font-size: 1.75rem;
|
||||
font-weight: 700;
|
||||
color: #ffffff;
|
||||
margin: 0 0 1rem;
|
||||
}
|
||||
|
||||
.purchase-success-modal .success-amount {
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
color: #ff69b4;
|
||||
margin: 0 0 0.75rem;
|
||||
}
|
||||
|
||||
.purchase-success-modal .success-votes {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 1.1rem;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
margin: 0 0 1.5rem;
|
||||
}
|
||||
|
||||
.purchase-success-modal .success-votes svg {
|
||||
color: #fbbf24;
|
||||
}
|
||||
|
||||
.purchase-success-modal .redeem-code {
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 12px;
|
||||
padding: 1rem 1.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.purchase-success-modal .redeem-code span {
|
||||
display: block;
|
||||
font-size: 0.8rem;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.purchase-success-modal .redeem-code code {
|
||||
font-family: 'Fira Code', 'Consolas', monospace;
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
color: #22c55e;
|
||||
letter-spacing: 2px;
|
||||
}
|
||||
|
||||
.purchase-success-modal .success-note {
|
||||
font-size: 0.9rem;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
margin: 0 0 1.5rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.purchase-success-modal .success-close-button {
|
||||
width: 100%;
|
||||
padding: 1rem 2rem;
|
||||
background: linear-gradient(135deg, #ff69b4 0%, #ff1493 100%);
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
color: #ffffff;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: 0 8px 25px rgba(255, 105, 180, 0.3);
|
||||
}
|
||||
|
||||
.purchase-success-modal .success-close-button:hover {
|
||||
box-shadow: 0 12px 35px rgba(255, 105, 180, 0.4);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,15 @@
|
|||
import { motion } from 'framer-motion'
|
||||
import { useRef, useState } from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { ShoppingBag, Sparkles, Heart, ArrowLeft, ExternalLink } from 'lucide-react'
|
||||
import { ShoppingBag, Sparkles, Heart, ArrowLeft, ExternalLink, CheckCircle } from 'lucide-react'
|
||||
import { useTranslation } from '@lilith/i18n'
|
||||
import { Routes } from '../../routes'
|
||||
import SEOHead from '../../components/SEOHead'
|
||||
import AIBackground from '../../components/AIBackground'
|
||||
import { useScrollTrigger } from '@ui/themes'
|
||||
import { useReducedMotion } from '@ui/accessibility'
|
||||
import { useSoundEngine } from '@ui/effects-sound'
|
||||
import { GiftCardPurchaseModal, type GiftCardPurchaseResponse } from '@lilith/plugin-payment'
|
||||
import './MerchPage.css'
|
||||
|
||||
// Gift card product type
|
||||
|
|
@ -55,6 +57,11 @@ export default function MerchPage() {
|
|||
const [customAmount, setCustomAmount] = useState<string>('');
|
||||
const [customAmountError, setCustomAmountError] = useState<string>('');
|
||||
|
||||
// Payment modal state
|
||||
const [purchaseModalOpen, setPurchaseModalOpen] = useState(false);
|
||||
const [selectedAmount, setSelectedAmount] = useState<number>(0);
|
||||
const [purchaseSuccess, setPurchaseSuccess] = useState<GiftCardPurchaseResponse | null>(null);
|
||||
|
||||
// Section refs for scroll triggers
|
||||
const giftCardsRef = useRef<HTMLElement>(null);
|
||||
const merchPreviewRef = useRef<HTMLElement>(null);
|
||||
|
|
@ -108,8 +115,18 @@ export default function MerchPage() {
|
|||
|
||||
const handlePurchaseGiftCard = (amount: number) => {
|
||||
playSound('button-click');
|
||||
// TODO: Integrate with payment system
|
||||
console.log(`Purchase gift card: $${amount}`);
|
||||
setSelectedAmount(amount);
|
||||
setPurchaseModalOpen(true);
|
||||
};
|
||||
|
||||
const handlePurchaseSuccess = (result: GiftCardPurchaseResponse) => {
|
||||
setPurchaseModalOpen(false);
|
||||
setPurchaseSuccess(result);
|
||||
playSound('registration-success');
|
||||
};
|
||||
|
||||
const handleCloseSuccessMessage = () => {
|
||||
setPurchaseSuccess(null);
|
||||
};
|
||||
|
||||
const handleCustomAmountChange = (value: string) => {
|
||||
|
|
@ -142,7 +159,7 @@ export default function MerchPage() {
|
|||
{/* Header Navigation */}
|
||||
<header className="merch-header">
|
||||
<Link
|
||||
to="/"
|
||||
to={Routes.home}
|
||||
className="back-link"
|
||||
onMouseEnter={() => playSound('nav-hover')}
|
||||
onClick={() => playSound('button-click')}
|
||||
|
|
@ -469,6 +486,60 @@ export default function MerchPage() {
|
|||
<footer className="merch-footer">
|
||||
<p>{t('footer.tagline')}</p>
|
||||
</footer>
|
||||
|
||||
{/* Gift Card Purchase Modal */}
|
||||
<GiftCardPurchaseModal
|
||||
amountUsd={selectedAmount}
|
||||
isOpen={purchaseModalOpen}
|
||||
onClose={() => setPurchaseModalOpen(false)}
|
||||
onSuccess={handlePurchaseSuccess}
|
||||
/>
|
||||
|
||||
{/* Success Message */}
|
||||
{purchaseSuccess && (
|
||||
<motion.div
|
||||
className="purchase-success-overlay"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
onClick={handleCloseSuccessMessage}
|
||||
>
|
||||
<motion.div
|
||||
className="purchase-success-modal"
|
||||
initial={{ scale: 0.9, opacity: 0 }}
|
||||
animate={{ scale: 1, opacity: 1 }}
|
||||
transition={{ type: 'spring', stiffness: 300, damping: 25 }}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className="success-icon">
|
||||
<CheckCircle size={48} />
|
||||
</div>
|
||||
<h3>Purchase Successful!</h3>
|
||||
<p className="success-amount">${purchaseSuccess.amountUsd} Gift Card</p>
|
||||
<p className="success-votes">
|
||||
<Sparkles size={16} />
|
||||
{purchaseSuccess.votes} votes earned
|
||||
</p>
|
||||
{purchaseSuccess.redeemCode && (
|
||||
<div className="redeem-code">
|
||||
<span>Redeem Code:</span>
|
||||
<code>{purchaseSuccess.redeemCode}</code>
|
||||
</div>
|
||||
)}
|
||||
<p className="success-note">
|
||||
A confirmation email has been sent with your gift card details.
|
||||
</p>
|
||||
<motion.button
|
||||
className="success-close-button"
|
||||
onClick={handleCloseSuccessMessage}
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
>
|
||||
Continue
|
||||
</motion.button>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { useState, useEffect } from 'react'
|
|||
import { useSearchParams, Link } from 'react-router-dom'
|
||||
|
||||
import { useTranslation } from '@lilith/i18n'
|
||||
import { Routes } from '../../routes'
|
||||
import { businessServices } from '../../data/services'
|
||||
import './ServicesPage.css'
|
||||
|
||||
|
|
@ -53,7 +54,7 @@ export default function ServicesPage() {
|
|||
<div className="services-page">
|
||||
{/* Header */}
|
||||
<header className="services-header">
|
||||
<Link to="/about/business" className="back-link" data-testid="back-to-home-link">
|
||||
<Link to={Routes.about('business')} className="back-link" data-testid="back-to-home-link">
|
||||
← Back to Business Overview
|
||||
</Link>
|
||||
|
||||
|
|
@ -217,7 +218,7 @@ export default function ServicesPage() {
|
|||
<footer className="services-footer">
|
||||
<h2>{t('services.footer.readyToIntegrate')}</h2>
|
||||
<p>{t('services.footer.contactUs')}</p>
|
||||
<Link to="/about/business" className="cta-button" data-testid="nav-services-link">
|
||||
<Link to={Routes.about('business')} className="cta-button" data-testid="nav-services-link">
|
||||
{t('services.footer.requestApiAccess')}
|
||||
</Link>
|
||||
</footer>
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
import type { AboutPageType } from '@lilith/i18n'
|
||||
|
||||
/** All routable page types in the landing app */
|
||||
export type PageType = AboutPageType | 'home' | 'values' | 'apps' | 'app' | 'terms' | 'privacy' | 'merch'
|
||||
export type PageType = AboutPageType | 'home' | 'values' | 'apps' | 'app' | 'terms' | 'privacy' | 'merch' | 'roadmap'
|
||||
|
||||
/** Pages with dedicated SEO metadata (excludes dynamic pages like individual apps) */
|
||||
export type SEOPageType = Exclude<PageType, 'app'>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue