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
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/*
Warnings:

- Added the required column `boardPreview` to the `BoardState` table without a default value. This is not possible if the table is not empty.

*/
-- AlterTable
ALTER TABLE "BoardState" ADD COLUMN "boardPreview" JSONB NOT NULL;
1 change: 1 addition & 0 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,5 @@ model BoardState {
createdAt DateTime @default(now())
boardData Json
boardName String
boardPreview Json
}
5 changes: 4 additions & 1 deletion src/iframe/life-game.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,17 @@
</head>
<body
style="
margin: 0;
height: 100%;
display: flex;
align-items: flex-start;
justify-content: center;
padding-top: 10px;
"
>
<table id="game-board" style="border-collapse: collapse"></table>
<div style="max-width: 100%">
<table id="game-board" style="border-collapse: collapse"></table>
</div>
<script>
"@JAVASCRIPT@";
</script>
Expand Down
1 change: 1 addition & 0 deletions src/iframe/life-game.js
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,7 @@ on.save_board = async () => {
};

on.apply_board = (newBoard) => {
boardSize = newBoard.length;
board = newBoard;
renderBoard();
generationChange(0);
Expand Down
38 changes: 38 additions & 0 deletions src/lib/board-preview.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
const PREVIEW_SIZE = 20;

/**
* 任意のサイズの盤面データから、中央 20x20 のプレビューを生成します。
* 20x20 に満たない場合は、中央に配置し、周囲を false (空白) で埋めます。
*/
export function createPreview(boardData: boolean[][]): boolean[][] {
const boardHeight = boardData.length;
const boardWidth = boardData[0]?.length || 0;

const finalPreview: boolean[][] = Array.from({ length: PREVIEW_SIZE }, () =>
Array(PREVIEW_SIZE).fill(false),
);

const sourceStartRow = Math.max(0, Math.floor((boardHeight - PREVIEW_SIZE) / 2));
const sourceStartCol = Math.max(0, Math.floor((boardWidth - PREVIEW_SIZE) / 2));

const destStartRow = Math.max(0, Math.floor((PREVIEW_SIZE - boardHeight) / 2));
const destStartCol = Math.max(0, Math.floor((PREVIEW_SIZE - boardWidth) / 2));

const rowsToCopy = Math.min(PREVIEW_SIZE - destStartRow, boardHeight - sourceStartRow);
const colsToCopy = Math.min(PREVIEW_SIZE - destStartCol, boardWidth - sourceStartCol);

if (rowsToCopy <= 0 || colsToCopy <= 0) {
return finalPreview;
}

for (let i = 0; i < rowsToCopy; i++) {
for (let j = 0; j < colsToCopy; j++) {
if (boardData[sourceStartRow + i]?.[sourceStartCol + j] !== undefined) {
finalPreview[destStartRow + i][destStartCol + j] =
boardData[sourceStartRow + i][sourceStartCol + j];
}
}
}

return finalPreview;
}
84 changes: 71 additions & 13 deletions src/routes/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import patterns from "$lib/board-templates";
import * as icons from "$lib/icons/index.ts";
import { saveBoard, fetchBoardList, loadBoardById, type BoardListItem } from "./api.ts";
import { createPreview } from "$lib/board-preview";

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

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

type LoadState =
Expand Down Expand Up @@ -61,7 +64,9 @@
break;
}
case "save_board": {
saveState = { saving: true, boardData: event.data.data as boolean[][], boardName: "" };
const board = event.data.data as boolean[][];
const preview = createPreview(board);
saveState = { saving: true, boardData: board, boardName: "", boardPreview: preview };
break;
}
default: {
Expand Down Expand Up @@ -109,6 +114,7 @@

const board = await loadBoardById(id, isJapanese);
if (board) {
sizeValue = board.length;
sendEvent("apply_board", board);
}
}
Expand Down Expand Up @@ -216,17 +222,35 @@
<form method="dialog" class="modal-box">
<h3 class="font-bold text-lg">{isJapanese ? "盤面を保存" : "Save board"}</h3>
{#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="flex flex-row gap-4 mt-4">
<div class="w-90 flex flex-col gap-4">
<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>
<div class="flex flex-col flex-shrink-0">
<div class="text-center text-sm mb-2">
{isJapanese ? "プレビュー" : "Preview"}
</div>
<div class="board-preview">
{#each saveState.boardPreview as row, i (i)}
<div class="preview-row">
{#each row as cell, j (j)}
<div class="preview-cell {cell ? 'alive' : ''}"></div>
{/each}
</div>
{/each}
</div>
</div>
</div>
<div class="modal-action">
<button type="button" class="btn" onclick={() => (saveState = { saving: false })}
>{isJapanese ? "キャンセル" : "Cancel"}</button
Expand Down Expand Up @@ -262,6 +286,7 @@
<table class="table w-full">
<thead>
<tr>
<th class="pl-5">{isJapanese ? "プレビュー" : "Preview"}</th>
<th>{isJapanese ? "盤面名" : "Board Name"}</th>
<th>{isJapanese ? "保存日時" : "Saved At"}</th>
<th></th>
Expand All @@ -270,6 +295,17 @@
<tbody>
{#each loadState.list as item (item.id)}
<tr class="hover:bg-base-300">
<td>
<div class="board-preview">
{#each item.boardPreview as row, i (i)}
<div class="preview-row">
{#each row as cell, j (j)}
<div class="preview-cell {cell ? 'alive' : ''}"></div>
{/each}
</div>
{/each}
</div>
</td>
<td>{item.boardName}</td>
<td>{new Date(item.createdAt).toLocaleString(isJapanese ? "ja-JP" : "en-US")}</td>
<td class="text-right">
Expand Down Expand Up @@ -494,3 +530,25 @@
</button>
</div>
</div>

<style>
.board-preview {
display: grid;
grid-template-columns: repeat(20, 3px);
grid-template-rows: repeat(20, 3px);
width: 60px;
height: 60px;
border: 1px solid #9ca3af;
background-color: white;
}
.preview-row {
display: contents;
}
.preview-cell {
width: 3px;
height: 3px;
}
.preview-cell.alive {
background-color: black;
}
</style>
40 changes: 9 additions & 31 deletions src/routes/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,7 @@ export async function saveBoard(data: { board: boolean[][]; name: string }, isJa
});

if (!response.ok) {
if (isJapanese) {
throw new Error("サーバーとの通信に失敗しました。");
} else {
throw new Error("Failed to communicate with the server.");
}
throw new Error("Failed to communicate with the server.");
}

if (isJapanese) {
Expand All @@ -22,11 +18,10 @@ export async function saveBoard(data: { board: boolean[][]; name: string }, isJa
alert("Board saved!");
}
} catch (err) {
console.error("Save Error:", err);
if (isJapanese) {
console.error("保存エラー:", err);
alert("保存に失敗しました。");
} else {
console.error("Save Error:", err);
alert("Failed to save.");
}
}
Expand All @@ -36,6 +31,7 @@ export type BoardListItem = {
id: number;
boardName: string;
createdAt: string;
boardPreview: boolean[][];
};

export async function fetchBoardList(isJapanese: boolean): Promise<BoardListItem[] | undefined> {
Expand All @@ -44,29 +40,20 @@ export async function fetchBoardList(isJapanese: boolean): Promise<BoardListItem

if (!response.ok) {
if (response.status === 404) {
if (isJapanese) {
throw new Error("保存されているデータがありません。");
} else {
throw new Error("There is no saved data.");
}
throw new Error("There is no saved data.");
} else {
if (isJapanese) {
throw new Error("サーバーとの通信に失敗しました。");
} else {
throw new Error("Failed to communicate with the server.");
}
throw new Error("Failed to communicate with the server.");
}
}

const boardList = await response.json();

return boardList as BoardListItem[];
} catch (err) {
console.error("Load error", err);
if (isJapanese) {
console.error("読込エラー:", err);
alert("読み込みに失敗しました。");
} else {
console.error("Load error", err);
alert("Failed to load.");
}
}
Expand All @@ -81,29 +68,20 @@ export async function loadBoardById(

if (!response.ok) {
if (response.status === 404) {
if (isJapanese) {
throw new Error("指定されたIDのデータが見つかりません。");
} else {
throw new Error("The specified ID data was not found.");
}
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.");
}
throw new Error("Failed to communicate with the server.");
}
}

const loadedBoard = await response.json();

return loadedBoard as boolean[][];
} catch (err) {
console.error("Load error", err);
if (isJapanese) {
console.error("読込エラー:", err);
alert("読み込みに失敗しました。");
} else {
console.error("Load error", err);
alert("Failed to load.");
}
}
Expand Down
22 changes: 16 additions & 6 deletions src/routes/api/board/+server.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { json } from "@sveltejs/kit";
import { prisma } from "@/lib/prisma.server.ts";
import { createPreview } from "@/lib/board-preview.js";
import * as v from "valibot";

const BoardSchema = v.object({
Expand All @@ -8,24 +9,32 @@ const BoardSchema = v.object({
});

export async function POST({ request }) {
let body;
let requestData;
try {
body = v.parse(BoardSchema, await request.json());
requestData = await request.json();
} catch (error) {
console.error("Request validation failed:", error);
console.error("Request body JSON parsing failed:", error);
return json({ message: "無効なリクエスト形式です。" }, { status: 400 });
}

const result = v.safeParse(BoardSchema, requestData);
if (!result.success) {
console.error("Request validation failed:", result.issues);
return json({ message: "無効なリクエストデータです。" }, { status: 400 });
}

const { board, name } = body;
const { board, name } = result.output;
const preview = createPreview(board);

const newState = await prisma.boardState.create({
data: {
boardData: board,
boardName: name,
boardPreview: preview,
},
});

return json(newState, { status: 201 });
return json({ id: newState.id }, { status: 201 });
}

export async function GET({ url }) {
Expand Down Expand Up @@ -58,11 +67,12 @@ export async function GET({ url }) {
id: true,
boardName: true,
createdAt: true,
boardPreview: true,
},
});

if (!allStates || allStates.length === 0) {
return json({ message: "No state found" }, { status: 404 });
return json({ message: "保存されている盤面がありません。" }, { status: 404 });
}

return json(allStates);
Expand Down