Skip to content

Commit d8e3f87

Browse files
authored
Merge pull request #24 from ut-code/board-selection-ui
add Board selection UI
2 parents 78f9de5 + 4a2538c commit d8e3f87

3 files changed

Lines changed: 184 additions & 45 deletions

File tree

src/routes/+page.svelte

Lines changed: 102 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import lifeGameJS from "@/iframe/life-game.js?raw";
66
import patterns from "$lib/board-templates";
77
import * as icons from "$lib/icons/index.ts";
8-
import { loadBoard, saveBoard } from "./api.ts";
8+
import { saveBoard, fetchBoardList, loadBoardById, type BoardListItem } from "./api.ts";
99
1010
let editingCode = $state(lifeGameJS);
1111
let appliedCode = $state(lifeGameJS);
@@ -23,9 +23,14 @@
2323
let generationFigure = $state(0);
2424
let sizeValue = $state(20);
2525
26-
type SaveState = { saving: false } | { saving: true; boardData: boolean[][] };
26+
type SaveState = { saving: false } | { saving: true; boardData: boolean[][]; boardName: string };
2727
let saveState: SaveState = $state({ saving: false });
28-
let boardNameInput = $state("");
28+
29+
type LoadState =
30+
| { state: "closed" }
31+
| { state: "loading" }
32+
| { state: "list"; list: BoardListItem[] };
33+
let loadState: LoadState = $state({ state: "closed" });
2934
3035
type OngoingEvent =
3136
| "play"
@@ -56,8 +61,7 @@
5661
break;
5762
}
5863
case "save_board": {
59-
saveState = { saving: true, boardData: event.data.data as boolean[][] };
60-
boardNameInput = "";
64+
saveState = { saving: true, boardData: event.data.data as boolean[][], boardName: "" };
6165
break;
6266
}
6367
default: {
@@ -81,20 +85,32 @@
8185
async function handleSave() {
8286
if (!saveState.saving) return;
8387
84-
const name = boardNameInput.trim() === "" ? "Unnamed Board" : boardNameInput.trim();
88+
const name = saveState.boardName.trim() === "" ? "Unnamed Board" : saveState.boardName.trim();
8589
8690
await saveBoard({ board: saveState.boardData, name: name }, isJapanese);
8791
8892
saveState = { saving: false };
89-
boardNameInput = "";
9093
}
9194
9295
async function handleLoad() {
93-
const board = await loadBoard(isJapanese);
96+
loadState = { state: "loading" };
97+
98+
const list = await fetchBoardList(isJapanese);
99+
100+
if (list) {
101+
loadState = { state: "list", list };
102+
} else {
103+
loadState = { state: "closed" };
104+
}
105+
}
106+
107+
async function selectBoard(id: number) {
108+
loadState = { state: "closed" };
109+
110+
const board = await loadBoardById(id, isJapanese);
94111
if (board) {
95112
sendEvent("apply_board", board);
96113
}
97-
return;
98114
}
99115
</script>
100116

@@ -196,34 +212,87 @@
196212
</div>
197213
</div>
198214

199-
<input type="checkbox" class="modal-toggle" bind:checked={saveState.saving} />
200-
<div class="modal" class:modal-open={saveState.saving}>
201-
<div class="modal-box">
215+
<dialog class="modal" open={saveState.saving}>
216+
<form method="dialog" class="modal-box">
202217
<h3 class="font-bold text-lg">{isJapanese ? "盤面を保存" : "Save board"}</h3>
203-
<p class="py-4">
204-
{isJapanese
205-
? "保存する盤面に名前を付けてください(任意)。"
206-
: "Please name the board you wish to save (optional)."}
207-
</p>
208-
<input
209-
type="text"
210-
placeholder={isJapanese ? "盤面名を入力" : "Enter board name"}
211-
class="input input-bordered w-full max-w-xs"
212-
bind:value={boardNameInput}
213-
/>
218+
{#if saveState.saving}
219+
<p class="py-4">
220+
{isJapanese
221+
? "保存する盤面に名前を付けてください(任意)。"
222+
: "Please name the board you wish to save (optional)."}
223+
</p>
224+
<input
225+
type="text"
226+
placeholder={isJapanese ? "盤面名を入力" : "Enter board name"}
227+
class="input input-bordered w-full max-w-xs"
228+
bind:value={saveState.boardName}
229+
/>
230+
<div class="modal-action">
231+
<button type="button" class="btn" onclick={() => (saveState = { saving: false })}
232+
>{isJapanese ? "キャンセル" : "Cancel"}</button
233+
>
234+
<button
235+
type="submit"
236+
class="btn btn-primary"
237+
onclick={handleSave}
238+
disabled={!saveState.saving}
239+
>
240+
{isJapanese ? "保存" : "Save"}
241+
</button>
242+
</div>
243+
{/if}
244+
</form>
245+
</dialog>
246+
247+
<dialog class="modal" open={loadState.state !== "closed"}>
248+
<div class="modal-box w-11/12 max-w-5xl">
249+
<h3 class="font-bold text-lg">{isJapanese ? "盤面をロード" : "Load board"}</h3>
250+
251+
{#if loadState.state === "loading"}
252+
<p class="py-4">
253+
{isJapanese ? "保存されている盤面を読み込み中..." : "Loading saved boards..."}
254+
</p>
255+
<span class="loading loading-spinner loading-lg"></span>
256+
{:else if loadState.state === "list" && loadState.list.length === 0}
257+
<p class="py-4">
258+
{isJapanese ? "保存されている盤面はありません。" : "No saved boards found."}
259+
</p>
260+
{:else if loadState.state === "list"}
261+
<div class="overflow-x-auto h-96">
262+
<table class="table w-full">
263+
<thead>
264+
<tr>
265+
<th>{isJapanese ? "盤面名" : "Board Name"}</th>
266+
<th>{isJapanese ? "保存日時" : "Saved At"}</th>
267+
<th></th>
268+
</tr>
269+
</thead>
270+
<tbody>
271+
{#each loadState.list as item (item.id)}
272+
<tr class="hover:bg-base-300">
273+
<td>{item.boardName}</td>
274+
<td>{new Date(item.createdAt).toLocaleString(isJapanese ? "ja-JP" : "en-US")}</td>
275+
<td class="text-right">
276+
<button class="btn btn-sm btn-primary" onclick={() => selectBoard(item.id)}>
277+
{isJapanese ? "ロード" : "Load"}
278+
</button>
279+
</td>
280+
</tr>
281+
{/each}
282+
</tbody>
283+
</table>
284+
</div>
285+
{/if}
286+
214287
<div class="modal-action">
215-
<button class="btn" onclick={() => (saveState = { saving: false })}
216-
>{isJapanese ? "キャンセル" : "Cancel"}</button
217-
>
218-
<button class="btn btn-primary" onclick={handleSave} disabled={!saveState.saving}>
219-
{isJapanese ? "保存" : "Save"}
288+
<button class="btn" onclick={() => (loadState = { state: "closed" })}>
289+
{isJapanese ? "閉じる" : "Close"}
220290
</button>
221291
</div>
222292
</div>
223-
</div>
293+
</dialog>
224294

225-
<input type="checkbox" class="modal-toggle" bind:checked={resetModalOpen} />
226-
<div class="modal" class:modal-open={resetModalOpen}>
295+
<dialog class="modal" open={resetModalOpen}>
227296
<div class="modal-box">
228297
<h3 class="font-bold text-lg">{isJapanese ? "リセット確認" : "Reset confirmation"}</h3>
229298
<p class="py-4">
@@ -246,7 +315,7 @@
246315
>
247316
</div>
248317
</div>
249-
</div>
318+
</dialog>
250319

251320
<div class="flex box-border h-screen" style="height: calc(100vh - 4rem - 3rem);">
252321
<div
@@ -374,6 +443,7 @@
374443
class="btn btn-ghost hover:bg-[rgb(220,220,220)] text-black"
375444
onclick={() => {
376445
isProgress = false;
446+
sendEvent("pause");
377447
sendEvent("save_board");
378448
}}
379449
>

src/routes/api.ts

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,13 @@ export async function saveBoard(data: { board: boolean[][]; name: string }, isJa
3232
}
3333
}
3434

35-
export async function loadBoard(isJapanese: boolean): Promise<boolean[][] | undefined> {
35+
export type BoardListItem = {
36+
id: number;
37+
boardName: string;
38+
createdAt: string;
39+
};
40+
41+
export async function fetchBoardList(isJapanese: boolean): Promise<BoardListItem[] | undefined> {
3642
try {
3743
const response = await fetch("/api/board");
3844

@@ -52,9 +58,46 @@ export async function loadBoard(isJapanese: boolean): Promise<boolean[][] | unde
5258
}
5359
}
5460

61+
const boardList = await response.json();
62+
63+
return boardList as BoardListItem[];
64+
} catch (err) {
65+
if (isJapanese) {
66+
console.error("読込エラー:", err);
67+
alert("読み込みに失敗しました。");
68+
} else {
69+
console.error("Load error", err);
70+
alert("Failed to load.");
71+
}
72+
}
73+
}
74+
75+
export async function loadBoardById(
76+
id: number,
77+
isJapanese: boolean,
78+
): Promise<boolean[][] | undefined> {
79+
try {
80+
const response = await fetch(`/api/board?id=${id}`);
81+
82+
if (!response.ok) {
83+
if (response.status === 404) {
84+
if (isJapanese) {
85+
throw new Error("指定されたIDのデータが見つかりません。");
86+
} else {
87+
throw new Error("The specified ID data was not found.");
88+
}
89+
} else {
90+
if (isJapanese) {
91+
throw new Error("サーバーとの通信に失敗しました。");
92+
} else {
93+
throw new Error("Failed to communicate with the server.");
94+
}
95+
}
96+
}
97+
5598
const loadedBoard = await response.json();
5699

57-
return loadedBoard as boolean[][]; // TODO: add proper types
100+
return loadedBoard as boolean[][];
58101
} catch (err) {
59102
if (isJapanese) {
60103
console.error("読込エラー:", err);

src/routes/api/board/+server.ts

Lines changed: 37 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -28,17 +28,43 @@ export async function POST({ request }) {
2828
return json(newState, { status: 201 });
2929
}
3030

31-
export async function GET() {
32-
// データベースから一番「最後」に保存されたデータを1件だけ探す
33-
const latestState = await prisma.boardState.findFirst({
34-
orderBy: {
35-
createdAt: "desc", // 作成日時(createdAt)の降順(desc)で並び替え
36-
},
37-
});
31+
export async function GET({ url }) {
32+
const boardId = url.searchParams.get("id");
3833

39-
if (!latestState) {
40-
return json({ message: "No state found" }, { status: 404 });
41-
}
34+
if (boardId) {
35+
//IDが指定された場合、そのIDの盤面を返す
36+
const id = parseInt(boardId, 10);
37+
if (isNaN(id)) {
38+
return json({ message: "無効なIDです。" }, { status: 400 });
39+
}
40+
41+
const state = await prisma.boardState.findUnique({
42+
where: { id: id },
43+
select: { boardData: true },
44+
});
4245

43-
return json(latestState.boardData);
46+
if (!state) {
47+
return json({ message: `ID: ${id} の盤面は見つかりません。` }, { status: 404 });
48+
}
49+
50+
return json(state.boardData);
51+
} else {
52+
//IDが指定されなかった場合、全ての盤面のリストを返す
53+
const allStates = await prisma.boardState.findMany({
54+
orderBy: {
55+
createdAt: "desc",
56+
},
57+
select: {
58+
id: true,
59+
boardName: true,
60+
createdAt: true,
61+
},
62+
});
63+
64+
if (!allStates || allStates.length === 0) {
65+
return json({ message: "No state found" }, { status: 404 });
66+
}
67+
68+
return json(allStates);
69+
}
4470
}

0 commit comments

Comments
 (0)