Initialize ZUJ OL Apps website with React + TypeScript + Vite
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user