feat: integrate Logto for authentication

- Add @logto/react dependencies to package.json and lockfile.
- Replace custom App layout with LogtoProvider for authentication handling.
- Configure Logto settings (endpoint, appId) in frontend.tsx.
- Refactor FindUserPage search logic to trigger request on value change instead of manual search key refresh.
This commit is contained in:
kyuuseiryuu 2026-03-07 02:24:01 +09:00
parent 4d73ce5f2b
commit 23888b31bc
7 changed files with 251 additions and 88 deletions

View File

@ -6,6 +6,7 @@
"name": "kaiqiu-rank-list", "name": "kaiqiu-rank-list",
"dependencies": { "dependencies": {
"@ant-design/icons": "^6.1.0", "@ant-design/icons": "^6.1.0",
"@logto/react": "^4.0.13",
"ahooks": "^3.9.6", "ahooks": "^3.9.6",
"antd": "^6.2.1", "antd": "^6.2.1",
"cheerio": "^1.2.0", "cheerio": "^1.2.0",
@ -50,6 +51,14 @@
"@emotion/unitless": ["@emotion/unitless@0.10.0", "", {}, "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg=="], "@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/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=="], "@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=="], "@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/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=="], "@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=="], "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=="], "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=="], "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=="], "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=="], "js-cookie": ["js-cookie@3.0.5", "", {}, "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw=="],
"json2mq": ["json2mq@0.2.0", "", { "dependencies": { "string-convert": "^0.2.0" } }, "sha512-SzoRg7ux5DWTII9J2qkrZrqV1gt+rTaoufMxEzXbS26Uid0NwaJd123HcoB80TgubEppxxIGdNxCx50fEoEWQA=="], "json2mq": ["json2mq@0.2.0", "", { "dependencies": { "string-convert": "^0.2.0" } }, "sha512-SzoRg7ux5DWTII9J2qkrZrqV1gt+rTaoufMxEzXbS26Uid0NwaJd123HcoB80TgubEppxxIGdNxCx50fEoEWQA=="],
"lodash": ["lodash@4.17.23", "", {}, "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w=="], "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=="], "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=="], "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=="], "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": ["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=="], "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=="], "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": ["undici@7.19.0", "", {}, "sha512-Heho1hJD81YChi+uS2RkSjcVO+EQLmLSyUlHyp7Y/wFbxQaGb4WXVKD073JytrjXJVkSZVzoE2MCSOKugFGtOQ=="],
"undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],

View File

@ -10,6 +10,7 @@
}, },
"dependencies": { "dependencies": {
"@ant-design/icons": "^6.1.0", "@ant-design/icons": "^6.1.0",
"@logto/react": "^4.0.13",
"ahooks": "^3.9.6", "ahooks": "^3.9.6",
"antd": "^6.2.1", "antd": "^6.2.1",
"cheerio": "^1.2.0", "cheerio": "^1.2.0",

View File

@ -7,93 +7,18 @@
import { StrictMode } from "react"; import { StrictMode } from "react";
import { createRoot } from "react-dom/client"; import { createRoot } from "react-dom/client";
import { App } from "./App"; import { ConfigProvider, Empty, theme } from "antd";
import { ConfigProvider, Empty, Spin, theme } from "antd";
import zhCN from 'antd/locale/zh_CN'; import zhCN from 'antd/locale/zh_CN';
import { createBrowserRouter, RouterProvider, Outlet, useNavigation } from "react-router"; import { RouterProvider } from "react-router";
import ProfilePage from "./page/ProfilePage"; import { LogtoProvider, type LogtoConfig } from '@logto/react';
import EventPage from "./page/EventPage"; import { route } from "./routes";
import type { MatchInfo, XCXMember } from "./types";
import { FindUserPage } from "./page/FindUserPage";
import { FavePlayersPage } from "./page/FavPlayersPage";
import { MenuButtons } from "./components/MenuButtons";
const elem = document.getElementById("root")!; const elem = document.getElementById("root")!;
const route = createBrowserRouter([ const config: LogtoConfig = {
{ endpoint: 'https://logto.ksr.la/',
path: '/', appId: 'iq0oceaeqazpcned7hzil',
element: <Layout />, };
children: [
{
path: '',
index: true,
Component: App,
HydrateFallback: () => <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: () => <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: () => <HydrateFallback />
},
{
path: 'find',
Component: FindUserPage,
}
],
},
]);
function HydrateFallback() {
return (
<Spin spinning>
<div style={{ height: '100vh' }} />
</Spin>
);
}
function Layout() {
const navigation = useNavigation();
const loading = navigation.state === 'loading';
return loading ? <HydrateFallback /> : (<>
<Outlet />
<MenuButtons />
</>);
}
const app = ( const app = (
<StrictMode> <StrictMode>
<ConfigProvider <ConfigProvider
@ -101,7 +26,9 @@ const app = (
locale={zhCN} locale={zhCN}
renderEmpty={() => <Empty description={'暂无数据'} />} renderEmpty={() => <Empty description={'暂无数据'} />}
> >
<LogtoProvider config={config}>
<RouterProvider router={route} /> <RouterProvider router={route} />
</LogtoProvider>
</ConfigProvider> </ConfigProvider>
</StrictMode> </StrictMode>
); );

View File

@ -7,10 +7,10 @@ import dayjs from "dayjs";
export function FindUserPage() { export function FindUserPage() {
const [searchKey, setSearchKey] = useLocalStorageState<string>('findUser:searchKey'); const [searchKey, setSearchKey] = useLocalStorageState<string>('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(); const findOutUsers: XCXFindUserResp = await (await fetch(`/api/user/find?page=${page}&key=${searchKey}`)).json();
return findOutUsers; return findOutUsers;
}, { manual: true, refreshDeps: [searchKey], cacheKey: 'findUser:result' }); }, { manual: true, cacheKey: 'findUser:result' });
return ( return (
<div className="app"> <div className="app">
<Typography.Title level={1}></Typography.Title> <Typography.Title level={1}></Typography.Title>
@ -22,7 +22,11 @@ export function FindUserPage() {
placeholder="输入昵称或姓名查找" placeholder="输入昵称或姓名查找"
value={searchKey} value={searchKey}
onChange={e => setSearchKey(e.target.value)} onChange={e => setSearchKey(e.target.value)}
onSearch={async () => findUserReq.runAsync()} onSearch={async (value) => {
setSearchKey(value);
if (!value) return;
return findUserReq.runAsync(value);
}}
/> />
<Table <Table
size="small" size="small"
@ -34,7 +38,7 @@ export function FindUserPage() {
current: findUserReq.data?.current_page, current: findUserReq.data?.current_page,
pageSize: findUserReq.data?.per_page, pageSize: findUserReq.data?.per_page,
onChange(page) { onChange(page) {
findUserReq.run(page); findUserReq.run(searchKey, page);
}, },
}} }}
> >

View File

@ -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 (
<Spin spinning>
<div style={{ height: '100vh', width: '100vw' }} />
</Spin>
);
}
return null;
};

