fix(frontend): show newest completed races first #50

Merged
admin merged 2 commits from docs/complete-release-history into main 2026-08-13 21:43:37 +00:00
4 changed files with 102 additions and 17 deletions

View File

@@ -11,9 +11,34 @@
#### Added
- Управление паролем и сессиями: проверка текущего пароля, список активных сессий, отзыв выбранной или всех остальных сессий.
- Фильтрация стартов по статусу, периоду и дистанции; backfill и обновление автоматически получаемых обложек.
### [1.5.3] — 2026-07-12
#### Fixed
- Миграции базы данных автоматически выполняются перед запуском backend-контейнера.
### [1.5.2] — 2026-07-12
#### Added
- Настраиваемое имя публичного профиля и раздельная видимость будущих и завершённых стартов.
- Публичные результаты финиша с учётом настроек видимости владельца.
### [1.5.1] — 2026-07-12
#### Added
- Публичные страницы пользователей и API опубликованного календаря стартов.
### [1.5.0] — 2026-07-12
#### Added
- Проверка и смена текущего пароля.
- Список активных сессий, отзыв выбранной сессии или всех остальных сессий.
### [1.4.2] — 2026-07-12
#### Changed
@@ -83,6 +108,12 @@
## Frontend
### [0.10.2] — 2026-08-14
#### Fixed
- По умолчанию завершённые старты отображаются от самых свежих к более старым; явно выбранная сортировка применяется без переопределения.
### [0.10.1] — 2026-08-13
#### Changed
@@ -101,17 +132,48 @@
- В конфигурацию TypeScript для Vite добавлены DOM-типы.
### [0.9.3] — 2026-07-14
### [0.9.3] — 2026-07-13
#### Added
#### Changed
- Экран «Аккаунт»: подтверждённый email, смена пароля и управление другими активными сессиями.
- На дашборде появились фильтруемые «Последние старты» с пагинацией и переходом в старт, ссылки из карточек рекордов и интерактивный график прогресса.
- Поиск, фильтры, сортировка и экспорт истории; график прогресса переведён на AG Charts.
- Фильтры календаря сворачиваются и показывают число активных условий; hero-блок дашборда стал компактнее.
### [0.9.1] — 2026-07-13
#### Fixed
- Упрощён обзор календаря стартов и зарегистрированы модули AG Charts.
- Зарегистрированы необходимые модули AG Charts для корректного отображения графика.
### [0.9.0] — 2026-07-13
#### Added
- Поиск, фильтры и сортировка истории стартов с синхронизацией параметров в URL.
- Экспорт выбранных стартов в CSV и iCalendar.
- Интерактивный график прогресса на AG Charts.
- Фильтрация стартов по статусу, диапазону дат и дистанции.
### [0.8.2] — 2026-07-12
#### Added
- Имена публичных профилей и отдельные настройки видимости будущих и завершённых стартов.
- Отображение опубликованных результатов на публичной странице.
### [0.8.1] — 2026-07-12
#### Added
- Публичная страница пользователя с опубликованным календарём стартов.
- Управление видимостью публичного профиля в аккаунте.
### [0.8.0] — 2026-07-12
#### Added
- Экран «Аккаунт»: подтверждённый email, смена пароля и управление активными сессиями.
- Фильтруемые «Последние старты» с пагинацией и переходом в старт на дашборде.
- Ссылки из карточек рекордов и интерактивный график прогресса.
### [0.7.1] — 2026-07-12
@@ -165,6 +227,25 @@
- Редизайн дашборда и визуального представления стартов.
### [0.4.3] — 2026-04-13
#### Fixed
- В календаре скрыты всплывающие карточки для дней без стартов.
### [0.4.2] — 2026-04-13
#### Fixed
- Анимация наведения применяется ко всей строке старта, а не только к внутренней ссылке.
### [0.4.1] — 2026-04-13
#### Fixed
- Старт автоматически получает статус «Пробежал» при вводе корректного времени финиша.
- Исправлены переходы с дашборда и поведение списка и календаря стартов.
### [0.4.0] — 2026-04-13
#### Added

View File

