diff --git a/src/App.tsx b/src/App.tsx
index 47e0711..8970234 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -1,539 +1,10 @@
-import { z } from "zod"
-import { useReducer, useRef, useEffect, createContext, useContext, useState } from "react"
-import {
- CssBaseline,
- Box,
- Button,
- Typography,
- Skeleton,
- Dialog,
- DialogContent,
-} from "@mui/material"
-
-type Result = { tag: "ok"; value: A } | { tag: "error"; error: E }
-
-const Result = {
- ok(value: A): Result {
- return { tag: "ok", value }
- },
- err(error: E): Result {
- return { tag: "error", error }
- },
- map(result: Result, f: (x: A) => B): Result {
- switch (result.tag) {
- case "ok":
- return { tag: "ok", value: f(result.value) }
- case "error":
- return { tag: "error", error: result.error }
- }
- },
-}
-
-type RequestError =
- | { tag: "networkError"; error: unknown }
- | { tag: "httpError"; status: number; statusText: string }
- | { tag: "jsonParseError"; error: unknown }
- | { tag: "invalidResponse"; error: z.ZodError }
-
-const RequestError = {
- toString(error: RequestError): string {
- switch (error.tag) {
- case "networkError":
- return "Network error. Check your connection and try again."
- case "httpError":
- return `Server returned ${String(error.status)} ${error.statusText}.`
- case "jsonParseError":
- return "The server returned an invalid response."
- case "invalidResponse":
- return "The image data had an unexpected format."
- }
- },
-}
-
-// === Image ===
-type ImageId = string
-type ImageRef = string
-
-// === Dimension ===
-type Dimension = {
- width: number
- height: number
-}
-const Dimension = {
- medium: { width: 180, height: 180 },
- big: { width: 720, height: 720 },
-} satisfies {
- medium: Dimension
- big: Dimension
-}
-
-// === Page ===
-type PageNumber = number
-
-type Page = {
- page: PageNumber
- limit: number // page size
-}
-type PageKey = string
-
-const LIMIT = 10
-const FIRST_PAGE: PageNumber = 1
-
-const Page = {
- init(): Page {
- return { page: FIRST_PAGE, limit: LIMIT }
- },
- next(page: Page): Page {
- return { ...page, page: page.page + 1 }
- },
- previous(page: Page): Page {
- // Could do
- // if (page.page == FIRST_PAGE) { return page }
- // this preserves identity of object, so is nicer for `useEffect`, but it's waaaay to subtle. So I'm not relying on that.
- return { ...page, page: Math.max(FIRST_PAGE, page.page - 1) }
- },
- eq(page0: Page, page1: Page): boolean {
- return page0.page === page1.page && page0.limit === page1.limit
- },
- // for hashing in maps to avoid identity problems
- key(page: Page): PageKey {
- return `${String(page.page)}:${String(page.limit)}`
- },
- isFirst(page: Page): boolean {
- return page.page === FIRST_PAGE
- },
-}
-
-// === Generic safe fetch functions ===
-async function fetchJsonSafe(url: string): Promise> {
- let response: Response
- try {
- response = await fetch(url)
- } catch (error: unknown) {
- return Result.err({ tag: "networkError", error })
- }
-
- if (!response.ok) {
- return Result.err({
- tag: "httpError",
- status: response.status,
- statusText: response.statusText,
- })
- } else {
- try {
- const json: unknown = await response.json()
- return Result.ok(json)
- } catch (error: unknown) {
- return Result.err({ tag: "jsonParseError", error })
- }
- }
-}
-
-// Assumes `f` is some sort of a zod parsing function that may throw `z.ZodError`
-async function fetchJsonSafeWith(
- url: string,
- f: (json: unknown) => A,
-): Promise> {
- const result = await fetchJsonSafe(url)
- switch (result.tag) {
- case "error":
- return Result.err(result.error)
- case "ok":
- try {
- return Result.ok(f(result.value))
- } catch (error: unknown) {
- if (error instanceof z.ZodError) {
- return Result.err({ tag: "invalidResponse", error })
- } else {
- throw error
- }
- }
- }
-}
-
-// === Generic Remote Data ===
-type Remote = { tag: "loading" } | Result
-
-const Remote = {
- loading(): Remote {
- return { tag: "loading" }
- },
- error(error: E): Remote {
- return { tag: "error", error }
- },
- loaded(value: A): Remote {
- return { tag: "ok", value }
- },
-}
-
-// === api ===
-const picsumApiImageSchema = z.object({
- id: z.string(),
- author: z.string(),
- width: z.number(),
- height: z.number(),
- url: z.string(),
- download_url: z.string(), // WARNING: the api-endpoint returns "url" and "download_url". You definitely want "download_url" for image sources.
-})
-type PicsumApiImage = z.infer
-
-async function getPicsumImages({
- page,
- limit,
-}: Page): Promise> {
- return fetchJsonSafeWith(
- `https://picsum.photos/v2/list?limit=${String(limit)}&page=${String(page)}`,
- (json) => z.array(picsumApiImageSchema).parse(json),
- )
-}
-
-async function getImageIds(page: Page): Promise> {
- const result = await getPicsumImages(page)
- return Result.map(result, (data) => data.map(({ id }) => id))
-}
-
-// Use this for `
`
-function getImageSource(id: ImageId, dimension: Dimension): ImageRef {
- return `https://picsum.photos/id/${id}/${String(dimension.width)}/${String(dimension.height)}`
-}
-
-// === App ===
-type AppError = RequestError
-
-type State = {
- imageIds: Remote
- page: Page
- selectedImage: ImageId | undefined
- refreshSignal: boolean // This is here only because of the refresh button. It is an artificial way to trigger a reload without the current page changing. The value is meaningless. It just has to change.
-}
-
-const State = {
- init(): State {
- return {
- imageIds: Remote.loading(),
- page: Page.init(),
- selectedImage: undefined,
- refreshSignal: false,
- }
- },
-}
-
-type Msg =
- | { tag: "imagesReceived"; result: Result; forPage: Page }
- | { tag: "retryButtonClicked" }
- | { tag: "previousButtonClicked" }
- | { tag: "nextButtonClicked" }
- | { tag: "imageClicked"; imageId: ImageId }
- | { tag: "modalCloseButtonClicked" }
-
-function useApp(): [State, Dispatch] {
- const [state, dispatch] = useReducer(update, State.init())
-
- // === Caching API calls ===
- // Could also cache `Promise` in case two requests are made really fast one after another (not really the case in this app so whatever)
- const cacheRef = useRef