Konflikte erklären und die Auswahl merken

JavaScriptBeginner
Jetzt üben

Einführung

Ein Festivalzeitplan ist nur nützlich, wenn ein Besucher tatsächlich an den gewählten Programmpunkten teilnehmen kann. Daylight Festival hat angrenzende, kompatible Termine und überschneidende Termine auf unterschiedlichen Bühnen. Du lässt beim Hinzufügen den tatsächlichen Konflikt erklären und merkst dir anschließend nur IDs, damit normales Aktualisieren Datensätze des aktuellen Programms wiederherstellt.

Du benötigst die React- und JavaScript-Voraussetzungen aus dem ersten Lab sowie dessen Auswahlmuster mit aktuellen Datensätzen und abgeleiteter Zusammenfassung. Diese Einheit startet unabhängig mit vorbereitetem Auswahlverhalten; frühere VMs, Dateien oder Browserauswahlen sind nicht nötig. Du lernst halboffene Intervalle, die Prüfung aller Auswahlen, JSON-Wiederherstellung, den Geltungsbereich von localStorage und den minimalen useEffect-/Lazy-Initializer-Lebenszyklus, der gespeicherte Auswahlen vor Überschreiben schützt. Die 60 Minuten sind eine Schätzung des Autors einschließlich Lesen und normaler Fehler. Backend, Buchungen und geräteübergreifende Speicherung sind nicht beteiligt.

Nutze Workspace unter /home/labex/project/festival-schedule/work und Festival für die echten Links zu eigenständigen Seiten. Bearbeite nur src/App.jsx, src/conflicts.js und src/storage.js; src/selection.js ist die vollständig vorbereitete Hilfsdatei aus dem ersten Lab. Erhalte Einstellungen/Katalog, Renderer/Stile, Einstiegspunkt, Study, Abhängigkeiten und Dienstkonfiguration. Study verwendet einen anderen Speicherschlüssel und zählt niemals als deine Arbeit. Website-Beschriftungen bleiben englisch.

Tatsächliche Überschneidungen ablehnen und angrenzende Termine erlauben

In diesem Schritt berücksichtigt jedes Hinzufügen alle vorhandenen Auswahlen und benennt einen tatsächlichen Konflikt.

Öffne in Festival den Link Open Study schedule in a new page. Verwende zuerst das normale Beispiel: Füge Garden First Light, 10:00–10:30 und Studio Paper Signals, 10:30–11:15 hinzu. Wähle View my schedule und lies die beiden Karten und 75 Minuten. Probiere dann Studio Colour Routes, 10:50–11:10. Eine Meldung neben diesem Kandidaten benennt Paper Signals; die beiden ausgewählten Datensätze und 75 Minuten bleiben unverändert. Der vorbereitete Renderer hält die Kartenmeldung sichtbar, auch bei schmaler Anzeige. Dies ist das bereitgestellte Beispiel, nicht deine Implementierung.

Schließe Study und kehre zum Festival-Einstieg des Produkts zurück. Öffne Open Work schedule in a new page und lasse die Seite offen. Die Auswahlhilfen sind vorbereitet, aber die anfängliche Add-Funktion lehnt keine Überschneidungen ab und hält die Auswahl nur im Arbeitsspeicher. Wechsle zum Bearbeiten zu Workspace. Das Startverzeichnis ist /home/labex/project/festival-schedule/work; verwende bei Bedarf die aus dem ersten Lab bekannten Befehle cd, pwd und cat, um einen Pfad zu bestätigen. Installiere wegen eines Dateinamenfehlers keine Pakete neu.

Stelle einen Programmpunkt als halboffenes Intervall [start, end) dar. Sein Anfang gehört dazu, sein Ende ist für den nächsten frei. Alle Zeiten sind ganzzahlige Minuten an einem festgelegten lokalen Datum. Zum Beispiel entspricht 10:30 der Zahl 630. Garden First Light ist [600, 630), Paper Signals ist [630, 675): Gleichheit bei 630 ist erlaubt. Colour Routes [650, 670) überschneidet Paper Signals, aber nicht den ersten ausgewählten Termin. Nur die erste Auswahl zu prüfen wäre falsch.

