Skip to content

Commit 8a60e4c

Browse files
authored
Merge pull request #29 from ut-code/feature/save-code
コード保存/読込機能を追加
2 parents d774569 + 8bb1d31 commit 8a60e4c

13 files changed

Lines changed: 617 additions & 196 deletions

File tree

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
-- CreateTable
2+
CREATE TABLE "CodeState" (
3+
"id" SERIAL NOT NULL,
4+
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
5+
"codeData" TEXT NOT NULL,
6+
"codeName" TEXT NOT NULL,
7+
8+
CONSTRAINT "CodeState_pkey" PRIMARY KEY ("id")
9+
);
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/*
2+
Warnings:
3+
4+
- You are about to drop the `BoardState` table. If the table is not empty, all the data it contains will be lost.
5+
- You are about to drop the `CodeState` table. If the table is not empty, all the data it contains will be lost.
6+
7+
*/
8+
-- DropTable
9+
DROP TABLE "BoardState";
10+
11+
-- DropTable
12+
DROP TABLE "CodeState";
13+
14+
-- CreateTable
15+
CREATE TABLE "Board" (
16+
"id" SERIAL NOT NULL,
17+
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
18+
"data" JSONB NOT NULL,
19+
"name" TEXT NOT NULL,
20+
"preview" JSONB NOT NULL,
21+
22+
CONSTRAINT "Board_pkey" PRIMARY KEY ("id")
23+
);
24+
25+
-- CreateTable
26+
CREATE TABLE "Code" (
27+
"id" SERIAL NOT NULL,
28+
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
29+
"data" TEXT NOT NULL,
30+
"name" TEXT NOT NULL,
31+
32+
CONSTRAINT "Code_pkey" PRIMARY KEY ("id")
33+
);

prisma/schema.prisma

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,17 @@ datasource db {
1515
url = env("DATABASE_URL")
1616
}
1717

