查找用户
@@ -22,7 +22,11 @@ export function FindUserPage() {
placeholder="输入昵称或姓名查找"
value={searchKey}
onChange={e => setSearchKey(e.target.value)}
- onSearch={async () => findUserReq.runAsync()}
+ onSearch={async (value) => {
+ setSearchKey(value);
+ if (!value) return;
+ return findUserReq.runAsync(value);
+ }}
/>
diff --git a/src/page/Logto/Callback.tsx b/src/page/Logto/Callback.tsx
new file mode 100644
index 0000000..6122ece
--- /dev/null
+++ b/src/page/Logto/Callback.tsx
@@ -0,0 +1,27 @@
+import { useHandleSignInCallback } from '@logto/react';
+import { Spin } from 'antd';
+import { useEffect } from 'react';
+import { useNavigate } from 'react-router';
+
+export const CallbackPage = () => {
+ const { isLoading, isAuthenticated, error } = useHandleSignInCallback(() => {
+ // Navigate to root path when finished
+ });
+
+ const navigate = useNavigate();
+ useEffect(() => {
+ if (isAuthenticated) {
+ console.debug({ isLoading, isAuthenticated, error });
+ navigate('/user-center');
+ }
+ }, [isAuthenticated]);
+ // When it's working in progress
+ if (isLoading) {
+ return (
+
+
+
+ );
+ }
+ return null;
+};
\ No newline at end of file
diff --git a/src/page/UserCenter.tsx b/src/page/UserCenter.tsx
new file mode 100644
index 0000000..68a79b2
--- /dev/null
+++ b/src/page/UserCenter.tsx
@@ -0,0 +1,81 @@
+import { EditOutlined, HomeOutlined, KeyOutlined, LockOutlined, LoginOutlined, LogoutOutlined, MailOutlined, MobileOutlined } from "@ant-design/icons";
+import { useLogto, type IdTokenClaims } from "@logto/react";
+import { Avatar, Button, Divider, Flex, Modal, Typography } from "antd";
+import { useCallback, useEffect, useState } from "react";
+import { useNavigate } from "react-router";
+
+enum modifyRoutes {
+ username = '/account/username',
+ email = '/account/email',
+ passwd = '/account/password',
+ passkey = '/account/passkey/manage',
+ auth_app = '/account/authenticator-app',
+}
+
+const redirect = encodeURIComponent(`${window.location.origin}/user-center`);
+const logto = 'https://logto.ksr.la';
+
+export const UserCenter = () => {
+ const { signIn, isAuthenticated, signOut, getIdTokenClaims } = useLogto();
+ const [user, setUser] = useState();
+ const navigate = useNavigate();
+ useEffect(() => {
+ if (!isAuthenticated) return;
+ getIdTokenClaims().then(claims => setUser(claims));
+ }, [isAuthenticated]);
+ const handleModifyInfo = useCallback((url: string) => {
+ window.location.href = `${logto}${url}?redirect=${redirect}`;
+ }, []);
+ if (!isAuthenticated) {
+ return (
+
+ }
+ size="large"
+ onClick={() => signIn(`${window.location.origin}/auth/callback`)}
+ >
+ 登录
+
+
+ );
+ }
+ console.debug(user);
+ return (
+
+
+
+ {user?.username ?? user?.name ?? '未设置'}
+
+ 修改信息
+ } onClick={() => handleModifyInfo(modifyRoutes.username)}>修改用户名({user?.username ?? '未设置'})
+ } onClick={() => handleModifyInfo(modifyRoutes.email)}>修改 E-Mail
+ } onClick={() => handleModifyInfo(modifyRoutes.auth_app)}>修改密码
+ } onClick={() => handleModifyInfo(modifyRoutes.auth_app)}>修改验证器
+ } onClick={() => handleModifyInfo(modifyRoutes.passkey)}>管理 Passkey
+
+
+
+ }
+ onClick={() => Modal.confirm({
+ title: '确认登出?',
+ cancelText: '回到首页',
+ cancelButtonProps: { icon: },
+ onCancel: () => navigate('/'),
+ okText: '确认登出',
+ okButtonProps: {
+ icon: ,
+ danger: true,
+ },
+ onOk: () => signOut(`${window.location.origin}/user-center`),
+ })}
+ >
+ 登出
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/routes.tsx b/src/routes.tsx
new file mode 100644
index 0000000..5cfe5ef
--- /dev/null
+++ b/src/routes.tsx
@@ -0,0 +1,98 @@
+import { Spin } from "antd";
+import { createBrowserRouter, Outlet, useNavigation } from "react-router";
+import { MenuButtons } from "./components/MenuButtons";
+import ProfilePage from "./page/ProfilePage";
+import EventPage from "./page/EventPage";
+import type { MatchInfo, XCXMember } from "./types";
+import { FindUserPage } from "./page/FindUserPage";
+import { FavePlayersPage } from "./page/FavPlayersPage";
+import { UserCenter } from "./page/UserCenter";
+import { CallbackPage } from "./page/Logto/Callback";
+import App from "./App";
+
+function HydrateFallback() {
+ return (
+
+
+
+ );
+}
+
+function Layout() {
+ const navigation = useNavigation();
+ const loading = navigation.state === 'loading';
+ return loading ? : (<>
+
+
+ >);
+}
+
+export const route = createBrowserRouter([
+ {
+ path: '/',
+ element: ,
+ children: [
+ {
+ path: '',
+ index: true,
+ Component: App,
+ HydrateFallback: () =>
+ },
+ {
+ path: '/fav-players',
+ Component: FavePlayersPage,
+ },
+ {
+ path: 'event/:matchId',
+ loader: async ({ params }) => {
+ const info: MatchInfo = await (await fetch(`/api/match/${params.matchId}`)).json();
+ const members = info.itemId
+ ? await (await fetch(`/api/match/${params.matchId}/${info.itemId}`)).json()
+ : info.players.map((e, i) => ({
+ number: i + 1,
+ uid: e.uid,
+ name: e.name,
+ score: e.score,
+ realname: e.name,
+ } as XCXMember));
+ const uids = info.players.map(e => e.uid);
+ const uidScore = await fetch(`/api/user/nowScores`, {
+ method: "POST",
+ body: JSON.stringify({ uids }),
+ });
+ return { info, members, uidScore: new Map(Object.entries(await uidScore.json())) };
+ },
+ Component: EventPage,
+ HydrateFallback: () =>
+ },
+ {
+ path: 'profile/:uid',
+ loader: async ({ params }) => {
+ const { uid } = params;
+ const profile = await (await fetch(`/api/user/${uid}`)).json();
+ const tags = await (await fetch(`/api/user/${uid}/tags`)).json();
+ return { profile, uid, tags };
+ },
+ Component: ProfilePage,
+ HydrateFallback: () =>
+ },
+ {
+ path: 'find',
+ Component: FindUserPage,
+ },
+ {
+ path: 'user-center',
+ Component: UserCenter,
+ },
+ {
+ path: 'auth',
+ children: [
+ {
+ path: 'callback',
+ Component: CallbackPage,
+ },
+ ],
+ },
+ ],
+ },
+]);