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
1 change: 0 additions & 1 deletion src/iframe/life-game.js
Original file line number Diff line number Diff line change
Expand Up @@ -281,7 +281,6 @@ on.save_board = async () => {
};

on.apply_board = (newBoard) => {
boardSize = newBoard.length;
board = newBoard;
renderBoard();
generationChange(0);
Expand Down
18 changes: 10 additions & 8 deletions src/lib/api/board.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { toast } from "$lib/models/ToastStore.svelte";

export async function saveBoard(data: { board: number[][]; name: string }, isJapanese: boolean) {
try {
const response = await fetch("/api/board", {
Expand All @@ -13,16 +15,16 @@ export async function saveBoard(data: { board: number[][]; name: string }, isJap
}

if (isJapanese) {
alert("盤面を保存しました!");
toast.show("盤面を保存しました!", "success");
} else {
alert("Board saved!");
toast.show("Board saved!", "success");
}
} catch (err) {
console.error("Save Error:", err);
if (isJapanese) {
alert("保存に失敗しました。");
toast.show("保存に失敗しました。", "error");
} else {
alert("Failed to save.");
toast.show("Failed to save.", "error");
}
}
}
Expand Down Expand Up @@ -52,9 +54,9 @@ export async function fetchBoardList(isJapanese: boolean): Promise<BoardListItem
} catch (err) {
console.error("Load error", err);
if (isJapanese) {
alert("読み込みに失敗しました。");
toast.show("読み込みに失敗しました。", "error");
} else {
alert("Failed to load.");
toast.show("Failed to load.", "error");
}
}
}
Expand All @@ -80,9 +82,9 @@ export async function loadBoardById(
} catch (err) {
console.error("Load error", err);
if (isJapanese) {
alert("読み込みに失敗しました。");
toast.show("読み込みに失敗しました。", "error");
} else {
alert("Failed to load.");
toast.show("Failed to load.", "error");
}
}
}
18 changes: 10 additions & 8 deletions src/lib/api/code.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { toast } from "$lib/models/ToastStore.svelte";

export async function saveCode(data: { code: string; name: string }, isJapanese: boolean) {
try {
const response = await fetch("/api/code", {
Expand All @@ -13,16 +15,16 @@ export async function saveCode(data: { code: string; name: string }, isJapanese:
}

if (isJapanese) {
alert("コードを保存しました!");
toast.show("コードを保存しました!", "success");
} else {
alert("Code saved!");
toast.show("Code saved!", "success");
}
} catch (err) {
console.error("Save Error:", err);
if (isJapanese) {
alert("保存に失敗しました。");
toast.show("保存に失敗しました。", "error");
} else {
alert("Failed to save.");
toast.show("Failed to save.", "error");
}
}
}
Expand Down Expand Up @@ -51,9 +53,9 @@ export async function fetchCodeList(isJapanese: boolean): Promise<CodeListItem[]
} catch (err) {
console.error("Load error", err);
if (isJapanese) {
alert("読み込みに失敗しました。");
toast.show("読み込みに失敗しました。", "error");
} else {
alert("Failed to load.");
toast.show("Failed to load.", "error");
}
}
}
Expand All @@ -76,9 +78,9 @@ export async function loadCodeById(id: number, isJapanese: boolean): Promise<str
} catch (err) {
console.error("Load error", err);
if (isJapanese) {
alert("読み込みに失敗しました。");
toast.show("読み込みに失敗しました。", "error");
} else {
alert("Failed to load.");
toast.show("Failed to load.", "error");
}
}
}
7 changes: 5 additions & 2 deletions src/lib/components/BoardModals.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@
>
<button
type="submit"
class="btn btn-primary"
class="btn btn-success text-black"
onclick={() => manager.save(isJapanese)}
disabled={!manager.saveState.saving}
>
Expand Down Expand Up @@ -103,7 +103,10 @@
<td>{item.name}</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={() => onSelect(item.id)}>
<button
class="btn btn-sm btn-success text-black"
onclick={() => onSelect(item.id)}
>
{isJapanese ? "ロード" : "Load"}
</button>
</td>
Expand Down
7 changes: 5 additions & 2 deletions src/lib/components/CodeModals.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
>
<button
type="submit"
class="btn btn-primary"
class="btn btn-success text-black"
onclick={() => manager.save(isJapanese)}
disabled={!manager.saveState.saving}
>
Expand Down Expand Up @@ -77,7 +77,10 @@
<td>{item.name}</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={() => onSelect(item.id)}>
<button
class="btn btn-sm btn-success text-black"
onclick={() => onSelect(item.id)}
>
{isJapanese ? "ロード" : "Load"}
</button>
</td>
Expand Down
24 changes: 24 additions & 0 deletions src/lib/components/GlobalToast.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<script lang="ts">
import { toast } from "$lib/models/ToastStore.svelte";