Die Überschneidungsbedingung lautet candidate.start < session.end && session.start < candidate.end. Beide strengen Vergleiche müssen zutreffen; < durch <= zu ersetzen würde angrenzende Termine fälschlich ablehnen. find prüft die ausgewählten Datensätze, bis ein Konflikt gefunden wird, und gibt dieses tatsächliche Objekt zurück, andernfalls undefined. Bei mehreren Konflikten darf jeder tatsächliche Konflikt zurückgegeben werden; die Fehlermeldung muss ihn ehrlich identifizieren.

Speichere die Konflikthilfe mit dem zitierten Here-Dokument aus dem ersten Lab. Das abschließende JS steht allein, und > ersetzt die angegebene Datei:

cat > src/conflicts.js <<'JS'
export function findConflict(candidate, selected) {
  return selected.find(session =>
    candidate.start < session.end && session.start < candidate.end
  );
}
JS

Verbinde sie nun vor dem Aktualisieren des ID-Zustands. Die folgende Komponente importiert die gerade geschriebene Hilfe. Sie ermittelt ausgewählte Datensätze aus der gesamten aktuellen Auswahl, nicht aus dem nach Bühne gefilterten Programm. Eine Ablehnung setzt die Rückmeldung für den angeklickten Kandidaten und kehrt zurück, ohne IDs oder Summen zu verändern. Titel, Bühne und Zeit unterscheiden die beiden First Light. Andere klare Formulierungen sind gültig; entscheidend ist die Beziehung zwischen dem Kandidaten und einem tatsächlich ausgewählten Konflikt. Die übrigen Zustände und abgeleiteten Hilfen folgen dem vorherigen Lab.

