From 1c5de29393bd3ec54cc58ce680f7d62e8c580ed6 Mon Sep 17 00:00:00 2001 From: wappon28dev Date: Sat, 7 Sep 2024 01:12:48 +0900 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20=E3=83=81=E3=83=BC=E3=83=A0?= =?UTF-8?q?=E3=81=AE=E9=81=B8=E6=8A=9E=E3=81=A8=E5=88=9D=E6=9C=9F=E5=8C=96?= =?UTF-8?q?=E5=87=A6=E7=90=86=E3=81=AE=E6=94=B9=E5=96=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Expanded.tsx | 13 + src/components/team/Selector.tsx | 136 ---------- src/hooks/db/_esaDB.ts | 6 +- src/hooks/db/achievements.ts | 1 + src/hooks/db/unlocked-achievements.ts | 1 + src/hooks/member.ts | 6 +- src/hooks/teams.ts | 25 +- src/lib/services/esa.ts | 32 ++- src/lib/stores/teams.ts | 18 ++ src/lib/utils/fetchers.ts | 91 ------- src/pages/_app.tsx | 6 +- src/pages/auth/callback/index.tsx | 249 ++++++++++++++++++- src/pages/members/index.tsx | 19 +- src/pages/ranking/index.tsx | 21 +- src/pages/unlocked/index.tsx | 18 +- src/types/post-data/achievements.ts | 2 +- src/types/post-data/unlocked-achievements.ts | 3 +- 17 files changed, 345 insertions(+), 302 deletions(-) create mode 100644 src/components/Expanded.tsx delete mode 100644 src/components/team/Selector.tsx diff --git a/src/components/Expanded.tsx b/src/components/Expanded.tsx new file mode 100644 index 0000000..3630027 --- /dev/null +++ b/src/components/Expanded.tsx @@ -0,0 +1,13 @@ +import styled from "styled-components"; + +export const Expanded = styled.div` + height: 100%; + width: 100%; +`; + +export const ExpandedCenter = styled(Expanded)<{ gap?: number }>` + display: grid; + place-items: center; + place-content: center; + gap: ${({ gap }) => (gap != null ? `${gap * 4}px` : undefined)}; +`; diff --git a/src/components/team/Selector.tsx b/src/components/team/Selector.tsx deleted file mode 100644 index 659db18..0000000 --- a/src/components/team/Selector.tsx +++ /dev/null @@ -1,136 +0,0 @@ -import { Button, Flex } from "@radix-ui/themes"; -import { useState, type ReactElement } from "react"; -import { useNavigate } from "react-router-dom"; -import styled from "styled-components"; -import useSWRImmutable from "swr/immutable"; -import { match } from "ts-pattern"; -import { ErrorScreen } from "@/components/ErrorScreen"; -import { useMember } from "@/hooks/member"; -import { S } from "@/lib/consts"; -import { handleSWRError } from "@/lib/utils/swr"; - -const FlexStyled = styled(Flex)` - gap: 15rem; -`; - -const ButtonStyle = styled(Button)` - transform: scale(2); - padding: 0; - height: 100px; - width: 100px; -`; - -const DivCenter = styled.div` - height: 100vh; - display: flex; - justify-content: center; - align-items: center; - top: 0; -`; - -const DivStyled = styled.div` - position: absolute; - top: calc(50% + 100px); - margin-top: 20px; -`; - -const ButtonStyled = styled(Button)` - font-weight: 600; - font-family: sans-serif; - font-size: 1rem; - - background-color: #e7e7e7; - color: #00cdc2; - border: 1px solid #00cdc2; - - width: fit-content; - height: fit-content; - - padding: 1.2vh 1.3vw 1.2vh 1.8vw; - margin-top: 4vh; - margin-left: 0.3vw; - - border-radius: 50px; - display: flex; - align-items: center; - justify-content: center; - overflow: hidden; - position: relative; - z-index: 1; - - box-shadow: - 6px 6px 16px #b5bec9, - -6px -6px 16px #ffffff; - - transform-origin: 50% 50%; - transition: 300ms; - - ::after { - content: ""; - position: absolute; - width: 100%; - height: 120%; - background-color: #00cdc2; - - top: 0; - left: 0; - z-index: -1; - transform-origin: 100% 50%; - transform: scaleX(0%); - transition: transform 300ms; - } - - &:hover { - box-shadow: none; - transform: scale(1.06); - } - - &:hover ::after { - transform-origin: 0% 50%; - transform: scaleX(100%); - transform: none; - } -`; - -export function TeamSelector(): ReactElement { - const navigate = useNavigate(); - const { fetchJoinedTeams, markTeamNameAsSelected } = useMember(); - const swrJoinedTeams = useSWRImmutable("joinedTeams", fetchJoinedTeams); - const [teamName, setTeamName] = useState(""); - - return match(swrJoinedTeams) - .with(S.Loading, () =>

Loading...

) - .with(S.Success, ({ data }) => ( - - - {data.map((team) => ( - { - markTeamNameAsSelected(team.name); - setTeamName(team.name); - }} - size="4" - > - {team.name} - - ))} - - - {teamName !== "" && ( - { - navigate("/ranking"); - }} - size="4" - > - {teamName}に参加する - - )} - - - )) - .otherwise(({ data, error }) => ( - - )); -} diff --git a/src/hooks/db/_esaDB.ts b/src/hooks/db/_esaDB.ts index a23bcdb..7573bf6 100644 --- a/src/hooks/db/_esaDB.ts +++ b/src/hooks/db/_esaDB.ts @@ -6,6 +6,7 @@ import { type AnySchema } from "yup"; import { type useTeam as _useTeam } from "@/hooks/teams"; import { DB_VERSION, waitMs } from "@/lib/consts"; import { $config } from "@/lib/stores/config"; +import { enableIgnoreResCacheTemporarily } from "@/lib/stores/teams"; import { yPostData, type PostData } from "@/types/post-data/_struct"; import { type Nullable } from "@/types/utils"; @@ -16,6 +17,7 @@ export function useEsaDB( postName: string; schema: AnySchema; atom: WritableAtom>; + initData: T; }, ) { const { baseCategory } = useStore($config); @@ -30,6 +32,8 @@ export function useEsaDB( const category = `${baseCategory}/${config.postName}`; const init = async (): Promise => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + using _ = enableIgnoreResCacheTemporarily(); const postId = await searchPostId().catch(() => undefined); if (postId == null) { @@ -61,7 +65,7 @@ export function useEsaDB( const postData = { _name: "esachievement", _version: DB_VERSION, - data: undefined, + data: config.initData, } as const satisfies PostData; return await __createNewPost({ diff --git a/src/hooks/db/achievements.ts b/src/hooks/db/achievements.ts index a6624db..0822bcb 100644 --- a/src/hooks/db/achievements.ts +++ b/src/hooks/db/achievements.ts @@ -12,4 +12,5 @@ export const useAchievements = (useTeam: typeof _useTeam) => postName: "achievements", schema: yAchievementsPostData, atom: $currentAchievements, + initData: [], }); diff --git a/src/hooks/db/unlocked-achievements.ts b/src/hooks/db/unlocked-achievements.ts index cf03bad..56bee87 100644 --- a/src/hooks/db/unlocked-achievements.ts +++ b/src/hooks/db/unlocked-achievements.ts @@ -12,4 +12,5 @@ export const useUnlockedAchievements = (useTeam: typeof _useTeam) => postName: "unlockedAchievements", schema: yUnlockedAchievementsPostData, atom: $currentUnlockedAchievements, + initData: [], }); diff --git a/src/hooks/member.ts b/src/hooks/member.ts index e54b16c..13a4de8 100644 --- a/src/hooks/member.ts +++ b/src/hooks/member.ts @@ -3,7 +3,7 @@ import { useStore } from "@nanostores/react"; import { match } from "ts-pattern"; import { A } from "@/lib/consts"; -import { esaClient } from "@/lib/services/esa"; +import { getEsaClient } from "@/lib/services/esa"; import { $hasAuthenticated } from "@/lib/stores/auth"; import { $selectedTeamName } from "@/lib/stores/teams"; import { type InferResponseType } from "@/types/openapi"; @@ -18,7 +18,7 @@ export function useMember() { const fetchJoinedTeams = async (): Promise< InferResponseType<"/teams", "get">["teams"] > => { - const result = await esaClient.GET("/teams"); + const result = await getEsaClient().GET("/teams"); return await match(result) .with(A.Success, ({ data }) => data.teams) .otherwise(async ({ response }) => { @@ -35,7 +35,7 @@ export function useMember() { const fetchCurrentMember = async (): Promise< InferResponseType<"/user", "get"> > => { - const result = await esaClient.GET("/user"); + const result = await getEsaClient().GET("/user"); return await match(result) .with(A.Success, ({ data }) => data) .otherwise(async ({ response }) => { diff --git a/src/hooks/teams.ts b/src/hooks/teams.ts index 56af309..2b20f7b 100644 --- a/src/hooks/teams.ts +++ b/src/hooks/teams.ts @@ -1,7 +1,7 @@ import { useStore } from "@nanostores/react"; import { match } from "ts-pattern"; import { A } from "@/lib/consts"; -import { esaClient } from "@/lib/services/esa"; +import { getEsaClient } from "@/lib/services/esa"; import { $selectedTeamName } from "@/lib/stores/teams"; import { type InferRequestBodyType, @@ -35,7 +35,9 @@ export function useTeam() { const fetchAbout = async (): Promise< InferResponseType<"/teams/{team_name}", "get"> > => - await match(await esaClient.GET("/teams/{team_name}", paramsWithTeamName)) + await match( + await getEsaClient().GET("/teams/{team_name}", paramsWithTeamName), + ) .with(A.Success, ({ data }) => data) .otherwise(handleError); @@ -43,7 +45,7 @@ export function useTeam() { InferResponseType<"/teams/{team_name}/stats", "get"> > => await match( - await esaClient.GET("/teams/{team_name}/stats", paramsWithTeamName), + await getEsaClient().GET("/teams/{team_name}/stats", paramsWithTeamName), ) .with(A.Success, ({ data }) => data) .otherwise(handleError); @@ -52,7 +54,10 @@ export function useTeam() { InferResponseType<"/teams/{team_name}/members", "get">["members"] > => await match( - await esaClient.GET("/teams/{team_name}/members", paramsWithTeamName), + await getEsaClient().GET( + "/teams/{team_name}/members", + paramsWithTeamName, + ), ) .with(A.Success, ({ data }) => data.members) .otherwise(handleError); @@ -61,7 +66,7 @@ export function useTeam() { postBody: InferRequestBodyType<"/teams/{team_name}/posts", "post">["post"], ): Promise> => await match( - await esaClient.POST("/teams/{team_name}/posts", { + await getEsaClient().POST("/teams/{team_name}/posts", { ...paramsWithTeamName, body: { post: postBody, @@ -75,7 +80,7 @@ export function useTeam() { category: string, ): Promise["posts"]> => await match( - await esaClient.GET("/teams/{team_name}/posts", { + await getEsaClient().GET("/teams/{team_name}/posts", { params: { ...paramsWithTeamName.params, query: { @@ -93,7 +98,7 @@ export function useTeam() { InferResponseType<"/teams/{team_name}/posts/{post_number}", "get"> > => await match( - await esaClient.GET("/teams/{team_name}/posts/{post_number}", { + await getEsaClient().GET("/teams/{team_name}/posts/{post_number}", { params: { path: { ...paramsWithTeamName.params.path, @@ -115,7 +120,7 @@ export function useTeam() { InferResponseType<"/teams/{team_name}/posts/{post_number}", "patch"> > => await match( - await esaClient.PATCH("/teams/{team_name}/posts/{post_number}", { + await getEsaClient().PATCH("/teams/{team_name}/posts/{post_number}", { params: { path: { ...paramsWithTeamName.params.path, @@ -130,7 +135,7 @@ export function useTeam() { const deletePost = async (postNumber: number): Promise => { await match( - await esaClient.DELETE("/teams/{team_name}/posts/{post_number}", { + await getEsaClient().DELETE("/teams/{team_name}/posts/{post_number}", { params: { path: { ...paramsWithTeamName.params.path, @@ -147,7 +152,7 @@ export function useTeam() { InferResponseType<"/teams/{team_name}/emojis", "get"> > => await match( - await esaClient.GET("/teams/{team_name}/emojis", paramsWithTeamName), + await getEsaClient().GET("/teams/{team_name}/emojis", paramsWithTeamName), ) .with(A.Success, ({ data }) => data) .otherwise(handleError); diff --git a/src/lib/services/esa.ts b/src/lib/services/esa.ts index 9ba8ff7..2e70471 100644 --- a/src/lib/services/esa.ts +++ b/src/lib/services/esa.ts @@ -1,7 +1,8 @@ -import createClient from "openapi-fetch"; +import createClient, { type MiddlewareRequest } from "openapi-fetch"; import { type paths } from "./esa.gen"; import { getEnv } from "@/lib/consts"; import { $accessTokenData } from "@/lib/stores/auth"; +import { $shouldIgnoreResCache } from "@/lib/stores/teams"; import { type AccessTokenData } from "@/types/auth"; export function getAuthorizePageUrl(): string { @@ -35,16 +36,31 @@ export async function requestAccessTokenData( return await res.json(); } -export const esaClient = createClient({ +function processRequest(req: MiddlewareRequest): MiddlewareRequest { + const token = $accessTokenData.get(); + if (token == null) throw new Error("Access token has not been set"); + + req.headers.set("Authorization", `Bearer ${token.access_token}`); + return req; +} + +const esaClient = createClient({ baseUrl: "/api", }); esaClient.use({ - onRequest: async (req) => { - const token = $accessTokenData.get(); - if (token == null) throw new Error("Access token has not been set"); + onRequest: processRequest, +}); - req.headers.set("Authorization", `Bearer ${token.access_token}`); - return req; - }, +const esaClientUnCached = createClient({ + baseUrl: "/api", + cache: "no-cache", }); + +esaClientUnCached.use({ + onRequest: processRequest, +}); + +export function getEsaClient(): typeof esaClient { + return $shouldIgnoreResCache.get() ? esaClientUnCached : esaClient; +} diff --git a/src/lib/stores/teams.ts b/src/lib/stores/teams.ts index e10dc47..05d0742 100644 --- a/src/lib/stores/teams.ts +++ b/src/lib/stores/teams.ts @@ -1,7 +1,25 @@ +/* eslint-disable no-console */ + import { persistentAtom } from "@nanostores/persistent"; +import { atom } from "nanostores"; import { getLocalStorageKey } from "@/lib/consts"; export const $selectedTeamName = persistentAtom( getLocalStorageKey("selectedTeamName"), undefined, ); + +export const $shouldIgnoreResCache = atom(false); + +// eslint-disable-next-line @typescript-eslint/explicit-function-return-type +export function enableIgnoreResCacheTemporarily() { + console.warn("Ignore response cache temporarily"); + $shouldIgnoreResCache.set(true); + + return { + [Symbol.dispose]: () => { + console.warn("Stop ignoring response cache"); + $shouldIgnoreResCache.set(false); + }, + }; +} diff --git a/src/lib/utils/fetchers.ts b/src/lib/utils/fetchers.ts index a9fab42..9cdfe3f 100644 --- a/src/lib/utils/fetchers.ts +++ b/src/lib/utils/fetchers.ts @@ -1,7 +1,5 @@ import { type Member } from "@/types/member"; -import { type Achievement } from "@/types/post-data/achievements"; import { type UnlockedAchievement } from "@/types/post-data/unlocked-achievements"; -import { type Nullable } from "@/types/utils"; export function getUnlockedAchievementsFromMember( member: Member, @@ -9,92 +7,3 @@ export function getUnlockedAchievementsFromMember( ): UnlockedAchievement[] { return unlockedAchievements.filter((u) => u.memberEmail === member.email); } - -export async function fetchMembersAndUnlockedAchievements( - fetchMembers: () => Promise, - fetchUnlockedAchievements: () => Promise>, -): Promise<{ - members: Member[]; - unlockedAchievements: UnlockedAchievement[]; -}> { - const members = await fetchMembers(); - const unlockedAchievements = await fetchUnlockedAchievements(); - - if (members == null) { - throw new Error("`members` is null! Maybe you forgot to call `init()`"); - } - - if (unlockedAchievements == null) { - throw new Error( - "`unlockedAchievements` is null! Maybe you forgot to call `init()`", - ); - } - - return { - members, - unlockedAchievements, - }; -} - -export async function fetchAchievementsWithUnlocked( - fetchAchievements: () => Promise>, - fetchUnlockedAchievements: () => Promise>, -): Promise<{ - achievements: Achievement[]; - unlockedAchievements: UnlockedAchievement[]; -}> { - const achievements = await fetchAchievements(); - const unlockedAchievements = await fetchUnlockedAchievements(); - - if (achievements == null) { - throw new Error( - "`achievements` is null! Maybe you forgot to call `init()`", - ); - } - - if (unlockedAchievements == null) { - throw new Error( - "`unlockedAchievements` is null! Maybe you forgot to call `init()`", - ); - } - - return { - achievements, - unlockedAchievements, - }; -} -export async function fetchMembersAndUnlockedAchievementsAndAchievements( - fetchMembers: () => Promise, - fetchAchievements: () => Promise>, - fetchUnlockedAchievements: () => Promise>, -): Promise<{ - members: Member[]; - achievements: Achievement[]; - unlockedAchievements: UnlockedAchievement[]; -}> { - const members = await fetchMembers(); - const achievements = await fetchAchievements(); - const unlockedAchievements = await fetchUnlockedAchievements(); - - if (members == null) { - throw new Error("`members` is null! Maybe you forgot to call `init()`"); - } - - if (achievements == null) { - throw new Error( - "`achievements` is null! Maybe you forgot to call `init()`", - ); - } - - if (unlockedAchievements == null) { - throw new Error( - "`unlockedAchievements` is null! Maybe you forgot to call `init()`", - ); - } - - return { - members, - achievements, - unlockedAchievements, - }; -} diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index 81ea41e..831dbf2 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -4,6 +4,7 @@ import { type ReactElement } from "react"; import { Outlet, useRouteError } from "react-router-dom"; import styled from "styled-components"; import { Center } from "@/components/Center"; +import { Expanded } from "@/components/Expanded"; import { Header } from "@/components/Header"; import { Redirects } from "@/components/Redirects"; @@ -48,7 +49,6 @@ const Main = styled.main` font-family: "Noto Sans JP Variable"; word-break: keep-all; `; -const BodyStyle = styled.div``; const ThemeStyle = styled(Theme)` background-color: #e7e7e7; overflow: hidden; @@ -64,9 +64,9 @@ export default function Layout(): ReactElement {
- + - +
diff --git a/src/pages/auth/callback/index.tsx b/src/pages/auth/callback/index.tsx index 4afb883..f770012 100644 --- a/src/pages/auth/callback/index.tsx +++ b/src/pages/auth/callback/index.tsx @@ -1,16 +1,249 @@ import { useStore } from "@nanostores/react"; -import { type ReactElement } from "react"; +import { Button, Flex } from "@radix-ui/themes"; +import { type ReactNode, useEffect, useState, type ReactElement } from "react"; +import { useNavigate } from "react-router-dom"; +import styled from "styled-components"; import useSWRImmutable from "swr/immutable"; -import { match } from "ts-pattern"; -import { Center } from "@/components/Center"; +import { match, P } from "ts-pattern"; import { ErrorScreen } from "@/components/ErrorScreen"; -import { TeamSelector } from "@/components/team/Selector"; -import { S } from "@/lib/consts"; +import { Expanded, ExpandedCenter } from "@/components/Expanded"; +import { useAchievements } from "@/hooks/db/achievements"; +import { useUnlockedAchievements } from "@/hooks/db/unlocked-achievements"; +import { useMember } from "@/hooks/member"; +import { useTeam } from "@/hooks/teams"; +import { APP_NAME, S } from "@/lib/consts"; import { requestAccessTokenData } from "@/lib/services/esa"; import { $accessTokenData } from "@/lib/stores/auth"; import { handleSWRError } from "@/lib/utils/swr"; -import { useNavigate } from "@/router"; import { type AccessTokenData } from "@/types/auth"; +import { type ArrayElem } from "@/types/utils"; + +const Heading = styled.h1` + font-size: 1.5rem; + font-weight: bold; +`; + +const TeamIcon = styled.img` + border-radius: 20px; + cursor: pointer; + width: 130px; + height: 130px; + + &:hover, + &[aria-selected="true"] { + box-shadow: 0 0 10px #00cdc2; + } +`; + +const TeamInfo = styled(Flex)` + direction: column; + align-items: center; + + > p { + min-height: 1lh; + } + + p:first-child { + font-size: 1.2rem; + font-weight: bold; + } + p:last-child { + color: gray; + } +`; + +const ButtonStyled = styled(Button)` + font-weight: 600; + font-family: sans-serif; + font-size: 1rem; + cursor: pointer; + + background-color: #e7e7e7; + color: #00cdc2; + border: 1px solid #00cdc2; + + width: fit-content; + height: fit-content; + + padding: 1.2vh 1.3vw 1.2vh 1.8vw; + + border-radius: 50px; + display: flex; + align-items: center; + justify-content: center; + overflow: hidden; + position: relative; + z-index: 1; + + box-shadow: + 6px 6px 16px #b5bec9, + -6px -6px 16px #ffffff; + + transform-origin: 50% 50%; + transition: 300ms; + + ::after { + content: ""; + position: absolute; + width: 100%; + height: 120%; + background-color: #00cdc2; + + top: 0; + left: 0; + z-index: -1; + transform-origin: 100% 50%; + transform: scaleX(0%); + transition: transform 300ms; + } + + &:hover { + box-shadow: none; + transform: scale(1.06); + + &::after { + transform-origin: 0% 50%; + transform: scaleX(100%); + transform: none; + } + } + + &:disabled { + cursor: not-allowed; + opacity: 0.3; + transform: scale(1.06); + box-shadow: none; + } +`; + +type Team = ArrayElem< + Awaited["fetchJoinedTeams"]>> +>; + +type InitStatus = + | { + type: "READY" | "LOADING" | "SUCCESS"; + } + | { + type: "ERROR"; + error: Error; + }; + +function TeamSelectorLoading({ + setInitStatus, +}: { + setInitStatus: (status: InitStatus) => void; +}): ReactNode { + const navigate = useNavigate(); + const { init: initAchievements } = useAchievements(useTeam); + const { init: initUnlockedAchievements } = useUnlockedAchievements(useTeam); + + async function initDB(): Promise { + await initAchievements(); + await initUnlockedAchievements(); + + navigate("/ranking"); + } + + useEffect(() => { + void initDB() + .then(() => { + setInitStatus({ type: "SUCCESS" }); + }) + .catch((error) => { + setInitStatus({ type: "ERROR", error }); + }); + }); + + return `${APP_NAME} を初期化中...`; +} + +function TeamSelector(): ReactElement { + const { fetchJoinedTeams, markTeamNameAsSelected } = useMember(); + + const [hoveredTeam, setHoveredTeamName] = useState(); + const [selectedTeam, setSelectedTeamName] = useState(); + const [initStatus, setInitStatus] = useState({ + type: "READY", + }); + + const swrJoinedTeams = useSWRImmutable("joinedTeams", fetchJoinedTeams); + const activeTeam = selectedTeam ?? hoveredTeam; + + return match(swrJoinedTeams) + .with(S.Loading, () =>

Loading...

) + .with(S.Success, ({ data }) => ( + + チームを選択してください + + {data.map((team) => ( + { + const alreadySelected = selectedTeam === team; + setSelectedTeamName(alreadySelected ? undefined : team); + }} + onMouseEnter={() => { + setHoveredTeamName(team); + }} + onMouseLeave={() => { + setHoveredTeamName(undefined); + }} + src={team.icon} + /> + ))} + + +

{activeTeam?.name}

+

{activeTeam?.description}

+
+ + { + setInitStatus({ type: "LOADING" }); + }} + size="4" + > + {match({ + selectedTeam, + initStatus, + }) + .with({ initStatus: { type: "LOADING" } }, () => { + if (selectedTeam == null) { + throw new Error("selectedTeam is null"); + } + markTeamNameAsSelected(selectedTeam.name); + + return ; + }) + .with( + { initStatus: { type: "SUCCESS" } }, + () => `${APP_NAME} を初期化しました!`, + ) + .with( + { initStatus: { type: "ERROR" } }, + ({ initStatus: { error } }) => , + ) + .with( + { selectedTeam: P.nullish }, + () => "チームを選択してください…", + ) + .with( + { selectedTeam: P.nonNullable }, + () => `${activeTeam?.name} で参加`, + ) + .exhaustive()} + + +
+ )) + .otherwise(({ data, error }) => ( + + )); +} export default function Page(): ReactElement { const swrTokenAndTeams = useSWRImmutable("tokenAndTeams", fetchTokenAndTeams); @@ -35,13 +268,13 @@ export default function Page(): ReactElement { } return ( -
+ {match(swrTokenAndTeams) .with(S.Loading, () =>

Loading...

) .with(S.Success, () => ) .otherwise(({ data, error }) => ( ))} -
+ ); } diff --git a/src/pages/members/index.tsx b/src/pages/members/index.tsx index 9688966..708b4ea 100644 --- a/src/pages/members/index.tsx +++ b/src/pages/members/index.tsx @@ -8,10 +8,7 @@ import { MemberCard } from "@/components/member/Card"; import { useUnlockedAchievements } from "@/hooks/db/unlocked-achievements"; import { useTeam } from "@/hooks/teams"; import { S } from "@/lib/consts"; -import { - fetchMembersAndUnlockedAchievements, - getUnlockedAchievementsFromMember, -} from "@/lib/utils/fetchers"; +import { getUnlockedAchievementsFromMember } from "@/lib/utils/fetchers"; import { handleSWRError } from "@/lib/utils/swr"; const BoxStyle = styled(Box)` @@ -23,16 +20,12 @@ const BoxStyle = styled(Box)` export default function Page(): ReactElement { const { fetchMembers } = useTeam(); const { fetch: fetchUnlockedAchievements } = useUnlockedAchievements(useTeam); - const swrMembersAndUnlockedAchievements = useSWRImmutable( - "membersAndUnlockedAchievements", - async () => - await fetchMembersAndUnlockedAchievements( - fetchMembers, - fetchUnlockedAchievements, - ), - ); + const swrMU = useSWRImmutable("mu", async () => ({ + members: await fetchMembers(), + unlockedAchievements: await fetchUnlockedAchievements(), + })); - return match(swrMembersAndUnlockedAchievements) + return match(swrMU) .with(S.Loading, () =>
Loading...
) .with(S.Success, ({ data: { members, unlockedAchievements } }) => ( <> diff --git a/src/pages/ranking/index.tsx b/src/pages/ranking/index.tsx index 441a79d..947eb75 100644 --- a/src/pages/ranking/index.tsx +++ b/src/pages/ranking/index.tsx @@ -10,10 +10,7 @@ import { useAchievements } from "@/hooks/db/achievements"; import { useUnlockedAchievements } from "@/hooks/db/unlocked-achievements"; import { useTeam } from "@/hooks/teams"; import { S } from "@/lib/consts"; -import { - fetchMembersAndUnlockedAchievementsAndAchievements, - getUnlockedAchievementsFromMember, -} from "@/lib/utils/fetchers"; +import { getUnlockedAchievementsFromMember } from "@/lib/utils/fetchers"; import { handleSWRError } from "@/lib/utils/swr"; const RankingListStyle = styled.div` @@ -54,17 +51,13 @@ export default function Page(): ReactElement { const { fetchMembers } = useTeam(); const { fetch: fetchAchievements } = useAchievements(useTeam); const { fetch: fetchUnlockedAchievements } = useUnlockedAchievements(useTeam); - const swrMembersAndUnlockedAchievementsAndAchievements = useSWRImmutable( - "membersAndUnlockedAchievementsAndAchievements", - async () => - await fetchMembersAndUnlockedAchievementsAndAchievements( - fetchMembers, - fetchAchievements, - fetchUnlockedAchievements, - ), - ); + const swrAMU = useSWRImmutable("amu", async () => ({ + achievements: await fetchAchievements(), + members: await fetchMembers(), + unlockedAchievements: await fetchUnlockedAchievements(), + })); - return match(swrMembersAndUnlockedAchievementsAndAchievements) + return match(swrAMU) .with(S.Loading, () =>

Loading...

) .with( S.Success, diff --git a/src/pages/unlocked/index.tsx b/src/pages/unlocked/index.tsx index e69e85b..ddb457a 100644 --- a/src/pages/unlocked/index.tsx +++ b/src/pages/unlocked/index.tsx @@ -10,7 +10,6 @@ import { useUnlockedAchievements } from "@/hooks/db/unlocked-achievements"; import { useMember } from "@/hooks/member"; import { useTeam } from "@/hooks/teams"; import { S } from "@/lib/consts"; -import { fetchAchievementsWithUnlocked } from "@/lib/utils/fetchers"; import { handleSWRError } from "@/lib/utils/swr"; import { type CurrentMember } from "@/types/member"; import { type Achievement } from "@/types/post-data/achievements"; @@ -29,18 +28,11 @@ export default function Page(): ReactElement { useUnlockedAchievements(useTeam); const swrAchievementsWithUnlocked = useSWRImmutable( "achievementsWithUnlocked", - async () => { - const achievementsKit = await fetchAchievementsWithUnlocked( - fetchAchievements, - fetchUnlockedAchievements, - ); - const currentMember = await fetchCurrentMember(); - - return { - ...achievementsKit, - currentMember, - }; - }, + async () => ({ + achievements: await fetchAchievements(), + unlockedAchievements: await fetchUnlockedAchievements(), + currentMember: await fetchCurrentMember(), + }), ); const [isUILocked, setIsUILocked] = useState(false); diff --git a/src/types/post-data/achievements.ts b/src/types/post-data/achievements.ts index d6cdb49..917095d 100644 --- a/src/types/post-data/achievements.ts +++ b/src/types/post-data/achievements.ts @@ -13,5 +13,5 @@ export const yAchievement = yup.object().shape({ export type Achievement = InferType; export type AchievementTag = Achievement["tags"]; -export const yAchievementsPostData = array().of(yAchievement); +export const yAchievementsPostData = array().of(yAchievement).required(); export type AchievementsPostData = InferType; diff --git a/src/types/post-data/unlocked-achievements.ts b/src/types/post-data/unlocked-achievements.ts index e63dd73..9c44580 100644 --- a/src/types/post-data/unlocked-achievements.ts +++ b/src/types/post-data/unlocked-achievements.ts @@ -8,7 +8,8 @@ export const yUnlockedAchievement = yup.object().shape({ }); export const yUnlockedAchievementsPostData = yup .array() - .of(yUnlockedAchievement); + .of(yUnlockedAchievement) + .required(); export type UnlockedAchievement = InferType; export type UnlockedAchievementsPostData = InferType<