← Back to the exercise

The show details

The answer to the exercise in The show details.

Each card kept its own open flag, so two clicks drew two panels. selectedId lives in App, so there is only one. Load more runs /^[0-9]+$/ first, so s1 never hits the network.

import { useState } from "react";
import "./styles.css";
import { fromTvmaze } from "./show";
import SearchForm from "./SearchForm";
import ShowList from "./ShowList";

const start = [
  { id: "s1", title: "The Bear", year: 2022, genre: "Drama", genres: ["Drama"], rating: 8.6, still: "still-warm", watched: false },
  { id: "99", title: "Slow Horses", year: 2022, genre: "Thriller", genres: ["Thriller"], rating: 8.4, still: "still-warm", watched: false, image: null },
  { id: "s2", title: "Severance", year: 2022, genre: "Thriller", genres: ["Thriller"], rating: 8.7, still: "still-cold", watched: false },
  { id: "s3", title: "Andor", year: 2022, genre: "Sci-Fi", genres: ["Sci-Fi"], rating: 8.4, still: "still-dusty", watched: true },
];

export default function App() {
  const [watchlist, setWatchlist] = useState(start);
  const [query, setQuery] = useState("");
  const [results, setResults] = useState([
    { id: "99", title: "Slow Horses", year: 2022, genre: "Thriller", genres: ["Thriller"], rating: 8.4, still: "still-warm", watched: false, image: null },
  ]);
  const [status, setStatus] = useState("idle");
  const [selectedId, setSelectedId] = useState(null); // one selection
  const [details, setDetails] = useState(null);
  const [detailsLoading, setDetailsLoading] = useState(false);

  function handleToggle(id) {
    setWatchlist(
      watchlist.map((show) =>
        show.id === id ? { ...show, watched: !show.watched } : show
      )
    );
  }

  function handleAdd(show) {
    if (watchlist.some((s) => s.id === show.id)) {
      return;
    }
    setWatchlist([...watchlist, { ...show, watched: false }]);
  }

  function handleRemove(id) {
    setWatchlist(watchlist.filter((s) => s.id !== id));
  }

  function handleSelect(id) {
    if (id === selectedId) {
      return;
    }
    setSelectedId(id);
    setDetails(null);
  }

  async function handleLoadMore() {
    if (!selectedId || !/^[0-9]+$/.test(selectedId)) {
      return;
    }

    setDetailsLoading(true);
    try {
      const response = await fetch(
        "https://api.tvmaze.com/shows/" + selectedId
      );
      if (!response.ok) {
        throw new Error("Request failed");
      }
      const data = await response.json();
      setDetails({
        network: data.network?.name ?? data.webChannel?.name ?? "Unknown",
        status: data.status ?? "Unknown",
        runtime: data.runtime ?? data.averageRuntime ?? null,
      });
    } catch {
      setDetails(null);
    }
    setDetailsLoading(false);
  }

  async function handleSearch(event) {
    event.preventDefault();
    const trimmed = query.trim();
    if (!trimmed) {
      setResults([]);
      setStatus("empty");
      return;
    }

    setStatus("loading");
    setResults([]);

    try {
      const response = await fetch(
        "https://api.tvmaze.com/search/shows?q=" +
          encodeURIComponent(trimmed)
      );
      if (!response.ok) {
        throw new Error("Request failed");
      }
      const data = await response.json();
      setResults(data.map((item) => fromTvmaze(item.show)));
      setStatus("success");
    } catch {
      setStatus("error");
    }
  }

  const watched = watchlist.filter((show) => show.watched).length;
  const selected = watchlist.find((s) => s.id === selectedId);

  return (
    <div className="page">
      <header className="header">
        <h1>My watchlist</h1>
        <p className="count">
          {watchlist.length} shows · {watched} watched
        </p>
      </header>
      <SearchForm
        query={query}
        onQueryChange={setQuery}
        onSearch={handleSearch}
        disabled={status === "loading"}
      />
      {status === "loading" ? (
        <p className="status">Searching…</p>
      ) : null}
      {status === "empty" ? (
        <p className="status">Type a show name first.</p>
      ) : null}
      {status === "error" ? (
        <p className="status status-error">
          Could not load shows. Try again.
        </p>
      ) : null}
      {status === "success" && results.length === 0 ? (
        <p className="status">No shows for that name.</p>
      ) : null}
      {results.length > 0 ? (
        <div className="results">
          <p className="section-title">Results</p>
          <ShowList
            shows={results}
            onAdd={handleAdd}
            isSaved={(id) => watchlist.some((s) => s.id === id)}
          />
        </div>
      ) : null}
      <div className="saved">
        <ShowList
          shows={watchlist}
          onToggle={handleToggle}
          onRemove={handleRemove}
          onSelect={handleSelect}
          selectedId={selectedId}
        />
        {selected ? (
          <aside className="panel">
            <button
              type="button"
              className="close"
              onClick={() => {
                setSelectedId(null);
                setDetails(null);
              }}
            >
              Close
            </button>
            <h2>{selected.title}</h2>
            <p>{selected.year}</p>
            <p>{selected.genres.join(", ")}</p>
            <p>★ {selected.rating}</p>
            {/^[0-9]+$/.test(selected.id) ? (
              <button
                type="button"
                onClick={handleLoadMore}
                disabled={detailsLoading}
              >
                {detailsLoading ? "Loading…" : "Load more"}
              </button>
            ) : (
              <p>This one was added by us</p>
            )}
            {details ? <p>{details.network}</p> : null}
            {details ? <p>{details.status}</p> : null}
            {details && details.runtime ? (
              <p>{details.runtime} min</p>
            ) : null}
          </aside>
        ) : null}
      </div>
    </div>
  );
}