Skip to content

Commit 43ab37d

Browse files
committed
ui: select rolesCount with slider, schema: rolesCount is not nullable
1 parent eb5bf52 commit 43ab37d

File tree

6 files changed

+106
-87
lines changed

6 files changed

+106
-87
lines changed

service/routes/projects/index.ts

-4
Original file line numberDiff line numberDiff line change
@@ -255,11 +255,7 @@ const route = new Hono<HonoOptions>()
255255
capacity: role.max, // multiple mode の場合、max と min は同一
256256
}));
257257

258-
console.log(participantInput);
259-
console.log(roleInput);
260-
261258
const matching = multipleMatch(participantInput, roleInput);
262-
console.log(matching);
263259

264260
const result: {
265261
id: string;

service/routes/projects/preferences.ts

+17-3
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
import { and, eq } from "drizzle-orm";
1+
import { and, count, eq } from "drizzle-orm";
22
import { Hono } from "hono";
33
import { HTTPException } from "hono/http-exception";
44
import { db } from "service/db/client";
5-
import { participants, ratings } from "service/db/schema.ts";
5+
import { participants, ratings, roles } from "service/db/schema.ts";
66
import { getBrowserID } from "service/features/auth/index.ts";
77
import type { HonoOptions } from "service/types";
88
import { json, param } from "service/validator/hono.ts";
@@ -79,12 +79,26 @@ const route = new Hono<HonoOptions>()
7979
const browser_id = await getBrowserID(c);
8080
const { projectId } = c.req.valid("param");
8181
const body = c.req.valid("json");
82+
83+
const d = db(c);
84+
85+
// this will always be .length = 1
86+
const project = await d
87+
.select({ rolesLen: count() })
88+
.from(roles)
89+
.where(eq(roles.project_id, projectId));
90+
if (body.rolesCount > (project[0]?.rolesLen ?? 0)) {
91+
throw new HTTPException(409, {
92+
message: "you sent more count than there is role",
93+
});
94+
}
95+
8296
const participant = (
8397
await db(c)
8498
.update(participants)
8599
.set({
86100
name: body.participantName,
87-
roles_count: body.rolesCount || null,
101+
roles_count: body.rolesCount,
88102
})
89103
.where(and(eq(participants.browser_id, browser_id)))
90104
.returning({ id: participants.id })

share/schema.ts

+2-3
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import {
22
array,
33
minLength,
44
minValue,
5-
nullable,
65
number,
76
object,
87
pipe,
@@ -22,9 +21,9 @@ export const ProjectSchema = object({
2221
});
2322

2423
export const PreferenceSchema = object({
25-
browserId: nullable(string()), // TODO: non-null でよいのでは
24+
// browserId: string() -> validation の挟まるレイヤーでは存在しない、cookie からもってくるため
2625
participantName: string(),
27-
rolesCount: nullable(number()),
26+
rolesCount: number(),
2827
ratings: array(
2928
object({
3029
roleId: string(),

web/src/providers/toast/toast-control.svelte.ts

-1
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@ export class ToastController {
3434

3535
return new Promise((resolve) => {
3636
const timeout = toast.timeout ?? DEFAULT_TIMEOUT;
37-
console.log(timeout);
3837
setTimeout(() => {
3938
this.toasts = this.toasts.filter((toast) => toast.id !== id);
4039
resolve();

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

+3-15
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,5 @@
11
<script lang="ts">
2-
import { onDestroy } from "svelte";
3-
42
const { data } = $props();
5-
6-
let copyTimeout = 0;
7-
8-
const interval = setInterval(() => {
9-
if (copyTimeout > 0) {
10-
copyTimeout--;
11-
}
12-
}, 100);
13-
14-
onDestroy(() => clearInterval(interval));
153
</script>
164

175
<div>
@@ -29,7 +17,9 @@
2917
<h2 class="text-xl">{result.projectName}</h2>
3018
<p>{result.projectDesc}</p>
3119
</div>
32-
{#if matches.length}
20+
{#if !matches.length}
21+
役職のある人はいません。
22+
{:else}
3323
{#each matches as [_roleId, role]}
3424
<div class="hm-block">
3525
<h2 class="text-xl">{role.role_name}</h2>
@@ -38,8 +28,6 @@
3828
{/each}
3929
</div>
4030
{/each}
41-
{:else}
42-
役職のある人はいません。
4331
{/if}
4432
{/if}
4533
{/await}

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

+84-61
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
const { data }: PageProps = $props();
1111
const client = createClient({ fetch });
1212
13-
const project = data.project;
1413
// TODO: ローディング中の UI を追加
1514
let participantName = $state<string>(data.prev?.name ?? "");
1615
let rolesCount = $state<number>(data.prev?.roles_count ?? 1);
@@ -20,58 +19,66 @@
2019
return { role, score };
2120
}),
2221
);
23-
console.log(data.roles);
2422
2523
async function postPreference() {
2624
formState = "submitting";
27-
28-
const preference = safeParse(PreferenceSchema, {
29-
browserId: null,
30-
participantName: participantName,
31-
rolesCount: data.project.multiple_roles === 1 ? rolesCount : null,
32-
ratings: ratings.map((rating) => ({
33-
roleId: rating.role.id,
34-
score: rating.score,
35-
})),
36-
});
37-
// TODO: handle it better
38-
if (!preference.success) throw new Error("failed to validate preference");
39-
40-
if (data.prev) {
41-
// PUT
42-
const res = await client.projects[":projectId"].preferences.$put({
43-
json: preference.output,
44-
param: { projectId: project.id },
25+
try {
26+
const projectId = data.project.id;
27+
const preference = safeParse(PreferenceSchema, {
28+
participantName,
29+
rolesCount: data.project.multiple_roles === 1 ? rolesCount : null,
30+
ratings: ratings.map((rating) => ({
31+
roleId: rating.role.id,
32+
score: rating.score,
33+
})),
4534
});
46-
if (!res.ok)
47-
throw new Error(
48-
`Failed to submit: got ${res.status} with text ${await res.text()}`,
49-
);
50-
} else {
51-
// POST
52-
const res = await client.projects[":projectId"].preferences.$post({
53-
json: preference.output,
54-
param: { projectId: project.id },
55-
});
56-
if (!res.ok)
57-
throw new Error(
58-
`Failed to submit: got ${res.status} with text ${await res.json()}`,
59-
);
35+
// TODO: handle it better
36+
if (!preference.success) throw new Error("failed to validate preference");
37+
38+
if (data.prev) {
39+
// PUT
40+
const res = await client.projects[":projectId"].preferences.$put({
41+
json: preference.output,
42+
param: { projectId },
43+
});
44+
if (!res.ok)
45+
throw new Error(
46+
`Failed to submit: got ${res.status} with text ${await res.text()}`,
47+
);
48+
} else {
49+
// POST
50+
const res = await client.projects[":projectId"].preferences.$post({
51+
json: preference.output,
52+
param: { projectId },
53+
});
54+
if (!res.ok)
55+
throw new Error(
56+
`Failed to submit: got ${res.status} with text ${await res.json()}`,
57+
);
58+
}
59+
goto("/done");
60+
formState = "done";
61+
} catch (err) {
62+
console.error(err);
63+
formState = "error";
64+
setTimeout(() => {
65+
formState = "ready";
66+
}, 1000);
6067
}
61-
goto("/done");
62-
formState = "done";
6368
}
6469
6570
let formState = $state<"ready" | "submitting" | "error" | "done">("ready");
6671
const closed = $derived.by(() => {
6772
if (data.project.closed_at === null) return false;
6873
return new Date(data.project.closed_at).getTime() < Date.now();
6974
});
75+
const maxRoles = $derived(data.roles.length);
76+
7077
const formVerb = $derived(data.prev ? "更新" : "送信");
7178
7279
const resultLink = $derived(
7380
generateURL({
74-
pathname: `${project.id}/result`,
81+
pathname: `${data.project.id}/result`,
7582
}).href,
7683
);
7784
</script>
@@ -83,11 +90,12 @@
8390
<a class="btn btn-primary" href={resultLink}> 結果を見る </a>
8491
</div>
8592
{/if}
86-
{#if project === null}
93+
{#if data.project === null}
8794
<div class="hm-blocks-container">
8895
<p>プロジェクトが見つかりませんでした</p>
8996
</div>
9097
{:else}
98+
{@const p = data.project}
9199
<form
92100
method="POST"
93101
onsubmit={async (e) => {
@@ -97,9 +105,9 @@
97105
>
98106
<div class="hm-blocks-container">
99107
<div class="hm-block">
100-
<h2 class="text-xl">{project.name}</h2>
101-
{#if project.description}
102-
<p class="text-sm">{project.description}</p>
108+
<h2 class="text-xl">{p.name}</h2>
109+
{#if p.description}
110+
<p class="text-sm">{p.description}</p>
103111
{/if}
104112
</div>
105113
<div class="hm-block">
@@ -114,43 +122,58 @@
114122
disabled={closed}
115123
/>
116124
</div>
117-
{#if project.multiple_roles == 1}
125+
{#if p.multiple_roles == 1}
118126
<div class="hm-block">
119127
<h2 class="text-xl">配属される役職数の希望</h2>
120128
<input
121129
type="number"
122-
class="input bg-white text-base"
123-
placeholder="回答を入力"
130+
class="input validator bg-white text-base"
124131
bind:value={rolesCount}
132+
max={data.roles.length}
125133
disabled={closed}
126134
/>
135+
<div class="w-full max-w-xs">
136+
<input
137+
bind:value={rolesCount}
138+
class="range range-primary"
139+
type="range"
140+
min="1"
141+
max={maxRoles}
142+
step="1"
143+
/>
144+
<div class="flex justify-between px-2.5 mt-2 text-xs">
145+
{#each { length: maxRoles } as _}
146+
<span class="select-none">|</span>
147+
{/each}
148+
</div>
149+
<div class="flex justify-between px-2.5 mt-2 text-xs">
150+
{#each Array.from( { length: maxRoles }, ).map((_, i) => i + 1) as val}
151+
<span class="select-none">{val}</span>
152+
{/each}
153+
</div>
154+
</div>
127155
</div>
128156
{/if}
129157
<RolesSelector bind:ratings {closed} />
130158
<div class="flex justify-end">
131-
{#if closed}
132-
<button type="submit" class="btn btn-primary" disabled>
159+
<button
160+
type="submit"
161+
class="btn btn-primary"
162+
disabled={closed || formState !== "ready"}
163+
>
164+
{#if closed}
133165
既に締め切られています
134-
</button>
135-
{:else if formState === "ready"}
136-
<button type="submit" class="btn btn-primary">
166+
{:else if formState === "ready"}
137167
{formVerb}
138-
</button>
139-
{:else if formState === "submitting"}
140-
<button type="submit" class="btn btn-primary" disabled>
168+
{:else if formState === "submitting"}
141169
<span class="loading loading-spinner"></span>
142170
{formVerb}中...
143-
</button>
144-
{:else if formState === "error"}
145-
<button type="submit" class="btn btn-primary" disabled>
146-
<span class="loading loading-spinner"></span>
171+
{:else if formState === "error"}
147172
{formVerb}に失敗しました
148-
</button>
149-
{:else if formState === "done"}
150-
<button type="submit" class="btn btn-primary" disabled>
173+
{:else if formState === "done"}
151174
完了
152-
</button>
153-
{/if}
175+
{/if}
176+
</button>
154177
</div>
155178
</div>
156179
</form>

0 commit comments

Comments
 (0)