diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx index 2d369fa..2636ccd 100644 --- a/apps/web/src/App.tsx +++ b/apps/web/src/App.tsx @@ -1,78 +1,63 @@ -import type { CheckResponse } from "@cleanhashtags/shared"; import { useState } from "react"; -import { checkHashtags, errorMessage } from "./api/client"; +import { Footer } from "./components/Footer"; import { HashtagForm } from "./components/HashtagForm"; import { Header } from "./components/Header"; import { ResultsList } from "./components/ResultsList"; - -// A discriminated union: `status` says which of the four situations we are in, -// and TypeScript only lets us read `response` or `message` when they exist. -type CheckState = - | { status: "idle" } - | { status: "loading"; count: number } - | { status: "success"; response: CheckResponse } - | { status: "error"; message: string }; +import { useHashtagCheck } from "./hooks/useHashtagCheck"; // A component is a function that returns JSX. React calls it to find out what // to show; the capitalised name is how JSX tells components from HTML tags. export function App() { - // Lifted state: the textarea's text used to live inside HashtagForm. Reset - // (in ResultsList) must clear it, so it moves up to the closest common - // parent of both components and is passed down as props. + // Lifted state: Reset (in ResultsList) must clear the textarea, so the text + // lives here, in the closest common parent, and is passed down as props. const [text, setText] = useState(""); - const [check, setCheck] = useState({ status: "idle" }); - - // The request starts because the user submitted the form, so it lives in the - // event handler. No useEffect needed: nothing has to "sync" after rendering. - async function runCheck(tags: string[]) { - setCheck({ status: "loading", count: tags.length }); - try { - const response = await checkHashtags(tags); - setCheck({ status: "success", response }); - } catch (error) { - setCheck({ status: "error", message: errorMessage(error) }); - } - } + // All request state (loading, results, errors) now lives in a custom hook. + const { state, check, reset } = useHashtagCheck(); - function reset() { + function handleReset() { setText(""); - setCheck({ status: "idle" }); + reset(); } return ( - <> +
-
+
void runCheck(tags)} - busy={check.status === "loading"} + // `check` handles its own errors. + onSubmit={(tags) => void check(tags)} + busy={state.status === "loading"} /> - {check.status === "loading" && ( -

- Checking {check.count} {check.count === 1 ? "hashtag" : "hashtags"}… + {state.status === "loading" && ( +

+ Checking {state.count} {state.count === 1 ? "hashtag" : "hashtags"}…

)} - {check.status === "error" &&

{check.message}

} + {state.status === "error" && ( +

+ {state.message} +

+ )} - {check.status === "success" && ( + {state.status === "success" && ( // A new key per response makes React start ResultsList fresh, so // its local "Copied" message does not carry over to the next check. )}
- +
+
); } diff --git a/apps/web/src/components/Footer.test.tsx b/apps/web/src/components/Footer.test.tsx new file mode 100644 index 0000000..2605b75 --- /dev/null +++ b/apps/web/src/components/Footer.test.tsx @@ -0,0 +1,13 @@ +import { render, screen } from "@testing-library/react"; +import { describe, expect, it } from "vitest"; +import { Footer } from "./Footer"; + +describe("Footer", () => { + it("explains how fresh and reliable verdicts are", () => { + render(