Mantener una única fuente de verdad para la agenda

JavaScriptBeginner
Practicar Ahora

Introducción

Daylight Festival ofrece seis sesiones en tres escenarios. Dos sesiones pueden tener el mismo título, por lo que tu agenda personal debe recordar la identidad real de cada una. Conectarás una página React adaptable ya preparada con una única selección y derivarás sus tarjetas, cantidad y minutos totales a partir del programa actual. Filtrar el programa no debe perder una elección anterior.

Necesitas conocer componentes de React, JSX, props, useState, eventos, renderizado condicional, map/filter, claves de listas y entradas controladas, además de arrays, objetos y funciones de JavaScript y HTML/CSS básicos. Este laboratorio enseña estado derivado, búsqueda de registros actuales, actualizaciones inmutables de la selección, reducción de duraciones y avisos sobre ID ausentes. Empieza de forma independiente; no necesita archivos de una VM anterior. Los 55 minutos son una estimación del autor, no un tiempo medido con principiantes. Todas las sesiones e identidades son sintéticas.

Usa Workspace para la terminal y Festival para los enlaces a páginas reales. En este laboratorio edita únicamente /home/labex/project/festival-schedule/work/src/selection.js y src/App.jsx. Conserva src/settings.js, src/main.jsx, src/conflicts.js, src/storage.js, el renderizador y los estilos preparados de shared, Study, las dependencias y la configuración del servicio. Este laboratorio no exige decisiones sobre conflictos ni almacenamiento del navegador. El ejemplo Study completo ya incluye esos comportamientos preparados; los construirás en el siguiente laboratorio. Las etiquetas del sitio permanecen en inglés.

Resolver las elecciones a sesiones actuales

En este paso usarás un ejemplo normal y después convertirás ID estables en los registros reales del programa actual.

En Festival, elige Open Study schedule in a new page. Es un enlace real a una página independiente del navegador, fuera de la pestaña incrustada del producto. Realiza las acciones del sitio en esa nueva página. Usa Tab para llegar a Add to schedule de First Light, Garden, 10:00–10:30, y pulsa Enter. Continúa hasta Paper Signals, Studio, 10:30–11:15, y añádela. Elige View my schedule: aparecen las dos tarjetas reales y 2 sessions · 75 minutes. El enlace de navegación del día salta al encabezado de la agenda; Back to the programme vuelve al catálogo. Este ejemplo tiene elecciones propias y no es tu trabajo ni un resultado de evaluación. El diseño, los mensajes de las tarjetas y la navegación ya están preparados; construirás la lógica de selección que los conecta.

Cierra Study. En la página de entrada Festival del producto, elige Open Work schedule in a new page y déjala abierta. Está incompleta. Cambia a la pestaña Workspace del producto para editar archivos. Cambiar una pestaña del producto es distinto de seguir un enlace del sitio, un ancla de la página o el botón Atrás del navegador. El comando preparado de Workspace entra en el directorio de trabajo; el comando visible siguiente también hace explícita la ruta inicial. cd cambia el directorio de esta terminal, una ruta absoluta empieza por /, pwd imprime la ruta actual y cat muestra un archivo pequeño sin modificarlo.

cd /home/labex/project/festival-schedule/work
pwd
cat src/settings.js

El catálogo usa ID como D11 y D13. Ambos se llaman First Light, pero su escenario y horario son distintos. El settings.initialIDs preparado contiene D11 y RETIRED-07: una pequeña selección inicial sintética proporcionada para este laboratorio, no almacenamiento de una VM anterior. D11 sigue existiendo; RETIRED-07 ya no. Solo un registro actual debe convertirse en tarjeta. El ID ausente recibirá una explicación explícita en el paso 3.

Un pequeño error en el nombre de archivo es recuperable. El siguiente comando escribe mal selection a propósito. Debe indicar que el archivo no existe y terminar con estado distinto de cero; no cambia nada:

cat src/selction.js

Lee el error, compara la escritura y usa la ruta correcta:

cat src/selection.js

