Explicar conflitos e lembrar escolhas

JavaScriptBeginner
Pratique Agora

Introdução

Uma agenda de festival só é útil se o visitante puder realmente participar das sessões escolhidas. O Daylight Festival tem sessões adjacentes compatíveis e sessões sobrepostas em palcos diferentes que não combinam. Você fará as adições explicarem o conflito real e depois lembrará apenas IDs para que uma atualização normal restaure os registros do programa atual.

Você precisa dos pré-requisitos de React e JavaScript do primeiro laboratório e do padrão de seleção com registros atuais e resumo derivado. Esta unidade começa de forma independente com esse comportamento preparado, sem precisar de VM, arquivos ou escolhas anteriores do navegador. Você aprenderá intervalos semiabertos, verificação de toda a seleção, recuperação de JSON, escopo de localStorage e o ciclo mínimo de useEffect/inicialização sob demanda necessário para não sobrescrever escolhas salvas. Seus 60 minutos são uma estimativa do autor que inclui leitura e erros comuns. Não há backend, reservas ou persistência entre dispositivos.

Use Workspace em /home/labex/project/festival-schedule/work e Festival para links reais de páginas independentes. Edite somente src/App.jsx, src/conflicts.js e src/storage.js; src/selection.js é o auxiliar completo preparado do primeiro laboratório. Preserve configurações/catálogo, renderizador/estilos, ponto de entrada, Study, dependências e configuração do serviço. Study usa outra chave e nunca conta como seu trabalho. Os rótulos do site continuam em inglês.

Rejeitar sobreposições reais e permitir sessões adjacentes

Nesta etapa, faça cada adição considerar todas as escolhas existentes e identificar uma sessão realmente conflitante.

Em Festival, abra Open Study schedule in a new page. Use primeiro o exemplo normal: adicione Garden First Light, 10:00–10:30 e Studio Paper Signals, 10:30–11:15. Escolha View my schedule e leia os dois cartões e os 75 minutos. Depois tente Studio Colour Routes, 10:50–11:10. Uma mensagem junto à candidata identifica Paper Signals; os dois registros selecionados e os 75 minutos não mudam. O renderizador preparado mantém a mensagem visível, inclusive em telas estreitas. É o exemplo fornecido, não sua implementação.

Feche Study e retorne à entrada Festival do produto. Abra Open Work schedule in a new page e deixe-a aberta. Os auxiliares de seleção já estão preparados, mas a função Add inicial não rejeita sobreposições e as escolhas estão só em memória. Mude para Workspace para editar. O diretório inicial é /home/labex/project/festival-schedule/work; use cd, pwd e cat do primeiro laboratório se precisar confirmar um caminho. Não reinstale pacotes por um nome de arquivo incorreto.

Represente uma sessão como intervalo semiaberto [start, end). O início é incluído e o fim fica livre para a próxima sessão. Todos os horários são minutos inteiros de uma mesma data local declarada. Por exemplo, 10:30 é 630. Garden First Light é [600, 630) e Paper Signals é [630, 675): igualdade em 630 é válida. Colour Routes [650, 670) sobrepõe Paper Signals, mas não a primeira sessão selecionada. Verificar apenas a primeira escolha seria errado.

A condição de sobreposição é candidate.start < session.end && session.start < candidate.end. As duas comparações estritas devem ser verdadeiras; trocar < por <= rejeitaria a adjacência incorretamente. find percorre os registros selecionados até encontrar conflito e retorna esse objeto real, ou undefined se não houver. Quando há vários, pode retornar qualquer conflito real; a mensagem precisa identificá-lo de forma verdadeira.

Salve o auxiliar de conflitos com o here-document entre aspas aprendido no primeiro laboratório. O JS final fica sozinho na linha, e > substitui o arquivo indicado:

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

Conecte-o agora antes de atualizar o estado dos IDs. O componente abaixo importa a função que você acabou de escrever. Ele resolve os registros de toda a seleção atual, não do programa filtrado por palco. Uma rejeição define a mensagem da candidata clicada e retorna sem alterar IDs ou totais. O texto usa título, palco e horário para não confundir as duas First Light. Outra redação clara é válida; a relação importante é entre a candidata e um conflito real selecionado. O restante do estado e dos auxiliares derivados segue o laboratório anterior.

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

