Skip to content
Snippets Groups Projects
Verified Commit 56b526a2 authored by Janne Mareike Koschinski's avatar Janne Mareike Koschinski
Browse files

Implement routing

parent ae7e5b66
Branches
No related tags found
1 merge request!1Replace entire project structure
Pipeline #2581 failed
......@@ -16,14 +16,20 @@
"@types/node": "^12.20.17",
"@types/react": "^17.0.15",
"@types/react-dom": "^17.0.9",
"axios": "^0.21.1",
"mdi-material-ui": "^6.22.1",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-query": "^3.19.1",
"react-router": "^5.2.0",
"react-router-dom": "^5.2.0",
"react-scripts": "4.0.3",
"typescript": "^4.3.5",
"web-vitals": "^1.1.2"
},
"devDependencies": {
"@types/react-router": "^5.1.16",
"@types/react-router-dom": "^5.1.8"
}
},
"node_modules/@babel/code-frame": {
......@@ -3661,6 +3667,12 @@
"@types/node": "*"
}
},
"node_modules/@types/history": {
"version": "4.7.9",
"resolved": "https://registry.npmjs.org/@types/history/-/history-4.7.9.tgz",
"integrity": "sha512-MUc6zSmU3tEVnkQ78q0peeEjKWPUADMlC/t++2bI8WnAG2tvYRPIgHG8lWkXwqc8MsUF6Z2MOf+Mh5sazOmhiQ==",
"dev": true
},
"node_modules/@types/html-minifier-terser": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-5.1.2.tgz",
......@@ -3754,6 +3766,27 @@
"@types/react": "*"
}
},
"node_modules/@types/react-router": {
"version": "5.1.16",
"resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-5.1.16.tgz",
"integrity": "sha512-8d7nR/fNSqlTFGHti0R3F9WwIertOaaA1UEB8/jr5l5mDMOs4CidEgvvYMw4ivqrBK+vtVLxyTj2P+Pr/dtgzg==",
"dev": true,
"dependencies": {
"@types/history": "*",
"@types/react": "*"
}
},
"node_modules/@types/react-router-dom": {
"version": "5.1.8",
"resolved": "https://registry.npmjs.org/@types/react-router-dom/-/react-router-dom-5.1.8.tgz",
"integrity": "sha512-03xHyncBzG0PmDmf8pf3rehtjY0NpUj7TIN46FrT5n1ZWHPZvXz32gUyNboJ+xsL8cpg8bQVLcllptcQHvocrw==",
"dev": true,
"dependencies": {
"@types/history": "*",
"@types/react": "*",
"@types/react-router": "*"
}
},
"node_modules/@types/react-transition-group": {
"version": "4.4.2",
"resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.2.tgz",
......@@ -4683,6 +4716,14 @@
"node": ">=4"
}
},
"node_modules/axios": {
"version": "0.21.1",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.21.1.tgz",
"integrity": "sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA==",
"dependencies": {
"follow-redirects": "^1.10.0"
}
},
"node_modules/axobject-query": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-2.2.0.tgz",
......@@ -24680,6 +24721,12 @@
"@types/node": "*"
}
},
"@types/history": {
"version": "4.7.9",
"resolved": "https://registry.npmjs.org/@types/history/-/history-4.7.9.tgz",
"integrity": "sha512-MUc6zSmU3tEVnkQ78q0peeEjKWPUADMlC/t++2bI8WnAG2tvYRPIgHG8lWkXwqc8MsUF6Z2MOf+Mh5sazOmhiQ==",
"dev": true
},
"@types/html-minifier-terser": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-5.1.2.tgz",
......@@ -24773,6 +24820,27 @@
"@types/react": "*"
}
},
"@types/react-router": {
"version": "5.1.16",
"resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-5.1.16.tgz",
"integrity": "sha512-8d7nR/fNSqlTFGHti0R3F9WwIertOaaA1UEB8/jr5l5mDMOs4CidEgvvYMw4ivqrBK+vtVLxyTj2P+Pr/dtgzg==",
"dev": true,
"requires": {
"@types/history": "*",
"@types/react": "*"
}
},
"@types/react-router-dom": {
"version": "5.1.8",
"resolved": "https://registry.npmjs.org/@types/react-router-dom/-/react-router-dom-5.1.8.tgz",
"integrity": "sha512-03xHyncBzG0PmDmf8pf3rehtjY0NpUj7TIN46FrT5n1ZWHPZvXz32gUyNboJ+xsL8cpg8bQVLcllptcQHvocrw==",
"dev": true,
"requires": {
"@types/history": "*",
"@types/react": "*",
"@types/react-router": "*"
}
},
"@types/react-transition-group": {
"version": "4.4.2",
"resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.2.tgz",
......@@ -25496,6 +25564,14 @@
"resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.3.2.tgz",
"integrity": "sha512-5LMaDRWm8ZFPAEdzTYmgjjEdj1YnQcpfrVajO/sn/LhbpGp0Y0H64c2hLZI1gRMxfA+w1S71Uc/nHaOXgcCvGg=="
},
"axios": {
"version": "0.21.1",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.21.1.tgz",
"integrity": "sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA==",
"requires": {
"follow-redirects": "^1.10.0"
}
},
"axobject-query": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-2.2.0.tgz",
......@@ -8,14 +8,12 @@
"@testing-library/jest-dom": "^5.14.1",
"@testing-library/react": "^11.2.7",
"@testing-library/user-event": "^12.8.3",
"@types/jest": "^26.0.24",
"@types/node": "^12.20.17",
"@types/react": "^17.0.15",
"@types/react-dom": "^17.0.9",
"axios": "^0.21.1",
"mdi-material-ui": "^6.22.1",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-query": "^3.19.1",
"react-router": "^5.2.0",
"react-router-dom": "^5.2.0",
"react-scripts": "4.0.3",
"typescript": "^4.3.5",
......@@ -44,5 +42,13 @@
"last 1 firefox version",
"last 1 safari version"
]
},
"devDependencies": {
"@types/jest": "^26.0.24",
"@types/node": "^12.20.17",
"@types/react": "^17.0.15",
"@types/react-dom": "^17.0.9",
"@types/react-router": "^5.1.16",
"@types/react-router-dom": "^5.1.8"
}
}
import React from 'react';
import './App.css';
import ImageList from "./components/ImageList";
import {BaseUrlProvider} from './api/baseUrlContext';
import {QueryClient, QueryClientProvider} from "react-query";
import UploadView from "./components/UploadView";
import {BrowserRouter, Link, Route} from "react-router-dom";
import ImageListPage from "./pages/ImageListPage";
import {Redirect, Switch} from "react-router";
import ImageDetailPage from "./pages/ImageDetailPage";
import {AppBar, Button, Toolbar, Typography} from "@material-ui/core";
import {useErrorDisplay} from "./components/ErrorContext";
const queryClient = new QueryClient();
function App() {
const [ErrorDisplay, ErrorWrapper] = useErrorDisplay();
return (
<BaseUrlProvider value="http://localhost:8080/">
<QueryClientProvider client={queryClient}>
<BrowserRouter>
<AppBar position="static">
<Toolbar>
<Typography variant="h6">
i.k8r.eu
</Typography>
<Button
color="inherit"
component={Link}
to="/upload"
>
Upload
</Button>
<Button
color="inherit"
component={Link}
to="/images"
>
Images
</Button>
</Toolbar>
</AppBar>
<ErrorDisplay/>
<ErrorWrapper>
<Switch>
<Route path="/upload">
<UploadView/>
<ImageList/>
</Route>
<Route path="/images">
<ImageListPage/>
</Route>
<Route path="/i/:imageId">
<ImageDetailPage/>
</Route>
<Redirect from="/" to="/images"/>
</Switch>
</ErrorWrapper>
</BrowserRouter>
</QueryClientProvider>
</BaseUrlProvider>
);
......
import React from 'react';
import '../App.css';
import {useListImages} from "../api/useListImages";
import ImageView from "./ImageView";
export default function ImageList() {
const {status, data, error} = useListImages();
return (
<div>
<p>{status}</p>
<p>{error as string}</p>
{data?.map(image => (
<ImageView
key={image.id}
image={image}
/>
))}
</div>
);
}
import {Fragment} from "react";
import {Link} from "react-router-dom";
import {Image} from "../api/model/Image";
import {parseRatio} from "../metadata/Ratio";
export interface ImageListViewProps {
images: Image[],
}
export function ImageListView({images}: ImageListViewProps) {
const rows: (Image | null)[][] = [];
if (images) {
let row = [];
for (let i = 0; i < images.length; i++) {
if (i % 4 === 0 && row.length !== 0) {
rows.push(row);
row = [];
}
row.push(images[i]);
}
rows.push([...row, ...([null, null, null].slice(0, 4 - row.length))]);
}
return (
<Fragment>
<style>
{`img {
height: 100%;
max-width: 100%;
margin: 2px;
}
a:first-child img {
margin-left: -8px;
}
a:last-child img {
margin-right: -8px;
}`}
</style>
<div style={{padding: "0 8px", fontSize: 0}}>
{rows.map(row => {
let ratios = 0;
for (const image of row) {
const [w, h] = image ? determineAspectRatio(image) : [1, 1];
ratios += w / h;
}
return (
<div style={{aspectRatio: "" + ratios + "/" + 1}}>
{row.map(image => image && (
<Link to={`/i/${image.id}`}>
<img src={image.url} alt=""/>
</Link>
))}
</div>
)
})}
</div>
</Fragment>
)
}
function determineAspectRatio(image: Image): [number, number] {
const aspectRatio = parseRatio(image.metadata["AspectRatio"]);
if (aspectRatio === undefined) {
console.log("Did not find aspect ratio: ", image.id);
return [1, 1];
} else {
console.log("Found aspect ratio: ", image.id, aspectRatio.numerator, aspectRatio.denominator);
return [aspectRatio.numerator, aspectRatio.denominator];
}
}
import {Image} from "../api/model/Image";
import React, {useMemo, useState} from "react";
import {useUpdateImage} from "../api/useUpdateImage";
import {useDeleteImage} from "../api/useDeleteImage";
import {parseMetadata} from "../metadata/ImageMetadata";
import ImageMetadataView from "./ImageMetadataView";
import {Button, List, ListItem, ListItemIcon, ListItemText, TextField} from "@material-ui/core";
import {Event, Info} from "@material-ui/icons";
import {File, Tag} from "mdi-material-ui";
export interface ImageProps {
image: Image
}
export default function ImageView({image}: ImageProps) {
const {mutate: update, error: updateError, isLoading: updateLoading} = useUpdateImage();
const {mutate: remove, error: removeError, isLoading: removeLoading} = useDeleteImage();
const [title, setTitle] = useState<string>(image.title);
const [description, setDescription] = useState<string>(image.description);
const metadata = useMemo(() =>
parseMetadata(image.metadata),
[image]);
return (
<div>
<p>UpdateError: {JSON.stringify(updateError, null, 2)}</p>
<p>RemoveError: {JSON.stringify(removeError, null, 2)}</p>
<p>UpdateLoading: {JSON.stringify(updateLoading, null, 2)}</p>
<p>RemoveLoading: {JSON.stringify(removeLoading, null, 2)}</p>
<img src={image.url + "l"} alt=""/>
<List dense>
<ListItem dense>
<ListItemIcon><Info/></ListItemIcon>
<ListItemText primary="Id" secondary={image.id}/>
</ListItem>
<ListItem dense>
<ListItemText inset primary="Owner" secondary={image.owner}/>
</ListItem>
<ListItem dense>
<ListItemText inset primary="State" secondary={image.state}/>
</ListItem>
<ListItem dense>
<ListItemIcon><Tag/></ListItemIcon>
{/* TODO: Fix this ugly nesting */}
<ListItemText
primary="Title"
secondary={<TextField
fullWidth
value={title}
onChange={({target: {value}}) =>
setTitle(value)}
/>}
/>
</ListItem>
<ListItem dense>
{/* TODO: Fix this ugly nesting */}
<ListItemText
inset
primary="Description"
secondary={<TextField
fullWidth
value={description}
onChange={({target: {value}}) =>
setDescription(value)}
/>}
/>
</ListItem>
<ListItem dense>
<ListItemIcon><File/></ListItemIcon>
<ListItemText primary="Filename" secondary={image.original_name}/>
</ListItem>
<ListItem dense>
<ListItemText inset primary="MIME Type" secondary={image.mime_type}/>
</ListItem>
<ListItem dense>
<ListItemIcon><Event/></ListItemIcon>
<ListItemText primary="Uploaded At" secondary={image.created_at}/>
</ListItem>
<ListItem dense>
<ListItemText inset primary="Modified At" secondary={image.updated_at}/>
</ListItem>
<ImageMetadataView metadata={metadata}/>
</List>
<Button
onClick={() => update({
...image,
title,
description,
})}
>Save</Button>
<Button
onClick={() => remove(image)}
>Delete</Button>
</div>
)
}
import {useUpdateImage} from "../api/useUpdateImage";
import {useDeleteImage} from "../api/useDeleteImage";
import {useEffect, useMemo, useState} from "react";
import {parseMetadata} from "../metadata/ImageMetadata";
import {
Button,
CircularProgress,
Grid,
LinearProgress,
List,
ListItem,
ListItemIcon,
ListItemText,
TextField
} from "@material-ui/core";
import {Delete, Event, Info, Save} from "@material-ui/icons";
import {File, Tag} from "mdi-material-ui";
import ImageMetadataView from "../components/ImageMetadataView";
import {useGetImage} from "../api/useGetImage";
import {useParams} from "react-router";
import {ErrorPortal} from "../components/ErrorContext";
import {ErrorAlert} from "../components/ErrorAlert";
export interface ImageDetailPageParams {
imageId: string
}
export default function ImageDetailPage() {
const {imageId} = useParams<ImageDetailPageParams>();
const {data: image, error: imageError, isLoading: imageLoading} = useGetImage(imageId);
const {mutate: update, error: updateError, isLoading: updateLoading} = useUpdateImage();
const {mutate: remove, error: removeError, isLoading: removeLoading} = useDeleteImage();
const [title, setTitle] = useState<string>(image?.title || "");
const [description, setDescription] = useState<string>(image?.description || "");
useEffect(() => setTitle(image?.title || ""), [image?.title]);
useEffect(() => setDescription(image?.description || ""), [image?.description]);
const metadata = useMemo(() => parseMetadata(image?.metadata), [image]);
if (image === undefined || metadata === undefined) {
return (
<div>Error: 404</div>
);
}
return (
<div>
{imageLoading && (
<LinearProgress/>
)}
<ErrorPortal>
<ErrorAlert severity="error" error={imageError}/>
<ErrorAlert severity="error" error={updateError}/>
<ErrorAlert severity="error" error={removeError}/>
</ErrorPortal>
<Grid container>
<Grid item xs={12} md={6}>
<img src={image.url + "l"} alt={image.title} style={{width: "100%"}}/>
<List dense>
<ListItem dense>
<ListItemIcon><Info/></ListItemIcon>
<ListItemText primary="Id" secondary={image.id}/>
</ListItem>
<ListItem dense>
<ListItemText inset primary="Owner" secondary={image.owner}/>
</ListItem>
<ListItem dense>
<ListItemText inset primary="State" secondary={image.state}/>
</ListItem>
<ListItem dense>
<ListItemIcon><Tag/></ListItemIcon>
{/* TODO: Fix this ugly nesting */}
<ListItemText
primary="Title"
secondary={<TextField
fullWidth
value={title}
onChange={({target: {value}}) =>
setTitle(value)}
/>}
/>
</ListItem>
<ListItem dense>
{/* TODO: Fix this ugly nesting */}
<ListItemText
inset
primary="Description"
secondary={<TextField
fullWidth
value={description}
onChange={({target: {value}}) =>
setDescription(value)}
/>}
/>
</ListItem>
</List>
<Button
variant="contained"
color="primary"
disabled={updateLoading}
startIcon={updateLoading ? <CircularProgress style={{color: "#fff"}} size="1em"/> : <Save/>}
onClick={() => update({
...image,
title,
description,
})}
>Save</Button>
<Button
variant="contained"
color="secondary"
disabled={removeLoading}
startIcon={removeLoading ? <CircularProgress style={{color: "#fff"}} size="1em"/> : <Delete/>}
onClick={() => remove(image)}
>Delete</Button>
</Grid>
<Grid item xs={12} md={6}>
<List dense>
<ListItem dense>
<ListItemIcon><File/></ListItemIcon>
<ListItemText primary="Filename" secondary={image.original_name}/>
</ListItem>
<ListItem dense>
<ListItemText inset primary="MIME Type" secondary={image.mime_type}/>
</ListItem>
<ListItem dense>
<ListItemIcon><Event/></ListItemIcon>
<ListItemText primary="Uploaded At" secondary={image.created_at}/>
</ListItem>
<ListItem dense>
<ListItemText inset primary="Modified At" secondary={image.updated_at}/>
</ListItem>
<ImageMetadataView metadata={metadata}/>
</List>
</Grid>
</Grid>
</div>
)
}
import {Fragment} from "react";
import {useListImages} from "../api/useListImages";
import {ImageList, ImageListItem, ImageListItemBar, LinearProgress} from "@material-ui/core";
import {Link} from "react-router-dom";
import {ErrorPortal} from "../components/ErrorContext";
import {ErrorAlert} from "../components/ErrorAlert";
export default function ImageListPage() {
const {data: images, error, isLoading} = useListImages();
return (
<Fragment>
{isLoading && (
<LinearProgress/>
)}
<ErrorPortal>
<ErrorAlert severity="error" error={error}/>
</ErrorPortal>
<ImageList cols={5}>
{images?.map(image => (
<ImageListItem component={Link} to={`/i/${image.id}`}>
<img src={image.url} alt={image.title}/>
<ImageListItemBar
title={image.title}
subtitle={image.original_name}
/>
</ImageListItem>
))}
</ImageList>
</Fragment>
);
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment