Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions app/musics/page.tsx
Original file line number Diff line number Diff line change
@@ -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 <MusicList data={data} limit={limit} />;
}
1 change: 1 addition & 0 deletions components/dashboard-home.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export default function DashboardHome({ userName }: { userName: string }) {
<Container>
<SpaceBetween size="m">
<div>管理者用ダッシュボードへようこそ、{userName} さん。</div>
<Button href="/musics">楽曲管理</Button>
<Button href="/auth/logout">ログアウト</Button>
</SpaceBetween>
</Container>
Expand Down
83 changes: 83 additions & 0 deletions components/music-list.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="min-h-screen min-w-80">
<TopNavigation
id="dashboard-header"
identity={{ href: "/", title: "XLAIR Dashboard" }}
/>
<AppLayout
headerSelector="#dashboard-header"
// Remove toolsHide when an AppLayout tools panel is introduced.
toolsHide
content={
<ContentLayout
header={
<Header actions={<Button href="/">ホーム</Button>} variant="h1">
楽曲管理
</Header>
}
>
<Container>
<SpaceBetween size="m">
<Table
columnDefinitions={[
{ header: "タイトル", cell: (item) => 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={
<Header counter={`(${data.items.length})`}>楽曲一覧</Header>
}
empty={<span>楽曲がありません。</span>}
/>
<div className="flex justify-end">
<Button disabled={!nextPageHref} href={nextPageHref}>
次へ
</Button>
</div>
</SpaceBetween>
</Container>
</ContentLayout>
}
/>
</div>
);
}
54 changes: 54 additions & 0 deletions lib/api.ts
Original file line number Diff line number Diff line change
@@ -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<MusicListResponse> {
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<MusicListResponse>;
}