Files
nofx/web/src/components/RegisterPage.tsx
icy f24b0fc2ab fix: resolve merge conflicts in RegisterPage.tsx
- Remove merge conflict markers
- Keep HeaderBar import instead of ArrowLeft
- Remove duplicate betaMode state declaration
- Remove duplicate useEffect hook
- Update translation keys to use authStep* variants
- Ensure proper beta mode functionality

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-02 07:05:07 +08:00

367 lines
15 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useState, useEffect } from 'react';
import { useAuth } from '../contexts/AuthContext';
import { useLanguage } from '../contexts/LanguageContext';
import { t } from '../i18n/translations';
import { getSystemConfig } from '../lib/config';
import HeaderBar from './landing/HeaderBar';
export function RegisterPage() {
const { language } = useLanguage();
const { register, completeRegistration } = useAuth();
const [step, setStep] = useState<'register' | 'setup-otp' | 'verify-otp'>('register');
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
const [betaCode, setBetaCode] = useState('');
const [betaMode, setBetaMode] = useState(false);
const [otpCode, setOtpCode] = useState('');
const [userID, setUserID] = useState('');
const [otpSecret, setOtpSecret] = useState('');
const [qrCodeURL, setQrCodeURL] = useState('');
const [error, setError] = useState('');
const [loading, setLoading] = useState(false);
useEffect(() => {
// 获取系统配置,检查是否开启内测模式
getSystemConfig().then(config => {
setBetaMode(config.beta_mode || false);
}).catch(err => {
console.error('Failed to fetch system config:', err);
});
}, []);
const handleRegister = async (e: React.FormEvent) => {
e.preventDefault();
setError('');
if (password !== confirmPassword) {
setError(t('passwordMismatch', language));
return;
}
if (password.length < 6) {
setError(t('passwordTooShort', language));
return;
}
if (betaMode && !betaCode.trim()) {
setError('内测期间,注册需要提供内测码');
return;
}
setLoading(true);
const result = await register(email, password, betaCode.trim() || undefined);
if (result.success && result.userID) {
setUserID(result.userID);
setOtpSecret(result.otpSecret || '');
setQrCodeURL(result.qrCodeURL || '');
setStep('setup-otp');
} else {
setError(result.message || t('registrationFailed', language));
}
setLoading(false);
};
const handleSetupComplete = () => {
setStep('verify-otp');
};
const handleOTPVerify = async (e: React.FormEvent) => {
e.preventDefault();
setError('');
setLoading(true);
const result = await completeRegistration(userID, otpCode);
if (!result.success) {
setError(result.message || t('registrationFailed', language));
}
// 成功的话AuthContext会自动处理登录状态
setLoading(false);
};
const copyToClipboard = (text: string) => {
navigator.clipboard.writeText(text);
};
return (
<div className="min-h-screen" style={{ background: 'var(--brand-black)' }}>
<HeaderBar
isLoggedIn={false}
isHomePage={false}
currentPage="register"
language={language}
onLanguageChange={() => {}}
onPageChange={(page) => {
console.log('RegisterPage onPageChange called with:', page);
if (page === 'competition') {
window.location.href = '/competition';
}
}}
/>
<div className="flex items-center justify-center pt-20" style={{ minHeight: 'calc(100vh - 80px)' }}>
<div className="w-full max-w-md">
{/* Logo */}
<div className="text-center mb-8">
<div className="w-16 h-16 mx-auto mb-4 flex items-center justify-center">
<img src="/icons/nofx.svg" alt="NoFx Logo" className="w-16 h-16 object-contain" />
</div>
<h1 className="text-2xl font-bold" style={{ color: '#EAECEF' }}>
{t('appTitle', language)}
</h1>
<p className="text-sm mt-2" style={{ color: '#848E9C' }}>
{step === 'register' && t('registerTitle', language)}
{step === 'setup-otp' && t('setupTwoFactor', language)}
{step === 'verify-otp' && t('verifyOTP', language)}
</p>
</div>
{/* Registration Form */}
<div className="rounded-lg p-6" style={{ background: 'var(--panel-bg)', border: '1px solid var(--panel-border)' }}>
{step === 'register' && (
<form onSubmit={handleRegister} className="space-y-4">
<div>
<label className="block text-sm font-semibold mb-2" style={{ color: 'var(--brand-light-gray)' }}>
{t('email', language)}
</label>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
className="w-full px-3 py-2 rounded"
style={{ background: 'var(--brand-black)', border: '1px solid var(--panel-border)', color: 'var(--brand-light-gray)' }}
placeholder={t('emailPlaceholder', language)}
required
/>
</div>
<div>
<label className="block text-sm font-semibold mb-2" style={{ color: 'var(--brand-light-gray)' }}>
{t('password', language)}
</label>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
className="w-full px-3 py-2 rounded"
style={{ background: 'var(--brand-black)', border: '1px solid var(--panel-border)', color: 'var(--brand-light-gray)' }}
placeholder={t('passwordPlaceholder', language)}
required
/>
</div>
<div>
<label className="block text-sm font-semibold mb-2" style={{ color: 'var(--brand-light-gray)' }}>
{t('confirmPassword', language)}
</label>
<input
type="password"
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
className="w-full px-3 py-2 rounded"
style={{ background: 'var(--brand-black)', border: '1px solid var(--panel-border)', color: 'var(--brand-light-gray)' }}
placeholder={t('confirmPasswordPlaceholder', language)}
required
/>
</div>
{betaMode && (
<div>
<label className="block text-sm font-semibold mb-2" style={{ color: '#EAECEF' }}>
*
</label>
<input
type="text"
value={betaCode}
onChange={(e) => setBetaCode(e.target.value.replace(/[^a-z0-9]/gi, '').toLowerCase())}
className="w-full px-3 py-2 rounded font-mono"
style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }}
placeholder="请输入6位内测码"
maxLength={6}
required={betaMode}
/>
<p className="text-xs mt-1" style={{ color: '#848E9C' }}>
6
</p>
</div>
)}
{error && (
<div className="text-sm px-3 py-2 rounded" style={{ background: 'var(--binance-red-bg)', color: 'var(--binance-red)' }}>
{error}
</div>
)}
<button
type="submit"
disabled={loading || (betaMode && !betaCode.trim())}
className="w-full px-4 py-2 rounded text-sm font-semibold transition-all hover:scale-105 disabled:opacity-50"
style={{ background: 'var(--brand-yellow)', color: 'var(--brand-black)' }}
>
{loading ? t('loading', language) : t('registerButton', language)}
</button>
</form>
)}
{step === 'setup-otp' && (
<div className="space-y-4">
<div className="text-center">
<div className="text-4xl mb-2">📱</div>
<h3 className="text-lg font-semibold mb-2" style={{ color: '#EAECEF' }}>
{t('setupTwoFactor', language)}
</h3>
<p className="text-sm" style={{ color: '#848E9C' }}>
{t('setupTwoFactorDesc', language)}
</p>
</div>
<div className="space-y-3">
<div className="p-3 rounded" style={{ background: 'var(--brand-black)', border: '1px solid var(--panel-border)' }}>
<p className="text-sm font-semibold mb-2" style={{ color: 'var(--brand-light-gray)' }}>
{t('authStep1Title', language)}
</p>
<p className="text-xs" style={{ color: 'var(--text-secondary)' }}>
{t('authStep1Desc', language)}
</p>
</div>
<div className="p-3 rounded" style={{ background: 'var(--brand-black)', border: '1px solid var(--panel-border)' }}>
<p className="text-sm font-semibold mb-2" style={{ color: 'var(--brand-light-gray)' }}>
{t('authStep2Title', language)}
</p>
<p className="text-xs mb-2" style={{ color: '#848E9C' }}>
{t('authStep2Desc', language)}
</p>
{qrCodeURL && (
<div className="mt-2">
<p className="text-xs mb-2" style={{ color: '#848E9C' }}>{t('qrCodeHint', language)}</p>
<div className="bg-white p-2 rounded text-center">
<img src={`https://api.qrserver.com/v1/create-qr-code/?size=150x150&data=${encodeURIComponent(qrCodeURL)}`}
alt="QR Code" className="mx-auto" />
</div>
</div>
)}
<div className="mt-2">
<p className="text-xs mb-1" style={{ color: '#848E9C' }}>{t('otpSecret', language)}</p>
<div className="flex items-center gap-2">
<code className="flex-1 px-2 py-1 text-xs rounded font-mono"
style={{ background: 'var(--panel-bg-hover)', color: 'var(--brand-light-gray)' }}>
{otpSecret}
</code>
<button
onClick={() => copyToClipboard(otpSecret)}
className="px-2 py-1 text-xs rounded"
style={{ background: 'var(--brand-yellow)', color: 'var(--brand-black)' }}
>
{t('copy', language)}
</button>
</div>
</div>
</div>
<div className="p-3 rounded" style={{ background: 'var(--brand-black)', border: '1px solid var(--panel-border)' }}>
<p className="text-sm font-semibold mb-2" style={{ color: 'var(--brand-light-gray)' }}>
{t('authStep3Title', language)}
</p>
<p className="text-xs" style={{ color: 'var(--text-secondary)' }}>
{t('authStep3Desc', language)}
</p>
</div>
</div>
<button
onClick={handleSetupComplete}
className="w-full px-4 py-2 rounded text-sm font-semibold transition-all hover:scale-105"
style={{ background: '#F0B90B', color: '#000' }}
>
{t('setupCompleteContinue', language)}
</button>
</div>
)}
{step === 'verify-otp' && (
<form onSubmit={handleOTPVerify} className="space-y-4">
<div className="text-center mb-4">
<div className="text-4xl mb-2">🔐</div>
<p className="text-sm" style={{ color: '#848E9C' }}>
{t('enterOTPCode', language)}<br />
{t('completeRegistrationSubtitle', language)}
</p>
</div>
<div>
<label className="block text-sm font-semibold mb-2" style={{ color: 'var(--brand-light-gray)' }}>
{t('otpCode', language)}
</label>
<input
type="text"
value={otpCode}
onChange={(e) => setOtpCode(e.target.value.replace(/\D/g, '').slice(0, 6))}
className="w-full px-3 py-2 rounded text-center text-2xl font-mono"
style={{ background: 'var(--brand-black)', border: '1px solid var(--panel-border)', color: 'var(--brand-light-gray)' }}
placeholder={t('otpPlaceholder', language)}
maxLength={6}
required
/>
</div>
{error && (
<div className="text-sm px-3 py-2 rounded" style={{ background: 'var(--binance-red-bg)', color: 'var(--binance-red)' }}>
{error}
</div>
)}
<div className="flex gap-3">
<button
type="button"
onClick={() => setStep('setup-otp')}
className="flex-1 px-4 py-2 rounded text-sm font-semibold"
style={{ background: 'var(--panel-bg-hover)', color: 'var(--text-secondary)' }}
>
{t('back', language)}
</button>
<button
type="submit"
disabled={loading || otpCode.length !== 6}
className="flex-1 px-4 py-2 rounded text-sm font-semibold transition-all hover:scale-105 disabled:opacity-50"
style={{ background: '#F0B90B', color: '#000' }}
>
{loading ? t('loading', language) : t('completeRegistration', language)}
</button>
</div>
</form>
)}
</div>
{/* Login Link */}
{step === 'register' && (
<div className="text-center mt-6">
<p className="text-sm" style={{ color: 'var(--text-secondary)' }}>
{' '}
<button
onClick={() => {
window.history.pushState({}, '', '/login');
window.dispatchEvent(new PopStateEvent('popstate'));
}}
className="font-semibold hover:underline transition-colors"
style={{ color: 'var(--brand-yellow)' }}
>
</button>
</p>
</div>
)}
</div>
</div>
</div>
);
}