Ditch old css. Use Material UI. Add Modal. Make it work on mobile.

This commit is contained in:
Yura Dupyn 2026-05-15 00:36:08 +02:00
parent e9cf90180f
commit ca1a2bf1ca
4 changed files with 134 additions and 59 deletions

View file

@ -1,5 +1,14 @@
import { z } from "zod" import { z } from "zod"
import { useReducer, useRef, useEffect, createContext, useContext } from "react" import { useReducer, useRef, useEffect, createContext, useContext } from "react"
import {
CssBaseline,
Box,
Button,
Typography,
Skeleton,
Dialog,
DialogContent,
} from "@mui/material"
// TODO: Use Material UI later. // TODO: Use Material UI later.
// TODO: Improve error-handling. Introduce some proper server response. // TODO: Improve error-handling. Introduce some proper server response.
@ -26,8 +35,8 @@ type Dimension = {
height: number height: number
} }
const Dimension = { const Dimension = {
medium: { width: 200, height: 200 } as Dimension, medium: { width: 180, height: 180 } as Dimension,
big: { width: 300, height: 500 } as Dimension, big: { width: 720, height: 720 } as Dimension,
} }
// === Page === // === Page ===
@ -62,6 +71,9 @@ const Page = {
key(page: Page): PageKey { key(page: Page): PageKey {
return `${page.page}:${page.limit}` return `${page.page}:${page.limit}`
}, },
isFirst(page: Page): boolean {
return page.page === FIRST_PAGE
},
} }
// === api === // === api ===
@ -230,7 +242,7 @@ function useDispatch() {
function RemoteImages({ images }: { images: Remote<ImageId[], AppError> }) { function RemoteImages({ images }: { images: Remote<ImageId[], AppError> }) {
switch (images.tag) { switch (images.tag) {
case "loading": case "loading":
return <div>Loading...</div> return <ImagesSkeleton />
case "error": case "error":
return <div>Error: TODO</div> return <div>Error: TODO</div>
case "loaded": case "loaded":
@ -238,21 +250,82 @@ function RemoteImages({ images }: { images: Remote<ImageId[], AppError> }) {
} }
} }
function imageGridStyle(dimension: Dimension) {
return {
display: "grid",
gridTemplateColumns: {
xs: `repeat(2, ${dimension.width}px)`,
md: `repeat(5, ${dimension.width}px)`,
},
gridAutoRows: `${dimension.height}px`,
gap: 2,
}
}
// 2x5 on mobile, 5x2 on desktop (assuming 10 images per page)
function ImagesSkeleton() {
const images = Array.from({ length: 10 })
return (
<Box sx={imageGridStyle(Dimension.medium)}>
{images.map((_, index) => (
<Skeleton
key={index}
variant="rectangular"
width={Dimension.medium.width}
height={Dimension.medium.height}
></Skeleton>
))}
</Box>
)
}
function Images({ images }: { images: ImageId[] }) { function Images({ images }: { images: ImageId[] }) {
const dispatch = useDispatch() const dispatch = useDispatch()
// TODO: Is there some basic `Col/Row` component?
return ( return (
<div style={{ display: "flex", flexDirection: "column" }}> <Box sx={imageGridStyle(Dimension.medium)}>
{images.map((imageId) => ( {images.map((imageId) => (
<img <Box
style={{ width: Dimension.medium.width, height: Dimension.medium.height }} component="img"
key={imageId}
src={getImageSource(imageId, Dimension.medium)} src={getImageSource(imageId, Dimension.medium)}
onClick={() => dispatch({ tag: "imageClicked", imageId })} onClick={() => dispatch({ tag: "imageClicked", imageId })}
sx={{
width: Dimension.medium.width,
height: Dimension.medium.height,
objectFit: "cover",
cursor: "pointer",
}}
key={imageId}
alt="" alt=""
/> />
))} ))}
</div> </Box>
)
}
function ImageModal({ selectedImage }: { selectedImage: ImageId | undefined }) {
const dispatch = useDispatch()
return (
<Dialog
open={selectedImage !== undefined}
onClose={() => dispatch({ tag: "modalCloseButtonClicked" })}
maxWidth={false}
>
<DialogContent sx={{ p: 0, overflow: "hidden" }}>
{selectedImage !== undefined && (
<Box
component="img"
src={getImageSource(selectedImage, Dimension.big)}
sx={{
display: "block",
maxWidth: "90vw",
maxHeight: "80vh",
objectFit: "contain",
}}
alt=""
/>
)}
</DialogContent>
</Dialog>
) )
} }
@ -260,13 +333,53 @@ export default function App() {
const [state, dispatch] = useApp() const [state, dispatch] = useApp()
return ( return (
<>
<CssBaseline />
<DispatchContext.Provider value={dispatch}> <DispatchContext.Provider value={dispatch}>
<main> <Box
<h1>Picturarium</h1> component="main"
sx={{
minHeight: "100vh",
display: "flex",
flexDirection: "column",
alignItems: "center",
}}
>
<Typography variant="h2">Picturarium</Typography>
<RemoteImages images={state.imageIds} /> <RemoteImages images={state.imageIds} />
<button onClick={() => dispatch({ tag: "previousButtonClicked" })}>prev</button>
<button onClick={() => dispatch({ tag: "nextButtonClicked" })}>next</button> <Box
</main> sx={{
display: "flex",
flexDirection: "row",
gap: 2,
justifyContent: "center",
alignItems: "center",
}}
>
<Button
onClick={() => dispatch({ tag: "previousButtonClicked" })}
disabled={Page.isFirst(state.page)}
>
prev
</Button>
<Typography
variant="body1"
sx={{
minWidth: 60,
textAlign: "center",
color: "oklch(65% 0.02 260)",
}}
>
{state.page.page}
</Typography>
<Button onClick={() => dispatch({ tag: "nextButtonClicked" })}>next</Button>
</Box>
</Box>
<ImageModal selectedImage={state.selectedImage} />
</DispatchContext.Provider> </DispatchContext.Provider>
</>
) )
} }

View file

@ -1,41 +0,0 @@
/* === Reset === */
*,
*::before,
*::after {
box-sizing: border-box;
}
html {
line-height: 1.45;
}
body {
margin: 0;
}
h1,
h2,
h3,
h4,
h5,
h6 {
margin: 0;
}
p {
margin: 0;
}
ul {
list-style: none;
padding: 0;
margin: 0;
}
button {
border: none;
padding: 0;
cursor: pointer;
}
/* === main === */

View file

@ -1,7 +1,7 @@
import { StrictMode } from "react" import { StrictMode } from "react"
import { createRoot } from "react-dom/client" import { createRoot } from "react-dom/client"
import App from "./App" import App from "./App"
import "./index.css" import "./style.css"
const rootElement = document.getElementById("root") const rootElement = document.getElementById("root")

3
src/style.css Normal file
View file

@ -0,0 +1,3 @@
:root {
--base-hue: 260;
}