fix: resolve login redirect loop issue (#422)

- Redirect to /traders instead of / after successful login/registration
- Make 'Get Started Now' button redirect logged-in users to /traders
- Prevent infinite loop where logged-in users are shown landing page repeatedly

Fixes issue where after login success, clicking "Get Started Now" would
show login modal again instead of entering the main application.

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

Co-Authored-By: tinkle-community <tinklefund@gmail.com>
This commit is contained in:
Ember
2025-11-04 22:30:31 +08:00
parent 92272fc2b0
commit ed09482f4b
2 changed files with 125 additions and 68 deletions

View File

@@ -130,30 +130,30 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
body: JSON.stringify({ user_id: userID, otp_code: otpCode }), body: JSON.stringify({ user_id: userID, otp_code: otpCode }),
}); })
const data = await response.json(); const data = await response.json()
if (response.ok) { if (response.ok) {
// 登录成功保存token和用户信息 // 登录成功保存token和用户信息
const userInfo = { id: data.user_id, email: data.email }; const userInfo = { id: data.user_id, email: data.email }
setToken(data.token); setToken(data.token)
setUser(userInfo); setUser(userInfo)
localStorage.setItem('auth_token', data.token); localStorage.setItem('auth_token', data.token)
localStorage.setItem('auth_user', JSON.stringify(userInfo)); localStorage.setItem('auth_user', JSON.stringify(userInfo))
// 跳转到首页 // 跳转到配置页面
window.history.pushState({}, '', '/'); window.history.pushState({}, '', '/traders')
window.dispatchEvent(new PopStateEvent('popstate')); window.dispatchEvent(new PopStateEvent('popstate'))
return { success: true, message: data.message }; return { success: true, message: data.message }
} else { } else {
return { success: false, message: data.error }; return { success: false, message: data.error }
} }
} catch (error) { } catch (error) {
return { success: false, message: 'OTP验证失败请重试' }; return { success: false, message: 'OTP验证失败请重试' }
} }
}; }
const completeRegistration = async (userID: string, otpCode: string) => { const completeRegistration = async (userID: string, otpCode: string) => {
try { try {
@@ -163,30 +163,30 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
body: JSON.stringify({ user_id: userID, otp_code: otpCode }), body: JSON.stringify({ user_id: userID, otp_code: otpCode }),
}); })
const data = await response.json(); const data = await response.json()
if (response.ok) { if (response.ok) {
// 注册完成,自动登录 // 注册完成,自动登录
const userInfo = { id: data.user_id, email: data.email }; const userInfo = { id: data.user_id, email: data.email }
setToken(data.token); setToken(data.token)
setUser(userInfo); setUser(userInfo)
localStorage.setItem('auth_token', data.token); localStorage.setItem('auth_token', data.token)
localStorage.setItem('auth_user', JSON.stringify(userInfo)); localStorage.setItem('auth_user', JSON.stringify(userInfo))
// 跳转到首页 // 跳转到配置页面
window.history.pushState({}, '', '/'); window.history.pushState({}, '', '/traders')
window.dispatchEvent(new PopStateEvent('popstate')); window.dispatchEvent(new PopStateEvent('popstate'))
return { success: true, message: data.message }; return { success: true, message: data.message }
} else { } else {
return { success: false, message: data.error }; return { success: false, message: data.error }
} }
} catch (error) { } catch (error) {
return { success: false, message: '注册完成失败,请重试' }; return { success: false, message: '注册完成失败,请重试' }
} }
}; }
const logout = () => { const logout = () => {
setUser(null); setUser(null);

View File

@@ -32,48 +32,105 @@ export function LandingPage() {
user={user} user={user}
onLogout={logout} onLogout={logout}
onPageChange={(page) => { onPageChange={(page) => {
console.log('LandingPage onPageChange called with:', page); console.log('LandingPage onPageChange called with:', page)
if (page === 'competition') { if (page === 'competition') {
window.location.href = '/competition'; window.location.href = '/competition'
} else if (page === 'traders') { } else if (page === 'traders') {
window.location.href = '/traders'; window.location.href = '/traders'
} else if (page === 'trader') { } else if (page === 'trader') {
window.location.href = '/dashboard'; window.location.href = '/dashboard'
} }
}} }}
/> />
<div className='min-h-screen px-4 sm:px-6 lg:px-8' style={{ background: 'var(--brand-black)', color: 'var(--brand-light-gray)' }}> <div
<HeroSection language={language} /> className='min-h-screen px-4 sm:px-6 lg:px-8'
<AboutSection language={language} /> style={{
<FeaturesSection language={language} /> background: 'var(--brand-black)',
<HowItWorksSection language={language} /> color: 'var(--brand-light-gray)',
<CommunitySection /> }}
>
<HeroSection language={language} />
<AboutSection language={language} />
<FeaturesSection language={language} />
<HowItWorksSection language={language} />
<CommunitySection />
{/* CTA */} {/* CTA */}
<AnimatedSection backgroundColor='var(--panel-bg)'> <AnimatedSection backgroundColor='var(--panel-bg)'>
<div className='max-w-4xl mx-auto text-center'> <div className='max-w-4xl mx-auto text-center'>
<motion.h2 className='text-5xl font-bold mb-6' style={{ color: 'var(--brand-light-gray)' }} initial={{ opacity: 0, y: 30 }} whileInView={{ opacity: 1, y: 0 }} viewport={{ once: true }}> <motion.h2
{t('readyToDefine', language)} className='text-5xl font-bold mb-6'
</motion.h2> style={{ color: 'var(--brand-light-gray)' }}
<motion.p className='text-xl mb-12' style={{ color: 'var(--text-secondary)' }} initial={{ opacity: 0, y: 30 }} whileInView={{ opacity: 1, y: 0 }} viewport={{ once: true }} transition={{ delay: 0.1 }}> initial={{ opacity: 0, y: 30 }}
{t('startWithCrypto', language)} whileInView={{ opacity: 1, y: 0 }}
</motion.p> viewport={{ once: true }}
<div className='flex flex-wrap justify-center gap-4'> >
<motion.button onClick={() => setShowLoginModal(true)} className='flex items-center gap-2 px-10 py-4 rounded-lg font-semibold text-lg' style={{ background: 'var(--brand-yellow)', color: 'var(--brand-black)' }} whileHover={{ scale: 1.05 }} whileTap={{ scale: 0.95 }}> {t('readyToDefine', language)}
{t('getStartedNow', language)} </motion.h2>
<motion.div animate={{ x: [0, 5, 0] }} transition={{ duration: 1.5, repeat: Infinity }}> <motion.p
<ArrowRight className='w-5 h-5' /> className='text-xl mb-12'
</motion.div> style={{ color: 'var(--text-secondary)' }}
</motion.button> initial={{ opacity: 0, y: 30 }}
<motion.a href='https://github.com/tinkle-community/nofx/tree/dev' target='_blank' rel='noopener noreferrer' className='flex items-center gap-2 px-10 py-4 rounded-lg font-semibold text-lg' style={{ background: 'transparent', color: 'var(--brand-light-gray)', border: '2px solid var(--brand-yellow)' }} whileHover={{ scale: 1.05, backgroundColor: 'rgba(240, 185, 11, 0.1)' }} whileTap={{ scale: 0.95 }}> whileInView={{ opacity: 1, y: 0 }}
{t('viewSourceCode', language)} viewport={{ once: true }}
</motion.a> transition={{ delay: 0.1 }}
>
{t('startWithCrypto', language)}
</motion.p>
<div className='flex flex-wrap justify-center gap-4'>
<motion.button
onClick={() => {
if (isLoggedIn) {
window.location.href = '/traders'
} else {
setShowLoginModal(true)
}
}}
className='flex items-center gap-2 px-10 py-4 rounded-lg font-semibold text-lg'
style={{
background: 'var(--brand-yellow)',
color: 'var(--brand-black)',
}}
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
>
{t('getStartedNow', language)}
<motion.div
animate={{ x: [0, 5, 0] }}
transition={{ duration: 1.5, repeat: Infinity }}
>
<ArrowRight className='w-5 h-5' />
</motion.div>
</motion.button>
<motion.a
href='https://github.com/tinkle-community/nofx/tree/dev'
target='_blank'
rel='noopener noreferrer'
className='flex items-center gap-2 px-10 py-4 rounded-lg font-semibold text-lg'
style={{
background: 'transparent',
color: 'var(--brand-light-gray)',
border: '2px solid var(--brand-yellow)',
}}
whileHover={{
scale: 1.05,
backgroundColor: 'rgba(240, 185, 11, 0.1)',
}}
whileTap={{ scale: 0.95 }}
>
{t('viewSourceCode', language)}
</motion.a>
</div>
</div> </div>
</div> </AnimatedSection>
</AnimatedSection>
{showLoginModal && <LoginModal onClose={() => setShowLoginModal(false)} language={language} />} {showLoginModal && (
<FooterSection language={language} /> <LoginModal
onClose={() => setShowLoginModal(false)}
language={language}
/>
)}
<FooterSection language={language} />
</div> </div>
</> </>
) )