Contiene funciones vacías. currentRecords seleccionará registros del catálogo actual por ID, no por título ni posición del array. filter crea un array nuevo; includes comprueba si se eligió un ID. Como los ID del catálogo son únicos, repetir uno no puede crear otra tarjeta. Ordena el nuevo array por inicio numérico y, en caso de empate, por ID ASCII. El || del comparador usa la comparación de ID solo cuando la diferencia de tiempos es cero. condition ? first : second elige entre dos valores. El catálogo original se conserva.

Guarda un archivo completo mediante un here-document. En cat > path <<'JS', > reemplaza ese archivo, << proporciona las líneas siguientes como entrada y JS entre comillas impide que el shell expanda el JavaScript. El JS final ocupa su propia línea. Estos comandos son una forma de editar desde la terminal; también puedes editar los mismos archivos en WebIDE y guardarlos con Ctrl+S. Las otras tres funciones quedan incompletas para los siguientes pasos.

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

Vuelve a la página Work independiente y actualiza el navegador normalmente. View my schedule muestra ahora la tarjeta real de Garden First Light, 10:00–10:30, con su descripción actual. No debe inventarse una tarjeta para RETIRED-07. El resumen conserva sus valores iniciales y Add/Remove aún no están conectados; el siguiente paso los completa. Localiza en el archivo la comparación con session.id: cambiar el orden del catálogo no debe cambiar qué objeto significa D11.

Si el navegador muestra un error de módulo o sintaxis, vuelve a Workspace, ejecuta pwd y cat src/selection.js, compara las importaciones, nombres de funciones, comillas y llaves con el bloque completo y guarda la corrección en esa ruta exacta. Después vuelve a Work y actualiza normalmente. No restablezcas la VM ni reinstales paquetes para corregir un nombre de archivo o un error de JavaScript. Ejecuta la comprobación del paso cuando la tarjeta actual sea visible.

Derivar el resumen y actualizar una sola selección

En este paso conectarás Add y Remove con una única lista de ID y calcularás su cantidad y duración.

Empieza en Workspace, en /home/labex/project/festival-schedule/work. Una cantidad o duración guardada por separado puede dejar de corresponder con las tarjetas al eliminar una. Mantén selectedIDs como estado de selección y deriva los registros actuales en cada renderizado. Su longitud da la cantidad. reduce combina registros en un solo valor: empieza con cero minutos y suma cada end - start. Los horarios preparados son minutos enteros de una misma fecha local; no necesitas analizar Date ni convertir zonas horarias. D11 dura 30 minutos y D12, 45: juntas suman 75. Los intervalos entre sesiones no son tiempo de asistencia.

Reemplaza selection.js con la misma búsqueda por ID y la reducción completa. El filtro y la explicación de ID ausentes siguen siendo funciones iniciales para el paso 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

Conecta ahora el componente React. useState proporciona cada valor de estado y su función de actualización. Los ID seleccionados, el escenario visible y los mensajes tienen responsabilidades diferentes; cambiar de escenario no debe reemplazar la lista de ID. Las tarjetas elegidas y el resumen son valores calculados, no estados adicionales.

La función de actualización ids => [...ids, id] recibe el array de ID actual y crea uno nuevo mediante la expansión .... La eliminación crea un array nuevo con filter; elimina el ID coincidente, no todas las sesiones con el mismo título. Comprobar si un ID ya está seleccionado impide duplicados. find obtiene el registro real del ID pulsado. feedback es un pequeño objeto que se pasa al renderizador preparado: sessionID sitúa el resultado de Add junto a esa tarjeta y text contiene el mensaje legible. El renderizador mantiene visible el mensaje sin mover el foco del teclado a una región de estado en vivo.

clock procede del View.jsx preparado y da formato a los minutos enteros para mostrarlos. describe combina título, escenario y horario para distinguir sesiones homónimas. Todas las props siguientes proceden de estado o registros derivados concretos. Reemplaza por completo 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

Vuelve a Work y actualiza. La selección sintética inicial produce 1 session · 30 minutes y la tarjeta Garden First Light. Añade Paper Signals: pasa a 2 sesiones y 75 minutos. Añade First Light de Loft, 12:00–12:30: deben conservarse ambas First Light con escenario y horario distintos, dando 3 sesiones y 105 minutos. Elimina únicamente Garden First Light. Permanecen Paper Signals y la First Light posterior de Loft: 2 sesiones y 75 minutos. Esto también demuestra que la duración suma asistencia, no el tiempo transcurrido desde el primer inicio hasta el último final.

