Add reviews page, switch between pages
This commit is contained in:
parent
a3de9ba924
commit
6fe9fbb3cb
|
@ -15,8 +15,10 @@
|
||||||
"@fontsource/roboto": "^5.0.14",
|
"@fontsource/roboto": "^5.0.14",
|
||||||
"@mui/icons-material": "^6.0.0",
|
"@mui/icons-material": "^6.0.0",
|
||||||
"@mui/material": "^6.0.0",
|
"@mui/material": "^6.0.0",
|
||||||
|
"icons": "link:@mui/types/material/icons",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1"
|
"react-dom": "^18.3.1",
|
||||||
|
"yup": "^1.4.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.9.0",
|
"@eslint/js": "^9.9.0",
|
||||||
|
|
|
@ -23,12 +23,18 @@ importers:
|
||||||
'@mui/material':
|
'@mui/material':
|
||||||
specifier: ^6.0.0
|
specifier: ^6.0.0
|
||||||
version: 6.0.0(@emotion/react@11.13.3(@types/react@18.3.4)(react@18.3.1))(@emotion/styled@11.13.0(@emotion/react@11.13.3(@types/react@18.3.4)(react@18.3.1))(@types/react@18.3.4)(react@18.3.1))(@types/react@18.3.4)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
version: 6.0.0(@emotion/react@11.13.3(@types/react@18.3.4)(react@18.3.1))(@emotion/styled@11.13.0(@emotion/react@11.13.3(@types/react@18.3.4)(react@18.3.1))(@types/react@18.3.4)(react@18.3.1))(@types/react@18.3.4)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||||
|
icons:
|
||||||
|
specifier: link:@mui/types/material/icons
|
||||||
|
version: link:@mui/types/material/icons
|
||||||
react:
|
react:
|
||||||
specifier: ^18.3.1
|
specifier: ^18.3.1
|
||||||
version: 18.3.1
|
version: 18.3.1
|
||||||
react-dom:
|
react-dom:
|
||||||
specifier: ^18.3.1
|
specifier: ^18.3.1
|
||||||
version: 18.3.1(react@18.3.1)
|
version: 18.3.1(react@18.3.1)
|
||||||
|
yup:
|
||||||
|
specifier: ^1.4.0
|
||||||
|
version: 1.4.0
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@eslint/js':
|
'@eslint/js':
|
||||||
specifier: ^9.9.0
|
specifier: ^9.9.0
|
||||||
|
@ -1172,6 +1178,9 @@ packages:
|
||||||
prop-types@15.8.1:
|
prop-types@15.8.1:
|
||||||
resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==}
|
resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==}
|
||||||
|
|
||||||
|
property-expr@2.0.6:
|
||||||
|
resolution: {integrity: sha512-SVtmxhRE/CGkn3eZY1T6pC8Nln6Fr/lu1mKSgRud0eC73whjGfoAogbn78LkD8aFL0zz3bAFerKSnOl7NlErBA==}
|
||||||
|
|
||||||
punycode@2.3.1:
|
punycode@2.3.1:
|
||||||
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
|
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
|
@ -1294,6 +1303,9 @@ packages:
|
||||||
text-table@0.2.0:
|
text-table@0.2.0:
|
||||||
resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==}
|
resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==}
|
||||||
|
|
||||||
|
tiny-case@1.0.3:
|
||||||
|
resolution: {integrity: sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q==}
|
||||||
|
|
||||||
to-fast-properties@2.0.0:
|
to-fast-properties@2.0.0:
|
||||||
resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==}
|
resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==}
|
||||||
engines: {node: '>=4'}
|
engines: {node: '>=4'}
|
||||||
|
@ -1302,6 +1314,9 @@ packages:
|
||||||
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
|
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
|
||||||
engines: {node: '>=8.0'}
|
engines: {node: '>=8.0'}
|
||||||
|
|
||||||
|
toposort@2.0.2:
|
||||||
|
resolution: {integrity: sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg==}
|
||||||
|
|
||||||
ts-api-utils@1.3.0:
|
ts-api-utils@1.3.0:
|
||||||
resolution: {integrity: sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==}
|
resolution: {integrity: sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==}
|
||||||
engines: {node: '>=16'}
|
engines: {node: '>=16'}
|
||||||
|
@ -1312,6 +1327,10 @@ packages:
|
||||||
resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
|
resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
|
||||||
engines: {node: '>= 0.8.0'}
|
engines: {node: '>= 0.8.0'}
|
||||||
|
|
||||||
|
type-fest@2.19.0:
|
||||||
|
resolution: {integrity: sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==}
|
||||||
|
engines: {node: '>=12.20'}
|
||||||
|
|
||||||
typescript-eslint@8.3.0:
|
typescript-eslint@8.3.0:
|
||||||
resolution: {integrity: sha512-EvWjwWLwwKDIJuBjk2I6UkV8KEQcwZ0VM10nR1rIunRDIP67QJTZAHBXTX0HW/oI1H10YESF8yWie8fRQxjvFA==}
|
resolution: {integrity: sha512-EvWjwWLwwKDIJuBjk2I6UkV8KEQcwZ0VM10nR1rIunRDIP67QJTZAHBXTX0HW/oI1H10YESF8yWie8fRQxjvFA==}
|
||||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||||
|
@ -1386,6 +1405,9 @@ packages:
|
||||||
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
|
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
|
yup@1.4.0:
|
||||||
|
resolution: {integrity: sha512-wPbgkJRCqIf+OHyiTBQoJiP5PFuAXaWiJK6AmYkzQAh5/c2K9hzSApBZG5wV9KoKSePF7sAxmNSvh/13YHkFDg==}
|
||||||
|
|
||||||
snapshots:
|
snapshots:
|
||||||
|
|
||||||
'@ampproject/remapping@2.3.0':
|
'@ampproject/remapping@2.3.0':
|
||||||
|
@ -2474,6 +2496,8 @@ snapshots:
|
||||||
object-assign: 4.1.1
|
object-assign: 4.1.1
|
||||||
react-is: 16.13.1
|
react-is: 16.13.1
|
||||||
|
|
||||||
|
property-expr@2.0.6: {}
|
||||||
|
|
||||||
punycode@2.3.1: {}
|
punycode@2.3.1: {}
|
||||||
|
|
||||||
queue-microtask@1.2.3: {}
|
queue-microtask@1.2.3: {}
|
||||||
|
@ -2591,12 +2615,16 @@ snapshots:
|
||||||
|
|
||||||
text-table@0.2.0: {}
|
text-table@0.2.0: {}
|
||||||
|
|
||||||
|
tiny-case@1.0.3: {}
|
||||||
|
|
||||||
to-fast-properties@2.0.0: {}
|
to-fast-properties@2.0.0: {}
|
||||||
|
|
||||||
to-regex-range@5.0.1:
|
to-regex-range@5.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
is-number: 7.0.0
|
is-number: 7.0.0
|
||||||
|
|
||||||
|
toposort@2.0.2: {}
|
||||||
|
|
||||||
ts-api-utils@1.3.0(typescript@5.5.4):
|
ts-api-utils@1.3.0(typescript@5.5.4):
|
||||||
dependencies:
|
dependencies:
|
||||||
typescript: 5.5.4
|
typescript: 5.5.4
|
||||||
|
@ -2605,6 +2633,8 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
prelude-ls: 1.2.1
|
prelude-ls: 1.2.1
|
||||||
|
|
||||||
|
type-fest@2.19.0: {}
|
||||||
|
|
||||||
typescript-eslint@8.3.0(eslint@9.9.1)(typescript@5.5.4):
|
typescript-eslint@8.3.0(eslint@9.9.1)(typescript@5.5.4):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@typescript-eslint/eslint-plugin': 8.3.0(@typescript-eslint/parser@8.3.0(eslint@9.9.1)(typescript@5.5.4))(eslint@9.9.1)(typescript@5.5.4)
|
'@typescript-eslint/eslint-plugin': 8.3.0(@typescript-eslint/parser@8.3.0(eslint@9.9.1)(typescript@5.5.4))(eslint@9.9.1)(typescript@5.5.4)
|
||||||
|
@ -2647,3 +2677,10 @@ snapshots:
|
||||||
yaml@1.10.2: {}
|
yaml@1.10.2: {}
|
||||||
|
|
||||||
yocto-queue@0.1.0: {}
|
yocto-queue@0.1.0: {}
|
||||||
|
|
||||||
|
yup@1.4.0:
|
||||||
|
dependencies:
|
||||||
|
property-expr: 2.0.6
|
||||||
|
tiny-case: 1.0.3
|
||||||
|
toposort: 2.0.2
|
||||||
|
type-fest: 2.19.0
|
||||||
|
|
33
src/App.tsx
33
src/App.tsx
|
@ -2,12 +2,41 @@ import "./App.css";
|
||||||
import { Route, Routes } from "react-router-dom";
|
import { Route, Routes } from "react-router-dom";
|
||||||
import Home from "./pages/Home";
|
import Home from "./pages/Home";
|
||||||
import NotFound from "./pages/NotFound";
|
import NotFound from "./pages/NotFound";
|
||||||
|
import ReviewsPage from "./pages/ReviewsPage";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { ServerSideReview } from "./types";
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
|
const [endpoint, setEndpoint] = useState("");
|
||||||
|
const [secure, setSecure] = useState(false);
|
||||||
|
const [currentReviews, setCurrentReviews] = useState<ServerSideReview[]>([]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/" element={<Home />} />
|
<Route
|
||||||
<Route path="*" element={<NotFound />} />
|
path="/"
|
||||||
|
element={
|
||||||
|
<Home
|
||||||
|
endpoint={endpoint}
|
||||||
|
setEndpoint={setEndpoint}
|
||||||
|
secure={secure}
|
||||||
|
setSecure={setSecure}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/reviews"
|
||||||
|
element={
|
||||||
|
<ReviewsPage
|
||||||
|
endpoint={endpoint}
|
||||||
|
secure={secure}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="*"
|
||||||
|
element={<NotFound />}
|
||||||
|
/>
|
||||||
</Routes>
|
</Routes>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,11 +14,13 @@ import React, { useState } from "react";
|
||||||
import { CheckBox } from "@mui/icons-material";
|
import { CheckBox } from "@mui/icons-material";
|
||||||
|
|
||||||
export interface EndpointDialogProps {
|
export interface EndpointDialogProps {
|
||||||
endpoint: [string, React.Dispatch<React.SetStateAction<string>>];
|
endpoint: string;
|
||||||
secure: [boolean, React.Dispatch<React.SetStateAction<boolean>>];
|
setEndpoint: React.Dispatch<React.SetStateAction<string>>;
|
||||||
|
secure: boolean;
|
||||||
|
setSecure: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const EndpointDialog = ({ endpoint, secure }: 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("");
|
||||||
|
@ -49,11 +51,11 @@ const EndpointDialog = ({ endpoint, secure }: EndpointDialogProps) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const isValidEndpoint = () => {
|
const isValidEndpoint = () => {
|
||||||
return !(endpoint[0].startsWith("http://") || endpoint[0].startsWith("https://"));
|
return !(endpoint.startsWith("http://") || endpoint.startsWith("https://"));
|
||||||
}
|
}
|
||||||
|
|
||||||
const saveEndpoint = () => {
|
const saveEndpoint = () => {
|
||||||
localStorage.setItem("apiEndpoint", JSON.stringify(endpoint[0]));
|
localStorage.setItem("apiEndpoint", JSON.stringify(endpoint));
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -72,9 +74,9 @@ const EndpointDialog = ({ endpoint, secure }: EndpointDialogProps) => {
|
||||||
required
|
required
|
||||||
name="endpointBox"
|
name="endpointBox"
|
||||||
type="endpointBox"
|
type="endpointBox"
|
||||||
value={endpoint[0]}
|
value={endpoint}
|
||||||
onChange={({ target }) => {
|
onChange={({ target }) => {
|
||||||
endpoint[1](target.value);
|
setEndpoint(target.value);
|
||||||
|
|
||||||
isValidEndpoint()
|
isValidEndpoint()
|
||||||
? closeTheError()
|
? closeTheError()
|
||||||
|
@ -90,8 +92,8 @@ const EndpointDialog = ({ endpoint, secure }: EndpointDialogProps) => {
|
||||||
<FormControlLabel
|
<FormControlLabel
|
||||||
control={
|
control={
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={secure[0]}
|
checked={secure}
|
||||||
onClick={() => secure[1](!secure[0])}
|
onClick={() => setSecure(!secure)}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
label="HTTPS"
|
label="HTTPS"
|
||||||
|
@ -107,7 +109,7 @@ const EndpointDialog = ({ endpoint, secure }: EndpointDialogProps) => {
|
||||||
closeTheError();
|
closeTheError();
|
||||||
saveEndpoint();
|
saveEndpoint();
|
||||||
|
|
||||||
const fullUri = `${secure[0] ? "https" : "http"}://${endpoint[0]}`;
|
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 {
|
} else {
|
||||||
|
|
53
src/components/PageSwitcher.tsx
Normal file
53
src/components/PageSwitcher.tsx
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
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}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant={selected === 0 ? "contained" : "outlined"}
|
||||||
|
onClick={() => changeSelected(0)}
|
||||||
|
component={Link}
|
||||||
|
to="/"
|
||||||
|
>
|
||||||
|
Post
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
|
||||||
|
const seeReviewsButton = (
|
||||||
|
<Tooltip
|
||||||
|
title="See other's reviews"
|
||||||
|
placement="top"
|
||||||
|
TransitionComponent={Zoom}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant={selected === 1 ? "contained" : "outlined"}
|
||||||
|
onClick={() => changeSelected(1)}
|
||||||
|
component={Link}
|
||||||
|
to="/reviews"
|
||||||
|
>
|
||||||
|
Reviews
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ButtonGroup aria-label="page-switcher" style={{margin: "20px 5px"}}>
|
||||||
|
{postReviewsButton}
|
||||||
|
{seeReviewsButton}
|
||||||
|
</ButtonGroup>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PageSwitcher;
|
87
src/components/ShowReviews.tsx
Normal file
87
src/components/ShowReviews.tsx
Normal file
|
@ -0,0 +1,87 @@
|
||||||
|
import { List, ListItem, ListItemText, Paper, Rating, Slide, Typography } from "@mui/material";
|
||||||
|
import { ServerSideReview } from "../types";
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
export interface ShowReviewsProps {
|
||||||
|
reviews: ServerSideReview[];
|
||||||
|
}
|
||||||
|
|
||||||
|
function ShowReviews({ reviews }: ShowReviewsProps) {
|
||||||
|
if (!reviews.length) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
sx={{color: "rgb(140, 140, 140)", margin: "10px 0"}}
|
||||||
|
>
|
||||||
|
No reviews yet...
|
||||||
|
</Typography>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<List sx={{ width: "100%", maxWidth: 460}}>
|
||||||
|
{reviews.map((review, index) => {
|
||||||
|
return (
|
||||||
|
<Slide
|
||||||
|
direction="up"
|
||||||
|
in
|
||||||
|
del
|
||||||
|
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"}}
|
||||||
|
>
|
||||||
|
{review.username}
|
||||||
|
</Typography>
|
||||||
|
}
|
||||||
|
secondary={
|
||||||
|
<>
|
||||||
|
<Typography
|
||||||
|
component="span"
|
||||||
|
variant="caption"
|
||||||
|
sx={{display: "block"}}
|
||||||
|
>
|
||||||
|
{new Date(review.timestamp).toLocaleString()}
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Typography
|
||||||
|
component="span"
|
||||||
|
variant="body1"
|
||||||
|
sx={{display: "block", margin: "2px 0", fontWeight: "bold"}}
|
||||||
|
>
|
||||||
|
{review.title}
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Typography
|
||||||
|
component="span"
|
||||||
|
variant="body2"
|
||||||
|
sx={{display: "block", maxWidth: 350, margin: "5px 0"}}
|
||||||
|
>
|
||||||
|
{review.content}
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Rating
|
||||||
|
value={review.rating}
|
||||||
|
readOnly
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</ListItem>
|
||||||
|
</Paper>
|
||||||
|
</Slide>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</List>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ShowReviews;
|
|
@ -4,13 +4,20 @@ import ButtonRow, { ActionProps } from "../components/ButtonRow";
|
||||||
import ReviewField, { ReviewFieldProps } from "../components/ReviewField";
|
import ReviewField, { ReviewFieldProps } from "../components/ReviewField";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import EndpointDialog from "../components/EndpointDialong";
|
import EndpointDialog from "../components/EndpointDialong";
|
||||||
|
import PageSwitcher from "../components/PageSwitcher";
|
||||||
|
|
||||||
function Home() {
|
interface HomeProps {
|
||||||
|
endpoint: string;
|
||||||
|
setEndpoint: React.Dispatch<React.SetStateAction<string>>;
|
||||||
|
secure: boolean;
|
||||||
|
setSecure: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function Home({ endpoint, setEndpoint, secure, setSecure }: HomeProps) {
|
||||||
const [rating, setNewRating] = useState<number | null>(0);
|
const [rating, setNewRating] = useState<number | null>(0);
|
||||||
const [modal, showModal] = useState(false);
|
const [modal, showModal] = useState(false);
|
||||||
const [showAlert, changeAlert] = useState(false);
|
const [showAlert, changeAlert] = useState(false);
|
||||||
const [alertText, changeAlertText] = useState("");
|
const [alertText, changeAlertText] = useState("");
|
||||||
const [secure, setSecure] = useState(false);
|
|
||||||
|
|
||||||
const [showInfo, changeInfo] = useState(false);
|
const [showInfo, changeInfo] = useState(false);
|
||||||
const [infoText, changeInfoText] = useState("");
|
const [infoText, changeInfoText] = useState("");
|
||||||
|
@ -132,13 +139,17 @@ function Home() {
|
||||||
const info = <Alert severity="info">{infoText}</Alert>;
|
const info = <Alert severity="info">{infoText}</Alert>;
|
||||||
|
|
||||||
const alert = <Alert severity="error">{alertText}</Alert>;
|
const alert = <Alert severity="error">{alertText}</Alert>;
|
||||||
const [endpoint, setEndpoint] = useState(JSON.parse(localStorage.getItem("apiEndpoint")!) || "");
|
setEndpoint(JSON.parse(localStorage.getItem("apiEndpoint")!) || "");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div id="settings">
|
<PageSwitcher />
|
||||||
<EndpointDialog endpoint={[endpoint, setEndpoint]} secure={[secure, setSecure]}/>
|
<EndpointDialog
|
||||||
</div>
|
endpoint={endpoint}
|
||||||
|
setEndpoint={setEndpoint}
|
||||||
|
secure={secure}
|
||||||
|
setSecure={setSecure}
|
||||||
|
/>
|
||||||
|
|
||||||
<div id="app">
|
<div id="app">
|
||||||
<Typography variant="h3">Simple Review Client</Typography>
|
<Typography variant="h3">Simple Review Client</Typography>
|
||||||
|
|
87
src/pages/ReviewsPage.tsx
Normal file
87
src/pages/ReviewsPage.tsx
Normal file
|
@ -0,0 +1,87 @@
|
||||||
|
import { CircularProgress, IconButton, Tooltip, Typography, Zoom } from '@mui/material';
|
||||||
|
import RefreshIcon from "@mui/icons-material/Refresh";
|
||||||
|
import PageSwitcher from '../components/PageSwitcher';
|
||||||
|
import "../index.css";
|
||||||
|
import ShowReviews from '../components/ShowReviews';
|
||||||
|
import { ServerSideReview } from '../types';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
interface ReviewsPageProps {
|
||||||
|
endpoint: string;
|
||||||
|
secure: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ReviewsPage({ endpoint, secure }: ReviewsPageProps) {
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [statusText, setStatusText] = useState("");
|
||||||
|
const [statusTextColor, setStatusTextColor] = useState("rgb(140, 140, 140)");
|
||||||
|
const [currentReviews, setCurrentReviews] = useState<ServerSideReview[]>([]);
|
||||||
|
|
||||||
|
const minutes = (time: number): number => time * 60000;
|
||||||
|
|
||||||
|
const loadReviews = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
setStatusText("Loading...");
|
||||||
|
setStatusTextColor("rgb(140, 140, 140)");
|
||||||
|
|
||||||
|
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 => {
|
||||||
|
setStatusText(err.toString());
|
||||||
|
setStatusTextColor("rgb(250, 20, 0)");
|
||||||
|
});
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
const refreshButton = (
|
||||||
|
<Tooltip
|
||||||
|
title="Refresh"
|
||||||
|
placement="top"
|
||||||
|
TransitionComponent={Zoom}
|
||||||
|
>
|
||||||
|
<IconButton
|
||||||
|
aria-label="refresh"
|
||||||
|
onClick={loadReviews}
|
||||||
|
>
|
||||||
|
<RefreshIcon />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadReviews();
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<PageSwitcher />
|
||||||
|
{loading
|
||||||
|
? <CircularProgress />
|
||||||
|
: refreshButton
|
||||||
|
}
|
||||||
|
|
||||||
|
<div id="app">
|
||||||
|
<Typography variant="h3">
|
||||||
|
Simple Review Client
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
{loading
|
||||||
|
? <Typography
|
||||||
|
variant="body1"
|
||||||
|
sx={{color: statusTextColor, margin: "10px 0"}}
|
||||||
|
>
|
||||||
|
{statusText}
|
||||||
|
</Typography>
|
||||||
|
: <ShowReviews reviews={currentReviews} />}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ReviewsPage;
|
27
src/types.ts
Normal file
27
src/types.ts
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
import { object, string, number, InferType } from "yup";
|
||||||
|
|
||||||
|
const rating = number()
|
||||||
|
.positive()
|
||||||
|
.max(5)
|
||||||
|
.test(
|
||||||
|
"maxDigitsAfterDecimal",
|
||||||
|
"Rating can only have at most one integer at half intervals (.0 or .5)",
|
||||||
|
(number) => (number! * 10) % 5 === 0,
|
||||||
|
)
|
||||||
|
.required();
|
||||||
|
|
||||||
|
export const userReviewSchema = object({
|
||||||
|
username: string().min(2).max(30).required(),
|
||||||
|
rating: rating,
|
||||||
|
title: string().max(50).notRequired(),
|
||||||
|
content: string().max(2000).notRequired(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const serverReviewSchema = userReviewSchema.shape({
|
||||||
|
id: string().length(6).required(),
|
||||||
|
timestamp: string().required(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type UserSideReview = InferType<typeof userReviewSchema>;
|
||||||
|
export type ServerSideReview = InferType<typeof serverReviewSchema>;
|
||||||
|
export type userRating = InferType<typeof rating>;
|
Loading…
Reference in a new issue