18-
model BoardState {
18+
model Board {
1919
id Int @id @default(autoincrement())
2020
createdAt DateTime @default(now())
21-
boardData Json
22-
boardName String
23-
boardPreview Json
21+
data Json
22+
name String
23+
preview Json
2424
}
25+
26+
model Code {
27+
id Int @id @default(autoincrement())
28+
createdAt DateTime @default(now())
29+
data String
30+
name String
31+
}
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,9 @@ export async function saveBoard(data: { board: boolean[][]; name: string }, isJa
2929

3030
export type BoardListItem = {
3131
id: number;
32-
boardName: string;
32+
name: string;
3333
createdAt: string;
34-
boardPreview: boolean[][];
34+
preview: boolean[][];
3535
};
3636

3737
export async function fetchBoardList(isJapanese: boolean): Promise<BoardListItem[] | undefined> {

src/lib/api/code.ts

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
export async function saveCode(data: { code: string; name: string }, isJapanese: boolean) {
2+
try {
3+
const response = await fetch("/api/code", {
4+
method: "POST",
5+
headers: {
6+
"Content-Type": "application/json",
7+
},
8+
body: JSON.stringify(data),
9+
});
10+
11+
if (!response.ok) {
12+
throw new Error("Failed to communicate with the server.");
13+
}
14+
15+
if (isJapanese) {
16+
alert("コードを保存しました!");
17+
} else {
18+
alert("Code saved!");
19+
}
20+
} catch (err) {
21+
console.error("Save Error:", err);
22+
if (isJapanese) {
23+
alert("保存に失敗しました。");
24+
} else {
25+
alert("Failed to save.");
26+
}
27+
}
28+
}
29+
30+
export type CodeListItem = {
31+
id: number;
32+
name: string;
33+
createdAt: string;
34+
};
35+
36+
export async function fetchCodeList(isJapanese: boolean): Promise<CodeListItem[] | undefined> {
37+
try {
38+
const response = await fetch("/api/code");
39+
40+
if (!response.ok) {
41+
if (response.status === 404) {
42+
throw new Error("There is no saved data.");
43+
} else {
44+
throw new Error("Failed to communicate with the server.");
45+
}
46+
}
47+
48+
const codeList = await response.json();
49+
50+
return codeList as CodeListItem[];
51+
} catch (err) {
52+
console.error("Load error", err);
53+
if (isJapanese) {
54+
alert("読み込みに失敗しました。");
55+
} else {
56+
alert("Failed to load.");
57+
}
58+
}
59+
}
60+
61+
export async function loadCodeById(id: number, isJapanese: boolean): Promise<string | undefined> {
62+
try {
63+
const response = await fetch(`/api/code?id=${id}`);
64+
65+
if (!response.ok) {
66+
if (response.status === 404) {
67+
throw new Error("The specified ID data was not found.");
68+
} else {
69+
throw new Error("Failed to communicate with the server.");
70+
}
71+
}
72+
73+
const loadedCode = await response.json();
74+
75+
return loadedCode as string;
76+
} catch (err) {
77+
console.error("Load error", err);
78+
if (isJapanese) {
79+
alert("読み込みに失敗しました。");
80+
} else {
81+
alert("Failed to load.");
82+
}
83+
}
84+
}

src/lib/board-preview.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ const PREVIEW_SIZE = 20;
44
* 任意のサイズの盤面データから、中央 20x20 のプレビューを生成します。
55
* 20x20 に満たない場合は、中央に配置し、周囲を false (空白) で埋めます。
66
*/
7-
export function createPreview(boardData: boolean[][]): boolean[][] {
7+
export function createBoardPreview(boardData: boolean[][]): boolean[][] {
88
const boardHeight = boardData.length;
99
const boardWidth = boardData[0]?.length || 0;
1010

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
<script lang="ts">
2+
import type { BoardManager } from "$lib/models/BoardManager.svelte";
3+
4+
let {
5+
manager,
6+
isJapanese,
7+
onSelect,
8+
}: {
9+
manager: BoardManager;
10+
isJapanese: boolean;
11+
onSelect: (id: number) => void;
12+
} = $props();
13+
</script>
14+
15+
<dialog class="modal" open={manager.saveState.saving}>
16+
<form method="dialog" class="modal-box">
17+
<h3 class="font-bold text-lg">{isJapanese ? "盤面を保存" : "Save board"}</h3>
18+
{#if manager.saveState.saving}
19+
<div class="flex flex-row gap-4 mt-4">
20+
<div class="w-90 flex flex-col gap-4">
21+
<p class="py-4">
22+
{isJapanese
23+
? "保存する盤面に名前を付けてください(任意)。"
24+
: "Please name the board you wish to save (optional)."}
25+
</p>
26+
<input
27+
type="text"
28+
placeholder={isJapanese ? "盤面名を入力" : "Enter board name"}
29+
class="input input-bordered w-full max-w-xs"
30+
bind:value={manager.saveState.name}
31+
/>
32+
</div>
33+
<div class="flex flex-col flex-shrink-0">
34+
<div class="text-center text-sm mb-2">
35+
{isJapanese ? "プレビュー" : "Preview"}
36+
</div>
37+
<div class="board-preview">
38+
{#each manager.saveState.preview as row, i (i)}
39+
<div class="preview-row">
40+
{#each row as cell, j (j)}
41+
<div class="preview-cell {cell ? 'alive' : ''}"></div>
42+
{/each}
43+
</div>
44+
{/each}
45+
</div>
46+
</div>
47+
</div>
48+
<div class="modal-action">
49+
<button type="button" class="btn" onclick={() => manager.closeSaveModal()}
50+
>{isJapanese ? "キャンセル" : "Cancel"}</button
51+
>
52+
<button
53+
type="submit"
54+
class="btn btn-primary"
55+
onclick={() => manager.save(isJapanese)}
56+
disabled={!manager.saveState.saving}
57+
>
58+
{isJapanese ? "保存" : "Save"}
59+
</button>
60+
</div>
61+
{/if}
62+
</form>
63+
</dialog>
64+
65+
<dialog class="modal" open={manager.loadState.state !== "closed"}>
66+
<div class="modal-box w-11/12 max-w-5xl">
67+
<h3 class="font-bold text-lg">{isJapanese ? "盤面をロード" : "Load board"}</h3>
68+
69+
{#if manager.loadState.state === "loading"}
70+
<p class="py-4">
71+
{isJapanese ? "保存されている盤面を読み込み中..." : "Loading saved boards..."}
72+
</p>
73+
<span class="loading loading-spinner loading-lg"></span>
74+
{:else if manager.loadState.state === "list" && manager.loadState.list.length === 0}
75+
<p class="py-4">
76+
{isJapanese ? "保存されている盤面はありません。" : "No saved boards found."}
77+
</p>
78+
{:else if manager.loadState.state === "list"}
79+
<div class="overflow-x-auto h-96">
80+
<table class="table w-full">
81+
<thead>
82+
<tr>
83+
<th class="pl-5">{isJapanese ? "プレビュー" : "Preview"}</th>
84+
<th>{isJapanese ? "盤面名" : "Board Name"}</th>
85+
<th>{isJapanese ? "保存日時" : "Saved At"}</th>
86+
<th></th>
87+
</tr>
88+
</thead>
89+
<tbody>
90+
{#each manager.loadState.list as item (item.id)}
91+
<tr class="hover:bg-base-300">
92+
<td>
93+
<div class="board-preview">
94+
{#each item.preview as row, i (i)}
95+
<div class="preview-row">
96+
{#each row as cell, j (j)}
97+
<div class="preview-cell {cell ? 'alive' : ''}"></div>
98+
{/each}
99+
</div>
100+
{/each}
101+
</div>
102+
</td>
103+
<td>{item.name}</td>
104+
<td>{new Date(item.createdAt).toLocaleString(isJapanese ? "ja-JP" : "en-US")}</td>
105+
<td class="text-right">
106+
<button class="btn btn-sm btn-primary" onclick={() => onSelect(item.id)}>
107+
{isJapanese ? "ロード" : "Load"}
108+
</button>
109+
</td>
110+
</tr>
111+
{/each}
112+
</tbody>
113+
</table>
114+
</div>
115+
{/if}
116+
117+
<div class="modal-action">
118+
<button class="btn" onclick={() => manager.closeLoadModal()}>
119+
{isJapanese ? "閉じる" : "Close"}
120+
</button>
121+
</div>
122+
</div>
123+
</dialog>
124+
125+
<style>
126+
.board-preview {
127+
display: grid;
128+
grid-template-columns: repeat(20, 3px);
129+
grid-template-rows: repeat(20, 3px);
130+
width: 60px;
131+
height: 60px;
132+
border: 1px solid #9ca3af;
133+
background-color: white;
134+
}
135+
.preview-row {
136+
display: contents;
137+
}
138+
.preview-cell {
139+
width: 3px;
140+
height: 3px;
141+
}
142+
.preview-cell.alive {
143+
background-color: black;
144+
}
145+
</style>

0 commit comments

Comments
 (0)