Manter uma única fonte de verdade para a agenda

JavaScriptBeginner
Pratique Agora

Introdução

O Daylight Festival tem seis sessões em três palcos. Duas sessões podem ter o mesmo título, então sua agenda pessoal deve lembrar as identidades reais das sessões. Você conectará uma página React responsiva já preparada a uma única seleção e derivará seus cartões, quantidade e minutos totais do programa atual. Filtrar o programa não deve perder uma escolha anterior.

Você precisa conhecer componentes React, JSX, props, useState, eventos, renderização condicional, map/filter, chaves de listas e entradas controladas, além de arrays, objetos e funções JavaScript e HTML/CSS básico. Este laboratório ensina estado derivado, busca de registros atuais, atualizações imutáveis da seleção, redução de durações e avisos de IDs ausentes. Ele começa de forma independente, sem arquivos de uma VM anterior. Os 55 minutos são uma estimativa do autor, não um tempo medido com iniciantes. Todas as sessões e identidades são sintéticas.

Use Workspace para o terminal e Festival para os links de páginas reais. Neste laboratório, edite somente /home/labex/project/festival-schedule/work/src/selection.js e src/App.jsx. Preserve src/settings.js, src/main.jsx, src/conflicts.js, src/storage.js, o renderizador e os estilos preparados de shared, Study, dependências e configuração do serviço. Decisões de conflito e armazenamento do navegador não são requisitos aqui. O exemplo Study completo já os contém como comportamentos preparados; você os construirá no próximo laboratório. Os rótulos do site continuam em inglês.

Associar escolhas às sessões atuais

Nesta etapa, use primeiro um exemplo normal e depois transforme IDs estáveis nos registros reais do programa atual.

Em Festival, escolha Open Study schedule in a new page. É um link real para uma página independente do navegador, fora da aba incorporada do produto. Faça as ações do site nessa nova página. Use Tab para chegar a Add to schedule de First Light, Garden, 10:00–10:30, e pressione Enter. Continue até Paper Signals, Studio, 10:30–11:15, e adicione a sessão. Escolha View my schedule: aparecem os dois cartões reais e 2 sessions · 75 minutes. O link de navegação do dia salta para o título da agenda; Back to the programme retorna ao catálogo. Esse exemplo tem escolhas próprias e não é seu trabalho nem um resultado de avaliação. Layout, mensagens dos cartões e navegação já estão preparados; você construirá a lógica de seleção ao redor deles.

Feche Study. Na página de entrada Festival do produto, escolha Open Work schedule in a new page e mantenha-a aberta. Ela está incompleta. Mude para a aba Workspace do produto para editar arquivos. Trocar uma aba do produto é diferente de um link do site, uma âncora ou o botão Voltar do navegador. O comando preparado de Workspace entra no diretório de trabalho; o comando visível abaixo também torna explícito o caminho inicial. cd muda o diretório deste terminal, um caminho absoluto começa com /, pwd imprime o caminho atual e cat mostra um arquivo pequeno sem alterá-lo.

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

O catálogo usa IDs como D11 e D13. Ambas as sessões são First Light, mas os palcos e horários diferem. O settings.initialIDs preparado contém D11 e RETIRED-07: uma pequena seleção inicial sintética fornecida para este laboratório, não armazenamento de uma VM anterior. D11 ainda existe; RETIRED-07 não. Apenas um registro atual deve virar cartão. O ID ausente receberá uma explicação explícita na etapa 3.

Um pequeno erro no nome de arquivo pode ser corrigido. O próximo comando escreve selection errado de propósito. Ele deve informar que o arquivo não existe e terminar com status diferente de zero; nada é alterado:

cat src/selction.js

Leia o erro, compare a grafia e use o caminho correto:

cat src/selection.js

