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

Code before:
  let start = 0;
  let stopAudio = false;

  // this function exists because we couldn't reliably stop the audio when
  // playing only part of the audio file. So it would get cut off
  function maybeStopAudio() {
    const runningTime = Date.now() - start;

    if (runningTime >= duration) {
      stopAudio = true;
      audioRef.current.pause();
    }

    if (!stopAudio) {
      window.requestAnimationFrame(maybeStopAudio);
    }
  }

  const buildTranscript = () => {
    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 = () => {

Review comment (suggestion):
```suggestion
        command.character &&
        command.dialogue &&
        command.startTime
```

I'm not sure why we're null-checking instead of truthy checking. If we *really* need to null check, we should at least use strict inequality:

```suggestion
        command.character !== null &&
        command.dialogue !== null &&
        command.startTime !== null
```

Code after:
        return {
          ...character,
          opacity: character.opacity ?? 1,
          isTalking: false
        };
      }),
    [setup.characters]
  );

  const [isPlaying, setIsPlaying] = useState(false);
  const [sceneIsReady, setSceneIsReady] = useState(false);
  const [accessibilityOn, setAccessibilityOn] = useState(false);
  const [characters, setCharacters] = useState(initCharacters);
  const [dialogue, setDialogue] = useState(initDialogue);
  const [background, setBackground] = useState(initBackground);
  const startClocktimeRef = useRef<number>(0);
  const pausedAtRef = useRef<number>(0);
  const startTimerRef = useRef<number>();
  const finishTimerRef = useRef<number>();
  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;