Volte a Work e atualize normalmente. O initialIDs independente desta unidade é vazio. Adicione Garden First Light e Paper Signals para obter 75 minutos. Tente Colour Routes: Paper Signals é o segundo objeto escolhido e a rejeição deve manter 75 minutos. Selecione Stage: Loft e tente First Light, 10:15–10:45. A Garden First Light realmente conflitante está oculta do programa pelo filtro, mas continua em My schedule. A nova rejeição deve identificar essa sessão Garden e manter as mesmas duas escolhas. Um filtro não deve enfraquecer a regra de conflito.

Volte Stage para All stages. Adicione Garden Small Stages, 11:15–11:45: ela é adjacente a Paper Signals, então o total vira 105 minutos. Por View my schedule, remova Paper Signals. Garden First Light e Small Stages somam 60. Adicione novamente Colour Routes, 10:50–11:10: agora ela cabe, dando 80 minutos na ordem First Light, Colour Routes, Small Stages. A Loft First Light de 10:15 continua em conflito com Garden First Light; não a use como o exemplo de adição permitida após retirar um conflito.

Na tela estreita, a navegação do dia oferece My schedule sem procurar por todo o programa, e a rejeição aparece ao lado do cartão clicado. Use Tab/Enter de verdade numa candidata e confira se a mensagem fica legível sem mover o foco para uma região de status. Se houver erro de código, inspecione src/conflicts.js e src/App.jsx, restaure o bloco completo correspondente e atualize. Esta etapa ainda guarda escolhas só em memória; atualizar pode redefini-las até a etapa 2. Execute a verificação de conflitos.

Restaurar IDs antes de sincronizar o armazenamento

Nesta etapa, restaure uma lista de IDs salva antes de escrever e recupere dados salvos inutilizáveis ou desatualizados.

Comece em Workspace. localStorage é armazenamento do navegador para uma origem: esquema, host e porta. Caminhos da mesma origem compartilham a área; use uma chave específica do curso e da unidade, settings.storageKey, não uma chave genérica data. Study tem outra chave. Não chame clear() nem remova outras chaves. Uma URL de uma VM nova, um contexto limpo, outro navegador ou dispositivo podem não conter essas escolhas. A política do navegador pode negar armazenamento. É uma conveniência para a mesma página e navegador, não reserva, backup ou promessa de retenção permanente.

Valores armazenados são strings. JSON.stringify(ids) transforma o array de IDs em uma string JSON; JSON.parse(raw) a lê de volta. Guarde somente IDs como ["D11","D12"], não cópias de títulos e horários que podem ficar antigas. Resolva os IDs restaurados contra o catálogo atual e derive os mesmos cartões, quantidade e minutos. A ordem do array salvo não determina a exibição: My schedule usa o início atual e o ID.

As regras públicas de recuperação são:

  • Uma chave ausente usa o settings.initialIDs preparado desta unidade. Aqui o array é vazio. O desafio independente tem seus próprios IDs iniciais explícitos.
  • JSON malformado, um valor que não seja array ou um array com elemento não textual produzem uma agenda vazia utilizável e um aviso legível. Um array vazio é válido.
  • IDs de string repetidos não duplicam cartões. IDs ausentes do catálogo são omitidos com aviso; permanecem as escolhas atuais válidas e compatíveis, com detalhes atuais.
  • Se a própria lista salva tem conflitos, processe-a na ordem de entrada salva. Mantenha as primeiras escolhas compatíveis nessa ordem e explique que escolhas sobrepostas foram ignoradas. É a mesma regra de admissão, não otimização de horários.
  • Se a leitura for negada, permita uma visita em memória com aviso. Se a escrita falhar, preserve as escolhas e diga que não puderam ser salvas. Nessa situação não há promessa de restauração após atualizar.

O auxiliar a seguir introduz algumas ferramentas JavaScript. try executa código que pode lançar uma exceção; catch oferece recuperação legível em vez de deixar a página falhar. raw === null identifica uma chave ausente. Array.isArray e typeof validam o formato público da lista de IDs; throw new Error(...) envia um formato inválido para a mesma recuperação. for (const id of saved) visita IDs em ordem, continue pula um e push acrescenta a novos arrays locais. Esses arrays temporários não são estado React. join(' ') combina as frases de aviso aplicáveis. A função de leitura não escreve nem remove dados do armazenamento.

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

