Initial implementation of routing

This commit is contained in:
powermaker450 2024-09-10 15:44:10 -04:00
parent b1675f3229
commit 0d6f7e3e8d
6 changed files with 251 additions and 150 deletions

View file

@ -22,12 +22,14 @@
"@eslint/js": "^9.9.0", "@eslint/js": "^9.9.0",
"@types/react": "^18.3.3", "@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0", "@types/react-dom": "^18.3.0",
"@types/react-router-dom": "^5.3.3",
"@vitejs/plugin-react": "^4.3.1", "@vitejs/plugin-react": "^4.3.1",
"eslint": "^9.9.0", "eslint": "^9.9.0",
"eslint-plugin-react-hooks": "^5.1.0-rc.0", "eslint-plugin-react-hooks": "^5.1.0-rc.0",
"eslint-plugin-react-refresh": "^0.4.9", "eslint-plugin-react-refresh": "^0.4.9",
"globals": "^15.9.0", "globals": "^15.9.0",
"prettier": "^3.3.3", "prettier": "^3.3.3",
"react-router-dom": "^6.26.2",
"typescript": "^5.5.4", "typescript": "^5.5.4",
"typescript-eslint": "^8.0.1", "typescript-eslint": "^8.0.1",
"vite": "^5.4.1" "vite": "^5.4.1"

View file

@ -39,6 +39,9 @@ importers:
'@types/react-dom': '@types/react-dom':
specifier: ^18.3.0 specifier: ^18.3.0
version: 18.3.0 version: 18.3.0
'@types/react-router-dom':
specifier: ^5.3.3
version: 5.3.3
'@vitejs/plugin-react': '@vitejs/plugin-react':
specifier: ^4.3.1 specifier: ^4.3.1
version: 4.3.1(vite@5.4.2) version: 4.3.1(vite@5.4.2)
@ -57,6 +60,9 @@ importers:
prettier: prettier:
specifier: ^3.3.3 specifier: ^3.3.3
version: 3.3.3 version: 3.3.3
react-router-dom:
specifier: ^6.26.2
version: 6.26.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
typescript: typescript:
specifier: ^5.5.4 specifier: ^5.5.4
version: 5.5.4 version: 5.5.4
@ -517,6 +523,10 @@ packages:
'@popperjs/core@2.11.8': '@popperjs/core@2.11.8':
resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==} resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==}
'@remix-run/router@1.19.2':
resolution: {integrity: sha512-baiMx18+IMuD1yyvOGaHM9QrVUPGGG0jC+z+IPHnRJWUAUvaKuWKyE8gjDj2rzv3sz9zOGoRSPgeBVHRhZnBlA==}
engines: {node: '>=14.0.0'}
'@rollup/rollup-android-arm-eabi@4.21.1': '@rollup/rollup-android-arm-eabi@4.21.1':
resolution: {integrity: sha512-2thheikVEuU7ZxFXubPDOtspKn1x0yqaYQwvALVtEcvFhMifPADBrgRPyHV0TF3b+9BgvgjgagVyvA/UqPZHmg==} resolution: {integrity: sha512-2thheikVEuU7ZxFXubPDOtspKn1x0yqaYQwvALVtEcvFhMifPADBrgRPyHV0TF3b+9BgvgjgagVyvA/UqPZHmg==}
cpu: [arm] cpu: [arm]
@ -612,6 +622,9 @@ packages:
'@types/estree@1.0.5': '@types/estree@1.0.5':
resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==} resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==}
'@types/history@4.7.11':
resolution: {integrity: sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA==}
'@types/parse-json@4.0.2': '@types/parse-json@4.0.2':
resolution: {integrity: sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==} resolution: {integrity: sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==}
@ -621,6 +634,12 @@ packages:
'@types/react-dom@18.3.0': '@types/react-dom@18.3.0':
resolution: {integrity: sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==} resolution: {integrity: sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==}
'@types/react-router-dom@5.3.3':
resolution: {integrity: sha512-kpqnYK4wcdm5UaWI3fLcELopqLrHgLqNsdpHauzlQktfkHL3npOSwtj1Uz9oKBAzs7lFtVkV8j83voAz2D8fhw==}
'@types/react-router@5.1.20':
resolution: {integrity: sha512-jGjmu/ZqS7FjSH6owMcD5qpq19+1RS9DeVRqfl1FeBMxTDQAGwlMWOcs52NDoXaNKyG3d1cYQFMs9rCrb88o9Q==}
'@types/react-transition-group@4.4.11': '@types/react-transition-group@4.4.11':
resolution: {integrity: sha512-RM05tAniPZ5DZPzzNFP+DmrcOdD0efDUxMy3145oljWSl3x9ZV5vhme98gTxFrj2lhXvmGNnUiuDyJgY9IKkNA==} resolution: {integrity: sha512-RM05tAniPZ5DZPzzNFP+DmrcOdD0efDUxMy3145oljWSl3x9ZV5vhme98gTxFrj2lhXvmGNnUiuDyJgY9IKkNA==}
@ -1175,6 +1194,19 @@ packages:
resolution: {integrity: sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==} resolution: {integrity: sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
react-router-dom@6.26.2:
resolution: {integrity: sha512-z7YkaEW0Dy35T3/QKPYB1LjMK2R1fxnHO8kWpUMTBdfVzZrWOiY9a7CtN8HqdWtDUWd5FY6Dl8HFsqVwH4uOtQ==}
engines: {node: '>=14.0.0'}
peerDependencies:
react: '>=16.8'
react-dom: '>=16.8'
react-router@6.26.2:
resolution: {integrity: sha512-tvN1iuT03kHgOFnLPfLJ8V95eijteveqdOSk+srqfePtQvqCExB8eHOYnlilbOcyJyKnYkr1vJvf7YqotAJu1A==}
engines: {node: '>=14.0.0'}
peerDependencies:
react: '>=16.8'
react-transition-group@4.4.5: react-transition-group@4.4.5:
resolution: {integrity: sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==} resolution: {integrity: sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==}
peerDependencies: peerDependencies:
@ -1794,6 +1826,8 @@ snapshots:
'@popperjs/core@2.11.8': {} '@popperjs/core@2.11.8': {}
'@remix-run/router@1.19.2': {}
'@rollup/rollup-android-arm-eabi@4.21.1': '@rollup/rollup-android-arm-eabi@4.21.1':
optional: true optional: true
@ -1865,6 +1899,8 @@ snapshots:
'@types/estree@1.0.5': {} '@types/estree@1.0.5': {}
'@types/history@4.7.11': {}
'@types/parse-json@4.0.2': {} '@types/parse-json@4.0.2': {}
'@types/prop-types@15.7.12': {} '@types/prop-types@15.7.12': {}
@ -1873,6 +1909,17 @@ snapshots:
dependencies: dependencies:
'@types/react': 18.3.4 '@types/react': 18.3.4
'@types/react-router-dom@5.3.3':
dependencies:
'@types/history': 4.7.11
'@types/react': 18.3.4
'@types/react-router': 5.1.20
'@types/react-router@5.1.20':
dependencies:
'@types/history': 4.7.11
'@types/react': 18.3.4
'@types/react-transition-group@4.4.11': '@types/react-transition-group@4.4.11':
dependencies: dependencies:
'@types/react': 18.3.4 '@types/react': 18.3.4
@ -2443,6 +2490,18 @@ snapshots:
react-refresh@0.14.2: {} react-refresh@0.14.2: {}
react-router-dom@6.26.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
dependencies:
'@remix-run/router': 1.19.2
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
react-router: 6.26.2(react@18.3.1)
react-router@6.26.2(react@18.3.1):
dependencies:
'@remix-run/router': 1.19.2
react: 18.3.1
react-transition-group@4.4.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1): react-transition-group@4.4.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
dependencies: dependencies:
'@babel/runtime': 7.25.4 '@babel/runtime': 7.25.4