@@ -1,12 +1,12 @@
{
"name": "calendar-run-frontend",
"version": "0.10.1",
"version": "0.10.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "calendar-run-frontend",
"version": "0.10.1",
"version": "0.10.2",
"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.10.1",
"version": "0.10.2",
"type": "module",
"scripts": {
"dev": "vite",

View File

@@ -10,7 +10,7 @@ import {
getRaceStatusClassName,
getRaceStatusLabel,
parseRaceDate,
sortByDateAsc,
sortByDateDesc,
downloadTextFile,
racesToCsv,
racesToIcs,
@@ -37,6 +37,7 @@ const VIEW_STORAGE_KEY = "races-view-mode";
type ViewMode = "list" | "calendar";
type RaceListTab = "upcoming" | "completed";
type RaceSort = "dateAsc" | "dateDesc" | "titleAsc" | "distanceAsc";
type RaceSortOption = RaceSort | "";
function yearSelectOptions(races: Race[], selectedYear = ""): number[] {
const current = new Date().getFullYear();
@@ -146,7 +147,7 @@ export function RacesPage(): JSX.Element {
const [distanceMin, setDistanceMin] = useState<string>(() => searchParams.get("distanceMin") ?? "");
const [distanceMax, setDistanceMax] = useState<string>(() => searchParams.get("distanceMax") ?? "");
const [searchQuery, setSearchQuery] = useState<string>(() => searchParams.get("q") ?? "");
const [sortOrder, setSortOrder] = useState<RaceSort>(() => (searchParams.get("sort") as RaceSort | null) ?? "dateAsc");
const [sortOrder, setSortOrder] = useState<RaceSortOption>(() => (searchParams.get("sort") as RaceSort | null) ?? "");
const [viewMode, setViewMode] = useState<ViewMode>(() => searchParams.get("view") === "calendar" ? "calendar" : readInitialViewMode());
const [activeListTab, setActiveListTab] = useState<RaceListTab>("upcoming");
const [isAdvancedFiltersOpen, setIsAdvancedFiltersOpen] = useState<boolean>(() =>
@@ -263,7 +264,7 @@ export function RacesPage(): JSX.Element {
if (distanceMin) next.set("distanceMin", distanceMin);
if (distanceMax) next.set("distanceMax", distanceMax);
if (searchQuery) next.set("q", searchQuery);
if (sortOrder !== "dateAsc") next.set("sort", sortOrder);
if (sortOrder) next.set("sort", sortOrder);
setSearchParams(next, { replace: true });
}, [dateFrom, dateTo, distanceMax, distanceMin, monthFilter, searchQuery, setSearchParams, sortOrder, statusFilter, viewMode, yearFilter]);
@@ -281,9 +282,11 @@ export function RacesPage(): JSX.Element {
const { upcoming, completed } = useMemo(
() => ({
upcoming: filteredRaces.filter((race) => race.status !== "completed"),
completed: sortByDateAsc(filteredRaces.filter((race) => race.status === "completed")),
completed: sortOrder
? filteredRaces.filter((race) => race.status === "completed")
: sortByDateDesc(filteredRaces.filter((race) => race.status === "completed")),
}),
[filteredRaces],
[filteredRaces, sortOrder],
);
const statusMessage = useMemo(() => {
if (errorMessage && !isLoading) {
@@ -309,7 +312,7 @@ export function RacesPage(): JSX.Element {
dateTo,
distanceMin,
distanceMax,
sortOrder !== "dateAsc" ? sortOrder : "",
sortOrder,
].filter(Boolean).length;
if (errorMessage && races.length === 0 && !isLoading) {
@@ -478,7 +481,8 @@ export function RacesPage(): JSX.Element {
</label>
<label className="races-filter__field">
<span className="races-filter__label">Сортировка</span>
<select className="races-filter__select" value={sortOrder} onChange={(event) => setSortOrder(event.target.value as RaceSort)}>
<select className="races-filter__select" value={sortOrder} onChange={(event) => setSortOrder(event.target.value as RaceSortOption)}>
<option value="">По умолчанию</option>
<option value="dateAsc">Дата: сначала ранние</option>
<option value="dateDesc">Дата: сначала поздние</option>
<option value="titleAsc">Название: АЯ</option>