はじめに
Daylight Festival には、3 つのステージに 6 つのセッションがあります。同じタイトルのセッションもあるため、個人の予定表では実際のセッションの識別情報を保持する必要があります。用意されたレスポンシブな React ページを一つの選択データに結び付け、現在のプログラムからカード、件数、合計分数を導出します。プログラムを絞り込んでも、以前の選択を失ってはいけません。
React コンポーネント、JSX、props、useState、イベント、条件付きレンダリング、map/filter、リストの key、制御された入力に加え、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 の外に、独立したブラウザページを開く実際のリンクです。サイトの操作は新しいページで行います。Tab で First Light、Garden、10:00–10:30 の Add to schedule に移動し、Enter を押します。続いて Paper Signals、Studio、10:30–11:15 に進み、追加します。View my schedule を選ぶと、実際の 2 枚のカードと 2 sessions · 75 minutes が表示されます。その日のナビゲーションリンクは予定表の見出しへ移動し、Back to the programme はカタログに戻ります。この例には独立した選択があり、自分の成果物や採点結果ではありません。レイアウト、カードのフィードバック、ナビゲーションは準備済みで、その周囲の選択ロジックを構築します。
Study を閉じます。製品の Festival 入口ページで Open Work schedule in a new page を選び、開いたままにします。こちらは未完成です。ファイルを編集するには、製品の Workspace Tab に切り替えます。製品の Tab の切り替えは、サイトのリンク、ページ内アンカー、ブラウザの戻るボタンとは異なります。準備済みの Workspace コマンドは作業ディレクトリに移動しますが、以下の見えるコマンドでも開始パスを明示します。cd はこのターミナルのディレクトリを変更し、/ で始まるものは絶対パスです。pwd は現在のパスを、cat は小さなファイルの内容を変更せずに表示します。
cd /home/labex/project/festival-schedule/work
pwd
cat src/settings.js
カタログは D11、D13 などの ID を使います。どちらも 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 は、シェルが 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 を実行します。import、関数名、引用符、波括弧を完全なブロックと照合し、正確なパスに修正を保存してから Work に戻り、普通に再読み込みします。ファイル名や JavaScript の綴りを直すために VM をリセットしたりパッケージを再インストールしたりしないでください。現在のカードが見えたら、このステップのチェックを実行します。
概要を導出し、一つの選択を更新する
このステップでは Add と Remove を一つの ID リストに接続し、件数と所要時間を計算します。
Workspace の /home/labex/project/festival-schedule/work から始めます。件数や時間を別に保存すると、カードを削除した際に実際の状態とずれる可能性があります。選択状態は selectedIDs に保持し、レンダリングのたびに現在のレコードを導出します。その配列の長さが件数です。reduce は複数のレコードを一つの値にまとめます。ゼロ分から始め、各 end - start を加えます。用意された時間は同じローカル日付の整数分で、Date の解析やタイムゾーン変換は不要です。D11 は 30 分、D12 は 45 分なので、合計 75 分です。セッション間の空き時間は参加時間に含めません。
同じ ID 検索と完成した集約を含む selection.js に置き換えます。フィルターと欠損 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 配列を受け取り、スプレッド ... で新しい配列を作ります。削除は 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 分になります。Loft, 12:00–12:30 の First Light を追加します。二つの First Light は異なるステージと時刻で両方残り、3 セッション、105 分になります。Garden First Light だけを削除すると、Paper Signals と遅い Loft First Light が残り、2 セッション、75 分になります。これは、最初の開始から最後の終了までの経過時間ではなく、参加時間の合計であることも示します。
提供されたレンダラーは各セッション ID を React の key に使います。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 に戻ります。
ステージコントロール、ページ内ナビゲーション、利用可能な Add/Remove ボタンを Tab と Enter で操作してみます。狭い画面でも View my schedule は日のナビゲーションに残り、Add のフィードバックはクリックしたカードの隣に出ます。件数だけでなくタイトル、ステージ、時間、説明を比較してください。準備済みレンダラーの見えるフォーカス表示と意味のあるラベルを保ちます。最後のステップチェックを実行します。これで一貫した選択と現在データの表示ができました。次の独立ラボでは時間の重複とブラウザ復元を加えます。
下の参照状態に合わせるには、独立 Work ページに戻って普通に再読み込みします。独立した初期選択の Garden First Light と掲載終了 ID の通知に戻ります。Stage を All stages にします。Paper Signals がまだ Added でなければ、その Add to schedule を選びます。その後、現在の 2 枚のカードと 75 分の概要を、別に表示される利用不可選択の通知と比較します。掲載終了 ID はカードや分数を生みません。

まとめ
安定した ID を基に React の選択を構築し、現在のカタログのレコードに対応付けて、カード、件数、予定時間の合計を導出しました。同名のレコードを区別し、絞り込み時も選択を保持し、掲載終了した合成 ID については有効なセッションを失わずに明確な説明を表示しました。
この単元では意図的に選択をメモリ内だけに保持し、セッションの重複も防いでいません。次のラボはこの選択動作が用意された独立環境から始まり、時間の重複判定と同一オリジンのブラウザ保存を学びます。
