feat(fav-players): optimize un-fav API response and refactor player list UI
- Modify the DELETE /api/fav endpoint to include the player uid in the response JSON. - Refactor FavPlayersPage to remove explicit dependency on `aud` for API calls, relying solely on `useAuthHeaders`. - Add a dedicated "un-fav" button to player cards to allow users to unfollow individual players. - Implement logic to distinguish between local un-fav (for unauthenticated users) and server-side un-fav (for authenticated users). - Improve UI layout using `styled-components` and updated Ant Design components (`Typography.Title`). - Add a link to sign in and view cloud favorites when the local list is empty.
This commit is contained in:
parent
2d928ab1e3
commit
86c3b6651b
@ -172,7 +172,7 @@ const server = serve({
|
|||||||
async DELETE(req) {
|
async DELETE(req) {
|
||||||
const payload = await verifyLogtoToken(req.headers);
|
const payload = await verifyLogtoToken(req.headers);
|
||||||
await unFavPlayer(`${payload.sub}`, req.params.uid);
|
await unFavPlayer(`${payload.sub}`, req.params.uid);
|
||||||
return Response.json({ ok: 'ok' });
|
return Response.json({ ok: 'ok' , uid: req.params.uid });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
'/api/battle/:eventId': {
|
'/api/battle/:eventId': {
|
||||||
|
|||||||
@ -1,13 +1,14 @@
|
|||||||
import { Avatar, Button, Card, Divider, Flex, Image, message as AntdMessage, Popconfirm, Radio, Segmented, Spin, Typography, App } from "antd";
|
import { Avatar, Button, Card, Divider, Flex, Image, message as AntdMessage, Popconfirm, Radio, Segmented, Spin, Typography, App } from "antd";
|
||||||
import { useFavPlayerStore } from "../store/useFavPlayerStore";
|
import { useFavPlayerStore, type FavPlayer } from "../store/useFavPlayerStore";
|
||||||
import { useNavigate } from "react-router";
|
import { useNavigate } from "react-router";
|
||||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
import { useLogto, type IdTokenClaims } from "@logto/react";
|
import { useLogto, type IdTokenClaims } from "@logto/react";
|
||||||
import { DeleteOutlined, LoginOutlined, SyncOutlined, UploadOutlined } from "@ant-design/icons";
|
import { DeleteOutlined, LoginOutlined, ShopTwoTone, StarFilled, SyncOutlined, UploadOutlined } from "@ant-design/icons";
|
||||||
import { useRequest } from "ahooks";
|
import { useRequest } from "ahooks";
|
||||||
import type { XCXProfile } from "../types";
|
import type { XCXProfile } from "../types";
|
||||||
import useAutoLogin from "../hooks/useAutoLogin";
|
import useAutoLogin from "../hooks/useAutoLogin";
|
||||||
import { useAuthHeaders } from "../hooks/useAuthHeaders";
|
import { useAuthHeaders } from "../hooks/useAuthHeaders";
|
||||||
|
import styled from "styled-components";
|
||||||
|
|
||||||
enum SortType {
|
enum SortType {
|
||||||
DEFAULT = '注册时间',
|
DEFAULT = '注册时间',
|
||||||
@ -19,15 +20,22 @@ enum ShowType {
|
|||||||
ACCOUNT = '账号收藏',
|
ACCOUNT = '账号收藏',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const StyledContainer = styled.div`
|
||||||
|
.player-name {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.unfav-btn {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
`;
|
||||||
export function FavePlayersPage() {
|
export function FavePlayersPage() {
|
||||||
const { favMap, unFav } = useFavPlayerStore(state => state);
|
const { favMap, unFav } = useFavPlayerStore(state => state);
|
||||||
const [sortType, setSortType] = useState<SortType>(SortType.DEFAULT);
|
const [sortType, setSortType] = useState<SortType>(SortType.DEFAULT);
|
||||||
const [showType, setShowType] = useState(ShowType.LOCAL);
|
const [showType, setShowType] = useState(ShowType.LOCAL);
|
||||||
const [claims, setClaims] = useState<IdTokenClaims>();
|
|
||||||
const headers = useAuthHeaders();
|
const headers = useAuthHeaders();
|
||||||
const { autoSignIn } = useAutoLogin();
|
const { autoSignIn } = useAutoLogin();
|
||||||
const localList = Object.values(favMap);
|
const localList = Object.values(favMap);
|
||||||
const favListRequest = useRequest<XCXProfile[], [string]>(async (aud: string) => {
|
const favListRequest = useRequest<XCXProfile[], []>(async () => {
|
||||||
const res = await fetch(`/api/fav`, { headers });
|
const res = await fetch(`/api/fav`, { headers });
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
return data;
|
return data;
|
||||||
@ -52,29 +60,34 @@ export function FavePlayersPage() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isAuthenticated) return;
|
if (!isAuthenticated) return;
|
||||||
const id = setTimeout(async () => {
|
const id = setTimeout(async () => {
|
||||||
const claims = await getIdTokenClaims();
|
favListRequest.runAsync();
|
||||||
setClaims(claims);
|
|
||||||
favListRequest.runAsync(claims?.aud!);
|
|
||||||
}, 300);
|
}, 300);
|
||||||
return () => clearTimeout(id);
|
return () => clearTimeout(id);
|
||||||
}, [isAuthenticated, getIdTokenClaims]);
|
}, [isAuthenticated, getIdTokenClaims]);
|
||||||
const handleSyncFav = useCallback(async () => {
|
const handleSyncFav = useCallback(async () => {
|
||||||
if (!isAuthenticated) return;
|
if (!isAuthenticated) return;
|
||||||
const claims = await getIdTokenClaims()!;
|
|
||||||
const aud = claims?.aud;
|
|
||||||
const jobs = list.map(async u => {
|
const jobs = list.map(async u => {
|
||||||
await fetch(`/api/fav/${u.uid}`, { method: 'PUT', headers });
|
await fetch(`/api/fav/${u.uid}`, { method: 'PUT', headers });
|
||||||
});
|
});
|
||||||
message.open({ key: 'sync', content: '同步中...', type: 'loading' });
|
message.open({ key: 'sync', content: '同步中...', type: 'loading' });
|
||||||
await Promise.all(jobs);
|
await Promise.all(jobs);
|
||||||
await favListRequest.runAsync(aud!);
|
await favListRequest.runAsync();
|
||||||
message.open({ key: 'sync', content: '已同步', type: 'success' });
|
message.open({ key: 'sync', content: '已同步', type: 'success' });
|
||||||
}, [isAuthenticated, list]);
|
}, [isAuthenticated, list]);
|
||||||
const handleClearLocal = useCallback(() => {
|
const handleClearLocal = useCallback(() => {
|
||||||
list.forEach(e => unFav(e.uid));
|
list.forEach(e => unFav(e.uid));
|
||||||
}, []);
|
}, []);
|
||||||
|
const handleUnFav = useCallback(async (e: FavPlayer) => {
|
||||||
|
if (showType === ShowType.LOCAL) {
|
||||||
|
unFav(e.uid);
|
||||||
|
}
|
||||||
|
if (showType === ShowType.ACCOUNT) {
|
||||||
|
await fetch(`/api/fav/${e.uid}`, { method: 'DELETE', headers });
|
||||||
|
await favListRequest.runAsync();
|
||||||
|
}
|
||||||
|
}, [showType, favListRequest, headers]);
|
||||||
return (
|
return (
|
||||||
<div className="app">
|
<StyledContainer className="app">
|
||||||
<Flex vertical gap={48}>
|
<Flex vertical gap={48}>
|
||||||
<Typography.Title>收藏的球员</Typography.Title>
|
<Typography.Title>收藏的球员</Typography.Title>
|
||||||
<Flex justify="center" align="center" gap={12} style={{ display: isAuthenticated ? '' : 'none' }}>
|
<Flex justify="center" align="center" gap={12} style={{ display: isAuthenticated ? '' : 'none' }}>
|
||||||
@ -88,7 +101,7 @@ export function FavePlayersPage() {
|
|||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
onClick={async () => favListRequest.runAsync(claims?.aud!)}
|
onClick={async () => favListRequest.runAsync()}
|
||||||
icon={<SyncOutlined spin={favListRequest.loading} />}
|
icon={<SyncOutlined spin={favListRequest.loading} />}
|
||||||
/>
|
/>
|
||||||
</Flex>
|
</Flex>
|
||||||
@ -107,21 +120,25 @@ export function FavePlayersPage() {
|
|||||||
{!favListRequest.loading && list.length === 0 ? (
|
{!favListRequest.loading && list.length === 0 ? (
|
||||||
<>
|
<>
|
||||||
<Divider>暂无收藏的球员</Divider>
|
<Divider>暂无收藏的球员</Divider>
|
||||||
|
{showType === ShowType.LOCAL && (
|
||||||
|
<Button type='link' onClick={() => autoSignIn()}>登录后查看云端收藏</Button>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<Flex wrap gap={16} align="center" justify="center">
|
<Flex wrap gap={16} align="center" justify="center">
|
||||||
{list.map(e => (
|
{list.map(e => (
|
||||||
<Card
|
<Card
|
||||||
hoverable
|
|
||||||
size="small"
|
size="small"
|
||||||
key={e.uid}
|
key={e.uid}
|
||||||
style={{ width: 240 }}
|
style={{ minWidth: 280 }}
|
||||||
>
|
>
|
||||||
<Flex gap={12}>
|
<Flex gap={12}>
|
||||||
|
<Flex vertical align="center" justify="center">
|
||||||
{e.avatar?.includes('noavatar')
|
{e.avatar?.includes('noavatar')
|
||||||
? <Avatar size={64} shape="square">{e.name}</Avatar>
|
? <Avatar size={64} shape="square">{e.name}</Avatar>
|
||||||
: <Image width={64} height={64} src={e.avatar} style={{ objectFit: 'cover' }} />}
|
: <Image width={64} height={64} src={e.avatar} style={{ objectFit: 'cover' }} />}
|
||||||
|
</Flex>
|
||||||
<Flex
|
<Flex
|
||||||
vertical
|
vertical
|
||||||
justify="center"
|
justify="center"
|
||||||
@ -129,9 +146,14 @@ export function FavePlayersPage() {
|
|||||||
style={{ flex: 1 }}
|
style={{ flex: 1 }}
|
||||||
onClick={() => navigate(`/profile/${e.uid}`)}
|
onClick={() => navigate(`/profile/${e.uid}`)}
|
||||||
>
|
>
|
||||||
<h2 style={{ margin: 0 }}>{e.name}</h2>
|
<Typography.Title level={3} className="player-name">{e.name}</Typography.Title>
|
||||||
<Typography.Text type="secondary">{e.realname}</Typography.Text>
|
<Typography.Text type="secondary">{e.realname}</Typography.Text>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
<Flex align="center" justify="center">
|
||||||
|
<Button className="unfav-btn" type="link" onClick={() => handleUnFav(e)}>
|
||||||
|
取关
|
||||||
|
</Button>
|
||||||
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
</Card>
|
</Card>
|
||||||
))}
|
))}
|
||||||
@ -162,6 +184,6 @@ export function FavePlayersPage() {
|
|||||||
)}
|
)}
|
||||||
</Spin>
|
</Spin>
|
||||||
</Flex>
|
</Flex>
|
||||||
</div>
|
</StyledContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Loading…
Reference in New Issue
Block a user