diff --git a/package.json b/package.json index 727a286..7a2493c 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,8 @@ "ress": "^5.0.2", "sharp": "^0.32.1", "swr": "^2.1.5", - "uuid": "^9.0.0" + "uuid": "^9.0.0", + "zod": "^3.21.4" }, "devDependencies": { "@stylelint/postcss-css-in-js": "^0.38.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6d68257..1cc4f16 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -59,6 +59,9 @@ dependencies: uuid: specifier: ^9.0.0 version: 9.0.0 + zod: + specifier: ^3.21.4 + version: 3.21.4 devDependencies: '@stylelint/postcss-css-in-js': diff --git a/src/components/DashBoard/MyAchieves.tsx b/src/components/DashBoard/MyAchieves.tsx index 4e5a844..68d5a7a 100644 --- a/src/components/DashBoard/MyAchieves.tsx +++ b/src/components/DashBoard/MyAchieves.tsx @@ -1,20 +1,27 @@ -import { Flex, Loader } from '@mantine/core'; +import { Loader } from '@mantine/core'; import type { FC } from 'react'; -import { MissionPanel } from '@/components/DashBoard/MissionPanel'; +import useSWR from 'swr'; +import { MissionList } from '@/components/MissionList'; import { useUserInfo } from '@/hooks/useUserInfo'; +import { fetcher } from '@/lib/fetcher'; +import { getApiBaseUrl } from '@/lib/getApiBaseUrl'; +import type { GetMissionsResponse } from '@/schema/schema'; export const MyAchieves: FC = () => { - const { data, error } = useUserInfo(); + const { data: user, error } = useUserInfo(); + const { data: missions } = useSWR( + `${getApiBaseUrl()}/missions`, + fetcher, + ); - if (data == undefined) return ; + if (user == undefined || missions === undefined) + return ; if (error !== undefined) return
Something went wrong
; - return ( - - {data.achieves.map((missionId) => ( - - ))} - - ); + const achieves = missions + ? missions.filter((mission) => user.achieves.includes(mission.id)) + : undefined; + + return ; }; diff --git a/src/components/DashBoard/index.tsx b/src/components/DashBoard/index.tsx index c11c582..58c970a 100644 --- a/src/components/DashBoard/index.tsx +++ b/src/components/DashBoard/index.tsx @@ -70,9 +70,9 @@ export const DashBoard: FC = () => {

Your recent achievement

-
+
-
+ diff --git a/src/components/MissionList.tsx b/src/components/MissionList.tsx new file mode 100644 index 0000000..0c1e587 --- /dev/null +++ b/src/components/MissionList.tsx @@ -0,0 +1,76 @@ +import { css } from '@emotion/react'; +import { Card, Overlay, useMantineTheme, Text, Skeleton } from '@mantine/core'; +import { IconCheck } from '@tabler/icons-react'; +import Link from 'next/link'; +import type { FC } from 'react'; +import type { GetMissionResponse, GetUserResponse } from '@/schema/schema'; + +export type MissionListProps = { + missions?: GetMissionResponse[]; + user?: GetUserResponse; +}; + +export const MissionList: FC = ({ missions, user }) => { + const theme = useMantineTheme(); + + return ( +
+ {missions + ? missions.map((mission) => ( + + + {user?.achieves.includes(mission.id) && ( + + + + )} + + {mission.name} + + + {mission.description} + + + + )) + : new Array(3).fill(0).map((_, i) => )} +
+ ); +}; diff --git a/src/pages/missions.tsx b/src/pages/missions.tsx index 385631b..0388917 100644 --- a/src/pages/missions.tsx +++ b/src/pages/missions.tsx @@ -1,18 +1,10 @@ import { css } from '@emotion/react'; -import { - Card, - Overlay, - RingProgress, - Skeleton, - Text, - useMantineTheme, -} from '@mantine/core'; -import { IconCheck } from '@tabler/icons-react'; +import { RingProgress, useMantineTheme } from '@mantine/core'; import type { NextPage } from 'next'; -import Link from 'next/link'; import useSWR from 'swr'; import { Description } from '@/components/Description'; import { Layout } from '@/components/Layout'; +import { MissionList } from '@/components/MissionList'; import { useUserInfo } from '@/hooks/useUserInfo'; import { fetcher } from '@/lib/fetcher'; import { getApiBaseUrl } from '@/lib/getApiBaseUrl'; @@ -63,67 +55,7 @@ const Missions: NextPage = () => { /> )} -
- {missions - ? missions.map((mission) => ( - - - {user?.achieves.includes(mission.id) && ( - - - - )} - - {mission.name} - - - {mission.description} - - - - )) - : new Array(3).fill(0).map((_, i) => )} -
+ diff --git a/src/pages/missions/[missionId].tsx b/src/pages/missions/[missionId].tsx index fa25ff1..574f1e5 100644 --- a/src/pages/missions/[missionId].tsx +++ b/src/pages/missions/[missionId].tsx @@ -10,9 +10,10 @@ import { Text, useMantineTheme, } from '@mantine/core'; -import type { NextPage } from 'next'; +import type { GetStaticPaths, GetStaticProps, NextPage } from 'next'; import { useRouter } from 'next/router'; import useSWR from 'swr'; +import { z } from 'zod'; import { Description } from '@/components/Description'; import { Layout } from '@/components/Layout'; import { useNotification } from '@/components/Notification/useNotification'; @@ -26,10 +27,39 @@ import type { PatchUserMissionRequest, } from '@/schema/schema'; -const Mission: NextPage = () => { +const updateMissionState = async ( + missionId: string, + userId: string, + clear: boolean, +) => { + const body: PatchUserMissionRequest = { + clear, + clearedAt: new Date().toISOString(), + }; + + await fetch(`${getApiBaseUrl()}/users/${userId}/missions/${missionId}`, { + method: 'PATCH', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(body), + }); + + const newMission: GetMissionResponse = await fetch( + `${getApiBaseUrl()}/missions/${missionId}`, + ).then((res) => res.json()); + + return newMission; +}; + +type MissionPageProps = { + missionName: string; +}; + +const Mission: NextPage = ({ missionName }) => { const theme = useMantineTheme(); const { missionId } = useRouter().query as { missionId: string }; - const { data: missions } = useSWR( + const { data: mission, mutate } = useSWR( `${getApiBaseUrl()}/missions/${missionId}`, fetcher, ); @@ -38,40 +68,30 @@ const Mission: NextPage = () => { const { animate, Canvas } = useClearAnimation(); const toggleClearHandler = async () => { - if (missions === undefined) return; + if (mission === undefined) return; if (user === undefined) return; if (userError !== undefined) return; - const clear = user.id ? !missions.achievers.includes(user.id) : false; - - const body: PatchUserMissionRequest = { - clear, - clearedAt: new Date().toISOString(), - }; + const clear = user.id ? !mission.achievers.includes(user.id) : false; try { - const res = await fetch( - `${getApiBaseUrl()}/users/${user.id}/missions/${missionId}`, - { - method: 'PATCH', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(body), + await mutate(updateMissionState(missionId, user.id, clear), { + populateCache: true, + revalidate: false, + rollbackOnError: true, + optimisticData: { + ...mission, + achievers: clear + ? mission.achievers.concat(user.id) + : mission.achievers.filter((achiever) => achiever !== user.id), }, - ); - - if (!res.ok) { - return notify({ - title: 'エラーが発生しました', - variant: 'error', - }); - } + }); if (clear) { animate(); } - } catch (error) { + } catch (err) { + console.error(err); return notify({ title: 'エラーが発生しました', variant: 'error', @@ -82,7 +102,7 @@ const Mission: NextPage = () => { return ( <> @@ -96,8 +116,8 @@ const Mission: NextPage = () => { line-height: 2rem; `} > - {missions ? ( - missions.name + {mission ? ( + mission.name ) : ( )} @@ -108,14 +128,14 @@ const Mission: NextPage = () => { padding: 1rem; `} > - {missions ? ( + {mission ? ( - {missions.description} + {mission.description} ) : ( @@ -126,10 +146,10 @@ const Mission: NextPage = () => {
- {missions ? ( + {mission ? ( user !== undefined && userError === undefined && - missions.achievers.includes(user.id) ? ( + mission.achievers.includes(user.id) ? (