diff --git a/bun.lock b/bun.lock index 0a1dc3e..e45a74c 100644 --- a/bun.lock +++ b/bun.lock @@ -6,6 +6,7 @@ "name": "kaiqiu-rank-list", "dependencies": { "@ant-design/icons": "^6.1.0", + "@logto/react": "^4.0.13", "ahooks": "^3.9.6", "antd": "^6.2.1", "cheerio": "^1.2.0", @@ -50,6 +51,14 @@ "@emotion/unitless": ["@emotion/unitless@0.10.0", "", {}, "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg=="], + "@logto/browser": ["@logto/browser@3.0.12", "", { "dependencies": { "@logto/client": "^3.1.7", "@silverhand/essentials": "^2.9.3", "js-base64": "^3.7.4" } }, "sha512-Ec45IExLYS64bF22wS7dZuWgOMmC2w3FZmWWnVCv2fX2vKQVs0wiI+FE/PlNhEvi8up4AW0zHO4NTGwF7ipFsQ=="], + + "@logto/client": ["@logto/client@3.1.7", "", { "dependencies": { "@logto/js": "^6.1.1", "@silverhand/essentials": "^2.9.3", "camelcase-keys": "^9.1.3", "jose": "^5.2.2" } }, "sha512-t/5wXMhiXtmbmP6Cmcl4uMsYetq21vSZuYZztPHXv6QX0dx7lSKBvYi/65ERoS+fmNmtV2/i4Ojf1U41o0TLPQ=="], + + "@logto/js": ["@logto/js@6.1.1", "", { "dependencies": { "@silverhand/essentials": "^2.9.3", "camelcase-keys": "^9.1.3" } }, "sha512-G0lRS7VyOXdB06WYajEh9Kq2E3m11JshiKIKLj6LRPI1qZ06JYQ+Jsej3K60/4OIZMSzUas4FVnY+ORrhDdktA=="], + + "@logto/react": ["@logto/react@4.0.13", "", { "dependencies": { "@logto/browser": "^3.0.12", "@silverhand/essentials": "^2.9.3" }, "peerDependencies": { "react": ">=16.8.0" } }, "sha512-CU4rjJmueY0CQoJZq7BDZt/9sQYpxKDwVBrGHR55ljl4zPFF2URJPixqCtEEfWq5/pFk7MEnIOePOYbj7BWKfQ=="], + "@rc-component/async-validator": ["@rc-component/async-validator@5.1.0", "", { "dependencies": { "@babel/runtime": "^7.24.4" } }, "sha512-n4HcR5siNUXRX23nDizbZBQPO0ZM/5oTtmKZ6/eqL0L2bo747cklFdZGRN2f+c9qWGICwDzrhW0H7tE9PptdcA=="], "@rc-component/cascader": ["@rc-component/cascader@1.11.0", "", { "dependencies": { "@rc-component/select": "~1.5.0", "@rc-component/tree": "~1.1.0", "@rc-component/util": "^1.4.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-VDiEsskThWi8l0/1Nquc9I4ytcMKQYAb9Jkm6wiX5O5fpcMRsm+b8OulBMbr/b4rFTl/2y2y4GdKqQ+2whD+XQ=="], @@ -136,6 +145,8 @@ "@rc-component/virtual-list": ["@rc-component/virtual-list@1.0.2", "", { "dependencies": { "@babel/runtime": "^7.20.0", "@rc-component/resize-observer": "^1.0.1", "@rc-component/util": "^1.4.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-uvTol/mH74FYsn5loDGJxo+7kjkO4i+y4j87Re1pxJBs0FaeuMuLRzQRGaXwnMcV1CxpZLi2Z56Rerj2M00fjQ=="], + "@silverhand/essentials": ["@silverhand/essentials@2.9.3", "", {}, "sha512-OM9pyGc/yYJMVQw+fFOZZaTHXDWc45sprj+ky+QjC9inhf5w51L1WBmzAwFuYkHAwO1M19fxVf2sTH9KKP48yg=="], + "@types/bun": ["@types/bun@1.3.6", "", { "dependencies": { "bun-types": "1.3.6" } }, "sha512-uWCv6FO/8LcpREhenN1d1b6fcspAB+cefwD7uti8C8VffIv0Um08TKMn98FynpTiU38+y2dUO55T11NgDt8VAA=="], "@types/js-cookie": ["@types/js-cookie@3.0.6", "", {}, "sha512-wkw9yd1kEXOPnvEeEV1Go1MmxtBJL0RR79aOTAApecWFVu7w0NNXNqhcWgvw2YgZDYadliXkl14pa3WXw5jlCQ=="], @@ -158,6 +169,10 @@ "bun-types": ["bun-types@1.3.6", "", { "dependencies": { "@types/node": "*" } }, "sha512-OlFwHcnNV99r//9v5IIOgQ9Uk37gZqrNMCcqEaExdkVq3Avwqok1bJFmvGMCkCE0FqzdY8VMOZpfpR3lwI+CsQ=="], + "camelcase": ["camelcase@8.0.0", "", {}, "sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA=="], + + "camelcase-keys": ["camelcase-keys@9.1.3", "", { "dependencies": { "camelcase": "^8.0.0", "map-obj": "5.0.0", "quick-lru": "^6.1.1", "type-fest": "^4.3.2" } }, "sha512-Rircqi9ch8AnZscQcsA1C47NFdaO3wukpmIRzYcDOrmvgt78hM/sj5pZhZNec2NM12uk5vTwRHZ4anGcrC4ZTg=="], + "camelize": ["camelize@1.0.1", "", {}, "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ=="], "cheerio": ["cheerio@1.2.0", "", { "dependencies": { "cheerio-select": "^2.1.0", "dom-serializer": "^2.0.0", "domhandler": "^5.0.3", "domutils": "^3.2.2", "encoding-sniffer": "^0.2.1", "htmlparser2": "^10.1.0", "parse5": "^7.3.0", "parse5-htmlparser2-tree-adapter": "^7.1.0", "parse5-parser-stream": "^7.1.2", "undici": "^7.19.0", "whatwg-mimetype": "^4.0.0" } }, "sha512-WDrybc/gKFpTYQutKIK6UvfcuxijIZfMfXaYm8NMsPQxSYvf+13fXUJ4rztGGbJcBQ/GF55gvrZ0Bc0bj/mqvg=="], @@ -202,12 +217,18 @@ "is-mobile": ["is-mobile@5.0.0", "", {}, "sha512-Tz/yndySvLAEXh+Uk8liFCxOwVH6YutuR74utvOcu7I9Di+DwM0mtdPVZNaVvvBUM2OXxne/NhOs1zAO7riusQ=="], + "jose": ["jose@5.10.0", "", {}, "sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg=="], + + "js-base64": ["js-base64@3.7.8", "", {}, "sha512-hNngCeKxIUQiEUN3GPJOkz4wF/YvdUdbNL9hsBcMQTkKzboD7T/q3OYOuuPZLUE6dBxSGpwhk5mwuDud7JVAow=="], + "js-cookie": ["js-cookie@3.0.5", "", {}, "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw=="], "json2mq": ["json2mq@0.2.0", "", { "dependencies": { "string-convert": "^0.2.0" } }, "sha512-SzoRg7ux5DWTII9J2qkrZrqV1gt+rTaoufMxEzXbS26Uid0NwaJd123HcoB80TgubEppxxIGdNxCx50fEoEWQA=="], "lodash": ["lodash@4.17.23", "", {}, "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w=="], + "map-obj": ["map-obj@5.0.0", "", {}, "sha512-2L3MIgJynYrZ3TYMriLDLWocz15okFakV6J12HXvMXDHui2x/zgChzg1u9mFFGbbGWE+GsLpQByt4POb9Or+uA=="], + "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], "nth-check": ["nth-check@2.1.1", "", { "dependencies": { "boolbase": "^1.0.0" } }, "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w=="], @@ -224,6 +245,8 @@ "postcss-value-parser": ["postcss-value-parser@4.2.0", "", {}, "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="], + "quick-lru": ["quick-lru@6.1.2", "", {}, "sha512-AAFUA5O1d83pIHEhJwWCq/RQcRukCkn/NSm2QsTEMle5f2hP0ChI2+3Xb051PZCkLryI/Ir1MVKviT2FIloaTQ=="], + "react": ["react@19.2.3", "", {}, "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA=="], "react-dom": ["react-dom@19.2.3", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.3" } }, "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg=="], @@ -260,6 +283,8 @@ "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="], + "undici": ["undici@7.19.0", "", {}, "sha512-Heho1hJD81YChi+uS2RkSjcVO+EQLmLSyUlHyp7Y/wFbxQaGb4WXVKD073JytrjXJVkSZVzoE2MCSOKugFGtOQ=="], "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], diff --git a/package.json b/package.json index 632706e..869c0a8 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ }, "dependencies": { "@ant-design/icons": "^6.1.0", + "@logto/react": "^4.0.13", "ahooks": "^3.9.6", "antd": "^6.2.1", "cheerio": "^1.2.0", diff --git a/src/frontend.tsx b/src/frontend.tsx index 90fe487..e876e0d 100644 --- a/src/frontend.tsx +++ b/src/frontend.tsx @@ -7,93 +7,18 @@ import { StrictMode } from "react"; import { createRoot } from "react-dom/client"; -import { App } from "./App"; -import { ConfigProvider, Empty, Spin, theme } from "antd"; +import { ConfigProvider, Empty, theme } from "antd"; import zhCN from 'antd/locale/zh_CN'; -import { createBrowserRouter, RouterProvider, Outlet, useNavigation } from "react-router"; -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 { MenuButtons } from "./components/MenuButtons"; +import { RouterProvider } from "react-router"; +import { LogtoProvider, type LogtoConfig } from '@logto/react'; +import { route } from "./routes"; const elem = document.getElementById("root")!; -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, - } - ], - }, -]); - -function HydrateFallback() { - return ( - -
- - ); -} - -function Layout() { - const navigation = useNavigation(); - const loading = navigation.state === 'loading'; - return loading ? : (<> - - - ); -} - +const config: LogtoConfig = { + endpoint: 'https://logto.ksr.la/', + appId: 'iq0oceaeqazpcned7hzil', +}; const app = ( } > - + + + ); diff --git a/src/page/FindUserPage.tsx b/src/page/FindUserPage.tsx index 6225961..146c07e 100644 --- a/src/page/FindUserPage.tsx +++ b/src/page/FindUserPage.tsx @@ -7,10 +7,10 @@ import dayjs from "dayjs"; export function FindUserPage() { const [searchKey, setSearchKey] = useLocalStorageState('findUser:searchKey'); - const findUserReq = useRequest(async (page: number = 1) => { + const findUserReq = useRequest(async (searchKey: string, page: number = 1) => { const findOutUsers: XCXFindUserResp = await (await fetch(`/api/user/find?page=${page}&key=${searchKey}`)).json(); return findOutUsers; - }, { manual: true, refreshDeps: [searchKey], cacheKey: 'findUser:result' }); + }, { manual: true, cacheKey: 'findUser:result' }); return (
查找用户 @@ -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 ( +
+ +
+ ); + } + console.debug(user); + return ( + + + + {user?.username ?? user?.name ?? '未设置'} + + 修改信息 + + + + + + + + + + + ); +} \ 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, + }, + ], + }, + ], + }, +]);