Видеоуроки, интерактивный редактор и сохранение прогресса — бесплатно, сразу после входа.
ВойтиСоздать аккаунт — бесплатноЗакончили урок?
Войдите, чтобы отмечать прогресс
В этом решении мы разбиваем историю действий на независимые компоненты с чёткой структурой и логикой фильтрации по временным периодам.
ActivityHeader объединяет заголовок со счётчиком и выпадающий список для фильтрации. Мы выделили шапку отдельно, потому что она содержит элементы управления всем списком. Счётчик count показывает количество отфильтрованных действий, а не всех - это даёт пользователю обратную связь о результатах фильтра. Элемент <select> управляется через controlled component - текущее значение приходит через prop filter, изменения отправляются через onChange(e.target.value), извлекая выбранное значение из события.
ActivityIcon отображает эмодзи-иконку типа действия. Мы вынесли иконку в компонент для единообразия и возможности изменить внешний вид всех иконок в одном месте. Это простой презентационный компонент, который оборачивает эмодзи в <div> с классом для стилизации - обычно это круглая рамка с фоном. В будущем можно добавить цветовую индикацию в зависимости от типа действия.
ActivityContent отвечает за отображение текстовой части действия. Мы выделили контент отдельно, чтобы отделить структуру (описание и время) от иконки. Текст действия выводится в параграфе, время - в <span> с отдельным классом для стилизации более мелким шрифтом или другим цветом. Это разделение позволяет гибко управлять вёрсткой без изменения логики.
ActivityItem собирает вместе иконку и контент. Мы создали этот компонент, чтобы инкапсулировать структуру одного действия - горизонтальную компоновку иконки слева и текста справа. Компонент получает весь объект activity и извлекает из него нужные поля для дочерних компонентов. Эта карточка представляет одну запись в истории действий.
NoActivities отображает сообщение, когда список пустой после фильтрации. Мы выделили пустое состояние в компонент для переиспользуемости и централизации текста сообщения. Это улучшает UX - если пользователь выберет "За сегодня" и сегодня не было действий, он увидит понятное объяснение вместо пустого экрана. Компонент статичный, но можно добавить props для разных вариантов сообщений.
ActivityList - это контейнер для списка с обработкой пустого состояния. Мы выделили список отдельно, чтобы инкапсулировать логику условного рендеринга. Тернарный оператор проверяет activities.length > 0 - если массив не пустой, создаётся список через map, если пустой, показывается NoActivities. Компонент не знает про фильтрацию, он просто отображает переданный массив.
Функция фильтрует действия по выбранному временному периоду. Сначала вычисляются граничные даты для сравнения. now - текущий момент времени. today - начало текущего дня (00:00), создаётся через конструктор с явным указанием года, месяца и дня без времени. weekAgo - начало дня неделю назад, вычисляется как today минус 7 дней в миллисекундах.
Мы создаём новый объект Date, передавая только год, месяц и день. Время по умолчанию устанавливается в 00:00:00. Это важно для корректного сравнения - если бы мы использовали просто now, то сравнение activity.date >= today отфильтровало бы только действия, произошедшие после текущего времени, а не за весь день. Методы getFullYear(), getMonth(), getDate() извлекают компоненты даты из текущего момента.
Мы вычисляем дату неделю назад через миллисекунды. Метод getTime() возвращает timestamp - количество миллисекунд с 1 января 1970 года. Вычитаем 7 дней, где каждый день - это 24 часа × 60 минут × 60 секунд × 1000 миллисекунд. Результат передаём в конструктор Date, который создаёт объект даты из timestamp. Поскольку вычитаем из today (начало дня), получаем начало дня неделю назад.
Оператор switch проверяет значение filter и выбирает соответствующую логику фильтрации. Для 'today' отбираем действия, где activity.date >= today - дата действия больше или равна началу текущего дня. Для 'week' проверяем activity.date >= weekAgo - последние 7 дней. Для 'all' или любого другого значения возвращаем все действия без фильтрации через default. Оператор сравнения >= работает с объектами Date, сравнивая их внутренние timestamp.
JavaScript позволяет сравнивать объекты Date операторами <, >, <=, >=. При сравнении Date автоматически преобразуются в числа (timestamp), и сравниваются эти числа. Условие activity.date >= today истинно, если действие произошло сегодня или позже. Это работает, потому что today установлен на 00:00 текущего дня - любое действие сегодня будет иметь дату больше или равную этому моменту.
Мы вызываем функцию фильтрации при каждом рендере. Это вычисляемое значение, а не состояние. При изменении filter или activities React перерендерит компонент, функция выполнится заново, и filteredActivities обновится. Это проще и надёжнее, чем хранить отфильтрованный массив в отдельном состоянии и синхронизировать его с изменениями исходных данных.
В начальных данных мы создаём даты действий, вычитая время из текущего момента. Date.now() возвращает текущий timestamp в миллисекундах. Вычитаем количество миллисекунд в сутках для "вчера", умножаем на 2 для "2 дня назад", на 7 для "неделю назад". Конструктор Date принимает timestamp и создаёт объект даты. Это позволяет имитировать действия, произошедшие в разное время.
Компонент App собирает шапку и список вместе. Структура максимально простая - два блока. Всё состояние (activities, filter) и логика фильтрации остаются в App. В ActivityHeader передаём текущий фильтр, функцию изменения и количество отфильтрованных действий. В ActivityList передаём уже отфильтрованный массив - компонент списка не знает про фильтрацию, он просто отображает то, что получил. Функция setFilter передаётся напрямую как onChange, потому что она принимает строку, и компонент передаёт ей строку из e.target.value.
Мы передаём setFilter напрямую без обёртки, потому что ActivityHeader вызывает onChange(e.target.value) - передаёт строку. setFilter принимает строку и обновляет состояние. Сигнатуры совпадают, поэтому дополнительная функция не нужна. Это упрощает код по сравнению с onChange={(value) => setFilter(value)}.