Skip to content

Commit 19bf5da

Browse files
Merge pull request #44 from ut-code/filters
セメスターと評価方法のフィルターのUIを作りました
2 parents d847849 + 165d2d0 commit 19bf5da

File tree

8 files changed

+392
-0
lines changed

8 files changed

+392
-0
lines changed
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
/*
2+
* 評価方法フィルターのコンポーネント
3+
*/
4+
5+
"use client";
6+
import React, { ReactNode } from "react";
7+
import { Evaluation } from "@/app/type";
8+
import Checkbox from "../UI/Checkbox";
9+
10+
const evaluations: Evaluation[] = ["試験", "レポート", "出席", "平常"];
11+
12+
/**
13+
* 評価方法フィルターのプロパティ
14+
*/
15+
interface EvaluationProp {
16+
evaluation_included?: Evaluation[];
17+
evaluation_excluded?: Evaluation[];
18+
setEvaluation: (
19+
evaluation_included: Evaluation[],
20+
evaluation_excluded: Evaluation[],
21+
) => void;
22+
}
23+
24+
/**
25+
* 評価方法フィルターのコンポーネント
26+
* @param prop 評価方法フィルターのプロパティ
27+
* @returns 評価方法フィルターのコンポーネント
28+
*/
29+
export const EvaluationFilter: React.FC<EvaluationProp> = (
30+
prop: EvaluationProp,
31+
) => {
32+
const slots: ReactNode[] = [];
33+
34+
slots.push(<div key={"void"} />);
35+
slots.push(<div key={"label_in"}>含む</div>);
36+
slots.push(<div key={"label_ex"}>除外</div>);
37+
38+
evaluations.map((ev) => {
39+
slots.push(<div key={ev + "header"}>{ev.substring(0, 2)}</div>);
40+
slots.push(
41+
<CheckboxInGrid
42+
isInclude={true}
43+
ev={ev}
44+
key={ev + "included"}
45+
prop={prop}
46+
/>,
47+
);
48+
slots.push(
49+
<CheckboxInGrid
50+
isInclude={false}
51+
ev={ev}
52+
key={ev + "excluded"}
53+
prop={prop}
54+
/>,
55+
);
56+
});
57+
58+
return <div className="grid grid-rows-3 grid-flow-col gap-2">{slots}</div>;
59+
};
60+
61+
/**
62+
* グリッド上に並べるチェックボックスのコンポーネント
63+
* @param param0 プロパティ
64+
* @param param0.isInclude このチェックボックスが、含めたい評価方法を示しているか否か
65+
* @param param0.ev 評価方法
66+
* @param param0.prop 評価方法のフィルターコンポーネントのプロパティ
67+
* @returns チェックボックスコンポーネント
68+
*/
69+
const CheckboxInGrid: React.FC<{
70+
isInclude: boolean;
71+
ev: Evaluation;
72+
prop: EvaluationProp;
73+
}> = ({ isInclude, ev, prop }) => {
74+
let evaluation_included = prop.evaluation_included ?? [];
75+
let evaluation_excluded = prop.evaluation_excluded ?? [];
76+
let myEvaluation = isInclude ? evaluation_included : evaluation_excluded;
77+
let otherEvaluation = isInclude ? evaluation_excluded : evaluation_included;
78+
79+
// クリックされたときの挙動
80+
const onClick = (ev: Evaluation) => {
81+
if (myEvaluation.includes(ev)) {
82+
// もともとチェックされていたボックスをクリックしたら、外す
83+
myEvaluation.splice(myEvaluation.indexOf(ev), 1);
84+
} else {
85+
// もともとチェックされていなかったボックスをクリックしたら
86+
const index = otherEvaluation.indexOf(ev); // 相方のチェックがされているかを確認
87+
if (index >= 0) otherEvaluation.splice(index, 1); // 相方のチェックを外す
88+
myEvaluation.push(ev); // 自分のボックスにチェックを入れる
89+
}
90+
91+
prop.setEvaluation(evaluation_included, evaluation_excluded);
92+
};
93+
94+
return (
95+
<Checkbox
96+
checked={myEvaluation.includes(ev)}
97+
onChange={(_) => onClick(ev)}
98+
/>
99+
);
100+
};
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
/*
2+
* セメスターフィルターのコンポーネント
3+
*/
4+
5+
"use client";
6+
import React from "react";
7+
import { Semester } from "@/app/type";
8+
import { FlagButton } from "../UI/FlagButton";
9+
10+
/**
11+
* セメスターフィルターのプロパティ
12+
*/
13+
interface SemesterProp {
14+
selectedSemesters?: Semester[]; // 選択されているセメスター
15+
setSelectedSemesters: (semesters: Semester[]) => void;
16+
}
17+
18+
/**
19+
* セメスターフィルターのコンポーネント
20+
* @param prop セメスターフィルターのプロパティ
21+
* @returns コンポーネント
22+
*/
23+
export const SemestersCheckbox: React.FC<SemesterProp> = (
24+
prop: SemesterProp,
25+
) => {
26+
const selectedSemesters = prop.selectedSemesters ?? [];
27+
28+
// ボタンがクリックされたときの関数
29+
const onClick = (semester: Semester) => {
30+
if (selectedSemesters.includes(semester)) {
31+
prop.setSelectedSemesters(
32+
selectedSemesters.filter((s) => s !== semester),
33+
);
34+
} else {
35+
prop.setSelectedSemesters([...selectedSemesters, semester]);
36+
}
37+
};
38+
39+
return (
40+
<div className="grid grid-cols-4 gap-2">
41+
<FlagButton
42+
label={"S1"}
43+
isSelected={selectedSemesters.includes("S1")}
44+
onClick={() => onClick("S1")}
45+
className="aspect-square"
46+
/>
47+
<FlagButton
48+
label={"S2"}
49+
isSelected={selectedSemesters.includes("S2")}
50+
onClick={() => onClick("S2")}
51+
className="aspect-square"
52+
/>
53+
<FlagButton
54+
label={"A1"}
55+
isSelected={selectedSemesters.includes("A1")}
56+
onClick={() => onClick("A1")}
57+
className="aspect-square"
58+
/>
59+
<FlagButton
60+
label={"A2"}
61+
isSelected={selectedSemesters.includes("A2")}
62+
onClick={() => onClick("A2")}
63+
className="aspect-square"
64+
/>
65+
<FlagButton
66+
label={"S"}
67+
isSelected={selectedSemesters.includes("S")}
68+
onClick={() => onClick("S")}
69+
className="col-span-2"
70+
/>
71+
<FlagButton
72+
label={"A"}
73+
isSelected={selectedSemesters.includes("A")}
74+
onClick={() => onClick("A")}
75+
className="col-span-2"
76+
/>
77+
</div>
78+
);
79+
};
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
/*
2+
* 全てのフィルターを表示するコンポーネント
3+
*/
4+
5+
"use client";
6+
import { useState } from "react";
7+
import { ClassType, Evaluation, Semester } from "@/app/type";
8+
import { SemestersCheckbox } from "./FilterComponents/Semester";
9+
import { FilterCard } from "./UI/FilterCard";
10+
import { EvaluationFilter } from "./FilterComponents/Evaluation";
11+
12+
/**
13+
* フィルタの型定義
14+
*/
15+
type Filter = {
16+
isFreewordForSyllabusDetail?: boolean; // フリーワード検索
17+
semesters?: Semester[]; // セメスター
18+
evaluation_included?: Evaluation[]; // 含めたい評価方法
19+
evaluation_excluded?: Evaluation[]; // 除外したい評価方法
20+
classTypes?: ClassType[]; // 種別
21+
showRegistered?: boolean; // 履修登録済みの授業を表示する
22+
showNotRegistered?: boolean; // 未履修の授業を表示する
23+
};
24+
25+
/**
26+
* フィルタUI
27+
* @returns フィルタUI
28+
*/
29+
export const FilterUI: React.FC = () => {
30+
// 現在のフィルター
31+
const [filter, setFilter] = useState<Filter>({});
32+
33+
return (
34+
<div className="flex gap-8 flex-wrap">
35+
<FilterCard title={"セメスター"}>
36+
<SemestersCheckbox
37+
selectedSemesters={filter.semesters}
38+
setSelectedSemesters={(semesters: Semester[]) =>
39+
setFilter({ ...filter, semesters })
40+
}
41+
/>
42+
</FilterCard>
43+
44+
<FilterCard title={"評価方法"}>
45+
<EvaluationFilter
46+
evaluation_included={filter.evaluation_included}
47+
evaluation_excluded={filter.evaluation_excluded}
48+
setEvaluation={(
49+
evaluation_included: Evaluation[],
50+
evaluation_excluded,
51+
) =>
52+
setFilter({ ...filter, evaluation_included, evaluation_excluded })
53+
}
54+
/>
55+
</FilterCard>
56+
</div>
57+
);
58+
};
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
/*
2+
* FilterUIのサンプルページ
3+
*/
4+
5+
import { FilterUI } from "../FilterUI";
6+
7+
const FilterUISample: React.FC = () => {
8+
return <FilterUI />;
9+
};
10+
11+
export default FilterUISample;
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
"use client";
2+
import React from "react";
3+
4+
type CheckboxProps = {
5+
checked: boolean; // チェック状態
6+
onChange: (checked: boolean) => void; // チェック状態が変化したときのコールバック
7+
className?: string;
8+
};
9+
10+
/**
11+
* チェックボックスコンポーネント
12+
* @param param0 チェックボックスのプロパティ
13+
* @param param0.checked チェック状態
14+
* @param param0.onChange チェック状態が変化したときのコールバック
15+
* @param param0.className tailwindcss
16+
* @returns チェックボックスコンポーネント
17+
*/
18+
const Checkbox: React.FC<CheckboxProps> = ({
19+
checked,
20+
onChange,
21+
className,
22+
}) => {
23+
return (
24+
<input
25+
type="checkbox"
26+
checked={checked}
27+
onChange={() => onChange(!checked)}
28+
className={"accent-primary w-6 h-6 " + className}
29+
/>
30+
);
31+
};
32+
33+
export default Checkbox;
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/*
2+
* フィルターの内容とタイトルをセットにするコンポーネント
3+
*/
4+
5+
import React, { ReactNode } from "react";
6+
7+
/**
8+
* フィルターカードのプロパティ
9+
*/
10+
export interface FilterCardProps {
11+
/** タイトル */
12+
title: string;
13+
14+
/** フィルター */
15+
children: ReactNode;
16+
}
17+
18+
/**
19+
* フィルターの内容とタイトルをセットにするコンポーネント
20+
* @param param0 プロパティ
21+
* @param param0.title フィルターのタイトル
22+
* @param param0.children フィルターコンポーネント
23+
* @returns コンポーネント
24+
*/
25+
export const FilterCard: React.FC<FilterCardProps> = ({ title, children }) => {
26+
return (
27+
<div className="gap-8 flex-wrap">
28+
<div className="text-2xl m-4">{title}</div>
29+
{children}
30+
</div>
31+
);
32+
};
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
/*
2+
* 選択状態を示すボタン。
3+
* (ボタンの背景色を変更することで選択状態を示す。)
4+
* セメスター選択ボタンや、種別選択ボタンで使用。
5+
*/
6+
7+
import React from "react";
8+
9+
/**
10+
* 選択状態を示すボタンのプロパティ
11+
*/
12+
interface FlagButtonProp {
13+
/**
14+
* ボタンのラベル
15+
*/
16+
label: string;
17+
/**
18+
* 選択状態かどうか
19+
*/
20+
isSelected: boolean;
21+
/**
22+
* ボタンがクリックされたときの処理
23+
*/
24+
onClick: () => void;
25+
26+
/**
27+
* ボタンのスタイル
28+
*/
29+
className?: string;
30+
}
31+
32+
/**
33+
* 選択状態を示すボタンのコンポーネント
34+
* @param prop 選択状態を示すボタンのプロパティ
35+
* @returns 選択状態を示すボタンのコンポーネント
36+
*/
37+
export const FlagButton: React.FC<FlagButtonProp> = (prop: FlagButtonProp) => {
38+
const className = prop.className ?? "";
39+
return (
40+
<button
41+
className={
42+
`${
43+
prop.isSelected ? "bg-primary/30" : "bg-surface"
44+
} text-text-default px-4 py-2 rounded-full outline-2 outline-primary/30 outline` +
45+
" " +
46+
className
47+
}
48+
onClick={prop.onClick}
49+
>
50+
{prop.label}
51+
</button>
52+
);
53+
};

0 commit comments

Comments
 (0)