Prettier
This commit is contained in:
parent
bb0a7e5e04
commit
65faa26245
12
src/App.tsx
12
src/App.tsx
|
@ -26,17 +26,9 @@ function App() {
|
|||
/>
|
||||
<Route
|
||||
path="/reviews"
|
||||
element={
|
||||
<ReviewsPage
|
||||
endpoint={endpoint}
|
||||
secure={secure}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="*"
|
||||
element={<NotFound />}
|
||||
element={<ReviewsPage endpoint={endpoint} secure={secure} />}
|
||||
/>
|
||||
<Route path="*" element={<NotFound />} />
|
||||
</Routes>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -13,10 +13,7 @@ interface ActionButtonProps {
|
|||
|
||||
const ButtonRow = ({ buttons }: ActionButtonProps) => {
|
||||
return (
|
||||
<Grid2
|
||||
container
|
||||
spacing={2}
|
||||
>
|
||||
<Grid2 container spacing={2}>
|
||||
{buttons.map((button) => {
|
||||
return (
|
||||
<Button
|
||||
|
|
|
@ -22,7 +22,12 @@ export interface EndpointDialogProps {
|
|||
setSecure: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
}
|
||||
|
||||
const EndpointDialog = ({ endpoint, setEndpoint, secure, setSecure }: EndpointDialogProps) => {
|
||||
const EndpointDialog = ({
|
||||
endpoint,
|
||||
setEndpoint,
|
||||
secure,
|
||||
setSecure,
|
||||
}: EndpointDialogProps) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [error, setError] = useState(false);
|
||||
const [errorText, setErrorText] = useState("");
|
||||
|
@ -45,27 +50,23 @@ const EndpointDialog = ({ endpoint, setEndpoint, secure, setSecure }: EndpointDi
|
|||
const showTheError = () => {
|
||||
setErrorText('Please omit "http://" or "https://" from the endpoint.');
|
||||
setError(true);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
const closeTheError = () => {
|
||||
setErrorText("");
|
||||
setError(false);
|
||||
}
|
||||
};
|
||||
|
||||
const isValidEndpoint = () => {
|
||||
return !(endpoint.startsWith("http://") || endpoint.startsWith("https://"));
|
||||
}
|
||||
};
|
||||
|
||||
const saveEndpoint = () => {
|
||||
localStorage.setItem("apiEndpoint", JSON.stringify(endpoint));
|
||||
}
|
||||
};
|
||||
|
||||
const settingsButton = (
|
||||
<Tooltip
|
||||
title="Settings"
|
||||
placement="top"
|
||||
TransitionComponent={Zoom}
|
||||
>
|
||||
<Tooltip title="Settings" placement="top" TransitionComponent={Zoom}>
|
||||
<IconButton aria-label="settings" onClick={openSettings}>
|
||||
<SettingsIcon />
|
||||
</IconButton>
|
||||
|
@ -76,10 +77,7 @@ const EndpointDialog = ({ endpoint, setEndpoint, secure, setSecure }: EndpointDi
|
|||
<>
|
||||
{settingsButton}
|
||||
|
||||
<Dialog
|
||||
open={open}
|
||||
onClose={closeSettings}
|
||||
>
|
||||
<Dialog open={open} onClose={closeSettings}>
|
||||
<DialogTitle>Endpoint URL</DialogTitle>
|
||||
<DialogContent>
|
||||
<TextField
|
||||
|
@ -90,9 +88,7 @@ const EndpointDialog = ({ endpoint, setEndpoint, secure, setSecure }: EndpointDi
|
|||
onChange={({ target }) => {
|
||||
setEndpoint(target.value);
|
||||
|
||||
isValidEndpoint()
|
||||
? closeTheError()
|
||||
: showTheError();
|
||||
isValidEndpoint() ? closeTheError() : showTheError();
|
||||
}}
|
||||
placeholder="localhost:8080"
|
||||
error={error}
|
||||
|
@ -103,31 +99,32 @@ const EndpointDialog = ({ endpoint, setEndpoint, secure, setSecure }: EndpointDi
|
|||
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
checked={secure}
|
||||
onClick={() => setSecure(!secure)}
|
||||
/>
|
||||
<Checkbox checked={secure} onClick={() => setSecure(!secure)} />
|
||||
}
|
||||
label="HTTPS"
|
||||
/>
|
||||
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button
|
||||
disabled={error}
|
||||
onClick={() => {
|
||||
if (isValidEndpoint()) {
|
||||
closeSettings();
|
||||
closeTheError();
|
||||
saveEndpoint();
|
||||
if (isValidEndpoint()) {
|
||||
closeSettings();
|
||||
closeTheError();
|
||||
saveEndpoint();
|
||||
|
||||
const fullUri = `${secure ? "https" : "http"}://${endpoint}`;
|
||||
const fullUri = `${secure ? "https" : "http"}://${endpoint}`;
|
||||
|
||||
console.log(`Endpoint set to ${fullUri}.\n\nPOST URI: ${fullUri}/post\nGET URI: ${fullUri}/reviews`);
|
||||
} else {
|
||||
showTheError();
|
||||
}
|
||||
}}>Set</Button>
|
||||
console.log(
|
||||
`Endpoint set to ${fullUri}.\n\nPOST URI: ${fullUri}/post\nGET URI: ${fullUri}/reviews`,
|
||||
);
|
||||
} else {
|
||||
showTheError();
|
||||
}
|
||||
}}
|
||||
>
|
||||
Set
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</>
|
||||
|
|
|
@ -1,19 +1,15 @@
|
|||
import { Button, ButtonGroup, Tooltip, Zoom } from '@mui/material';
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Button, ButtonGroup, Tooltip, Zoom } from "@mui/material";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
function PageSwitcher() {
|
||||
const [selected, changeSelected] = useState(0);
|
||||
useEffect(() => {
|
||||
changeSelected(document.location.pathname === "/" ? 0 : 1);
|
||||
})
|
||||
});
|
||||
|
||||
const postReviewsButton = (
|
||||
<Tooltip
|
||||
title="Post a review"
|
||||
placement="top"
|
||||
TransitionComponent={Zoom}
|
||||
>
|
||||
<Tooltip title="Post a review" placement="top" TransitionComponent={Zoom}>
|
||||
<Button
|
||||
variant={selected === 0 ? "contained" : "outlined"}
|
||||
onClick={() => changeSelected(0)}
|
||||
|
@ -43,7 +39,7 @@ function PageSwitcher() {
|
|||
);
|
||||
|
||||
return (
|
||||
<ButtonGroup aria-label="page-switcher" style={{margin: "20px 5px"}}>
|
||||
<ButtonGroup aria-label="page-switcher" style={{ margin: "20px 5px" }}>
|
||||
{postReviewsButton}
|
||||
{seeReviewsButton}
|
||||
</ButtonGroup>
|
||||
|
|
|
@ -17,7 +17,7 @@ interface ReviewFieldOpts {
|
|||
|
||||
const ReviewField = ({ fields }: ReviewFieldOpts) => {
|
||||
return (
|
||||
<Box sx={{margin: "20px 0"}}>
|
||||
<Box sx={{ margin: "20px 0" }}>
|
||||
<Stack spacing={1}>
|
||||
{fields.map((field) => {
|
||||
const [errorText, setErrorText] = useState("");
|
||||
|
@ -26,12 +26,12 @@ const ReviewField = ({ fields }: ReviewFieldOpts) => {
|
|||
const showTheError = () => {
|
||||
setErrorText(field.errorText);
|
||||
setError(true);
|
||||
}
|
||||
};
|
||||
|
||||
const hideTheError = () => {
|
||||
setErrorText("");
|
||||
setError(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<TextField
|
||||
|
@ -41,7 +41,11 @@ const ReviewField = ({ fields }: ReviewFieldOpts) => {
|
|||
value={field.dynamicState[0]}
|
||||
multiline={field.expandable ?? false}
|
||||
onChange={({ target }) => {
|
||||
field.dynamicState[1](target.value.length <= field.maxLength ? target.value : field.dynamicState[0]);
|
||||
field.dynamicState[1](
|
||||
target.value.length <= field.maxLength
|
||||
? target.value
|
||||
: field.dynamicState[0],
|
||||
);
|
||||
|
||||
field.validateInput(field.dynamicState[0])
|
||||
? hideTheError()
|
||||
|
|
|
@ -1,6 +1,14 @@
|
|||
import { List, ListItem, ListItemText, Paper, Rating, Slide, Typography } from "@mui/material";
|
||||
import {
|
||||
List,
|
||||
ListItem,
|
||||
ListItemText,
|
||||
Paper,
|
||||
Rating,
|
||||
Slide,
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
import { ServerSideReview } from "../types";
|
||||
import React from 'react'
|
||||
import React from "react";
|
||||
|
||||
export interface ShowReviewsProps {
|
||||
reviews: ServerSideReview[];
|
||||
|
@ -12,7 +20,7 @@ function ShowReviews({ reviews }: ShowReviewsProps) {
|
|||
<>
|
||||
<Typography
|
||||
variant="body2"
|
||||
sx={{color: "rgb(140, 140, 140)", margin: "10px 0"}}
|
||||
sx={{ color: "rgb(140, 140, 140)", margin: "10px 0" }}
|
||||
>
|
||||
No reviews yet...
|
||||
</Typography>
|
||||
|
@ -21,22 +29,17 @@ function ShowReviews({ reviews }: ShowReviewsProps) {
|
|||
}
|
||||
|
||||
return (
|
||||
<List sx={{ width: "100%", maxWidth: 460}}>
|
||||
<List sx={{ width: "100%", maxWidth: 460 }}>
|
||||
{reviews.map((review, index) => {
|
||||
return (
|
||||
<Slide
|
||||
direction="up"
|
||||
in
|
||||
mountOnEnter
|
||||
key={review.id}
|
||||
>
|
||||
<Paper elevation={2} sx={{width: "100%", maxWidth: 460}}>
|
||||
<ListItem sx={{margin: "10px"}}>
|
||||
<Slide direction="up" in mountOnEnter key={review.id}>
|
||||
<Paper elevation={2} sx={{ width: "100%", maxWidth: 460 }}>
|
||||
<ListItem sx={{ margin: "10px" }}>
|
||||
<ListItemText
|
||||
primary={
|
||||
<Typography
|
||||
variant="h5"
|
||||
sx={{display: "block", fontWeight: "bold"}}
|
||||
sx={{ display: "block", fontWeight: "bold" }}
|
||||
>
|
||||
{review.username}
|
||||
</Typography>
|
||||
|
@ -46,7 +49,7 @@ function ShowReviews({ reviews }: ShowReviewsProps) {
|
|||
<Typography
|
||||
component="span"
|
||||
variant="caption"
|
||||
sx={{display: "block"}}
|
||||
sx={{ display: "block" }}
|
||||
>
|
||||
{new Date(review.timestamp).toLocaleString()}
|
||||
</Typography>
|
||||
|
@ -54,7 +57,11 @@ function ShowReviews({ reviews }: ShowReviewsProps) {
|
|||
<Typography
|
||||
component="span"
|
||||
variant="body1"
|
||||
sx={{display: "block", margin: "2px 0", fontWeight: "bold"}}
|
||||
sx={{
|
||||
display: "block",
|
||||
margin: "2px 0",
|
||||
fontWeight: "bold",
|
||||
}}
|
||||
>
|
||||
{review.title}
|
||||
</Typography>
|
||||
|
@ -62,16 +69,17 @@ function ShowReviews({ reviews }: ShowReviewsProps) {
|
|||
<Typography
|
||||
component="span"
|
||||
variant="body2"
|
||||
sx={{display: "block", maxWidth: 350, margin: "5px 0"}}
|
||||
sx={{
|
||||
display: "block",
|
||||
maxWidth: 350,
|
||||
margin: "5px 0",
|
||||
}}
|
||||
>
|
||||
{review.content}
|
||||
</Typography>
|
||||
|
||||
<Rating
|
||||
value={review.rating}
|
||||
readOnly
|
||||
/>
|
||||
</>
|
||||
<Rating value={review.rating} readOnly />
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</ListItem>
|
||||
|
|
|
@ -16,5 +16,5 @@ const theme = createTheme({
|
|||
createRoot(document.getElementById("root")!).render(
|
||||
<BrowserRouter>
|
||||
<App />
|
||||
</BrowserRouter>
|
||||
</BrowserRouter>,
|
||||
);
|
||||
|
|
|
@ -1,4 +1,13 @@
|
|||
import { Alert, Button, Dialog, DialogActions, DialogContent, Grow, Rating, Typography } from "@mui/material";
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
Grow,
|
||||
Rating,
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
import "../App.css";
|
||||
import ButtonRow, { ActionProps } from "../components/ButtonRow";
|
||||
import ReviewField, { ReviewFieldProps } from "../components/ReviewField";
|
||||
|
@ -28,14 +37,14 @@ function Home({ endpoint, setEndpoint, secure, setSecure }: HomeProps) {
|
|||
dynamicState: useState(""),
|
||||
validateInput: ({ length }) => length >= 2 && length <= 30,
|
||||
errorText: "Name must be at least 2 characters",
|
||||
maxLength: 30
|
||||
maxLength: 30,
|
||||
},
|
||||
{
|
||||
name: "Title",
|
||||
dynamicState: useState(""),
|
||||
validateInput: ({ length }) => !length || length <= 50,
|
||||
errorText: "Title must be less than 50 characters",
|
||||
maxLength: 50
|
||||
maxLength: 50,
|
||||
},
|
||||
{
|
||||
name: "Content",
|
||||
|
@ -43,7 +52,7 @@ function Home({ endpoint, setEndpoint, secure, setSecure }: HomeProps) {
|
|||
expandable: true,
|
||||
validateInput: ({ length }) => !length || length <= 2000,
|
||||
errorText: "Content must be less than 2000 characters",
|
||||
maxLength: 2000
|
||||
maxLength: 2000,
|
||||
},
|
||||
];
|
||||
|
||||
|
@ -71,22 +80,27 @@ function Home({ endpoint, setEndpoint, secure, setSecure }: HomeProps) {
|
|||
return;
|
||||
}
|
||||
|
||||
await fetch(endpoint ? `${secure ? "https" : "http"}://${endpoint}/post` : "http://localhost:8080/post", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
rating: rating,
|
||||
username: fields[0].dynamicState[0],
|
||||
title: fields[1].dynamicState[0],
|
||||
content: fields[2].dynamicState[0],
|
||||
}),
|
||||
})
|
||||
await fetch(
|
||||
endpoint
|
||||
? `${secure ? "https" : "http"}://${endpoint}/post`
|
||||
: "http://localhost:8080/post",
|
||||
{
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
rating: rating,
|
||||
username: fields[0].dynamicState[0],
|
||||
title: fields[1].dynamicState[0],
|
||||
content: fields[2].dynamicState[0],
|
||||
}),
|
||||
},
|
||||
)
|
||||
.then(async (response) => {
|
||||
const result = await response.json();
|
||||
|
||||
if (result.error) {
|
||||
changeAlertText(`${result.error.type}: ${result.error.message}`);
|
||||
changeAlert(true);
|
||||
|
||||
|
||||
setTimeout(() => {
|
||||
changeAlert(false);
|
||||
}, 2000);
|
||||
|
@ -94,11 +108,10 @@ function Home({ endpoint, setEndpoint, secure, setSecure }: HomeProps) {
|
|||
changeInfoText(`Success: ${result.message}`);
|
||||
changeInfo(true);
|
||||
|
||||
|
||||
setTimeout(() => {
|
||||
changeInfo(false);
|
||||
}, 2000);
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
changeAlertText(err.toString());
|
||||
|
@ -112,16 +125,14 @@ function Home({ endpoint, setEndpoint, secure, setSecure }: HomeProps) {
|
|||
};
|
||||
|
||||
const getEmptyFields = () => {
|
||||
return (
|
||||
!endpoint
|
||||
? "Endpoint is not set!"
|
||||
: !rating
|
||||
? "You must input a rating!"
|
||||
: fields[0].dynamicState[0].length < 2
|
||||
? "You must enter a username at least 2 characters long!"
|
||||
: ""
|
||||
);
|
||||
}
|
||||
return !endpoint
|
||||
? "Endpoint is not set!"
|
||||
: !rating
|
||||
? "You must input a rating!"
|
||||
: fields[0].dynamicState[0].length < 2
|
||||
? "You must enter a username at least 2 characters long!"
|
||||
: "";
|
||||
};
|
||||
|
||||
const buttons: ActionProps[] = [
|
||||
{
|
||||
|
@ -159,9 +170,9 @@ function Home({ endpoint, setEndpoint, secure, setSecure }: HomeProps) {
|
|||
precision={0.5}
|
||||
size="large"
|
||||
value={rating}
|
||||
sx={{marginTop: "10px"}}
|
||||
sx={{ marginTop: "10px" }}
|
||||
onChange={(event, newRating) => {
|
||||
event
|
||||
event;
|
||||
setNewRating(newRating);
|
||||
}}
|
||||
/>
|
||||
|
@ -170,10 +181,7 @@ function Home({ endpoint, setEndpoint, secure, setSecure }: HomeProps) {
|
|||
|
||||
<ButtonRow buttons={buttons} />
|
||||
|
||||
<div
|
||||
id="alert-box"
|
||||
style={{margin: "20px 0"}}
|
||||
>
|
||||
<div id="alert-box" style={{ margin: "20px 0" }}>
|
||||
<Grow in={showAlert} mountOnEnter unmountOnExit>
|
||||
{alert}
|
||||
</Grow>
|
||||
|
@ -183,21 +191,13 @@ function Home({ endpoint, setEndpoint, secure, setSecure }: HomeProps) {
|
|||
</Grow>
|
||||
</div>
|
||||
|
||||
<Dialog
|
||||
open={modal}
|
||||
onClose={() => showModal(false)}
|
||||
>
|
||||
<Dialog open={modal} onClose={() => showModal(false)}>
|
||||
<DialogContent>
|
||||
<Typography variant="body1">
|
||||
{getEmptyFields()}
|
||||
</Typography>
|
||||
<Typography variant="body1">{getEmptyFields()}</Typography>
|
||||
</DialogContent>
|
||||
|
||||
<DialogActions>
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={() => showModal(false)}
|
||||
>
|
||||
<Button variant="contained" onClick={() => showModal(false)}>
|
||||
Ok
|
||||
</Button>
|
||||
</DialogActions>
|
||||
|
|
|
@ -4,21 +4,15 @@ import { Link } from "react-router-dom";
|
|||
function NotFound() {
|
||||
return (
|
||||
<div id="app">
|
||||
<Typography variant="h3">
|
||||
Not found
|
||||
</Typography>
|
||||
<Typography variant="h3">Not found</Typography>
|
||||
|
||||
<br />
|
||||
|
||||
<Button
|
||||
variant="contained"
|
||||
component={Link}
|
||||
to="/"
|
||||
>
|
||||
<Button variant="contained" component={Link} to="/">
|
||||
Back to home
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export default NotFound;
|
||||
|
|
|
@ -1,10 +1,16 @@
|
|||
import { CircularProgress, IconButton, Tooltip, Typography, Zoom } from '@mui/material';
|
||||
import {
|
||||
CircularProgress,
|
||||
IconButton,
|
||||
Tooltip,
|
||||
Typography,
|
||||
Zoom,
|
||||
} from "@mui/material";
|
||||
import RefreshIcon from "@mui/icons-material/Refresh";
|
||||
import PageSwitcher from '../components/PageSwitcher';
|
||||
import PageSwitcher from "../components/PageSwitcher";
|
||||
import "../index.css";
|
||||
import ShowReviews from '../components/ShowReviews';
|
||||
import { ServerSideReview } from '../types';
|
||||
import { useEffect, useState } from 'react';
|
||||
import ShowReviews from "../components/ShowReviews";
|
||||
import { ServerSideReview } from "../types";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
interface ReviewsPageProps {
|
||||
endpoint: string;
|
||||
|
@ -24,31 +30,27 @@ function ReviewsPage({ endpoint, secure }: ReviewsPageProps) {
|
|||
setStatusText("Loading...");
|
||||
setStatusTextColor("rgb(140, 140, 140)");
|
||||
|
||||
await fetch(endpoint ? `${secure ? "https" : "http"}://${endpoint}/reviews` : "http://localhost:8080/reviews")
|
||||
.then(async r => {
|
||||
await fetch(
|
||||
endpoint
|
||||
? `${secure ? "https" : "http"}://${endpoint}/reviews`
|
||||
: "http://localhost:8080/reviews",
|
||||
)
|
||||
.then(async (r) => {
|
||||
const response: ServerSideReview[] = await r.json();
|
||||
|
||||
setCurrentReviews(response.reverse());
|
||||
setStatusText("");
|
||||
setLoading(false);
|
||||
})
|
||||
.catch(err => {
|
||||
})
|
||||
.catch((err) => {
|
||||
setStatusText(err.toString());
|
||||
setStatusTextColor("rgb(250, 20, 0)");
|
||||
});
|
||||
|
||||
});
|
||||
};
|
||||
|
||||
const refreshButton = (
|
||||
<Tooltip
|
||||
title="Refresh"
|
||||
placement="top"
|
||||
TransitionComponent={Zoom}
|
||||
>
|
||||
<IconButton
|
||||
aria-label="refresh"
|
||||
onClick={loadReviews}
|
||||
>
|
||||
<Tooltip title="Refresh" placement="top" TransitionComponent={Zoom}>
|
||||
<IconButton aria-label="refresh" onClick={loadReviews}>
|
||||
<RefreshIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
|
@ -56,29 +58,26 @@ function ReviewsPage({ endpoint, secure }: ReviewsPageProps) {
|
|||
|
||||
useEffect(() => {
|
||||
loadReviews();
|
||||
}, [])
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageSwitcher />
|
||||
{loading
|
||||
? <CircularProgress />
|
||||
: refreshButton
|
||||
}
|
||||
{loading ? <CircularProgress /> : refreshButton}
|
||||
|
||||
<div id="app">
|
||||
<Typography variant="h3">
|
||||
Simple Review Client
|
||||
</Typography>
|
||||
<Typography variant="h3">Simple Review Client</Typography>
|
||||
|
||||
{loading
|
||||
? <Typography
|
||||
variant="body1"
|
||||
sx={{color: statusTextColor, margin: "10px 0"}}
|
||||
>
|
||||
{statusText}
|
||||
</Typography>
|
||||
: <ShowReviews reviews={currentReviews} />}
|
||||
{loading ? (
|
||||
<Typography
|
||||
variant="body1"
|
||||
sx={{ color: statusTextColor, margin: "10px 0" }}
|
||||
>
|
||||
{statusText}
|
||||
</Typography>
|
||||
) : (
|
||||
<ShowReviews reviews={currentReviews} />
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
|
Loading…
Reference in a new issue