O arquivo contém funções vazias. currentRecords selecionará registros do catálogo atual por ID, não por título ou posição no array. filter cria um novo array; includes verifica se um ID foi escolhido. Como os IDs do catálogo são únicos, repetir um ID não pode duplicar um cartão. Ordene o novo array pelo início numérico e depois pelo ID ASCII em caso de empate. O || do comparador só usa a comparação de IDs quando a diferença de tempo é zero. condition ? first : second escolhe entre dois valores. O catálogo original é preservado.

Salve um arquivo completo com um here-document. Em cat > path <<'JS', > substitui esse arquivo, << fornece as linhas seguintes como entrada e JS entre aspas impede que o shell expanda o JavaScript. O JS final fica sozinho na linha. Esses comandos são uma forma de editar pelo terminal; você também pode editar os mesmos arquivos no WebIDE e salvar com Ctrl+S. As outras três funções continuam incompletas para as próximas etapas.

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

Volte à página Work independente e atualize normalmente o navegador. View my schedule agora mostra a Garden First Light real, 10:00–10:30, com sua descrição atual. Não invente um cartão para RETIRED-07. O resumo ainda tem os valores iniciais e Add/Remove não estão conectados; a próxima etapa os completa. No arquivo, identifique a comparação com session.id: mudar a ordem do catálogo não deve mudar qual objeto D11 representa.

Se o navegador mostrar erro de módulo ou sintaxe, volte a Workspace, execute pwd e cat src/selection.js, compare imports, nomes de funções, aspas e chaves com o bloco completo e salve a correção no caminho exato. Depois volte a Work e atualize normalmente. Não redefina a VM nem reinstale pacotes para corrigir um nome de arquivo ou erro de JavaScript. Execute a verificação da etapa quando o cartão atual estiver visível.

Derivar o resumo e atualizar uma única seleção

Nesta etapa, conecte Add e Remove a uma única lista de IDs e calcule sua quantidade e duração.

Comece em Workspace, em /home/labex/project/festival-schedule/work. Uma quantidade ou duração armazenada separadamente pode ficar incorreta quando um cartão é removido. Mantenha selectedIDs como estado da seleção e derive os registros atuais a cada renderização. O tamanho desse array de registros fornece a quantidade. reduce reúne registros em um valor: comece com zero minutos e some cada end - start. Os horários preparados são minutos inteiros de uma mesma data local; não é necessário interpretar Date nem converter fusos. D11 dura 30 minutos e D12, 45: juntos, 75. Intervalos entre sessões não contam como participação.

Substitua selection.js pela mesma busca por ID com a redução completa. O filtro e a explicação de IDs ausentes ainda ficam como funções iniciais para a etapa 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

Conecte agora o componente React. useState fornece cada valor de estado e sua função de atualização. IDs selecionados, palco visível e mensagens têm responsabilidades diferentes; mudar o palco não deve substituir a lista de IDs. Os cartões escolhidos e o resumo são valores calculados, não estados extras.

A atualização ids => [...ids, id] recebe o array atual de IDs e cria um novo usando a expansão .... A remoção cria um novo array com filter; remove o ID correspondente, não todas as sessões homônimas. Verificar se um ID já foi selecionado impede duplicatas. find obtém o registro real do ID clicado. feedback é um pequeno objeto passado ao renderizador preparado: sessionID coloca o resultado de Add ao lado daquele cartão e text é sua mensagem legível. O renderizador mantém a mensagem visível sem mover o foco do teclado para uma região de status dinâmica.

clock vem do View.jsx preparado e formata minutos inteiros para exibição. describe combina título, palco e horário para distinguir sessões homônimas. Todas as props abaixo têm uma origem concreta no estado ou nos registros derivados. Substitua App.jsx por completo:

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

Volte a Work e atualize. A seleção sintética inicial produz 1 session · 30 minutes e o cartão Garden First Light. Adicione Paper Signals: são 2 sessões e 75 minutos. Adicione First Light de Loft, 12:00–12:30: ambas as First Light devem permanecer com palco e horário distintos, totalizando 3 sessões e 105 minutos. Remova apenas Garden First Light. Permanecem Paper Signals e a First Light mais tarde de Loft: 2 sessões e 75 minutos. Isso também prova que a duração soma a participação, não todo o tempo do primeiro início ao último fim.

