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 { 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 (
|
||||||
<DispatchContext.Provider value={dispatch}>
|
<>
|
||||||
<main>
|
<CssBaseline />
|
||||||
<h1>Picturarium</h1>
|
<DispatchContext.Provider value={dispatch}>
|
||||||
<RemoteImages images={state.imageIds} />
|
<Box
|
||||||
<button onClick={() => dispatch({ tag: "previousButtonClicked" })}>prev</button>
|
component="main"
|
||||||
<button onClick={() => dispatch({ tag: "nextButtonClicked" })}>next</button>
|
sx={{
|
||||||
</main>
|
minHeight: "100vh",
|
||||||
</DispatchContext.Provider>
|
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 { 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
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