Repository: freeCodeCamp/freeCodeCamp
File: client/src/templates/Challenges/components/scene/scene.tsx (TypeScript)

Code before:
    let transcript = '';
    commands.forEach(command => {
      console.log(command);
      if (
        command.character != null &&
        command.dialogue != null &&
        command.startTime != null
      ) {
        transcript =
          transcript +
          '\n' +
          command.startTime +
          '&nbsp;&nbsp;&nbsp;&nbsp;' +
          command.character +
          ':' +
          ' ' +
          command.dialogue.text;
      }
    });
    return transcript;
  };

  const toggleTranscript = () => {
    const transcriptElement = document.getElementById('transcript')!;
    if (transcriptElement.innerText == '') {
      transcriptElement.innerHTML = buildTranscript();
    } else {
      transcriptElement.innerHTML = '';
    }
  };

  const playScene = () => {
    setShowDialogue(true);
    setTimeout(() => {
      if (audioRef.current.paused) {
        start = Date.now();
        void audioRef.current.play();
      }
      // if there are no timestamps, we can let the audio play to the end
      if (hasTimestamps) maybeStopAudio();
    }, sToMs(audio.startTime));

    commands.forEach((command, commandIndex) => {
      // Start command timeout
      setTimeout(() => {
        if (command.background) setBackground(command.background);

        setDialogue(
          command.dialogue
            ? { ...command.dialogue, label: command.character }
            : initDialogue

Review comment (suggestion):
innerHTML feels like an anti-pattern in react. Can we not use a `useState` and conditionally render the transcript? We're not even using HTML elements in the buildTranscript function.

Code after:
  const animationRef = useRef<number>();
  const usedCommandsRef = useRef(new Set<number>());

  const [currentTime, setCurrentTime] = useState(0);
  // TODO: I'm using a ref so that the maybeStopAudio closure doesn't get stuck
  // with the initial value of isPlaying. Given that we also have a state,
  // isPlaying, it feels like there's a better way.
  const isPlayingSceneRef = useRef(false);

  // memoizing to prevent the useEffect from running on every render
  const sortedCommands = useMemo(() => {
    const normalized = commands.flatMap(command => {
      const { startTime, finishTime, ...rest } = command;

      const startCommand = {
        ...rest,
        time: sToMs(startTime),
        isTalking: !!rest.dialogue
      };
      const finishCommand = finishTime
        ? { ...rest, time: sToMs(finishTime), isTalking: false }
        : null;

      return finishCommand ? [startCommand, finishCommand] : [startCommand];
    });
    normalized.sort((a, b) => a.time - b.time);
    return normalized;
  }, [commands]);

  // an extra 500ms at the end to let the characters fade out (CSS transition
  const resetTime = sortedCommands.at(-1)!.time + 500;

  const audioLoaded = () => {
    setSceneIsReady(true);
  };

  const buildTranscript = () => {
    let transcriptText = '';
    commands.forEach(command => {
      if (command.character && command.dialogue && command.startTime) {
        transcriptText =
          transcriptText +
          '\n' +
          '<strong>' +
          command.character +
          '</strong>:' +
          ' ' +
          command.dialogue.text +
          '\n';
      }
    });