cat > src/App.jsx <<'JS'
import React, {useState} from 'react';
import View, {clock} from '../../shared/View.jsx';
import {findConflict} from './conflicts.js';
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;
    const conflict = findConflict(session, selected);
    if (conflict) {
      setFeedback({sessionID: id, text: 'Cannot add ' + describe(session) + '. It overlaps ' + describe(conflict) + '.'});
      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

Kehre zu Work zurück und aktualisiere normal. Das unabhängige initialIDs dieser Einheit ist leer. Füge Garden First Light und Paper Signals für 75 Minuten hinzu. Probiere Colour Routes: Paper Signals ist das zweite ausgewählte Objekt, und nach der Ablehnung müssen 75 Minuten bleiben. Wähle Stage: Loft und probiere First Light, 10:15–10:45. Der tatsächliche Konflikt Garden First Light ist durch den Filter im Programm verborgen, bleibt aber in My schedule. Die neue Ablehnung muss diesen Garden-Termin benennen und dieselben beiden Auswahlen behalten. Ein Filter darf die Konfliktregel nicht abschwächen.

Stelle Stage wieder auf All stages. Füge Garden Small Stages, 11:15–11:45 hinzu: Dieser Termin grenzt an Paper Signals, also steigt die Summe auf 105 Minuten. Entferne Paper Signals über View my schedule. Garden First Light und Small Stages ergeben 60. Füge Colour Routes, 10:50–11:10 erneut hinzu: Jetzt passt es, mit 80 Minuten in der Reihenfolge First Light, Colour Routes, Small Stages. Die gleichnamige Loft First Light um 10:15 kollidiert weiterhin mit Garden First Light; verwende sie nicht als den nach Entfernung eines Konflikts erfolgreich hinzugefügten Termin.

Bei schmaler Anzeige bietet die Tagesnavigation My schedule ohne langes Suchen im Programm, und die Ablehnung steht neben der angeklickten Karte. Bediene einen Kandidaten tatsächlich mit Tab/Enter und prüfe, dass die Meldung lesbar bleibt, ohne den Fokus in einen Statusbereich zu verschieben. Bei einem Code-Tippfehler untersuche die gespeicherten Dateien src/conflicts.js und src/App.jsx, stelle den vollständigen betroffenen Block wieder her und aktualisiere. Die Auswahl ist weiterhin nur im Arbeitsspeicher; Aktualisieren kann sie bis Schritt 2 zurücksetzen. Starte die Konfliktprüfung.

IDs vor dem Synchronisieren des Speichers wiederherstellen

In diesem Schritt stellst du eine gespeicherte ID-Liste vor dem Schreiben wieder her und verarbeitest unbrauchbare oder veraltete gespeicherte Daten.

Beginne in Workspace. localStorage ist Browserspeicher für einen Ursprung aus Schema, Host und Port. Pfade desselben Ursprungs teilen den Bereich; nutze deshalb den kurs- und einheitsspezifischen Schlüssel settings.storageKey statt eines allgemeinen Schlüssels data. Study besitzt einen anderen Schlüssel. Rufe weder clear() auf noch entferne andere Schlüssel. Eine URL einer neuen VM, ein frischer Browserkontext, ein anderer Browser oder ein anderes Gerät müssen diese Auswahl nicht enthalten. Browserrichtlinien können den Speicherzugriff verweigern. Dies ist eine Hilfe für dieselbe Browserseite, keine Reservierung, Sicherung oder Zusage dauerhafter Aufbewahrung.

Speicherwerte sind Zeichenfolgen. JSON.stringify(ids) wandelt das ID-Array in einen JSON-String um; JSON.parse(raw) liest ihn wieder. Speichere nur IDs wie ["D11","D12"], keine möglicherweise veraltenden Titel- und Zeitkopien. Ordne wiederhergestellte IDs dem aktuellen Katalog zu und leite dieselben Karten, Anzahl und Minuten wie zuvor ab. Die Reihenfolge des gespeicherten Arrays ist keine Anforderung an die Anzeige: My schedule verwendet aktuelle Startzeit und ID.

Es gelten diese öffentlichen Wiederherstellungsregeln:

  • Ein fehlender Schlüssel verwendet das vorbereitete settings.initialIDs dieser Einheit. Hier ist das Array leer. Die unabhängige Challenge besitzt eigene, ausdrücklich bereitgestellte Anfangs-IDs.
  • Ungültiges JSON, ein Wert ohne Array-Form oder ein Array mit einem Nicht-String-Element ergeben einen leeren nutzbaren Zeitplan mit lesbarem Hinweis. Ein leeres Array ist gültig.
  • Wiederholte String-IDs erzeugen keine doppelten Karten. Im Katalog fehlende IDs werden mit Hinweis übersprungen; gültige kompatible Auswahlen behalten ihre aktuellen Details.
  • Enthält die gespeicherte Liste selbst Konflikte, verarbeite sie in ihrer ursprünglichen gespeicherten Eingabereihenfolge. Behalte die zuerst angetroffenen kompatiblen Auswahlen und erkläre das Überspringen von Überschneidungen. Dies ist dieselbe Zulassungsregel, keine Zeitplanoptimierung.
  • Wird das Lesen verweigert, ermögliche einen Besuch im Arbeitsspeicher mit Hinweis. Schlägt das Schreiben fehl, behalte die aktuelle Auswahl und erkläre, dass sie nicht gespeichert werden konnte. In diesem Fall wird keine Wiederherstellung nach Aktualisieren versprochen.

Die folgende Hilfe führt einige JavaScript-Werkzeuge ein. try führt möglicherweise fehlschlagenden Code aus; catch bietet eine lesbare Wiederherstellung, statt die Seite abstürzen zu lassen. raw === null kennzeichnet einen fehlenden Schlüssel. Array.isArray und typeof prüfen die öffentliche ID-Listenform; throw new Error(...) leitet eine ungültige Form in denselben Fehlerpfad. for (const id of saved) besucht IDs in ihrer Reihenfolge, continue überspringt eine und push hängt an neue lokale Arrays an. Diese temporären Arrays sind kein React-Zustand. join(' ') verbindet die zutreffenden Hinweissätze. Der Lesevorgang dieser Hilfsfunktion schreibt oder entfernt keine gespeicherten Daten.

cat > src/storage.js <<'JS'
import {catalog, settings} from './settings.js';
import {findConflict} from './conflicts.js';

export function readSaved() {
  let raw;
  try {
    raw = localStorage.getItem(settings.storageKey);
  } catch {
    return {ids: [], notice: 'Saved choices are unavailable. You can plan in memory for this visit.'};
  }
  try {
    const saved = raw === null ? settings.initialIDs : JSON.parse(raw);
    if (!Array.isArray(saved) || saved.some(id => typeof id !== 'string')) {
      throw new Error('Expected a list of session IDs');
    }
    const ids = [];
    const records = [];
    let missing = false;
    let conflicting = false;
    for (const id of saved) {
      if (ids.includes(id)) continue;
      const session = catalog.find(item => item.id === id);
      if (!session) {missing = true; continue;}
      if (findConflict(session, records)) {conflicting = true; continue;}
      ids.push(id);
      records.push(session);
    }
    const notices = [];
    if (missing) notices.push('Some saved sessions are no longer listed. Your available choices are kept.');
    if (conflicting) notices.push('Some saved choices overlapped. The earlier compatible choices are kept.');
    return {ids, notice: notices.join(' ')};
  } catch {
    return {ids: [], notice: 'Your saved list could not be read. Start a new schedule here.'};
  }
}

export function writeSaved(ids) {
  try {
    localStorage.setItem(settings.storageKey, JSON.stringify(ids));
    return true;
  } catch {
    return false;
  }
}
JS

Stelle nun den Anfangszustand sicher wieder her. useState(() => readSaved()) übergibt eine Funktion als verzögerten Initialisierer: React ruft sie beim Initialisieren dieser Komponente auf. Sie liefert {ids, notice}, ohne Speicher zu schreiben, einen anderen Schlüssel zu verändern oder Daten abzurufen. Halte diese Momentaufnahme in initial, initialisiere die Auswahl aus initial.ids und behalte den Hinweis als Rückmeldung. StrictMode kann während der Entwicklung einen Initialisierer mehrmals aufrufen, um Probleme zu erkennen; ein nur lesender Initialisierer lässt sich sicher wiederholen. Eine andere Implementierung mit ausdrücklicher Wiederherstellungssperre ist ebenfalls erlaubt, solange sie nicht vor der Wiederherstellung eine leere Liste schreibt.

useEffect wird aus React importiert und synchronisiert externen Speicher, nachdem ein Rendern übernommen wurde. Die Abhängigkeitsliste [selectedIDs] lässt die Synchronisierung bei Änderung dieses Arrays erneut laufen. StrictMode kann während der Entwicklung einen zusätzlichen Effect-Zyklus ausführen; dieselbe ID-Liste zu schreiben ist idempotent: Wiederholung lässt dasselbe gespeicherte Ergebnis zurück und erzeugt keine doppelte Auswahl. Auswahl, Sortierung, Anzahl und Konfliktentscheidungen bleiben normale Render- beziehungsweise Handler-Arbeit, keine Effects. Leer zu starten, sofort leer zu schreiben und erst später IDs zu laden würde einen tatsächlich gespeicherten Zeitplan löschen.

Ersetze die Komponente. readSaved und writeSaved stammen aus der gerade gespeicherten Hilfe; die übrigen Importe wurden in Schritt 1 verwendet. remembering zeigt der vorbereiteten Ansicht, dass diese Version Browserspeicherung versucht. Ein Schreibfehler erzeugt trotzdem den ausdrücklichen Hinweis auf die Nutzung nur im Arbeitsspeicher.

cat > src/App.jsx <<'JS'
import React, {useState, useEffect} from 'react';
import View, {clock} from '../../shared/View.jsx';
import {readSaved, writeSaved} from './storage.js';
import {findConflict} from './conflicts.js';
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 [initial] = useState(() => readSaved());
  const [selectedIDs, setSelectedIDs] = useState(initial.ids);
  const [stage, setStage] = useState('all');
  const [feedback, setFeedback] = useState({sessionID: null, text: initial.notice});
  const selected = currentRecords(selectedIDs, catalog);
  const visible = visibleRecords(catalog, stage);
  const minutes = totalMinutes(selected);

  useEffect(() => {
    if (!writeSaved(selectedIDs)) {
      setFeedback({sessionID: null, text: 'These choices work for this visit, but your browser could not save them.'});
    }
  }, [selectedIDs]);

  function add(id) {
    const session = catalog.find(item => item.id === id);
    if (!session || selectedIDs.includes(id)) return;
    const conflict = findConflict(session, selected);
    if (conflict) {
      setFeedback({sessionID: id, text: 'Cannot add ' + describe(session) + '. It overlaps ' + describe(conflict) + '.'});
      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)} remembering={true} />;
}
JS

