Введение
В Daylight Festival шесть сессий на трёх сценах. Две сессии могут называться одинаково, поэтому личное расписание должно помнить их реальные идентификаторы. Вы свяжете подготовленную адаптивную страницу React с одним набором выбранных ID, а затем получите карточки, количество и суммарные минуты из текущей программы. Фильтрация программы не должна терять предыдущий выбор.
Нужны знания компонентов React, JSX, props, useState, событий, условного рендеринга, map/filter, ключей списков и управляемых полей, а также массивов, объектов и функций JavaScript и основ HTML/CSS. В этой работе изучаются производные значения состояния, поиск текущих записей, неизменяемые обновления выбора, суммирование длительности и сообщения об отсутствующих ID. Среда запускается независимо; файлы предыдущей VM не нужны. 55 минут — оценка автора, а не измеренное время начинающих. Все сессии и идентификаторы синтетические.
Используйте Workspace для терминала и Festival для настоящих ссылок на страницы. В этой работе редактируйте только /home/labex/project/festival-schedule/work/src/selection.js и src/App.jsx. Сохраните src/settings.js, src/main.jsx, src/conflicts.js, src/storage.js, подготовленные рендерер и стили shared, Study, зависимости и настройки сервиса. Проверка конфликтов и хранение в браузере пока не требуются. В завершённом примере Study они уже подготовлены; вы реализуете их в следующей работе. Подписи сайта остаются английскими.
Сопоставить выбор с текущими сессиями
В этом шаге сначала используйте обычный пример, затем превратите стабильные ID в реальные записи текущей программы.
В Festival выберите Open Study schedule in a new page. Это настоящая ссылка на отдельную страницу браузера вне встроенной вкладки продукта. Действия сайта выполняйте на новой странице. Клавишей Tab перейдите к Add to schedule для First Light, Garden, 10:00–10:30, затем нажмите Enter. Перейдите к Paper Signals, Studio, 10:30–11:15 и добавьте её. Выберите View my schedule: появятся две реальные карточки и 2 sessions · 75 minutes. Ссылка навигации дня переходит к заголовку расписания, а Back to the programme возвращает к каталогу. У примера собственный выбор; это не ваша работа и не результат проверки. Макет, сообщения карточек и навигация уже подготовлены; вы строите связанную с ними логику выбора.
Закройте Study. На входной странице Festival продукта выберите Open Work schedule in a new page и оставьте страницу открытой. Она не завершена. Для редактирования файлов переключитесь на вкладку Workspace продукта. Переключение вкладки продукта отличается от ссылки сайта, якоря страницы и кнопки «Назад» браузера. Подготовленная команда Workspace открывает рабочий каталог; следующая видимая команда также явно задаёт начальный путь. cd меняет каталог этого терминала, абсолютный путь начинается с /, pwd печатает текущий путь, а cat показывает небольшой файл без изменений.
cd /home/labex/project/festival-schedule/work
pwd
cat src/settings.js
Каталог использует ID вроде D11 и D13. Обе сессии называются First Light, но отличаются сценой и временем. Подготовленный settings.initialIDs содержит D11 и RETIRED-07: небольшой синтетический начальный выбор для этой работы, а не хранилище прошлой VM. D11 ещё существует, RETIRED-07 — уже нет. Карточкой должна становиться только текущая запись. Отсутствующий ID получит явное объяснение в шаге 3.
Небольшая ошибка в имени файла исправима. Следующая команда намеренно пишет selection с ошибкой. Она должна сообщить, что файла нет, и завершиться с ненулевым кодом; данные не изменяются:
cat src/selction.js
Прочитайте ошибку, сравните написание и используйте правильный путь:
cat src/selection.js
Файл содержит пустые функции. currentRecords будет выбирать из текущего каталога по ID, не по названию или позиции массива. filter создаёт новый массив; includes проверяет, выбран ли ID. ID каталога уникальны, поэтому повтор ID не создаст дубликат карточки. Сортируйте новый массив по числовому началу, затем по ASCII ID при одинаковом начале. || в функции сравнения использует ID только при нулевой разнице времени. condition ? first : second выбирает одно из двух значений. Исходный каталог сохраняется.
Сохраните полный файл через here-document. В cat > path <<'JS' оператор > заменяет файл, << передаёт последующие строки на ввод, а JS в кавычках запрещает shell подставлять значения в JavaScript. Завершающий JS находится на отдельной строке. Это способ редактирования через терминал; можно вместо этого редактировать те же файлы в WebIDE и сохранять Ctrl+S. Остальные три функции остаются незавершёнными для следующих шагов.
cat > src/selection.js <<'JS'
export function currentRecords(ids, catalog) {
return catalog.filter(session => ids.includes(session.id)).sort((a, b) =>
a.start - b.start || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0)
);
}
export function totalMinutes(records) {
return 0;
}
export function visibleRecords(catalog, stage) {
return catalog;
}
export function missingNotice(ids, catalog) {
return '';
}
JS
Вернитесь на отдельную страницу Work и обычно обновите браузер. View my schedule теперь показывает настоящую Garden First Light, 10:00–10:30, с текущим описанием. Для RETIRED-07 нельзя выдумывать карточку. Сводка ещё имеет исходные значения, Add/Remove не подключены; следующий шаг это завершит. Найдите сравнение с session.id: изменение порядка каталога не должно менять объект, обозначенный D11.
Если браузер показывает ошибку модуля или синтаксиса, вернитесь в Workspace, выполните pwd и cat src/selection.js, сравните импорты, имена функций, кавычки и скобки с полным блоком и сохраните исправление по точному пути. Затем вернитесь в Work и обновите обычным образом. Не сбрасывайте VM и не переустанавливайте пакеты для исправления имени файла или JavaScript. Запустите проверку шага, когда текущая карточка видна.
Вычислить сводку и обновлять единственный выбор
В этом шаге подключите Add и Remove к одному списку ID и вычислите количество и длительность.
Начните в Workspace по пути /home/labex/project/festival-schedule/work. Отдельно хранимое количество или время может перестать соответствовать карточкам после удаления. Используйте selectedIDs как состояние выбора, получая текущие записи при каждом рендеринге. Длина массива этих записей даёт количество. reduce сводит записи к одному значению: начните с нуля минут и добавляйте каждое end - start. Подготовленные времена — целые минуты одной местной даты; разбор Date и преобразование часовых поясов не нужны. D11 длится 30 минут, D12 — 45, вместе 75. Промежутки между сессиями не входят во время участия.
Замените selection.js, сохранив поиск по ID и дополнив суммирование. Фильтр и объяснение отсутствующих ID пока остаются заготовками для шага 3.
cat > src/selection.js <<'JS'
export function currentRecords(ids, catalog) {
return catalog.filter(session => ids.includes(session.id)).sort((a, b) =>
a.start - b.start || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0)
);
}
export function totalMinutes(records) {
return records.reduce((minutes, session) => minutes + session.end - session.start, 0);
}
export function visibleRecords(catalog, stage) {
return catalog;
}
export function missingNotice(ids, catalog) {
return '';
}
JS
Теперь подключите компонент React. useState предоставляет значение состояния и функцию обновления. ID выбора, видимая сцена и сообщения имеют разные задачи; смена сцены не должна заменять список ID. Выбранные карточки и сводка — вычисленные значения, а не дополнительные состояния.
Функция обновления ids => [...ids, id] получает текущий массив ID и создаёт новый через spread .... Удаление создаёт новый массив через filter; исключается совпавший ID, не все сессии с таким названием. Проверка уже выбранного ID предотвращает дубликаты. find получает реальную запись нажатого ID. feedback — небольшой объект для подготовленного рендерера: sessionID размещает результат Add рядом с нужной карточкой, а text содержит читаемое сообщение. Рендерер сохраняет сообщение видимым, не переводя клавиатурный фокус в область динамического статуса.
clock из подготовленного View.jsx форматирует целые минуты для отображения. describe объединяет название, сцену и время, различая одноимённые сессии. Все props ниже имеют конкретный источник в состоянии или производных записях. Полностью замените App.jsx:
cat > src/App.jsx <<'JS'
import React, {useState} from 'react';
import View, {clock} from '../../shared/View.jsx';
import {catalog, settings} from './settings.js';
import {currentRecords, totalMinutes, visibleRecords, missingNotice} from './selection.js';
const describe = session => session.title + ' (' + session.stage + ', ' + clock(session.start) + '-' + clock(session.end) + ')';
export default function App() {
const [selectedIDs, setSelectedIDs] = useState(settings.initialIDs);
const [stage, setStage] = useState('all');
const [feedback, setFeedback] = useState({sessionID: null, text: ''});
const selected = currentRecords(selectedIDs, catalog);
const visible = visibleRecords(catalog, stage);
const minutes = totalMinutes(selected);
function add(id) {
const session = catalog.find(item => item.id === id);
if (!session || selectedIDs.includes(id)) return;
setSelectedIDs(ids => [...ids, id]);
setFeedback({sessionID: id, text: 'Added ' + describe(session) + '.'});
}
function remove(id) {
setSelectedIDs(ids => ids.filter(existing => existing !== id));
setFeedback({sessionID: null, text: ''});
}
function changeStage(value) {
setStage(value);
setFeedback({sessionID: null, text: ''});
}
return <View settings={settings} visible={visible} selected={selected}
count={selected.length} minutes={minutes} selectedIDs={selectedIDs}
stage={stage} onStage={changeStage} onAdd={add} onRemove={remove}
feedback={feedback} missingNotice={missingNotice(selectedIDs, catalog)} />;
}
JS
Вернитесь в Work и обновите. Синтетический начальный выбор даёт 1 session · 30 minutes и карточку Garden First Light. Добавьте Paper Signals: получится 2 сессии и 75 минут. Добавьте First Light на Loft, 12:00–12:30: обе First Light должны остаться с разными сценами и временем, дав 3 сессии и 105 минут. Удалите только Garden First Light. Останутся Paper Signals и более поздняя Loft First Light: 2 сессии и 75 минут. Это также доказывает, что длительность суммирует участие, а не весь период от первого начала до последнего конца.
Предоставленный рендерер использует ID сессии как React key. Ключи отслеживают идентичность при изменении списка; они не заменяют состояние выбора. Кнопка Add становится Added, повторный выбор не должен создавать копию. В этой части фильтр сцены ещё использует заготовку, а пересечения намеренно пока не предотвращаются. Не утверждайте, что любой набор уже образует бесконфликтное расписание. Запустите проверку, когда карточки и сводка согласованы.
Фильтровать программу без потери выбора
В этом шаге завершите фильтр сцены и объясните недоступный прежний ID, сохранив действительные сессии.
Начните в Workspace. visibleRecords фильтрует только массив программы по stage === 'all' или совпадающей сцене и сортирует новый массив по началу и ID. Она никогда не меняет selectedIDs. missingNotice проверяет, есть ли выбранный ID без текущей записи. some возвращает true, если хотя бы один элемент удовлетворяет условию; внутренний some ищет соответствующий текущий ID, а ! означает отсутствие совпадения. Уведомление — обычная обратная связь, не ошибка, уничтожающая всё расписание. Если ничего не отсутствует, функция может вернуть пустую строку.
Сохраните полный вспомогательный файл:
cat > src/selection.js <<'JS'
export function currentRecords(ids, catalog) {
return catalog.filter(session => ids.includes(session.id)).sort((a, b) =>
a.start - b.start || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0)
);
}
export function totalMinutes(records) {
return records.reduce((minutes, session) => minutes + session.end - session.start, 0);
}
export function visibleRecords(catalog, stage) {
return catalog.filter(session => stage === 'all' || session.stage === stage).sort((a, b) =>
a.start - b.start || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0)
);
}
export function missingNotice(ids, catalog) {
const missing = ids.some(id => !catalog.some(session => session.id === id));
return missing ? 'Some earlier choices are no longer listed. Your available sessions are kept.' : '';
}
JS
Вернитесь в Work и обновите. Подготовленный D11 остаётся настоящей карточкой на 30 минут. Видимое уведомление объясняет, что некоторые прежние варианты больше не представлены; оно не выдумывает старое название для RETIRED-07 и не удаляет D11. Источник по-прежнему — публичный settings.initialIDs этой независимой части, не localStorage. Снова добавьте Paper Signals, получив два выбора и 75 минут.
Установите Stage в Garden. Программа теперь содержит Garden, а View my schedule по-прежнему содержит Garden First Light и Studio Paper Signals, 2 сессии и 75 минут. Верните Stage в All stages: Paper Signals всё ещё Added. Удалите её через My schedule и убедитесь, что ушёл только этот ID. Уведомление об удалённом синтетическом ID может остаться; оно не добавляет карточек или минут. Обычное обновление возвращает первую часть к подготовленным начальным ID, поскольку сохранение ещё не реализовано.
Попробуйте Tab и Enter для выбора сцены, навигационных ссылок и доступных Add/Remove. На узком экране View my schedule остаётся в навигации дня, а результат Add появляется рядом с нажатой карточкой. Сравнивайте название, сцену, время и описание, не только количество. Сохраните видимый фокус и осмысленные подписи подготовленного рендерера. Запустите итоговую проверку шага. Теперь выбор согласован с текущими данными; следующая независимая работа добавит конфликты времени и восстановление браузера.
Чтобы воспроизвести пример ниже, вернитесь на отдельную страницу Work и обычно обновите её. Это возвращает независимый начальный выбор Garden First Light и уведомление об удалённом ID. Установите Stage в All stages. Если Paper Signals ещё не Added, нажмите её Add to schedule. Затем сравните две текущие карточки и сводку на 75 минут с отдельным уведомлением о недоступном выборе. Удалённый ID не создаёт карточек или минут.

Резюме
Вы построили выбор в React на стабильных ID, сопоставили его с текущим каталогом и получили карточки, количество и суммарное время. Одноимённые записи остались различимыми, фильтрация сохранила выбор, а удалённый синтетический ID получил понятное объяснение без потери доступных сессий.
Эта часть намеренно хранит выбор только в памяти и не предотвращает пересечения по времени. Следующая работа независимо запускается с подготовленным выбором и добавляет проверку конфликтов и браузерное хранение в пределах одного источника.
