From 429139577122da81195b87c4ea969049afae711c Mon Sep 17 00:00:00 2001 From: n4mlz Date: Tue, 1 Sep 2026 12:58:59 +0900 Subject: [PATCH 01/15] =?UTF-8?q?:sparkles:=20=E6=A5=BD=E6=9B=B2=E3=81=AE?= =?UTF-8?q?=E4=BD=9C=E6=88=90=E3=83=BB=E7=B7=A8=E9=9B=86=E7=94=BB=E9=9D=A2?= =?UTF-8?q?=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/musics/[musicId]/edit/page.tsx | 13 ++ app/musics/actions.ts | 22 +++ app/musics/new/page.tsx | 5 + components/music-detail.tsx | 7 +- components/music-form.tsx | 302 +++++++++++++++++++++++++++++ components/music-list.tsx | 15 +- lib/api.ts | 65 +++++++ 7 files changed, 427 insertions(+), 2 deletions(-) create mode 100644 app/musics/[musicId]/edit/page.tsx create mode 100644 app/musics/actions.ts create mode 100644 app/musics/new/page.tsx create mode 100644 components/music-form.tsx diff --git a/app/musics/[musicId]/edit/page.tsx b/app/musics/[musicId]/edit/page.tsx new file mode 100644 index 0000000..22422dc --- /dev/null +++ b/app/musics/[musicId]/edit/page.tsx @@ -0,0 +1,13 @@ +import MusicForm from "@/components/music-form"; +import { fetchMusic } from "@/lib/api"; + +export default async function EditMusicPage({ + params, +}: { + params: Promise<{ musicId: string }>; +}) { + const { musicId } = await params; + const data = await fetchMusic(musicId); + + return ; +} diff --git a/app/musics/actions.ts b/app/musics/actions.ts new file mode 100644 index 0000000..979ae9c --- /dev/null +++ b/app/musics/actions.ts @@ -0,0 +1,22 @@ +"use server"; + +import { + type CreateMusicInput, + createMusic, + type MusicWithSheets, + type UpdateMusicInput, + updateMusic, +} from "@/lib/api"; + +export async function createMusicAction( + input: CreateMusicInput, +): Promise { + return createMusic(input); +} + +export async function updateMusicAction( + musicId: string, + input: UpdateMusicInput, +): Promise { + return updateMusic(musicId, input); +} diff --git a/app/musics/new/page.tsx b/app/musics/new/page.tsx new file mode 100644 index 0000000..ba15e96 --- /dev/null +++ b/app/musics/new/page.tsx @@ -0,0 +1,5 @@ +import MusicForm from "@/components/music-form"; + +export default function NewMusicPage() { + return ; +} diff --git a/components/music-detail.tsx b/components/music-detail.tsx index b030d49..67b8a99 100644 --- a/components/music-detail.tsx +++ b/components/music-detail.tsx @@ -32,7 +32,12 @@ export default function MusicDetail({ data }: { data: MusicWithSheets }) { header={
楽曲一覧に戻る} + actions={ + + + + + } > {music.title}
diff --git a/components/music-form.tsx b/components/music-form.tsx new file mode 100644 index 0000000..b219eb5 --- /dev/null +++ b/components/music-form.tsx @@ -0,0 +1,302 @@ +"use client"; + +import Alert from "@cloudscape-design/components/alert"; +import Button from "@cloudscape-design/components/button"; +import Checkbox from "@cloudscape-design/components/checkbox"; +import Container from "@cloudscape-design/components/container"; +import ContentLayout from "@cloudscape-design/components/content-layout"; +import Form from "@cloudscape-design/components/form"; +import FormField from "@cloudscape-design/components/form-field"; +import Header from "@cloudscape-design/components/header"; +import Input from "@cloudscape-design/components/input"; +import SpaceBetween from "@cloudscape-design/components/space-between"; +import { useState } from "react"; +import { createMusicAction, updateMusicAction } from "@/app/musics/actions"; +import DashboardLayout from "@/components/dashboard-layout"; +import type { + CreateMusicInput, + MusicWithSheets, + UpdateMusicInput, +} from "@/lib/api"; + +type Difficulty = "easy" | "normal" | "hard"; +type SheetDraft = { id?: string; level: string; notesDesigner: string }; +type FormValues = { + title: string; + artist: string; + bpm: string; + jacket: string; + isTest: boolean; + sheets: Record; +}; + +const difficulties: Array<{ key: Difficulty; label: string }> = [ + { key: "easy", label: "Easy" }, + { key: "normal", label: "Normal" }, + { key: "hard", label: "Hard" }, +]; + +function isPositiveSingleDecimal(value: string) { + return /^\d+(\.\d)?$/.test(value) && Number(value) > 0; +} + +function initialValues(data?: MusicWithSheets): FormValues { + const sheets = Object.fromEntries( + difficulties.map(({ key }) => { + const sheet = data?.sheets.find((item) => item.difficulty === key); + return [ + key, + { + id: sheet?.id, + level: sheet ? String(sheet.level) : "", + notesDesigner: sheet?.notesDesigner ?? "", + }, + ]; + }), + ) as Record; + return { + title: data?.music.title ?? "", + artist: data?.music.artist ?? "", + bpm: data ? String(data.music.bpm) : "", + jacket: data?.music.jacket ?? "", + isTest: data?.music.isTest ?? false, + sheets, + }; +} + +export default function MusicForm({ + data, + title, +}: { + data?: MusicWithSheets; + title: string; +}) { + const [values, setValues] = useState(() => initialValues(data)); + const [errors, setErrors] = useState>({}); + const [submitError, setSubmitError] = useState(); + const [isSubmitting, setIsSubmitting] = useState(false); + const [isSaved, setIsSaved] = useState(false); + const [savedMusicId, setSavedMusicId] = useState(); + + const isEdit = Boolean(data); + const updateValue = ( + key: keyof Omit, + value: string | boolean, + ) => setValues((current) => ({ ...current, [key]: value })); + + function validate() { + const nextErrors: Record = {}; + if (!values.title.trim()) nextErrors.title = "タイトルを入力してください。"; + if (!values.artist.trim()) + nextErrors.artist = "アーティストを入力してください。"; + if (!values.jacket.trim()) + nextErrors.jacket = "ジャケットを入力してください。"; + if (!isPositiveSingleDecimal(values.bpm)) + nextErrors.bpm = "BPM は正の数値(小数第1位まで)で入力してください。"; + for (const { key, label } of difficulties) { + const sheet = values.sheets[key]; + if (!isPositiveSingleDecimal(sheet.level)) + nextErrors[`${key}.level`] = + `${label} のレベルは正の数値(小数第1位まで)で入力してください。`; + if (!sheet.notesDesigner.trim()) + nextErrors[`${key}.notesDesigner`] = + `${label} の譜面制作者を入力してください。`; + if (isEdit && !sheet.id) + nextErrors[`${key}.id`] = `${label} の譜面 ID がありません。`; + } + setErrors(nextErrors); + return nextErrors; + } + + async function handleSubmit(event: React.FormEvent) { + event.preventDefault(); + setSubmitError(undefined); + setIsSaved(false); + if (Object.keys(validate()).length > 0) return; + setIsSubmitting(true); + try { + const fields = { + title: values.title.trim(), + artist: values.artist.trim(), + bpm: Number(values.bpm), + genre: "ORIGINAL" as const, + jacket: values.jacket.trim(), + isTest: values.isTest, + }; + const result = data + ? await updateMusicAction(data.music.id, { + ...fields, + sheets: difficulties.map(({ key }) => ({ + id: values.sheets[key].id as string, + difficulty: key, + level: Number(values.sheets[key].level), + notesDesigner: values.sheets[key].notesDesigner.trim(), + })), + } satisfies UpdateMusicInput) + : await createMusicAction({ + ...fields, + sheets: difficulties.map(({ key }) => ({ + difficulty: key, + level: Number(values.sheets[key].level), + notesDesigner: values.sheets[key].notesDesigner.trim(), + })), + } satisfies CreateMusicInput); + setSavedMusicId(result.music.id); + setIsSaved(true); + } catch (error) { + setSubmitError( + error instanceof Error ? error.message : "保存に失敗しました。", + ); + } finally { + setIsSubmitting(false); + } + } + + return ( + + {title}}> +
+ + + + + } + > + + {submitError ? ( + + {submitError} + + ) : null} + {isSaved && savedMusicId ? ( + 詳細を表示 + } + > + 保存しました。 + + ) : null} + 楽曲情報}> + + + + updateValue("title", detail.value) + } + /> + + + + updateValue("artist", detail.value) + } + /> + + + + updateValue("bpm", detail.value) + } + /> + + + + + + + updateValue("jacket", detail.value) + } + /> + + + updateValue("isTest", detail.checked) + } + > + テスト楽曲 + + + + 譜面}> + + {difficulties.map(({ key, label }) => { + const sheet = values.sheets[key]; + return ( + {label}} + > + + + + setValues((current) => ({ + ...current, + sheets: { + ...current.sheets, + [key]: { ...sheet, level: detail.value }, + }, + })) + } + /> + + + + setValues((current) => ({ + ...current, + sheets: { + ...current.sheets, + [key]: { + ...sheet, + notesDesigner: detail.value, + }, + }, + })) + } + /> + + + + ); + })} + + + +
+ +
+
+ ); +} diff --git a/components/music-list.tsx b/components/music-list.tsx index 5097f11..ca2395d 100644 --- a/components/music-list.tsx +++ b/components/music-list.tsx @@ -30,7 +30,20 @@ export default function MusicList({ return ( - 楽曲管理}> + + 楽曲を追加 + + } + > + 楽曲管理 + + } + > diff --git a/lib/api.ts b/lib/api.ts index ef43a46..817963a 100644 --- a/lib/api.ts +++ b/lib/api.ts @@ -35,6 +35,32 @@ export type MusicListResponse = { nextCursor: string | null; }; +export type MusicFields = { + title: string; + artist: string; + bpm: number; + genre: "ORIGINAL"; + jacket: string; + isTest: boolean; +}; + +export type CreateMusicInput = MusicFields & { + sheets: Array<{ + difficulty: Sheet["difficulty"]; + level: number; + notesDesigner: string; + }>; +}; + +export type UpdateMusicInput = MusicFields & { + sheets: Array<{ + id: string; + difficulty: Sheet["difficulty"]; + level: number; + notesDesigner: string; + }>; +}; + async function getAccessToken(returnTo: string) { const audience = process.env.AUTH0_AUDIENCE ?? "https://api.xlair.dev"; try { @@ -98,3 +124,42 @@ export async function fetchMusic(musicId: string): Promise { return response.json() as Promise; } + +async function writeMusic( + path: string, + method: "POST" | "PUT", + body: CreateMusicInput | UpdateMusicInput, + returnTo: string, +): Promise { + const accessToken = await getAccessToken(returnTo); + const response = await fetch(`${process.env.API_BASE_URL}${path}`, { + method, + headers: { + Authorization: `Bearer ${accessToken.token}`, + "Content-Type": "application/json", + }, + body: JSON.stringify(body), + cache: "no-store", + }); + + if (!response.ok) { + throw new Error( + `Failed to ${method === "POST" ? "create" : "update"} music: ${response.status}`, + ); + } + + return response.json() as Promise; +} + +export function createMusic(input: CreateMusicInput) { + return writeMusic("/admin/musics", "POST", input, "/musics/new"); +} + +export function updateMusic(musicId: string, input: UpdateMusicInput) { + return writeMusic( + `/admin/musics/${encodeURIComponent(musicId)}`, + "PUT", + input, + `/musics/${musicId}/edit`, + ); +} From 5b9a070f67854f8cd6696fa8d329ab057a860e39 Mon Sep 17 00:00:00 2001 From: n4mlz Date: Tue, 1 Sep 2026 17:40:04 +0900 Subject: [PATCH 02/15] =?UTF-8?q?:sparkles:=20=E6=A5=BD=E6=9B=B2=E7=99=BB?= =?UTF-8?q?=E9=8C=B2=E6=97=A5=E3=81=AE=E5=85=A5=E5=8A=9B=E6=AC=84=E3=82=92?= =?UTF-8?q?=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/music-form.tsx | 21 +++++++++++++++++++++ lib/api.ts | 1 + 2 files changed, 22 insertions(+) diff --git a/components/music-form.tsx b/components/music-form.tsx index b219eb5..8a32056 100644 --- a/components/music-form.tsx +++ b/components/music-form.tsx @@ -26,6 +26,7 @@ type FormValues = { artist: string; bpm: string; jacket: string; + registrationDate: string; isTest: boolean; sheets: Record; }; @@ -59,6 +60,7 @@ function initialValues(data?: MusicWithSheets): FormValues { artist: data?.music.artist ?? "", bpm: data ? String(data.music.bpm) : "", jacket: data?.music.jacket ?? "", + registrationDate: data?.music.registrationDate.slice(0, 10) ?? "", isTest: data?.music.isTest ?? false, sheets, }; @@ -91,6 +93,11 @@ export default function MusicForm({ nextErrors.artist = "アーティストを入力してください。"; if (!values.jacket.trim()) nextErrors.jacket = "ジャケットを入力してください。"; + if (!values.registrationDate) + nextErrors.registrationDate = "登録日を入力してください。"; + else if (!/^\d{4}-\d{2}-\d{2}$/.test(values.registrationDate)) + nextErrors.registrationDate = + "登録日は YYYY-MM-DD 形式で入力してください。"; if (!isPositiveSingleDecimal(values.bpm)) nextErrors.bpm = "BPM は正の数値(小数第1位まで)で入力してください。"; for (const { key, label } of difficulties) { @@ -121,6 +128,7 @@ export default function MusicForm({ bpm: Number(values.bpm), genre: "ORIGINAL" as const, jacket: values.jacket.trim(), + registrationDate: `${values.registrationDate}T00:00:00.000Z`, isTest: values.isTest, }; const result = data @@ -229,6 +237,19 @@ export default function MusicForm({ } /> + + + updateValue("registrationDate", detail.value) + } + /> + diff --git a/lib/api.ts b/lib/api.ts index 817963a..9da83d0 100644 --- a/lib/api.ts +++ b/lib/api.ts @@ -41,6 +41,7 @@ export type MusicFields = { bpm: number; genre: "ORIGINAL"; jacket: string; + registrationDate: string; isTest: boolean; }; From 89530cc6b78b5a80cde700691af17d6ba1303fd0 Mon Sep 17 00:00:00 2001 From: n4mlz Date: Tue, 1 Sep 2026 20:18:14 +0900 Subject: [PATCH 03/15] =?UTF-8?q?:recycle:=20=E6=A5=BD=E6=9B=B2=E6=9B=B4?= =?UTF-8?q?=E6=96=B0=20API=20=E3=81=AE=20HTTP=20method=20=E3=82=92?= =?UTF-8?q?=E7=B5=B1=E4=B8=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/api.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/api.ts b/lib/api.ts index 9da83d0..019d1ec 100644 --- a/lib/api.ts +++ b/lib/api.ts @@ -128,7 +128,7 @@ export async function fetchMusic(musicId: string): Promise { async function writeMusic( path: string, - method: "POST" | "PUT", + method: "POST", body: CreateMusicInput | UpdateMusicInput, returnTo: string, ): Promise { @@ -159,7 +159,7 @@ export function createMusic(input: CreateMusicInput) { export function updateMusic(musicId: string, input: UpdateMusicInput) { return writeMusic( `/admin/musics/${encodeURIComponent(musicId)}`, - "PUT", + "POST", input, `/musics/${musicId}/edit`, ); From 441e064b6835b18cedfa65db5e54eaf36124b4e3 Mon Sep 17 00:00:00 2001 From: n4mlz Date: Sun, 13 Sep 2026 16:10:03 +0900 Subject: [PATCH 04/15] =?UTF-8?q?:sparkles:=20=E6=A5=BD=E6=9B=B2=E7=AE=A1?= =?UTF-8?q?=E7=90=86=E7=94=BB=E9=9D=A2=E3=81=AE=E5=B0=8E=E7=B7=9A=E3=82=92?= =?UTF-8?q?=E6=94=B9=E5=96=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/musics/[musicId]/error.tsx | 9 +++- components/music-breadcrumbs.tsx | 22 ++++++++ components/music-detail-loading.tsx | 8 ++- components/music-detail.tsx | 14 ++--- components/music-form.tsx | 80 ++++++++++++++--------------- components/music-list-loading.tsx | 8 ++- components/music-list.tsx | 4 ++ components/music-table.tsx | 1 - 8 files changed, 94 insertions(+), 52 deletions(-) create mode 100644 components/music-breadcrumbs.tsx diff --git a/app/musics/[musicId]/error.tsx b/app/musics/[musicId]/error.tsx index fb4bf90..d516180 100644 --- a/app/musics/[musicId]/error.tsx +++ b/app/musics/[musicId]/error.tsx @@ -8,18 +8,23 @@ import Header from "@cloudscape-design/components/header"; import SpaceBetween from "@cloudscape-design/components/space-between"; import DashboardLayout from "@/components/dashboard-layout"; +import MusicBreadcrumbs from "@/components/music-breadcrumbs"; export default function MusicDetailError({ reset }: { reset: () => void }) { return ( - 楽曲管理}> + + } + header={
楽曲管理
} + > 楽曲が存在しないか、一時的に取得できません。時間をおいて再試行してください。
-
diff --git a/components/music-breadcrumbs.tsx b/components/music-breadcrumbs.tsx new file mode 100644 index 0000000..3243264 --- /dev/null +++ b/components/music-breadcrumbs.tsx @@ -0,0 +1,22 @@ +"use client"; + +import BreadcrumbGroup from "@cloudscape-design/components/breadcrumb-group"; + +export default function MusicBreadcrumbs({ + current, + currentHref, +}: { + current: string; + currentHref: string; +}) { + return ( + + ); +} diff --git a/components/music-detail-loading.tsx b/components/music-detail-loading.tsx index 0b673aa..aa4a521 100644 --- a/components/music-detail-loading.tsx +++ b/components/music-detail-loading.tsx @@ -4,11 +4,17 @@ import Header from "@cloudscape-design/components/header"; import Spinner from "@cloudscape-design/components/spinner"; import DashboardLayout from "@/components/dashboard-layout"; +import MusicBreadcrumbs from "@/components/music-breadcrumbs"; export default function MusicDetailLoading() { return ( - 楽曲管理}> + + } + header={
楽曲管理
} + >
- - - - } + actions={} > {music.title} } + breadcrumbs={ + + } > 楽曲情報}> diff --git a/components/music-form.tsx b/components/music-form.tsx index 8a32056..972819a 100644 --- a/components/music-form.tsx +++ b/components/music-form.tsx @@ -5,14 +5,18 @@ import Button from "@cloudscape-design/components/button"; import Checkbox from "@cloudscape-design/components/checkbox"; import Container from "@cloudscape-design/components/container"; import ContentLayout from "@cloudscape-design/components/content-layout"; +import DatePicker from "@cloudscape-design/components/date-picker"; import Form from "@cloudscape-design/components/form"; import FormField from "@cloudscape-design/components/form-field"; import Header from "@cloudscape-design/components/header"; import Input from "@cloudscape-design/components/input"; import SpaceBetween from "@cloudscape-design/components/space-between"; +import { useRouter } from "next/navigation"; import { useState } from "react"; + import { createMusicAction, updateMusicAction } from "@/app/musics/actions"; import DashboardLayout from "@/components/dashboard-layout"; +import MusicBreadcrumbs from "@/components/music-breadcrumbs"; import type { CreateMusicInput, MusicWithSheets, @@ -77,8 +81,7 @@ export default function MusicForm({ const [errors, setErrors] = useState>({}); const [submitError, setSubmitError] = useState(); const [isSubmitting, setIsSubmitting] = useState(false); - const [isSaved, setIsSaved] = useState(false); - const [savedMusicId, setSavedMusicId] = useState(); + const router = useRouter(); const isEdit = Boolean(data); const updateValue = ( @@ -118,7 +121,6 @@ export default function MusicForm({ async function handleSubmit(event: React.FormEvent) { event.preventDefault(); setSubmitError(undefined); - setIsSaved(false); if (Object.keys(validate()).length > 0) return; setIsSubmitting(true); try { @@ -131,26 +133,27 @@ export default function MusicForm({ registrationDate: `${values.registrationDate}T00:00:00.000Z`, isTest: values.isTest, }; - const result = data - ? await updateMusicAction(data.music.id, { - ...fields, - sheets: difficulties.map(({ key }) => ({ - id: values.sheets[key].id as string, - difficulty: key, - level: Number(values.sheets[key].level), - notesDesigner: values.sheets[key].notesDesigner.trim(), - })), - } satisfies UpdateMusicInput) - : await createMusicAction({ - ...fields, - sheets: difficulties.map(({ key }) => ({ - difficulty: key, - level: Number(values.sheets[key].level), - notesDesigner: values.sheets[key].notesDesigner.trim(), - })), - } satisfies CreateMusicInput); - setSavedMusicId(result.music.id); - setIsSaved(true); + if (data) { + await updateMusicAction(data.music.id, { + ...fields, + sheets: difficulties.map(({ key }) => ({ + id: values.sheets[key].id as string, + difficulty: key, + level: Number(values.sheets[key].level), + notesDesigner: values.sheets[key].notesDesigner.trim(), + })), + } satisfies UpdateMusicInput); + } else { + await createMusicAction({ + ...fields, + sheets: difficulties.map(({ key }) => ({ + difficulty: key, + level: Number(values.sheets[key].level), + notesDesigner: values.sheets[key].notesDesigner.trim(), + })), + } satisfies CreateMusicInput); + } + router.push("/musics"); } catch (error) { setSubmitError( error instanceof Error ? error.message : "保存に失敗しました。", @@ -162,7 +165,15 @@ export default function MusicForm({ return ( - {title}}> + + } + header={
{title}
} + >
) : null} - {isSaved && savedMusicId ? ( - 詳細を表示 - } - > - 保存しました。 - - ) : null} 楽曲情報}> @@ -237,14 +238,11 @@ export default function MusicForm({ } /> - - + updateValue("registrationDate", detail.value) } diff --git a/components/music-list-loading.tsx b/components/music-list-loading.tsx index fb30409..3df5c47 100644 --- a/components/music-list-loading.tsx +++ b/components/music-list-loading.tsx @@ -4,11 +4,17 @@ import Header from "@cloudscape-design/components/header"; import Spinner from "@cloudscape-design/components/spinner"; import DashboardLayout from "@/components/dashboard-layout"; +import MusicBreadcrumbs from "@/components/music-breadcrumbs"; export default function MusicListLoading() { return ( - 楽曲管理}> + + } + header={
楽曲管理
} + >
import("@/components/music-table"), { @@ -31,6 +32,9 @@ export default function MusicList({ return ( + } header={
item.music.artist, }, { header: "BPM", cell: (item) => item.music.bpm }, - { header: "譜面", cell: (item) => item.sheets.length }, { header: "登録日時", cell: (item) => From f93b1a563a4ff4d264272d490540f1d3e60af187 Mon Sep 17 00:00:00 2001 From: n4mlz Date: Sun, 13 Sep 2026 16:11:40 +0900 Subject: [PATCH 05/15] =?UTF-8?q?:fix:=20=E6=A5=BD=E6=9B=B2=E7=B7=A8?= =?UTF-8?q?=E9=9B=86=E5=BE=8C=E3=81=AE=E9=81=B7=E7=A7=BB=E5=85=88=E3=82=92?= =?UTF-8?q?=E8=A9=B3=E7=B4=B0=E3=83=9A=E3=83=BC=E3=82=B8=E3=81=AB=E5=A4=89?= =?UTF-8?q?=E6=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/music-form.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/music-form.tsx b/components/music-form.tsx index 972819a..ad5862f 100644 --- a/components/music-form.tsx +++ b/components/music-form.tsx @@ -153,7 +153,7 @@ export default function MusicForm({ })), } satisfies CreateMusicInput); } - router.push("/musics"); + router.push(data ? `/musics/${data.music.id}` : "/musics"); } catch (error) { setSubmitError( error instanceof Error ? error.message : "保存に失敗しました。", From 5f1aa49321fd24023f76dc4073aea8dbb13bf6fc Mon Sep 17 00:00:00 2001 From: n4mlz Date: Sun, 13 Sep 2026 16:12:33 +0900 Subject: [PATCH 06/15] =?UTF-8?q?:fix:=20=E6=A5=BD=E6=9B=B2=E4=B8=80?= =?UTF-8?q?=E8=A6=A7=E3=81=AE=E3=83=91=E3=83=B3=E3=81=8F=E3=81=9A=E3=82=92?= =?UTF-8?q?=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/music-breadcrumbs.tsx | 21 +++++++++------------ components/music-list-loading.tsx | 4 +--- components/music-list.tsx | 4 +--- 3 files changed, 11 insertions(+), 18 deletions(-) diff --git a/components/music-breadcrumbs.tsx b/components/music-breadcrumbs.tsx index 3243264..f8ccbb0 100644 --- a/components/music-breadcrumbs.tsx +++ b/components/music-breadcrumbs.tsx @@ -6,17 +6,14 @@ export default function MusicBreadcrumbs({ current, currentHref, }: { - current: string; - currentHref: string; + current?: string; + currentHref?: string; }) { - return ( - - ); + const items = [ + { text: "ホーム", href: "/" }, + { text: "楽曲管理", href: "/musics" }, + ...(current ? [{ text: current, href: currentHref ?? "/musics" }] : []), + ]; + + return ; } diff --git a/components/music-list-loading.tsx b/components/music-list-loading.tsx index 3df5c47..c30ed12 100644 --- a/components/music-list-loading.tsx +++ b/components/music-list-loading.tsx @@ -10,9 +10,7 @@ export default function MusicListLoading() { return ( - } + breadcrumbs={} header={
楽曲管理
} > diff --git a/components/music-list.tsx b/components/music-list.tsx index f46ff54..2700fde 100644 --- a/components/music-list.tsx +++ b/components/music-list.tsx @@ -32,9 +32,7 @@ export default function MusicList({ return ( - } + breadcrumbs={} header={
Date: Sun, 13 Sep 2026 16:23:14 +0900 Subject: [PATCH 07/15] =?UTF-8?q?:fix:=20=E6=A5=BD=E6=9B=B2=E3=83=86?= =?UTF-8?q?=E3=83=BC=E3=83=96=E3=83=AB=E3=81=AE=E4=BD=99=E7=99=BD=E3=82=92?= =?UTF-8?q?=E8=AA=BF=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/music-sheets-table.tsx | 1 + components/music-table.tsx | 1 + 2 files changed, 2 insertions(+) diff --git a/components/music-sheets-table.tsx b/components/music-sheets-table.tsx index 0cc55b6..e0da84b 100644 --- a/components/music-sheets-table.tsx +++ b/components/music-sheets-table.tsx @@ -14,6 +14,7 @@ export default function MusicSheetsTable({ }) { return ( item.difficultyLabel }, { header: "レベル", cell: (item) => item.level }, diff --git a/components/music-table.tsx b/components/music-table.tsx index b09e019..2486946 100644 --- a/components/music-table.tsx +++ b/components/music-table.tsx @@ -9,6 +9,7 @@ import type { MusicListResponse } from "@/lib/api"; export default function MusicTable({ data }: { data: MusicListResponse }) { return (
Date: Sun, 13 Sep 2026 16:25:06 +0900 Subject: [PATCH 08/15] =?UTF-8?q?:fix:=20=E6=A5=BD=E6=9B=B2=E4=B8=80?= =?UTF-8?q?=E8=A6=A7=E3=81=AE=E6=97=A5=E4=BB=98=E8=A1=A8=E7=A4=BA=E3=81=A8?= =?UTF-8?q?=E4=BD=99=E7=99=BD=E3=82=92=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/music-detail.tsx | 4 +++- components/music-sheets-table.tsx | 1 - components/music-table.tsx | 3 +-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/components/music-detail.tsx b/components/music-detail.tsx index 540ea2a..1e36203 100644 --- a/components/music-detail.tsx +++ b/components/music-detail.tsx @@ -55,7 +55,9 @@ export default function MusicDetail({ data }: { data: MusicWithSheets }) { item.difficultyLabel }, { header: "レベル", cell: (item) => item.level }, diff --git a/components/music-table.tsx b/components/music-table.tsx index 2486946..6f2c0cf 100644 --- a/components/music-table.tsx +++ b/components/music-table.tsx @@ -9,7 +9,6 @@ import type { MusicListResponse } from "@/lib/api"; export default function MusicTable({ data }: { data: MusicListResponse }) { return (
- new Date(item.music.registrationDate).toLocaleString("ja-JP"), + new Date(item.music.registrationDate).toLocaleDateString("ja-JP"), }, ]} items={data.items} From 1bcbd17c45a6a27b857ace2fb544da5380707b2a Mon Sep 17 00:00:00 2001 From: n4mlz Date: Sun, 13 Sep 2026 16:27:37 +0900 Subject: [PATCH 09/15] =?UTF-8?q?:fix:=20=E6=A5=BD=E6=9B=B2=E3=83=86?= =?UTF-8?q?=E3=83=BC=E3=83=96=E3=83=AB=E3=81=AE=E5=88=97=E8=A6=8B=E5=87=BA?= =?UTF-8?q?=E3=81=97=E3=82=92=E6=95=B4=E5=88=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/music-sheets-table.tsx | 17 ++++++++++++---- components/music-table.tsx | 32 ++++++++++++++++++++----------- 2 files changed, 34 insertions(+), 15 deletions(-) diff --git a/components/music-sheets-table.tsx b/components/music-sheets-table.tsx index 0cc55b6..c9bce3d 100644 --- a/components/music-sheets-table.tsx +++ b/components/music-sheets-table.tsx @@ -2,7 +2,7 @@ import Header from "@cloudscape-design/components/header"; import Table from "@cloudscape-design/components/table"; - +import TableCell from "@/components/table-cell"; import type { Sheet } from "@/lib/api"; type SheetWithLabel = Sheet & { difficultyLabel: string }; @@ -15,9 +15,18 @@ export default function MusicSheetsTable({ return (
item.difficultyLabel }, - { header: "レベル", cell: (item) => item.level }, - { header: "譜面制作者", cell: (item) => item.notesDesigner }, + { + header: 難易度, + cell: (item) => {item.difficultyLabel}, + }, + { + header: レベル, + cell: (item) => {item.level}, + }, + { + header: 譜面制作者, + cell: (item) => {item.notesDesigner}, + }, ]} items={sheets} header={
譜面一覧
} diff --git a/components/music-table.tsx b/components/music-table.tsx index 6f2c0cf..7cc81b7 100644 --- a/components/music-table.tsx +++ b/components/music-table.tsx @@ -3,7 +3,7 @@ import Header from "@cloudscape-design/components/header"; import Link from "@cloudscape-design/components/link"; import Table from "@cloudscape-design/components/table"; - +import TableCell from "@/components/table-cell"; import type { MusicListResponse } from "@/lib/api"; export default function MusicTable({ data }: { data: MusicListResponse }) { @@ -11,22 +11,32 @@ export default function MusicTable({ data }: { data: MusicListResponse }) {
タイトル, cell: (item) => ( - - {item.music.title} - + + + {item.music.title} + + ), }, { - header: "アーティスト", - cell: (item) => item.music.artist, + header: アーティスト, + cell: (item) => {item.music.artist}, + }, + { + header: BPM, + cell: (item) => {item.music.bpm}, }, - { header: "BPM", cell: (item) => item.music.bpm }, { - header: "登録日時", - cell: (item) => - new Date(item.music.registrationDate).toLocaleDateString("ja-JP"), + header: 登録日時, + cell: (item) => ( + + {new Date(item.music.registrationDate).toLocaleDateString( + "ja-JP", + )} + + ), }, ]} items={data.items} From 9d232e2c9ba9a7e4b3233ffcaa3977d8e26d9d1f Mon Sep 17 00:00:00 2001 From: n4mlz Date: Sun, 13 Sep 2026 16:27:57 +0900 Subject: [PATCH 10/15] =?UTF-8?q?:refactor:=20=E3=83=86=E3=83=BC=E3=83=96?= =?UTF-8?q?=E3=83=AB=E3=82=BB=E3=83=AB=E3=81=AE=E4=BD=99=E7=99=BD=E3=82=92?= =?UTF-8?q?=E5=85=B1=E9=80=9A=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/table-cell.tsx | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 components/table-cell.tsx diff --git a/components/table-cell.tsx b/components/table-cell.tsx new file mode 100644 index 0000000..a26b4a9 --- /dev/null +++ b/components/table-cell.tsx @@ -0,0 +1,5 @@ +import type { ReactNode } from "react"; + +export default function TableCell({ children }: { children: ReactNode }) { + return
{children}
; +} From 7512b92e9facc9556f149d56d8f42b8a7649bc70 Mon Sep 17 00:00:00 2001 From: n4mlz Date: Sun, 13 Sep 2026 16:31:13 +0900 Subject: [PATCH 11/15] =?UTF-8?q?:fix:=20=E6=A5=BD=E6=9B=B2=E3=83=86?= =?UTF-8?q?=E3=83=BC=E3=83=96=E3=83=AB=E5=85=A8=E4=BD=93=E3=81=AE=E4=BD=99?= =?UTF-8?q?=E7=99=BD=E3=82=92=E8=AA=BF=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/music-sheets-table.tsx | 35 +++++++-------- components/music-table.tsx | 71 +++++++++++++++---------------- components/table-cell.tsx | 5 --- 3 files changed, 50 insertions(+), 61 deletions(-) delete mode 100644 components/table-cell.tsx diff --git a/components/music-sheets-table.tsx b/components/music-sheets-table.tsx index c9bce3d..530de57 100644 --- a/components/music-sheets-table.tsx +++ b/components/music-sheets-table.tsx @@ -2,7 +2,7 @@ import Header from "@cloudscape-design/components/header"; import Table from "@cloudscape-design/components/table"; -import TableCell from "@/components/table-cell"; + import type { Sheet } from "@/lib/api"; type SheetWithLabel = Sheet & { difficultyLabel: string }; @@ -13,24 +13,19 @@ export default function MusicSheetsTable({ sheets: SheetWithLabel[]; }) { return ( -
難易度, - cell: (item) => {item.difficultyLabel}, - }, - { - header: レベル, - cell: (item) => {item.level}, - }, - { - header: 譜面制作者, - cell: (item) => {item.notesDesigner}, - }, - ]} - items={sheets} - header={
譜面一覧
} - empty={譜面がありません。} - /> +
+
譜面一覧
+
+
item.difficultyLabel }, + { header: "レベル", cell: (item) => item.level }, + { header: "譜面制作者", cell: (item) => item.notesDesigner }, + ]} + items={sheets} + empty={譜面がありません。} + /> + + ); } diff --git a/components/music-table.tsx b/components/music-table.tsx index 7cc81b7..7a17f04 100644 --- a/components/music-table.tsx +++ b/components/music-table.tsx @@ -3,45 +3,44 @@ import Header from "@cloudscape-design/components/header"; import Link from "@cloudscape-design/components/link"; import Table from "@cloudscape-design/components/table"; -import TableCell from "@/components/table-cell"; + import type { MusicListResponse } from "@/lib/api"; export default function MusicTable({ data }: { data: MusicListResponse }) { return ( -
タイトル, - cell: (item) => ( - - - {item.music.title} - - - ), - }, - { - header: アーティスト, - cell: (item) => {item.music.artist}, - }, - { - header: BPM, - cell: (item) => {item.music.bpm}, - }, - { - header: 登録日時, - cell: (item) => ( - - {new Date(item.music.registrationDate).toLocaleDateString( - "ja-JP", - )} - - ), - }, - ]} - items={data.items} - header={
楽曲一覧
} - empty={楽曲がありません。} - /> +
+
楽曲一覧
+
+
( + + {item.music.title} + + ), + }, + { + header: "アーティスト", + cell: (item) => item.music.artist, + }, + { + header: "BPM", + cell: (item) => item.music.bpm, + }, + { + header: "登録日時", + cell: (item) => + new Date(item.music.registrationDate).toLocaleDateString( + "ja-JP", + ), + }, + ]} + items={data.items} + empty={楽曲がありません。} + /> + + ); } diff --git a/components/table-cell.tsx b/components/table-cell.tsx deleted file mode 100644 index a26b4a9..0000000 --- a/components/table-cell.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import type { ReactNode } from "react"; - -export default function TableCell({ children }: { children: ReactNode }) { - return
{children}
; -} From 91e53a3db0b306818a25ff4cb3c341afcfff4e19 Mon Sep 17 00:00:00 2001 From: n4mlz Date: Sun, 13 Sep 2026 16:32:52 +0900 Subject: [PATCH 12/15] =?UTF-8?q?:fix:=20=E6=A5=BD=E6=9B=B2=E3=83=86?= =?UTF-8?q?=E3=83=BC=E3=83=96=E3=83=AB=E3=81=AE=E5=86=85=E5=81=B4=E4=BD=99?= =?UTF-8?q?=E7=99=BD=E3=82=92=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/music-sheets-table.tsx | 21 ++++++----- components/music-table.tsx | 59 +++++++++++++++---------------- 2 files changed, 38 insertions(+), 42 deletions(-) diff --git a/components/music-sheets-table.tsx b/components/music-sheets-table.tsx index 530de57..16d4143 100644 --- a/components/music-sheets-table.tsx +++ b/components/music-sheets-table.tsx @@ -15,17 +15,16 @@ export default function MusicSheetsTable({ return (
譜面一覧
-
-
item.difficultyLabel }, - { header: "レベル", cell: (item) => item.level }, - { header: "譜面制作者", cell: (item) => item.notesDesigner }, - ]} - items={sheets} - empty={譜面がありません。} - /> - +
item.difficultyLabel }, + { header: "レベル", cell: (item) => item.level }, + { header: "譜面制作者", cell: (item) => item.notesDesigner }, + ]} + items={sheets} + empty={譜面がありません。} + /> ); } diff --git a/components/music-table.tsx b/components/music-table.tsx index 7a17f04..e2d59d1 100644 --- a/components/music-table.tsx +++ b/components/music-table.tsx @@ -10,37 +10,34 @@ export default function MusicTable({ data }: { data: MusicListResponse }) { return (
楽曲一覧
-
-
( - - {item.music.title} - - ), - }, - { - header: "アーティスト", - cell: (item) => item.music.artist, - }, - { - header: "BPM", - cell: (item) => item.music.bpm, - }, - { - header: "登録日時", - cell: (item) => - new Date(item.music.registrationDate).toLocaleDateString( - "ja-JP", - ), - }, - ]} - items={data.items} - empty={楽曲がありません。} - /> - +
( + + {item.music.title} + + ), + }, + { + header: "アーティスト", + cell: (item) => item.music.artist, + }, + { + header: "BPM", + cell: (item) => item.music.bpm, + }, + { + header: "登録日時", + cell: (item) => + new Date(item.music.registrationDate).toLocaleDateString("ja-JP"), + }, + ]} + items={data.items} + empty={楽曲がありません。} + /> ); } From bf507cf7662ae1c63e0a3642be69588ffdc26e7e Mon Sep 17 00:00:00 2001 From: n4mlz Date: Sun, 13 Sep 2026 16:40:23 +0900 Subject: [PATCH 13/15] =?UTF-8?q?:fix:=20Cloudscape=20=E3=83=86=E3=83=BC?= =?UTF-8?q?=E3=83=96=E3=83=AB=E3=81=AE=20variant=20=E3=82=92=E6=95=B4?= =?UTF-8?q?=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/music-list.tsx | 19 +++++----- components/music-sheets-table.tsx | 24 ++++++------- components/music-table.tsx | 60 +++++++++++++++---------------- 3 files changed, 48 insertions(+), 55 deletions(-) diff --git a/components/music-list.tsx b/components/music-list.tsx index 2700fde..608770e 100644 --- a/components/music-list.tsx +++ b/components/music-list.tsx @@ -1,7 +1,6 @@ "use client"; 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"; @@ -46,16 +45,14 @@ export default function MusicList({ } > - - - - {nextPageHref ? ( -
- -
- ) : null} -
-
+ + + {nextPageHref ? ( +
+ +
+ ) : null} +
); diff --git a/components/music-sheets-table.tsx b/components/music-sheets-table.tsx index 16d4143..e3792ae 100644 --- a/components/music-sheets-table.tsx +++ b/components/music-sheets-table.tsx @@ -13,18 +13,16 @@ export default function MusicSheetsTable({ sheets: SheetWithLabel[]; }) { return ( -
-
譜面一覧
-
item.difficultyLabel }, - { header: "レベル", cell: (item) => item.level }, - { header: "譜面制作者", cell: (item) => item.notesDesigner }, - ]} - items={sheets} - empty={譜面がありません。} - /> - +
譜面一覧} + columnDefinitions={[ + { header: "難易度", cell: (item) => item.difficultyLabel }, + { header: "レベル", cell: (item) => item.level }, + { header: "譜面制作者", cell: (item) => item.notesDesigner }, + ]} + items={sheets} + empty={譜面がありません。} + /> ); } diff --git a/components/music-table.tsx b/components/music-table.tsx index e2d59d1..04cf016 100644 --- a/components/music-table.tsx +++ b/components/music-table.tsx @@ -8,36 +8,34 @@ import type { MusicListResponse } from "@/lib/api"; export default function MusicTable({ data }: { data: MusicListResponse }) { return ( -
-
楽曲一覧
-
( - - {item.music.title} - - ), - }, - { - header: "アーティスト", - cell: (item) => item.music.artist, - }, - { - header: "BPM", - cell: (item) => item.music.bpm, - }, - { - header: "登録日時", - cell: (item) => - new Date(item.music.registrationDate).toLocaleDateString("ja-JP"), - }, - ]} - items={data.items} - empty={楽曲がありません。} - /> - +
楽曲一覧} + columnDefinitions={[ + { + header: "タイトル", + cell: (item) => ( + + {item.music.title} + + ), + }, + { + header: "アーティスト", + cell: (item) => item.music.artist, + }, + { + header: "BPM", + cell: (item) => item.music.bpm, + }, + { + header: "登録日時", + cell: (item) => + new Date(item.music.registrationDate).toLocaleDateString("ja-JP"), + }, + ]} + items={data.items} + empty={楽曲がありません。} + /> ); } From d525e7baaa68e5a97554b2c87f1e1092bc244afd Mon Sep 17 00:00:00 2001 From: n4mlz Date: Sun, 13 Sep 2026 16:43:29 +0900 Subject: [PATCH 14/15] =?UTF-8?q?:fix:=20=E8=A9=B3=E7=B4=B0=E7=94=BB?= =?UTF-8?q?=E9=9D=A2=E3=81=AE=E8=AD=9C=E9=9D=A2=E3=83=86=E3=83=BC=E3=83=96?= =?UTF-8?q?=E3=83=AB=E3=82=92=E6=95=B4=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/music-detail.tsx | 14 ++++++-------- components/music-sheets-table.tsx | 2 +- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/components/music-detail.tsx b/components/music-detail.tsx index 1e36203..53dfad0 100644 --- a/components/music-detail.tsx +++ b/components/music-detail.tsx @@ -65,14 +65,12 @@ export default function MusicDetail({ data }: { data: MusicWithSheets }) { /> - 譜面}> - ({ - ...sheet, - difficultyLabel: difficultyLabels[sheet.difficulty], - }))} - /> - + ({ + ...sheet, + difficultyLabel: difficultyLabels[sheet.difficulty], + }))} + /> diff --git a/components/music-sheets-table.tsx b/components/music-sheets-table.tsx index e3792ae..4490e8e 100644 --- a/components/music-sheets-table.tsx +++ b/components/music-sheets-table.tsx @@ -14,7 +14,7 @@ export default function MusicSheetsTable({ }) { return (
譜面一覧} columnDefinitions={[ { header: "難易度", cell: (item) => item.difficultyLabel }, From 870374a9a702d4d7504c7820a4c36b0c7b99313a Mon Sep 17 00:00:00 2001 From: n4mlz Date: Sun, 13 Sep 2026 16:53:18 +0900 Subject: [PATCH 15/15] =?UTF-8?q?:fix:=20=E3=83=86=E3=83=BC=E3=83=96?= =?UTF-8?q?=E3=83=AB=E5=85=A8=E4=BD=93=E3=81=AE=E6=B0=B4=E5=B9=B3=E4=BD=99?= =?UTF-8?q?=E7=99=BD=E3=82=92=E8=AA=BF=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/music-sheets-table.tsx | 1 + components/music-table.tsx | 1 + 2 files changed, 2 insertions(+) diff --git a/components/music-sheets-table.tsx b/components/music-sheets-table.tsx index 4490e8e..93cebcc 100644 --- a/components/music-sheets-table.tsx +++ b/components/music-sheets-table.tsx @@ -14,6 +14,7 @@ export default function MusicSheetsTable({ }) { return (
譜面一覧} columnDefinitions={[ diff --git a/components/music-table.tsx b/components/music-table.tsx index 04cf016..5415937 100644 --- a/components/music-table.tsx +++ b/components/music-table.tsx @@ -9,6 +9,7 @@ import type { MusicListResponse } from "@/lib/api"; export default function MusicTable({ data }: { data: MusicListResponse }) { return (
楽曲一覧} columnDefinitions={[