diff --git a/app/musics/page.tsx b/app/musics/page.tsx new file mode 100644 index 0000000..a5a20b3 --- /dev/null +++ b/app/musics/page.tsx @@ -0,0 +1,14 @@ +import MusicList from "@/components/music-list"; +import { fetchMusics } from "@/lib/api"; + +type MusicPageProps = { + searchParams: Promise<{ cursor?: string; limit?: string }>; +}; + +export default async function MusicPage({ searchParams }: MusicPageProps) { + const params = await searchParams; + const limit = params.limit ? Number(params.limit) : undefined; + const data = await fetchMusics({ cursor: params.cursor, limit }); + + return ; +} diff --git a/components/dashboard-home.tsx b/components/dashboard-home.tsx index 3a07830..ed8d0cc 100644 --- a/components/dashboard-home.tsx +++ b/components/dashboard-home.tsx @@ -24,6 +24,7 @@ export default function DashboardHome({ userName }: { userName: string }) {
管理者用ダッシュボードへようこそ、{userName} さん。
+
diff --git a/components/music-list.tsx b/components/music-list.tsx new file mode 100644 index 0000000..b3f707c --- /dev/null +++ b/components/music-list.tsx @@ -0,0 +1,83 @@ +"use client"; + +import AppLayout from "@cloudscape-design/components/app-layout"; +import Button from "@cloudscape-design/components/button"; +import Container from "@cloudscape-design/components/container"; +import ContentLayout from "@cloudscape-design/components/content-layout"; +import Header from "@cloudscape-design/components/header"; +import SpaceBetween from "@cloudscape-design/components/space-between"; +import Table from "@cloudscape-design/components/table"; +import TopNavigation from "@cloudscape-design/components/top-navigation"; + +import type { MusicListResponse } from "@/lib/api"; + +export default function MusicList({ + data, + limit, +}: { + data: MusicListResponse; + limit?: number; +}) { + const nextPageHref = data.nextCursor + ? `/musics?${new URLSearchParams({ + cursor: data.nextCursor, + ...(limit ? { limit: String(limit) } : {}), + }).toString()}` + : undefined; + + return ( +
+ + ホーム} variant="h1"> + 楽曲管理 + + } + > + + + item.music.title }, + { + header: "アーティスト", + cell: (item) => item.music.artist, + }, + { header: "BPM", cell: (item) => item.music.bpm }, + { header: "譜面", cell: (item) => item.sheets.length }, + { + header: "登録日時", + cell: (item) => + new Date(item.music.registrationDate).toLocaleString( + "ja-JP", + ), + }, + ]} + items={data.items} + header={ +
楽曲一覧
+ } + empty={楽曲がありません。} + /> +
+ +
+ + + + } + /> + + ); +} diff --git a/lib/api.ts b/lib/api.ts new file mode 100644 index 0000000..71f4348 --- /dev/null +++ b/lib/api.ts @@ -0,0 +1,54 @@ +import { auth0 } from "@/lib/auth0"; + +export type Sheet = { + id: string; + musicId: string; + difficulty: "easy" | "normal" | "hard"; + level: number; + notesDesigner: string; +}; + +export type Music = { + id: string; + title: string; + artist: string; + bpm: number; + genre: string; + jacket: string; + registrationDate: string; + isTest: boolean; +}; + +export type MusicWithSheets = { + music: Music; + sheets: Sheet[]; +}; + +export type MusicListResponse = { + items: MusicWithSheets[]; + nextCursor: string | null; +}; + +export async function fetchMusics( + searchParams: { cursor?: string; limit?: number } = {}, +): Promise { + const audience = process.env.AUTH0_AUDIENCE ?? "https://api.xlair.dev"; + const accessToken = await auth0.getAccessToken({ audience }); + const params = new URLSearchParams(); + if (searchParams.cursor) params.set("cursor", searchParams.cursor); + if (searchParams.limit) params.set("limit", String(searchParams.limit)); + + const response = await fetch( + `${process.env.API_BASE_URL}/admin/musics?${params.toString()}`, + { + headers: { Authorization: `Bearer ${accessToken.token}` }, + cache: "no-store", + }, + ); + + if (!response.ok) { + throw new Error(`Failed to fetch musics: ${response.status}`); + } + + return response.json() as Promise; +}