fix(frontend): simplify race calendar overview #47

Merged
admin merged 1 commits from fix/collapse-start-filters into main 2026-07-13 21:14:11 +00:00
5 changed files with 65 additions and 7 deletions

View File

@@ -1,12 +1,12 @@
{
"name": "calendar-run-frontend",
"version": "0.9.1",
"version": "0.9.3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "calendar-run-frontend",
"version": "0.9.1",
"version": "0.9.3",
"dependencies": {
"ag-charts-community": "^14.0.0",
"ag-charts-react": "^14.0.0",

View File

@@ -1,7 +1,7 @@
{
"name": "calendar-run-frontend",
"private": true,
"version": "0.9.1",
"version": "0.9.3",
"type": "module",
"scripts": {
"dev": "vite",

View File

@@ -192,7 +192,6 @@ export function DashboardPage(): JSX.Element {
>
<div className="dashboard-hero__content">
<p className="dashboard-hero__eyebrow">Календарь сезона</p>
<h1 className="dashboard-hero__title">Беговой штаб</h1>
<p className="dashboard-hero__text">
Планируйте старты, держите фокус на ближайшей гонке и сравнивайте прогресс по дистанциям.
</p>

View File

@@ -145,6 +145,9 @@ export function RacesPage(): JSX.Element {
const [sortOrder, setSortOrder] = useState<RaceSort>(() => (searchParams.get("sort") as RaceSort | null) ?? "dateAsc");
const [viewMode, setViewMode] = useState<ViewMode>(() => searchParams.get("view") === "calendar" ? "calendar" : readInitialViewMode());
const [activeListTab, setActiveListTab] = useState<RaceListTab>("upcoming");
const [isFiltersOpen, setIsFiltersOpen] = useState<boolean>(() =>
["year", "month", "status", "dateFrom", "dateTo", "distanceMin", "distanceMax", "q", "sort"].some((key) => searchParams.has(key)),
);
const setViewModePersist = useCallback((mode: ViewMode) => {
setViewMode(mode);
@@ -297,6 +300,17 @@ export function RacesPage(): JSX.Element {
]
.filter(Boolean)
.join(" ");
const activeFilterCount = [
viewMode === "list" ? yearFilter : "",
monthFilter,
statusFilter,
searchQuery,
dateFrom,
dateTo,
distanceMin,
distanceMax,
sortOrder !== "dateAsc" ? sortOrder : "",
].filter(Boolean).length;
if (errorMessage && races.length === 0 && !isLoading) {
return (
@@ -331,7 +345,14 @@ export function RacesPage(): JSX.Element {
</button>
</div>
</div>
<div className="races-hero__filters">
<details
className="races-hero__filters"
open={isFiltersOpen}
onToggle={(event) => setIsFiltersOpen(event.currentTarget.open)}
>
<summary className="races-hero__filters-summary">
<span>Фильтры{activeFilterCount > 0 ? ` · ${activeFilterCount} активн.` : ""}</span>
</summary>
<div className="races-filter" role="search" aria-label="Фильтр по дате">
<label className="races-filter__field">
<span className="races-filter__label">Год</span>
@@ -409,7 +430,7 @@ export function RacesPage(): JSX.Element {
<button type="button" className="btn btn--secondary" disabled={filteredRaces.length === 0} onClick={() => downloadTextFile("calendar-run.csv", racesToCsv(filteredRaces), "text/csv;charset=utf-8")}>Скачать CSV</button>
<button type="button" className="btn btn--secondary" disabled={filteredRaces.length === 0} onClick={() => downloadTextFile("calendar-run.ics", racesToIcs(filteredRaces), "text/calendar;charset=utf-8")}>Скачать календарь</button>
</div>
</div>
</details>
</section>
<div className="races-status" aria-live="polite">

View File

@@ -1564,8 +1564,46 @@ body {
backdrop-filter: blur(14px);
}
.races-hero__filters-summary {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--space-3);
color: #ffffff;
font-weight: 700;
cursor: pointer;
list-style: none;
}
.races-hero__filters-summary::-webkit-details-marker {
display: none;
}
.races-hero__filters-summary::after {
content: "+";
display: inline-flex;
align-items: center;
justify-content: center;
width: 1.5rem;
height: 1.5rem;
border: 1px solid rgba(255, 255, 255, 0.42);
border-radius: 50%;
font-size: 1.15rem;
font-weight: 400;
line-height: 1;
}
.races-hero__filters[open] .races-hero__filters-summary::after {
content: "";
}
.races-hero__filters-summary:focus-visible {
outline: 2px solid var(--color-lime);
outline-offset: var(--space-1);
}
.races-hero__filters .races-filter {
margin-top: 0;
margin-top: var(--space-5);
}
.races-hero__filters .races-filter__label {