/ app-1 / src / components / HackerNews.tsx
HackerNews.tsx
  1  import * as React from 'react'
  2  import { RotatingLines } from 'react-loader-spinner'
  3  
  4  const fetchData = async ({ query = '', page = 0, tag = '' }) => {
  5    return fetch(
  6      `https://hn.algolia.com/api/v1/search?query=${query}&tags=${encodeURIComponent(
  7        tag
  8      )}&page=${page}`
  9    )
 10      .then((response) => response.json())
 11      .then((json) => ({
 12        results: json.hits || [],
 13        pages: json.nbPages || 0,
 14        resultsPerPage: json.hitsPerPage || 20
 15      }))
 16  }
 17  
 18  export default function HackerNewsSearch() {
 19    const [query, setQuery] = React.useState("")
 20    const [results, setResults] = React.useState([])
 21    const [tag, setTag] = React.useState("story")
 22    const [page, setPage] = React.useState(0)
 23    const [resultsPerPage, setResultsPerPage] = React.useState(0)
 24    const [totalPages, setTotalPages] = React.useState(50)
 25    const [loading, setLoading] = React.useState(false)
 26  
 27    const handleSearch: React.ChangeEventHandler<HTMLInputElement> = (e) => {
 28      setQuery(e.target.value)
 29      setPage(0)
 30    }
 31  
 32    const handleTag: React.ChangeEventHandler<HTMLSelectElement> = (e) => {
 33      setTag(e.target.value)
 34      setPage(0)
 35    }
 36  
 37    const handleNextPage = () => {
 38      setPage(prevPage => prevPage >= totalPages ? totalPages : prevPage + 1)
 39    }
 40  
 41    const handlePrevPage = () => {
 42      setPage(prevPage => prevPage === 0 ? 0 : page - 1)
 43    }
 44  
 45    React.useEffect(() => {
 46      let isStale = false
 47      setLoading(true)
 48      const fetchNews = async (query: string) => {
 49        try {
 50          const fetchedNews = await fetchData({ query, page, tag })
 51  
 52          if (isStale) {
 53            return
 54          }
 55  
 56          if (fetchedNews) {
 57            const { results, pages, resultsPerPage } = fetchedNews
 58            console.debug({ results, resultsPerPage, pages })
 59            setResults(results)
 60            setResultsPerPage(resultsPerPage)
 61            setTotalPages(pages)
 62            setLoading(false)
 63          } else {
 64            throw new Error(`Failed to fetch news for ${query} in tag ${tag}`)
 65          }
 66        } catch (fetchNewsError) {
 67          setLoading(false)
 68          console.debug({ error: (fetchNewsError as Error).message })
 69        }
 70      }
 71  
 72      fetchNews(query)
 73  
 74      return () => {
 75        setLoading(false)
 76        isStale = true
 77      }
 78    }, [query, page, tag])
 79  
 80    return (
 81      <main>
 82        <h1>Hacker News Search</h1>
 83        <form onSubmit={(e) => e.preventDefault()}>
 84          <div>
 85            <label htmlFor="query">Search</label>
 86            <input
 87              type="text"
 88              id="query"
 89              name="query"
 90              value={query}
 91              onChange={handleSearch}
 92              placeholder="Search Hacker News..."
 93            />
 94          </div>
 95          <div>
 96            <label htmlFor="tag">Tag</label>
 97            <select id="tag" name="tag" onChange={handleTag} value={tag}>
 98              <option value="story">Story</option>
 99              <option value="ask_hn">Ask HN</option>
100              <option value="show_hn">Show HN</option>
101              <option value="poll">Poll</option>
102            </select>
103          </div>
104        </form>
105        <section>
106          <header>
107            <h2>
108              <span>No Results OR Page {page + 1} of {totalPages}</span>
109              <RotatingLines
110                strokeColor="grey"
111                strokeWidth="5"
112                animationDuration="0.75"
113                width="20"
114                visible={loading}
115              />
116            </h2>
117            <div>
118              <button className="link" onClick={handlePrevPage} disabled={page <= 0}>
119                Previous
120              </button>
121              <button className="link" onClick={handleNextPage} disabled={page >= totalPages - 1}>
122                Next
123              </button>
124            </div>
125          </header>
126          <ul>
127            {results.map(({ url, objectID, title }, index) => {
128              const href =
129                url || `https://news.ycombinator.com/item?id=${objectID}`
130  
131              return (
132                <li key={objectID}>
133                  <span>{(index + 1) % resultsPerPage}.</span>
134                  <a href={href} target="_blank" rel="noreferrer">
135                    {title}
136                  </a>
137                </li>
138              )
139            })}
140          </ul>
141        </section>
142      </main>
143    )
144  }