feat(likes): sync local favorites and add auto-login flow
- Add Prisma code generation in Dockerfile build step. - Implement instant state updates for favoriting players via store hooks. - Enhance 'FavPlayersPage' with: - New "Sync to cloud" and "Clear local favorites" actions. - Updated tabs showing counts for local vs. account favorites. - Auto-detection of view mode based on authentication status. - Add auto-login support in 'UserCenter' triggered by query parameter `autoSignIn=true`.
This commit is contained in:
parent
80aebac57a
commit
9499de0180
@ -1,6 +1,6 @@
|
||||
FROM oven/bun:latest
|
||||
COPY . /app
|
||||
WORKDIR /app
|
||||
RUN bun install
|
||||
RUN bun install && bunx --bun prisma generate
|
||||
ENTRYPOINT [ "bun", "start"]
|
||||
EXPOSE 3000
|
||||
|
||||
@ -22,6 +22,7 @@ const StyledContainer = styled.div`
|
||||
|
||||
export function FavButton(props: Props) {
|
||||
const { isAuthenticated, getIdTokenClaims } = useLogto();
|
||||
const { fav, unFav } = useFavPlayerStore(store => store);
|
||||
const favReq = useRequest(async () => {
|
||||
if (!isAuthenticated) return;
|
||||
const claims = await getIdTokenClaims();
|
||||
@ -48,10 +49,12 @@ export function FavButton(props: Props) {
|
||||
if (!props.user) return;
|
||||
setValue(value);
|
||||
if (value) {
|
||||
fav(props.user);
|
||||
favReq.run();
|
||||
setValue(1);
|
||||
} else {
|
||||
unFavReq.run();
|
||||
unFav(props.user.uid);
|
||||
setValue(0);
|
||||
}
|
||||
}, []);
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
import { Avatar, Button, Card, Divider, Flex, Image, Segmented, Spin, Typography } from "antd";
|
||||
import { Avatar, Button, Card, Divider, Flex, Image, Popconfirm, Radio, Segmented, Spin, Typography } from "antd";
|
||||
import { useFavPlayerStore } from "../store/useFavPlayerStore";
|
||||
import { useNavigate } from "react-router";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { useLogto } from "@logto/react";
|
||||
import { LoginOutlined, SyncOutlined, UploadOutlined } from "@ant-design/icons";
|
||||
import { DeleteOutlined, LoginOutlined, UploadOutlined } from "@ant-design/icons";
|
||||
import { useRequest } from "ahooks";
|
||||
import type { XCXProfile } from "../types";
|
||||
|
||||
@ -18,7 +18,7 @@ enum ShowType {
|
||||
}
|
||||
|
||||
export function FavePlayersPage() {
|
||||
const { favMap } = useFavPlayerStore(state => state);
|
||||
const { favMap, unFav } = useFavPlayerStore(state => state);
|
||||
const [sortType, setSortType] = useState<SortType>(SortType.DEFAULT);
|
||||
const list = useMemo(() => {
|
||||
const l = Object.values(favMap);
|
||||
@ -30,7 +30,7 @@ export function FavePlayersPage() {
|
||||
}
|
||||
}, [favMap, sortType]);
|
||||
const navigate = useNavigate();
|
||||
const { isAuthenticated, getIdTokenClaims, signIn } = useLogto();
|
||||
const { isAuthenticated, getIdTokenClaims } = useLogto();
|
||||
const favListRequest = useRequest<XCXProfile[], [string]>(async (aud: string) => {
|
||||
const res = await fetch(`/api/fav/${aud}`);
|
||||
const data = await res.json();
|
||||
@ -55,6 +55,12 @@ export function FavePlayersPage() {
|
||||
await favListRequest.runAsync(aud!);
|
||||
}, [isAuthenticated, list]);
|
||||
const [showType, setShowType] = useState(ShowType.LOCAL);
|
||||
const handleClearLocal = useCallback(() => {
|
||||
list.forEach(e => unFav(e.uid));
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
setShowType(isAuthenticated ? ShowType.ACCOUNT : ShowType.LOCAL);
|
||||
}, [isAuthenticated]);
|
||||
const showList = useMemo(() => {
|
||||
if (showType === ShowType.LOCAL) {
|
||||
return list;
|
||||
@ -71,13 +77,13 @@ export function FavePlayersPage() {
|
||||
<Flex vertical gap={48}>
|
||||
<Typography.Title>收藏的球员</Typography.Title>
|
||||
<div style={{ display: isAuthenticated ? 'block' : 'none' }}>
|
||||
<Segmented
|
||||
disabled={!isAuthenticated}
|
||||
<Radio.Group
|
||||
optionType="button"
|
||||
value={showType}
|
||||
onChange={setShowType}
|
||||
onChange={e => setShowType(e.target.value)}
|
||||
options={[
|
||||
ShowType.LOCAL,
|
||||
ShowType.ACCOUNT,
|
||||
{ label: `${ShowType.LOCAL}(${list.length})`, value: ShowType.LOCAL},
|
||||
{ label: `${ShowType.ACCOUNT}(${favListRequest.data?.length ?? 0})`, value: ShowType.ACCOUNT}
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
@ -125,18 +131,25 @@ export function FavePlayersPage() {
|
||||
</Card>
|
||||
))}
|
||||
</Flex>
|
||||
<div style={{ marginTop: 12 }}>
|
||||
{ (showType === ShowType.LOCAL && isAuthenticated && list.length > 0) && <Button onClick={handleSyncFav} icon={<UploadOutlined />}>更新收藏</Button> }
|
||||
<Flex vertical gap={12} style={{ marginTop: 12 }}>
|
||||
{ (showType === ShowType.LOCAL && isAuthenticated && list.length > 0) && (
|
||||
<Button onClick={handleSyncFav} icon={<UploadOutlined />}>同步到云端</Button>
|
||||
) }
|
||||
{ (showType === ShowType.LOCAL && !isAuthenticated && list.length > 0) && (
|
||||
<Button
|
||||
onClick={() => {
|
||||
sessionStorage.setItem('redirect', window.location.pathname);
|
||||
navigate('/user-center');
|
||||
navigate('/user-center?autoSignIn=true');
|
||||
}}
|
||||
icon={<LoginOutlined />}
|
||||
>登陆后可同步收藏球员</Button>
|
||||
) }
|
||||
</div>
|
||||
{(showType === ShowType.LOCAL && (
|
||||
<Popconfirm title='确定要清空本地收藏吗?' onConfirm={handleClearLocal}>
|
||||
<Button danger icon={<DeleteOutlined />}>清空</Button>
|
||||
</Popconfirm>
|
||||
))}
|
||||
</Flex>
|
||||
</>
|
||||
)}
|
||||
</Spin>
|
||||
|
||||
@ -2,7 +2,7 @@ import { EditOutlined, FileTextOutlined, HomeOutlined, KeyOutlined, LockOutlined
|
||||
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";
|
||||
import { useLocation, useNavigate } from "react-router";
|
||||
|
||||
enum modifyRoutes {
|
||||
username = '/account/username',
|
||||
@ -20,6 +20,18 @@ export const UserCenter = () => {
|
||||
const { signIn, isAuthenticated, signOut, getIdTokenClaims } = useLogto();
|
||||
const [user, setUser] = useState<IdTokenClaims>();
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const handleSignIn = useCallback(() => {
|
||||
const redirect = `${window.location.origin}/auth/callback`;
|
||||
signIn(redirect);
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
if (isAuthenticated) return;
|
||||
const autoSignIn = new URLSearchParams(location.search).get('autoSignIn');
|
||||
if (autoSignIn) {
|
||||
handleSignIn();
|
||||
}
|
||||
}, [isAuthenticated]);
|
||||
useEffect(() => {
|
||||
if (!isAuthenticated) return;
|
||||
getIdTokenClaims().then(claims => setUser(claims));
|
||||
@ -35,10 +47,7 @@ export const UserCenter = () => {
|
||||
type="primary"
|
||||
icon={<LogoutOutlined />}
|
||||
size="large"
|
||||
onClick={() => {
|
||||
const redirect = `${window.location.origin}/auth/callback`;
|
||||
signIn(redirect);
|
||||
}}
|
||||
onClick={handleSignIn}
|
||||
>
|
||||
登录
|
||||
</Button>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user