Ditch old css. Use Material UI. Add Modal. Make it work on mobile.
This commit is contained in:
parent
e9cf90180f
commit
ca1a2bf1ca
4 changed files with 134 additions and 59 deletions
147
src/App.tsx
147
src/App.tsx
|
|
@ -1,5 +1,14 @@
|
|||
import { z } from "zod"
|
||||
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: Improve error-handling. Introduce some proper server response.
|
||||
|
|
@ -26,8 +35,8 @@ type Dimension = {
|
|||
height: number
|
||||
}
|
||||
const Dimension = {
|
||||
medium: { width: 200, height: 200 } as Dimension,
|
||||
big: { width: 300, height: 500 } as Dimension,
|
||||
medium: { width: 180, height: 180 } as Dimension,
|
||||
big: { width: 720, height: 720 } as Dimension,
|
||||
}
|
||||
|
||||
// === Page ===
|
||||
|
|
@ -62,6 +71,9 @@ const Page = {
|
|||
key(page: Page): PageKey {
|
||||
return `${page.page}:${page.limit}`
|
||||
},
|
||||
isFirst(page: Page): boolean {
|
||||
return page.page === FIRST_PAGE
|
||||
},
|
||||
}
|
||||
|
||||
// === api ===
|
||||
|
|
@ -230,7 +242,7 @@ function useDispatch() {
|
|||
function RemoteImages({ images }: { images: Remote<ImageId[], AppError> }) {
|
||||
switch (images.tag) {
|
||||
case "loading":
|
||||
return <div>Loading...</div>
|
||||
return <ImagesSkeleton />
|
||||
case "error":
|
||||
return <div>Error: TODO</div>
|
||||
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[] }) {
|
||||
const dispatch = useDispatch()
|
||||
// TODO: Is there some basic `Col/Row` component?
|
||||
return (
|
||||
<div style={{ display: "flex", flexDirection: "column" }}>
|
||||
<Box sx={imageGridStyle(Dimension.medium)}>
|
||||
{images.map((imageId) => (
|
||||
<img
|
||||
style={{ width: Dimension.medium.width, height: Dimension.medium.height }}
|
||||
key={imageId}
|
||||
<Box
|
||||
component="img"
|
||||
src={getImageSource(imageId, Dimension.medium)}
|
||||
onClick={() => dispatch({ tag: "imageClicked", imageId })}
|
||||
sx={{
|
||||
width: Dimension.medium.width,
|
||||
height: Dimension.medium.height,
|
||||
objectFit: "cover",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
key={imageId}
|
||||
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()
|
||||
|
||||
return (
|
||||
<DispatchContext.Provider value={dispatch}>
|
||||
<main>
|
||||
<h1>Picturarium</h1>
|
||||
<RemoteImages images={state.imageIds} />
|
||||
<button onClick={() => dispatch({ tag: "previousButtonClicked" })}>prev</button>
|
||||
<button onClick={() => dispatch({ tag: "nextButtonClicked" })}>next</button>
|
||||
</main>
|
||||
</DispatchContext.Provider>
|
||||
<>
|
||||
<CssBaseline />
|
||||
<DispatchContext.Provider value={dispatch}>
|
||||
<Box
|
||||
component="main"
|
||||
sx={{
|
||||
minHeight: "100vh",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<Typography variant="h2">Picturarium</Typography>
|
||||
|
||||
<RemoteImages images={state.imageIds} />
|
||||
|
||||
<Box
|
||||
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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 === */
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import { StrictMode } from "react"
|
||||
import { createRoot } from "react-dom/client"
|
||||
import App from "./App"
|
||||
import "./index.css"
|
||||
import "./style.css"
|
||||
|
||||
const rootElement = document.getElementById("root")
|
||||
|
||||
|
|
|
|||
3
src/style.css
Normal file
3
src/style.css
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
:root {
|
||||
--base-hue: 260;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue