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:
1 parent
cc9a8bcc0e
commit
01405ca8cf
68 files changed
+4429
-433
No files matched your search
+22
-20
@@ -1,5 +1,6 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useAuth } from '../context/AuthContext';
|
||||
import { getUserUsage, redeemCode, Q, quotaToDollar } from '../api';
|
||||
import SpotlightCard from '../components/bits/SpotlightCard';
|
||||
@@ -8,6 +9,7 @@ import ShinyText from '../components/bits/ShinyText';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
export default function Dashboard() {
|
||||
const { t } = useTranslation();
|
||||
const { user, refreshUser } = useAuth();
|
||||
const [usage, setUsage] = useState(null);
|
||||
const [redeemInput, setRedeemInput] = useState('');
|
||||
@@ -29,7 +31,7 @@ export default function Dashboard() {
|
||||
try {
|
||||
const res = await redeemCode(redeemInput.trim());
|
||||
if (res.data.success) {
|
||||
toast.success('Code redeemed successfully!');
|
||||
toast.success(t('dashboard.redeemSuccess'));
|
||||
setRedeemInput('');
|
||||
// Refresh both usage AND user data to update balance
|
||||
await Promise.all([loadUsage(), refreshUser()]);
|
||||
@@ -49,31 +51,31 @@ export default function Dashboard() {
|
||||
{/* Welcome */}
|
||||
<div className="mb-8">
|
||||
<h1 className="text-2xl font-heading font-bold text-white mb-1">
|
||||
Welcome, <ShinyText text={user?.display_name || user?.username || 'User'} className="!inline" speed={3} color="#a5b4fc" shineColor="#e0e7ff" />
|
||||
{t('dashboard.welcome')} <ShinyText text={user?.display_name || user?.username || 'User'} className="!inline" speed={3} color="#a5b4fc" shineColor="#e0e7ff" />
|
||||
</h1>
|
||||
<p className="text-sm text-neutral-400">Manage your API usage, keys, and account</p>
|
||||
<p className="text-sm text-neutral-400">{t('dashboard.manageDesc')}</p>
|
||||
</div>
|
||||
|
||||
{/* Stats Cards */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-10">
|
||||
<SpotlightCard className="!bg-neutral-900/60 !border-neutral-800/60 !p-6" spotlightColor="rgba(34,197,94,0.15)">
|
||||
<p className="text-sm text-neutral-400 mb-2">Balance</p>
|
||||
<p className="text-sm text-neutral-400 mb-2">{t('dashboard.balance')}</p>
|
||||
<div className="text-3xl font-bold text-white">
|
||||
$<CountUp from={0} to={balanceDollars} duration={1.5} />
|
||||
</div>
|
||||
<p className="text-xs text-neutral-500 mt-1">{quota.toLocaleString()} quota units</p>
|
||||
<p className="text-xs text-neutral-500 mt-1">{t('dashboard.quotaUnits', { count: quota.toLocaleString() })}</p>
|
||||
</SpotlightCard>
|
||||
|
||||
<SpotlightCard className="!bg-neutral-900/60 !border-neutral-800/60 !p-6" spotlightColor="rgba(129,140,248,0.15)">
|
||||
<p className="text-sm text-neutral-400 mb-2">Used</p>
|
||||
<p className="text-sm text-neutral-400 mb-2">{t('dashboard.used')}</p>
|
||||
<div className="text-3xl font-bold text-white">
|
||||
$<CountUp from={0} to={usedQuota / Q} duration={1.5} />
|
||||
</div>
|
||||
<p className="text-xs text-neutral-500 mt-1">{usedQuota.toLocaleString()} quota units</p>
|
||||
<p className="text-xs text-neutral-500 mt-1">{t('dashboard.quotaUnits', { count: usedQuota.toLocaleString() })}</p>
|
||||
</SpotlightCard>
|
||||
|
||||
<SpotlightCard className="!bg-neutral-900/60 !border-neutral-800/60 !p-6" spotlightColor="rgba(244,114,182,0.15)">
|
||||
<p className="text-sm text-neutral-400 mb-2">Total Requests</p>
|
||||
<p className="text-sm text-neutral-400 mb-2">{t('dashboard.totalRequests')}</p>
|
||||
<div className="text-3xl font-bold text-white">
|
||||
<CountUp from={0} to={requestCount} duration={1.5} />
|
||||
</div>
|
||||
@@ -84,40 +86,40 @@ export default function Dashboard() {
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{/* Redeem Code */}
|
||||
<div className="glass rounded-2xl p-6">
|
||||
<h2 className="text-lg font-semibold text-white mb-4">Redeem Code</h2>
|
||||
<h2 className="text-lg font-semibold text-white mb-4">{t('dashboard.redeemCode')}</h2>
|
||||
<form onSubmit={handleRedeem} className="flex gap-3">
|
||||
<input
|
||||
type="text"
|
||||
value={redeemInput}
|
||||
onChange={(e) => setRedeemInput(e.target.value)}
|
||||
className="input flex-1"
|
||||
placeholder="Enter redemption code"
|
||||
placeholder={t('dashboard.enterCode')}
|
||||
/>
|
||||
<button type="submit" disabled={redeeming} className="btn-primary whitespace-nowrap">
|
||||
{redeeming ? 'Redeeming...' : 'Redeem'}
|
||||
{redeeming ? t('dashboard.redeeming') : t('dashboard.redeem')}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{/* Quick Links */}
|
||||
<div className="glass rounded-2xl p-6">
|
||||
<h2 className="text-lg font-semibold text-white mb-4">Quick Links</h2>
|
||||
<h2 className="text-lg font-semibold text-white mb-4">{t('dashboard.quickLinks')}</h2>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<Link to="/tokens" className="glass-sm !rounded-xl px-4 py-3 hover:bg-white/[0.06] transition-colors group">
|
||||
<p className="text-sm font-medium text-white group-hover:text-brand-400 transition-colors">API Keys</p>
|
||||
<p className="text-xs text-neutral-500">Manage your keys</p>
|
||||
<p className="text-sm font-medium text-white group-hover:text-brand-400 transition-colors">{t('dashboard.apiKeys')}</p>
|
||||
<p className="text-xs text-neutral-500">{t('dashboard.manageKeys')}</p>
|
||||
</Link>
|
||||
<Link to="/packages" className="glass-sm !rounded-xl px-4 py-3 hover:bg-white/[0.06] transition-colors group">
|
||||
<p className="text-sm font-medium text-white group-hover:text-brand-400 transition-colors">Packages</p>
|
||||
<p className="text-xs text-neutral-500">View plans</p>
|
||||
<p className="text-sm font-medium text-white group-hover:text-brand-400 transition-colors">{t('dashboard.packages')}</p>
|
||||
<p className="text-xs text-neutral-500">{t('dashboard.viewPlans')}</p>
|
||||
</Link>
|
||||
<Link to="/pricing" className="glass-sm !rounded-xl px-4 py-3 hover:bg-white/[0.06] transition-colors group">
|
||||
<p className="text-sm font-medium text-white group-hover:text-brand-400 transition-colors">Pricing</p>
|
||||
<p className="text-xs text-neutral-500">Model prices</p>
|
||||
<p className="text-sm font-medium text-white group-hover:text-brand-400 transition-colors">{t('dashboard.pricing')}</p>
|
||||
<p className="text-xs text-neutral-500">{t('dashboard.modelPrices')}</p>
|
||||
</Link>
|
||||
<div className="glass-sm !rounded-xl px-4 py-3 opacity-50">
|
||||
<p className="text-sm font-medium text-white">API Docs</p>
|
||||
<p className="text-xs text-neutral-500">Coming soon</p>
|
||||
<p className="text-sm font-medium text-white">{t('dashboard.apiDocs')}</p>
|
||||
<p className="text-xs text-neutral-500">{t('dashboard.comingSoon')}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,247 +0,0 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
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 Home() {
|
||||
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 || 'AI API Platform'}
|
||||
</GradientText>
|
||||
</div>
|
||||
|
||||
<BlurText
|
||||
text="Access the world's most powerful AI models through a single API endpoint. Simple, affordable, reliable."
|
||||
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">
|
||||
Go to Dashboard
|
||||
</StarBorder>
|
||||
</Link>
|
||||
) : (
|
||||
<>
|
||||
<Link to="/register" className="btn-primary text-base !px-8 !py-3">
|
||||
Get Started Free
|
||||
</Link>
|
||||
<Link to="/pricing" className="btn-secondary text-base !px-8 !py-3">
|
||||
View Pricing
|
||||
</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">AI Models</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">Uptime</p>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className="text-3xl font-bold text-white">
|
||||
<<CountUp from={200} to={50} duration={2} direction="down" />ms
|
||||
</div>
|
||||
<p className="text-sm text-neutral-500 mt-1">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">Why Choose Us</h2>
|
||||
<p className="text-neutral-400">Everything you need to build AI-powered applications</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">Lightning Fast</h3>
|
||||
<p className="text-sm text-neutral-400 leading-relaxed">
|
||||
Optimized routing and caching deliver responses with minimal latency. Load-balanced across multiple providers for maximum reliability.
|
||||
</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">Secure & Private</h3>
|
||||
<p className="text-sm text-neutral-400 leading-relaxed">
|
||||
Enterprise-grade security with encrypted API keys. No request logging by default. Your data stays your data.
|
||||
</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">Pay As You Go</h3>
|
||||
<p className="text-sm text-neutral-400 leading-relaxed">
|
||||
Transparent pricing with no hidden fees. Only pay for what you use. Multiple top-up options and subscription packages available.
|
||||
</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">Available Models</h2>
|
||||
<p className="text-neutral-400">
|
||||
Access <span className="text-brand-400 font-medium">{enabledModels.length}</span> AI models through a unified API
|
||||
</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">
|
||||
View all {enabledModels.length} models →
|
||||
</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">Plans & Packages</h2>
|
||||
<p className="text-neutral-400">Choose a plan that fits your needs</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">{pkg.duration} days</p>
|
||||
)}
|
||||
</div>
|
||||
<Link
|
||||
to={user ? '/packages' : '/register'}
|
||||
className="btn-primary text-center mt-4 !py-2"
|
||||
>
|
||||
{user ? 'Subscribe' : 'Get Started'}
|
||||
</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">
|
||||
Ready to Get Started?
|
||||
</h2>
|
||||
<p className="text-neutral-400 mb-8 max-w-md mx-auto">
|
||||
Create your account and start building with AI in minutes. No credit card required.
|
||||
</p>
|
||||
<div className="flex items-center justify-center gap-4">
|
||||
{user ? (
|
||||
<Link to="/dashboard" className="btn-primary text-base !px-8 !py-3">
|
||||
Go to Dashboard
|
||||
</Link>
|
||||
) : (
|
||||
<>
|
||||
<Link to="/register" className="btn-primary text-base !px-8 !py-3">
|
||||
Create Free Account
|
||||
</Link>
|
||||
<Link to="/login" className="btn-secondary text-base !px-8 !py-3">
|
||||
Sign In
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+13
-11
@@ -1,10 +1,12 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Link, Navigate, useNavigate, useLocation } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useAuth } from '../context/AuthContext';
|
||||
import { useSite } from '../context/SiteContext';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
export default function Login() {
|
||||
const { t } = useTranslation();
|
||||
const { login, user } = useAuth();
|
||||
const { site } = useSite();
|
||||
const navigate = useNavigate();
|
||||
@@ -21,14 +23,14 @@ export default function Login() {
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
if (!form.username || !form.password) {
|
||||
toast.error('Please fill in all fields');
|
||||
toast.error(t('login.fillAllFields'));
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
try {
|
||||
const result = await login(form.username, form.password);
|
||||
if (result.success) {
|
||||
toast.success('Welcome back!');
|
||||
toast.success(t('login.welcomeBackToast'));
|
||||
const from = location.state?.from?.pathname || '/dashboard';
|
||||
navigate(from, { replace: true });
|
||||
return; // component may unmount — skip setLoading
|
||||
@@ -45,34 +47,34 @@ export default function Login() {
|
||||
<div className="w-full max-w-md">
|
||||
<div className="glass rounded-2xl p-8">
|
||||
<div className="text-center mb-8">
|
||||
<h1 className="text-2xl font-heading font-bold text-white mb-2">Welcome Back</h1>
|
||||
<h1 className="text-2xl font-heading font-bold text-white mb-2">{t('login.welcomeBack')}</h1>
|
||||
<p className="text-sm text-neutral-400">
|
||||
Sign in to {site?.name || 'your account'}
|
||||
{site?.name ? t('login.signInTo', { name: site.name }) : t('login.signInToDefault')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-neutral-300 mb-1.5">Username</label>
|
||||
<label className="block text-sm font-medium text-neutral-300 mb-1.5">{t('login.username')}</label>
|
||||
<input
|
||||
type="text"
|
||||
value={form.username}
|
||||
onChange={(e) => setForm({ ...form, username: e.target.value })}
|
||||
className="input"
|
||||
placeholder="Enter your username"
|
||||
placeholder={t('login.enterUsername')}
|
||||
autoComplete="username"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-neutral-300 mb-1.5">Password</label>
|
||||
<label className="block text-sm font-medium text-neutral-300 mb-1.5">{t('login.password')}</label>
|
||||
<input
|
||||
type="password"
|
||||
value={form.password}
|
||||
onChange={(e) => setForm({ ...form, password: e.target.value })}
|
||||
className="input"
|
||||
placeholder="Enter your password"
|
||||
placeholder={t('login.enterPassword')}
|
||||
autoComplete="current-password"
|
||||
required
|
||||
/>
|
||||
@@ -86,15 +88,15 @@ export default function Login() {
|
||||
{loading && (
|
||||
<div className="w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin" />
|
||||
)}
|
||||
{loading ? 'Signing in...' : 'Sign In'}
|
||||
{loading ? t('login.signingIn') : t('login.signInBtn')}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div className="mt-6 text-center">
|
||||
<p className="text-sm text-neutral-400">
|
||||
Don't have an account?{' '}
|
||||
{t('login.noAccount')}{' '}
|
||||
<Link to="/register" className="text-brand-400 hover:text-brand-300 transition-colors font-medium">
|
||||
Create one
|
||||
{t('login.createOne')}
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
+19
-18
@@ -1,11 +1,13 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useAuth } from '../context/AuthContext';
|
||||
import { getSitePackages, subscribePackage, Q } from '../api';
|
||||
import SpotlightCard from '../components/bits/SpotlightCard';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
export default function Packages() {
|
||||
const { t } = useTranslation();
|
||||
const { user, refreshUser } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
const [packages, setPackages] = useState([]);
|
||||
@@ -36,7 +38,7 @@ export default function Packages() {
|
||||
try {
|
||||
const res = await subscribePackage(pkgId);
|
||||
if (res.data.success) {
|
||||
toast.success('Subscribed successfully!');
|
||||
toast.success(t('packages.subscribedSuccess'));
|
||||
setConfirmPkg(null);
|
||||
// Refresh user data to update balance
|
||||
await refreshUser();
|
||||
@@ -65,17 +67,17 @@ export default function Packages() {
|
||||
return (
|
||||
<div className="max-w-7xl mx-auto px-6 py-10">
|
||||
<div className="text-center mb-12">
|
||||
<h1 className="text-3xl font-heading font-bold text-white mb-3">Packages & Plans</h1>
|
||||
<h1 className="text-3xl font-heading font-bold text-white mb-3">{t('packages.title')}</h1>
|
||||
<p className="text-neutral-400 max-w-xl mx-auto">
|
||||
Choose a package that fits your usage. All plans include access to all available models.
|
||||
{t('packages.subtitle')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{enabled.length === 0 ? (
|
||||
<div className="text-center py-12 text-neutral-400">
|
||||
<p>No packages available at the moment.</p>
|
||||
<p>{t('packages.noPackages')}</p>
|
||||
<Link to="/pricing" className="text-brand-400 hover:text-brand-300 transition-colors mt-2 inline-block">
|
||||
Check model pricing →
|
||||
{t('packages.checkPricing')} →
|
||||
</Link>
|
||||
</div>
|
||||
) : (
|
||||
@@ -104,7 +106,7 @@ export default function Packages() {
|
||||
)}
|
||||
</div>
|
||||
{pkg.duration > 0 && (
|
||||
<p className="text-sm text-neutral-500 mt-1">{pkg.duration} days access</p>
|
||||
<p className="text-sm text-neutral-500 mt-1">{t('packages.daysAccess', { count: pkg.duration })}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -115,7 +117,7 @@ export default function Packages() {
|
||||
<svg className="w-4 h-4 text-green-500 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
${(pkg.quota_amount / Q).toFixed(2)} credit included
|
||||
{t('packages.creditIncluded', { amount: (pkg.quota_amount / Q).toFixed(2) })}
|
||||
</li>
|
||||
)}
|
||||
{pkg.rate_limit > 0 && (
|
||||
@@ -123,20 +125,20 @@ export default function Packages() {
|
||||
<svg className="w-4 h-4 text-green-500 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
{pkg.rate_limit} requests/min
|
||||
{t('packages.requestsMin', { count: pkg.rate_limit })}
|
||||
</li>
|
||||
)}
|
||||
<li className="flex items-center gap-2 text-sm text-neutral-300">
|
||||
<svg className="w-4 h-4 text-green-500 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
All models access
|
||||
{t('packages.allModels')}
|
||||
</li>
|
||||
<li className="flex items-center gap-2 text-sm text-neutral-300">
|
||||
<svg className="w-4 h-4 text-green-500 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
OpenAI-compatible API
|
||||
{t('packages.openaiApi')}
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@@ -146,7 +148,7 @@ export default function Packages() {
|
||||
disabled={subscribing === pkg.id}
|
||||
className="btn-primary w-full text-center"
|
||||
>
|
||||
{subscribing === pkg.id ? 'Processing...' : user ? 'Subscribe Now' : 'Sign Up to Subscribe'}
|
||||
{subscribing === pkg.id ? t('packages.processing') : user ? t('packages.subscribeNow') : t('packages.signUpToSubscribe')}
|
||||
</button>
|
||||
</div>
|
||||
</SpotlightCard>
|
||||
@@ -162,23 +164,22 @@ export default function Packages() {
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm" onClick={() => !subscribing && setConfirmPkg(null)}>
|
||||
<div className="glass rounded-2xl p-6 w-full max-w-sm" onClick={(e) => e.stopPropagation()}>
|
||||
<h2 className="text-lg font-semibold text-white mb-3">Confirm Subscription</h2>
|
||||
<h2 className="text-lg font-semibold text-white mb-3">{t('packages.confirmTitle')}</h2>
|
||||
<p className="text-sm text-neutral-400 mb-2">
|
||||
Subscribe to <span className="text-white font-medium">{confirmPkg.name}</span> for{' '}
|
||||
<span className="text-white font-medium">${pkgPrice.toFixed(2)}</span>?
|
||||
{t('packages.confirmDesc', { name: confirmPkg.name, price: pkgPrice.toFixed(2) })}
|
||||
</p>
|
||||
<p className="text-sm text-neutral-400 mb-4">
|
||||
Your balance: <span className={`font-medium ${insufficient ? 'text-red-400' : 'text-green-400'}`}>${userBalance.toFixed(2)}</span>
|
||||
{t('packages.yourBalance')} <span className={`font-medium ${insufficient ? 'text-red-400' : 'text-green-400'}`}>${userBalance.toFixed(2)}</span>
|
||||
</p>
|
||||
{insufficient && (
|
||||
<div className="bg-red-500/10 border border-red-500/20 rounded-xl p-3 mb-4">
|
||||
<p className="text-sm text-red-300">Insufficient balance. Please top up before subscribing.</p>
|
||||
<p className="text-sm text-red-300">{t('packages.insufficientBalance')}</p>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex justify-end gap-3">
|
||||
<button onClick={() => setConfirmPkg(null)} disabled={subscribing} className="btn-secondary">Cancel</button>
|
||||
<button onClick={() => setConfirmPkg(null)} disabled={subscribing} className="btn-secondary">{t('tokens.cancel')}</button>
|
||||
<button onClick={confirmSubscribe} disabled={insufficient || subscribing} className="btn-primary">
|
||||
{subscribing ? 'Processing...' : 'Confirm'}
|
||||
{subscribing ? t('packages.processing') : t('packages.confirm')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+11
-10
@@ -1,13 +1,14 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { getSiteModels } from '../api';
|
||||
|
||||
export default function Pricing() {
|
||||
const { t } = useTranslation();
|
||||
const [models, setModels] = useState([]);
|
||||
const [search, setSearch] = useState('');
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
// /api/dist/site/pricing returns same data as /api/dist/site/models — only need one call
|
||||
getSiteModels()
|
||||
.then((r) => {
|
||||
if (r.data.success) setModels(r.data.data || []);
|
||||
@@ -34,9 +35,9 @@ export default function Pricing() {
|
||||
return (
|
||||
<div className="max-w-7xl mx-auto px-6 py-10">
|
||||
<div className="text-center mb-10">
|
||||
<h1 className="text-3xl font-heading font-bold text-white mb-3">Model Pricing</h1>
|
||||
<h1 className="text-3xl font-heading font-bold text-white mb-3">{t('pricing.title')}</h1>
|
||||
<p className="text-neutral-400 max-w-xl mx-auto">
|
||||
Transparent, pay-as-you-go pricing for all available models. Prices shown per 1M tokens.
|
||||
{t('pricing.subtitle')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -51,24 +52,24 @@ export default function Pricing() {
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
className="input !pl-10"
|
||||
placeholder="Search models..."
|
||||
placeholder={t('pricing.searchPlaceholder')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{filtered.length === 0 ? (
|
||||
<div className="text-center py-12 text-neutral-400">
|
||||
{search ? 'No models match your search' : 'No models available'}
|
||||
{search ? t('pricing.noMatch') : t('pricing.noModels')}
|
||||
</div>
|
||||
) : (
|
||||
<div className="glass-sm rounded-xl overflow-hidden">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b border-white/[0.06]">
|
||||
<th className="text-left px-5 py-3.5 font-medium text-neutral-400">Model</th>
|
||||
<th className="text-right px-5 py-3.5 font-medium text-neutral-400">Input / 1M tokens</th>
|
||||
<th className="text-right px-5 py-3.5 font-medium text-neutral-400">Output / 1M tokens</th>
|
||||
<th className="text-center px-5 py-3.5 font-medium text-neutral-400">Status</th>
|
||||
<th className="text-left px-5 py-3.5 font-medium text-neutral-400">{t('pricing.model')}</th>
|
||||
<th className="text-right px-5 py-3.5 font-medium text-neutral-400">{t('pricing.inputPrice')}</th>
|
||||
<th className="text-right px-5 py-3.5 font-medium text-neutral-400">{t('pricing.outputPrice')}</th>
|
||||
<th className="text-center px-5 py-3.5 font-medium text-neutral-400">{t('pricing.status')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -90,7 +91,7 @@ export default function Pricing() {
|
||||
: 'bg-neutral-500/10 text-neutral-400 border-neutral-500/20'
|
||||
}`}>
|
||||
<span className={`w-1.5 h-1.5 rounded-full ${m.status === 'healthy' ? 'bg-green-500' : 'bg-neutral-500'}`} />
|
||||
{m.status === 'healthy' ? 'Online' : 'Unknown'}
|
||||
{m.status === 'healthy' ? t('pricing.online') : t('pricing.unknown')}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
+20
-18
@@ -1,10 +1,12 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Link, Navigate, useNavigate } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useAuth } from '../context/AuthContext';
|
||||
import { useSite } from '../context/SiteContext';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
export default function Register() {
|
||||
const { t } = useTranslation();
|
||||
const { register, user } = useAuth();
|
||||
const { site } = useSite();
|
||||
const navigate = useNavigate();
|
||||
@@ -19,19 +21,19 @@ export default function Register() {
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
if (!form.username || !form.password) {
|
||||
toast.error('Please fill in all required fields');
|
||||
toast.error(t('register.fillRequired'));
|
||||
return;
|
||||
}
|
||||
if (form.password !== form.password2) {
|
||||
toast.error('Passwords do not match');
|
||||
toast.error(t('register.passwordMismatch'));
|
||||
return;
|
||||
}
|
||||
if (form.password.length < 8) {
|
||||
toast.error('Password must be 8-20 characters');
|
||||
toast.error(t('register.passwordLength'));
|
||||
return;
|
||||
}
|
||||
if (form.password.length > 20) {
|
||||
toast.error('Password must be 8-20 characters');
|
||||
toast.error(t('register.passwordLength'));
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
@@ -42,7 +44,7 @@ export default function Register() {
|
||||
email: form.email || undefined,
|
||||
});
|
||||
if (result.success) {
|
||||
toast.success('Account created! Please sign in.');
|
||||
toast.success(t('register.accountCreated'));
|
||||
navigate('/login', { replace: true });
|
||||
return; // component may unmount — skip setLoading
|
||||
}
|
||||
@@ -58,59 +60,59 @@ export default function Register() {
|
||||
<div className="w-full max-w-md">
|
||||
<div className="glass rounded-2xl p-8">
|
||||
<div className="text-center mb-8">
|
||||
<h1 className="text-2xl font-heading font-bold text-white mb-2">Create Account</h1>
|
||||
<h1 className="text-2xl font-heading font-bold text-white mb-2">{t('register.createAccount')}</h1>
|
||||
<p className="text-sm text-neutral-400">
|
||||
Get started with {site?.name || 'our platform'}
|
||||
{site?.name ? t('register.getStartedWith', { name: site.name }) : t('register.getStartedDefault')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-neutral-300 mb-1.5">Username *</label>
|
||||
<label className="block text-sm font-medium text-neutral-300 mb-1.5">{t('register.username')} *</label>
|
||||
<input
|
||||
type="text"
|
||||
value={form.username}
|
||||
onChange={(e) => setForm({ ...form, username: e.target.value })}
|
||||
className="input"
|
||||
placeholder="Choose a username"
|
||||
placeholder={t('register.chooseUsername')}
|
||||
autoComplete="username"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-neutral-300 mb-1.5">Email</label>
|
||||
<label className="block text-sm font-medium text-neutral-300 mb-1.5">{t('register.email')}</label>
|
||||
<input
|
||||
type="email"
|
||||
value={form.email}
|
||||
onChange={(e) => setForm({ ...form, email: e.target.value })}
|
||||
className="input"
|
||||
placeholder="your@email.com (optional)"
|
||||
placeholder={t('register.emailPlaceholder')}
|
||||
autoComplete="email"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-neutral-300 mb-1.5">Password *</label>
|
||||
<label className="block text-sm font-medium text-neutral-300 mb-1.5">{t('register.password')} *</label>
|
||||
<input
|
||||
type="password"
|
||||
value={form.password}
|
||||
onChange={(e) => setForm({ ...form, password: e.target.value })}
|
||||
className="input"
|
||||
placeholder="8-20 characters"
|
||||
placeholder={t('register.passwordPlaceholder')}
|
||||
autoComplete="new-password"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-neutral-300 mb-1.5">Confirm Password *</label>
|
||||
<label className="block text-sm font-medium text-neutral-300 mb-1.5">{t('register.confirmPassword')} *</label>
|
||||
<input
|
||||
type="password"
|
||||
value={form.password2}
|
||||
onChange={(e) => setForm({ ...form, password2: e.target.value })}
|
||||
className="input"
|
||||
placeholder="Repeat your password"
|
||||
placeholder={t('register.repeatPassword')}
|
||||
autoComplete="new-password"
|
||||
required
|
||||
/>
|
||||
@@ -124,15 +126,15 @@ export default function Register() {
|
||||
{loading && (
|
||||
<div className="w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin" />
|
||||
)}
|
||||
{loading ? 'Creating...' : 'Create Account'}
|
||||
{loading ? t('register.creating') : t('register.createAccountBtn')}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div className="mt-6 text-center">
|
||||
<p className="text-sm text-neutral-400">
|
||||
Already have an account?{' '}
|
||||
{t('register.hasAccount')}{' '}
|
||||
<Link to="/login" className="text-brand-400 hover:text-brand-300 transition-colors font-medium">
|
||||
Sign in
|
||||
{t('register.signIn')}
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
+28
-26
@@ -1,8 +1,10 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { getTokens, createToken, updateToken, deleteToken } from '../api';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
export default function Tokens() {
|
||||
const { t } = useTranslation();
|
||||
const [tokens, setTokens] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [showCreate, setShowCreate] = useState(false);
|
||||
@@ -27,7 +29,7 @@ export default function Tokens() {
|
||||
const handleCreate = async (e) => {
|
||||
e.preventDefault();
|
||||
if (!createName.trim()) {
|
||||
toast.error('Please enter a name');
|
||||
toast.error(t('tokens.enterName'));
|
||||
return;
|
||||
}
|
||||
setCreating(true);
|
||||
@@ -53,7 +55,7 @@ export default function Tokens() {
|
||||
status: token.status === 1 ? 2 : 1,
|
||||
});
|
||||
if (res.data.success) {
|
||||
toast.success(token.status === 1 ? 'Token disabled' : 'Token enabled');
|
||||
toast.success(token.status === 1 ? t('tokens.tokenDisabled') : t('tokens.tokenEnabled'));
|
||||
await load();
|
||||
}
|
||||
} catch (e) { /* interceptor */ }
|
||||
@@ -64,7 +66,7 @@ export default function Tokens() {
|
||||
try {
|
||||
const res = await deleteToken(deleteConfirm.id);
|
||||
if (res.data.success) {
|
||||
toast.success('Token deleted');
|
||||
toast.success(t('tokens.tokenDeleted'));
|
||||
setDeleteConfirm(null);
|
||||
await load();
|
||||
}
|
||||
@@ -85,7 +87,7 @@ export default function Tokens() {
|
||||
document.body.removeChild(ta);
|
||||
}
|
||||
setCopiedId(text);
|
||||
toast.success('Copied to clipboard!');
|
||||
toast.success(t('tokens.copiedToClipboard'));
|
||||
setTimeout(() => setCopiedId(null), 2000);
|
||||
};
|
||||
|
||||
@@ -101,11 +103,11 @@ export default function Tokens() {
|
||||
<div className="max-w-5xl mx-auto px-6 py-10">
|
||||
<div className="flex items-center justify-between mb-8">
|
||||
<div>
|
||||
<h1 className="text-2xl font-heading font-bold text-white">API Keys</h1>
|
||||
<p className="text-sm text-neutral-400 mt-1">Manage your API access tokens</p>
|
||||
<h1 className="text-2xl font-heading font-bold text-white">{t('tokens.title')}</h1>
|
||||
<p className="text-sm text-neutral-400 mt-1">{t('tokens.subtitle')}</p>
|
||||
</div>
|
||||
<button onClick={() => setShowCreate(true)} className="btn-primary">
|
||||
+ New Key
|
||||
{t('tokens.newKey')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -113,26 +115,26 @@ export default function Tokens() {
|
||||
{showCreate && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm" onClick={() => setShowCreate(false)}>
|
||||
<div className="glass rounded-2xl p-6 w-full max-w-md" onClick={(e) => e.stopPropagation()}>
|
||||
<h2 className="text-lg font-semibold text-white mb-4">Create API Key</h2>
|
||||
<h2 className="text-lg font-semibold text-white mb-4">{t('tokens.createApiKey')}</h2>
|
||||
<form onSubmit={handleCreate} className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-neutral-300 mb-1.5">Name</label>
|
||||
<label className="block text-sm font-medium text-neutral-300 mb-1.5">{t('tokens.name')}</label>
|
||||
<input
|
||||
type="text"
|
||||
value={createName}
|
||||
onChange={(e) => setCreateName(e.target.value)}
|
||||
className="input"
|
||||
placeholder="e.g. my-app-key"
|
||||
placeholder={t('tokens.namePlaceholder')}
|
||||
autoFocus
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="flex justify-end gap-3">
|
||||
<button type="button" onClick={() => setShowCreate(false)} className="btn-secondary">
|
||||
Cancel
|
||||
{t('tokens.cancel')}
|
||||
</button>
|
||||
<button type="submit" disabled={creating} className="btn-primary">
|
||||
{creating ? 'Creating...' : 'Create'}
|
||||
{creating ? t('tokens.creating') : t('tokens.create')}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
@@ -144,10 +146,10 @@ export default function Tokens() {
|
||||
{newKey && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm">
|
||||
<div className="glass rounded-2xl p-6 w-full max-w-lg">
|
||||
<h2 className="text-lg font-semibold text-white mb-2">Your New API Key</h2>
|
||||
<h2 className="text-lg font-semibold text-white mb-2">{t('tokens.newApiKey')}</h2>
|
||||
<div className="bg-yellow-500/10 border border-yellow-500/20 rounded-xl p-3 mb-4">
|
||||
<p className="text-sm text-yellow-300">
|
||||
This is the only time you'll see this key. Copy it now and store it securely.
|
||||
{t('tokens.keyWarning')}
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-black/40 rounded-xl p-4 flex items-center gap-3">
|
||||
@@ -158,12 +160,12 @@ export default function Tokens() {
|
||||
onClick={() => handleCopy(newKey)}
|
||||
className="btn-primary !px-4 !py-1.5 flex-shrink-0"
|
||||
>
|
||||
{copiedId === newKey ? 'Copied!' : 'Copy'}
|
||||
{copiedId === newKey ? t('tokens.copied') : t('tokens.copy')}
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex justify-end mt-4">
|
||||
<button onClick={() => setNewKey(null)} className="btn-secondary">
|
||||
I've saved my key
|
||||
{t('tokens.savedKey')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -174,14 +176,14 @@ export default function Tokens() {
|
||||
{deleteConfirm && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm" onClick={() => setDeleteConfirm(null)}>
|
||||
<div className="glass rounded-2xl p-6 w-full max-w-sm" onClick={(e) => e.stopPropagation()}>
|
||||
<h2 className="text-lg font-semibold text-white mb-3">Delete Token</h2>
|
||||
<h2 className="text-lg font-semibold text-white mb-3">{t('tokens.deleteToken')}</h2>
|
||||
<p className="text-sm text-neutral-400 mb-4">
|
||||
Delete <span className="text-white font-medium">"{deleteConfirm.name}"</span>? This cannot be undone.
|
||||
{t('tokens.deleteConfirm', { name: deleteConfirm.name })}
|
||||
</p>
|
||||
<div className="flex justify-end gap-3">
|
||||
<button onClick={() => setDeleteConfirm(null)} className="btn-secondary">Cancel</button>
|
||||
<button onClick={() => setDeleteConfirm(null)} className="btn-secondary">{t('tokens.cancel')}</button>
|
||||
<button onClick={handleDelete} className="px-6 py-2.5 rounded-xl bg-red-600 text-white text-sm font-medium hover:bg-red-500 transition-colors">
|
||||
Delete
|
||||
{t('tokens.delete')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -196,9 +198,9 @@ export default function Tokens() {
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z" />
|
||||
</svg>
|
||||
</div>
|
||||
<p className="text-neutral-400 mb-4">No API keys yet</p>
|
||||
<p className="text-neutral-400 mb-4">{t('tokens.noKeys')}</p>
|
||||
<button onClick={() => setShowCreate(true)} className="btn-primary">
|
||||
Create Your First Key
|
||||
{t('tokens.createFirst')}
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
@@ -231,13 +233,13 @@ export default function Tokens() {
|
||||
: 'border-neutral-600 text-neutral-400 hover:bg-neutral-800'
|
||||
}`}
|
||||
>
|
||||
{token.status === 1 ? 'Enabled' : 'Disabled'}
|
||||
{token.status === 1 ? t('tokens.enabled') : t('tokens.disabled')}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setDeleteConfirm(token)}
|
||||
className="px-3 py-1 text-xs rounded-lg border border-red-500/20 text-red-400 hover:bg-red-500/10 transition-colors"
|
||||
>
|
||||
Delete
|
||||
{t('tokens.delete')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -247,9 +249,9 @@ export default function Tokens() {
|
||||
|
||||
{/* API Usage Note */}
|
||||
<div className="glass rounded-2xl p-6 mt-8">
|
||||
<h3 className="text-sm font-semibold text-white mb-3">Quick Start</h3>
|
||||
<h3 className="text-sm font-semibold text-white mb-3">{t('tokens.quickStart')}</h3>
|
||||
<div className="bg-black/40 rounded-xl p-4 font-mono text-sm">
|
||||
<p className="text-neutral-500"># Use your API key with OpenAI-compatible endpoint</p>
|
||||
<p className="text-neutral-500">{t('tokens.quickStartComment')}</p>
|
||||
<p className="text-green-400 mt-2">
|
||||
curl {window.location.origin}/v1/chat/completions \
|
||||
</p>
|
||||
|
||||
Reference in new issue
Block a user