Restaure agora o estado inicial com segurança. useState(() => readSaved()) passa uma função como inicializador sob demanda: React a chama ao inicializar este componente. Ela retorna {ids, notice} sem escrever armazenamento, mudar outra chave nem buscar dados. Mantenha esse retrato em initial, inicialize os IDs escolhidos com initial.ids e preserve o aviso como mensagem. StrictMode de desenvolvimento pode chamar um inicializador mais de uma vez para detectar problemas; um inicializador somente de leitura pode se repetir com segurança. Outra implementação com liberação explícita da escrita após a restauração também é válida, desde que não escreva uma lista vazia antes de restaurar.

useEffect é importado de React e sincroniza o armazenamento externo depois que uma renderização é confirmada. A lista de dependências [selectedIDs] pede repetir a sincronização quando esse array muda. StrictMode de desenvolvimento pode executar um ciclo extra do efeito; escrever a mesma lista de IDs é idempotente: repetir deixa o mesmo resultado salvo sem duplicar escolhas. Seleção, ordenação, quantidade e conflitos continuam sendo trabalho normal da renderização ou dos manipuladores, não efeitos. Começar vazio, escrever vazio imediatamente e carregar os IDs só depois apagaria uma agenda real salva.

Substitua o componente. readSaved e writeSaved vêm do auxiliar que você acabou de salvar; os outros imports foram usados na etapa 1. remembering informa à visualização preparada que esta versão tenta persistência no navegador, enquanto uma escrita falha ainda produz o aviso explícito de uso somente em memória.

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

Volte a Work e atualize normalmente. A primeira atualização após a mudança pode ficar vazia: a etapa 1 nunca salvou as escolhas da memória. Adicione novamente Garden First Light e Paper Signals e atualize exatamente a mesma URL na mesma página do navegador. Devem voltar os dois registros atuais e os 75 minutos. Um contexto independente limpo deve começar pela seleção inicial desta unidade; não afirme que a URL compartilha essa agenda privada.

Use depois a página separada de treinamento controlado. Escolha Practice recovery no rodapé; ela substitui Work na mesma aba. Leia o aviso: cada exercício substitui apenas a chave de escolhas desta unidade e, portanto, troca as escolhas de teste atuais. Escolha Load unreadable JSON e depois Return to Work. O componente normal deve mostrar uma agenda vazia utilizável e um aviso de recuperação legível. Adicione a Loft First Light de 12:00–12:30 e atualize normalmente: voltam seu cartão atual e 30 minutos. Isso prova uso normal após recuperar, não apenas uma mensagem de erro.

Volte a Practice recovery, escolha Load retired IDs e depois Return to Work. A predefinição visível contém D11, um ID retirado e D12. Os dois registros disponíveis ficam, somando 75 minutos, com explicação da escolha antiga omitida. Quando uma lista limpa substitui a entrada antiga, outra atualização não precisa repetir o aviso. Use finalmente Load conflicting IDs: a predefinição é D11, D13, D12. D13 sobrepõe D11, que vem antes na entrada salva; a recuperação mantém D11 e D12 com aviso, não as três sessões nem uma agenda vazia. A página Practice é treinamento explícito, não uma redefinição invisível no fluxo do visitante.

Volte a Work e confirme que Add, filtro e Remove ainda operam sobre as escolhas atuais recuperadas. Teste a adjacência válida de Small Stages e a rejeição de Colour Routes como na etapa 1. Se uma política real bloquear o armazenamento, mantenha a mensagem explícita de memória, use a página durante esta visita e abra um contexto permitido se precisar de persistência após atualizar. Não apague dados do navegador sem relação para forçar o sucesso. Execute a verificação final quando os caminhos normais e de recuperação funcionarem.

Após a sequência de remoção e nova adição, os cartões restaurados First Light, Colour Routes e Small Stages somam 80 minutos. Compare os detalhes atuais e os estados de Add:

Escolhas atuais restauradas e resumo derivado

Em uma tela estreita, a rejeição da candidata do horário mais cedo em Loft identifica a sessão Garden já escolhida junto ao botão; a navegação da agenda continua acessível acima.

Rejeição legível junto à candidata em tela estreita

Resumo

Você construiu uma regra de participação verdadeira: cada registro atual selecionado participa da verificação de sobreposição semiaberta, a adjacência é válida e remover uma escolha pode permitir uma candidata. Depois restaurou dados do navegador restritos a uma chave e compostos apenas por IDs antes de sincronizar as gravações, preservando relações atuais e recuperando escolhas salvas ilegíveis, ausentes ou conflitantes.

A restauração se limita à mesma origem do navegador e a um ambiente do curso existente com armazenamento permitido. O desafio independente altera dados e relações do programa usando os mesmos mecanismos.