diff --git a/src/pages/datapoints.jsx b/src/pages/datapoints.jsx index 90ea49d..6327f52 100644 --- a/src/pages/datapoints.jsx +++ b/src/pages/datapoints.jsx @@ -3,7 +3,7 @@ import Layout from '@theme/Layout'; import BrowserOnly from '@docusaurus/BrowserOnly'; import Head from '@docusaurus/Head'; import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; -import { getMe, listDp, getFilterOptions, getCounts } from '@site/src/lib/dp/api'; +import { getMe, listDp, getFilterOptions, getCounts, listPrograms } from '@site/src/lib/dp/api'; import SignInButtons from '@site/src/lib/auth/SignInButtons'; import { startOAuth } from '@site/src/lib/auth/oauth'; import styles from './datapoints.module.css'; @@ -19,7 +19,7 @@ const EMPTY_FILTER_OPTS = { schools: [], tiers: [], years: [], ugCats: [], major const COPY = { 'zh-Hans': { pageTitle: 'DataPoints', - pageDesc: 'CS / MSCS 申请历史 DataPoints — 可按学校、Tier、年份、结果、本科背景筛选', + pageDesc: 'CS / MSCS 申请历史 DataPoints — 可按项目、年份、结果、本科背景筛选', headerTitle: 'CS Application DataPoints', metaApplicants: '位申请者', metaPrograms: '个项目', @@ -29,8 +29,8 @@ const COPY = { loadFail: '加载数据失败:', searchPlaceholder: '搜索学校 / 项目', searchLabel: '搜索', - filterSchool: '学校', - filterTier: '项目档次', + filterProgram: '项目', + programSearchPlaceholder: '搜索学校 / 项目', filterYear: '年份', filterResult: '结果', filterUgCat: '本科类别', @@ -110,7 +110,7 @@ const COPY = { }, en: { pageTitle: 'DataPoints', - pageDesc: 'CS / MSCS application DataPoints — filterable by school, tier, year, result, and undergrad background', + pageDesc: 'CS / MSCS application DataPoints — filterable by program, year, result, and undergrad background', headerTitle: 'CS Application DataPoints', metaApplicants: 'applicants', metaPrograms: 'programs', @@ -120,8 +120,8 @@ const COPY = { loadFail: 'Failed to load data: ', searchPlaceholder: 'Search school / program', searchLabel: 'Search', - filterSchool: 'School', - filterTier: 'Program tier', + filterProgram: 'Program', + programSearchPlaceholder: 'Search school / program', filterYear: 'Year', filterResult: 'Result', filterUgCat: 'Undergrad type', @@ -196,7 +196,7 @@ function pickLocale(loc) { // ---------- URL <-> filter state helpers ---------- -const URL_KEYS = ['school', 'tier', 'year', 'result', 'ugCat', 'major']; +const URL_KEYS = ['programId', 'year', 'result', 'ugCat', 'major']; function readFiltersFromUrl() { if (typeof window === 'undefined') return {}; @@ -332,19 +332,29 @@ function Table({ counts, filterOpts, me, meChecked, t }) { const [openApplicantId, setOpenApplicantId] = useState(null); const initial = useMemo(() => readFiltersFromUrl(), []); - const [school, setSchool] = useState(initial.school || ''); - const [tier, setTier] = useState(initial.tier || ''); + const [programId, setProgramId] = useState(initial.programId || ''); + const [programs, setPrograms] = useState([]); const [year, setYear] = useState(initial.year || ''); const [result, setResult] = useState(initial.result || ''); const [ugCat, setUgCat] = useState(initial.ugCat || ''); const [major, setMajor] = useState(initial.major || ''); const [page, setPage] = useState(0); + // Program list powers the searchable "项目" filter and resolves a programId + // (e.g. one restored from the URL) back to its school·program label. useEffect(() => { - writeFiltersToUrl({ school, tier, year, result, ugCat, major }); - }, [school, tier, year, result, ugCat, major]); + let cancelled = false; + listPrograms({ limit: 500 }) + .then((r) => { if (!cancelled) setPrograms(r.rows || []); }) + .catch(() => {}); + return () => { cancelled = true; }; + }, []); + + useEffect(() => { + writeFiltersToUrl({ programId, year, result, ugCat, major }); + }, [programId, year, result, ugCat, major]); - useEffect(() => setPage(0), [school, tier, year, result, ugCat, major]); + useEffect(() => setPage(0), [programId, year, result, ugCat, major]); // Fetch rows from API on filter/page change. Keep previous rows visible // while a new fetch is in flight so the table doesn't blink. @@ -366,8 +376,7 @@ function Table({ counts, filterOpts, me, meChecked, t }) { setApiError(null); try { const res = await listDp({ - school: school || undefined, - tier: tier || undefined, + programId: programId || undefined, year: year || undefined, result: result || undefined, ugCategory: ugCat || undefined, @@ -385,21 +394,31 @@ function Table({ counts, filterOpts, me, meChecked, t }) { } }, delay); return () => { cancelled = true; clearTimeout(handle); }; - }, [school, tier, year, result, ugCat, major, page]); + }, [programId, year, result, ugCat, major, page]); const totalPages = Math.max(1, Math.ceil(totalCount / PAGE_SIZE)); const paged = rows; + const selectedProgram = useMemo( + () => programs.find((p) => p.id === programId) || null, + [programs, programId], + ); + const activeFilters = useMemo(() => { const items = []; - if (school) items.push({ key: 'school', label: t.filterSchool, value: school }); - if (tier) items.push({ key: 'tier', label: t.filterTier, value: tier }); + if (programId) { + items.push({ + key: 'programId', + label: t.filterProgram, + value: selectedProgram ? `${selectedProgram.school} · ${selectedProgram.program}` : programId, + }); + } if (year) items.push({ key: 'year', label: t.filterYear, value: year }); if (result) items.push({ key: 'result', label: t.filterResult, value: result }); if (ugCat) items.push({ key: 'ugCat', label: t.filterUgCat, value: ugCat }); if (major) items.push({ key: 'major', label: t.filterMajor, value: major }); return items; - }, [school, tier, year, result, ugCat, major, t]); + }, [programId, selectedProgram, year, result, ugCat, major, t]); return (