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
path="/reviews"
element={
<ReviewsPage
endpoint={endpoint}
secure={secure}
/>
}
/>
<Route
path="*"
element={<NotFound />}
element={<ReviewsPage endpoint={endpoint} secure={secure} />}
/>
<Route path="*" element={<NotFound />} />
</Routes>
);
}

View file

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

View file

@ -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,14 +99,10 @@ 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
@ -123,11 +115,16 @@ const EndpointDialog = ({ endpoint, setEndpoint, secure, setSecure }: EndpointDi
const fullUri = `${secure ? "https" : "http"}://${endpoint}`;
console.log(`Endpoint set to ${fullUri}.\n\nPOST URI: ${fullUri}/post\nGET URI: ${fullUri}/reviews`);
console.log(
`Endpoint set to ${fullUri}.\n\nPOST URI: ${fullUri}/post\nGET URI: ${fullUri}/reviews`,
);
} else {
showTheError();
}
}}>Set</Button>
}}
>
Set
</Button>
</DialogActions>
</Dialog>
</>

View file

@ -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>

View file

@ -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()

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 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,15 +69,16 @@ 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 />
</>
}
/>

View file

@ -16,5 +16,5 @@ const theme = createTheme({
createRoot(document.getElementById("root")!).render(
<BrowserRouter>
<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 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,7 +80,11 @@ function Home({ endpoint, setEndpoint, secure, setSecure }: HomeProps) {
return;
}
await fetch(endpoint ? `${secure ? "https" : "http"}://${endpoint}/post` : "http://localhost:8080/post", {
await fetch(
endpoint
? `${secure ? "https" : "http"}://${endpoint}/post`
: "http://localhost:8080/post",
{
method: "POST",
body: JSON.stringify({
rating: rating,
@ -79,7 +92,8 @@ function Home({ endpoint, setEndpoint, secure, setSecure }: HomeProps) {
title: fields[1].dynamicState[0],
content: fields[2].dynamicState[0],
}),
})
},
)
.then(async (response) => {
const result = await response.json();
@ -94,7 +108,6 @@ function Home({ endpoint, setEndpoint, secure, setSecure }: HomeProps) {
changeInfoText(`Success: ${result.message}`);
changeInfo(true);
setTimeout(() => {
changeInfo(false);
}, 2000);
@ -112,16 +125,14 @@ function Home({ endpoint, setEndpoint, secure, setSecure }: HomeProps) {
};
const getEmptyFields = () => {
return (
!endpoint
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>

View file

@ -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;

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 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
{loading ? (
<Typography
variant="body1"
sx={{color: statusTextColor, margin: "10px 0"}}
sx={{ color: statusTextColor, margin: "10px 0" }}
>
{statusText}
</Typography>
: <ShowReviews reviews={currentReviews} />}
) : (
<ShowReviews reviews={currentReviews} />
)}
</div>
</>
);