Kehre zu Work zurück und aktualisiere normal. Die erste Aktualisierung nach dieser Codeänderung darf leer sein: Schritt 1 hat seine Auswahl im Arbeitsspeicher nie gespeichert. Füge Garden First Light und Paper Signals erneut hinzu und aktualisiere genau dieselbe URL auf derselben Browserseite. Beide aktuellen Datensätze und 75 Minuten müssen zurückkommen. Ein frischer unabhängiger Browserkontext sollte stattdessen mit der Anfangsauswahl dieser Einheit starten; behaupte nicht, diese private Auswahl werde durch eine URL geteilt.

Verwende anschließend die getrennte kontrollierte Übungsseite. Wähle Practice recovery in der Fußzeile; sie ersetzt Work im selben Tab. Lies die Warnung: Jede Übung ersetzt nur den Auswahlschlüssel dieser Einheit und damit die aktuellen Testauswahlen. Wähle Load unreadable JSON, dann Return to Work. Die normale Komponente muss einen leeren nutzbaren Zeitplan und einen lesbaren Wiederherstellungshinweis zeigen. Füge die Loft First Light von 12:00–12:30 hinzu und aktualisiere normal: Ihre aktuelle Karte und 30 Minuten kehren zurück. Dies beweist normale Nutzung nach einer Wiederherstellung, nicht nur die Anzeige eines Fehlertexts.

