refactor(landing): update core layout and UI components
Update Header, Layout, AboutHeader, LegalFooter, RegistrationForm, SEOHead, SimonSelector, and UserTypePanel components. Improve styling and component structure. 🤖 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
387475028e
commit
4b8619fb8e
10 changed files with 155 additions and 222 deletions
|
|
@ -10,6 +10,7 @@ import { Link, useNavigate } from 'react-router-dom';
|
|||
import { ArrowLeft, ExternalLink } from 'lucide-react';
|
||||
import { useTranslation, useAboutPageContent, useAboutPageOrder, useAboutPageTitles, usePrefetchAboutPage, type AboutPageType } from '@lilith/i18n';
|
||||
import { useSoundEngine } from '../hooks/useSoundEngine';
|
||||
import { Routes } from '../routes';
|
||||
import './AboutHeader.css';
|
||||
|
||||
interface AboutHeaderProps {
|
||||
|
|
@ -32,9 +33,9 @@ export default function AboutHeader({ pageType }: AboutHeaderProps) {
|
|||
const handleRegister = () => {
|
||||
playSound('button-click');
|
||||
if (CTA_USER_TYPES.includes(pageType as (typeof CTA_USER_TYPES)[number])) {
|
||||
navigate(`/?register=${pageType}`);
|
||||
navigate(Routes.register(pageType));
|
||||
} else {
|
||||
navigate('/');
|
||||
navigate(Routes.home);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -45,7 +46,7 @@ export default function AboutHeader({ pageType }: AboutHeaderProps) {
|
|||
{/* Navigation Bar */}
|
||||
<nav className="about-header__nav">
|
||||
<Link
|
||||
to="/"
|
||||
to={Routes.home}
|
||||
className="about-header__back"
|
||||
onMouseEnter={() => playSound('nav-hover')}
|
||||
onClick={() => playSound('button-click')}
|
||||
|
|
@ -58,7 +59,7 @@ export default function AboutHeader({ pageType }: AboutHeaderProps) {
|
|||
{aboutPageOrder.map((page) => (
|
||||
<Link
|
||||
key={page}
|
||||
to={`/about/${page}`}
|
||||
to={Routes.about(page)}
|
||||
className={`about-header__nav-link ${page === pageType ? 'about-header__nav-link--active' : ''}`}
|
||||
onMouseEnter={() => {
|
||||
playSound('nav-hover');
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import { useNavigate } from 'react-router-dom'
|
|||
|
||||
import { useSoundEngine } from '@ui/effects-sound'
|
||||
import { useI18n } from '../../i18n'
|
||||
import { Routes } from '../../routes'
|
||||
import './Header.css'
|
||||
|
||||
interface HeaderProps {
|
||||
|
|
@ -40,10 +41,16 @@ const FALLBACK_NAV = {
|
|||
platform: 'Platform',
|
||||
apps: 'Apps',
|
||||
values: 'Values',
|
||||
roadmap: 'Roadmap',
|
||||
company: 'Company',
|
||||
investors: 'Investors',
|
||||
terms: 'Terms',
|
||||
privacy: 'Privacy',
|
||||
// Shop navigation
|
||||
shop: 'Shop',
|
||||
giftCards: 'Gift Cards',
|
||||
apparel: 'Apparel',
|
||||
merchIdeas: 'Submit Ideas',
|
||||
}
|
||||
|
||||
// Helper to safely get translation with fallback
|
||||
|
|
@ -69,9 +76,9 @@ export default function Header({ pageType }: HeaderProps) {
|
|||
const handleRegister = () => {
|
||||
playSound('button-click')
|
||||
if (pageType && CTA_USER_TYPES.includes(pageType as (typeof CTA_USER_TYPES)[number])) {
|
||||
navigate(`/?register=${pageType}`)
|
||||
navigate(Routes.register(pageType))
|
||||
} else {
|
||||
navigate('/')
|
||||
navigate(Routes.home)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -81,8 +88,8 @@ export default function Header({ pageType }: HeaderProps) {
|
|||
const navigationItems: NavigationItem[] = [
|
||||
{
|
||||
label: getNav(i18n, 'home'),
|
||||
href: '/',
|
||||
onClick: () => handleNavClick('/'),
|
||||
href: Routes.home,
|
||||
onClick: () => handleNavClick(Routes.home),
|
||||
},
|
||||
{
|
||||
label: getNav(i18n, 'forWorkers'),
|
||||
|
|
@ -93,8 +100,8 @@ export default function Header({ pageType }: HeaderProps) {
|
|||
{getNav(i18n, 'providers')} <Badge variant="primary" size="sm">v1</Badge>
|
||||
</>
|
||||
),
|
||||
href: '/about/provider',
|
||||
onClick: () => handleNavClick('/about/provider', 'provider'),
|
||||
href: Routes.about('provider'),
|
||||
onClick: () => handleNavClick(Routes.about('provider'), 'provider'),
|
||||
},
|
||||
{
|
||||
label: (
|
||||
|
|
@ -102,8 +109,8 @@ export default function Header({ pageType }: HeaderProps) {
|
|||
{getNav(i18n, 'performers')} <Badge variant="primary" size="sm">v3</Badge>
|
||||
</>
|
||||
),
|
||||
href: '/about/performer',
|
||||
onClick: () => handleNavClick('/about/performer', 'performer'),
|
||||
href: Routes.about('performer'),
|
||||
onClick: () => handleNavClick(Routes.about('performer'), 'performer'),
|
||||
},
|
||||
{
|
||||
label: (
|
||||
|
|
@ -111,8 +118,8 @@ export default function Header({ pageType }: HeaderProps) {
|
|||
{getNav(i18n, 'fangirls')} <Badge variant="primary" size="sm">v7</Badge>
|
||||
</>
|
||||
),
|
||||
href: '/about/fangirl',
|
||||
onClick: () => handleNavClick('/about/fangirl', 'fangirl'),
|
||||
href: Routes.about('fangirl'),
|
||||
onClick: () => handleNavClick(Routes.about('fangirl'), 'fangirl'),
|
||||
},
|
||||
{
|
||||
label: (
|
||||
|
|
@ -120,8 +127,8 @@ export default function Header({ pageType }: HeaderProps) {
|
|||
{getNav(i18n, 'camgirls')} <Badge variant="primary" size="sm">v11</Badge>
|
||||
</>
|
||||
),
|
||||
href: '/about/camgirl',
|
||||
onClick: () => handleNavClick('/about/camgirl', 'camgirl'),
|
||||
href: Routes.about('camgirl'),
|
||||
onClick: () => handleNavClick(Routes.about('camgirl'), 'camgirl'),
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
@ -130,13 +137,13 @@ export default function Header({ pageType }: HeaderProps) {
|
|||
children: [
|
||||
{
|
||||
label: getNav(i18n, 'clients'),
|
||||
href: '/about/client',
|
||||
onClick: () => handleNavClick('/about/client', 'client'),
|
||||
href: Routes.about('client'),
|
||||
onClick: () => handleNavClick(Routes.about('client'), 'client'),
|
||||
},
|
||||
{
|
||||
label: getNav(i18n, 'fans'),
|
||||
href: '/about/fan',
|
||||
onClick: () => handleNavClick('/about/fan', 'fan'),
|
||||
href: Routes.about('fan'),
|
||||
onClick: () => handleNavClick(Routes.about('fan'), 'fan'),
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
@ -145,13 +152,18 @@ export default function Header({ pageType }: HeaderProps) {
|
|||
children: [
|
||||
{
|
||||
label: getNav(i18n, 'apps'),
|
||||
href: '/apps',
|
||||
onClick: () => handleNavClick('/apps'),
|
||||
href: Routes.apps,
|
||||
onClick: () => handleNavClick(Routes.apps),
|
||||
},
|
||||
{
|
||||
label: getNav(i18n, 'roadmap'),
|
||||
href: Routes.roadmap,
|
||||
onClick: () => handleNavClick(Routes.roadmap),
|
||||
},
|
||||
{
|
||||
label: getNav(i18n, 'values'),
|
||||
href: '/values',
|
||||
onClick: () => handleNavClick('/values'),
|
||||
href: Routes.values,
|
||||
onClick: () => handleNavClick(Routes.values),
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
@ -160,18 +172,38 @@ export default function Header({ pageType }: HeaderProps) {
|
|||
children: [
|
||||
{
|
||||
label: getNav(i18n, 'investors'),
|
||||
href: '/about/investor',
|
||||
onClick: () => handleNavClick('/about/investor', 'investor'),
|
||||
href: Routes.about('investor'),
|
||||
onClick: () => handleNavClick(Routes.about('investor'), 'investor'),
|
||||
},
|
||||
{
|
||||
label: getNav(i18n, 'terms'),
|
||||
href: '/terms',
|
||||
onClick: () => handleNavClick('/terms'),
|
||||
href: Routes.terms,
|
||||
onClick: () => handleNavClick(Routes.terms),
|
||||
},
|
||||
{
|
||||
label: getNav(i18n, 'privacy'),
|
||||
href: '/privacy',
|
||||
onClick: () => handleNavClick('/privacy'),
|
||||
href: Routes.privacy,
|
||||
onClick: () => handleNavClick(Routes.privacy),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: getNav(i18n, 'shop'),
|
||||
children: [
|
||||
{
|
||||
label: getNav(i18n, 'giftCards'),
|
||||
href: Routes.shopGiftCards,
|
||||
onClick: () => handleNavClick(Routes.shopGiftCards),
|
||||
},
|
||||
{
|
||||
label: getNav(i18n, 'apparel'),
|
||||
href: Routes.shopApparel,
|
||||
onClick: () => handleNavClick(Routes.shopApparel),
|
||||
},
|
||||
{
|
||||
label: getNav(i18n, 'merchIdeas'),
|
||||
href: Routes.shopIdeas,
|
||||
onClick: () => handleNavClick(Routes.shopIdeas),
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@
|
|||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
/* Content flows from top - individual pages handle vertical alignment */
|
||||
/* Bottom padding for fixed footer */
|
||||
padding-bottom: var(--footer-height);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import type { AboutPageType } from '@lilith/i18n'
|
|||
import Header from '../Header'
|
||||
import FloatingSettings from '../FloatingSettings'
|
||||
import LegalFooter from '../LegalFooter'
|
||||
import CartDrawer from '../CartDrawer'
|
||||
|
||||
import './Layout.css'
|
||||
|
||||
|
|
@ -58,6 +59,9 @@ export default function Layout() {
|
|||
|
||||
{/* Global Legal Footer */}
|
||||
<LegalFooter />
|
||||
|
||||
{/* Global Cart Drawer */}
|
||||
<CartDrawer />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { motion } from 'framer-motion'
|
||||
import { FOOTER_TEXT } from '../constants/footer'
|
||||
import { Routes } from '../routes'
|
||||
|
||||
import './LegalFooter.css'
|
||||
|
||||
|
|
@ -14,11 +15,11 @@ export default function LegalFooter() {
|
|||
<div className="legal-footer-content">
|
||||
<p className="legal-footer-text">{FOOTER_TEXT.copyright}</p>
|
||||
<div className="legal-footer-links">
|
||||
<a href="/terms" className="legal-footer-link">
|
||||
<a href={Routes.terms} className="legal-footer-link">
|
||||
{FOOTER_TEXT.terms}
|
||||
</a>
|
||||
<span className="legal-footer-separator">•</span>
|
||||
<a href="/privacy" className="legal-footer-link">
|
||||
<a href={Routes.privacy} className="legal-footer-link">
|
||||
{FOOTER_TEXT.privacy}
|
||||
</a>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import toast from 'react-hot-toast'
|
|||
import { useSoundEngine, useThrottledSound } from '@ui/effects-sound'
|
||||
import type { RegistrationFormData } from '../types'
|
||||
import { useI18n } from '../i18n'
|
||||
import { Routes } from '../routes'
|
||||
|
||||
import FeatureWaitlistModal from './FeatureWaitlistModal'
|
||||
import './RegistrationForm.css'
|
||||
|
|
@ -214,11 +215,11 @@ export default function RegistrationForm({ userType, onBack }: RegistrationFormP
|
|||
/>
|
||||
<label htmlFor="terms" className="checkbox-label">
|
||||
{safeGet(i18n, 'registration.termsPrefix', 'I agree to the')}{' '}
|
||||
<a href="/terms" target="_blank" rel="noopener noreferrer">
|
||||
<a href={Routes.terms} target="_blank" rel="noopener noreferrer">
|
||||
{safeGet(i18n, 'registration.termsOfService', 'Terms of Service')}
|
||||
</a>{' '}
|
||||
{safeGet(i18n, 'registration.and', 'and')}{' '}
|
||||
<a href="/privacy" target="_blank" rel="noopener noreferrer">
|
||||
<a href={Routes.privacy} target="_blank" rel="noopener noreferrer">
|
||||
{safeGet(i18n, 'registration.privacyPolicy', 'Privacy Policy')}
|
||||
</a>
|
||||
</label>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
import { useSEO } from '@lilith/i18n'
|
||||
import { useSEO, type AboutPageType } from '@lilith/i18n'
|
||||
import { useEffect } from 'react'
|
||||
|
||||
import type { PageType, SEOPageType } from '../pages/types'
|
||||
import { Routes, type StaticRouteKey } from '../routes'
|
||||
|
||||
interface SEOHeadProps {
|
||||
pageType?: PageType;
|
||||
|
|
@ -11,20 +12,17 @@ interface SEOHeadProps {
|
|||
|
||||
const BASE_URL = 'https://lilith.app'
|
||||
|
||||
const CANONICAL_PATHS: Partial<Record<SEOPageType, string>> = {
|
||||
home: '',
|
||||
values: '/values',
|
||||
apps: '/apps',
|
||||
terms: '/terms',
|
||||
privacy: '/privacy',
|
||||
merch: '/merch',
|
||||
}
|
||||
/** Static pages that have direct Routes entries */
|
||||
const STATIC_PAGES: StaticRouteKey[] = ['home', 'values', 'apps', 'terms', 'privacy', 'shop', 'shopGiftCards', 'shopApparel', 'shopIdeas', 'roadmap', 'services']
|
||||
|
||||
function getCanonicalUrl(pageType: SEOPageType): string {
|
||||
if (pageType in CANONICAL_PATHS) {
|
||||
return `${BASE_URL}${CANONICAL_PATHS[pageType]}`
|
||||
// Static pages use Routes directly
|
||||
if (STATIC_PAGES.includes(pageType as StaticRouteKey)) {
|
||||
const path = Routes[pageType as StaticRouteKey]
|
||||
return `${BASE_URL}${path}`
|
||||
}
|
||||
return `${BASE_URL}/about/${pageType}`
|
||||
// About pages use the dynamic builder
|
||||
return `${BASE_URL}${Routes.about(pageType as AboutPageType)}`
|
||||
}
|
||||
|
||||
function updateMetaTag(selector: string, content: string): void {
|
||||
|
|
|
|||
|
|
@ -141,7 +141,8 @@
|
|||
width: 50%;
|
||||
height: 50%;
|
||||
cursor: pointer;
|
||||
transition: all 0.4s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||
/* Fast exit transition (when un-hovering) */
|
||||
transition: all 0.15s ease-out;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
|
@ -288,44 +289,8 @@
|
|||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Hover states - pause idle animations and enhance */
|
||||
.simon-quadrant:hover {
|
||||
filter: brightness(1.3) saturate(1.2);
|
||||
z-index: 20;
|
||||
animation: none !important;
|
||||
}
|
||||
|
||||
.simon-quadrant:hover::before {
|
||||
animation: none !important;
|
||||
}
|
||||
|
||||
.simon-quadrant-1:hover {
|
||||
transform: scale(1.08) translate(-3%, -3%);
|
||||
box-shadow:
|
||||
0 0 80px rgba(255, 215, 0, 0.8),
|
||||
0 0 120px rgba(255, 215, 0, 0.4);
|
||||
}
|
||||
|
||||
.simon-quadrant-2:hover {
|
||||
transform: scale(1.08) translate(3%, -3%);
|
||||
box-shadow:
|
||||
0 0 80px rgba(65, 105, 225, 0.8),
|
||||
0 0 120px rgba(65, 105, 225, 0.4);
|
||||
}
|
||||
|
||||
.simon-quadrant-3:hover {
|
||||
transform: scale(1.08) translate(-3%, 3%);
|
||||
box-shadow:
|
||||
0 0 80px rgba(50, 205, 50, 0.8),
|
||||
0 0 120px rgba(50, 205, 50, 0.4);
|
||||
}
|
||||
|
||||
.simon-quadrant-4:hover {
|
||||
transform: scale(1.08) translate(3%, 3%);
|
||||
box-shadow:
|
||||
0 0 80px rgba(220, 20, 60, 0.8),
|
||||
0 0 120px rgba(220, 20, 60, 0.4);
|
||||
}
|
||||
/* Hover states - now handled by Framer Motion whileHover for reliable state tracking */
|
||||
/* Box-shadow glow effects still applied via CSS for performance */
|
||||
|
||||
/* Active/click state */
|
||||
.simon-quadrant:active {
|
||||
|
|
@ -373,7 +338,7 @@
|
|||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
/* CENTER CIRCLE - completely stable, never zooms or scales on hover/text change */
|
||||
/* CENTER CIRCLE - static decorative element (like real Simon game) */
|
||||
.simon-center {
|
||||
position: absolute;
|
||||
/* Use inset auto + margin auto for true centering */
|
||||
|
|
@ -387,22 +352,14 @@
|
|||
border-radius: 50%;
|
||||
background:
|
||||
radial-gradient(circle at 30% 30%, #2a2a3a, #1a1a2a 50%, #0a0a15);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 30;
|
||||
cursor: pointer;
|
||||
pointer-events: none;
|
||||
border: 4px solid rgba(255, 255, 255, 0.15);
|
||||
box-shadow:
|
||||
0 0 0 8px rgba(0, 0, 0, 0.5),
|
||||
0 0 60px rgba(0, 0, 0, 0.8),
|
||||
inset 0 0 40px rgba(0, 0, 0, 0.5),
|
||||
0 0 100px rgba(255, 255, 255, 0.1);
|
||||
/* Only transition border-color and box-shadow, NOT transform */
|
||||
transition:
|
||||
border-color 0.4s cubic-bezier(0.34, 1.56, 0.64, 1),
|
||||
box-shadow 0.4s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||
/* Initial fade-in + continuous glow */
|
||||
animation:
|
||||
centerFadeIn 0.5s ease-out 0.3s both,
|
||||
|
|
@ -436,51 +393,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
.simon-center:hover {
|
||||
/* No transform - circle stays completely stable, only glow intensifies */
|
||||
border-color: rgba(255, 255, 255, 0.3);
|
||||
box-shadow:
|
||||
0 0 0 8px rgba(0, 0, 0, 0.6),
|
||||
0 0 100px rgba(147, 112, 219, 0.5),
|
||||
inset 0 0 40px rgba(0, 0, 0, 0.3),
|
||||
0 0 200px rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
/* Text container with perspective for 3D rotations */
|
||||
.center-text-container {
|
||||
perspective: 500px;
|
||||
perspective-origin: center center;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 3em;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Center text - 3D transformable */
|
||||
.center-text {
|
||||
font-size: clamp(1rem, 3vw, 1.4rem);
|
||||
font-weight: 600;
|
||||
background: linear-gradient(135deg, #fff 0%, #b8b8b8 100%);
|
||||
background-clip: text;
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
line-height: 1.4;
|
||||
padding: 0 1rem;
|
||||
transform-style: preserve-3d;
|
||||
backface-visibility: hidden;
|
||||
will-change: transform, opacity;
|
||||
}
|
||||
|
||||
.center-hint {
|
||||
font-size: 0.75rem;
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
margin-top: 0.5rem;
|
||||
font-style: italic;
|
||||
}
|
||||
/* No hover state needed - center is purely decorative */
|
||||
|
||||
/* FOOTER */
|
||||
.simon-footer {
|
||||
|
|
@ -598,27 +511,11 @@
|
|||
}
|
||||
}
|
||||
|
||||
/* Hover media query for touch devices */
|
||||
/* Touch devices - Framer Motion handles hover, just enhance active state */
|
||||
@media (hover: none) {
|
||||
.simon-quadrant:hover {
|
||||
transform: none;
|
||||
filter: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.simon-quadrant:active {
|
||||
filter: brightness(1.4) saturate(1.2);
|
||||
}
|
||||
|
||||
.simon-center:hover {
|
||||
/* No changes on touch devices - circle stays stable */
|
||||
border-color: rgba(255, 255, 255, 0.15);
|
||||
box-shadow:
|
||||
0 0 0 8px rgba(0, 0, 0, 0.5),
|
||||
0 0 60px rgba(0, 0, 0, 0.8),
|
||||
inset 0 0 40px rgba(0, 0, 0, 0.5),
|
||||
0 0 100px rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
/* Reduced motion preference */
|
||||
|
|
@ -633,20 +530,9 @@
|
|||
animation: none;
|
||||
}
|
||||
|
||||
.simon-quadrant,
|
||||
.simon-center,
|
||||
.center-text {
|
||||
.simon-quadrant {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
/* Disable 3D perspective for reduced motion */
|
||||
.center-text-container {
|
||||
perspective: none;
|
||||
}
|
||||
|
||||
.center-text {
|
||||
transform-style: flat;
|
||||
}
|
||||
}
|
||||
|
||||
/* High contrast mode */
|
||||
|
|
@ -656,10 +542,6 @@
|
|||
text-shadow: none;
|
||||
}
|
||||
|
||||
.center-text {
|
||||
-webkit-text-fill-color: #fff;
|
||||
}
|
||||
|
||||
.simon-quadrant::before {
|
||||
display: none;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { useUserTypes, type UserType } from '@lilith/i18n'
|
||||
import { motion, AnimatePresence } from 'framer-motion'
|
||||
import { motion } from 'framer-motion'
|
||||
import type { MouseEvent } from 'react'
|
||||
import { useState, useRef } from 'react'
|
||||
|
||||
|
|
@ -17,8 +17,6 @@ import './SimonSelector.css'
|
|||
const FALLBACK_COMMON = {
|
||||
brandName: 'lilith',
|
||||
tagline: 'Sexual Liberation Technology',
|
||||
chooseYourPath: 'Choose Your Path',
|
||||
moreClicks: '{{count}} more...',
|
||||
footerText: 'Choose your path to begin',
|
||||
}
|
||||
|
||||
|
|
@ -37,6 +35,15 @@ const QUADRANT_GRADIENTS: Record<UserType, string> = {
|
|||
investor: 'linear-gradient(135deg, #9370DB 0%, #BA55D3 100%)',
|
||||
}
|
||||
|
||||
// Glow colors for each quadrant (used in hover box-shadow)
|
||||
const QUADRANT_GLOW_COLORS: Record<number, string> = {
|
||||
1: 'rgba(255, 215, 0, 0.8)', // gold/yellow for client
|
||||
2: 'rgba(65, 105, 225, 0.8)', // blue for fan
|
||||
3: 'rgba(50, 205, 50, 0.8)', // green for provider
|
||||
4: 'rgba(220, 20, 60, 0.8)', // red for creator
|
||||
}
|
||||
|
||||
|
||||
interface SimonSelectorProps {
|
||||
onSelect: (userType: UserType) => void;
|
||||
}
|
||||
|
|
@ -50,9 +57,7 @@ export default function SimonSelector({ onSelect }: SimonSelectorProps) {
|
|||
const i18n = useI18n()
|
||||
const USER_TYPES = useUserTypes()
|
||||
|
||||
const [hoveredType, setHoveredType] = useState<UserType | null>(null)
|
||||
const [selectedType, setSelectedType] = useState<UserType | null>(null)
|
||||
const [investorClicks, setInvestorClicks] = useState(0)
|
||||
|
||||
// Ripple states for each quadrant
|
||||
const [rippleStates, setRippleStates] = useState<Record<UserType, RippleState>>({
|
||||
|
|
@ -104,18 +109,6 @@ export default function SimonSelector({ onSelect }: SimonSelectorProps) {
|
|||
onSelect(userType)
|
||||
}
|
||||
|
||||
const handleCenterClick = () => {
|
||||
// Play center hover sound
|
||||
playSound('center-hover')
|
||||
|
||||
const newClicks = investorClicks + 1
|
||||
setInvestorClicks(newClicks)
|
||||
|
||||
if (newClicks >= 5) {
|
||||
onSelect('investor')
|
||||
}
|
||||
}
|
||||
|
||||
const handleQuadrantHover = (userType: UserType) => {
|
||||
// Play quadrant-specific hover sound based on user type position
|
||||
const quadrantSounds: Record<UserType, 'quadrant-hover-nw' | 'quadrant-hover-ne' | 'quadrant-hover-sw' | 'quadrant-hover-se'> = {
|
||||
|
|
@ -123,16 +116,11 @@ export default function SimonSelector({ onSelect }: SimonSelectorProps) {
|
|||
fan: 'quadrant-hover-ne', // Quadrant 2 (top-right)
|
||||
provider: 'quadrant-hover-sw', // Quadrant 3 (bottom-left)
|
||||
creator: 'quadrant-hover-se', // Quadrant 4 (bottom-right)
|
||||
investor: 'quadrant-hover-nw', // Center (Easter egg, use NW as default)
|
||||
investor: 'quadrant-hover-nw', // Fallback (not used currently)
|
||||
}
|
||||
playSound(quadrantSounds[userType])
|
||||
setHoveredType(userType)
|
||||
}
|
||||
|
||||
const centerText = hoveredType
|
||||
? USER_TYPES.find((ut) => ut.id === hoveredType)?.hoverText || getCommon(i18n, 'chooseYourPath')
|
||||
: getCommon(i18n, 'chooseYourPath')
|
||||
|
||||
return (
|
||||
<div className="simon-container" data-testid="simon-container">
|
||||
{/* AI Background */}
|
||||
|
|
@ -149,17 +137,26 @@ export default function SimonSelector({ onSelect }: SimonSelectorProps) {
|
|||
</motion.div>
|
||||
|
||||
<div className="simon-grid">
|
||||
{USER_TYPES.map((userType, index) => (
|
||||
{USER_TYPES.map((userType, index) => {
|
||||
const quadrantNum = index + 1
|
||||
const glowColor = QUADRANT_GLOW_COLORS[quadrantNum] || 'rgba(255, 255, 255, 0.5)'
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
key={userType.id}
|
||||
ref={(el) => { quadrantRefs.current[userType.id] = el }}
|
||||
className={`simon-quadrant simon-quadrant-${index + 1}`}
|
||||
className={`simon-quadrant simon-quadrant-${quadrantNum}`}
|
||||
data-testid={`${userType.id}-quadrant`}
|
||||
initial={{ opacity: 0, scale: 0.8 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
whileHover={{
|
||||
filter: 'brightness(1.3) saturate(1.2)',
|
||||
boxShadow: `0 0 60px ${glowColor}, 0 0 100px ${glowColor.replace('0.8', '0.5')}`,
|
||||
transition: { duration: 0.15 },
|
||||
}}
|
||||
transition={{ duration: 0.5, delay: index * 0.1 }}
|
||||
onHoverStart={() => handleQuadrantHover(userType.id)}
|
||||
onHoverEnd={() => setHoveredType(null)}
|
||||
onHoverEnd={() => {}}
|
||||
onClick={(e) => handleQuadrantClick(userType.id, e)}
|
||||
>
|
||||
<div className="quadrant-label">{userType.label}</div>
|
||||
|
|
@ -171,27 +168,15 @@ export default function SimonSelector({ onSelect }: SimonSelectorProps) {
|
|||
clickPosition={rippleStates[userType.id].position}
|
||||
/>
|
||||
</motion.div>
|
||||
))}
|
||||
)
|
||||
})}
|
||||
|
||||
<AnimatePresence mode="wait">
|
||||
<motion.div
|
||||
key={centerText}
|
||||
className="simon-center"
|
||||
data-testid="simon-center"
|
||||
initial={{ opacity: 0, scale: 0.8 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
exit={{ opacity: 0, scale: 0.8 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
onClick={handleCenterClick}
|
||||
>
|
||||
<p className="center-text" data-testid="center-text">{centerText}</p>
|
||||
{investorClicks > 0 && investorClicks < 5 && (
|
||||
<p className="center-hint" data-testid="center-hint">
|
||||
{getCommon(i18n, 'moreClicks').replace('{{count}}', String(5 - investorClicks))}
|
||||
</p>
|
||||
)}
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
{/* Static center circle - decorative Simon game element */}
|
||||
<div
|
||||
className="simon-center"
|
||||
data-testid="simon-center"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<motion.div
|
||||
|
|
|
|||
|
|
@ -242,11 +242,40 @@
|
|||
.user-type-panel-benefit-icon {
|
||||
font-size: 1.5rem;
|
||||
flex-shrink: 0;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 10px;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
|
||||
/* User-type accent colors for icons */
|
||||
.user-type-panel-client .user-type-panel-benefit-icon {
|
||||
color: #ffd700;
|
||||
background: rgba(255, 215, 0, 0.15);
|
||||
}
|
||||
|
||||
.user-type-panel-fan .user-type-panel-benefit-icon {
|
||||
color: #4169e1;
|
||||
background: rgba(65, 105, 225, 0.15);
|
||||
}
|
||||
|
||||
.user-type-panel-provider .user-type-panel-benefit-icon {
|
||||
color: #32cd32;
|
||||
background: rgba(50, 205, 50, 0.15);
|
||||
}
|
||||
|
||||
.user-type-panel-creator .user-type-panel-benefit-icon {
|
||||
color: #dc143c;
|
||||
background: rgba(220, 20, 60, 0.15);
|
||||
}
|
||||
|
||||
.user-type-panel-investor .user-type-panel-benefit-icon {
|
||||
color: #9370db;
|
||||
background: rgba(147, 112, 219, 0.15);
|
||||
}
|
||||
|
||||
.user-type-panel-benefit-title {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue