Initialize ZUJ OL Apps website with React + TypeScript + Vite

This commit is contained in:
2026-05-22 09:21:24 +08:00
commit 5e79c96364
55 changed files with 7409 additions and 0 deletions
+98
View File
@@ -0,0 +1,98 @@
import { useSearchParams } from 'react-router-dom';
import { useI18n } from '../hooks/useI18n';
import { useProjectFilters } from '../hooks/useProjectFilters';
import { siteData } from '../data/siteData';
import ProjectCard from '../components/ProjectCard';
import SelectControl from '../components/SelectControl';
export default function ProjectsPage() {
const { t, bi } = useI18n();
const [searchParams] = useSearchParams();
const catParam = searchParams.get('cat');
const {
search, setSearch,
tech, setTech,
platform, setPlatform,
status, setStatus,
sort, setSort,
allTech, allPlatforms, allStatuses,
filteredProjects,
} = useProjectFilters();
const displayedProjects = catParam
? filteredProjects.filter(p => p.type.includes(catParam))
: filteredProjects;
const techOptions = [
{ value: '', label: t('projects.filter.all') },
...allTech.map((item) => ({ value: item, label: item })),
];
const platformOptions = [
{ value: '', label: t('projects.filter.all') },
...allPlatforms.map((item) => ({ value: item, label: item })),
];
const statusOptions = [
{ value: '', label: t('projects.filter.all') },
...allStatuses.map((item) => ({ value: item, label: bi(siteData.statuses[item]?.label) })),
];
const sortOptions = [
{ value: 'updated', label: t('projects.sort.updated') },
{ value: 'stars', label: t('projects.sort.stars') },
{ value: 'name', label: t('projects.sort.name') },
];
return (
<div className="container fade-in">
<div className="page-header">
<h1 className="page-title">{t('projects.title')}</h1>
<p className="page-subtitle">{t('projects.subtitle')}</p>
</div>
<div className="filter-bar">
<input
type="text"
className="search-input"
value={search}
onChange={e => setSearch(e.target.value)}
placeholder={t('projects.search')}
/>
<div className="filter-group">
<span className="filter-group-label">{t('projects.filter.tech')}</span>
<SelectControl
value={tech}
options={techOptions}
onChange={setTech}
searchable={techOptions.length > 8}
searchPlaceholder={t('projects.filter.tech')}
emptyLabel={t('projects.noResults')}
/>
</div>
<div className="filter-group">
<span className="filter-group-label">{t('projects.filter.platform')}</span>
<SelectControl value={platform} options={platformOptions} onChange={setPlatform} />
</div>
<div className="filter-group">
<span className="filter-group-label">{t('projects.filter.status')}</span>
<SelectControl value={status} options={statusOptions} onChange={setStatus} />
</div>
<div className="filter-group filter-group-sort">
<span className="filter-group-label">Sort</span>
<SelectControl value={sort} options={sortOptions} onChange={setSort} />
</div>
</div>
<div className="card-grid">
{displayedProjects.length === 0 ? (
<div className="empty-state">{t('projects.noResults')}</div>
) : (
displayedProjects.map(p => <ProjectCard key={p.id} project={p} />)
)}
</div>
</div>
);
}