feat: add client-side components for user login, signup, and account management.
This commit is contained in:
@@ -4,25 +4,29 @@ import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import type { User } from "@supabase/supabase-js";
|
||||
import type { LucideIcon } from "lucide-react";
|
||||
import {
|
||||
ArrowLeft,
|
||||
Bot,
|
||||
CheckCircle2,
|
||||
ChevronLeft,
|
||||
Clock,
|
||||
Copy,
|
||||
KeyRound,
|
||||
Crown,
|
||||
Fingerprint,
|
||||
Hash,
|
||||
Loader2,
|
||||
LogIn,
|
||||
LogOut,
|
||||
Mail,
|
||||
RefreshCw,
|
||||
ShieldCheck,
|
||||
UserCircle2,
|
||||
Shield,
|
||||
User as UserIcon,
|
||||
UserCheck,
|
||||
} from "lucide-react";
|
||||
import { useI18n } from "@/hooks/useI18n";
|
||||
import {
|
||||
getSupabaseBrowserClient,
|
||||
hasSupabasePublicEnv,
|
||||
} from "@/lib/supabase/client";
|
||||
import styles from "./AccountCenter.module.css";
|
||||
|
||||
type AuthMeResponse = {
|
||||
authenticated?: boolean;
|
||||
@@ -34,11 +38,18 @@ type AuthMeResponse = {
|
||||
subscription_active?: boolean | null;
|
||||
};
|
||||
|
||||
type InfoItemProps = {
|
||||
icon: LucideIcon;
|
||||
label: string;
|
||||
value: string;
|
||||
status?: "default" | "primary";
|
||||
};
|
||||
|
||||
function formatTime(value: string | undefined | null, locale: string) {
|
||||
if (!value) return "";
|
||||
if (!value) return "--";
|
||||
try {
|
||||
const dt = new Date(value);
|
||||
if (Number.isNaN(dt.getTime())) return "";
|
||||
if (Number.isNaN(dt.getTime())) return "--";
|
||||
return new Intl.DateTimeFormat(locale, {
|
||||
year: "numeric",
|
||||
month: "2-digit",
|
||||
@@ -47,7 +58,7 @@ function formatTime(value: string | undefined | null, locale: string) {
|
||||
minute: "2-digit",
|
||||
}).format(dt);
|
||||
} catch {
|
||||
return "";
|
||||
return "--";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,8 +72,27 @@ function normalizeProvider(user: User | null) {
|
||||
return "";
|
||||
}
|
||||
|
||||
function InfoItem({ icon: Icon, label, value, status = "default" }: InfoItemProps) {
|
||||
return (
|
||||
<div className="group flex items-center justify-between rounded-2xl border border-white/5 bg-white/5 p-4 transition-all hover:bg-white/10">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="rounded-lg bg-slate-800 p-2 text-slate-400 transition-colors group-hover:text-blue-400">
|
||||
<Icon size={18} />
|
||||
</div>
|
||||
<span className="text-sm font-medium text-slate-400">{label}</span>
|
||||
</div>
|
||||
<span
|
||||
className={`text-sm font-semibold ${
|
||||
status === "primary" ? "text-blue-400" : "text-slate-200"
|
||||
}`}
|
||||
>
|
||||
{value}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function AccountCenter() {
|
||||
const { locale, t } = useI18n();
|
||||
const router = useRouter();
|
||||
|
||||
const [loading, setLoading] = useState(true);
|
||||
@@ -91,9 +121,10 @@ export function AccountCenter() {
|
||||
setUser(userResult.data?.user ?? null);
|
||||
|
||||
if (!backendResult.ok) {
|
||||
const raw = (await backendResult.text()).slice(0, 240);
|
||||
const raw = (await backendResult.text()).slice(0, 260);
|
||||
throw new Error(`HTTP ${backendResult.status} ${raw}`.trim());
|
||||
}
|
||||
|
||||
const backendJson = (await backendResult.json()) as AuthMeResponse;
|
||||
setBackend(backendJson);
|
||||
setUpdatedAt(new Date().toISOString());
|
||||
@@ -124,42 +155,54 @@ export function AccountCenter() {
|
||||
const onSignOut = async () => {
|
||||
if (supabaseReady) {
|
||||
try {
|
||||
const supabase = getSupabaseBrowserClient();
|
||||
await supabase.auth.signOut();
|
||||
await getSupabaseBrowserClient().auth.signOut();
|
||||
} catch {}
|
||||
}
|
||||
router.replace("/auth/login");
|
||||
router.replace("/");
|
||||
};
|
||||
|
||||
const userId = backend?.user_id || user?.id || "";
|
||||
const isAuthenticated = Boolean(userId);
|
||||
const email = backend?.email || user?.email || "";
|
||||
const providerRaw = normalizeProvider(user);
|
||||
const provider = providerRaw ? providerRaw.toUpperCase() : t("account.na");
|
||||
const lastSignIn = formatTime(user?.last_sign_in_at, locale) || t("account.na");
|
||||
const updatedAtLabel = formatTime(updatedAt, locale) || t("account.na");
|
||||
const displayName =
|
||||
String(user?.user_metadata?.full_name || "").trim() ||
|
||||
(email ? String(email).split("@")[0] : "") ||
|
||||
t("account.guestName");
|
||||
const initials = displayName.slice(0, 2).toUpperCase();
|
||||
"PolyWeather 用户";
|
||||
const initials = (displayName.slice(0, 2) || "PW").toUpperCase();
|
||||
const providerRaw = normalizeProvider(user);
|
||||
const provider = providerRaw ? providerRaw.toUpperCase() : "--";
|
||||
const lastSignIn = formatTime(
|
||||
user?.last_sign_in_at,
|
||||
typeof navigator !== "undefined" ? navigator.language : "zh-CN",
|
||||
);
|
||||
const updatedAtLabel = formatTime(
|
||||
updatedAt,
|
||||
typeof navigator !== "undefined" ? navigator.language : "zh-CN",
|
||||
);
|
||||
|
||||
const modeLabel = useMemo(() => {
|
||||
const mode = String(backend?.entitlement_mode || "").trim().toLowerCase();
|
||||
if (mode === "supabase_required") return t("account.mode.supabaseRequired");
|
||||
if (mode === "supabase_optional") return t("account.mode.supabaseOptional");
|
||||
if (mode === "supabase") return t("account.mode.supabase");
|
||||
if (mode === "legacy_token") return t("account.mode.legacy");
|
||||
if (mode === "disabled") return t("account.mode.disabled");
|
||||
return t("account.mode.unknown");
|
||||
}, [backend?.entitlement_mode, t]);
|
||||
if (mode === "supabase_required") return "Supabase 强制登录";
|
||||
if (mode === "supabase_optional") return "Supabase 可选登录";
|
||||
if (mode === "legacy_token") return "Legacy Token 鉴权";
|
||||
if (mode === "disabled") return "未启用鉴权";
|
||||
return "未知模式";
|
||||
}, [backend?.entitlement_mode]);
|
||||
|
||||
const subscriptionLabel = useMemo(() => {
|
||||
if (!backend?.subscription_required) return t("account.subscription.notRequired");
|
||||
if (backend.subscription_active === true) return t("account.subscription.active");
|
||||
if (backend.subscription_active === false) return t("account.subscription.inactive");
|
||||
return t("account.subscription.unknown");
|
||||
}, [backend?.subscription_active, backend?.subscription_required, t]);
|
||||
const backendStatus = useMemo(() => {
|
||||
if (backend?.authenticated) return "通过";
|
||||
if (backend?.auth_required) return "未登录";
|
||||
return "游客模式";
|
||||
}, [backend?.authenticated, backend?.auth_required]);
|
||||
|
||||
const subscriptionRequirement = backend?.subscription_required
|
||||
? "已启用订阅校验"
|
||||
: "当前未强制订阅";
|
||||
const subscriptionResult = !backend?.subscription_required
|
||||
? "当前未强制订阅"
|
||||
: backend?.subscription_active
|
||||
? "有效订阅"
|
||||
: "无有效订阅";
|
||||
|
||||
const bindCommand = userId
|
||||
? `/bind ${userId}${email ? ` ${email}` : ""}`
|
||||
@@ -169,185 +212,187 @@ export function AccountCenter() {
|
||||
try {
|
||||
await navigator.clipboard.writeText(bindCommand);
|
||||
setCopied(true);
|
||||
window.setTimeout(() => setCopied(false), 1300);
|
||||
window.setTimeout(() => setCopied(false), 2000);
|
||||
} catch {}
|
||||
};
|
||||
|
||||
return (
|
||||
<main className={styles.page}>
|
||||
<div className={styles.aurora} />
|
||||
<div className={styles.gridNoise} />
|
||||
<div className={styles.shell}>
|
||||
<header className={styles.topBar}>
|
||||
<div className={styles.brandBlock}>
|
||||
<h1 className={styles.title}>{t("account.title")}</h1>
|
||||
<p className={styles.subtitle}>{t("account.subtitle")}</p>
|
||||
<div className="relative min-h-screen w-full overflow-hidden bg-[#0b0f1a] p-4 font-sans text-slate-200 md:p-8">
|
||||
<div className="pointer-events-none absolute right-0 top-0 h-[500px] w-[500px] rounded-full bg-blue-600/10 blur-[120px]" />
|
||||
<div className="pointer-events-none absolute bottom-0 left-0 h-[500px] w-[500px] rounded-full bg-indigo-600/10 blur-[120px]" />
|
||||
|
||||
<div className="relative z-10 mx-auto max-w-5xl">
|
||||
<header className="mb-8 flex flex-col justify-between gap-4 md:flex-row md:items-center">
|
||||
<div>
|
||||
<h1 className="bg-gradient-to-r from-white to-slate-400 bg-clip-text text-2xl font-bold text-transparent">
|
||||
账户中心
|
||||
</h1>
|
||||
<p className="mt-1 flex items-center gap-2 text-sm text-slate-500">
|
||||
<Shield size={14} /> 管理您的身份、权限与 Bot 绑定
|
||||
</p>
|
||||
</div>
|
||||
<div className={styles.actions}>
|
||||
<Link className={styles.ghostBtn} href="/">
|
||||
<ArrowLeft size={15} />
|
||||
{t("account.backDashboard")}
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<Link
|
||||
href="/"
|
||||
className="flex items-center gap-2 rounded-xl border border-white/10 bg-white/5 px-4 py-2 text-sm transition-all active:scale-95 hover:bg-white/10"
|
||||
>
|
||||
<ChevronLeft size={16} /> 返回看板
|
||||
</Link>
|
||||
<button
|
||||
type="button"
|
||||
className={styles.ghostBtn}
|
||||
onClick={() => void onRefresh()}
|
||||
disabled={refreshing || loading}
|
||||
className="flex items-center gap-2 rounded-xl border border-white/10 bg-white/5 px-4 py-2 text-sm transition-all active:scale-95 hover:bg-white/10 disabled:opacity-70"
|
||||
>
|
||||
{refreshing ? <Loader2 size={15} className={styles.spin} /> : <RefreshCw size={15} />}
|
||||
{t("account.refresh")}
|
||||
{refreshing || loading ? (
|
||||
<Loader2 size={16} className="animate-spin" />
|
||||
) : (
|
||||
<RefreshCw size={16} />
|
||||
)}
|
||||
刷新
|
||||
</button>
|
||||
{isAuthenticated ? (
|
||||
<button type="button" className={styles.primaryBtn} onClick={() => void onSignOut()}>
|
||||
<LogOut size={15} />
|
||||
{t("account.signOut")}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void onSignOut()}
|
||||
className="flex items-center gap-2 rounded-xl border border-red-500/20 bg-red-500/10 px-4 py-2 text-sm text-red-400 transition-all active:scale-95 hover:bg-red-500/20"
|
||||
>
|
||||
<LogOut size={16} /> 退出登录
|
||||
</button>
|
||||
) : (
|
||||
<Link className={styles.primaryBtn} href="/auth/login?next=%2Faccount">
|
||||
<LogIn size={15} />
|
||||
{t("account.signIn")}
|
||||
<Link
|
||||
href="/auth/login?next=%2Faccount"
|
||||
className="flex items-center gap-2 rounded-xl border border-blue-500/20 bg-blue-500/10 px-4 py-2 text-sm text-blue-300 transition-all active:scale-95 hover:bg-blue-500/20"
|
||||
>
|
||||
<LogIn size={16} /> 登录 / 注册
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section className={styles.heroCard}>
|
||||
<div className={styles.avatar}>{initials || "PW"}</div>
|
||||
<div className={styles.heroMain}>
|
||||
<h2>{displayName}</h2>
|
||||
<p>{email || t("account.na")}</p>
|
||||
<div className={styles.badges}>
|
||||
{isAuthenticated ? (
|
||||
<>
|
||||
<span className={styles.badge}>
|
||||
<CheckCircle2 size={14} />
|
||||
{t("account.authenticated")}
|
||||
</span>
|
||||
{backend?.subscription_active ? (
|
||||
<span className={styles.badge}>
|
||||
<ShieldCheck size={14} />
|
||||
{t("account.subscriptionActive")}
|
||||
</span>
|
||||
) : backend?.subscription_required ? (
|
||||
<span className={styles.badgeWarn}>
|
||||
<KeyRound size={14} />
|
||||
{t("account.subscriptionRequired")}
|
||||
</span>
|
||||
) : (
|
||||
<span className={styles.badgeGhost}>
|
||||
<ShieldCheck size={14} />
|
||||
{t("account.subscriptionUnknown")}
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<span className={styles.badgeGhost}>
|
||||
<ShieldCheck size={14} />
|
||||
{t("account.guest")}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.updatedText}>{t("account.updatedAt", { time: updatedAtLabel })}</div>
|
||||
</section>
|
||||
|
||||
{loading ? (
|
||||
<section className={styles.noticeRow}>
|
||||
<Loader2 size={16} className={styles.spin} />
|
||||
<span>{t("account.loading")}</span>
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
{errorText ? (
|
||||
<section className={styles.errorRow}>
|
||||
{t("account.error", { message: errorText })}
|
||||
</section>
|
||||
<div className="mb-6 rounded-xl border border-red-500/30 bg-red-500/10 px-4 py-3 text-sm text-red-300">
|
||||
加载失败: {errorText}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<section className={styles.cards}>
|
||||
<article className={styles.card}>
|
||||
<h3>
|
||||
<ShieldCheck size={17} />
|
||||
{t("account.card.membership")}
|
||||
</h3>
|
||||
<dl className={styles.metaList}>
|
||||
<div>
|
||||
<dt>{t("account.field.mode")}</dt>
|
||||
<dd>{modeLabel}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>{t("account.field.backendStatus")}</dt>
|
||||
<dd>
|
||||
{backend?.authenticated
|
||||
? t("account.backend.ok")
|
||||
: backend?.auth_required
|
||||
? t("account.backend.fail")
|
||||
: t("account.guest")}
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>{t("account.field.requirement")}</dt>
|
||||
<dd>
|
||||
{backend?.subscription_required
|
||||
? t("account.subscriptionRequired")
|
||||
: t("account.subscription.notRequired")}
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>{t("account.field.subscription")}</dt>
|
||||
<dd>{subscriptionLabel}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</article>
|
||||
|
||||
<article className={styles.card}>
|
||||
<h3>
|
||||
<UserCircle2 size={17} />
|
||||
{t("account.card.identity")}
|
||||
</h3>
|
||||
<dl className={styles.metaList}>
|
||||
<div>
|
||||
<dt>{t("account.field.email")}</dt>
|
||||
<dd>{email || t("account.na")}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>{t("account.field.userId")}</dt>
|
||||
<dd className={styles.mono}>{userId || t("account.na")}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>{t("account.field.provider")}</dt>
|
||||
<dd>{provider}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>{t("account.field.lastSignIn")}</dt>
|
||||
<dd>{lastSignIn}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</article>
|
||||
|
||||
<article className={styles.cardWide}>
|
||||
<h3>
|
||||
<Bot size={17} />
|
||||
{t("account.card.bot")}
|
||||
</h3>
|
||||
<p className={styles.hint}>{t("account.field.bindHint")}</p>
|
||||
<div className={styles.commandRow}>
|
||||
<code className={styles.command}>{bindCommand}</code>
|
||||
<button type="button" className={styles.copyBtn} onClick={() => void copyBindCommand()}>
|
||||
<Copy size={14} />
|
||||
{copied ? t("account.copied") : t("account.copy")}
|
||||
</button>
|
||||
</div>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
{!supabaseReady ? (
|
||||
<section className={styles.noticeRow}>
|
||||
<KeyRound size={15} />
|
||||
<span>NEXT_PUBLIC_SUPABASE_URL / ANON_KEY is not configured.</span>
|
||||
</section>
|
||||
<div className="mb-6 rounded-xl border border-amber-500/30 bg-amber-500/10 px-4 py-3 text-sm text-amber-300">
|
||||
NEXT_PUBLIC_SUPABASE_URL / NEXT_PUBLIC_SUPABASE_ANON_KEY 未配置。
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="grid grid-cols-1 gap-6 lg:grid-cols-12">
|
||||
<div className="relative overflow-hidden rounded-[2.5rem] border border-white/10 bg-gradient-to-br from-white/10 to-transparent p-8 shadow-2xl backdrop-blur-md lg:col-span-12">
|
||||
<div className="absolute right-0 top-0 p-6">
|
||||
<span className="font-mono text-xs text-slate-500">
|
||||
最近同步: {updatedAtLabel}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col items-center gap-8 md:flex-row">
|
||||
<div className="group relative">
|
||||
<div className="relative z-10 flex h-24 w-24 items-center justify-center rounded-3xl bg-gradient-to-tr from-blue-600 to-indigo-400 text-3xl font-bold shadow-xl shadow-blue-500/20">
|
||||
{initials}
|
||||
</div>
|
||||
<div className="absolute -inset-2 bg-blue-500/20 opacity-0 blur-xl transition-opacity group-hover:opacity-100" />
|
||||
</div>
|
||||
|
||||
<div className="flex-grow text-center md:text-left">
|
||||
<div className="mb-2 flex flex-col items-center gap-3 md:flex-row">
|
||||
<h2 className="text-3xl font-bold">{displayName}</h2>
|
||||
<span className="flex items-center gap-2 rounded-full border border-white/10 bg-white/10 px-3 py-1 text-xs font-semibold text-slate-400">
|
||||
<UserCheck size={12} />
|
||||
{isAuthenticated ? "已登录" : "游客模式"}
|
||||
</span>
|
||||
</div>
|
||||
<p className="font-mono text-slate-400">{email || "--"}</p>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-4">
|
||||
<div className="min-w-[120px] rounded-2xl border border-white/5 bg-black/20 px-6 py-4 text-center">
|
||||
<p className="mb-1 text-xs uppercase tracking-wider text-slate-500">
|
||||
当前角色
|
||||
</p>
|
||||
<p className="flex items-center justify-center gap-2 font-bold text-white">
|
||||
<Crown size={14} className="text-yellow-500" /> Free Tier
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-6 lg:col-span-6">
|
||||
<section className="rounded-3xl border border-white/10 bg-white/5 p-6 backdrop-blur-sm">
|
||||
<h3 className="mb-6 flex items-center gap-2 text-sm font-semibold uppercase tracking-widest text-blue-400">
|
||||
<Shield size={16} /> 会员与权限
|
||||
</h3>
|
||||
<div className="space-y-3">
|
||||
<InfoItem icon={Shield} label="鉴权模式" value={modeLabel} status="primary" />
|
||||
<InfoItem icon={UserIcon} label="后端状态" value={backendStatus} />
|
||||
<InfoItem icon={Crown} label="订阅要求" value={subscriptionRequirement} />
|
||||
<InfoItem icon={CheckCircle2} label="订阅结果" value={subscriptionResult} />
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div className="space-y-6 lg:col-span-6">
|
||||
<section className="rounded-3xl border border-white/10 bg-white/5 p-6 backdrop-blur-sm">
|
||||
<h3 className="mb-6 flex items-center gap-2 text-sm font-semibold uppercase tracking-widest text-indigo-400">
|
||||
<Fingerprint size={16} /> 身份信息
|
||||
</h3>
|
||||
<div className="space-y-3">
|
||||
<InfoItem icon={Mail} label="邮箱" value={email || "--"} />
|
||||
<InfoItem icon={Hash} label="用户 ID" value={userId || "--"} />
|
||||
<InfoItem icon={LogIn} label="登录方式" value={provider} />
|
||||
<InfoItem icon={Clock} label="最近登录" value={lastSignIn} />
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div className="lg:col-span-12">
|
||||
<section className="group relative overflow-hidden rounded-3xl border border-blue-500/20 bg-gradient-to-r from-blue-600/10 to-indigo-600/10 p-8 backdrop-blur-sm">
|
||||
<div className="absolute right-0 top-0 translate-x-1/4 -translate-y-1/4">
|
||||
<Bot
|
||||
size={200}
|
||||
className="rotate-12 text-blue-500/5 transition-transform duration-700 group-hover:rotate-0"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="relative z-10">
|
||||
<h3 className="mb-2 flex items-center gap-2 text-lg font-bold">
|
||||
<Bot size={20} className="text-blue-400" /> Bot 绑定
|
||||
</h3>
|
||||
<p className="mb-6 max-w-2xl text-sm text-slate-400">
|
||||
将下面命令发送至 Telegram Bot,即可把网页账户与机器人权限绑定,实现全平台气象推送。
|
||||
</p>
|
||||
|
||||
<div className="flex flex-col gap-3 md:flex-row">
|
||||
<div className="flex flex-grow items-center rounded-xl border border-white/10 bg-black/40 px-4 py-4 font-mono text-sm text-blue-300">
|
||||
{bindCommand}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void copyBindCommand()}
|
||||
className={`flex items-center justify-center gap-2 rounded-xl px-8 py-4 font-bold transition-all active:scale-95 ${
|
||||
copied
|
||||
? "bg-green-500 text-white"
|
||||
: "bg-blue-600 text-white shadow-lg shadow-blue-600/20 hover:bg-blue-500"
|
||||
}`}
|
||||
>
|
||||
{copied ? <CheckCircle2 size={18} /> : <Copy size={18} />}
|
||||
{copied ? "已复制" : "复制命令"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer className="mt-12 text-center text-xs text-slate-600">
|
||||
<p>© 2026 PolyWeather 全球高精度气象引擎 - 云端身份管理系统</p>
|
||||
</footer>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,15 @@
|
||||
|
||||
import { FormEvent, useEffect, useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import {
|
||||
ArrowRight,
|
||||
Chrome,
|
||||
Cloud,
|
||||
CloudRain,
|
||||
Lock,
|
||||
Mail,
|
||||
Sun,
|
||||
} from "lucide-react";
|
||||
import {
|
||||
getSupabaseBrowserClient,
|
||||
hasSupabasePublicEnv,
|
||||
@@ -119,154 +128,128 @@ export function LoginClient({ nextPath }: LoginClientProps) {
|
||||
}
|
||||
};
|
||||
|
||||
const isLogin = mode === "login";
|
||||
|
||||
return (
|
||||
<main
|
||||
style={{
|
||||
minHeight: "100vh",
|
||||
display: "grid",
|
||||
placeItems: "center",
|
||||
background:
|
||||
"radial-gradient(circle at 18% 15%, #0f2b59 0%, #0a1a39 38%, #050b16 100%)",
|
||||
color: "#d8e6ff",
|
||||
padding: "24px",
|
||||
}}
|
||||
>
|
||||
<section
|
||||
style={{
|
||||
width: "100%",
|
||||
maxWidth: 460,
|
||||
borderRadius: 16,
|
||||
border: "1px solid rgba(84, 118, 177, 0.45)",
|
||||
background: "rgba(8, 18, 37, 0.9)",
|
||||
boxShadow: "0 24px 60px rgba(0, 0, 0, 0.4)",
|
||||
padding: 24,
|
||||
}}
|
||||
>
|
||||
<h1 style={{ margin: 0, fontSize: 28 }}>PolyWeather 登录</h1>
|
||||
<p style={{ marginTop: 10, color: "#9db5df", lineHeight: 1.5 }}>
|
||||
优先推荐 Google 一键登录,邮箱注册/登录可并行使用。
|
||||
</p>
|
||||
<div className="relative flex min-h-screen w-full items-center justify-center overflow-hidden bg-[#0f172a] font-sans">
|
||||
<div className="absolute left-[-10%] top-[-10%] h-[40vw] w-[40vw] animate-pulse rounded-full bg-blue-600/20 blur-[120px]" />
|
||||
<div className="absolute bottom-[-10%] right-[-10%] h-[30vw] w-[30vw] rounded-full bg-indigo-500/20 blur-[100px]" />
|
||||
|
||||
<div className="relative mx-4 w-full max-w-[420px] rounded-[2rem] border border-white/10 bg-white/5 p-8 shadow-2xl backdrop-blur-xl">
|
||||
<div className="mb-8 flex flex-col items-center">
|
||||
<div className="mb-4 flex h-16 w-16 items-center justify-center rounded-2xl bg-gradient-to-tr from-blue-500 to-indigo-400 shadow-lg shadow-blue-500/20">
|
||||
<Cloud className="h-10 w-10 text-white" />
|
||||
</div>
|
||||
<h1 className="text-3xl font-bold tracking-tight text-white">PolyWeather</h1>
|
||||
<p className="mt-2 text-sm text-slate-400">探索世界每一个角落的气象细节</p>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void onGoogleSignIn()}
|
||||
disabled={loading}
|
||||
style={{
|
||||
width: "100%",
|
||||
marginTop: 12,
|
||||
padding: "12px 14px",
|
||||
borderRadius: 10,
|
||||
border: "1px solid rgba(132, 169, 237, 0.5)",
|
||||
background: "linear-gradient(135deg, #1a4c95 0%, #2a6ed2 100%)",
|
||||
color: "#f3f7ff",
|
||||
fontWeight: 700,
|
||||
cursor: "pointer",
|
||||
}}
|
||||
className="mb-6 flex w-full items-center justify-center rounded-xl bg-white px-4 py-3.5 font-semibold text-slate-900 shadow-lg transition-all duration-200 hover:bg-slate-100 active:scale-[0.98] disabled:cursor-not-allowed disabled:opacity-70"
|
||||
>
|
||||
使用 Google 一键登录
|
||||
<Chrome className="mr-3 h-5 w-5" />
|
||||
使用 Google 账号一键登录
|
||||
</button>
|
||||
|
||||
<div style={{ marginTop: 18, marginBottom: 14, color: "#8ea8d8" }}>
|
||||
或使用邮箱 {mode === "login" ? "登录" : "注册"}
|
||||
<div className="my-6 flex items-center">
|
||||
<div className="h-[1px] flex-grow bg-white/10" />
|
||||
<span className="px-4 text-xs font-medium uppercase tracking-widest text-slate-500">
|
||||
或使用邮箱
|
||||
</span>
|
||||
<div className="h-[1px] flex-grow bg-white/10" />
|
||||
</div>
|
||||
|
||||
<div style={{ display: "flex", gap: 8, marginBottom: 14 }}>
|
||||
<div className="mb-6 flex rounded-xl bg-black/20 p-1">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setMode("login")}
|
||||
style={{
|
||||
flex: 1,
|
||||
padding: "10px 12px",
|
||||
borderRadius: 8,
|
||||
border: "1px solid rgba(116, 148, 206, 0.45)",
|
||||
background: mode === "login" ? "#1c4a90" : "transparent",
|
||||
color: "#d8e6ff",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
className={`flex-1 rounded-lg py-2 text-sm font-medium transition-all ${
|
||||
isLogin
|
||||
? "bg-blue-600 text-white shadow-md"
|
||||
: "text-slate-400 hover:text-slate-200"
|
||||
}`}
|
||||
>
|
||||
邮箱登录
|
||||
登录
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setMode("signup")}
|
||||
style={{
|
||||
flex: 1,
|
||||
padding: "10px 12px",
|
||||
borderRadius: 8,
|
||||
border: "1px solid rgba(116, 148, 206, 0.45)",
|
||||
background: mode === "signup" ? "#1c4a90" : "transparent",
|
||||
color: "#d8e6ff",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
className={`flex-1 rounded-lg py-2 text-sm font-medium transition-all ${
|
||||
!isLogin
|
||||
? "bg-blue-600 text-white shadow-md"
|
||||
: "text-slate-400 hover:text-slate-200"
|
||||
}`}
|
||||
>
|
||||
邮箱注册
|
||||
注册
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<form onSubmit={(event) => void onEmailSubmit(event)}>
|
||||
<input
|
||||
type="email"
|
||||
required
|
||||
value={email}
|
||||
onChange={(event) => setEmail(event.target.value)}
|
||||
placeholder="you@example.com"
|
||||
style={{
|
||||
width: "100%",
|
||||
marginBottom: 10,
|
||||
padding: "12px",
|
||||
borderRadius: 8,
|
||||
border: "1px solid rgba(116, 148, 206, 0.4)",
|
||||
background: "rgba(10, 23, 47, 0.92)",
|
||||
color: "#e6f0ff",
|
||||
}}
|
||||
/>
|
||||
<input
|
||||
type="password"
|
||||
required
|
||||
minLength={6}
|
||||
value={password}
|
||||
onChange={(event) => setPassword(event.target.value)}
|
||||
placeholder="至少 6 位密码"
|
||||
style={{
|
||||
width: "100%",
|
||||
marginBottom: 12,
|
||||
padding: "12px",
|
||||
borderRadius: 8,
|
||||
border: "1px solid rgba(116, 148, 206, 0.4)",
|
||||
background: "rgba(10, 23, 47, 0.92)",
|
||||
color: "#e6f0ff",
|
||||
}}
|
||||
/>
|
||||
<form onSubmit={(event) => void onEmailSubmit(event)} className="space-y-4">
|
||||
<div className="relative">
|
||||
<Mail className="absolute left-4 top-1/2 h-5 w-5 -translate-y-1/2 text-slate-500" />
|
||||
<input
|
||||
type="email"
|
||||
required
|
||||
value={email}
|
||||
onChange={(event) => setEmail(event.target.value)}
|
||||
placeholder="you@example.com"
|
||||
className="w-full rounded-xl border border-white/10 bg-white/5 py-3.5 pl-12 pr-4 text-white placeholder:text-slate-600 transition-all focus:border-blue-500/50 focus:outline-none focus:ring-2 focus:ring-blue-500/50"
|
||||
/>
|
||||
</div>
|
||||
<div className="relative">
|
||||
<Lock className="absolute left-4 top-1/2 h-5 w-5 -translate-y-1/2 text-slate-500" />
|
||||
<input
|
||||
type="password"
|
||||
required
|
||||
minLength={6}
|
||||
value={password}
|
||||
onChange={(event) => setPassword(event.target.value)}
|
||||
placeholder={isLogin ? "输入密码" : "设置至少 6 位密码"}
|
||||
className="w-full rounded-xl border border-white/10 bg-white/5 py-3.5 pl-12 pr-4 text-white placeholder:text-slate-600 transition-all focus:border-blue-500/50 focus:outline-none focus:ring-2 focus:ring-blue-500/50"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
style={{
|
||||
width: "100%",
|
||||
padding: "12px 14px",
|
||||
borderRadius: 10,
|
||||
border: "1px solid rgba(105, 214, 179, 0.55)",
|
||||
background: "linear-gradient(135deg, #1b8a71 0%, #1aa387 100%)",
|
||||
color: "#f0fffb",
|
||||
fontWeight: 700,
|
||||
cursor: "pointer",
|
||||
}}
|
||||
className="group mt-8 flex w-full items-center justify-center rounded-xl bg-gradient-to-r from-blue-600 to-indigo-600 py-3.5 font-bold text-white shadow-xl shadow-blue-600/20 transition-all hover:from-blue-500 hover:to-indigo-500 active:scale-[0.98] disabled:cursor-not-allowed disabled:opacity-70"
|
||||
>
|
||||
{mode === "login" ? "邮箱登录" : "邮箱注册"}
|
||||
{isLogin ? "开启天气之旅" : "立即创建账号"}
|
||||
<ArrowRight className="ml-2 h-5 w-5 transition-transform group-hover:translate-x-1" />
|
||||
</button>
|
||||
</form>
|
||||
|
||||
{errorText ? (
|
||||
<p style={{ marginTop: 12, color: "#ff8b96" }}>{errorText}</p>
|
||||
) : null}
|
||||
{infoText ? (
|
||||
<p style={{ marginTop: 12, color: "#77e0be" }}>{infoText}</p>
|
||||
) : null}
|
||||
{errorText ? <p className="mt-4 text-sm text-rose-300">{errorText}</p> : null}
|
||||
{infoText ? <p className="mt-4 text-sm text-emerald-300">{infoText}</p> : null}
|
||||
|
||||
<p style={{ marginTop: 16, color: "#8ea8d8", fontSize: 13 }}>
|
||||
登录后将跳转到: <code>{nextPath}</code>
|
||||
<div className="mt-8 text-center">
|
||||
<p className="text-xs text-slate-500">
|
||||
{isLogin ? "登录后将为您个性化定制首页数据" : "注册即代表同意我们的服务条款"}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<p className="mt-3 text-center text-[11px] text-slate-500">
|
||||
登录后跳转到: <code>{nextPath}</code>
|
||||
</p>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
{!supabaseReady ? (
|
||||
<p className="mt-3 text-center text-sm text-rose-300">
|
||||
Supabase 未配置,无法使用登录
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className="absolute bottom-8 flex items-center gap-4 text-sm text-slate-600">
|
||||
<span className="flex items-center">
|
||||
<Sun className="mr-1 h-4 w-4" /> 实时数据
|
||||
</span>
|
||||
<span className="flex items-center">
|
||||
<CloudRain className="mr-1 h-4 w-4" /> 高精度预测
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user