Skip to content

Commit 690fac2

Browse files
authored
初期登録で、興味分野を登録できるようになった。 (#588)
# PRの概要 ## 具体的な変更内容 ## 影響範囲 ## 動作要件 ## 補足 ## レビューリクエストを出す前にチェック! - [ ] 改めてセルフレビューしたか - [ ] 手動での動作検証を行ったか - [ ] server の機能追加ならば、テストを書いたか - 理由: 書いた | server の機能追加ではない - [ ] 間違った使い方が存在するならば、それのドキュメントをコメントで書いたか - 理由: 書いた | 間違った使い方は存在しない - [ ] わかりやすいPRになっているか <!-- レビューリクエスト後は、Slackでもメンションしてお願いすることを推奨します。 -->
1 parent d1798b1 commit 690fac2

File tree

5 files changed

+253
-9
lines changed

5 files changed

+253
-9
lines changed

web/app/signup/common.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ export type Caller = "registration" | "configMenu";
22
export type StepProps<T> = {
33
onSave: (t: T) => void;
44
prev?: T;
5-
caller: Caller;
5+
caller?: Caller;
66
};
77

88
export type BackProp = {

web/app/signup/page.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,12 @@ import Step1 from "./steps/step1_profile";
1212
import Step2, { type Step2Data } from "./steps/step2_img";
1313
import Confirmation from "./steps/step3_confirmation";
1414
import Step4 from "./steps/step4_course";
15+
import Step5 from "./steps/step5_interests";
1516

1617
function Registration() {
1718
const { enqueueSnackbar } = useSnackbar();
1819
const router = useRouter();
19-
const [step, setStep] = useState<1 | 2 | 3 | 4>(1);
20+
const [step, setStep] = useState<1 | 2 | 3 | 4 | 5>(1);
2021

2122
const [step1Data, setStep1Data] = useState<Step1User>();
2223
const [step2Data, setStep2Data] = useState<Step2Data>();
@@ -71,15 +72,17 @@ function Registration() {
7172
/>
7273
);
7374
case 4:
74-
return <Step4 />;
75+
return <Step4 onSave={() => setStep(5)} />;
76+
case 5:
77+
return <Step5 back={() => setStep(4)} />;
7578
}
7679
}
7780
export default function RegistrationPage() {
7881
return (
7982
<NavigateByAuthState type="toHomeForAuthenticated">
8083
<div className="flex h-screen flex-col">
8184
<Header title="登録/Register" />
82-
<div className="mt-14 flex-1">
85+
<div className="flex-1">
8386
<Registration />
8487
</div>
8588
</div>

web/app/signup/steps/step1_profile.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ export default function Step1({ onSave, prev, caller }: StepProps<Step1User>) {
4343
}, [selectedFaculty, setValue, resetField]);
4444
return (
4545
<>
46-
<div className="m-4 mb-8 flex flex-col gap-4">
46+
<div className="m-4 flex h-full flex-col gap-4">
4747
<h1 className="text-xl">アカウント設定</h1>
4848
<div className="flex flex-col gap-2">
4949
<form onSubmit={handleSubmit(onSubmit)}>

web/app/signup/steps/step4_course.tsx

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
import Link from "next/link";
21
import { useMyID } from "~/api/user";
32
import FullScreenCircularProgress from "~/components/common/FullScreenCircularProgress";
43
import EditableCoursesTable from "~/components/course/EditableCoursesTable";
4+
import type { StepProps } from "../common";
55

6-
export default function Step4() {
6+
export default function Step4({ onSave }: StepProps<void>) {
77
const { state } = useMyID();
88
return (
99
<div className="flex h-full flex-col">
@@ -23,9 +23,13 @@ export default function Step4() {
2323
</div>
2424
<div className="flex w-full justify-between p-6">
2525
<span />
26-
<Link href="/tutorial" className="btn btn-primary">
26+
<button
27+
type="button"
28+
onClick={() => onSave()}
29+
className="btn btn-primary"
30+
>
2731
次へ
28-
</Link>
32+
</button>
2933
</div>
3034
</div>
3135
);
Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,237 @@
1+
"use client";
2+
3+
import type { InterestSubject } from "common/types";
4+
import { useRouter } from "next/navigation";
5+
import { enqueueSnackbar } from "notistack";
6+
import { useEffect, useState } from "react";
7+
import { MdAdd, MdClose } from "react-icons/md";
8+
import FullScreenCircularProgress from "~/components/common/FullScreenCircularProgress";
9+
import { useAlert } from "~/components/common/alert/AlertProvider";
10+
import * as subject from "../../../api/subject";
11+
import type { BackProp } from "../common";
12+
13+
export default function Step5({ back }: BackProp) {
14+
const { state } = subject.useMyInterests();
15+
const data = state.data;
16+
const error = state.current === "error" ? state.error : null;
17+
const loading = state.current === "loading";
18+
19+
const router = useRouter();
20+
const { showAlert } = useAlert();
21+
22+
const [allSubjects, setAllSubjects] = useState<InterestSubject[]>([]);
23+
const [filteredSubjects, setFilteredSubjects] = useState<InterestSubject[]>(
24+
[],
25+
);
26+
const [draftSubjects, setDraftSubjects] = useState<InterestSubject[]>(
27+
data ?? [],
28+
);
29+
const [isOpen, setIsOpen] = useState(false);
30+
const [newSubjectName, setNewSubjectName] = useState("");
31+
32+
useEffect(() => {
33+
getSubjects();
34+
}, []);
35+
36+
useEffect(() => {
37+
setDraftSubjects(data ?? []);
38+
}, [data]);
39+
40+
async function getSubjects() {
41+
const subjects = await subject.getAll();
42+
setAllSubjects(subjects);
43+
setFilteredSubjects(subjects);
44+
}
45+
46+
async function updateInterests(data: {
47+
interestSubjects: InterestSubject[];
48+
}) {
49+
const ids = data.interestSubjects.map((d) => d.id);
50+
const result = await subject.update(ids);
51+
if (!result.ok) {
52+
enqueueSnackbar("興味分野の保存に失敗しました", { variant: "error" });
53+
} else {
54+
enqueueSnackbar("興味分野を保存しました", { variant: "success" });
55+
}
56+
}
57+
58+
async function createSubject(name: string) {
59+
const result = await subject.create(name);
60+
if (!result.ok) {
61+
enqueueSnackbar("興味分野の作成に失敗しました", { variant: "error" });
62+
} else {
63+
enqueueSnackbar("興味分野を作成しました", { variant: "success" });
64+
}
65+
}
66+
67+
function handleBack() {
68+
// TODO: 差分がないときは確認なしで戻る
69+
showAlert({
70+
AlertMessage: "変更がある場合は、破棄されます。",
71+
subAlertMessage: "本当にページを移動しますか?",
72+
yesMessage: "移動",
73+
clickYes: () => {
74+
back();
75+
},
76+
});
77+
}
78+
79+
return loading ? (
80+
<FullScreenCircularProgress />
81+
) : error ? (
82+
<p>Error: {error.message}</p>
83+
) : !data ? (
84+
<p>データがありません。</p>
85+
) : (
86+
<>
87+
<div className="h-full overflow-y-scroll">
88+
<div className="mx-auto flex h-full max-w-lg flex-col px-4">
89+
<div className="flex-1">
90+
<div className="flex flex-wrap gap-2 p-2">
91+
{draftSubjects.map((subject, index) => (
92+
<span
93+
key={subject.id}
94+
className="rounded-md bg-[#F7FCFF] px-2 py-1 text-md text-primary"
95+
>
96+
#{subject.name}
97+
<button
98+
type="button"
99+
className="btn btn-circle btn-xs ml-1"
100+
onClick={() =>
101+
setDraftSubjects((prev) => {
102+
const copy = [...prev];
103+
copy.splice(index, 1);
104+
return copy;
105+
})
106+
}
107+
>
108+
<MdClose className="text-xs" />
109+
</button>
110+
</span>
111+
))}
112+
</div>
113+
<div className="mt-2 w-full">
114+
<input
115+
type="text"
116+
onChange={(e) => {
117+
const newFilteredSubjects = allSubjects.filter((subject) =>
118+
subject.name.includes(e.target.value.trim()),
119+
);
120+
setFilteredSubjects(newFilteredSubjects);
121+
}}
122+
placeholder="興味分野タグを絞り込み"
123+
className="input input-bordered w-full"
124+
/>
125+
</div>
126+
<ul className="mt-2">
127+
{filteredSubjects.length !== 0 ? (
128+
filteredSubjects
129+
.filter(
130+
(subject) =>
131+
!draftSubjects.some((draft) => draft.id === subject.id),
132+
)
133+
.map((subject) => (
134+
<li key={subject.id}>
135+
<button
136+
type="button"
137+
className="btn btn-ghost inline-flex h-full w-full justify-start p-2"
138+
onClick={() =>
139+
setDraftSubjects((prev) => [...prev, subject])
140+
}
141+
>
142+
<span className="font-normal text-lg">
143+
#{subject.name}
144+
</span>
145+
</button>
146+
</li>
147+
))
148+
) : (
149+
<li key="empty" className="p-2 text-gray-500">
150+
検索結果がありません
151+
</li>
152+
)}
153+
<li className="flex w-full items-center justify-center py-2">
154+
<button
155+
type="button"
156+
className="btn btn-secondary px-6 font-normal"
157+
onClick={() => setIsOpen(true)}
158+
>
159+
<MdAdd />
160+
タグを新規作成
161+
</button>
162+
</li>
163+
</ul>
164+
</div>
165+
<div className="my-2 flex justify-between">
166+
<button type="button" className="btn " onClick={handleBack}>
167+
前へ
168+
</button>
169+
<button
170+
type="button"
171+
className="btn btn-primary"
172+
onClick={() => {
173+
updateInterests({ interestSubjects: draftSubjects });
174+
router.push("/tutorial");
175+
}}
176+
>
177+
次へ
178+
</button>
179+
</div>
180+
</div>
181+
</div>
182+
{isOpen && (
183+
<dialog
184+
id="add-dialog"
185+
className="modal modal-open"
186+
onClose={() => setIsOpen(false)}
187+
>
188+
<div className="modal-box">
189+
<h3 className="mb-4 font-bold text-lg">興味分野タグの作成</h3>
190+
<input
191+
type="text"
192+
className="input input-bordered my-2 w-full"
193+
value={newSubjectName}
194+
onChange={(e) => setNewSubjectName(e.target.value)}
195+
placeholder="タグ名を入力"
196+
/>
197+
{newSubjectName && (
198+
<p className="py-4">
199+
興味分野タグ{" "}
200+
<span className="text-primary">#{newSubjectName}</span>{" "}
201+
を作成します
202+
</p>
203+
)}
204+
<div className="modal-action">
205+
<form method="dialog">
206+
<div className="flex gap-3">
207+
<button
208+
type="button"
209+
className="btn"
210+
onClick={() => {
211+
setIsOpen(false);
212+
setNewSubjectName("");
213+
}}
214+
>
215+
キャンセル
216+
</button>
217+
<button
218+
type="button"
219+
className="btn btn-primary"
220+
onClick={async () => {
221+
await createSubject(newSubjectName);
222+
setIsOpen(false);
223+
getSubjects();
224+
setNewSubjectName("");
225+
}}
226+
>
227+
作成
228+
</button>
229+
</div>
230+
</form>
231+
</div>
232+
</div>
233+
</dialog>
234+
)}
235+
</>
236+
);
237+
}

0 commit comments

Comments
 (0)