Improve login flow and account navigation (#340)

This commit is contained in:
shenlan 2025-10-01 11:20:39 +08:00 committed by GitHub
parent bcbe5cfaff
commit af7bcb89d9
4 changed files with 137 additions and 74 deletions

View File

@ -43,7 +43,11 @@ export async function POST(request: NextRequest) {
const cookieMaxAge = remember ? 60 * 60 * 24 * 30 : 60 * 60 * 24
const wantsJSON = prefersJson(request)
const successResponse = wantsJSON
? NextResponse.json({ success: true, message: data?.message ?? 'login_success' })
? NextResponse.json({
success: true,
message: data?.message ?? 'login_success',
redirectTo: '/',
})
: NextResponse.redirect(new URL('/', request.url), { status: 303 })
successResponse.cookies.set({

View File

@ -1,6 +1,6 @@
'use client'
import { FormEvent, useEffect, useRef, useState } from 'react'
import { FormEvent, useState } from 'react'
import { useRouter } from 'next/navigation'
import { useLanguage } from '@i18n/LanguageProvider'
@ -12,21 +12,11 @@ export function LoginForm() {
const { language } = useLanguage()
const copy = translations[language].login
const authCopy = translations[language].auth.login
const { user, login, logout } = useUser()
const { user, login } = useUser()
const [username, setUsername] = useState('')
const [password, setPassword] = useState('')
const [error, setError] = useState<string | null>(null)
const [success, setSuccess] = useState<string | null>(null)
const [isSubmitting, setIsSubmitting] = useState(false)
const redirectTimer = useRef<NodeJS.Timeout | null>(null)
useEffect(() => {
return () => {
if (redirectTimer.current) {
clearTimeout(redirectTimer.current)
}
}
}, [])
const handleSubmit = async (event: FormEvent<HTMLFormElement>) => {
event.preventDefault()
@ -40,7 +30,6 @@ export function LoginForm() {
}
setError(null)
setSuccess(null)
setIsSubmitting(true)
try {
const response = await fetch('/api/auth/login', {
@ -73,11 +62,10 @@ export function LoginForm() {
return
}
setSuccess(copy.success.replace('{username}', username.trim()))
await login(username.trim())
redirectTimer.current = setTimeout(() => {
router.push('/')
}, 800)
router.replace('/')
router.refresh()
return
} catch (submitError) {
console.warn('Login failed', submitError)
setError(copy.genericError)
@ -91,7 +79,7 @@ export function LoginForm() {
}
const handleLogout = () => {
logout()
router.push('/logout')
}
return (
@ -170,7 +158,6 @@ export function LoginForm() {
<p className="mt-4 text-xs text-gray-500">* {copy.disclaimer}</p>
</form>
{success ? <p className="mt-4 text-sm text-emerald-600">{success}</p> : null}
</div>
</div>
</div>

View File

@ -0,0 +1,76 @@
'use client'
import { useEffect } from 'react'
import { useRouter } from 'next/navigation'
import Navbar from '@components/Navbar'
import Footer from '@components/Footer'
import { AskAIButton } from '@components/AskAIButton'
import { useLanguage } from '@i18n/LanguageProvider'
import { translations } from '@i18n/translations'
import { useUser } from '@lib/userStore'
export default function LogoutPage() {
const router = useRouter()
const { logout } = useUser()
const { language } = useLanguage()
const navCopy = translations[language].nav.account
const signingOutMessage = language === 'zh' ? '正在安全退出,请稍候…' : 'Signing you out safely. One moment…'
useEffect(() => {
let cancelled = false
const performLogout = async () => {
try {
await logout()
} finally {
if (!cancelled) {
router.replace('/')
router.refresh()
}
}
}
void performLogout()
return () => {
cancelled = true
}
}, [logout, router])
return (
<div className="flex min-h-screen flex-col bg-gray-50">
<Navbar />
<main className="flex flex-1 items-center justify-center px-4 pb-16 pt-28 sm:px-6 lg:px-8">
<div className="w-full max-w-md rounded-3xl bg-white p-10 text-center shadow-xl ring-1 ring-gray-100">
<div className="mx-auto flex h-12 w-12 items-center justify-center rounded-full bg-purple-100 text-purple-600">
<svg
aria-hidden
className="h-6 w-6 animate-spin"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
/>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8v4a4 4 0 00-4 4H4z"
/>
</svg>
</div>
<h1 className="mt-6 text-2xl font-semibold text-gray-900">{navCopy.logout}</h1>
<p className="mt-3 text-sm text-gray-600">{signingOutMessage}</p>
</div>
</main>
<Footer />
<AskAIButton />
</div>
)
}

View File

@ -34,7 +34,7 @@ export default function Navbar() {
const [activeItem, setActiveItem] = useState<string | null>(null)
const [selectedChannels, setSelectedChannels] = useState<ReleaseChannel[]>(['stable'])
const { language } = useLanguage()
const { user, logout } = useUser()
const { user } = useUser()
const nav = translations[language].nav
const channelLabels = nav.releaseChannels
const accountCopy = nav.account
@ -98,6 +98,41 @@ export default function Navbar() {
const selectedChannelSet = useMemo(() => new Set(selectedChannels), [selectedChannels])
const accountChildren: NavSubItem[] = user
? [
{
key: 'userCenter',
label: accountCopy.userCenter,
href: '/panel',
togglePath: '/panel',
},
{
key: 'logout',
label: accountCopy.logout,
href: '/logout',
},
]
: [
{
key: 'register',
label: nav.account.register,
href: '/register',
togglePath: '/register',
},
{
key: 'login',
label: nav.account.login,
href: '/login',
togglePath: '/login',
},
{
key: 'demo',
label: nav.account.demo,
href: '/demo',
togglePath: '/demo',
},
]
const navItems: NavItem[] = [
{
key: 'openSource',
@ -152,46 +187,12 @@ export default function Navbar() {
},
{
key: 'account',
label: nav.account.title,
children: [
{
key: 'register',
label: nav.account.register,
href: '/register',
togglePath: '/register',
},
{
key: 'login',
label: nav.account.login,
href: '/login',
togglePath: '/login',
},
{
key: 'demo',
label: nav.account.demo,
href: '/demo',
togglePath: '/demo',
},
],
label: user?.username ?? nav.account.title,
children: accountChildren,
},
]
const navItemsWithAuth: NavItem[] = user
? navItems.map((item) => {
if (item.key !== 'account') {
return item
}
return {
...item,
children: item.children.filter(
(child) => child.key !== 'login' && child.key !== 'register',
),
}
})
: navItems
const visibleNavItems: NavItem[] = navItemsWithAuth
const visibleNavItems: NavItem[] = navItems
.map((item) => ({
...item,
children: item.children
@ -330,22 +331,19 @@ export default function Navbar() {
</div>
<div className="py-1 text-sm text-gray-700">
<a
href="/panel/"
href="/panel"
className="block px-4 py-2 hover:bg-gray-100"
onClick={() => setAccountMenuOpen(false)}
>
{accountCopy.userCenter}
</a>
<button
type="button"
onClick={() => {
setAccountMenuOpen(false)
void logout()
}}
<a
href="/logout"
className="flex w-full items-center px-4 py-2 text-left text-red-600 hover:bg-red-50"
onClick={() => setAccountMenuOpen(false)}
>
{accountCopy.logout}
</button>
</a>
</div>
</div>
) : null}
@ -449,21 +447,19 @@ export default function Navbar() {
</div>
</div>
<a
href="/panel/"
href="/panel"
className="mt-3 inline-flex items-center justify-center rounded-lg bg-white/80 px-3 py-1.5 text-xs font-semibold text-purple-600 transition hover:bg-white"
onClick={() => setMenuOpen(false)}
>
{accountCopy.userCenter}
</a>
<button
type="button"
onClick={() => {
void logout()
setMenuOpen(false)
}}
<a
href="/logout"
className="mt-3 inline-flex items-center justify-center rounded-lg border border-purple-200 px-3 py-1.5 text-xs font-semibold text-purple-600 transition hover:border-purple-300 hover:bg-purple-100 focus:outline-none focus:ring-2 focus:ring-purple-200 focus:ring-offset-2"
onClick={() => setMenuOpen(false)}
>
{accountCopy.logout}
</button>
</a>
</div>
) : null}
<div className="pt-2 flex flex-col gap-2">