| 1 | /** |
| 2 | * LoginDialog - 全局登录弹框组件 |
| 3 | * 在当前页面弹出登录表单,避免跳转到独立登录页 |
| 4 | */ |
| 5 | |
| 6 | 'use client' |
| 7 | |
| 8 | import Image from 'next/image' |
| 9 | import Link from 'next/link' |
| 10 | import { useRouter } from 'next/navigation' |
| 11 | import { memo, useCallback } from 'react' |
| 12 | import { useShallow } from 'zustand/shallow' |
| 13 | |
| 14 | import { EmailLoginForm } from '@/app/[lng]/auth/login/components/LoginContent/EmailLoginForm' |
| 15 | import { useTransClient } from '@/app/i18n/client' |
| 16 | import logo from '@/assets/images/logo.png' |
| 17 | import { Dialog, DialogContent, DialogTitle } from '@/components/ui/dialog' |
| 18 | |
| 19 | import { useIsMobile } from '@/hooks/useIsMobile' |
| 20 | import { cn } from '@/utils/className' |
| 21 | import { useLoginDialogStore } from '@/store/login-dialog' |
| 22 | |
| 23 | export default function LoginDialog() { |
| 24 | const { visible } = useLoginDialogStore( |
| 25 | useShallow(state => ({ visible: state.visible })), |
| 26 | ) |
| 27 | |
| 28 | if (!visible) |
| 29 | return null |
| 30 | |
| 31 | return <LoginDialogContent /> |
| 32 | } |
| 33 | |
| 34 | const LoginDialogContent = memo(() => { |
| 35 | const router = useRouter() |
| 36 | const { t } = useTransClient('login') |
| 37 | const isMobile = useIsMobile() |
| 38 | const { redirectUrl, inviteCode, fromGuard, closeLoginDialog } = useLoginDialogStore( |
| 39 | useShallow(state => ({ |
| 40 | redirectUrl: state.redirectUrl, |
| 41 | inviteCode: state.inviteCode, |
| 42 | fromGuard: state.fromGuard, |
| 43 | closeLoginDialog: state.closeLoginDialog, |
| 44 | })), |
| 45 | ) |
| 46 | |
| 47 | const handleOpenChange = useCallback((open: boolean) => { |
| 48 | if (!open) { |
| 49 | closeLoginDialog() |
| 50 | if (fromGuard) { |
| 51 | router.push('/') |
| 52 | } |
| 53 | } |
| 54 | }, [closeLoginDialog, fromGuard, router]) |
| 55 | |
| 56 | const handleLoginSuccess = useCallback(() => { |
| 57 | closeLoginDialog() |
| 58 | if (fromGuard) { |
| 59 | window.location.reload() |
| 60 | } |
| 61 | else if (redirectUrl) { |
| 62 | router.push(redirectUrl) |
| 63 | } |
| 64 | }, [closeLoginDialog, fromGuard, redirectUrl, router]) |
| 65 | |
| 66 | return ( |
| 67 | <Dialog open onOpenChange={handleOpenChange}> |
| 68 | <DialogContent |
| 69 | className={cn( |
| 70 | isMobile |
| 71 | ? 'fixed bottom-0 left-0 right-0 top-auto translate-x-0 translate-y-0 rounded-t-2xl rounded-b-none w-full max-w-none data-[state=open]:slide-in-from-bottom data-[state=closed]:slide-out-to-bottom data-[state=open]:zoom-in-100 data-[state=closed]:zoom-out-100' |
| 72 | : 'sm:w-[min(460px,95vw)]', |
| 73 | )} |
| 74 | > |
| 75 | <DialogTitle className="sr-only">{t('welcomeBack')}</DialogTitle> |
| 76 | |
| 77 | {/* Logo + 标题 */} |
| 78 | <div className="flex flex-col items-center pb-2 pt-2"> |
| 79 | <Image |
| 80 | src={logo} |
| 81 | alt="AiToEarn" |
| 82 | width={56} |
| 83 | height={56} |
| 84 | className="mb-4 drop-shadow-md" |
| 85 | /> |
| 86 | <h2 className="text-xl font-semibold text-foreground">{t('welcomeBack')}</h2> |
| 87 | <p className="mt-1.5 text-sm text-muted-foreground">{t('loginSubtitle')}</p> |
| 88 | </div> |
| 89 | |
| 90 | {/* 登录表单 */} |
| 91 | <div className="px-2"> |
| 92 | <EmailLoginForm |
| 93 | onLoginSuccess={handleLoginSuccess} |
| 94 | redirectUrl={redirectUrl} |
| 95 | inviteCode={inviteCode} |
| 96 | /> |
| 97 | </div> |
| 98 | |
| 99 | {/* 底部条款 */} |
| 100 | <p className="pb-2 text-center text-xs text-muted-foreground/70"> |
| 101 | {t('termsText')} |
| 102 | {' '} |
| 103 | <Link |
| 104 | href="/websit/terms-of-service" |
| 105 | className="text-muted-foreground underline hover:text-foreground" |
| 106 | > |
| 107 | {t('termsOfService')} |
| 108 | </Link> |
| 109 | {' '} |
| 110 | {t('and')} |
| 111 | {' '} |
| 112 | <Link |
| 113 | href="/websit/privacy-policy" |
| 114 | className="text-muted-foreground underline hover:text-foreground" |
| 115 | > |
| 116 | {t('privacyPolicy')} |
| 117 | </Link> |
| 118 | </p> |
| 119 | </DialogContent> |
| 120 | </Dialog> |
| 121 | ) |
| 122 | }) |
| 123 | |
| 124 | LoginDialogContent.displayName = 'LoginDialogContent' |
| 125 |