From 7cd21c2fcc9e29666b869bdb70ab8504b148dd55 Mon Sep 17 00:00:00 2001 From: Yuki Kobayashi <137767097+aster-void@users.noreply.github.com> Date: Wed, 16 Apr 2025 00:27:15 +0900 Subject: [PATCH 1/3] update scraper (#684) --- .cspell.json | 5 +- .gitignore | 3 + biome.json | 1 + flake.lock | 18 ++--- flake.nix | 5 +- scraper/sample.ts | 14 ++++ scraper/src/io.rs | 14 +++- scraper/src/main.rs | 10 ++- scraper/src/parser.rs | 35 ++++---- scraper/src/urls.rs | 6 +- server/src/seeds/insertKoukiCourses.ts | 106 +++++++++++++++++++++++++ 11 files changed, 181 insertions(+), 36 deletions(-) create mode 100644 scraper/sample.ts create mode 100644 server/src/seeds/insertKoukiCourses.ts diff --git a/.cspell.json b/.cspell.json index f4a0a683..234c79c0 100644 --- a/.cspell.json +++ b/.cspell.json @@ -21,12 +21,14 @@ "pkgs", "psql", "qiita", + "replacen", "reqwest", "rustc", "safify", "stdenv", "supabase", - "swiper" + "swiper", + "zenki" ], "dictionaries": [ "softwareTerms", @@ -52,6 +54,7 @@ "**/*.svg", "**/migration.sql", "**/data.json", + "**/server/src/seeds/json/**", "**/Cargo.*", "scraper/target", "**/rust-toolchain.toml", diff --git a/.gitignore b/.gitignore index 0535f467..1a29dfcb 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,9 @@ /.direnv /.husky +.cache +data.json + # Logs logs *.log diff --git a/biome.json b/biome.json index 3ff85b17..52e82609 100644 --- a/biome.json +++ b/biome.json @@ -22,6 +22,7 @@ "bun.lockb", "server/target", "data.json", + "server/src/seeds/json", "scraper/target", ".next", "next-env.d.ts", diff --git a/flake.lock b/flake.lock index 8b9b19c0..4b3fd8ff 100644 --- a/flake.lock +++ b/flake.lock @@ -38,11 +38,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1739262174, - "narHash": "sha256-W0436s/nXInSuxfiXKtT2n1nUkNw8Ibddz7w4GAweJ4=", + "lastModified": 1743501102, + "narHash": "sha256-7PCBQ4aGVF8OrzMkzqtYSKyoQuU2jtpPi4lmABpe5X4=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "2a39e74c3ea50a164168215a47b86f72180db76c", + "rev": "02f2af8c8a8c3b2c05028936a1e84daefa1171d4", "type": "github" }, "original": { @@ -54,11 +54,11 @@ }, "nixpkgs-unstable": { "locked": { - "lastModified": 1739019272, - "narHash": "sha256-7Fu7oazPoYCbDzb9k8D/DdbKrC3aU1zlnc39Y8jy/s8=", + "lastModified": 1743472173, + "narHash": "sha256-xwNv3FYTC5pl4QVZ79gUxqCEvqKzcKdXycpH5UbYscw=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "fa35a3c8e17a3de613240fea68f876e5b4896aec", + "rev": "88e992074d86ad50249de12b7fb8dbaadf8dc0c5", "type": "github" }, "original": { @@ -106,11 +106,11 @@ ] }, "locked": { - "lastModified": 1739240901, - "narHash": "sha256-YDtl/9w71m5WcZvbEroYoWrjECDhzJZLZ8E68S3BYok=", + "lastModified": 1743475035, + "narHash": "sha256-uLjVsb4Rxnp1zmFdPCDmdODd4RY6ETOeRj0IkC0ij/4=", "owner": "oxalica", "repo": "rust-overlay", - "rev": "03473e2af8a4b490f4d2cdb2e4d3b75f82c8197c", + "rev": "bee11c51c2cda3ac57c9e0149d94b86cc1b00d13", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index 79e4ae47..836c2d26 100644 --- a/flake.nix +++ b/flake.nix @@ -32,7 +32,7 @@ }; unstable = nixpkgs-unstable.legacyPackages.${system}; - rust-bin = pkgs.rust-bin.fromRustupToolchainFile ./scraper/rust-toolchain.toml; + rust-bin = pkgs.rust-bin.beta.latest.default; # pkgs.rust-bin.fromRustupToolchainFile ./scraper/rust-toolchain.toml; prisma = pkgs.callPackage ./server/prisma.nix {inherit prisma-utils;}; common = { @@ -62,8 +62,7 @@ }; in { packages.scraper = pkgs.callPackage ./scraper {toolchain = rust-bin;}; - devShells.default = pkgs.mkShell common; - devShells.scraper = pkgs.mkShell { + devShells.default = pkgs.mkShell { inherit (common) env; packages = common.packages diff --git a/scraper/sample.ts b/scraper/sample.ts new file mode 100644 index 00000000..ea8b12ba --- /dev/null +++ b/scraper/sample.ts @@ -0,0 +1,14 @@ +[ + { + name: "zenki", + courses: [ + { + name: "数理科学基礎", + teacher: "(人名)", + semester: "S1", + period: "月曜2限、水曜1限", + code: "30003 CAS-FC1871L1", + }, + ], + }, +]; diff --git a/scraper/src/io.rs b/scraper/src/io.rs index b6b3cba2..14fb798e 100644 --- a/scraper/src/io.rs +++ b/scraper/src/io.rs @@ -1,6 +1,5 @@ use crate::types::*; use anyhow::ensure; -use sha2::{Digest, Sha256}; use tokio::fs; use tokio::io::AsyncWriteExt; @@ -10,13 +9,20 @@ pub async fn write_to(file: &mut fs::File, content: Entry) -> anyhow::Result<()> Ok(()) } -use crate::CACHE_DIR; +use crate::cache_dir; pub async fn request(url: &str) -> anyhow::Result { println!("[request] sending request to {}", url); - let hash = Sha256::digest(url.as_bytes()); - let path = format!("{CACHE_DIR}/{:x}", hash); + let cache_key = url + .to_string() + .replacen("/", "_", 1000) + .replacen(":", "_", 1000) + .replacen("?", "_", 1000) + .replacen("&", "_", 1000) + .replacen("=", "_", 1000) + .to_string(); + let path = format!("{}/{cache_key}", cache_dir()); if let Ok(bytes) = fs::read(&path).await { if let Ok(text) = String::from_utf8(bytes) { return Ok(text); diff --git a/scraper/src/main.rs b/scraper/src/main.rs index d3fd67b6..e7a5a76b 100644 --- a/scraper/src/main.rs +++ b/scraper/src/main.rs @@ -16,13 +16,16 @@ use scraper::{Html, Selector}; use urls::URLS; const RESULT_FILE: &str = "./data.json"; -const CACHE_DIR: &str = "./.cache"; + +fn cache_dir() -> String { + "./.cache".to_string() +} #[tokio::main(flavor = "multi_thread")] async fn main() { println!("[log] starting..."); - let _ = fs::DirBuilder::new().create(CACHE_DIR).await; + let _ = fs::DirBuilder::new().create(cache_dir()).await; let mut file = fs::File::create(RESULT_FILE) .await @@ -59,7 +62,8 @@ async fn get_courses_of(base_url: &str) -> Vec { futures::future::join_all(courses) .await .into_iter() - .collect::>() + .flatten() + .collect() } lazy_static! { diff --git a/scraper/src/parser.rs b/scraper/src/parser.rs index f3214ed2..fef9ef0c 100644 --- a/scraper/src/parser.rs +++ b/scraper/src/parser.rs @@ -1,6 +1,6 @@ use anyhow::anyhow; use lazy_static::lazy_static; -use scraper::{Html, Selector}; +use scraper::{ElementRef, Html, Selector}; use crate::types::*; @@ -17,19 +17,24 @@ lazy_static! { Selector::parse(".catalog-page-detail-table-cell.code-cell").unwrap(); } -pub fn parse_course_info(html: Html) -> anyhow::Result { - Ok(Course { - name: select(&html, &NAME_SELECTOR, 1)?, - teacher: select(&html, &TEACHER_SELECTOR, 1)?, - semester: select_all(&html, &SEMESTER_SELECTOR, 1)?.join(","), - period: select(&html, &PERIOD_SELECTOR, 1)?, - code: select_all(&html, &CODE_SELECTOR, 1)?.join(" "), - }) +pub fn parse_course_info(html: Html) -> anyhow::Result> { + html.select(&Selector::parse(".catalog-page-detail-table-row").unwrap()) + .skip(1) + .map(|el| { + Ok(Course { + name: select(&el, &NAME_SELECTOR)?, + teacher: select(&el, &TEACHER_SELECTOR)?, + semester: select_all(&el, &SEMESTER_SELECTOR)?.join(","), + period: select(&el, &PERIOD_SELECTOR)?, + code: select_all(&el, &CODE_SELECTOR)?.join(" "), + }) + }) + .collect() } -fn select(html: &Html, selector: &Selector, nth: usize) -> anyhow::Result { - html.select(selector) - .nth(nth) +fn select(el: &ElementRef, selector: &Selector) -> anyhow::Result { + el.select(selector) + .next() .ok_or(anyhow!( "Couldn't find matching element for selector {:?}", selector, @@ -38,12 +43,12 @@ fn select(html: &Html, selector: &Selector, nth: usize) -> anyhow::Result( - html: &'a Html, + html: &'a ElementRef, selector: &'static Selector, - nth: usize, + // nth: usize, ) -> anyhow::Result> { html.select(selector) - .nth(nth) + .next() .ok_or(anyhow!( "Couldn't find matching element for selector {:?}", selector, diff --git a/scraper/src/urls.rs b/scraper/src/urls.rs index aaa7f78c..565cbd50 100644 --- a/scraper/src/urls.rs +++ b/scraper/src/urls.rs @@ -1,4 +1,8 @@ -pub static URLS: [(&str, &str); 10] = [ +pub static URLS: [(&str, &str); 11] = [ + ( + "zenki", + "https://catalog.he.u-tokyo.ac.jp/result?q=&type=all&faculty_id=&facet=%7B%22faculty_type%22%3A%5B%22jd%22%5D%7D&page=", + ), ( "law", "https://catalog.he.u-tokyo.ac.jp/result?type=ug&faculty_id=1&page=", diff --git a/server/src/seeds/insertKoukiCourses.ts b/server/src/seeds/insertKoukiCourses.ts new file mode 100644 index 00000000..75e01937 --- /dev/null +++ b/server/src/seeds/insertKoukiCourses.ts @@ -0,0 +1,106 @@ +import * as fs from "node:fs"; +import * as path from "node:path"; + +import { prisma } from "../database/client"; + +// 後期 (scraper) 形式のデータを読み込む。 +const FILE_PATH = path.join(__dirname, "data.json"); + +// sample +// [ +// { +// name: "zenki", +// courses: [ +// { +// name: "数理科学基礎", +// teacher: "(人名)", +// semester: "S1,S2", +// period: "月曜2限、水曜1限", +// code: "30003 CAS-FC1871L1", +// }, +// ], +// }, +// ]; + +async function main() { + const jsonData: { + courses: { + name: string; + teacher: string; + semester: string; + period: string; + code: string; + }[]; + }[] = JSON.parse(fs.readFileSync(FILE_PATH, "utf-8")); + console.log(jsonData); + + const coursesData = jsonData[0].courses + .filter((course) => course.semester.split("")[0] === "S") + .map((course) => { + const { code, name, teacher } = course; + return { + id: code.split(" ")[0], + name: name, + teacher: teacher, + }; + }); + + await prisma.course.createMany({ + data: coursesData, + }); + + const slotsData: { + day: "mon" | "tue" | "wed" | "thu" | "fri" | "sat" | "sun" | "other"; + period: number; + courseId: string; + }[] = []; + + for (const courseData of jsonData[0].courses) { + const { code, period } = courseData; + + if (courseData.semester.split("")[0] !== "S") continue; + + for (const p of period.split("、")) { + const [dayJp, periodStr] = p.split("曜"); + const day = + dayJp === "月" + ? "mon" + : dayJp === "火" + ? "tue" + : dayJp === "水" + ? "wed" + : dayJp === "木" + ? "thu" + : dayJp === "金" + ? "fri" + : dayJp === "土" + ? "sat" + : dayJp === "日" + ? "sun" + : "other"; + + slotsData.push({ + day, + period: Number.parseInt(periodStr?.split("")[0]) || 0, + courseId: code.split(" ")[0], + }); + } + } + + await prisma.slot.createMany({ + data: slotsData, + skipDuplicates: true, + }); + + console.log("Data inserted successfully!"); +} + +main() + .then(async () => { + await prisma.$disconnect(); + }) + .catch(async (e) => { + console.error(e); + await prisma.$disconnect(); + process.exit(1); + }); From 2bcb544b04a7c655cd85ecd34b9200df59a9dcda Mon Sep 17 00:00:00 2001 From: Shogo Nakamura <104970808+naka-12@users.noreply.github.com> Date: Wed, 16 Apr 2025 01:00:39 +0900 Subject: [PATCH 2/3] =?UTF-8?q?=E6=A4=9C=E7=B4=A2=E3=81=A71=E6=96=87?= =?UTF-8?q?=E5=AD=97=E5=85=A5=E5=8A=9B=E3=81=97=E3=81=A6=E3=81=8B=E3=82=89?= =?UTF-8?q?=E3=83=A6=E3=83=BC=E3=82=B6=E3=82=92=E8=A1=A8=E7=A4=BA=20(#706)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/app/chat/components/RoomList.tsx | 161 +++++++++++------------ web/app/friends/page.tsx | 4 +- web/app/home/components/NoMoreUser.tsx | 9 -- web/app/home/page.tsx | 4 +- web/app/search/page.tsx | 18 ++- web/components/common/BackgroundText.tsx | 7 + web/components/match/matching.tsx | 9 +- web/components/search/search.tsx | 12 +- 8 files changed, 108 insertions(+), 116 deletions(-) delete mode 100644 web/app/home/components/NoMoreUser.tsx create mode 100644 web/components/common/BackgroundText.tsx diff --git a/web/app/chat/components/RoomList.tsx b/web/app/chat/components/RoomList.tsx index 03635e5f..a43ac412 100644 --- a/web/app/chat/components/RoomList.tsx +++ b/web/app/chat/components/RoomList.tsx @@ -3,6 +3,7 @@ import { Box, List, Typography } from "@mui/material"; import type { RoomOverview } from "common/types"; import { useRouter, useSearchParams } from "next/navigation"; +import BackgroundText from "~/components/common/BackgroundText"; import { HumanListItem } from "~/components/human/humanListItem"; import RoomPage from "./RoomPage"; @@ -23,93 +24,85 @@ export function RoomList(props: RoomListProps) { return ( <> {!friendId ? ( - -

- {roomsData && roomsData.length === 0 && ( - <> - 誰ともマッチングしていません。 -
- リクエストを送りましょう! - - )} -

- {roomsData?.map((room) => { - if (room.isDM) { - if (room.matchingStatus === "otherRequest") { - return ( - { - e.stopPropagation(); - openRoom(room); - }} - > - + {roomsData && roomsData.length === 0 ? ( + + ) : ( + + {roomsData?.map((room) => { + if (room.isDM) { + if (room.matchingStatus === "otherRequest") { + return ( + { + e.stopPropagation(); + openRoom(room); + }} + > + + + ); + } + if (room.matchingStatus === "myRequest") { + return ( + { + e.stopPropagation(); + openRoom(room); + }} + > + + + ); + } + return ( + - - ); - } - if (room.matchingStatus === "myRequest") { + onClick={() => { + openRoom(room); + }} + > + + + ); + } return ( - { - e.stopPropagation(); - openRoom(room); - }} - > - - + + グループチャット: {room.name} + ); - } - return ( - { - openRoom(room); - }} - > - - - ); - } - return ( - - グループチャット: {room.name} - - ); - })} -
+ })} + + )} + ) : ( )} diff --git a/web/app/friends/page.tsx b/web/app/friends/page.tsx index ce2a859e..219d8f60 100644 --- a/web/app/friends/page.tsx +++ b/web/app/friends/page.tsx @@ -15,7 +15,7 @@ export default function Friends() { const [activeTab, setActiveTab] = useState("matching"); return ( -
+
-
+
{activeTab === "matching" ? : }
diff --git a/web/app/home/components/NoMoreUser.tsx b/web/app/home/components/NoMoreUser.tsx deleted file mode 100644 index 91b7aa02..00000000 --- a/web/app/home/components/NoMoreUser.tsx +++ /dev/null @@ -1,9 +0,0 @@ -export default function NoMoreUser() { - return ( -
-

- 「いいね!」を送るユーザーがいません。 -

-
- ); -} diff --git a/web/app/home/page.tsx b/web/app/home/page.tsx index da45ff4b..1e747856 100644 --- a/web/app/home/page.tsx +++ b/web/app/home/page.tsx @@ -15,7 +15,7 @@ import { useAboutMe, useRecommended } from "~/api/user"; import { Card } from "~/components/Card"; import { DraggableCard } from "~/components/DraggableCard"; import FullScreenCircularProgress from "~/components/common/FullScreenCircularProgress"; -import NoMoreUser from "./components/NoMoreUser"; +import BackgroundText from "../../components/common/BackgroundText"; import PersonDetailedMenu from "./components/PersonDetailedMenu"; import RoundButton from "./components/RoundButton"; @@ -121,7 +121,7 @@ export default function Home() { return ; } if (recommended.size() === 0 && loading === false) { - return ; + return ; } if (error) throw error; diff --git a/web/app/search/page.tsx b/web/app/search/page.tsx index f1114915..27c7a368 100644 --- a/web/app/search/page.tsx +++ b/web/app/search/page.tsx @@ -6,6 +6,7 @@ import { useAll, useMatched, useMyID, usePendingFromMe } from "~/api/user"; import FullScreenCircularProgress from "~/components/common/FullScreenCircularProgress"; import Search from "~/components/search/search"; import Table from "~/components/search/table"; +import BackgroundText from "../../components/common/BackgroundText"; export default function SearchPage({ searchParams, @@ -54,14 +55,17 @@ export default function SearchPage({ ); return ( -
-
-

ユーザー検索

- - {users ? ( - +
+ +
+ {query !== "" ? ( + users.length > 0 ? ( +
+ ) : ( + + ) ) : ( - ユーザーが見つかりません + )} diff --git a/web/components/common/BackgroundText.tsx b/web/components/common/BackgroundText.tsx new file mode 100644 index 00000000..069e09aa --- /dev/null +++ b/web/components/common/BackgroundText.tsx @@ -0,0 +1,7 @@ +export default function BackgroundText({ text }: { text: string }) { + return ( +
+

{text}

+
+ ); +} diff --git a/web/components/match/matching.tsx b/web/components/match/matching.tsx index f1348150..01a8a647 100644 --- a/web/components/match/matching.tsx +++ b/web/components/match/matching.tsx @@ -1,6 +1,7 @@ "use client"; import { deleteMatch } from "~/api/match"; import { useMatched } from "~/api/user"; +import BackgroundText from "../common/BackgroundText"; import FullScreenCircularProgress from "../common/FullScreenCircularProgress"; import { useModal } from "../common/modal/ModalProvider"; import { HumanListItem } from "../human/humanListItem"; @@ -15,16 +16,14 @@ export default function Matchings() { if (error) throw error; return ( -
+
{data && data.length === 0 && ( -

- 誰ともマッチングしていません。 リクエストを送りましょう! -

+ )} {current === "loading" ? ( ) : ( -
    +
      {data?.map((matchedUser) => matchedUser.id === 0 ? ( //メモ帳 diff --git a/web/components/search/search.tsx b/web/components/search/search.tsx index e826aa5c..740ad6d5 100644 --- a/web/components/search/search.tsx +++ b/web/components/search/search.tsx @@ -21,19 +21,17 @@ export default function Search({ placeholder, setSearchString }: Props) { } return ( -
      - +
      + ); } From 9f8defbd67a84c3af0e9c53976dc43f30e8e5460 Mon Sep 17 00:00:00 2001 From: Shogo Nakamura <104970808+naka-12@users.noreply.github.com> Date: Wed, 16 Apr 2025 02:53:01 +0900 Subject: [PATCH 3/3] =?UTF-8?q?=E6=8E=88=E6=A5=AD=E3=82=92=E5=AD=A6?= =?UTF-8?q?=E9=83=A8=E3=81=94=E3=81=A8=E3=83=95=E3=82=A3=E3=83=AB=E3=82=BF?= =?UTF-8?q?=20(#708)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/app/globals.css | 12 ++ .../CourseRegisterConfirmDialog.tsx | 4 +- .../course/components/SelectCourseDialog.tsx | 176 ++++++++++++------ .../course/components/TagFilter.tsx | 34 ++++ 4 files changed, 168 insertions(+), 58 deletions(-) create mode 100644 web/components/course/components/TagFilter.tsx diff --git a/web/app/globals.css b/web/app/globals.css index d0c12603..b4c8ea0d 100644 --- a/web/app/globals.css +++ b/web/app/globals.css @@ -8,6 +8,10 @@ max-height: 450px; } +.btn { + @apply font-normal; +} + .cm-li-btn { @apply no-animation h-auto w-full justify-start rounded-none border-none bg-white px-6 py-4 text-left font-normal text-base shadow-none hover:bg-zinc-100 focus:bg-zinc-300; } @@ -16,3 +20,11 @@ .cm-pb-footer { padding-bottom: calc(3rem + env(safe-area-inset-bottom)); } + +.scrollbar-hide::-webkit-scrollbar { + display: none; +} +.scrollbar-hide { + -ms-overflow-style: none; + scrollbar-width: none; +} diff --git a/web/components/course/components/CourseRegisterConfirmDialog.tsx b/web/components/course/components/CourseRegisterConfirmDialog.tsx index 074c480c..bdbc9b0c 100644 --- a/web/components/course/components/CourseRegisterConfirmDialog.tsx +++ b/web/components/course/components/CourseRegisterConfirmDialog.tsx @@ -46,9 +46,7 @@ export default function CourseRegisterConfirmDialog({ return (
      -

      - {mode === "add" ? "変更" : "削除"}の確認 -

      +

      {mode === "add" ? "変更" : "削除"}の確認

      {mode === "add" ? "次のように変更" : "次の授業を削除"} します。よろしいですか? diff --git a/web/components/course/components/SelectCourseDialog.tsx b/web/components/course/components/SelectCourseDialog.tsx index 52f5dafa..38dfa20a 100644 --- a/web/components/course/components/SelectCourseDialog.tsx +++ b/web/components/course/components/SelectCourseDialog.tsx @@ -1,9 +1,57 @@ import { DAY_TO_JAPANESE_MAP } from "common/consts"; import type { Course, Day } from "common/types"; import { useEffect, useState } from "react"; +import { MdClose, MdSearch } from "react-icons/md"; import courseApi from "~/api/course"; import CourseRegisterConfirmDialog from "./CourseRegisterConfirmDialog"; +import TagFilter from "./TagFilter"; +const faculties = [ + "all", + "zenki", + "law", + "medicine", + "engineering", + "arts", + "science", + "agriculture", + "economics", + "liberal-arts", + "education", + "pharmacy", +] as const; +export type FacultyKey = (typeof faculties)[number]; +const facultyRegExMap = new Map([ + ["all", /.*/], + ["zenki", /^[34].*/], + ["law", /^01.*/], + ["medicine", /^02.*/], + ["engineering", /^FEN.*/], + ["arts", /^04.*/], + ["science", /^05.*/], + ["agriculture", /^06.*/], + ["economics", /^07.*/], + ["liberal-arts", /^08.*/], + ["education", /^09.*/], + ["pharmacy", /^10.*/], +]); + +const facultyNameMap = new Map([ + ["all", "全て"], + ["zenki", "前期教養"], + ["law", "法"], + ["medicine", "医"], + ["engineering", "工"], + ["arts", "文"], + ["science", "理"], + ["agriculture", "農"], + ["economics", "経済"], + ["liberal-arts", "後期教養"], + ["education", "教育"], + ["pharmacy", "薬"], +]); + +// TODO: フィルタのロジックが異様にばらけているのでリファクタしよう・・ export default function SelectCourseDialog({ open, onClose, @@ -21,6 +69,7 @@ export default function SelectCourseDialog({ }) { const [availableCourses, setAvailableCourses] = useState([]); const [searchText, setSearchText] = useState(""); + const [selectedFaculty, setSelectedFaculty] = useState("all"); const [filteredAvailableCourses, setFilteredAvailableCourses] = useState< Course[] >([]); @@ -45,7 +94,7 @@ export default function SelectCourseDialog({ return ( // biome-ignore lint/a11y/useKeyWithClickEvents:

      e.stopPropagation()} >
      @@ -62,40 +111,42 @@ export default function SelectCourseDialog({
      -

      - {currentEdit - ? `${DAY_TO_JAPANESE_MAP.get(currentEdit.columnName)}曜${ - currentEdit.rowIndex + 1 - }限の授業を選択` - : "授業を選択"} -

      - +
      +

      + {currentEdit + ? `${DAY_TO_JAPANESE_MAP.get(currentEdit.columnName)}曜${ + currentEdit.rowIndex + 1 + }限の授業を編集中` + : "編集"} +

      + +
      -

      現在の授業

      +

      現在の授業

      {currentEdit?.course ? ( -
      +

      {currentEdit?.course?.name ?? "-"}

      -

      {`${ - currentEdit?.course?.teacher ?? "-" - } / ${currentEdit?.course?.id ?? "-"}`}

      +

      {`${currentEdit?.course?.teacher ?? "-"} / ${ + currentEdit?.course?.id ?? "-" + }`}

      - - { - const text = e.target.value.trim(); - setSearchText(text); - const newFilteredCourses = availableCourses.filter((course) => - course.name.includes(text), - ); - setFilteredAvailableCourses(newFilteredCourses); - }} - /> + +
      + { + setSelectedFaculty((prev) => (prev === tag ? "all" : tag)); + }} + /> +
      {filteredAvailableCourses.length === 0 ? (

      条件に当てはまる授業はありません。

      ) : (
        - {filteredAvailableCourses.map((course) => ( -
      • - -
      • - ))} + {filteredAvailableCourses + .filter((course) => + facultyRegExMap.get(selectedFaculty)?.test(course.id), + ) + .map((course) => ( +
      • + +
      • + ))}
      )}
      diff --git a/web/components/course/components/TagFilter.tsx b/web/components/course/components/TagFilter.tsx new file mode 100644 index 00000000..926641bf --- /dev/null +++ b/web/components/course/components/TagFilter.tsx @@ -0,0 +1,34 @@ +type Props = { + keyNameMap: Map; + selectedTag: T; + onTagChange: (tag: T) => void; +}; + +export default function TagFilter({ + keyNameMap, + selectedTag, + onTagChange, +}: Props) { + const tags = Array.from(keyNameMap.keys()); + return ( +
      + {tags.map((tag) => ( +
      + onTagChange(tag)} + /> + +
      + ))} +
      + ); +}