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
134 changes: 102 additions & 32 deletions src/routes/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import lifeGameJS from "@/iframe/life-game.js?raw";
import patterns from "$lib/board-templates";
import * as icons from "$lib/icons/index.ts";
import { loadBoard, saveBoard } from "./api.ts";
import { saveBoard, fetchBoardList, loadBoardById, type BoardListItem } from "./api.ts";

let editingCode = $state(lifeGameJS);
let appliedCode = $state(lifeGameJS);
Expand All @@ -23,9 +23,14 @@
let generationFigure = $state(0);
let sizeValue = $state(20);

type SaveState = { saving: false } | { saving: true; boardData: boolean[][] };
type SaveState = { saving: false } | { saving: true; boardData: boolean[][]; boardName: string };
let saveState: SaveState = $state({ saving: false });
let boardNameInput = $state("");

type LoadState =
| { state: "closed" }
| { state: "loading" }
| { state: "list"; list: BoardListItem[] };
let loadState: LoadState = $state({ state: "closed" });

type OngoingEvent =
| "play"
Expand Down Expand Up @@ -56,8 +61,7 @@
break;
}
case "save_board": {
saveState = { saving: true, boardData: event.data.data as boolean[][] };
boardNameInput = "";
saveState = { saving: true, boardData: event.data.data as boolean[][], boardName: "" };
break;
}
default: {
Expand All @@ -81,20 +85,32 @@
async function handleSave() {
if (!saveState.saving) return;

const name = boardNameInput.trim() === "" ? "Unnamed Board" : boardNameInput.trim();
const name = saveState.boardName.trim() === "" ? "Unnamed Board" : saveState.boardName.trim();

await saveBoard({ board: saveState.boardData, name: name }, isJapanese);

saveState = { saving: false };
boardNameInput = "";
}

async function handleLoad() {
const board = await loadBoard(isJapanese);
loadState = { state: "loading" };

const list = await fetchBoardList(isJapanese);

if (list) {
loadState = { state: "list", list };
} else {
loadState = { state: "closed" };
}
}

async function selectBoard(id: number) {
loadState = { state: "closed" };

const board = await loadBoardById(id, isJapanese);
if (board) {
sendEvent("apply_board", board);
}
return;
}
</script>

Expand Down Expand Up @@ -196,34 +212,87 @@
</div>
</div>

<input type="checkbox" class="modal-toggle" bind:checked={saveState.saving} />
<div class="modal" class:modal-open={saveState.saving}>
<div class="modal-box">
<dialog class="modal" open={saveState.saving}>
<form method="dialog" class="modal-box">
<h3 class="font-bold text-lg">{isJapanese ? "盤面を保存" : "Save board"}</h3>
<p class="py-4">
{isJapanese
? "保存する盤面に名前を付けてください(任意)。"
: "Please name the board you wish to save (optional)."}
</p>
<input
type="text"
placeholder={isJapanese ? "盤面名を入力" : "Enter board name"}
class="input input-bordered w-full max-w-xs"
bind:value={boardNameInput}
/>
{#if saveState.saving}
<p class="py-4">
{isJapanese
? "保存する盤面に名前を付けてください(任意)。"
: "Please name the board you wish to save (optional)."}
</p>
<input
type="text"
placeholder={isJapanese ? "盤面名を入力" : "Enter board name"}
class="input input-bordered w-full max-w-xs"
bind:value={saveState.boardName}
/>
<div class="modal-action">
<button type="button" class="btn" onclick={() => (saveState = { saving: false })}
>{isJapanese ? "キャンセル" : "Cancel"}</button
>
<button
type="submit"
class="btn btn-primary"
onclick={handleSave}
disabled={!saveState.saving}
>
{isJapanese ? "保存" : "Save"}
</button>
</div>
{/if}
</form>
</dialog>

<dialog class="modal" open={loadState.state !== "closed"}>
<div class="modal-box w-11/12 max-w-5xl">
<h3 class="font-bold text-lg">{isJapanese ? "盤面をロード" : "Load board"}</h3>

{#if loadState.state === "loading"}
<p class="py-4">
{isJapanese ? "保存されている盤面を読み込み中..." : "Loading saved boards..."}
</p>
<span class="loading loading-spinner loading-lg"></span>
{:else if loadState.state === "list" && loadState.list.length === 0}
<p class="py-4">
{isJapanese ? "保存されている盤面はありません。" : "No saved boards found."}
</p>
{:else if loadState.state === "list"}
<div class="overflow-x-auto h-96">
<table class="table w-full">
<thead>
<tr>
<th>{isJapanese ? "盤面名" : "Board Name"}</th>
<th>{isJapanese ? "保存日時" : "Saved At"}</th>
<th></th>
</tr>
</thead>
<tbody>
{#each loadState.list as item (item.id)}
<tr class="hover:bg-base-300">
<td>{item.boardName}</td>
<td>{new Date(item.createdAt).toLocaleString(isJapanese ? "ja-JP" : "en-US")}</td>
<td class="text-right">
<button class="btn btn-sm btn-primary" onclick={() => selectBoard(item.id)}>
{isJapanese ? "ロード" : "Load"}
</button>
</td>
</tr>
{/each}
</tbody>
</table>
</div>
{/if}

<div class="modal-action">
<button class="btn" onclick={() => (saveState = { saving: false })}
>{isJapanese ? "キャンセル" : "Cancel"}</button
>
<button class="btn btn-primary" onclick={handleSave} disabled={!saveState.saving}>
{isJapanese ? "保存" : "Save"}
<button class="btn" onclick={() => (loadState = { state: "closed" })}>
{isJapanese ? "閉じる" : "Close"}
</button>
</div>
</div>
</div>
</dialog>

<input type="checkbox" class="modal-toggle" bind:checked={resetModalOpen} />
<div class="modal" class:modal-open={resetModalOpen}>
<dialog class="modal" open={resetModalOpen}>
<div class="modal-box">
<h3 class="font-bold text-lg">{isJapanese ? "リセット確認" : "Reset confirmation"}</h3>
<p class="py-4">
Expand All @@ -246,7 +315,7 @@
>
</div>
</div>
</div>
</dialog>

<div class="flex box-border h-screen" style="height: calc(100vh - 4rem - 3rem);">
<div
Expand Down Expand Up @@ -374,6 +443,7 @@
class="btn btn-ghost hover:bg-[rgb(220,220,220)] text-black"
onclick={() => {
isProgress = false;
sendEvent("pause");
sendEvent("save_board");
}}
>
Expand Down
47 changes: 45 additions & 2 deletions src/routes/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,13 @@ export async function saveBoard(data: { board: boolean[][]; name: string }, isJa
}
}

export async function loadBoard(isJapanese: boolean): Promise<boolean[][] | undefined> {
export type BoardListItem = {
id: number;
boardName: string;
createdAt: string;
};

export async function fetchBoardList(isJapanese: boolean): Promise<BoardListItem[] | undefined> {
try {
const response = await fetch("/api/board");

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

const boardList = await response.json();

return boardList as BoardListItem[];
} catch (err) {
if (isJapanese) {
console.error("読込エラー:", err);
alert("読み込みに失敗しました。");
} else {
console.error("Load error", err);
alert("Failed to load.");
}
}
}

export async function loadBoardById(
id: number,
isJapanese: boolean,
): Promise<boolean[][] | undefined> {
try {
const response = await fetch(`/api/board?id=${id}`);

if (!response.ok) {
if (response.status === 404) {
if (isJapanese) {
throw new Error("指定されたIDのデータが見つかりません。");
} else {
throw new Error("The specified ID data was not found.");
}
} else {
if (isJapanese) {
throw new Error("サーバーとの通信に失敗しました。");
} else {
throw new Error("Failed to communicate with the server.");
}
}
}

const loadedBoard = await response.json();

return loadedBoard as boolean[][]; // TODO: add proper types
return loadedBoard as boolean[][];
} catch (err) {
if (isJapanese) {
console.error("読込エラー:", err);
Expand Down
48 changes: 37 additions & 11 deletions src/routes/api/board/+server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,17 +28,43 @@ export async function POST({ request }) {
return json(newState, { status: 201 });
}

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

if (!latestState) {
return json({ message: "No state found" }, { status: 404 });
}
if (boardId) {
//IDが指定された場合、そのIDの盤面を返す
const id = parseInt(boardId, 10);
if (isNaN(id)) {
return json({ message: "無効なIDです。" }, { status: 400 });
}

const state = await prisma.boardState.findUnique({
where: { id: id },
select: { boardData: true },
});

return json(latestState.boardData);
if (!state) {
return json({ message: `ID: ${id} の盤面は見つかりません。` }, { status: 404 });
}

return json(state.boardData);
} else {
//IDが指定されなかった場合、全ての盤面のリストを返す
const allStates = await prisma.boardState.findMany({
orderBy: {
createdAt: "desc",
},
select: {
id: true,
boardName: true,
createdAt: true,
},
});

if (!allStates || allStates.length === 0) {
return json({ message: "No state found" }, { status: 404 });
}

return json(allStates);
}
}