Kehre zu Practice recovery zurück, wähle Load retired IDs, dann Return to Work. Die sichtbare Vorgabe enthält D11, eine entfernte ID und D12. Beide verfügbaren Datensätze bleiben mit 75 Minuten und einer Erklärung der ausgelassenen früheren Auswahl. Hat eine bereinigte Liste die alte Eingabe ersetzt, muss eine spätere Aktualisierung denselben Hinweis nicht wiederholen. Verwende schließlich Load conflicting IDs: Die Vorgabe ist D11, D13, D12. D13 überschneidet das in der gespeicherten Eingabe vorher kommende D11; deshalb bleiben D11 und D12 mit Hinweis erhalten, nicht alle drei und auch kein leerer Zeitplan. Die Übungsseite ist ausdrücklich Training, kein unsichtbares Zurücksetzen im Besucherablauf.

Kehre zu Work zurück und bestätige, dass Add, Filter und Remove weiterhin auf die wiederhergestellten aktuellen Auswahlen wirken. Teste wie in Schritt 1 den gültig angrenzenden Small Stages und die Ablehnung von Colour Routes. Blockiert eine echte Browserrichtlinie den Speicher, behalte die ausdrückliche Meldung zur Nutzung nur im Arbeitsspeicher und benutze die Seite für diesen Besuch; öffne einen erlaubten Browserkontext, wenn du Speicherung über Aktualisierungen hinweg benötigst. Lösche keine fremden Browserdaten, um Erfolg zu erzwingen. Starte die letzte Prüfung, nachdem normale und Wiederherstellungspfade funktionieren.

Nach Entfernen und erneutem Hinzufügen ergeben die wiederhergestellten Karten First Light, Colour Routes und Small Stages 80 Minuten. Vergleiche ihre aktuellen Details und Add-Zustände:

Wiederhergestellte aktuelle Auswahl und abgeleitete Zusammenfassung

Bei schmaler Anzeige benennt die Ablehnung des frühen Loft-Kandidaten die bereits ausgewählte Garden-Veranstaltung neben seiner Schaltfläche; die Zeitplannavigation bleibt darüber erreichbar.

Lesbare Ablehnung neben dem Kandidaten bei schmaler Anzeige

Zusammenfassung

Du hast eine wahrheitsgetreue Teilnahmeregel gebaut: Jeder ausgewählte aktuelle Datensatz wird in die halboffene Überschneidungsprüfung einbezogen, angrenzende Termine sind erlaubt und das Entfernen eines Konflikts kann einen Kandidaten kompatibel machen. Anschließend hast du auf einen Schlüssel begrenzte Browserdaten aus IDs vor dem synchronisierten Schreiben wiederhergestellt, aktuelle Datensatzbeziehungen bewahrt und unlesbare, fehlende oder konfliktbehaftete gespeicherte Auswahlen verarbeitet.

Die Wiederherstellung gilt nur für denselben Browser-Ursprung und eine noch bestehende Kursumgebung mit erlaubtem Speicherzugriff. Die unabhängige Challenge verändert Programmdaten und Beziehungen mit denselben Mechanismen.