O renderizador fornecido usa o ID de cada sessão como key React. As chaves acompanham a identidade quando a lista muda; não substituem seu estado de seleção. O botão Add vira Added e repetir a seleção não deve criar outra cópia. Nesta unidade, o controle de palco ainda usa uma função inicial e as sobreposições não são impedidas deliberadamente. Não afirme que qualquer escolha possível já é uma agenda sem conflitos. Execute a verificação quando cartões e resumo estiverem coerentes.

Filtrar o programa sem perder escolhas

Nesta etapa, termine o filtro de palco e explique um ID anterior indisponível, preservando as sessões válidas.

Comece em Workspace. visibleRecords filtra apenas o array do programa por stage === 'all' ou pelo palco correspondente e ordena seu novo array por início e ID. Nunca edita selectedIDs. missingNotice verifica se algum ID escolhido não tem registro atual. some retorna true quando pelo menos um elemento atende à condição; o some interno procura um ID atual correspondente e ! significa que não encontrou. O aviso é uma mensagem normal, não um erro que descarta toda a agenda. A função pode retornar uma string vazia quando nada está ausente.

Salve o arquivo auxiliar completo:

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

Volte a Work e atualize. O D11 preparado continua sendo um cartão real de 30 minutos. Um aviso visível explica que algumas escolhas anteriores não são mais oferecidas; não inventa um título antigo para RETIRED-07 nem remove D11. A origem ainda é o settings.initialIDs público desta unidade independente, não localStorage. Adicione Paper Signals novamente para ter duas escolhas com 75 minutos.

Defina Stage como Garden. O programa agora contém as sessões de Garden, enquanto View my schedule ainda contém Garden First Light e Studio Paper Signals, 2 sessões e 75 minutos. Volte Stage para All stages: Paper Signals continua Added. Remova-a em My schedule e confirme que apenas esse ID sai da lista. O aviso do ID sintético retirado pode continuar; ele não contribui com cartão nem minutos. Uma atualização normal retorna esta primeira unidade aos IDs iniciais preparados, pois a persistência ainda não foi implementada.

Experimente Tab e Enter no controle de palco, links de navegação e botões Add/Remove disponíveis. Em uma tela estreita, View my schedule permanece na navegação do dia e o resultado de Add aparece junto ao cartão clicado. Compare título, palco, horário e descrição, não só a quantidade. Preserve o foco visível e os rótulos significativos do renderizador preparado. Execute a última verificação. Você já tem uma seleção consistente e uma visão dos dados atuais; o próximo laboratório independente acrescenta conflitos temporais e restauração do navegador.

Para reproduzir a referência abaixo, volte à página Work independente e atualize normalmente. Isso restaura a escolha inicial independente Garden First Light e o aviso do ID retirado. Defina Stage como All stages. Se Paper Signals ainda não estiver Added, escolha seu botão Add to schedule. Compare os dois cartões atuais e o resumo de 75 minutos com o aviso separado de escolha indisponível. O ID retirado não cria cartão nem minutos.

Escolhas atuais e aviso de uma escolha indisponível

Resumo

Você construiu uma seleção React baseada em IDs estáveis, encontrou seus registros no catálogo atual e derivou cartões, quantidade e minutos programados. Registros com o mesmo título permaneceram distintos, os filtros preservaram escolhas e um ID sintético retirado recebeu uma explicação clara sem perder sessões válidas.

Esta unidade mantém as escolhas intencionalmente apenas em memória e não impede sobreposições. O próximo laboratório começa de forma independente com esse comportamento de seleção preparado, depois ensina decisões de conflito e armazenamento do navegador na mesma origem.