diff --git a/functions/api/[[catchall]].ts b/functions/api/[[catchall]].ts index 727af66..dd1c032 100644 --- a/functions/api/[[catchall]].ts +++ b/functions/api/[[catchall]].ts @@ -10,8 +10,6 @@ export const onRequest: PagesFunction = async ({ request, env }) => { const url = new URL(req.url); const params = new URLSearchParams(url.search); - console.log(url); - const baseUrl = "https://api.esa.io/v1"; const newUrl = new URL( baseUrl + url.pathname.replace(/^\/api/, "") + "?" + params.toString(), diff --git a/src/assets/members.json b/src/assets/members.json index edfad88..7235d66 100644 --- a/src/assets/members.json +++ b/src/assets/members.json @@ -31,7 +31,7 @@ "posts_count": 0, "joined_at": "2022-09-17T01:35:57+09:00", "last_posted_at": "2022-09-17T01:35:57+09:00", - "email": "zzzzzz" + "email": "r916nis@gmail.com" }, { "myself": false, diff --git a/src/components/Redirects.tsx b/src/components/Redirects.tsx index 54bc384..7a37432 100644 --- a/src/components/Redirects.tsx +++ b/src/components/Redirects.tsx @@ -1,14 +1,16 @@ import { type ReactNode } from "react"; import { useLocation } from "react-router-dom"; import { $hasAuthenticated } from "@/lib/stores/auth"; -import { redirect } from "@/router"; +import { Navigate } from "@/router"; export function Redirects({ children }: { children: ReactNode }): ReactNode { const { pathname } = useLocation(); const isInPublicPaths = pathname === "/" || pathname.startsWith("/auth"); if (!$hasAuthenticated.get() && !isInPublicPaths) { - redirect("/"); + // eslint-disable-next-line no-console + console.warn("Member is not authenticated; Redirecting to `/`..."); + return ; } return children; diff --git a/src/components/member/Card.tsx b/src/components/member/Card.tsx index 67848dc..4776aa2 100644 --- a/src/components/member/Card.tsx +++ b/src/components/member/Card.tsx @@ -23,9 +23,12 @@ export function MemberCard({ return ( { + if (member.email == null) { + throw new Error("Member email is undefined"); + } navigate("/members/:id", { params: { - id: member.email.toString(), + id: member.email, }, }); }} diff --git a/src/components/member/RankingCard.tsx b/src/components/member/RankingCard.tsx index dbdfc3a..1ec510e 100644 --- a/src/components/member/RankingCard.tsx +++ b/src/components/member/RankingCard.tsx @@ -2,20 +2,21 @@ import { Avatar, Box, Flex, Text } from "@radix-ui/themes"; import { type ReactElement } from "react"; import styled from "styled-components"; import { Link } from "@/router"; +import { type Member } from "@/types/member"; export function RankingCard({ - memberEmail, - memberName, - memberIcon, - index, + member, point, + idx, }: { - memberEmail: string; - memberName: string; - memberIcon: string; - index: number; + member: Member; point: number; + idx: number; }): ReactElement { + const { name, email, icon } = member; + + if (email == null) throw new Error("email is null"); + const BoxStyle = styled(Box)` border-bottom: 1px solid; border-color: #cbd5e1; @@ -24,16 +25,16 @@ export function RankingCard({ `; return ( - + - {index + 1} + {idx + 1} - + - {memberName} + {name} {point}pt diff --git a/src/components/ranking/LogRecentUnlocked.tsx b/src/components/ranking/LogRecentUnlocked.tsx index bef80fc..27e71c1 100644 --- a/src/components/ranking/LogRecentUnlocked.tsx +++ b/src/components/ranking/LogRecentUnlocked.tsx @@ -6,6 +6,7 @@ import SampleMembers from "@/assets/members.json"; import { Link } from "@/router"; import { type Member } from "@/types/member"; import { type Achievement } from "@/types/post-data/achievements"; +import { type UnlockedAchievement } from "@/types/post-data/unlocked-achievements"; const BoxStyle = styled(Box)` border-bottom: 1px solid; @@ -15,14 +16,12 @@ const BoxStyle = styled(Box)` `; export function LogRecentUnlocked({ - achievementID, - unlockedDate, - memberEmail, + unlockedAchievement, }: { - achievementID: number; - unlockedDate: string; - memberEmail: string; + unlockedAchievement: UnlockedAchievement; }): ReactElement { + const { memberEmail, achievementID, createdAt } = unlockedAchievement; + const DateStyle = styled(Text)` padding-left: 10rem; `; @@ -48,7 +47,7 @@ export function LogRecentUnlocked({ {achievement.name} - {unlockedDate.slice(0, 10)} + {createdAt.toString().slice(0, 10)} diff --git a/src/hooks/db/_esaDB.ts b/src/hooks/db/_esaDB.ts index d898866..218e94b 100644 --- a/src/hooks/db/_esaDB.ts +++ b/src/hooks/db/_esaDB.ts @@ -45,7 +45,7 @@ export function useEsaDB( const fetch = async (): Promise => { const postsId = await searchPostId(); const { body_md } = await fetchPostByPostId(postsId); - return await config.schema.validate(JSON.parse(body_md)); + return await config.schema.validate(JSON.parse(body_md).data); }; const create = async (): Promise< diff --git a/src/pages/achievements/index.tsx b/src/pages/achievements/index.tsx index 80c3d7e..dc483b1 100644 --- a/src/pages/achievements/index.tsx +++ b/src/pages/achievements/index.tsx @@ -1,8 +1,12 @@ import { Box, Table } from "@radix-ui/themes"; -import { type ReactElement } from "react"; +import { useEffect, type ReactElement } from "react"; import styled from "styled-components"; -import Achievements from "@/assets/achievements.json"; +import useSWR from "swr"; +import { match } from "ts-pattern"; import { AchievementCard } from "@/components/achievements/Card"; +import { useAchievements } from "@/hooks/db/achievements"; +import { useTeam } from "@/hooks/teams"; +import { S } from "@/lib/consts"; import { type Achievement } from "@/types/post-data/achievements"; const BoxStyle = styled(Box)` @@ -16,9 +20,32 @@ const ScrollStyle = styled.div` `; export default function Page(): ReactElement { - return ( - - + const { init, fetch } = useAchievements(useTeam); + const swrAchievements = useSWR("achievements", fetchAchievements); + + async function fetchAchievements(): Promise<{ + achievements: Achievement[]; + }> { + const achievements = await fetch(); + + if (achievements == null) + throw new Error("No unlockedAchievements found."); + + return { + achievements, + }; + } + + + + useEffect(() => { + void init(); + }, []); + + return match(swrAchievements) + .with(S.Loading, () =>
Loading...
) + .with(S.Success, ({ data:{achievements} }) => ( + @@ -30,7 +57,7 @@ export default function Page(): ReactElement { - {Achievements.achievements.map((achievement) => { + {achievements.map((achievement) => { const typedAchievement = achievement as unknown as Achievement; return ( -
-
- ); +
+ )) + .otherwise(({ error }) => { + throw error; + }); } diff --git a/src/pages/auth/callback/index.tsx b/src/pages/auth/callback/index.tsx index e3ca81f..27b8ad6 100644 --- a/src/pages/auth/callback/index.tsx +++ b/src/pages/auth/callback/index.tsx @@ -1,5 +1,5 @@ import { useStore } from "@nanostores/react"; -import { Card, Flex } from "@radix-ui/themes"; +import { Button, Flex } from "@radix-ui/themes"; import { type ReactElement } from "react"; import styled from "styled-components"; import useSWR from "swr"; @@ -13,11 +13,19 @@ import { useNavigate } from "@/router"; import { type AccessTokenData } from "@/types/auth"; function TeamSelector(): ReactElement { + const navigate = useNavigate(); const { fetchJoinedTeams, markTeamNameAsSelected } = useMember(); const swrJoinedTeams = useSWR("joinedTeams", fetchJoinedTeams); const FlexStyled = styled(Flex)` - gap: 1rem; + gap: 15rem; + `; + + const ButtonStyle = styled(Button)` + transform: scale(2); + padding: 0; + height: 100px; + width: 100px; `; return match(swrJoinedTeams) @@ -25,14 +33,16 @@ function TeamSelector(): ReactElement { .with(S.Success, ({ data }) => ( {data.map((team) => ( - { markTeamNameAsSelected(team.name); + navigate("/ranking"); }} + size="4" > {team.name} - + ))} )) diff --git a/src/pages/members/index.tsx b/src/pages/members/index.tsx index cd8efa9..d2b3907 100644 --- a/src/pages/members/index.tsx +++ b/src/pages/members/index.tsx @@ -1,11 +1,22 @@ import { Box, Table } from "@radix-ui/themes"; -import { type ReactElement } from "react"; +import { useEffect, type ReactElement } from "react"; import styled from "styled-components"; -import SampleMember from "@/assets/members.json"; -import SampleUnlockedAchievements from "@/assets/unlockedAchievements.json"; +// import SampleMember from "@/assets/members.json"; +// import SampleUnlockedAchievements from "@/assets/unlockedAchievements.json"; +import useSWR from "swr"; +import { match } from "ts-pattern"; import { MemberCard } from "@/components/member/Card"; +import { useUnlockedAchievements } from "@/hooks/db/unlocked-achievements"; +import { useTeam } from "@/hooks/teams"; +import { S } from "@/lib/consts"; import { type Member } from "@/types/member"; +type MembersWithUnlockedCount = Array< + Member & { + unlockedCount: number; + } +>; + const BoxStyle = styled(Box)` margin: 0 auto; `; @@ -17,35 +28,60 @@ const ScrollStyle = styled.div` `; export default function Page(): ReactElement { - let point: number = 0; - return ( - - - - - - - 名前 - ポイント - - - - - {SampleMember.members.map((e: any) => { - const member: Member = e as Member; - point = 0; - SampleUnlockedAchievements.unlockedAchievements.forEach( - (unlockedAchievement) => { - if (unlockedAchievement.memberEmail === member.email) { - point += 1; - } - }, - ); - return ; - })} - - - - + const { fetchMembers } = useTeam(); + const { init, fetch } = useUnlockedAchievements(useTeam); + const swrMembersWithUnlockedCount = useSWR( + "membersWithUnlockedCount", + fetchMembersWithUnlockedCount, ); + + async function fetchMembersWithUnlockedCount(): Promise { + const members = await fetchMembers(); + const unlockedAchievements = await fetch(); + + if (unlockedAchievements == null) + throw new Error("No unlockedAchievements found."); + + return members + .map((m) => { + const unlockedCount = unlockedAchievements.filter( + (u) => u.memberEmail === m.email, + ).length; + return { + ...m, + unlockedCount, + }; + }) + .sort((a, b) => b.unlockedCount - a.unlockedCount); + } + + useEffect(() => { + void init(); + }, []); + + return match(swrMembersWithUnlockedCount) + .with(S.Loading, () =>
Loading...
) + .with(S.Success, ({ data }) => ( + + + + + + 名前 + ポイント + + + + + {data.map((m, idx) => ( + // eslint-disable-next-line react/no-array-index-key + + ))} + + + + )) + .otherwise(({ error }) => { + throw error; + }); } diff --git a/src/pages/ranking/index.tsx b/src/pages/ranking/index.tsx index 1f6dca9..8e0a20b 100644 --- a/src/pages/ranking/index.tsx +++ b/src/pages/ranking/index.tsx @@ -1,12 +1,15 @@ import { Text } from "@radix-ui/themes"; -import { type ReactElement } from "react"; +import { useEffect, type ReactElement } from "react"; import styled from "styled-components"; -import members from "@/assets/members.json"; -import UnlockedAchievements from "@/assets/unlockedAchievements.json"; +import useSWR from "swr"; +import { match } from "ts-pattern"; import { RankingCard } from "@/components/member/RankingCard"; import { LogRecentUnlocked } from "@/components/ranking/LogRecentUnlocked"; -import { esaClient } from "@/lib/services/esa"; +import { useUnlockedAchievements } from "@/hooks/db/unlocked-achievements"; +import { useTeam } from "@/hooks/teams"; +import { S } from "@/lib/consts"; import { type Member } from "@/types/member"; +import { type UnlockedAchievement } from "@/types/post-data/unlocked-achievements"; type MembersWithUnlockedCount = Array< Member & { @@ -15,6 +18,44 @@ type MembersWithUnlockedCount = Array< >; export default function Page(): ReactElement { + const { fetchMembers } = useTeam(); + const { init, fetch } = useUnlockedAchievements(useTeam); + const swrMembersWithUnlockedCount = useSWR( + "membersWithUnlockedCount", + fetchMembersWithUnlockedCount, + ); + + async function fetchMembersWithUnlockedCount(): Promise<{ + membersWithUnlockedCount: MembersWithUnlockedCount; + unlockedAchievements: UnlockedAchievement[]; + }> { + const members = await fetchMembers(); + const unlockedAchievements = await fetch(); + + if (unlockedAchievements == null) + throw new Error("No unlockedAchievements found."); + + const membersWithUnlockedCount = members + .map((m) => { + const unlockedCount = unlockedAchievements.filter( + (u) => u.memberEmail === m.email, + ).length; + return { + ...m, + unlockedCount, + }; + }) + .sort((a, b) => b.unlockedCount - a.unlockedCount); + + return { + membersWithUnlockedCount, + unlockedAchievements, + }; + } + + useEffect(() => { + void init(); + }, []); const RankingCardStyle = styled.div` position: relative; top: 4rem; @@ -60,60 +101,41 @@ export default function Page(): ReactElement { background-color: #f1f5f9; `; - void esaClient.GET("/teams/{team_name}", { - params: { - path: { - team_name: "sysken", - }, - }, - }); - - const memberList: MembersWithUnlockedCount = members.members.map((m) => { - const unlockedArchievements = - UnlockedAchievements.unlockedAchievements.filter( - (u) => u.memberEmail === m.email, - ); - return { - ...m, - unlockedCount: unlockedArchievements.length, - }; - }); + return match(swrMembersWithUnlockedCount) + .with(S.Loading, () =>

Loading...

) + .with( + S.Success, + ({ data: { membersWithUnlockedCount, unlockedAchievements } }) => ( +
+ + 実績解除ランキング + + + {membersWithUnlockedCount.map((m, idx) => ( + + ))} + - memberList.sort((a, b) => b.unlockedCount - a.unlockedCount); - - return ( -
- - 実績解除ランキング - - - {memberList.map((member, index) => ( - - ))} - - - - 最近の実績解除 - - - {UnlockedAchievements.unlockedAchievements.map( - (unlockedAchievements) => ( - - ), - )} - -
- ); + + 最近の実績解除 + + + {unlockedAchievements.map((u) => ( + + ))} + +
+ ), + ) + .otherwise(({ error }) => { + throw error; + }); } diff --git a/src/pages/test/index.tsx b/src/pages/test/index.tsx index 6e748c7..d5e9e07 100644 --- a/src/pages/test/index.tsx +++ b/src/pages/test/index.tsx @@ -63,7 +63,7 @@ export default function Page(): ReactElement { { achievementID: 1, createdAt: new Date(), - memberEmail: "email", + memberEmail: "info@expamle.com", }, ]); console.log("updated! => ", result); diff --git a/src/pages/unlocked/index.tsx b/src/pages/unlocked/index.tsx index 6659a09..8d37a21 100644 --- a/src/pages/unlocked/index.tsx +++ b/src/pages/unlocked/index.tsx @@ -1,12 +1,12 @@ -import { Icon } from "@iconify/react/dist/iconify.js"; -import { Box, IconButton, Table } from "@radix-ui/themes"; -import { type ReactElement } from "react"; +import { Box, Table } from "@radix-ui/themes"; +import { useEffect, type ReactElement } from "react"; import styled from "styled-components"; -import Achievements from "@/assets/achievements.json"; -import { AchievementCard } from "@/components/achievements/Card"; +import useSWR from "swr"; +import { match } from "ts-pattern"; import { UnlockableCard } from "@/components/achievements/UnlockableCard"; -import { Link } from "@/router.ts"; -import { type Achievement } from "@/types/post-data/achievements"; +import { useAchievements } from "@/hooks/db/achievements"; +import { useTeam } from "@/hooks/teams"; +import { S } from "@/lib/consts"; const BoxStyle = styled(Box)` margin: 0 auto; @@ -19,46 +19,37 @@ const PlusButton = styled(IconButton)` `; export default function Page(): ReactElement { - return ( - - - - - - - 名前 - 説明 - タグ - - + const { init, fetch } = useAchievements(useTeam); + const swrFetchAchievements = useSWR("fetchAchievements", fetch); + + useEffect(() => { + void init(); + }, []); - - {Achievements.achievements.map((achievement) => ( - - ))} - - - - - - - - - ); -} + return match(swrFetchAchievements) + .with(S.Loading, () =>

Loading...

) + .with(S.Success, ({ data }) => ( + + + + + + + 名前 + 説明 + タグ + + - - - - {Achievements.achievements.map((achievement) => ( - - ))} - - -; + + {data.map((achievement) => ( + + ))} + + + + )) + .otherwise(({ error }) => { + throw error; + }); +} \ No newline at end of file diff --git a/src/types/member.ts b/src/types/member.ts index 18db4d0..c77a548 100644 --- a/src/types/member.ts +++ b/src/types/member.ts @@ -1,21 +1,6 @@ -export type Member = { - myself: boolean; - name: string; // 漢字 名義 - screen_name: string; // k2xxxx_name - icon: string; // URL - role: string; // "owner" | "member" - posts_count: number; - joined_at: string; // JST - last_posted_at: string; // JST - email: string; -}; +import { type ArrayElem } from "./utils"; +import { type useTeam } from "@/hooks/teams"; -export type Members = { - members: Members[]; - prev_page: number | null; - next_page: number; - total_count: number; - page: number; - per_page: number; - max_per_page: number; -}; +export type Member = ArrayElem< + Awaited["fetchMembers"]>> +>;