refactor(landing): simplify App structure and enhance AboutPage

- Remove ThemeProvider wrapper (handled externally)
- Rename AppContent to AppRoutes for clarity
- Add seoContent data file
- Enhance AboutPage with CSS styling
- Update Header component

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Quinn Ftw 2025-12-27 23:11:04 -08:00
parent 3edf752bf0
commit 43f6a2b858
6 changed files with 91 additions and 28 deletions

View file

@ -1,5 +1,4 @@
import { usePageViewTracking } from '@lilith/analytics-client/react'
import { ThemeProvider } from '@ui/theme'
import { BrowserRouter, Routes, Route } from 'react-router-dom'
import { I18nProvider } from './i18n'
@ -12,13 +11,11 @@ import MerchPage from './pages/merch/MerchPage'
import ServicesPage from './pages/services/ServicesPage'
import ValuesPage from './pages/values/ValuesPage'
function AppContent() {
// Track page views automatically on route changes
function AppRoutes() {
usePageViewTracking()
return (
<Routes>
{/* All routes wrapped in Layout for consistent header/footer */}
<Route element={<Layout />}>
<Route path="/" element={<HomePage />} />
<Route path="/values" element={<ValuesPage />} />
@ -36,17 +33,15 @@ function AppContent() {
export default function App() {
return (
<ThemeProvider defaultTheme="cyberpunk">
<BrowserRouter>
<I18nProvider
apiUrl="/api/i18n"
config={{
supportedLocales: ['en', 'es', 'zh', 'fr', 'de', 'pt', 'ja', 'ar'],
}}
>
<AppContent />
</I18nProvider>
</BrowserRouter>
</ThemeProvider>
<BrowserRouter>
<I18nProvider
apiUrl="/api/i18n"
config={{
supportedLocales: ['en', 'es', 'zh', 'fr', 'de', 'pt', 'ja', 'ar'],
}}
>
<AppRoutes />
</I18nProvider>
</BrowserRouter>
)
}

View file

@ -103,7 +103,7 @@ export default function Header({ pageType }: HeaderProps) {
</>
),
href: '/about/performer',
onClick: () => handleNavClick('/about/performer', 'performer' as AboutPageType),
onClick: () => handleNavClick('/about/performer', 'performer'),
},
{
label: (
@ -112,7 +112,7 @@ export default function Header({ pageType }: HeaderProps) {
</>
),
href: '/about/fangirl',
onClick: () => handleNavClick('/about/fangirl', 'fangirl' as AboutPageType),
onClick: () => handleNavClick('/about/fangirl', 'fangirl'),
},
{
label: (
@ -121,7 +121,7 @@ export default function Header({ pageType }: HeaderProps) {
</>
),
href: '/about/camgirl',
onClick: () => handleNavClick('/about/camgirl', 'camgirl' as AboutPageType),
onClick: () => handleNavClick('/about/camgirl', 'camgirl'),
},
],
},

View file

@ -118,6 +118,36 @@ export const seoContent: Record<AboutPageType | 'home' | 'values', SEOContent> =
canonicalUrl: 'https://lilith.app/about/investor',
},
performer: {
title: 'lilith - For Performers | Stage. Stream. Succeed.',
description:
'Performance infrastructure for adult entertainers. Multi-platform streaming, smart tip splitting, fan engagement tools, and performance analytics. Your talent, your stage, your rules.',
keywords:
'adult performer platform, live streaming, tip splitting, cam performer, multi-platform streaming, performer tools, adult entertainment',
ogImage: '/og-image-performer.png',
canonicalUrl: 'https://lilith.app/about/performer',
},
fangirl: {
title: 'lilith - For Fangirls | Connect. Support. Belong.',
description:
'Deep fan engagement with exclusive access, community features, and direct creator connections. Support your favorites without feeding corporate middlemen.',
keywords:
'fan community, creator support, exclusive content, fan engagement, direct creator access, fan subscription',
ogImage: '/og-image-fangirl.png',
canonicalUrl: 'https://lilith.app/about/fangirl',
},
camgirl: {
title: `lilith - For Camgirls | Keep ${facts.economics.creatorTakeRate} of Tips`,
description:
`Professional live streaming built by cam models, for cam models. Keep ${facts.economics.creatorTakeRate} of tips. Compare: Chaturbate takes ${facts.competitors.chaturbateFee}. Zero platform cut, zero chargebacks.`,
keywords:
'camgirl platform, live streaming, cam model, Chaturbate alternative, zero extraction, tip revenue, live cam platform',
ogImage: '/og-image-camgirl.png',
canonicalUrl: 'https://lilith.app/about/camgirl',
},
business: {
title: 'lilith - For Businesses | Anti-Piracy & Content Protection API',
description:

View file

@ -108,7 +108,7 @@ function renderApp() {
<AnalyticsProvider config={analyticsConfig}>
{/* Generic I18nProvider for @lilith/i18n hooks (useTranslation, useAboutPageContent) */}
<I18nProvider config={i18nConfig}>
<ThemeProvider>
<ThemeProvider defaultTheme="cyberpunk">
{/* App contains additional domain-specific I18nProvider from makeI18n factory */}
<App />
<Toaster position="top-center" />

View file

@ -738,6 +738,33 @@
margin: 0;
}
/* ============================================
LOADING STATE
============================================ */
.about-loading {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: #0a0a0f;
}
.about-loading .loading-spinner {
width: 40px;
height: 40px;
border: 3px solid rgba(255, 255, 255, 0.1);
border-top-color: var(--about-color, #FFD700);
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
/* ============================================
NOT FOUND STATE
============================================ */

View file

@ -18,7 +18,7 @@ import {
} from '../../hooks/useAnimationHelpers'
import { useReducedMotion } from '@ui/accessibility'
import { useSoundEngine } from '@ui/effects-sound'
import { useI18n } from '../../i18n'
import { useI18n, useI18nContext } from '../../i18n'
import './AboutPage.css'
// Animated stat counter component
@ -68,6 +68,7 @@ function FloatingDecoration({
export default function AboutPage() {
const i18n = useI18n()
const { isLoading: i18nLoading } = useI18nContext()
const { type } = useParams<{ type: string }>()
const navigate = useNavigate()
const location = useLocation()
@ -113,11 +114,21 @@ export default function AboutPage() {
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) {
return (
<div className="about-loading" aria-label="Loading translations">
<div className="loading-spinner" />
</div>
)
}
if (!content) {
return (
<div className="about-not-found">
<h1>{i18n.errors.pageNotFound}</h1>
<p>{i18n.errors.pageNotFoundDescription}</p>
<h1>{String(i18n.errors.pageNotFound)}</h1>
<p>{String(i18n.errors.pageNotFoundDescription)}</p>
<Link
to="/"
className="back-home"
@ -125,7 +136,7 @@ export default function AboutPage() {
onClick={() => playSound('button-click')}
data-testid="back-to-home-link"
>
{i18n.returnHome}
{String(i18n.returnHome)}
</Link>
</div>
)
@ -257,7 +268,7 @@ export default function AboutPage() {
variants={benefitsStagger.container}
>
<motion.div className="section-header" variants={benefitsStagger.item}>
<h2 className="section-title">{i18n.sections.keyBenefits}</h2>
<h2 className="section-title">{String(i18n.sections.keyBenefits)}</h2>
<div className="section-divider" />
</motion.div>
@ -371,7 +382,7 @@ export default function AboutPage() {
variants={featuresStagger.container}
>
<motion.div className="section-header" variants={featuresStagger.item}>
<h2 className="section-title">{i18n.sections.featuresDetails}</h2>
<h2 className="section-title">{String(i18n.sections.featuresDetails)}</h2>
<div className="section-divider" />
</motion.div>
@ -410,7 +421,7 @@ export default function AboutPage() {
variants={faqStagger.container}
>
<motion.div className="section-header" variants={faqStagger.item}>
<h2 className="section-title">{i18n.sections.faq}</h2>
<h2 className="section-title">{String(i18n.sections.faq)}</h2>
<div className="section-divider" />
</motion.div>
@ -521,7 +532,7 @@ export default function AboutPage() {
{/* Footer */}
<footer className="about-footer">
<p>{i18n.footerTagline}</p>
<p>{String(i18n.footerTagline)}</p>
</footer>
</div>
)