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
|
FROM oven/bun:latest
|
||||||
COPY . /app
|
COPY . /app
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
RUN bun install
|
RUN bun install && bunx --bun prisma generate
|
||||||
ENTRYPOINT [ "bun", "start"]
|
ENTRYPOINT [ "bun", "start"]
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
|
|||||||
@ -22,6 +22,7 @@ const StyledContainer = styled.div`
|
|||||||
|
|
||||||
export function FavButton(props: Props) {
|
export function FavButton(props: Props) {
|
||||||
const { isAuthenticated, getIdTokenClaims } = useLogto();
|
const { isAuthenticated, getIdTokenClaims } = useLogto();
|
||||||
|
const { fav, unFav } = useFavPlayerStore(store => store);
|
||||||
const favReq = useRequest(async () => {
|
const favReq = useRequest(async () => {
|
||||||
if (!isAuthenticated) return;
|
if (!isAuthenticated) return;
|
||||||
const claims = await getIdTokenClaims();
|
const claims = await getIdTokenClaims();
|
||||||
@ -48,10 +49,12 @@ export function FavButton(props: Props) {
|
|||||||
if (!props.user) return;
|
if (!props.user) return;
|
||||||
setValue(value);
|
setValue(value);
|
||||||
if (value) {
|
if (value) {
|
||||||
|
fav(props.user);
|
||||||
favReq.run();
|
favReq.run();
|
||||||
setValue(1);
|
setValue(1);
|
||||||
} else {
|
} else {
|
||||||
unFavReq.run();
|
unFavReq.run();
|
||||||
|
unFav(props.user.uid);
|
||||||
setValue(0);
|
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 { useFavPlayerStore } 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 } from "@logto/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 { useRequest } from "ahooks";
|
||||||
import type { XCXProfile } from "../types";
|
import type { XCXProfile } from "../types";
|
||||||
|
|
||||||
@ -18,7 +18,7 @@ enum ShowType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function FavePlayersPage() {
|
export function FavePlayersPage() {
|
||||||
const { favMap } = useFavPlayerStore(state => state);
|
const { favMap, unFav } = useFavPlayerStore(state => state);
|
||||||
const [sortType, setSortType] = useState<SortType>(SortType.DEFAULT);
|
const [sortType, setSortType] = useState<SortType>(SortType.DEFAULT);
|
||||||
const list = useMemo(() => {
|
const list = useMemo(() => {
|
||||||
const l = Object.values(favMap);
|
const l = Object.values(favMap);
|
||||||
@ -30,7 +30,7 @@ export function FavePlayersPage() {
|
|||||||
}
|
}
|
||||||
}, [favMap, sortType]);
|
}, [favMap, sortType]);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { isAuthenticated, getIdTokenClaims, signIn } = useLogto();
|
const { isAuthenticated, getIdTokenClaims } = useLogto();
|
||||||
const favListRequest = useRequest<XCXProfile[], [string]>(async (aud: string) => {
|
const favListRequest = useRequest<XCXProfile[], [string]>(async (aud: string) => {
|
||||||
const res = await fetch(`/api/fav/${aud}`);
|
const res = await fetch(`/api/fav/${aud}`);
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
@ -55,6 +55,12 @@ export function FavePlayersPage() {
|
|||||||
await favListRequest.runAsync(aud!);
|
await favListRequest.runAsync(aud!);
|
||||||
}, [isAuthenticated, list]);
|
}, [isAuthenticated, list]);
|
||||||
const [showType, setShowType] = useState(ShowType.LOCAL);
|
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(() => {
|
const showList = useMemo(() => {
|
||||||
if (showType === ShowType.LOCAL) {
|
if (showType === ShowType.LOCAL) {
|
||||||
return list;
|
return list;
|
||||||
@ -71,13 +77,13 @@ export function FavePlayersPage() {
|
|||||||
<Flex vertical gap={48}>
|
<Flex vertical gap={48}>
|
||||||
<Typography.Title>收藏的球员</Typography.Title>
|
<Typography.Title>收藏的球员</Typography.Title>
|
||||||
<div style={{ display: isAuthenticated ? 'block' : 'none' }}>
|
<div style={{ display: isAuthenticated ? 'block' : 'none' }}>
|
||||||
<Segmented
|
<Radio.Group
|
||||||
disabled={!isAuthenticated}
|
optionType="button"
|
||||||
value={showType}
|
value={showType}
|
||||||
onChange={setShowType}
|
onChange={e => setShowType(e.target.value)}
|
||||||
options={[
|
options={[
|
||||||
ShowType.LOCAL,
|
{ label: `${ShowType.LOCAL}(${list.length})`, value: ShowType.LOCAL},
|
||||||
ShowType.ACCOUNT,
|
{ label: `${ShowType.ACCOUNT}(${favListRequest.data?.length ?? 0})`, value: ShowType.ACCOUNT}
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -125,18 +131,25 @@ export function FavePlayersPage() {
|
|||||||
</Card>
|
</Card>
|
||||||
))}
|
))}
|
||||||
</Flex>
|
</Flex>
|
||||||
<div style={{ marginTop: 12 }}>
|
<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={handleSyncFav} icon={<UploadOutlined />}>同步到云端</Button>
|
||||||
|
) }
|
||||||
{ (showType === ShowType.LOCAL && !isAuthenticated && list.length > 0) && (
|
{ (showType === ShowType.LOCAL && !isAuthenticated && list.length > 0) && (
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
sessionStorage.setItem('redirect', window.location.pathname);
|
sessionStorage.setItem('redirect', window.location.pathname);
|
||||||
navigate('/user-center');
|
navigate('/user-center?autoSignIn=true');
|
||||||
}}
|
}}
|
||||||
icon={<LoginOutlined />}
|
icon={<LoginOutlined />}
|
||||||
>登陆后可同步收藏球员</Button>
|
>登陆后可同步收藏球员</Button>
|
||||||
) }
|
) }
|
||||||
</div>
|
{(showType === ShowType.LOCAL && (
|
||||||
|
<Popconfirm title='确定要清空本地收藏吗?' onConfirm={handleClearLocal}>
|
||||||
|
<Button danger icon={<DeleteOutlined />}>清空</Button>
|
||||||
|
</Popconfirm>
|
||||||
|
))}
|
||||||
|
</Flex>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Spin>
|
</Spin>
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import { EditOutlined, FileTextOutlined, HomeOutlined, KeyOutlined, LockOutlined
|
|||||||
import { useLogto, type IdTokenClaims } from "@logto/react";
|
import { useLogto, type IdTokenClaims } from "@logto/react";
|
||||||
import { Avatar, Button, Divider, Flex, Modal, Typography } from "antd";
|
import { Avatar, Button, Divider, Flex, Modal, Typography } from "antd";
|
||||||
import { useCallback, useEffect, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
import { useNavigate } from "react-router";
|
import { useLocation, useNavigate } from "react-router";
|
||||||
|
|
||||||
enum modifyRoutes {
|
enum modifyRoutes {
|
||||||
username = '/account/username',
|
username = '/account/username',
|
||||||
@ -20,6 +20,18 @@ export const UserCenter = () => {
|
|||||||
const { signIn, isAuthenticated, signOut, getIdTokenClaims } = useLogto();
|
const { signIn, isAuthenticated, signOut, getIdTokenClaims } = useLogto();
|
||||||
const [user, setUser] = useState<IdTokenClaims>();
|
const [user, setUser] = useState<IdTokenClaims>();
|
||||||
const navigate = useNavigate();
|
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(() => {
|
useEffect(() => {
|
||||||
if (!isAuthenticated) return;
|
if (!isAuthenticated) return;
|
||||||
getIdTokenClaims().then(claims => setUser(claims));
|
getIdTokenClaims().then(claims => setUser(claims));
|
||||||
@ -35,10 +47,7 @@ export const UserCenter = () => {
|
|||||||
type="primary"
|
type="primary"
|
||||||
icon={<LogoutOutlined />}
|
icon={<LogoutOutlined />}
|
||||||
size="large"
|
size="large"
|
||||||
onClick={() => {
|
onClick={handleSignIn}
|
||||||
const redirect = `${window.location.origin}/auth/callback`;
|
|
||||||
signIn(redirect);
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
登录
|
登录
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user