Skip to content

Commit

Permalink
Merge pull request #642 from sparcs-kaist/#641-create-leaderboard-page
Browse files Browse the repository at this point in the history
#641 create leaderboard page
  • Loading branch information
14KGun authored Sep 20, 2023
2 parents 3ac3782 + 2fa4d81 commit 30aeade
Show file tree
Hide file tree
Showing 9 changed files with 294 additions and 28 deletions.
27 changes: 14 additions & 13 deletions src/components/Empty.tsx
Original file line number Diff line number Diff line change
@@ -1,40 +1,41 @@
import { ReactNode } from "react";
import { HTMLProps, ReactNode } from "react";

import theme from "tools/theme";

import NotInterestedIcon from "@material-ui/icons/NotInterested";

type ScreenType = "mobile" | "pc";

type EmptyProps = {
screen: ScreenType;
children: ReactNode;
marginBottom?: PixelValue;
};
type: "mobile" | "pc";
children?: ReactNode;
className?: string;
} & HTMLProps<HTMLDivElement>;

const Empty = ({ screen, children, marginBottom }: EmptyProps) => {
const styleCommon: CSS = {
const Empty = ({ type, children, className, ...divProps }: EmptyProps) => {
const styleCommon = {
display: "flex",
justifyContent: "center",
...theme.font14_bold,
color: theme.gray_text,
columnGap: "6px",
marginBottom: marginBottom,
};
const styleMobile: CSS = {
const styleMobile = {
...styleCommon,
padding: "24px 0",
borderRadius: "12px",
backgroundColor: theme.gray_background,
border: "0.25px solid " + theme.gray_line,
};
const stylePC: CSS = {
const stylePC = {
...styleCommon,
padding: "48px 0 26px",
};

return (
<div style={screen === "pc" ? stylePC : styleMobile}>
<div
css={type === "pc" ? stylePC : styleMobile}
className={className}
{...divProps}
>
<NotInterestedIcon style={{ fontSize: "16px" }} />
{children}
</div>
Expand Down
2 changes: 1 addition & 1 deletion src/components/ModalPopup/Body/BodyReport.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ const ReportList = (props: BodyReportProps) => {
};
if (!props.selectedReportHistory?.length) {
return (
<Empty screen="mobile">
<Empty type="mobile">
{props.option === "Reporting"
? t("page_report.empty_reported")
: t("page_report.empty_received")}
Expand Down
6 changes: 5 additions & 1 deletion src/components/Title.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import theme from "tools/theme";

import AddRoundedIcon from "@mui/icons-material/AddRounded";
import ConfirmationNumberRoundedIcon from "@mui/icons-material/ConfirmationNumberRounded";
import EmojiEventsRoundedIcon from "@mui/icons-material/EmojiEventsRounded";
import ErrorOutlineRoundedIcon from "@mui/icons-material/ErrorOutlineRounded";
import FavoriteRoundedIocn from "@mui/icons-material/FavoriteRounded";
import FeedRounedIcon from "@mui/icons-material/FeedRounded";
Expand Down Expand Up @@ -36,7 +37,8 @@ type IconProps = {
| "notice"
| "ticket"
| "festival"
| "shop";
| "shop"
| "leaderboard";
};

type TitleProps = {
Expand Down Expand Up @@ -85,6 +87,8 @@ const Icon = ({ type: icon }: IconProps) => {
return <FestivalRoundedIcon style={iconStyle} />;
case "shop":
return <ShoppingCartRoundedIcon style={iconStyle} />;
case "leaderboard":
return <EmojiEventsRoundedIcon style={iconStyle} />;
default:
return <></>;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@ const wrapUseQuery =
(method: Method) =>
(url: string, data?: any, dep?: [any]): [any, any, boolean] => {
const axios = useAxios();
const [res, setRes] = useState<any>({});
const [res, setRes] = useState<{ error: any; data: any }>({
error: null,
data: null,
});
const [loading, setLoading] = useState<any>(true);
const latestReqID = useRef(0);

Expand Down
268 changes: 263 additions & 5 deletions src/pages/Event/Event2023FallLeaderboard.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,247 @@
import { useMemo } from "react";

import { useValueRecoilState } from "hooks/useFetchRecoilState";
import useQuery from "hooks/useTaxiAPI";

import AdaptiveDiv from "components/AdaptiveDiv";
import Empty from "components/Empty";
import Footer from "components/Footer";
import HeaderWithLeftNav from "components/Header/HeaderWithLeftNav";
import Title from "components/Title";
import ProfileImage from "components/User/ProfileImage";
import WhiteContainer from "components/WhiteContainer";

import theme from "tools/theme";

import { ReactComponent as Ticket1Icon } from "static/events/2023fallTicket1.svg";
import { ReactComponent as Ticket2Icon } from "static/events/2023fallTicket2.svg";

const LeaderboardTopBar = () => (
<div
css={{
display: "flex",
alignItems: "center",
padding: "8px 12px",
gap: "8px",
...theme.font12,
color: theme.purple_disabled,
marginTop: "-10px",
}}
>
<span>순위</span>
<span css={{ marginLeft: "16px" }}>플레이어</span>
<Ticket1Icon
css={{
marginLeft: "auto",
width: "30px",
height: "27px",
marginTop: "-4px",
marginBottom: "-4px",
flexShrink: 0,
}}
/>
<Ticket2Icon
css={{
width: "30px",
height: "27px",
marginTop: "-4px",
marginBottom: "-4px",
flexShrink: 0,
}}
/>
<span css={{ width: "56px" }}>상품 확률</span>
</div>
);

type LeaderboardElem = {
nickname: string;
profileImageUrl: string;
ticket1Amount: number;
ticket2Amount: number;
probability: number;
};

type LeaderboardItemProps = {
value: LeaderboardElem;
rank: number;
isMe?: boolean;
};

const LeaderboardItem = ({
value,
rank,
isMe = false,
}: LeaderboardItemProps) => {
const styleContainer = (index: number) => {
switch (index) {
case 0:
return {
color: "#C6B200",
border: "0.5px solid #E4CD00",
background: "#FFEE5A",
boxShadow: "0px 1px 5px 0px #E4CD00",
...theme.font20,
fontSize: "24px",
};
case 1:
return {
color: "#96BCC6",
border: "0.5px solid #BBD4DA",
background: "#EEF6F8",
boxShadow: "0px 1px 5px 0px #BBD4DA",
...theme.font20,
fontSize: "24px",
};
case 2:
return {
color: "#CD6830",
border: "0.5px solid #DE8C5D",
background: "#FFC5A4",
boxShadow: "0px 1px 5px 0px #DE8C5D",
...theme.font20,
fontSize: "24px",
};
case -1:
return {
color: theme.purple_disabled,
background: theme.purple,
boxShadow: theme.shadow,
...theme.font20,
};
default:
return {
color: theme.purple_disabled,
background: theme.white,
boxShadow: theme.shadow,
...theme.font20,
};
}
};

const styleText = (index: number) => {
switch (index) {
case 0:
return "#6B6000";
case 1:
return "#337182";
case 2:
return "#9E3800";
case -1:
return theme.white;
default:
return theme.purple;
}
};

const styleTicketText = {
...theme.font16,
width: "30px",
flexShrink: 0,
textAlign: "center",
} as CSS;

const realProbability = useMemo(
() =>
1 -
(1 - value.probability) **
(value.ticket1Amount * 1 + value.ticket2Amount * 5),
[value]
);

return (
<WhiteContainer
css={{
display: "flex",
alignItems: "center",
padding: "8px 15px",
marginBottom: "8px",
gap: "8px",
...styleContainer(isMe ? -1 : rank),
}}
>
{rank + 1}
<div
css={{
width: "30px",
height: "30px",
borderRadius: "15px",
overflow: "hidden",
flexShrink: 0,
flexGrow: 0,
marginLeft: "5px",
}}
>
<ProfileImage url={value.profileImageUrl} />
</div>
{isMe && (
<div
css={{
width: "20px",
height: "20px",
...theme.font12_bold,
display: "flex",
alignItems: "center",
justifyContent: "center",
background: theme.purple_disabled,
borderRadius: "5px",
color: theme.purple,
}}
>
</div>
)}
<div
css={{
...theme.font16_bold,
...theme.ellipsis,
color: isMe ? theme.white : theme.black,
}}
>
{value.nickname}
</div>
<span css={{ marginLeft: "auto", ...styleTicketText }}>
{value.ticket1Amount || 0}
</span>
<span css={{ ...styleTicketText }}>{value.ticket2Amount || 0}</span>
<div
css={{
color: styleText(isMe ? -1 : rank),
...theme.font16_bold,
width: "56px",
flexShrink: 0,
textAlign: "right",
}}
title={(realProbability * 100).toString()}
>
<span css={{ ...theme.font20 }}>
{Math.trunc(realProbability * 100) || 0}
</span>
.{Math.floor(((realProbability * 100) % 1) * 10)}%
</div>
</WhiteContainer>
);
};

const Event2023FallLeaderboard = () => {
const { leaderboard, rank, probability } = useQuery.get(
"/events/2023fall/public-notice/leaderboard"
)[1] || { leaderboard: [], rank: 0 };
const { ticket1Amount, ticket2Amount } =
useValueRecoilState("event2023FallInfo") || {};
const { nickname, profileImgUrl } = useValueRecoilState("loginInfo") || {};
const myLeaderboardInfo = useMemo<Nullable<LeaderboardElem>>(() => {
if (!nickname || !profileImgUrl || !probability) return null;
return {
nickname,
profileImageUrl: profileImgUrl,
ticket1Amount: ticket1Amount || 0,
ticket2Amount: ticket2Amount || 0,
probability,
};
}, [nickname, profileImgUrl, ticket1Amount, ticket2Amount, probability]);

return (
<AdaptiveDiv type="center">
<>
<HeaderWithLeftNav
value="leaderboard"
options={[
Expand All @@ -21,10 +258,31 @@ const Event2023FallLeaderboard = () => {
},
]}
/>
<Title icon="notice" isHeader>
리더보드
</Title>
</AdaptiveDiv>
<AdaptiveDiv type="center">
<Title icon="leaderboard" isHeader>
리더보드
</Title>
{leaderboard.length > 0 ? (
<>
<LeaderboardTopBar />
{leaderboard.map((elem: LeaderboardElem, index: number) => (
<LeaderboardItem
key={index}
rank={index}
value={elem}
isMe={index === rank - 1}
/>
))}
{rank > 20 && myLeaderboardInfo && (
<LeaderboardItem rank={rank - 1} value={myLeaderboardInfo} isMe />
)}
</>
) : (
<Empty type="mobile">리더보드가 비어있습니다.</Empty>
)}
</AdaptiveDiv>
<Footer type="event-2023fall" />
</>
);
};

Expand Down
2 changes: 1 addition & 1 deletion src/pages/Home/RoomList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ const RoomList = (props: RoomListProps) => {
)}
</>
) : (
<Empty screen="mobile" marginBottom="15px">
<Empty type="mobile" css={{ marginBottom: "15px" }}>
해당 요일에 개설된 방이 없습니다
</Empty>
)}
Expand Down
Loading

0 comments on commit 30aeade

Please sign in to comment.