View file

@ -1,155 +1,14 @@
import { Alert, Grow, Rating, Typography } from "@mui/material";
import "./App.css"; import "./App.css";
import ButtonRow, { ActionProps } from "./components/ButtonRow"; import { Route, Routes } from "react-router-dom";
import ReviewField, { ReviewFieldProps } from "./components/ReviewField"; import Home from "./pages/Home";
import { useState } from "react"; import NotFound from "./pages/NotFound";
import EndpointDialog from "./components/EndpointDialong";
function App() { function App() {
const [rating, setNewRating] = useState<number | null>(0);
const [showAlert, changeAlert] = useState(false);
const [alertText, changeAlertText] = useState("");
const [secure, setSecure] = useState(false);
const [showInfo, changeInfo] = useState(false);
const [infoText, changeInfoText] = useState("");
const fields: ReviewFieldProps[] = [
{
name: "Name",
dynamicState: useState(""),
validateInput: ({ length }) => length >= 2 && length <= 30,
errorText: "Name must be at least 2 characters",
maxLength: 30
},
{
name: "Title",
dynamicState: useState(""),
validateInput: ({ length }) => !length || length <= 50,
errorText: "Title must be less than 50 characters",
maxLength: 50
},
{
name: "Content",
dynamicState: useState(""),
expandable: true,
validateInput: ({ length }) => !length || length <= 2000,
errorText: "Content must be less than 2000 characters",
maxLength: 2000
},
];
const clearValues = () => {
fields.forEach((field) => {
field.dynamicState[1]("");
});
setNewRating(null);
};
const showThenHide = async () => {
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);
} else {
changeInfoText(`Success: ${result.message}`);
changeInfo(true);
setTimeout(() => {
changeInfo(false);
}, 2000);
}
})
.catch((err) => {
changeAlertText(err.toString());
changeAlert(true);
setTimeout(() => {
changeAlert(false);
}, 2500);
});
};
const buttons: ActionProps[] = [
{
name: "Clear",
type: "outlined",
action: clearValues,
},
{
name: "Submit",
type: "contained",
action: showThenHide,
},
];
const info = <Alert severity="info">{infoText}</Alert>;
const alert = <Alert severity="error">{alertText}</Alert>;
const [endpoint, setEndpoint] = useState(JSON.parse(localStorage.getItem("apiEndpoint")!) || "");
return ( return (
<> <Routes>
<div id="settings"> <Route path="/" element={<Home />} />
<EndpointDialog endpoint={[endpoint, setEndpoint]} secure={[secure, setSecure]}/> <Route path="*" element={<NotFound />} />
</div> </Routes>
<div id="app">
<Typography variant="h3">Simple Review Client</Typography>
<br />
<Rating
name="review-rating"
precision={0.5}
size="large"
value={rating}
onChange={(event, newRating) => {
event
setNewRating(newRating);
}}
/>
<br />
<br />
<ReviewField fields={fields} />
<br />
<ButtonRow buttons={buttons} />
<br />
<div id="alert-box">
<Grow in={showAlert} mountOnEnter unmountOnExit>
{alert}
</Grow>
<Grow in={showInfo} mountOnEnter unmountOnExit>
{info}
</Grow>
</div>
</div>
</>
); );
} }

