Skip to content

Commit e7b5294

Browse files
committed
refac: separate toast from routes/**/page.svelte
1 parent 8e0c10f commit e7b5294

File tree

4 files changed

+81
-44
lines changed

4 files changed

+81
-44
lines changed

.helix/languages.toml

+2-2
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,9 @@ language-servers = [
2323

2424
[[language]]
2525
name = "svelte"
26-
formatter = { command = "prettier", args = ["--parser=svelte"] }
26+
formatter = { command = "bun", args = ["prettier", "--parser", "svelte"] }
2727
language-servers = ["tailwindcss-ls", "svelteserver"]
2828

2929
[[language]]
3030
name = "typescript"
31-
formatter = { command = "prettier", args = ["--parser=typescript"] }
31+
formatter = { command = "bun", args = ["prettier", "--parser", "typescript"] }
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
export type Toast = {
2+
message: string;
3+
kind: "success";
4+
timeout: number; // milliseconds
5+
};
6+
type InternalToast = Toast & {
7+
id: string;
8+
class: string;
9+
};
10+
11+
export class ToastController {
12+
toasts: InternalToast[] = $state([]);
13+
14+
push(toast: Toast) {
15+
const id = Math.random().toString();
16+
let class_: string;
17+
switch (toast.kind) {
18+
case "success":
19+
class_ = "alert-success";
20+
break;
21+
default:
22+
class_ = toast.kind satisfies never;
23+
}
24+
25+
this.toasts.push({
26+
...toast,
27+
id,
28+
class: class_,
29+
});
30+
31+
$effect(() => {
32+
const timeoutId = setTimeout(() => {
33+
this.toasts = this.toasts.filter((toast) => toast.id !== id);
34+
}, toast.timeout);
35+
return () => clearTimeout(timeoutId);
36+
});
37+
}
38+
}

web/src/providers/toast/toast.svelte

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<script lang="ts">
2+
import { fly } from "svelte/transition";
3+
import type { ToastController } from "./toast-control.svelte";
4+
5+
type Props = { toasts: ToastController };
6+
const { toasts }: Props = $props();
7+
</script>
8+
9+
<div class="mt-3 ml-3 toast-start toast-top absolute">
10+
{#each toasts.toasts as toast}
11+
<div class="alert {toast.class}" transition:fly>
12+
<span>{toast.message}</span>
13+
</div>
14+
{/each}
15+
</div>

web/src/routes/[projectId]/config/+page.svelte

+26-42
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
import { replaceState } from "$app/navigation";
33
import { page } from "$app/state";
44
import { onMount } from "svelte";
5-
import { fly } from "svelte/transition";
65
import { type Client, createClient } from "~/api/client";
76
import { generateURL } from "~/api/origins.svelte.ts";
87
import Header from "~/components/header.svelte";
@@ -11,67 +10,50 @@
1110
import MdiStopCircle from "virtual:icons/mdi/stop-circle";
1211
import MdiGraph from "virtual:icons/mdi/graph";
1312
13+
import Toast from "~/providers/toast/toast.svelte";
14+
import { ToastController } from "~/providers/toast/toast-control.svelte.ts";
15+
1416
const client: Client = createClient({ fetch });
1517
const { data } = $props();
18+
const toasts = new ToastController();
1619
17-
const newlyCreated = page.url.searchParams.get("created") !== null;
18-
const justClosed = page.url.searchParams.get("closed") !== null;
19-
let createdToastShown = $state(false);
20-
let closedToastShown = $state(false);
2120
let closeModalShown = $state(false);
2221
let removeModalShown = $state(false);
2322
2423
onMount(() => {
24+
const newlyCreated = page.url.searchParams.get("created") !== null;
25+
const justClosed = page.url.searchParams.get("closed") !== null;
2526
if (newlyCreated) {
26-
createdToastShown = true;
27-
// replace ?created with none s.t. it won't show "created!" after reload
28-
const next = new URL(window.location.href);
29-
next.search = "";
30-
setTimeout(() => {
31-
replaceState(next, {});
27+
toasts.push({
28+
kind: "success",
29+
message: "プロジェクトを作成しました。",
30+
timeout: 2000,
3231
});
33-
setTimeout(() => {
34-
createdToastShown = false;
35-
}, 2000);
3632
}
3733
if (justClosed) {
38-
closedToastShown = true;
39-
// replace ?closed with none s.t. it won't show "closed!" after reload
40-
const next = new URL(window.location.href);
41-
next.search = "";
42-
setTimeout(() => {
43-
replaceState(next, {});
34+
toasts.push({
35+
kind: "success",
36+
message: "提出を締め切りました。",
37+
timeout: 2000,
4438
});
45-
setTimeout(() => {
46-
closedToastShown = false;
47-
}, 2000);
4839
}
40+
// replace ?created and ?closed with none s.t. it won't show "closed!" after reload
41+
const next = new URL(window.location.href);
42+
next.search = "";
43+
setTimeout(() => {
44+
replaceState(next, {});
45+
});
4946
});
5047
5148
const link = generateURL({
5249
pathname: `${data.projectId}/submit`,
5350
}).href;
5451
55-
let copyTimeout = $state(0);
56-
onMount(() => {
57-
const id = setInterval(() => (copyTimeout > 0 ? copyTimeout-- : null), 100);
58-
return () => clearTimeout(id);
59-
});
52+
let copied = $state(false);
6053
</script>
6154

6255
<Header title="管理" />
63-
<div class="mt-3 ml-3 toast-start toast-top absolute">
64-
{#if createdToastShown}
65-
<div class="alert alert-success z-31" transition:fly>
66-
<span class="z-31">プロジェクトを作成しました。</span>
67-
</div>
68-
{/if}
69-
{#if closedToastShown}
70-
<div class="alert alert-success z-31" transition:fly>
71-
<span class="z-31">提出を締め切りました。</span>
72-
</div>
73-
{/if}
74-
</div>
56+
<Toast {toasts} />
7557

7658
<main class="hm-blocks-container">
7759
<div class="hm-block">
@@ -93,12 +75,14 @@
9375
<label class="input input-bordered w-full">
9476
<img alt="" src={chain} class="h-[1rem] opacity-50 select-none" />
9577
<input type="url" class="x-selectable" value={link} readonly />
96-
{#if copyTimeout === 0}
78+
{#if !copied}
9779
<button
9880
class="btn btn-sm btn-soft btn-primary"
9981
onclick={async () => {
10082
await navigator.clipboard.writeText(link);
101-
copyTimeout = 20;
83+
setTimeout(() => {
84+
copied = false;
85+
}, 2000);
10286
}}
10387
>
10488
copy

0 commit comments

Comments
 (0)