feat: add language switch button and optimize theme visuals

- Add LanguageSwitch component to all 4 theme layouts (starter/default/dark/minimal)
- Import Noto Sans SC for proper Chinese font rendering
- Fix theme flash on refresh by blocking render until site info loads
- Optimize default theme: violet/blue/cyan palette, SVG icons instead of emoji
- Optimize dark theme: unified emerald accent, reduced particle count, backdrop-blur
- Add Tailwind safelist for dark theme dynamic class names
- Prioritize localStorage in i18n detection so manual language choice persists

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
abingyydsandClaude Sonnet 4.6 committed 2026-03-21 10:19:38 +09:00
1 parent 9a18cf2215
commit 662685cc97
43 files changed
+420 -316

No files matched your search

+25
View File
@@ -0,0 +1,25 @@
import { useTranslation } from 'react-i18next';
export default function LanguageSwitch({ className = '' }) {
const { i18n } = useTranslation();
const isZh = i18n.language?.startsWith('zh');
const toggle = () => {
i18n.changeLanguage(isZh ? 'en' : 'zh');
};
return (
<button
onClick={toggle}
className={`flex items-center gap-1.5 px-2.5 py-1 rounded-md text-xs transition-colors ${className}`}
aria-label="Switch language"
>
<svg className="w-3.5 h-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.5}>
<path strokeLinecap="round" strokeLinejoin="round" d="M12 21a9 9 0 1 0 0-18 9 9 0 0 0 0 18Z" />
<path strokeLinecap="round" strokeLinejoin="round" d="M3.6 9h16.8M3.6 15h16.8" />
<path strokeLinecap="round" strokeLinejoin="round" d="M12 3a15.3 15.3 0 0 1 4 9 15.3 15.3 0 0 1-4 9 15.3 15.3 0 0 1-4-9 15.3 15.3 0 0 1 4-9Z" />
</svg>
<span>{isZh ? 'EN' : '中文'}</span>
</button>
);
}
+26 -1
View File
@@ -23,10 +23,35 @@ const themeRegistry = {
},
};
// Full-screen loading spinner shown while site info is being fetched
// Prevents theme flash (rendering default theme before API returns the real one)
function ThemeLoading() {
return (
<div className="flex items-center justify-center min-h-screen bg-gray-950">
<div className="w-8 h-8 border-2 border-white/20 border-t-white/70 rounded-full animate-spin" />
</div>
);
}
export function ThemeProvider({ children }) {
const { site } = useSite();
const { site, loading } = useSite();
// Block rendering until site info is loaded so we know the correct theme
if (loading) {
return <ThemeLoading />;
}
const themeName = site?.theme_template || 'starter';
return (
<ThemeInner themeName={themeName}>
{children}
</ThemeInner>
);
}
// Inner component to keep useMemo stable after loading completes
function ThemeInner({ themeName, children }) {
const theme = useMemo(() => {
const t = themeRegistry[themeName] || themeRegistry.starter;
return { name: themeName, ...t };
+1 -1
View File
@@ -13,7 +13,7 @@ i18n
fallbackLng: 'en',
interpolation: { escapeValue: false },
detection: {
order: ['navigator', 'htmlTag', 'querystring'],
order: ['localStorage', 'navigator', 'htmlTag', 'querystring'],
caches: ['localStorage'],
},
});
+64 -49
View File
@@ -27,10 +27,10 @@ export default function DarkHome() {
{/* Particles Background */}
<div className="absolute inset-0 h-[800px] overflow-hidden pointer-events-none">
<Particles
particleColors={['#00ff88', '#00ccff', '#ff0055']}
particleCount={80}
particleColors={['#10b981', '#06b6d4', '#8b5cf6']}
particleCount={60}
particleSpread={15}
speed={0.3}
speed={0.2}
particleBaseSize={80}
moveParticlesOnHover={true}
alphaParticles={false}
@@ -41,38 +41,39 @@ export default function DarkHome() {
{/* 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>
{/* Terminal badge */}
<div className="inline-flex items-center gap-2 px-4 py-1.5 rounded-full border border-emerald-500/20 bg-emerald-500/5 mb-8">
<span className="w-2 h-2 rounded-full bg-emerald-500 animate-pulse" />
<span className="text-xs font-mono text-emerald-400/80 uppercase tracking-wider">System Online</span>
</div>
<h1 className="text-5xl md:text-7xl font-heading font-extrabold text-white leading-tight mb-4">
<h1 className="text-5xl md:text-7xl font-extrabold text-white leading-tight mb-6 tracking-tight">
<DecryptedText
text={site?.name || t('home.defaultHeroTitle')}
speed={60}
maxIterations={10}
revealDirection="start"
className="text-white"
encryptedClassName="text-green-500/40"
encryptedClassName="text-emerald-500/30"
/>
</h1>
<p className="text-lg md:text-xl text-neutral-400 max-w-2xl mx-auto mb-10 font-mono">
{'> '}{t('home.heroSubtitle')}
<p className="text-lg md:text-xl text-neutral-400 max-w-2xl mx-auto mb-10 leading-relaxed">
<span className="text-emerald-400/60 font-mono">{'> '}</span>
{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">
<Link to="/dashboard" className="px-8 py-3.5 rounded-lg bg-emerald-500 text-black font-bold text-base hover:bg-emerald-400 transition-all font-mono shadow-lg shadow-emerald-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">
<Link to="/register" className="px-8 py-3.5 rounded-lg bg-emerald-500 text-black font-bold text-base hover:bg-emerald-400 transition-all font-mono shadow-lg shadow-emerald-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">
<Link to="/pricing" className="px-8 py-3.5 rounded-lg border border-emerald-500/20 text-emerald-400 font-medium text-base hover:bg-emerald-500/10 transition-all font-mono">
{t('home.viewPricing')}
</Link>
</>
@@ -80,18 +81,18 @@ export default function DarkHome() {
</div>
</div>
{/* Stats — terminal style */}
{/* Stats */}
<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' },
{ label: t('home.aiModels'), value: enabledModels.length || 50, suffix: '+', color: 'text-emerald-400', border: 'border-emerald-500/15' },
{ label: t('home.uptime'), value: 99.9, suffix: '%', color: 'text-cyan-400', border: 'border-cyan-500/15' },
{ label: t('home.latency'), value: 50, suffix: 'ms', prefix: '<', color: 'text-violet-400', border: 'border-violet-500/15' },
].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]}`}>
<div key={i} className={`text-center p-5 rounded-lg bg-[#030712]/60 border ${s.border} font-mono backdrop-blur-sm`}>
<div className={`text-3xl font-bold ${s.color}`}>
{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>
<p className="text-xs text-neutral-500 mt-1.5 uppercase tracking-wider">{s.label}</p>
</div>
))}
</div>
@@ -100,21 +101,35 @@ export default function DarkHome() {
{/* 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>
<h2 className="text-3xl font-bold text-white mb-3">{t('home.whyChooseUs')}</h2>
<p className="text-neutral-500">{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' },
{ icon: (
<svg className="w-5 h-5 text-emerald-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>
), title: t('home.lightningFast'), desc: t('home.lightningFastDesc'), accent: 'emerald' },
{ icon: (
<svg className="w-5 h-5 text-cyan-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>
), title: t('home.securePrivate'), desc: t('home.securePrivateDesc'), accent: 'cyan' },
{ icon: (
<svg className="w-5 h-5 text-violet-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>
), title: t('home.payAsYouGo'), desc: t('home.payAsYouGoDesc'), accent: 'violet' },
].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>
<div key={i} className={`p-6 rounded-xl bg-[#030712]/60 border border-${f.accent}-500/10 hover:border-${f.accent}-500/25 transition-all group backdrop-blur-sm`}>
<div className={`w-10 h-10 rounded-lg bg-${f.accent}-500/10 flex items-center justify-center mb-4`}>
{f.icon}
</div>
<h3 className="text-lg font-semibold text-white mb-2">{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 className={`h-px w-0 group-hover:w-full bg-gradient-to-r from-${f.accent}-500/50 to-transparent transition-all duration-500 mt-4`} />
</div>
))}
</div>
@@ -124,15 +139,15 @@ export default function DarkHome() {
{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>
<h2 className="text-3xl font-bold text-white mb-3">{t('home.availableModels')}</h2>
<p className="text-neutral-500">{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">
<div key={m.id || i} className="px-4 py-3 rounded-lg bg-[#030712]/60 border border-emerald-500/[0.08] flex items-center gap-3 hover:border-emerald-500/20 transition-colors group font-mono backdrop-blur-sm">
<span className="text-emerald-500/60 text-xs">$</span>
<span className="text-sm text-neutral-400 group-hover:text-emerald-400 transition-colors truncate">
{m.display_name || m.model_name}
</span>
</div>
@@ -141,7 +156,7 @@ export default function DarkHome() {
{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">
<Link to="/pricing" className="text-sm text-emerald-400 hover:text-emerald-300 transition-colors font-mono">
{t('home.viewAllModels', { count: enabledModels.length })} &rarr;
</Link>
</div>
@@ -153,23 +168,23 @@ export default function DarkHome() {
{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>
<h2 className="text-3xl font-bold text-white mb-3">{t('home.plansPackages')}</h2>
<p className="text-neutral-500">{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>
<div key={pkg.id} className="rounded-xl p-6 flex flex-col bg-[#030712]/60 border border-emerald-500/[0.08] hover:border-emerald-500/20 transition-all backdrop-blur-sm">
<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">
<span className="text-3xl font-bold text-green-400 font-mono">${pkg.price}</span>
<span className="text-3xl font-bold text-emerald-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>}
{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-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">
<Link to={user ? '/packages' : '/register'} className="mt-4 py-2.5 rounded-lg bg-emerald-500/10 border border-emerald-500/20 text-emerald-400 font-medium text-sm text-center hover:bg-emerald-500/15 transition-colors font-mono">
{user ? t('home.subscribe') : t('home.getStarted')}
</Link>
</div>
@@ -180,22 +195,22 @@ export default function DarkHome() {
{/* 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="rounded-xl p-12 text-center bg-[#030712]/60 border border-emerald-500/[0.08] relative overflow-hidden backdrop-blur-sm">
<div className="absolute inset-0 bg-gradient-to-r from-emerald-500/[0.03] via-transparent to-cyan-500/[0.03]" />
<div className="relative z-10">
<h2 className="text-3xl font-heading font-bold text-white mb-4 font-mono">{t('home.readyToStart')}</h2>
<h2 className="text-3xl 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="px-8 py-3 rounded-lg bg-green-500 text-black font-bold hover:bg-green-400 transition-all font-mono">
<Link to="/dashboard" className="px-8 py-3 rounded-lg bg-emerald-500 text-black font-bold hover:bg-emerald-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">
<Link to="/register" className="px-8 py-3 rounded-lg bg-emerald-500 text-black font-bold hover:bg-emerald-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">
<Link to="/login" className="px-8 py-3 rounded-lg border border-emerald-500/20 text-emerald-400 font-medium hover:bg-emerald-500/10 transition-all font-mono">
{t('home.signIn')}
</Link>
</>
+21 -18
View File
@@ -3,6 +3,7 @@ import { Outlet, Link, useNavigate, useLocation } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { useAuth } from '../../context/AuthContext';
import { useSite } from '../../context/SiteContext';
import LanguageSwitch from '../../components/LanguageSwitch';
export default function DarkLayout() {
const { t } = useTranslation();
@@ -25,26 +26,26 @@ export default function DarkLayout() {
const visibleNavItems = navItems.filter((n) => !n.auth || user);
return (
<div className="min-h-screen flex flex-col bg-black text-white">
<div className="min-h-screen flex flex-col bg-[#030712] 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 className="bg-emerald-500/5 border-b border-emerald-500/10 text-center py-2 px-4">
<p className="text-sm text-emerald-300/80 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">
{/* Header */}
<header className="sticky top-0 z-50 backdrop-blur-xl bg-[#030712]/80 border-b border-emerald-500/[0.08]">
<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">
<div className="w-8 h-8 rounded bg-emerald-500/10 border border-emerald-500/20 flex items-center justify-center text-emerald-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">
<span className="text-base font-semibold text-emerald-400 font-mono group-hover:text-emerald-300 transition-colors tracking-wide">
{siteName}
</span>
</Link>
@@ -56,8 +57,8 @@ export default function DarkLayout() {
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'
? 'text-emerald-400 bg-emerald-500/10'
: 'text-neutral-500 hover:text-emerald-400'
}`}
>
{n.label}
@@ -65,26 +66,28 @@ export default function DarkLayout() {
))}
</nav>
<div className="flex items-center gap-3">
<div className="flex items-center gap-2">
<LanguageSwitch className="text-neutral-500 hover:text-emerald-400 hover:bg-emerald-500/5 font-mono" />
{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">
<button onClick={async () => { await logout(); navigate('/'); }} className="text-sm text-neutral-500 hover:text-emerald-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">
<Link to="/login" className="text-sm text-neutral-500 hover:text-emerald-400 font-mono transition-colors hidden sm:block px-3 py-1.5">
{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">
<Link to="/register" className="px-4 py-1.5 rounded bg-emerald-500/10 border border-emerald-500/20 text-emerald-400 text-sm font-mono hover:bg-emerald-500/15 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">
<svg className="w-5 h-5 text-emerald-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>
@@ -92,11 +95,11 @@ export default function DarkLayout() {
</div>
{mobileMenuOpen && (
<div className="md:hidden border-t border-green-500/10 bg-black/95 backdrop-blur-xl">
<div className="md:hidden border-t border-emerald-500/[0.08] bg-[#030712]/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'}`}>
className={`px-3 py-2 text-sm font-mono rounded transition-colors ${location.pathname === n.to ? 'text-emerald-400 bg-emerald-500/10' : 'text-neutral-500 hover:text-emerald-400'}`}>
{n.label}
</Link>
))}
@@ -107,11 +110,11 @@ export default function DarkLayout() {
<main className="flex-1"><Outlet /></main>
<footer className="border-t border-green-500/10 mt-auto">
<footer className="border-t border-emerald-500/[0.08] 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">
<a href={`mailto:${site.contact_email}`} className="text-xs text-neutral-600 hover:text-emerald-400 font-mono transition-colors">
{t('nav.contact')}
</a>
)}
+51 -32
View File
@@ -27,33 +27,33 @@ export default function DefaultHome() {
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 className="absolute inset-0 h-[700px] overflow-hidden pointer-events-none opacity-60">
<Aurora colorStops={["#4338ca", "#7c3aed", "#06b6d4"]} 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">
<div className="mb-6">
<h1 className="text-5xl md:text-7xl font-extrabold text-white leading-tight tracking-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>
<p className="text-lg md:text-xl text-neutral-400 max-w-2xl mx-auto mb-4 leading-relaxed">
{t('home.heroSubtitle')}
</p>
<div className="flex flex-wrap items-center justify-center gap-3 mb-6">
<div className="flex flex-wrap items-center justify-center gap-3 mb-10">
<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"
mainClassName="px-4 py-2 bg-white/[0.06] backdrop-blur-sm rounded-full text-white text-sm font-medium overflow-hidden border border-white/[0.08]"
staggerFrom="last"
rotationInterval={3000}
/>
</div>
<div className="flex items-center justify-center gap-4 mt-8">
<div className="flex items-center justify-center gap-4">
{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')}
@@ -63,7 +63,7 @@ export default function DefaultHome() {
<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">
<Link to="/pricing" className="px-8 py-3.5 rounded-2xl border border-white/20 text-white font-medium text-base hover:bg-white/10 transition-all">
{t('home.viewPricing')}
</Link>
</>
@@ -72,13 +72,13 @@ export default function DefaultHome() {
</div>
{/* Stats */}
<div className="grid grid-cols-3 gap-8 max-w-2xl mx-auto mt-24">
<div className="grid grid-cols-3 gap-6 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 key={i} className="text-center p-5 rounded-2xl bg-white/[0.04] backdrop-blur-sm border border-white/[0.08]">
<div className="text-3xl font-bold text-white">
{stat.prefix}<CountUp from={0} to={stat.value} duration={2} />{stat.suffix}
</div>
@@ -91,27 +91,41 @@ export default function DefaultHome() {
{/* 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>
<h2 className="text-3xl 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">
<div className="grid md:grid-cols-3 gap-6">
{[
{ 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' },
{ icon: (
<svg className="w-6 h-6 text-violet-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>
), title: t('home.lightningFast'), desc: t('home.lightningFastDesc'), gradient: 'from-violet-500/10 to-blue-500/10', border: 'border-violet-500/20' },
{ icon: (
<svg className="w-6 h-6 text-blue-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>
), title: t('home.securePrivate'), desc: t('home.securePrivateDesc'), gradient: 'from-blue-500/10 to-cyan-500/10', border: 'border-blue-500/20' },
{ icon: (
<svg className="w-6 h-6 text-cyan-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>
), title: t('home.payAsYouGo'), desc: t('home.payAsYouGoDesc'), gradient: 'from-cyan-500/10 to-teal-500/10', border: 'border-cyan-500/20' },
].map((f, i) => (
<TiltedCard
key={i}
rotateAmplitude={8}
scaleOnHover={1.03}
rotateAmplitude={6}
scaleOnHover={1.02}
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>
<div className={`p-6 rounded-2xl bg-gradient-to-br ${f.gradient} border ${f.border} h-full`}>
<div className="w-12 h-12 rounded-xl bg-white/[0.06] flex items-center justify-center 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>
<p className="text-sm text-neutral-400 leading-relaxed">{f.desc}</p>
</div>
</TiltedCard>
))}
@@ -122,13 +136,13 @@ export default function DefaultHome() {
{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>
<h2 className="text-3xl 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 key={m.id || i} className="px-4 py-3 rounded-xl bg-white/[0.03] border border-white/[0.06] flex items-center gap-3 hover:bg-white/[0.06] 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}
@@ -151,15 +165,16 @@ export default function DefaultHome() {
{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>
<h2 className="text-3xl 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'
i === 1 ? 'bg-white/[0.06] border-white/[0.12] shadow-xl shadow-blue-500/5' : 'bg-white/[0.03] border-white/[0.06]'
}`}>
{i === 1 && <span className="text-xs text-blue-400 font-medium mb-2 uppercase tracking-wider">{t('home.popular') || 'Popular'}</span>}
<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">
@@ -171,7 +186,11 @@ export default function DefaultHome() {
</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">
<Link to={user ? '/packages' : '/register'} className={`mt-4 py-2.5 rounded-xl font-medium text-sm text-center transition-colors ${
i === 1
? 'bg-white text-black hover:bg-neutral-100'
: 'bg-white/[0.06] text-white border border-white/[0.08] hover:bg-white/[0.1]'
}`}>
{user ? t('home.subscribe') : t('home.getStarted')}
</Link>
</div>
@@ -182,10 +201,10 @@ export default function DefaultHome() {
{/* 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="rounded-3xl p-12 text-center relative overflow-hidden bg-gradient-to-br from-violet-600/10 via-blue-600/10 to-cyan-600/10 border border-white/[0.08]">
<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>
<h2 className="text-3xl 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="px-8 py-3 rounded-2xl bg-white text-black font-semibold hover:bg-neutral-100 transition-all">
@@ -196,7 +215,7 @@ export default function DefaultHome() {
<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">
<Link to="/login" className="px-8 py-3 rounded-2xl border border-white/20 text-white font-medium hover:bg-white/10 transition-all">
{t('home.signIn')}
</Link>
</>
+15 -12
View File
@@ -3,6 +3,7 @@ import { Outlet, Link, useNavigate, useLocation } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { useAuth } from '../../context/AuthContext';
import { useSite } from '../../context/SiteContext';
import LanguageSwitch from '../../components/LanguageSwitch';
export default function DefaultLayout() {
const { t } = useTranslation();
@@ -25,23 +26,23 @@ export default function DefaultLayout() {
const visibleNavItems = navItems.filter((n) => !n.auth || user);
return (
<div className="min-h-screen flex flex-col bg-[#0a0a1a]">
<div className="min-h-screen flex flex-col bg-[#09090b]">
{/* 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 className="bg-gradient-to-r from-violet-600/10 via-blue-600/10 to-cyan-600/10 border-b border-white/[0.06] text-center py-2.5 px-4">
<p className="text-sm text-neutral-300">{site.announcement}</p>
</div>
)}
{/* Header */}
<header className="sticky top-0 z-50 backdrop-blur-2xl bg-[#0a0a1a]/80 border-b border-white/[0.08]">
<header className="sticky top-0 z-50 backdrop-blur-2xl bg-[#09090b]/80 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-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">
<div className="w-9 h-9 rounded-xl bg-gradient-to-br from-violet-500 via-blue-500 to-cyan-500 flex items-center justify-center text-white font-bold text-sm shadow-lg shadow-blue-500/20">
{siteName.charAt(0)}
</div>
)}
@@ -51,14 +52,14 @@ export default function DefaultLayout() {
</Link>
{/* Desktop Nav */}
<nav className="hidden md:flex items-center gap-1 bg-white/5 rounded-full px-2 py-1">
<nav className="hidden md:flex items-center gap-1 bg-white/[0.04] rounded-full px-1.5 py-1 border border-white/[0.06]">
{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-white bg-white/[0.08]'
: 'text-neutral-400 hover:text-white'
}`}
>
@@ -67,8 +68,10 @@ export default function DefaultLayout() {
))}
</nav>
{/* Auth + Mobile toggle */}
<div className="flex items-center gap-3">
{/* Auth + Lang + Mobile toggle */}
<div className="flex items-center gap-2">
<LanguageSwitch className="text-neutral-400 hover:text-white hover:bg-white/5" />
{user ? (
<div className="flex items-center gap-3">
<span className="text-sm text-neutral-400 hidden sm:block">
@@ -83,7 +86,7 @@ export default function DefaultLayout() {
</div>
) : (
<>
<Link to="/login" className="text-sm text-neutral-300 hover:text-white transition-colors px-3 py-1.5 hidden sm:block">
<Link to="/login" className="text-sm text-neutral-400 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">
@@ -109,7 +112,7 @@ export default function DefaultLayout() {
</div>
{mobileMenuOpen && (
<div className="md:hidden border-t border-white/[0.08] bg-[#0a0a1a]/95 backdrop-blur-2xl">
<div className="md:hidden border-t border-white/[0.06] bg-[#09090b]/95 backdrop-blur-2xl">
<nav className="max-w-7xl mx-auto px-6 py-3 flex flex-col gap-1">
{visibleNavItems.map((n) => (
<Link
@@ -134,7 +137,7 @@ export default function DefaultLayout() {
<Outlet />
</main>
<footer className="border-t border-white/[0.08] mt-auto bg-[#0a0a1a]">
<footer className="border-t border-white/[0.06] mt-auto bg-[#09090b]">
<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">
+4 -1
View File
@@ -3,6 +3,7 @@ import { Outlet, Link, useNavigate, useLocation } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { useAuth } from '../../context/AuthContext';
import { useSite } from '../../context/SiteContext';
import LanguageSwitch from '../../components/LanguageSwitch';
export default function MinimalLayout() {
const { t } = useTranslation();
@@ -63,7 +64,9 @@ export default function MinimalLayout() {
))}
</nav>
<div className="flex items-center gap-4">
<div className="flex items-center gap-3">
<LanguageSwitch className="text-neutral-500 hover:text-white hover:bg-white/5" />
{user ? (
<div className="flex items-center gap-3">
<span className="text-sm text-neutral-500 hidden sm:block">{user.display_name || user.username}</span>
+6 -7
View File
@@ -9,7 +9,6 @@ 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();
@@ -40,7 +39,7 @@ export default function StarterHome() {
<GradientText
colors={['#818cf8', '#c084fc', '#f472b6', '#818cf8']}
animationSpeed={6}
className="text-5xl md:text-7xl font-heading font-extrabold leading-tight"
className="text-5xl md:text-7xl font-extrabold leading-tight tracking-tight"
>
{site?.name || t('home.defaultHeroTitle')}
</GradientText>
@@ -48,7 +47,7 @@ export default function StarterHome() {
<BlurText
text={t('home.heroSubtitle')}
className="text-lg md:text-xl text-neutral-400 max-w-2xl mx-auto mb-10"
className="text-lg md:text-xl text-neutral-400 max-w-2xl mx-auto mb-10 leading-relaxed"
delay={50}
animateBy="words"
/>
@@ -99,7 +98,7 @@ export default function StarterHome() {
{/* 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>
<h2 className="text-3xl font-bold text-white mb-3">{t('home.whyChooseUs')}</h2>
<p className="text-neutral-400">{t('home.whyChooseUsDesc')}</p>
</div>
@@ -140,7 +139,7 @@ export default function StarterHome() {
{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>
<h2 className="text-3xl font-bold text-white mb-3">{t('home.availableModels')}</h2>
<p className="text-neutral-400">
{t('home.availableModelsDesc', { count: enabledModels.length })}
</p>
@@ -174,7 +173,7 @@ export default function StarterHome() {
{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>
<h2 className="text-3xl font-bold text-white mb-3">{t('home.plansPackages')}</h2>
<p className="text-neutral-400">{t('home.choosePlan')}</p>
</div>
@@ -213,7 +212,7 @@ export default function StarterHome() {
<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">
<h2 className="text-3xl font-bold text-white mb-4">
{t('home.readyToStart')}
</h2>
<p className="text-neutral-400 mb-8 max-w-md mx-auto">
+5 -2
View File
@@ -3,6 +3,7 @@ import { Outlet, Link, useNavigate, useLocation } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { useAuth } from '../../context/AuthContext';
import { useSite } from '../../context/SiteContext';
import LanguageSwitch from '../../components/LanguageSwitch';
export default function StarterLayout() {
const { t } = useTranslation();
@@ -59,8 +60,10 @@ export default function StarterLayout() {
))}
</nav>
{/* Auth + Mobile toggle */}
<div className="flex items-center gap-3">
{/* Auth + Lang + Mobile toggle */}
<div className="flex items-center gap-2">
<LanguageSwitch className="text-neutral-400 hover:text-white hover:bg-white/5" />
{user ? (
<div className="flex items-center gap-3">
<span className="text-sm text-neutral-400 hidden sm:block">