El renderizador proporcionado utiliza el ID de cada sesión como clave React. Las claves siguen la identidad al cambiar una lista; no sustituyen el estado de selección. El botón Add se convierte en Added y elegir de nuevo no debe crear otra copia. En esta unidad, el control de escenario aún usa una función inicial y todavía no se impiden solapamientos de forma deliberada. No afirmes que cualquier combinación de elecciones ya constituye una agenda sin conflictos. Ejecuta la comprobación del paso cuando funcionen las relaciones entre tarjetas y resumen.

Filtrar el programa sin perder elecciones

En este paso completarás el filtro por escenario y explicarás un ID anterior no disponible, conservando las sesiones válidas.

Empieza en Workspace. visibleRecords filtra únicamente el array del programa mediante stage === 'all' o un escenario coincidente y ordena su nuevo array por inicio e ID. Nunca modifica selectedIDs. missingNotice pregunta si algún ID elegido no tiene registro actual. some devuelve true si al menos un elemento cumple una condición; el some interior busca un ID actual coincidente y ! significa que no lo hay. El aviso es un mensaje normal, no un error que descarte toda la agenda. La función puede devolver una cadena vacía cuando no falta nada.

Guarda el archivo completo de funciones auxiliares:

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

Vuelve a Work y actualiza. El D11 preparado sigue siendo una tarjeta real de 30 minutos. Un aviso visible explica que algunas elecciones anteriores ya no están en el programa; no inventa un título obsoleto para RETIRED-07 ni elimina D11. La fuente sigue siendo el settings.initialIDs público de esta unidad independiente, no localStorage. Añade otra vez Paper Signals para tener dos elecciones y 75 minutos.

Establece Stage en Garden. El programa contiene ahora las sesiones de Garden, mientras que View my schedule sigue mostrando Garden First Light y Studio Paper Signals, 2 sesiones y 75 minutos. Devuelve Stage a All stages: Paper Signals sigue como Added. Elimínala desde My schedule y confirma que sale únicamente ese ID. El aviso del ID sintético retirado puede permanecer; no aporta tarjetas ni minutos. Al actualizar normalmente, esta primera unidad vuelve a sus ID iniciales preparados porque aún no se ha implementado persistencia.

Prueba Tab y Enter en el control de escenario, los enlaces de navegación y los botones Add/Remove disponibles. En una pantalla estrecha, View my schedule permanece en la navegación del día y el resultado de Add aparece junto a la tarjeta pulsada. Compara título, escenario, horario y descripción, no solo la cantidad. Conserva el indicador de foco visible y las etiquetas significativas del renderizador preparado. Ejecuta la última comprobación. Ya tienes una selección consistente y una vista de datos actuales; el siguiente laboratorio independiente añade conflictos temporales y restauración del navegador.

Para reproducir la referencia siguiente, vuelve a la página Work independiente y actualiza normalmente. Esto restaura la elección inicial independiente Garden First Light y el aviso del ID retirado. Establece Stage en All stages. Si Paper Signals no aparece como Added, pulsa su botón Add to schedule. Compara entonces las dos tarjetas actuales y el resumen de 75 minutos con el aviso separado sobre la elección no disponible. El ID retirado no crea tarjetas ni minutos.

Elecciones actuales y aviso de una elección no disponible

Resumen

Has construido una selección React basada en ID estables, la has resuelto contra el catálogo actual y has derivado sus tarjetas, cantidad y minutos programados. Los registros con el mismo título se mantuvieron separados, el filtrado conservó las elecciones y un ID sintético retirado recibió una explicación clara sin perder sesiones válidas.

Esta unidad mantiene intencionadamente las elecciones en memoria y no impide sesiones solapadas. El siguiente laboratorio empieza de forma independiente con este comportamiento de selección preparado y enseña las decisiones sobre conflictos y el almacenamiento del navegador en el mismo origen.