let alertClass: string = $derived.by(() => {
switch (toast.type) {
case "success":
return "alert-success";
case "error":
return "alert-error";
default:
return "alert-info";
}
});
</script>

{#if toast.visible}
<div class="toast toast-middle toast-center">
<div class={`alert ${alertClass} p-3 text-lg`}>
<div>
<span>{toast.message}</span>
</div>
</div>
</div>
{/if}
38 changes: 38 additions & 0 deletions src/lib/models/ToastStore.svelte.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
export class ToastStore {
message = $state("");
type = $state<"success" | "error" | "info">("info");
visible = $state(false);

private timerId: ReturnType<typeof setTimeout> | undefined = $state(undefined);

/**
* トーストを表示する
* @param message 表示するメッセージ
* @param type "success" | "error" | "info"
* @param duration 表示時間(ミリ秒)
*/
show(message: string, type: "success" | "error" | "info" = "info", duration: number = 2000) {
this.message = message;
this.type = type;
this.visible = true;

if (this.timerId) {
clearTimeout(this.timerId);
}

this.timerId = setTimeout(() => {
this.hide();
}, duration);
}

hide() {
this.visible = false;
if (this.timerId) {
clearTimeout(this.timerId);
this.timerId = undefined;
}
}
}

// グローバルで使うための単一インスタンスを作成してエクスポート
export const toast = new ToastStore();
3 changes: 2 additions & 1 deletion src/routes/+layout.svelte
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
<script lang="ts">
import favicon from "$lib/assets/favicon.svg";
import "../app.css";
import GlobalToast from "$lib/components/GlobalToast.svelte";
let { children } = $props();
</script>

<svelte:head>
<link rel="icon" href={favicon} />
</svelte:head>

{@render children?.()}
<GlobalToast />{@render children?.()}
69 changes: 35 additions & 34 deletions src/routes/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import { CodeManager } from "$lib/models/CodeManager.svelte";
import BoardModals from "$lib/components/BoardModals.svelte";
import CodeModals from "$lib/components/CodeModals.svelte";
import { toast } from "$lib/models/ToastStore.svelte";
import CodeMirror from "svelte-codemirror-editor";
import { javascript } from "@codemirror/lang-javascript";
import { oneDark } from "@codemirror/theme-one-dark";
Expand All @@ -32,6 +33,16 @@
const boardManager = new BoardManager();
const codeManager = new CodeManager();

let disabledTemplates: { [key: string]: boolean } = $derived.by(() => {
const newDisabledState: { [key: string]: boolean } = {};
for (const key in patterns) {
const patternName = key as keyof typeof patterns;
const patternData = patterns[patternName];
newDisabledState[patternName] = sizeValue < (patternData.minBoardSize || 0);
}
return newDisabledState;
});

let timer: "running" | "stopped" = $state("stopped");
let intervalMs = $state(1000);
$effect(() => {
Expand All @@ -45,7 +56,6 @@
type OngoingEvent =
| "play"
| "pause"
| "state_update"
| "board_reset"
| "board_randomize"
| "place_template"
Expand All @@ -69,10 +79,11 @@
break;
}
case "Size shortage": {
alert(
toast.show(
isJapanese
? "盤面からはみ出してしまうため、キャンセルしました"
: "This action was canceled because it would have extended beyond the board.",
"error",
);
break;
}
Expand Down Expand Up @@ -101,7 +112,6 @@
async function onBoardSelect(id: number) {
const board = await boardManager.load(id, isJapanese);
if (board) {
sizeValue = board.length;
sendEvent("apply_board", board);
}
}
Expand Down Expand Up @@ -169,31 +179,27 @@
<div class="bg-base-200 shadow-lg p-4 h-48 w-full overflow-x-auto">
<div class="flex gap-4">
{#each Object.keys(patterns) as (keyof typeof patterns)[] as patternName (patternName)}
<div class="text-center flex-shrink-0">
<div
class="text-center flex-shrink-0"
style:opacity={disabledTemplates[patternName] ? 0.3 : 1}
>
<p class="font-bold mb-2">
{isJapanese ? patterns[patternName].names.ja : patterns[patternName].names.en}
</p>
<button
class="btn overflow-hidden p-0 w-24 h-24"
onclick={() => {
sendEvent("request_sync");

const patternData = patterns[patternName];
const patternShape = patternData.shape;

if (sizeValue < (patternData.minBoardSize || 0)) {
if (isJapanese) {
alert(
`このパターンには ${patternData.minBoardSize}x${patternData.minBoardSize} 以上の盤面が必要です`,
);
} else {
alert(
`This pattern requires a board size of at least ${patternData.minBoardSize}x${patternData.minBoardSize}.`,
);
}

if (disabledTemplates[patternName]) {
toast.show(
isJapanese
? `このパターンには ${patterns[patternName].minBoardSize}x${patterns[patternName].minBoardSize} 以上の盤面が必要です`
: `This pattern requires a board size of at least ${patterns[patternName].minBoardSize}x${patterns[patternName].minBoardSize}.`,
"error",
);
return;
}
const patternData = patterns[patternName];
const patternShape = patternData.shape;
bottomDrawerOpen = false;
sendEvent("place_template", patternShape);
}}
Expand Down Expand Up @@ -253,7 +259,7 @@
class="w-[80%] h-[90%] rounded-lg mx-auto my-5 bg-white shadow-lg"
onload={() => {
setTimeout(() => {
sendEvent("state_update");
sendEvent("request_sync");
console.log("generationFigure onload:", generationFigure);
}, 50);
}}
Expand Down Expand Up @@ -283,7 +289,7 @@
<!-- Left Section -->
<div class="flex items-center">
<button
class="btn rounded-none h-12 justify-start"
class="btn rounded-none h-12 justify-start w-30"
onclick={() => (bottomDrawerOpen = !bottomDrawerOpen)}
>
{#if bottomDrawerOpen}
Expand All @@ -295,8 +301,8 @@
{/if}
</button>

<div class="font-bold text-black ml-4">
{isJapanese ? "" + generationFigure + "世代" : "Generation:" + generationFigure}
<div class="font-bold text-black ml-4 w-25">
{isJapanese ? "世代数:" + generationFigure : "Generation:" + generationFigure}
</div>
</div>

Expand Down Expand Up @@ -329,17 +335,13 @@
<img class="size-6" src={icons.accelerate} alt="accelerate" />
</button>

<div class="font-bold text-black ml-2">
<div class="font-bold text-black ml-2 w-25">
{isJapanese ? "現在の速度" : "Current speed"}: x{1000 / intervalMs}
</div>

<div class="w-px bg-gray-400 h-6 mx-4"></div>
<!-- Separator -->

<button class="btn btn-ghost btn-circle hover:bg-[rgb(220,220,220)]">
<img class="size-6" src={icons.LeftArrow} alt="Left Arrow" />
</button>

<button
class="btn btn-ghost btn-circle hover:bg-[rgb(220,220,220)] swap"
onclick={() => {
Expand All @@ -357,10 +359,6 @@
<img class="size-6 swap-on" src={icons.Pause} alt="Pause" />
<img class="size-6 swap-off" src={icons.Play} alt="Play" />
</button>

<button class="btn btn-ghost btn-circle hover:bg-[rgb(220,220,220)]">
<img class="size-6" src={icons.RightArrow} alt="Right Arrow" />
</button>
</div>

<!-- Right Section -->
Expand Down Expand Up @@ -418,7 +416,10 @@
<!-- Separator -->
<div class="font-bold text-black">{isJapanese ? "コード" : "Code"}:</div>
<button
class="btn btn-ghost hover:bg-[rgb(220,220,220)] text-black"
class={[
"btn text-black",
editingCode === appliedCode ? "btn-ghost hover:bg-[rgb(220,220,220)]" : "btn-success",
]}
onclick={() => {
appliedCode = editingCode;
isProgress = false;
Expand Down