81
src/page/UserCenter.tsx Normal file
View File

@ -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<IdTokenClaims>();
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 (
<div className="app">
<Button
block
type="primary"
icon={<LogoutOutlined />}
size="large"
onClick={() => signIn(`${window.location.origin}/auth/callback`)}
>
</Button>
</div>
);
}
console.debug(user);
return (
<Flex className="app" gap={12} vertical align="center" style={{ maxWidth: 600 }}>
<Avatar size={128} src={user?.picture ?? user?.name} />
<Flex>
<Typography.Text>{user?.username ?? user?.name ?? '未设置'}</Typography.Text>
</Flex>
<Divider></Divider>
<Button block icon={<EditOutlined />} onClick={() => handleModifyInfo(modifyRoutes.username)}>({user?.username ?? '未设置'})</Button>
<Button block icon={<MailOutlined />} onClick={() => handleModifyInfo(modifyRoutes.email)}> E-Mail</Button>
<Button block icon={<LockOutlined />} onClick={() => handleModifyInfo(modifyRoutes.auth_app)}></Button>
<Button block icon={<MobileOutlined />} onClick={() => handleModifyInfo(modifyRoutes.auth_app)}></Button>
<Button block icon={<KeyOutlined />} onClick={() => handleModifyInfo(modifyRoutes.passkey)}> Passkey</Button>
<Divider />
<Button block type="primary" onClick={() => navigate('/')} icon={<HomeOutlined />}></Button>
<Divider />
<Button
block
danger
icon={<LoginOutlined />}
onClick={() => Modal.confirm({
title: '确认登出?',
cancelText: '回到首页',
cancelButtonProps: { icon: <HomeOutlined /> },
onCancel: () => navigate('/'),
okText: '确认登出',
okButtonProps: {
icon: <LogoutOutlined />,
danger: true,
},
onOk: () => signOut(`${window.location.origin}/user-center`),
})}
>
</Button>
</Flex>
);
}

98
src/routes.tsx Normal file
View File

@ -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 (
<Spin spinning>
<div style={{ height: '100vh' }} />
</Spin>
);
}
function Layout() {
const navigation = useNavigation();
const loading = navigation.state === 'loading';
return loading ? <HydrateFallback /> : (<>
<Outlet />
<MenuButtons />
</>);
}
export const route = createBrowserRouter([
{
path: '/',
element: <Layout />,
children: [
{
path: '',
index: true,
Component: App,
HydrateFallback: () => <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: () => <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: () => <HydrateFallback />
},
{
path: 'find',
Component: FindUserPage,
},
{
path: 'user-center',
Component: UserCenter,
},
{
path: 'auth',
children: [
{
path: 'callback',
Component: CallbackPage,
},
],
},
],
},
]);