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> ); }
Resize editor and preview
Resize sandbox height