feat: dist-site 多主题模板 + 中英双语 i18n

- 新增 4 套主题模板:starter/default/dark/minimal
  - Starter: 毛玻璃 + GradientText + SpotlightCard (原有风格)
  - Default: Aurora 极光背景 + SplitText + TiltedCard
  - Dark: Particles 粒子 + DecryptedText 赛博朋克风格
  - Minimal: 极简优雅 + FadeContent 淡入动画
- 新增 i18n 中英双语支持 (i18next + browser-languagedetector)
- 新增 ThemeProvider 运行时主题切换 (根据 theme_template 字段)
- 新增 11 个 react-bits 动画组件
- 所有页面文案国际化 (Login/Register/Dashboard/Tokens/Pricing/Packages)
- 新增 README.md + README.zh.md 含主题切换指南

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
abingyydsandClaude Sonnet 4.6 committed 2026-03-21 09:04:25 +09:00
1 parent cc9a8bcc0e
commit 01405ca8cf
68 files changed
+4429 -433

No files matched your search

+209
View File
@@ -0,0 +1,209 @@
import React, { useState, useEffect } from 'react';
import { Link } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { useAuth } from '../../context/AuthContext';
import { useSite } from '../../context/SiteContext';
import { getSiteModels, getSitePackages } from '../../api';
import Particles from '../../components/bits/Particles';
import DecryptedText from '../../components/bits/DecryptedText';
import CountUp from '../../components/bits/CountUp';
export default function DarkHome() {
const { t } = useTranslation();
const { user } = useAuth();
const { site } = useSite();
const [models, setModels] = useState([]);
const [packages, setPackages] = useState([]);
useEffect(() => {
getSiteModels().then(r => { if (r.data.success) setModels(r.data.data || []); }).catch(() => {});
getSitePackages().then(r => { if (r.data.success) setPackages(r.data.data || []); }).catch(() => {});
}, []);
const enabledModels = models.filter(m => m.enabled !== false);
return (
<div className="relative">
{/* Particles Background */}
<div className="absolute inset-0 h-[800px] overflow-hidden pointer-events-none">
<Particles
particleColors={['#00ff88', '#00ccff', '#ff0055']}
particleCount={80}
particleSpread={15}
speed={0.3}
particleBaseSize={80}
moveParticlesOnHover={true}
alphaParticles={false}
disableRotation={false}
/>
</div>
{/* Hero Section */}
<section className="relative max-w-7xl mx-auto px-6 pt-28 pb-24">
<div className="text-center max-w-4xl mx-auto">
{/* Terminal-style header */}
<div className="inline-flex items-center gap-2 px-4 py-1.5 rounded-full border border-green-500/30 bg-green-500/5 mb-8">
<span className="w-2 h-2 rounded-full bg-green-500 animate-pulse" />
<span className="text-xs font-mono text-green-400 uppercase tracking-wider">System Online</span>
</div>
<h1 className="text-5xl md:text-7xl font-heading font-extrabold text-white leading-tight mb-4">
<DecryptedText
text={site?.name || t('home.defaultHeroTitle')}
speed={60}
maxIterations={10}
revealDirection="start"
className="text-white"
encryptedClassName="text-green-500/40"
/>
</h1>
<p className="text-lg md:text-xl text-neutral-400 max-w-2xl mx-auto mb-10 font-mono">
{'> '}{t('home.heroSubtitle')}
</p>
<div className="flex items-center justify-center gap-4">
{user ? (
<Link to="/dashboard" className="px-8 py-3.5 rounded-lg bg-green-500 text-black font-bold text-base hover:bg-green-400 transition-all font-mono shadow-lg shadow-green-500/20">
{t('home.goToDashboard')}
</Link>
) : (
<>
<Link to="/register" className="px-8 py-3.5 rounded-lg bg-green-500 text-black font-bold text-base hover:bg-green-400 transition-all font-mono shadow-lg shadow-green-500/20">
{t('home.getStarted')}
</Link>
<Link to="/pricing" className="px-8 py-3.5 rounded-lg border border-green-500/30 text-green-400 font-bold text-base hover:bg-green-500/10 transition-all font-mono">
{t('home.viewPricing')}
</Link>
</>
)}
</div>
</div>
{/* Stats — terminal style */}
<div className="grid grid-cols-3 gap-4 max-w-2xl mx-auto mt-20">
{[
{ label: t('home.aiModels'), value: enabledModels.length || 50, suffix: '+', color: 'text-green-400 border-green-500/20' },
{ label: t('home.uptime'), value: 99.9, suffix: '%', color: 'text-cyan-400 border-cyan-500/20' },
{ label: t('home.latency'), value: 50, suffix: 'ms', prefix: '<', color: 'text-pink-400 border-pink-500/20' },
].map((s, i) => (
<div key={i} className={`text-center p-4 rounded-lg bg-black/60 border ${s.color} font-mono`}>
<div className={`text-3xl font-bold ${s.color.split(' ')[0]}`}>
{s.prefix}<CountUp from={0} to={s.value} duration={2} />{s.suffix}
</div>
<p className="text-xs text-neutral-500 mt-1 uppercase tracking-wider">{s.label}</p>
</div>
))}
</div>
</section>
{/* Features Section */}
<section className="relative max-w-7xl mx-auto px-6 py-20">
<div className="text-center mb-14">
<h2 className="text-3xl font-heading font-bold text-white mb-3">{t('home.whyChooseUs')}</h2>
<p className="text-neutral-500 font-mono">{t('home.whyChooseUsDesc')}</p>
</div>
<div className="grid md:grid-cols-3 gap-6">
{[
{ icon: '⚡', title: t('home.lightningFast'), desc: t('home.lightningFastDesc'), accent: 'green' },
{ icon: '🛡️', title: t('home.securePrivate'), desc: t('home.securePrivateDesc'), accent: 'cyan' },
{ icon: '📊', title: t('home.payAsYouGo'), desc: t('home.payAsYouGoDesc'), accent: 'pink' },
].map((f, i) => (
<div key={i} className={`p-6 rounded-xl bg-black/40 border border-${f.accent}-500/20 hover:border-${f.accent}-500/40 transition-all group`}>
<div className="text-2xl mb-3">{f.icon}</div>
<h3 className={`text-lg font-semibold text-${f.accent}-400 mb-2 font-mono`}>{f.title}</h3>
<p className="text-sm text-neutral-400 leading-relaxed">{f.desc}</p>
<div className={`h-0.5 w-0 group-hover:w-full bg-${f.accent}-500/50 transition-all duration-500 mt-4`} />
</div>
))}
</div>
</section>
{/* Models Preview */}
{enabledModels.length > 0 && (
<section className="relative max-w-7xl mx-auto px-6 py-20">
<div className="text-center mb-12">
<h2 className="text-3xl font-heading font-bold text-white mb-3">{t('home.availableModels')}</h2>
<p className="text-neutral-500 font-mono">{t('home.availableModelsDesc', { count: enabledModels.length })}</p>
</div>
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-2">
{enabledModels.slice(0, 12).map((m, i) => (
<div key={m.id || i} className="px-4 py-3 rounded-lg bg-black/40 border border-green-500/10 flex items-center gap-3 hover:border-green-500/30 transition-colors group font-mono">
<span className="text-green-500 text-xs">$</span>
<span className="text-sm text-neutral-400 group-hover:text-green-400 transition-colors truncate">
{m.display_name || m.model_name}
</span>
</div>
))}
</div>
{enabledModels.length > 12 && (
<div className="text-center mt-6">
<Link to="/pricing" className="text-sm text-green-400 hover:text-green-300 transition-colors font-mono">
{t('home.viewAllModels', { count: enabledModels.length })} &rarr;
</Link>
</div>
)}
</section>
)}
{/* Packages */}
{packages.length > 0 && (
<section className="relative max-w-7xl mx-auto px-6 py-20">
<div className="text-center mb-12">
<h2 className="text-3xl font-heading font-bold text-white mb-3">{t('home.plansPackages')}</h2>
<p className="text-neutral-500 font-mono">{t('home.choosePlan')}</p>
</div>
<div className="grid md:grid-cols-3 gap-6 max-w-4xl mx-auto">
{packages.filter(p => p.enabled).slice(0, 3).map((pkg) => (
<div key={pkg.id} className="rounded-xl p-6 flex flex-col bg-black/40 border border-green-500/10 hover:border-green-500/30 transition-all">
<h3 className="text-lg font-semibold text-white mb-1 font-mono">{pkg.name}</h3>
{pkg.description && <p className="text-sm text-neutral-400 mb-4">{pkg.description}</p>}
<div className="mt-auto pt-4">
<span className="text-3xl font-bold text-green-400 font-mono">${pkg.price}</span>
{pkg.original_price > pkg.price && (
<span className="text-sm text-neutral-600 line-through ml-2">${pkg.original_price}</span>
)}
{pkg.duration > 0 && <p className="text-xs text-neutral-600 mt-1 font-mono">{t('home.days', { count: pkg.duration })}</p>}
</div>
<Link to={user ? '/packages' : '/register'} className="mt-4 py-2.5 rounded-lg bg-green-500/10 border border-green-500/30 text-green-400 font-medium text-sm text-center hover:bg-green-500/20 transition-colors font-mono">
{user ? t('home.subscribe') : t('home.getStarted')}
</Link>
</div>
))}
</div>
</section>
)}
{/* CTA */}
<section className="relative max-w-7xl mx-auto px-6 py-24">
<div className="rounded-xl p-12 text-center bg-black/60 border border-green-500/20 relative overflow-hidden">
<div className="absolute inset-0 bg-gradient-to-r from-green-500/5 via-transparent to-cyan-500/5" />
<div className="relative z-10">
<h2 className="text-3xl font-heading font-bold text-white mb-4 font-mono">{t('home.readyToStart')}</h2>
<p className="text-neutral-400 mb-8 max-w-md mx-auto">{t('home.readyToStartDesc')}</p>
<div className="flex items-center justify-center gap-4">
{user ? (
<Link to="/dashboard" className="px-8 py-3 rounded-lg bg-green-500 text-black font-bold hover:bg-green-400 transition-all font-mono">
{t('home.goToDashboard')}
</Link>
) : (
<>
<Link to="/register" className="px-8 py-3 rounded-lg bg-green-500 text-black font-bold hover:bg-green-400 transition-all font-mono">
{t('home.createFreeAccount')}
</Link>
<Link to="/login" className="px-8 py-3 rounded-lg border border-green-500/30 text-green-400 font-bold hover:bg-green-500/10 transition-all font-mono">
{t('home.signIn')}
</Link>
</>
)}
</div>
</div>
</div>
</section>
</div>
);
}
+122
View File
@@ -0,0 +1,122 @@
import React, { useState } from 'react';
import { Outlet, Link, useNavigate, useLocation } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { useAuth } from '../../context/AuthContext';
import { useSite } from '../../context/SiteContext';
export default function DarkLayout() {
const { t } = useTranslation();
const { user, logout } = useAuth();
const { site } = useSite();
const navigate = useNavigate();
const location = useLocation();
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
const siteName = site?.name || 'AI Platform';
const navItems = [
{ to: '/', label: t('nav.home'), auth: false },
{ to: '/pricing', label: t('nav.pricing'), auth: false },
{ to: '/packages', label: t('nav.packages'), auth: false },
{ to: '/dashboard', label: t('nav.dashboard'), auth: true },
{ to: '/tokens', label: t('nav.apiKeys'), auth: true },
];
const visibleNavItems = navItems.filter((n) => !n.auth || user);
return (
<div className="min-h-screen flex flex-col bg-black text-white">
{/* Announcement Banner */}
{site?.announcement && (
<div className="bg-green-500/10 border-b border-green-500/20 text-center py-2 px-4">
<p className="text-sm text-green-300 font-mono">{site.announcement}</p>
</div>
)}
{/* Header — cyberpunk style */}
<header className="sticky top-0 z-50 backdrop-blur-xl bg-black/80 border-b border-green-500/10">
<div className="max-w-7xl mx-auto px-6 h-14 flex items-center justify-between">
<Link to="/" className="flex items-center gap-3 group">
{site?.logo ? (
<img src={site.logo} alt={siteName} className="h-7 w-auto" />
) : (
<div className="w-8 h-8 rounded-md bg-green-500/20 border border-green-500/30 flex items-center justify-center text-green-400 font-bold font-mono text-sm">
{siteName.charAt(0)}
</div>
)}
<span className="text-lg font-semibold text-green-400 font-mono group-hover:text-green-300 transition-colors">
{siteName}
</span>
</Link>
<nav className="hidden md:flex items-center gap-1">
{visibleNavItems.map((n) => (
<Link
key={n.to}
to={n.to}
className={`px-3 py-1.5 text-sm font-mono rounded transition-all ${
location.pathname === n.to
? 'text-green-400 bg-green-500/10'
: 'text-neutral-500 hover:text-green-400'
}`}
>
{n.label}
</Link>
))}
</nav>
<div className="flex items-center gap-3">
{user ? (
<div className="flex items-center gap-3">
<span className="text-sm text-neutral-500 font-mono hidden sm:block">{user.display_name || user.username}</span>
<button onClick={async () => { await logout(); navigate('/'); }} className="text-sm text-neutral-500 hover:text-green-400 font-mono transition-colors">
{t('nav.logout')}
</button>
</div>
) : (
<>
<Link to="/login" className="text-sm text-neutral-500 hover:text-green-400 font-mono transition-colors hidden sm:block">
{t('nav.login')}
</Link>
<Link to="/register" className="px-4 py-1.5 rounded-md bg-green-500/10 border border-green-500/30 text-green-400 text-sm font-mono hover:bg-green-500/20 transition-all">
{t('nav.signUp')}
</Link>
</>
)}
<button className="md:hidden p-1.5 rounded hover:bg-white/5" onClick={() => setMobileMenuOpen(!mobileMenuOpen)}>
<svg className="w-5 h-5 text-green-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
{mobileMenuOpen ? <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" /> : <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />}
</svg>
</button>
</div>
</div>
{mobileMenuOpen && (
<div className="md:hidden border-t border-green-500/10 bg-black/95 backdrop-blur-xl">
<nav className="max-w-7xl mx-auto px-6 py-3 flex flex-col gap-1">
{visibleNavItems.map((n) => (
<Link key={n.to} to={n.to} onClick={() => setMobileMenuOpen(false)}
className={`px-3 py-2 text-sm font-mono rounded transition-colors ${location.pathname === n.to ? 'text-green-400 bg-green-500/10' : 'text-neutral-500 hover:text-green-400'}`}>
{n.label}
</Link>
))}
</nav>
</div>
)}
</header>
<main className="flex-1"><Outlet /></main>
<footer className="border-t border-green-500/10 mt-auto">
<div className="max-w-7xl mx-auto px-6 py-6 flex flex-col md:flex-row items-center justify-between gap-4">
<p className="text-xs text-neutral-600 font-mono">&copy; {new Date().getFullYear()} {siteName}</p>
{site?.contact_email && (
<a href={`mailto:${site.contact_email}`} className="text-xs text-neutral-600 hover:text-green-400 font-mono transition-colors">
{t('nav.contact')}
</a>
)}
</div>
</footer>
</div>
);
}
+210
View File
@@ -0,0 +1,210 @@
import React, { useState, useEffect } from 'react';
import { Link } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { useAuth } from '../../context/AuthContext';
import { useSite } from '../../context/SiteContext';
import { getSiteModels, getSitePackages } from '../../api';
import Aurora from '../../components/bits/Aurora';
import SplitText from '../../components/bits/SplitText';
import RotatingText from '../../components/bits/RotatingText';
import TiltedCard from '../../components/bits/TiltedCard';
import CountUp from '../../components/bits/CountUp';
export default function DefaultHome() {
const { t } = useTranslation();
const { user } = useAuth();
const { site } = useSite();
const [models, setModels] = useState([]);
const [packages, setPackages] = useState([]);
useEffect(() => {
getSiteModels().then(r => { if (r.data.success) setModels(r.data.data || []); }).catch(() => {});
getSitePackages().then(r => { if (r.data.success) setPackages(r.data.data || []); }).catch(() => {});
}, []);
const enabledModels = models.filter(m => m.enabled !== false);
return (
<div className="relative">
{/* Aurora Background */}
<div className="absolute inset-0 h-[700px] overflow-hidden pointer-events-none">
<Aurora colorStops={["#3A29FF", "#FF94B4", "#FF3232"]} blend={0.5} amplitude={1.0} speed={0.5} />
</div>
{/* Hero Section */}
<section className="relative max-w-7xl mx-auto px-6 pt-28 pb-24">
<div className="text-center max-w-4xl mx-auto">
<div className="mb-4">
<h1 className="text-5xl md:text-7xl font-heading font-extrabold text-white leading-tight">
<SplitText text={site?.name || t('home.defaultHeroTitle')} delay={80} className="inline" />
</h1>
</div>
<div className="flex items-center justify-center gap-2 text-xl md:text-2xl text-neutral-300 mb-10">
<span>{t('home.heroSubtitle').split('.')[0]}.</span>
</div>
<div className="flex flex-wrap items-center justify-center gap-3 mb-6">
<RotatingText
texts={[t('home.lightningFast'), t('home.securePrivate'), t('home.payAsYouGo')]}
mainClassName="px-4 py-2 bg-white/10 backdrop-blur-sm rounded-full text-white text-sm font-medium overflow-hidden"
staggerFrom="last"
rotationInterval={3000}
/>
</div>
<div className="flex items-center justify-center gap-4 mt-8">
{user ? (
<Link to="/dashboard" className="px-8 py-3.5 rounded-2xl bg-white text-black font-semibold text-base hover:bg-neutral-100 transition-all shadow-xl shadow-white/10">
{t('home.goToDashboard')}
</Link>
) : (
<>
<Link to="/register" className="px-8 py-3.5 rounded-2xl bg-white text-black font-semibold text-base hover:bg-neutral-100 transition-all shadow-xl shadow-white/10">
{t('home.getStarted')}
</Link>
<Link to="/pricing" className="px-8 py-3.5 rounded-2xl border border-white/20 text-white font-semibold text-base hover:bg-white/10 transition-all">
{t('home.viewPricing')}
</Link>
</>
)}
</div>
</div>
{/* Stats */}
<div className="grid grid-cols-3 gap-8 max-w-2xl mx-auto mt-24">
{[
{ value: enabledModels.length || 50, suffix: '+', label: t('home.aiModels') },
{ value: 99.9, suffix: '%', label: t('home.uptime') },
{ value: 50, suffix: 'ms', label: t('home.latency'), prefix: '<' },
].map((stat, i) => (
<div key={i} className="text-center p-4 rounded-2xl bg-white/5 backdrop-blur-sm border border-white/10">
<div className="text-3xl font-bold text-white">
{stat.prefix}<CountUp from={0} to={stat.value} duration={2} />{stat.suffix}
</div>
<p className="text-sm text-neutral-400 mt-1">{stat.label}</p>
</div>
))}
</div>
</section>
{/* Features Section */}
<section className="relative max-w-7xl mx-auto px-6 py-20">
<div className="text-center mb-14">
<h2 className="text-3xl font-heading font-bold text-white mb-3">{t('home.whyChooseUs')}</h2>
<p className="text-neutral-400">{t('home.whyChooseUsDesc')}</p>
</div>
<div className="grid md:grid-cols-3 gap-8">
{[
{ icon: '⚡', title: t('home.lightningFast'), desc: t('home.lightningFastDesc'), color: 'from-yellow-500/20 to-orange-500/20' },
{ icon: '🔒', title: t('home.securePrivate'), desc: t('home.securePrivateDesc'), color: 'from-blue-500/20 to-cyan-500/20' },
{ icon: '💰', title: t('home.payAsYouGo'), desc: t('home.payAsYouGoDesc'), color: 'from-green-500/20 to-emerald-500/20' },
].map((f, i) => (
<TiltedCard
key={i}
rotateAmplitude={8}
scaleOnHover={1.03}
borderRadius="16px"
className="w-full"
>
<div className={`p-6 rounded-2xl bg-gradient-to-br ${f.color} border border-white/10 h-full`}>
<div className="text-3xl mb-4">{f.icon}</div>
<h3 className="text-lg font-semibold text-white mb-2">{f.title}</h3>
<p className="text-sm text-neutral-300 leading-relaxed">{f.desc}</p>
</div>
</TiltedCard>
))}
</div>
</section>
{/* Models Preview */}
{enabledModels.length > 0 && (
<section className="relative max-w-7xl mx-auto px-6 py-20">
<div className="text-center mb-12">
<h2 className="text-3xl font-heading font-bold text-white mb-3">{t('home.availableModels')}</h2>
<p className="text-neutral-400">{t('home.availableModelsDesc', { count: enabledModels.length })}</p>
</div>
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3">
{enabledModels.slice(0, 12).map((m, i) => (
<div key={m.id || i} className="px-4 py-3 rounded-xl bg-white/5 border border-white/10 flex items-center gap-3 hover:bg-white/10 transition-colors group">
<div className="w-2 h-2 rounded-full bg-green-400 flex-shrink-0" />
<span className="text-sm font-mono text-neutral-300 group-hover:text-white transition-colors truncate">
{m.display_name || m.model_name}
</span>
</div>
))}
</div>
{enabledModels.length > 12 && (
<div className="text-center mt-6">
<Link to="/pricing" className="text-sm text-blue-400 hover:text-blue-300 transition-colors">
{t('home.viewAllModels', { count: enabledModels.length })} &rarr;
</Link>
</div>
)}
</section>
)}
{/* Packages Preview */}
{packages.length > 0 && (
<section className="relative max-w-7xl mx-auto px-6 py-20">
<div className="text-center mb-12">
<h2 className="text-3xl font-heading font-bold text-white mb-3">{t('home.plansPackages')}</h2>
<p className="text-neutral-400">{t('home.choosePlan')}</p>
</div>
<div className="grid md:grid-cols-3 gap-6 max-w-4xl mx-auto">
{packages.filter(p => p.enabled).slice(0, 3).map((pkg, i) => (
<div key={pkg.id} className={`rounded-2xl p-6 flex flex-col border transition-all hover:scale-[1.02] ${
i === 1 ? 'bg-white/10 border-white/20 shadow-xl' : 'bg-white/5 border-white/10'
}`}>
<h3 className="text-lg font-semibold text-white mb-1">{pkg.name}</h3>
{pkg.description && <p className="text-sm text-neutral-400 mb-4">{pkg.description}</p>}
<div className="mt-auto pt-4">
<div className="flex items-baseline gap-1">
<span className="text-3xl font-bold text-white">${pkg.price}</span>
{pkg.original_price > pkg.price && (
<span className="text-sm text-neutral-500 line-through">${pkg.original_price}</span>
)}
</div>
{pkg.duration > 0 && <p className="text-xs text-neutral-500 mt-1">{t('home.days', { count: pkg.duration })}</p>}
</div>
<Link to={user ? '/packages' : '/register'} className="mt-4 py-2.5 rounded-xl bg-white text-black font-medium text-sm text-center hover:bg-neutral-100 transition-colors">
{user ? t('home.subscribe') : t('home.getStarted')}
</Link>
</div>
))}
</div>
</section>
)}
{/* CTA Section */}
<section className="relative max-w-7xl mx-auto px-6 py-24">
<div className="rounded-3xl p-12 text-center relative overflow-hidden bg-gradient-to-br from-blue-600/20 via-purple-600/20 to-pink-600/20 border border-white/10">
<div className="relative z-10">
<h2 className="text-3xl font-heading font-bold text-white mb-4">{t('home.readyToStart')}</h2>
<p className="text-neutral-300 mb-8 max-w-md mx-auto">{t('home.readyToStartDesc')}</p>
<div className="flex items-center justify-center gap-4">
{user ? (
<Link to="/dashboard" className="px-8 py-3 rounded-2xl bg-white text-black font-semibold hover:bg-neutral-100 transition-all">
{t('home.goToDashboard')}
</Link>
) : (
<>
<Link to="/register" className="px-8 py-3 rounded-2xl bg-white text-black font-semibold hover:bg-neutral-100 transition-all">
{t('home.createFreeAccount')}
</Link>
<Link to="/login" className="px-8 py-3 rounded-2xl border border-white/20 text-white font-semibold hover:bg-white/10 transition-all">
{t('home.signIn')}
</Link>
</>
)}
</div>
</div>
</div>
</section>
</div>
);
}
+151
View File
@@ -0,0 +1,151 @@
import React, { useState } from 'react';
import { Outlet, Link, useNavigate, useLocation } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { useAuth } from '../../context/AuthContext';
import { useSite } from '../../context/SiteContext';
export default function DefaultLayout() {
const { t } = useTranslation();
const { user, logout } = useAuth();
const { site } = useSite();
const navigate = useNavigate();
const location = useLocation();
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
const siteName = site?.name || 'AI Platform';
const navItems = [
{ to: '/', label: t('nav.home'), auth: false },
{ to: '/pricing', label: t('nav.pricing'), auth: false },
{ to: '/packages', label: t('nav.packages'), auth: false },
{ to: '/dashboard', label: t('nav.dashboard'), auth: true },
{ to: '/tokens', label: t('nav.apiKeys'), auth: true },
];
const visibleNavItems = navItems.filter((n) => !n.auth || user);
return (
<div className="min-h-screen flex flex-col bg-[#0a0a1a]">
{/* Announcement Banner */}
{site?.announcement && (
<div className="bg-gradient-to-r from-blue-600/20 via-purple-600/20 to-pink-600/20 border-b border-white/10 text-center py-2.5 px-4">
<p className="text-sm text-neutral-200">{site.announcement}</p>
</div>
)}
{/* Header */}
<header className="sticky top-0 z-50 backdrop-blur-2xl bg-[#0a0a1a]/80 border-b border-white/[0.08]">
<div className="max-w-7xl mx-auto px-6 h-16 flex items-center justify-between">
{/* Logo */}
<Link to="/" className="flex items-center gap-3 group">
{site?.logo ? (
<img src={site.logo} alt={siteName} className="h-8 w-auto" />
) : (
<div className="w-9 h-9 rounded-xl bg-gradient-to-br from-blue-500 via-purple-500 to-pink-500 flex items-center justify-center text-white font-bold text-sm shadow-lg shadow-purple-500/20">
{siteName.charAt(0)}
</div>
)}
<span className="text-lg font-semibold text-white group-hover:text-blue-400 transition-colors">
{siteName}
</span>
</Link>
{/* Desktop Nav */}
<nav className="hidden md:flex items-center gap-1 bg-white/5 rounded-full px-2 py-1">
{visibleNavItems.map((n) => (
<Link
key={n.to}
to={n.to}
className={`px-4 py-1.5 text-sm rounded-full transition-all ${
location.pathname === n.to
? 'text-white bg-white/10'
: 'text-neutral-400 hover:text-white'
}`}
>
{n.label}
</Link>
))}
</nav>
{/* Auth + Mobile toggle */}
<div className="flex items-center gap-3">
{user ? (
<div className="flex items-center gap-3">
<span className="text-sm text-neutral-400 hidden sm:block">
{user.display_name || user.username}
</span>
<button
onClick={async () => { await logout(); navigate('/'); }}
className="text-sm text-neutral-400 hover:text-white transition-colors"
>
{t('nav.logout')}
</button>
</div>
) : (
<>
<Link to="/login" className="text-sm text-neutral-300 hover:text-white transition-colors px-3 py-1.5 hidden sm:block">
{t('nav.login')}
</Link>
<Link to="/register" className="px-5 py-2 rounded-full bg-white text-black text-sm font-medium hover:bg-neutral-100 transition-all">
{t('nav.signUp')}
</Link>
</>
)}
<button
className="md:hidden p-1.5 rounded-lg hover:bg-white/5 transition-colors"
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
aria-label="Toggle menu"
>
<svg className="w-5 h-5 text-neutral-300" fill="none" viewBox="0 0 24 24" stroke="currentColor">
{mobileMenuOpen ? (
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
) : (
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
)}
</svg>
</button>
</div>
</div>
{mobileMenuOpen && (
<div className="md:hidden border-t border-white/[0.08] bg-[#0a0a1a]/95 backdrop-blur-2xl">
<nav className="max-w-7xl mx-auto px-6 py-3 flex flex-col gap-1">
{visibleNavItems.map((n) => (
<Link
key={n.to}
to={n.to}
onClick={() => setMobileMenuOpen(false)}
className={`px-3 py-2 text-sm rounded-lg transition-colors ${
location.pathname === n.to
? 'text-blue-400 bg-blue-500/10'
: 'text-neutral-400 hover:text-white hover:bg-white/5'
}`}
>
{n.label}
</Link>
))}
</nav>
</div>
)}
</header>
<main className="flex-1">
<Outlet />
</main>
<footer className="border-t border-white/[0.08] mt-auto bg-[#0a0a1a]">
<div className="max-w-7xl mx-auto px-6 py-8 flex flex-col md:flex-row items-center justify-between gap-4">
<p className="text-sm text-neutral-500">&copy; {new Date().getFullYear()} {siteName}.</p>
<div className="flex items-center gap-4">
{site?.contact_email && (
<a href={`mailto:${site.contact_email}`} className="text-sm text-neutral-500 hover:text-neutral-300 transition-colors">
{t('nav.contact')}
</a>
)}
</div>
</div>
</footer>
</div>
);
}
+188
View File
@@ -0,0 +1,188 @@
import React, { useState, useEffect } from 'react';
import { Link } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { useAuth } from '../../context/AuthContext';
import { useSite } from '../../context/SiteContext';
import { getSiteModels, getSitePackages } from '../../api';
import CountUp from '../../components/bits/CountUp';
import FadeContent from '../../components/bits/FadeContent';
export default function MinimalHome() {
const { t } = useTranslation();
const { user } = useAuth();
const { site } = useSite();
const [models, setModels] = useState([]);
const [packages, setPackages] = useState([]);
useEffect(() => {
getSiteModels().then(r => { if (r.data.success) setModels(r.data.data || []); }).catch(() => {});
getSitePackages().then(r => { if (r.data.success) setPackages(r.data.data || []); }).catch(() => {});
}, []);
const enabledModels = models.filter(m => m.enabled !== false);
return (
<div className="relative">
{/* Hero Section — clean and minimal */}
<section className="max-w-5xl mx-auto px-6 pt-32 pb-24">
<FadeContent blur duration={800} delay={100}>
<div className="max-w-3xl">
<h1 className="text-5xl md:text-6xl font-heading font-bold text-white leading-[1.1] tracking-tight">
{site?.name || t('home.defaultHeroTitle')}
</h1>
<p className="text-lg text-neutral-400 mt-6 leading-relaxed max-w-xl">
{t('home.heroSubtitle')}
</p>
<div className="flex items-center gap-4 mt-10">
{user ? (
<Link to="/dashboard" className="px-7 py-3 rounded-lg bg-white text-neutral-900 font-medium text-sm hover:bg-neutral-200 transition-colors">
{t('home.goToDashboard')} →
</Link>
) : (
<>
<Link to="/register" className="px-7 py-3 rounded-lg bg-white text-neutral-900 font-medium text-sm hover:bg-neutral-200 transition-colors">
{t('home.getStarted')} →
</Link>
<Link to="/pricing" className="px-7 py-3 text-neutral-400 font-medium text-sm hover:text-white transition-colors">
{t('home.viewPricing')}
</Link>
</>
)}
</div>
</div>
</FadeContent>
{/* Stats — simple inline */}
<FadeContent blur duration={800} delay={400}>
<div className="flex items-center gap-12 mt-20 border-t border-neutral-800 pt-10">
<div>
<div className="text-2xl font-bold text-white">
<CountUp from={0} to={enabledModels.length || 50} duration={2} />+
</div>
<p className="text-sm text-neutral-500 mt-0.5">{t('home.aiModels')}</p>
</div>
<div>
<div className="text-2xl font-bold text-white">
<CountUp from={0} to={99.9} duration={2.5} />%
</div>
<p className="text-sm text-neutral-500 mt-0.5">{t('home.uptime')}</p>
</div>
<div>
<div className="text-2xl font-bold text-white">
&lt;<CountUp from={200} to={50} duration={2} direction="down" />ms
</div>
<p className="text-sm text-neutral-500 mt-0.5">{t('home.latency')}</p>
</div>
</div>
</FadeContent>
</section>
{/* Features — two-column layout */}
<section className="max-w-5xl mx-auto px-6 py-20">
<FadeContent blur duration={800} delay={100}>
<h2 className="text-2xl font-heading font-bold text-white mb-2">{t('home.whyChooseUs')}</h2>
<p className="text-neutral-500 mb-10">{t('home.whyChooseUsDesc')}</p>
<div className="space-y-6">
{[
{ title: t('home.lightningFast'), desc: t('home.lightningFastDesc') },
{ title: t('home.securePrivate'), desc: t('home.securePrivateDesc') },
{ title: t('home.payAsYouGo'), desc: t('home.payAsYouGoDesc') },
].map((f, i) => (
<div key={i} className="flex gap-6 items-start py-6 border-b border-neutral-800/60 last:border-0 group">
<span className="text-sm font-mono text-neutral-600 mt-0.5">0{i + 1}</span>
<div>
<h3 className="text-base font-semibold text-white mb-1 group-hover:text-neutral-300 transition-colors">{f.title}</h3>
<p className="text-sm text-neutral-500 leading-relaxed max-w-lg">{f.desc}</p>
</div>
</div>
))}
</div>
</FadeContent>
</section>
{/* Models — simple list */}
{enabledModels.length > 0 && (
<section className="max-w-5xl mx-auto px-6 py-20">
<FadeContent blur duration={800} delay={100}>
<div className="flex items-end justify-between mb-8">
<div>
<h2 className="text-2xl font-heading font-bold text-white mb-2">{t('home.availableModels')}</h2>
<p className="text-neutral-500">{t('home.availableModelsDesc', { count: enabledModels.length })}</p>
</div>
{enabledModels.length > 8 && (
<Link to="/pricing" className="text-sm text-neutral-400 hover:text-white transition-colors">
{t('home.viewAllModels', { count: enabledModels.length })} →
</Link>
)}
</div>
<div className="grid grid-cols-2 md:grid-cols-4 gap-2">
{enabledModels.slice(0, 8).map((m, i) => (
<div key={m.id || i} className="px-4 py-3 rounded-lg border border-neutral-800/60 hover:border-neutral-700 transition-colors">
<span className="text-sm text-neutral-300 font-mono">{m.display_name || m.model_name}</span>
</div>
))}
</div>
</FadeContent>
</section>
)}
{/* Packages — clean cards */}
{packages.length > 0 && (
<section className="max-w-5xl mx-auto px-6 py-20">
<FadeContent blur duration={800} delay={100}>
<h2 className="text-2xl font-heading font-bold text-white mb-2">{t('home.plansPackages')}</h2>
<p className="text-neutral-500 mb-10">{t('home.choosePlan')}</p>
<div className="grid md:grid-cols-3 gap-4 max-w-4xl">
{packages.filter(p => p.enabled).slice(0, 3).map((pkg) => (
<div key={pkg.id} className="rounded-xl p-6 flex flex-col border border-neutral-800/60 hover:border-neutral-700 transition-colors">
<h3 className="text-base font-semibold text-white">{pkg.name}</h3>
{pkg.description && <p className="text-sm text-neutral-500 mt-1">{pkg.description}</p>}
<div className="mt-auto pt-6">
<span className="text-3xl font-bold text-white">${pkg.price}</span>
{pkg.original_price > pkg.price && (
<span className="text-sm text-neutral-600 line-through ml-2">${pkg.original_price}</span>
)}
{pkg.duration > 0 && <p className="text-xs text-neutral-600 mt-1">{t('home.days', { count: pkg.duration })}</p>}
</div>
<Link to={user ? '/packages' : '/register'} className="mt-4 py-2.5 rounded-lg bg-neutral-800 text-white font-medium text-sm text-center hover:bg-neutral-700 transition-colors">
{user ? t('home.subscribe') : t('home.getStarted')}
</Link>
</div>
))}
</div>
</FadeContent>
</section>
)}
{/* CTA — simple */}
<section className="max-w-5xl mx-auto px-6 py-24">
<FadeContent blur duration={800} delay={100}>
<div className="border-t border-neutral-800 pt-16 text-center">
<h2 className="text-2xl font-heading font-bold text-white mb-3">{t('home.readyToStart')}</h2>
<p className="text-neutral-500 mb-8 max-w-md mx-auto">{t('home.readyToStartDesc')}</p>
<div className="flex items-center justify-center gap-4">
{user ? (
<Link to="/dashboard" className="px-7 py-3 rounded-lg bg-white text-neutral-900 font-medium text-sm hover:bg-neutral-200 transition-colors">
{t('home.goToDashboard')} →
</Link>
) : (
<>
<Link to="/register" className="px-7 py-3 rounded-lg bg-white text-neutral-900 font-medium text-sm hover:bg-neutral-200 transition-colors">
{t('home.createFreeAccount')} →
</Link>
<Link to="/login" className="px-7 py-3 text-neutral-400 font-medium text-sm hover:text-white transition-colors">
{t('home.signIn')}
</Link>
</>
)}
</div>
</div>
</FadeContent>
</section>
</div>
);
}
+120
View File
@@ -0,0 +1,120 @@
import React, { useState } from 'react';
import { Outlet, Link, useNavigate, useLocation } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { useAuth } from '../../context/AuthContext';
import { useSite } from '../../context/SiteContext';
export default function MinimalLayout() {
const { t } = useTranslation();
const { user, logout } = useAuth();
const { site } = useSite();
const navigate = useNavigate();
const location = useLocation();
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
const siteName = site?.name || 'AI Platform';
const navItems = [
{ to: '/', label: t('nav.home'), auth: false },
{ to: '/pricing', label: t('nav.pricing'), auth: false },
{ to: '/packages', label: t('nav.packages'), auth: false },
{ to: '/dashboard', label: t('nav.dashboard'), auth: true },
{ to: '/tokens', label: t('nav.apiKeys'), auth: true },
];
const visibleNavItems = navItems.filter((n) => !n.auth || user);
return (
<div className="min-h-screen flex flex-col bg-neutral-950 text-white">
{/* Announcement */}
{site?.announcement && (
<div className="border-b border-neutral-800/60 text-center py-2 px-4">
<p className="text-sm text-neutral-400">{site.announcement}</p>
</div>
)}
{/* Header — minimal */}
<header className="sticky top-0 z-50 backdrop-blur-lg bg-neutral-950/80 border-b border-neutral-800/40">
<div className="max-w-5xl mx-auto px-6 h-14 flex items-center justify-between">
<Link to="/" className="flex items-center gap-2.5 group">
{site?.logo ? (
<img src={site.logo} alt={siteName} className="h-7 w-auto" />
) : (
<span className="text-base font-semibold text-white">{siteName}</span>
)}
{site?.logo && (
<span className="text-base font-semibold text-white">{siteName}</span>
)}
</Link>
<nav className="hidden md:flex items-center gap-6">
{visibleNavItems.map((n) => (
<Link
key={n.to}
to={n.to}
className={`text-sm transition-colors ${
location.pathname === n.to
? 'text-white'
: 'text-neutral-500 hover:text-white'
}`}
>
{n.label}
</Link>
))}
</nav>
<div className="flex items-center gap-4">
{user ? (
<div className="flex items-center gap-3">
<span className="text-sm text-neutral-500 hidden sm:block">{user.display_name || user.username}</span>
<button onClick={async () => { await logout(); navigate('/'); }} className="text-sm text-neutral-500 hover:text-white transition-colors">
{t('nav.logout')}
</button>
</div>
) : (
<>
<Link to="/login" className="text-sm text-neutral-500 hover:text-white transition-colors hidden sm:block">
{t('nav.login')}
</Link>
<Link to="/register" className="px-4 py-1.5 rounded-lg bg-white text-neutral-900 text-sm font-medium hover:bg-neutral-200 transition-colors">
{t('nav.signUp')}
</Link>
</>
)}
<button className="md:hidden p-1" onClick={() => setMobileMenuOpen(!mobileMenuOpen)}>
<svg className="w-5 h-5 text-neutral-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
{mobileMenuOpen ? <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M6 18L18 6M6 6l12 12" /> : <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M4 6h16M4 12h16M4 18h16" />}
</svg>
</button>
</div>
</div>
{mobileMenuOpen && (
<div className="md:hidden border-t border-neutral-800/40 bg-neutral-950/95">
<nav className="max-w-5xl mx-auto px-6 py-3 flex flex-col gap-1">
{visibleNavItems.map((n) => (
<Link key={n.to} to={n.to} onClick={() => setMobileMenuOpen(false)}
className={`px-3 py-2 text-sm rounded transition-colors ${location.pathname === n.to ? 'text-white' : 'text-neutral-500 hover:text-white'}`}>
{n.label}
</Link>
))}
</nav>
</div>
)}
</header>
<main className="flex-1"><Outlet /></main>
<footer className="border-t border-neutral-800/40 mt-auto">
<div className="max-w-5xl mx-auto px-6 py-6 flex flex-col md:flex-row items-center justify-between gap-3">
<p className="text-xs text-neutral-600">&copy; {new Date().getFullYear()} {siteName}</p>
{site?.contact_email && (
<a href={`mailto:${site.contact_email}`} className="text-xs text-neutral-600 hover:text-white transition-colors">
{t('nav.contact')}
</a>
)}
</div>
</footer>
</div>
);
}
+243
View File
@@ -0,0 +1,243 @@
import React, { useState, useEffect } from 'react';
import { Link } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { useAuth } from '../../context/AuthContext';
import { useSite } from '../../context/SiteContext';
import { getSiteModels, getSitePackages } from '../../api';
import GradientText from '../../components/bits/GradientText';
import BlurText from '../../components/bits/BlurText';
import SpotlightCard from '../../components/bits/SpotlightCard';
import CountUp from '../../components/bits/CountUp';
import StarBorder from '../../components/bits/StarBorder';
import ShinyText from '../../components/bits/ShinyText';
export default function StarterHome() {
const { t } = useTranslation();
const { user } = useAuth();
const { site } = useSite();
const [models, setModels] = useState([]);
const [packages, setPackages] = useState([]);
useEffect(() => {
getSiteModels().then(r => { if (r.data.success) setModels(r.data.data || []); }).catch(() => {});
getSitePackages().then(r => { if (r.data.success) setPackages(r.data.data || []); }).catch(() => {});
}, []);
const enabledModels = models.filter(m => m.enabled !== false);
return (
<div className="relative">
{/* Gradient Background */}
<div className="absolute inset-0 overflow-hidden pointer-events-none">
<div className="absolute top-0 left-1/2 -translate-x-1/2 w-[800px] h-[600px] bg-gradient-radial from-brand-600/20 via-purple-600/5 to-transparent blur-3xl" />
<div className="absolute top-[400px] right-0 w-[500px] h-[500px] bg-gradient-radial from-pink-600/10 to-transparent blur-3xl" />
</div>
{/* Hero Section */}
<section className="relative max-w-7xl mx-auto px-6 pt-24 pb-20">
<div className="text-center max-w-4xl mx-auto">
<div className="mb-6">
<GradientText
colors={['#818cf8', '#c084fc', '#f472b6', '#818cf8']}
animationSpeed={6}
className="text-5xl md:text-7xl font-heading font-extrabold leading-tight"
>
{site?.name || t('home.defaultHeroTitle')}
</GradientText>
</div>
<BlurText
text={t('home.heroSubtitle')}
className="text-lg md:text-xl text-neutral-400 max-w-2xl mx-auto mb-10"
delay={50}
animateBy="words"
/>
<div className="flex items-center justify-center gap-4">
{user ? (
<Link to="/dashboard">
<StarBorder as="div" color="#818cf8" speed="5s">
{t('home.goToDashboard')}
</StarBorder>
</Link>
) : (
<>
<Link to="/register" className="btn-primary text-base !px-8 !py-3">
{t('home.getStarted')}
</Link>
<Link to="/pricing" className="btn-secondary text-base !px-8 !py-3">
{t('home.viewPricing')}
</Link>
</>
)}
</div>
</div>
{/* Stats */}
<div className="grid grid-cols-3 gap-6 max-w-xl mx-auto mt-20">
<div className="text-center">
<div className="text-3xl font-bold text-white">
<CountUp from={0} to={enabledModels.length || 50} duration={2} separator="" />+
</div>
<p className="text-sm text-neutral-500 mt-1">{t('home.aiModels')}</p>
</div>
<div className="text-center">
<div className="text-3xl font-bold text-white">
<CountUp from={0} to={99.9} duration={2.5} />%
</div>
<p className="text-sm text-neutral-500 mt-1">{t('home.uptime')}</p>
</div>
<div className="text-center">
<div className="text-3xl font-bold text-white">
&lt;<CountUp from={200} to={50} duration={2} direction="down" />ms
</div>
<p className="text-sm text-neutral-500 mt-1">{t('home.latency')}</p>
</div>
</div>
</section>
{/* Features Section */}
<section className="relative max-w-7xl mx-auto px-6 py-20">
<div className="text-center mb-12">
<h2 className="text-3xl font-heading font-bold text-white mb-3">{t('home.whyChooseUs')}</h2>
<p className="text-neutral-400">{t('home.whyChooseUsDesc')}</p>
</div>
<div className="grid md:grid-cols-3 gap-6">
<SpotlightCard className="!bg-neutral-900/60 !border-neutral-800/60" spotlightColor="rgba(129,140,248,0.15)">
<div className="w-12 h-12 rounded-xl bg-brand-500/10 flex items-center justify-center mb-4">
<svg className="w-6 h-6 text-brand-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg>
</div>
<h3 className="text-lg font-semibold text-white mb-2">{t('home.lightningFast')}</h3>
<p className="text-sm text-neutral-400 leading-relaxed">{t('home.lightningFastDesc')}</p>
</SpotlightCard>
<SpotlightCard className="!bg-neutral-900/60 !border-neutral-800/60" spotlightColor="rgba(192,132,252,0.15)">
<div className="w-12 h-12 rounded-xl bg-purple-500/10 flex items-center justify-center mb-4">
<svg className="w-6 h-6 text-purple-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
</svg>
</div>
<h3 className="text-lg font-semibold text-white mb-2">{t('home.securePrivate')}</h3>
<p className="text-sm text-neutral-400 leading-relaxed">{t('home.securePrivateDesc')}</p>
</SpotlightCard>
<SpotlightCard className="!bg-neutral-900/60 !border-neutral-800/60" spotlightColor="rgba(244,114,182,0.15)">
<div className="w-12 h-12 rounded-xl bg-pink-500/10 flex items-center justify-center mb-4">
<svg className="w-6 h-6 text-pink-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
<h3 className="text-lg font-semibold text-white mb-2">{t('home.payAsYouGo')}</h3>
<p className="text-sm text-neutral-400 leading-relaxed">{t('home.payAsYouGoDesc')}</p>
</SpotlightCard>
</div>
</section>
{/* Models Preview */}
{enabledModels.length > 0 && (
<section className="relative max-w-7xl mx-auto px-6 py-20">
<div className="text-center mb-12">
<h2 className="text-3xl font-heading font-bold text-white mb-3">{t('home.availableModels')}</h2>
<p className="text-neutral-400">
{t('home.availableModelsDesc', { count: enabledModels.length })}
</p>
</div>
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3">
{enabledModels.slice(0, 12).map((m, i) => (
<div
key={m.id || i}
className="glass-sm px-4 py-3 flex items-center gap-3 hover:bg-white/[0.06] transition-colors group"
>
<div className="w-2 h-2 rounded-full bg-green-500 flex-shrink-0" />
<span className="text-sm font-mono text-neutral-300 group-hover:text-white transition-colors truncate">
{m.display_name || m.model_name}
</span>
</div>
))}
</div>
{enabledModels.length > 12 && (
<div className="text-center mt-6">
<Link to="/pricing" className="text-sm text-brand-400 hover:text-brand-300 transition-colors">
{t('home.viewAllModels', { count: enabledModels.length })} &rarr;
</Link>
</div>
)}
</section>
)}
{/* Packages Preview */}
{packages.length > 0 && (
<section className="relative max-w-7xl mx-auto px-6 py-20">
<div className="text-center mb-12">
<h2 className="text-3xl font-heading font-bold text-white mb-3">{t('home.plansPackages')}</h2>
<p className="text-neutral-400">{t('home.choosePlan')}</p>
</div>
<div className="grid md:grid-cols-3 gap-6 max-w-4xl mx-auto">
{packages.filter(p => p.enabled).slice(0, 3).map((pkg) => (
<div key={pkg.id} className="glass rounded-2xl p-6 flex flex-col hover:border-brand-500/30 transition-colors">
<h3 className="text-lg font-semibold text-white mb-1">{pkg.name}</h3>
{pkg.description && (
<p className="text-sm text-neutral-400 mb-4">{pkg.description}</p>
)}
<div className="mt-auto pt-4">
<div className="flex items-baseline gap-1">
<span className="text-3xl font-bold text-white">${pkg.price}</span>
{pkg.original_price > pkg.price && (
<span className="text-sm text-neutral-500 line-through">${pkg.original_price}</span>
)}
</div>
{pkg.duration > 0 && (
<p className="text-xs text-neutral-500 mt-1">{t('home.days', { count: pkg.duration })}</p>
)}
</div>
<Link
to={user ? '/packages' : '/register'}
className="btn-primary text-center mt-4 !py-2"
>
{user ? t('home.subscribe') : t('home.getStarted')}
</Link>
</div>
))}
</div>
</section>
)}
{/* CTA Section */}
<section className="relative max-w-7xl mx-auto px-6 py-24">
<div className="glass rounded-3xl p-12 text-center relative overflow-hidden">
<div className="absolute inset-0 bg-gradient-to-br from-brand-600/10 via-transparent to-purple-600/10" />
<div className="relative z-10">
<h2 className="text-3xl font-heading font-bold text-white mb-4">
{t('home.readyToStart')}
</h2>
<p className="text-neutral-400 mb-8 max-w-md mx-auto">
{t('home.readyToStartDesc')}
</p>
<div className="flex items-center justify-center gap-4">
{user ? (
<Link to="/dashboard" className="btn-primary text-base !px-8 !py-3">
{t('home.goToDashboard')}
</Link>
) : (
<>
<Link to="/register" className="btn-primary text-base !px-8 !py-3">
{t('home.createFreeAccount')}
</Link>
<Link to="/login" className="btn-secondary text-base !px-8 !py-3">
{t('home.signIn')}
</Link>
</>
)}
</div>
</div>
</div>
</section>
</div>
);
}
+173
View File
@@ -0,0 +1,173 @@
import React, { useState } from 'react';
import { Outlet, Link, useNavigate, useLocation } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { useAuth } from '../../context/AuthContext';
import { useSite } from '../../context/SiteContext';
export default function StarterLayout() {
const { t } = useTranslation();
const { user, logout } = useAuth();
const { site } = useSite();
const navigate = useNavigate();
const location = useLocation();
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
const siteName = site?.name || 'AI Platform';
const navItems = [
{ to: '/', label: t('nav.home'), auth: false },
{ to: '/pricing', label: t('nav.pricing'), auth: false },
{ to: '/packages', label: t('nav.packages'), auth: false },
{ to: '/dashboard', label: t('nav.dashboard'), auth: true },
{ to: '/tokens', label: t('nav.apiKeys'), auth: true },
];
const visibleNavItems = navItems.filter((n) => !n.auth || user);
return (
<div className="min-h-screen flex flex-col">
{/* Announcement Banner */}
{site?.announcement && (
<div className="bg-brand-600/20 border-b border-brand-500/20 text-center py-2 px-4">
<p className="text-sm text-brand-200">{site.announcement}</p>
</div>
)}
{/* Header */}
<header className="sticky top-0 z-50 backdrop-blur-xl bg-black/60 border-b border-white/[0.06]">
<div className="max-w-7xl mx-auto px-6 h-16 flex items-center justify-between">
{/* Logo */}
<Link to="/" className="flex items-center gap-3 group">
{site?.logo ? (
<img src={site.logo} alt={siteName} className="h-8 w-auto" />
) : (
<div className="w-8 h-8 rounded-lg bg-gradient-to-br from-brand-500 to-purple-600 flex items-center justify-center text-white font-bold text-sm">
{siteName.charAt(0)}
</div>
)}
<span className="text-lg font-semibold text-white group-hover:text-brand-400 transition-colors">
{siteName}
</span>
</Link>
{/* Desktop Nav */}
<nav className="hidden md:flex items-center gap-1">
{visibleNavItems.map((n) => (
<NavLink key={n.to} to={n.to} active={location.pathname === n.to}>
{n.label}
</NavLink>
))}
</nav>
{/* Auth + Mobile toggle */}
<div className="flex items-center gap-3">
{user ? (
<div className="flex items-center gap-3">
<span className="text-sm text-neutral-400 hidden sm:block">
{user.display_name || user.username}
</span>
<button
onClick={async () => { await logout(); navigate('/'); }}
className="text-sm text-neutral-400 hover:text-white transition-colors"
>
{t('nav.logout')}
</button>
</div>
) : (
<>
<Link to="/login" className="text-sm text-neutral-300 hover:text-white transition-colors px-3 py-1.5 hidden sm:block">
{t('nav.login')}
</Link>
<Link to="/register" className="btn-primary text-sm !px-4 !py-1.5">
{t('nav.signUp')}
</Link>
</>
)}
{/* Mobile hamburger */}
<button
className="md:hidden p-1.5 rounded-lg hover:bg-white/5 transition-colors"
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
aria-label="Toggle menu"
>
<svg className="w-5 h-5 text-neutral-300" fill="none" viewBox="0 0 24 24" stroke="currentColor">
{mobileMenuOpen ? (
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
) : (
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
)}
</svg>
</button>
</div>
</div>
{/* Mobile Nav Dropdown */}
{mobileMenuOpen && (
<div className="md:hidden border-t border-white/[0.06] bg-black/90 backdrop-blur-xl">
<nav className="max-w-7xl mx-auto px-6 py-3 flex flex-col gap-1">
{visibleNavItems.map((n) => (
<Link
key={n.to}
to={n.to}
onClick={() => setMobileMenuOpen(false)}
className={`px-3 py-2 text-sm rounded-lg transition-colors ${
location.pathname === n.to
? 'text-brand-400 bg-brand-500/10'
: 'text-neutral-400 hover:text-white hover:bg-white/5'
}`}
>
{n.label}
</Link>
))}
{!user && (
<Link
to="/login"
onClick={() => setMobileMenuOpen(false)}
className="px-3 py-2 text-sm text-neutral-400 hover:text-white rounded-lg hover:bg-white/5 transition-colors sm:hidden"
>
{t('nav.login')}
</Link>
)}
</nav>
</div>
)}
</header>
{/* Content */}
<main className="flex-1">
<Outlet />
</main>
{/* Footer */}
<footer className="border-t border-white/[0.06] mt-auto">
<div className="max-w-7xl mx-auto px-6 py-8 flex flex-col md:flex-row items-center justify-between gap-4">
<p className="text-sm text-neutral-500">
&copy; {new Date().getFullYear()} {siteName}.
</p>
<div className="flex items-center gap-4">
{site?.contact_email && (
<a href={`mailto:${site.contact_email}`} className="text-sm text-neutral-500 hover:text-neutral-300 transition-colors">
{t('nav.contact')}
</a>
)}
</div>
</div>
</footer>
</div>
);
}
function NavLink({ to, children, active }) {
return (
<Link
to={to}
className={`px-3 py-1.5 text-sm rounded-lg transition-all ${
active
? 'text-brand-400 bg-brand-500/10'
: 'text-neutral-400 hover:text-white hover:bg-white/5'
}`}
>
{children}
</Link>
);
}