This commit is contained in:
powermaker450 2024-09-11 23:41:14 -04:00
parent bb0a7e5e04
commit 65faa26245
10 changed files with 158 additions and 171 deletions

View file

@ -26,17 +26,9 @@ function App() {
/> />
<Route <Route
path="/reviews" path="/reviews"
element={ element={<ReviewsPage endpoint={endpoint} secure={secure} />}
<ReviewsPage
endpoint={endpoint}
secure={secure}
/>
}
/>
<Route
path="*"
element={<NotFound />}
/> />
<Route path="*" element={<NotFound />} />
</Routes> </Routes>
); );
} }

View file

@ -13,10 +13,7 @@ interface ActionButtonProps {
const ButtonRow = ({ buttons }: ActionButtonProps) => { const ButtonRow = ({ buttons }: ActionButtonProps) => {
return ( return (
<Grid2 <Grid2 container spacing={2}>
container
spacing={2}
>
{buttons.map((button) => { {buttons.map((button) => {
return ( return (
<Button <Button

View file

@ -22,7 +22,12 @@ export interface EndpointDialogProps {
setSecure: React.Dispatch<React.SetStateAction<boolean>>; 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 [open, setOpen] = useState(false);
const [error, setError] = useState(false); const [error, setError] = useState(false);
const [errorText, setErrorText] = useState(""); const [errorText, setErrorText] = useState("");
@ -45,27 +50,23 @@ const EndpointDialog = ({ endpoint, setEndpoint, secure, setSecure }: EndpointDi
const showTheError = () => { const showTheError = () => {
setErrorText('Please omit "http://" or "https://" from the endpoint.'); setErrorText('Please omit "http://" or "https://" from the endpoint.');
setError(true); setError(true);
} };
const closeTheError = () => { const closeTheError = () => {
setErrorText(""); setErrorText("");
setError(false); setError(false);
} };
const isValidEndpoint = () => { const isValidEndpoint = () => {
return !(endpoint.startsWith("http://") || endpoint.startsWith("https://")); return !(endpoint.startsWith("http://") || endpoint.startsWith("https://"));
} };
const saveEndpoint = () => { const saveEndpoint = () => {
localStorage.setItem("apiEndpoint", JSON.stringify(endpoint)); localStorage.setItem("apiEndpoint", JSON.stringify(endpoint));
} };
const settingsButton = ( const settingsButton = (
<Tooltip <Tooltip title="Settings" placement="top" TransitionComponent={Zoom}>
title="Settings"
placement="top"
TransitionComponent={Zoom}
>
<IconButton aria-label="settings" onClick={openSettings}> <IconButton aria-label="settings" onClick={openSettings}>
<SettingsIcon /> <SettingsIcon />
</IconButton> </IconButton>
@ -76,10 +77,7 @@ const EndpointDialog = ({ endpoint, setEndpoint, secure, setSecure }: EndpointDi
<> <>
{settingsButton} {settingsButton}
<Dialog <Dialog open={open} onClose={closeSettings}>
open={open}
onClose={closeSettings}
>
<DialogTitle>Endpoint URL</DialogTitle> <DialogTitle>Endpoint URL</DialogTitle>
<DialogContent> <DialogContent>
<TextField <TextField
@ -90,9 +88,7 @@ const EndpointDialog = ({ endpoint, setEndpoint, secure, setSecure }: EndpointDi
onChange={({ target }) => { onChange={({ target }) => {
setEndpoint(target.value); setEndpoint(target.value);
isValidEndpoint() isValidEndpoint() ? closeTheError() : showTheError();
? closeTheError()
: showTheError();
}} }}
placeholder="localhost:8080" placeholder="localhost:8080"
error={error} error={error}
@ -103,31 +99,32 @@ const EndpointDialog = ({ endpoint, setEndpoint, secure, setSecure }: EndpointDi
<FormControlLabel <FormControlLabel
control={ control={
<Checkbox <Checkbox checked={secure} onClick={() => setSecure(!secure)} />
checked={secure}
onClick={() => setSecure(!secure)}
/>
} }
label="HTTPS" label="HTTPS"
/> />
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>
<Button <Button
disabled={error} disabled={error}
onClick={() => { onClick={() => {
if (isValidEndpoint()) { if (isValidEndpoint()) {
closeSettings(); closeSettings();
closeTheError(); closeTheError();
saveEndpoint(); 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`); console.log(
} else { `Endpoint set to ${fullUri}.\n\nPOST URI: ${fullUri}/post\nGET URI: ${fullUri}/reviews`,
showTheError(); );
} } else {
}}>Set</Button> showTheError();
}
}}
>
Set
</Button>
</DialogActions> </DialogActions>
</Dialog> </Dialog>
</> </>

View file

@ -1,19 +1,15 @@
import { Button, ButtonGroup, Tooltip, Zoom } from '@mui/material'; import { Button, ButtonGroup, Tooltip, Zoom } from "@mui/material";
import React, { useEffect, useState } from 'react' import React, { useEffect, useState } from "react";
import { Link } from 'react-router-dom'; import { Link } from "react-router-dom";
function PageSwitcher() { function PageSwitcher() {
const [selected, changeSelected] = useState(0); const [selected, changeSelected] = useState(0);
useEffect(() => { useEffect(() => {
changeSelected(document.location.pathname === "/" ? 0 : 1); changeSelected(document.location.pathname === "/" ? 0 : 1);
}) });
const postReviewsButton = ( const postReviewsButton = (
<Tooltip <Tooltip title="Post a review" placement="top" TransitionComponent={Zoom}>
title="Post a review"
placement="top"
TransitionComponent={Zoom}
>
<Button <Button
variant={selected === 0 ? "contained" : "outlined"} variant={selected === 0 ? "contained" : "outlined"}
onClick={() => changeSelected(0)} onClick={() => changeSelected(0)}
@ -43,7 +39,7 @@ function PageSwitcher() {
); );
return ( return (
<ButtonGroup aria-label="page-switcher" style={{margin: "20px 5px"}}> <ButtonGroup aria-label="page-switcher" style={{ margin: "20px 5px" }}>
{postReviewsButton} {postReviewsButton}
{seeReviewsButton} {seeReviewsButton}
</ButtonGroup> </ButtonGroup>

View file

@ -17,7 +17,7 @@ interface ReviewFieldOpts {
const ReviewField = ({ fields }: ReviewFieldOpts) => { const ReviewField = ({ fields }: ReviewFieldOpts) => {
return ( return (
<Box sx={{margin: "20px 0"}}> <Box sx={{ margin: "20px 0" }}>
<Stack spacing={1}> <Stack spacing={1}>
{fields.map((field) => { {fields.map((field) => {
const [errorText, setErrorText] = useState(""); const [errorText, setErrorText] = useState("");
@ -26,12 +26,12 @@ const ReviewField = ({ fields }: ReviewFieldOpts) => {
const showTheError = () => { const showTheError = () => {
setErrorText(field.errorText); setErrorText(field.errorText);
setError(true); setError(true);
} };
const hideTheError = () => { const hideTheError = () => {
setErrorText(""); setErrorText("");
setError(false); setError(false);
} };
return ( return (
<TextField <TextField
@ -41,7 +41,11 @@ const ReviewField = ({ fields }: ReviewFieldOpts) => {
value={field.dynamicState[0]} value={field.dynamicState[0]}
multiline={field.expandable ?? false} multiline={field.expandable ?? false}
onChange={({ target }) => { 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]) field.validateInput(field.dynamicState[0])
? hideTheError() ? hideTheError()

View file

@ -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 { ServerSideReview } from "../types";
import React from 'react' import React from "react";
export interface ShowReviewsProps { export interface ShowReviewsProps {
reviews: ServerSideReview[]; reviews: ServerSideReview[];
@ -12,7 +20,7 @@ function ShowReviews({ reviews }: ShowReviewsProps) {
<> <>
<Typography <Typography
variant="body2" variant="body2"
sx={{color: "rgb(140, 140, 140)", margin: "10px 0"}} sx={{ color: "rgb(140, 140, 140)", margin: "10px 0" }}
> >
No reviews yet... No reviews yet...
</Typography> </Typography>
@ -21,22 +29,17 @@ function ShowReviews({ reviews }: ShowReviewsProps) {
} }
return ( return (
<List sx={{ width: "100%", maxWidth: 460}}> <List sx={{ width: "100%", maxWidth: 460 }}>
{reviews.map((review, index) => { {reviews.map((review, index) => {
return ( return (
<Slide <Slide direction="up" in mountOnEnter key={review.id}>
direction="up" <Paper elevation={2} sx={{ width: "100%", maxWidth: 460 }}>
in <ListItem sx={{ margin: "10px" }}>
mountOnEnter
key={review.id}
>
<Paper elevation={2} sx={{width: "100%", maxWidth: 460}}>
<ListItem sx={{margin: "10px"}}>
<ListItemText <ListItemText
primary={ primary={
<Typography <Typography
variant="h5" variant="h5"
sx={{display: "block", fontWeight: "bold"}} sx={{ display: "block", fontWeight: "bold" }}
> >
{review.username} {review.username}
</Typography> </Typography>
@ -46,7 +49,7 @@ function ShowReviews({ reviews }: ShowReviewsProps) {
<Typography <Typography
component="span" component="span"
variant="caption" variant="caption"
sx={{display: "block"}} sx={{ display: "block" }}
> >
{new Date(review.timestamp).toLocaleString()} {new Date(review.timestamp).toLocaleString()}
</Typography> </Typography>
@ -54,7 +57,11 @@ function ShowReviews({ reviews }: ShowReviewsProps) {
<Typography <Typography
component="span" component="span"
variant="body1" variant="body1"
sx={{display: "block", margin: "2px 0", fontWeight: "bold"}} sx={{
display: "block",
margin: "2px 0",
fontWeight: "bold",
}}
> >
{review.title} {review.title}
</Typography> </Typography>
@ -62,16 +69,17 @@ function ShowReviews({ reviews }: ShowReviewsProps) {
<Typography <Typography
component="span" component="span"
variant="body2" variant="body2"
sx={{display: "block", maxWidth: 350, margin: "5px 0"}} sx={{
display: "block",
maxWidth: 350,
margin: "5px 0",
}}
> >
{review.content} {review.content}
</Typography> </Typography>
<Rating <Rating value={review.rating} readOnly />
value={review.rating} </>
readOnly
/>
</>
} }
/> />
</ListItem> </ListItem>

View file

@ -16,5 +16,5 @@ const theme = createTheme({
createRoot(document.getElementById("root")!).render( createRoot(document.getElementById("root")!).render(
<BrowserRouter> <BrowserRouter>
<App /> <App />
</BrowserRouter> </BrowserRouter>,
); );

View file

@ -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 "../App.css";
import ButtonRow, { ActionProps } from "../components/ButtonRow"; import ButtonRow, { ActionProps } from "../components/ButtonRow";
import ReviewField, { ReviewFieldProps } from "../components/ReviewField"; import ReviewField, { ReviewFieldProps } from "../components/ReviewField";
@ -28,14 +37,14 @@ function Home({ endpoint, setEndpoint, secure, setSecure }: HomeProps) {
dynamicState: useState(""), dynamicState: useState(""),
validateInput: ({ length }) => length >= 2 && length <= 30, validateInput: ({ length }) => length >= 2 && length <= 30,
errorText: "Name must be at least 2 characters", errorText: "Name must be at least 2 characters",
maxLength: 30 maxLength: 30,
}, },
{ {
name: "Title", name: "Title",
dynamicState: useState(""), dynamicState: useState(""),
validateInput: ({ length }) => !length || length <= 50, validateInput: ({ length }) => !length || length <= 50,
errorText: "Title must be less than 50 characters", errorText: "Title must be less than 50 characters",
maxLength: 50 maxLength: 50,
}, },
{ {
name: "Content", name: "Content",
@ -43,7 +52,7 @@ function Home({ endpoint, setEndpoint, secure, setSecure }: HomeProps) {
expandable: true, expandable: true,
validateInput: ({ length }) => !length || length <= 2000, validateInput: ({ length }) => !length || length <= 2000,
errorText: "Content must be less than 2000 characters", errorText: "Content must be less than 2000 characters",
maxLength: 2000 maxLength: 2000,
}, },
]; ];
@ -71,22 +80,27 @@ function Home({ endpoint, setEndpoint, secure, setSecure }: HomeProps) {
return; return;
} }
await fetch(endpoint ? `${secure ? "https" : "http"}://${endpoint}/post` : "http://localhost:8080/post", { await fetch(
method: "POST", endpoint
body: JSON.stringify({ ? `${secure ? "https" : "http"}://${endpoint}/post`
rating: rating, : "http://localhost:8080/post",
username: fields[0].dynamicState[0], {
title: fields[1].dynamicState[0], method: "POST",
content: fields[2].dynamicState[0], body: JSON.stringify({
}), rating: rating,
}) username: fields[0].dynamicState[0],
title: fields[1].dynamicState[0],
content: fields[2].dynamicState[0],
}),
},
)
.then(async (response) => { .then(async (response) => {
const result = await response.json(); const result = await response.json();
if (result.error) { if (result.error) {
changeAlertText(`${result.error.type}: ${result.error.message}`); changeAlertText(`${result.error.type}: ${result.error.message}`);
changeAlert(true); changeAlert(true);
setTimeout(() => { setTimeout(() => {
changeAlert(false); changeAlert(false);
}, 2000); }, 2000);
@ -94,11 +108,10 @@ function Home({ endpoint, setEndpoint, secure, setSecure }: HomeProps) {
changeInfoText(`Success: ${result.message}`); changeInfoText(`Success: ${result.message}`);
changeInfo(true); changeInfo(true);
setTimeout(() => { setTimeout(() => {
changeInfo(false); changeInfo(false);
}, 2000); }, 2000);
} }
}) })
.catch((err) => { .catch((err) => {
changeAlertText(err.toString()); changeAlertText(err.toString());
@ -112,16 +125,14 @@ function Home({ endpoint, setEndpoint, secure, setSecure }: HomeProps) {
}; };
const getEmptyFields = () => { const getEmptyFields = () => {
return ( return !endpoint
!endpoint ? "Endpoint is not set!"
? "Endpoint is not set!" : !rating
: !rating ? "You must input a rating!"
? "You must input a rating!" : fields[0].dynamicState[0].length < 2
: fields[0].dynamicState[0].length < 2 ? "You must enter a username at least 2 characters long!"
? "You must enter a username at least 2 characters long!" : "";
: "" };
);
}
const buttons: ActionProps[] = [ const buttons: ActionProps[] = [
{ {
@ -159,9 +170,9 @@ function Home({ endpoint, setEndpoint, secure, setSecure }: HomeProps) {
precision={0.5} precision={0.5}
size="large" size="large"
value={rating} value={rating}
sx={{marginTop: "10px"}} sx={{ marginTop: "10px" }}
onChange={(event, newRating) => { onChange={(event, newRating) => {
event event;
setNewRating(newRating); setNewRating(newRating);
}} }}
/> />
@ -170,10 +181,7 @@ function Home({ endpoint, setEndpoint, secure, setSecure }: HomeProps) {
<ButtonRow buttons={buttons} /> <ButtonRow buttons={buttons} />
<div <div id="alert-box" style={{ margin: "20px 0" }}>
id="alert-box"
style={{margin: "20px 0"}}
>
<Grow in={showAlert} mountOnEnter unmountOnExit> <Grow in={showAlert} mountOnEnter unmountOnExit>
{alert} {alert}
</Grow> </Grow>
@ -183,21 +191,13 @@ function Home({ endpoint, setEndpoint, secure, setSecure }: HomeProps) {
</Grow> </Grow>
</div> </div>
<Dialog <Dialog open={modal} onClose={() => showModal(false)}>
open={modal}
onClose={() => showModal(false)}
>
<DialogContent> <DialogContent>
<Typography variant="body1"> <Typography variant="body1">{getEmptyFields()}</Typography>
{getEmptyFields()}
</Typography>
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>
<Button <Button variant="contained" onClick={() => showModal(false)}>
variant="contained"
onClick={() => showModal(false)}
>
Ok Ok
</Button> </Button>
</DialogActions> </DialogActions>

View file

@ -4,21 +4,15 @@ import { Link } from "react-router-dom";
function NotFound() { function NotFound() {
return ( return (
<div id="app"> <div id="app">
<Typography variant="h3"> <Typography variant="h3">Not found</Typography>
Not found
</Typography>
<br /> <br />
<Button <Button variant="contained" component={Link} to="/">
variant="contained"
component={Link}
to="/"
>
Back to home Back to home
</Button> </Button>
</div> </div>
) );
} }
export default NotFound; export default NotFound;

View file

@ -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 RefreshIcon from "@mui/icons-material/Refresh";
import PageSwitcher from '../components/PageSwitcher'; import PageSwitcher from "../components/PageSwitcher";
import "../index.css"; import "../index.css";
import ShowReviews from '../components/ShowReviews'; import ShowReviews from "../components/ShowReviews";
import { ServerSideReview } from '../types'; import { ServerSideReview } from "../types";
import { useEffect, useState } from 'react'; import { useEffect, useState } from "react";
interface ReviewsPageProps { interface ReviewsPageProps {
endpoint: string; endpoint: string;
@ -24,31 +30,27 @@ function ReviewsPage({ endpoint, secure }: ReviewsPageProps) {
setStatusText("Loading..."); setStatusText("Loading...");
setStatusTextColor("rgb(140, 140, 140)"); setStatusTextColor("rgb(140, 140, 140)");
await fetch(endpoint ? `${secure ? "https" : "http"}://${endpoint}/reviews` : "http://localhost:8080/reviews") await fetch(
.then(async r => { endpoint
? `${secure ? "https" : "http"}://${endpoint}/reviews`
: "http://localhost:8080/reviews",
)
.then(async (r) => {
const response: ServerSideReview[] = await r.json(); const response: ServerSideReview[] = await r.json();
setCurrentReviews(response.reverse()); setCurrentReviews(response.reverse());
setStatusText(""); setStatusText("");
setLoading(false); setLoading(false);
}) })
.catch(err => { .catch((err) => {
setStatusText(err.toString()); setStatusText(err.toString());
setStatusTextColor("rgb(250, 20, 0)"); setStatusTextColor("rgb(250, 20, 0)");
}); });
}; };
const refreshButton = ( const refreshButton = (
<Tooltip <Tooltip title="Refresh" placement="top" TransitionComponent={Zoom}>
title="Refresh" <IconButton aria-label="refresh" onClick={loadReviews}>
placement="top"
TransitionComponent={Zoom}
>
<IconButton
aria-label="refresh"
onClick={loadReviews}
>
<RefreshIcon /> <RefreshIcon />
</IconButton> </IconButton>
</Tooltip> </Tooltip>
@ -56,29 +58,26 @@ function ReviewsPage({ endpoint, secure }: ReviewsPageProps) {
useEffect(() => { useEffect(() => {
loadReviews(); loadReviews();
}, []) }, []);
return ( return (
<> <>
<PageSwitcher /> <PageSwitcher />
{loading {loading ? <CircularProgress /> : refreshButton}
? <CircularProgress />
: refreshButton
}
<div id="app"> <div id="app">
<Typography variant="h3"> <Typography variant="h3">Simple Review Client</Typography>
Simple Review Client
</Typography>
{loading {loading ? (
? <Typography <Typography
variant="body1" variant="body1"
sx={{color: statusTextColor, margin: "10px 0"}} sx={{ color: statusTextColor, margin: "10px 0" }}
> >
{statusText} {statusText}
</Typography> </Typography>
: <ShowReviews reviews={currentReviews} />} ) : (
<ShowReviews reviews={currentReviews} />
)}
</div> </div>
</> </>
); );