diff --git a/package.json b/package.json index a279fef..dedbc63 100644 --- a/package.json +++ b/package.json @@ -22,12 +22,14 @@ "@eslint/js": "^9.9.0", "@types/react": "^18.3.3", "@types/react-dom": "^18.3.0", + "@types/react-router-dom": "^5.3.3", "@vitejs/plugin-react": "^4.3.1", "eslint": "^9.9.0", "eslint-plugin-react-hooks": "^5.1.0-rc.0", "eslint-plugin-react-refresh": "^0.4.9", "globals": "^15.9.0", "prettier": "^3.3.3", + "react-router-dom": "^6.26.2", "typescript": "^5.5.4", "typescript-eslint": "^8.0.1", "vite": "^5.4.1" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 62b64d0..1cb7150 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -39,6 +39,9 @@ importers: '@types/react-dom': specifier: ^18.3.0 version: 18.3.0 + '@types/react-router-dom': + specifier: ^5.3.3 + version: 5.3.3 '@vitejs/plugin-react': specifier: ^4.3.1 version: 4.3.1(vite@5.4.2) @@ -57,6 +60,9 @@ importers: prettier: specifier: ^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: specifier: ^5.5.4 version: 5.5.4 @@ -517,6 +523,10 @@ packages: '@popperjs/core@2.11.8': 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': resolution: {integrity: sha512-2thheikVEuU7ZxFXubPDOtspKn1x0yqaYQwvALVtEcvFhMifPADBrgRPyHV0TF3b+9BgvgjgagVyvA/UqPZHmg==} cpu: [arm] @@ -612,6 +622,9 @@ packages: '@types/estree@1.0.5': 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': resolution: {integrity: sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==} @@ -621,6 +634,12 @@ packages: '@types/react-dom@18.3.0': 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': resolution: {integrity: sha512-RM05tAniPZ5DZPzzNFP+DmrcOdD0efDUxMy3145oljWSl3x9ZV5vhme98gTxFrj2lhXvmGNnUiuDyJgY9IKkNA==} @@ -1175,6 +1194,19 @@ packages: resolution: {integrity: sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==} 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: resolution: {integrity: sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==} peerDependencies: @@ -1794,6 +1826,8 @@ snapshots: '@popperjs/core@2.11.8': {} + '@remix-run/router@1.19.2': {} + '@rollup/rollup-android-arm-eabi@4.21.1': optional: true @@ -1865,6 +1899,8 @@ snapshots: '@types/estree@1.0.5': {} + '@types/history@4.7.11': {} + '@types/parse-json@4.0.2': {} '@types/prop-types@15.7.12': {} @@ -1873,6 +1909,17 @@ snapshots: dependencies: '@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': dependencies: '@types/react': 18.3.4 @@ -2443,6 +2490,18 @@ snapshots: 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): dependencies: '@babel/runtime': 7.25.4 diff --git a/src/App.tsx b/src/App.tsx index 0595624..e47c684 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,155 +1,14 @@ -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"; +import { Route, Routes } from "react-router-dom"; +import Home from "./pages/Home"; +import NotFound from "./pages/NotFound"; function App() { - const [rating, setNewRating] = useState(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 = {infoText}; - - const alert = {alertText}; - const [endpoint, setEndpoint] = useState(JSON.parse(localStorage.getItem("apiEndpoint")!) || ""); - return ( - <> -
- -
- -
- Simple Review Client - -
- - { - event - setNewRating(newRating); - }} - /> - -
-
- - - -
- - - -
- -
- - {alert} - - - - {info} - -
-
- + + } /> + } /> + ); } diff --git a/src/main.tsx b/src/main.tsx index cb2d126..18fc383 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -7,13 +7,14 @@ import { createRoot } from "react-dom/client"; import App from "./App.tsx"; import "./index.css"; import { createTheme } from "@mui/material"; +import { BrowserRouter } from "react-router-dom"; const theme = createTheme({ spacing: 4, }); createRoot(document.getElementById("root")!).render( - + - , + ); diff --git a/src/pages/Home.tsx b/src/pages/Home.tsx new file mode 100644 index 0000000..ab2b9e6 --- /dev/null +++ b/src/pages/Home.tsx @@ -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(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 = {infoText}; + + const alert = {alertText}; + const [endpoint, setEndpoint] = useState(JSON.parse(localStorage.getItem("apiEndpoint")!) || ""); + + return ( + <> +
+ +
+ +
+ Simple Review Client + +
+ + { + event + setNewRating(newRating); + }} + /> + +
+
+ + + +
+ + + +
+ +
+ + {alert} + + + + {info} + +
+
+ + ); +} + +export default Home; diff --git a/src/pages/NotFound.tsx b/src/pages/NotFound.tsx new file mode 100644 index 0000000..4bd981c --- /dev/null +++ b/src/pages/NotFound.tsx @@ -0,0 +1,24 @@ +import { Button, Typography } from "@mui/material"; +import { Link } from "react-router-dom"; + +function NotFound() { + return ( +
+ + Not found + + +
+ + +
+ ) +} + +export default NotFound;