View file

@ -7,13 +7,14 @@ import { createRoot } from "react-dom/client";
import App from "./App.tsx"; import App from "./App.tsx";
import "./index.css"; import "./index.css";
import { createTheme } from "@mui/material"; import { createTheme } from "@mui/material";
import { BrowserRouter } from "react-router-dom";
const theme = createTheme({ const theme = createTheme({
spacing: 4, spacing: 4,
}); });
createRoot(document.getElementById("root")!).render( createRoot(document.getElementById("root")!).render(
<StrictMode> <BrowserRouter>
<App /> <App />
</StrictMode>, </BrowserRouter>
); );

156
src/pages/Home.tsx Normal file
View file

@ -0,0 +1,156 @@
import { Alert, Grow, Rating, Typography } from "@mui/material";
import "../App.css";
import ButtonRow, { ActionProps } from "../components/ButtonRow";
import ReviewField, { ReviewFieldProps } from "../components/ReviewField";
import { useState } from "react";
import EndpointDialog from "../components/EndpointDialong";
function Home() {
const [rating, setNewRating] = useState<number | null>(0);
const [showAlert, changeAlert] = useState(false);
const [alertText, changeAlertText] = useState("");
const [secure, setSecure] = useState(false);
const [showInfo, changeInfo] = useState(false);
const [infoText, changeInfoText] = useState("");
const fields: ReviewFieldProps[] = [
{
name: "Name",
dynamicState: useState(""),
validateInput: ({ length }) => length >= 2 && length <= 30,
errorText: "Name must be at least 2 characters",
maxLength: 30
},
{
name: "Title",
dynamicState: useState(""),
validateInput: ({ length }) => !length || length <= 50,
errorText: "Title must be less than 50 characters",
maxLength: 50
},
{
name: "Content",
dynamicState: useState(""),
expandable: true,
validateInput: ({ length }) => !length || length <= 2000,
errorText: "Content must be less than 2000 characters",
maxLength: 2000
},
];
const clearValues = () => {
fields.forEach((field) => {
field.dynamicState[1]("");
});
setNewRating(null);
};
const showThenHide = async () => {
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);
} else {
changeInfoText(`Success: ${result.message}`);
changeInfo(true);
setTimeout(() => {
changeInfo(false);
}, 2000);
}
})
.catch((err) => {
changeAlertText(err.toString());
changeAlert(true);
setTimeout(() => {
changeAlert(false);
}, 2500);
});
};
const buttons: ActionProps[] = [
{
name: "Clear",
type: "outlined",
action: clearValues,
},
{
name: "Submit",
type: "contained",
action: showThenHide,
},
];
const info = <Alert severity="info">{infoText}</Alert>;
const alert = <Alert severity="error">{alertText}</Alert>;
const [endpoint, setEndpoint] = useState(JSON.parse(localStorage.getItem("apiEndpoint")!) || "");
return (
<>
<div id="settings">
<EndpointDialog endpoint={[endpoint, setEndpoint]} secure={[secure, setSecure]}/>
</div>
<div id="app">
<Typography variant="h3">Simple Review Client</Typography>
<br />
<Rating
name="review-rating"
precision={0.5}
size="large"
value={rating}
onChange={(event, newRating) => {
event
setNewRating(newRating);
}}
/>
<br />
<br />
<ReviewField fields={fields} />
<br />
<ButtonRow buttons={buttons} />
<br />
<div id="alert-box">
<Grow in={showAlert} mountOnEnter unmountOnExit>
{alert}
</Grow>
<Grow in={showInfo} mountOnEnter unmountOnExit>
{info}
</Grow>
</div>
</div>
</>
);
}
export default Home;

24
src/pages/NotFound.tsx Normal file
View file

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