diff --git a/backend/server.js b/backend/server.js index 7f9c7c8ae8..60b01f9b0c 100644 --- a/backend/server.js +++ b/backend/server.js @@ -71,4 +71,4 @@ server.use((err, req, res, next) => { // eslint-disable-line }) }) -module.exports = server +module.exports = server \ No newline at end of file diff --git a/codegrade_mvp.test.js b/codegrade_mvp.test.js index 8edffb4616..5886af9f40 100644 --- a/codegrade_mvp.test.js +++ b/codegrade_mvp.test.js @@ -206,4 +206,4 @@ describe('Advanced Applications', () => { await screen.findByText('Article 1 was deleted, Foo!', queryOptions, waitForOptions) }) }) -}) +}) \ No newline at end of file diff --git a/frontend/components/App.js b/frontend/components/App.js index c4b6d6ce5c..2324a97143 100644 --- a/frontend/components/App.js +++ b/frontend/components/App.js @@ -1,10 +1,11 @@ -import React, { useState } from 'react' +import React, { useState, } from 'react' import { NavLink, Routes, Route, useNavigate } from 'react-router-dom' import Articles from './Articles' import LoginForm from './LoginForm' import Message from './Message' import ArticleForm from './ArticleForm' import Spinner from './Spinner' +import axios from 'axios' const articlesUrl = 'http://localhost:9000/api/articles' const loginUrl = 'http://localhost:9000/api/login' @@ -18,8 +19,8 @@ export default function App() { // ✨ Research `useNavigate` in React Router v.6 const navigate = useNavigate() - const redirectToLogin = () => { /* ✨ implement */ } - const redirectToArticles = () => { /* ✨ implement */ } + const redirectToLogin = () => { navigate('/')} + const redirectToArticles = () => { navigate('/articles')} const logout = () => { // ✨ implement @@ -27,6 +28,9 @@ export default function App() { // and a message saying "Goodbye!" should be set in its proper state. // In any case, we should redirect the browser back to the login screen, // using the helper above. + localStorage.removeItem('token') + setMessage('Goodbye!') + redirectToLogin() } const login = ({ username, password }) => { @@ -36,6 +40,21 @@ export default function App() { // On success, we should set the token to local storage in a 'token' key, // put the server success message in its proper state, and redirect // to the Articles screen. Don't forget to turn off the spinner! + setSpinnerOn(true) + setMessage('') + axios.post(loginUrl, { username, password }) + .then(res => { + window.localStorage.setItem('token', res.data.token) + setMessage(res.data.message) + redirectToArticles() + }) + .catch(err => { + const responseMessage = err?.response?.data?.message + setMessage(responseMessage || `Somethin' horrible logging in: ${err.message}`) + }) + .finally(() => { + setSpinnerOn(false) + }) } const getArticles = () => { @@ -47,6 +66,22 @@ export default function App() { // If something goes wrong, check the status of the response: // if it's a 401 the token might have gone bad, and we should redirect to login. // Don't forget to turn off the spinner! + setSpinnerOn(true) + setMessage('') + axios.get(articlesUrl, { headers: { Authorization: localStorage.getItem('token') } }) + .then(res => { + setMessage(res.data.message) + setArticles(res.data.articles) + }) + .catch(err => { + setMessage(err?.response?.data?.message || 'Something bad happened') + if (err.response.status == 401) { + redirectToLogin() + } + }) + .finally(() => { + setSpinnerOn(false) + }) } const postArticle = article => { @@ -54,22 +89,287 @@ export default function App() { // The flow is very similar to the `getArticles` function. // You'll know what to do! Use log statements or breakpoints // to inspect the response from the server. + setSpinnerOn(true) + setMessage('') + axios.post(articlesUrl, article, { headers: { Authorization: localStorage.getItem('token') } }) + .then(res => { + setMessage(res.data.message) + setArticles(articles => { + return articles.concat(res.data.article) + }) + }) + .catch(err => { + setMessage(err?.response?.data?.message || 'Something bad happened') + if (err.response.status == 401) { + redirectToLogin() + } + }) + .finally(() => { + setSpinnerOn(false) + }) } const updateArticle = ({ article_id, article }) => { // ✨ implement // You got this! + setMessage('') + setSpinnerOn(true) + axios.put(`${articlesUrl}/${article_id}`, article, { headers: { Authorization: localStorage.getItem('token') } }) + .then(res => { + setMessage(res.data.message) + setArticles(articles => { + return articles.map(art => { + return art.article_id === article_id ? res.data.article : art + }) + }) + }) + .catch(err => { + setMessage(err?.response?.data?.message || 'Something bad happened') + if (err.response.status == 401) { + redirectToLogin() + } + }) + .finally(() => { + setSpinnerOn(false) + }) } const deleteArticle = article_id => { // ✨ implement + setMessage('') + setSpinnerOn(true) + axios.delete(`${articlesUrl}/${article_id}`, { headers: { Authorization: localStorage.getItem('token') } }) + .then(res => { + setMessage(res.data.message) + setArticles(articles => { + return articles.filter(art => { + return art.article_id != article_id + }) + }) + }) + .catch(err => { + setMessage(err?.response?.data?.message || 'Something bad happened') + if (err.response.status == 401) { + redirectToLogin() + } + }) + .finally(() => { + setSpinnerOn(false) + }) } + // const login = async ({ username, password }) => { + // // ✨ implement + // // We should flush the message state, turn on the spinner + // // and launch a request to the proper endpoint. + // // On success, we should set the token to local storage in a 'token' key, + // // put the server success message in its proper state, and redirect + // // to the Articles screen. Don't forget to turn off the spinner! + // setMessage('') + // setSpinnerOn(true) + + // try { + // const response = await fetch(loginUrl, { + // method: 'POST', + // headers: { + // 'Content-Type': 'application/json', + // }, + // body: JSON.stringify({ username, password }), + // }); + // const data = await response.json(); + // if (response.ok) { + // localStorage.setItem('token', data.token); + // setMessage(data.message || 'Login successful!'); + // redirectToArticles(); + // } else { + // setMessage(data.message || 'Login failed. Please try again.'); + // } + // setSpinnerOn(false); + // } catch (error) { + // console.log('Login error:', error) + // setMessage('Login failed. Please try again.'); + // setSpinnerOn(false); + // } + + // }; + + // const getArticles = async () => { + // // ✨ implement + // // We should flush the message state, turn on the spinner + // // and launch an authenticated request to the proper endpoint. + // // On success, we should set the articles in their proper state and + // // put the server success message in its proper state. + // // If something goes wrong, check the status of the response: + // // if it's a 401 the token might have gone bad, and we should redirect to login. + // // Don't forget to turn off the spinner! + // setMessage('') + // setSpinnerOn(true) + + // const token = localStorage.getItem('token') + // if(!token) { + // redirectToLogin() + // return; + // } + // try{ + // const response = await fetch(articlesUrl, { + // method: 'GET', + // headers: { + // 'Authorization': token, + // }, + // }) + // const data = await response.json() + + // if(response.ok){ + // setArticles(data.articles) + // setMessage(data.message) + // } else{ + // if(response.status === 401){ + // redirectToLogin() + // }else{ + // setMessage(data.message) + // } + // } + // setSpinnerOn(false) + // }catch (error){ + // setMessage('Error fetching') + // setSpinnerOn(false) + // } + // } + + // const postArticle = async (article) => { + // // ✨ implement + // // The flow is very similar to the `getArticles` function. + // // You'll know what to do! Use log statements or breakpoints + // // to inspect the response from the server. + // setMessage('') + // setSpinnerOn(true) + + // const token = localStorage.getItem('token') + // if(!token){ + // redirectToLogin() + // return; + // } + // try{ + // const response = await fetch(articlesUrl,{ + // method: 'POST', + // headers: { + // 'Content-Type': 'application/json', + // 'Authorization': token, + // }, + // body: JSON.stringify(article), + // }) + // const data = await response.json() + + // if(response.ok){ + // setArticles(prevArticles => [...prevArticles, data.article]); + // setMessage(data.message) + // } else{ + // if(response.status === 401){ + // redirectToLogin() + // } else{ + // setMessage(data.message) + // } + // } + + // } + // catch (error){ + // setMessage('Error posting...') + // } + // setSpinnerOn(false) + // } + + // const updateArticle = async({ article_id, article }) => { + // // ✨ implement + // // You got this! + // setMessage('') + // setSpinnerOn(true) + + // const token = localStorage.getItem('token') + // if(!token){ + // redirectToLogin() + // return; + // } + + // try{ + // const response = await fetch(`${articlesUrl}/${article_id}`, { + // method: 'PUT', + // headers: { + // 'Content-Type': 'application/json', + // 'Authorization': token, + // }, + // body: JSON.stringify(article), + // }) + // const data = await response.json() + + // if (response.ok){ + // setArticles(prevArticles => { + // return prevArticles.map(art => { + // return art.article_id === article_id ? data.article : art})}) + // setMessage(data.message) + // } else{ + // if (response.status === 401){ + // redirectToLogin() + // } else { + // setMessage(data.message) + // } + // } + + // } + // catch (error){ + // setMessage('Error Updating....') + // } + // setSpinnerOn(false) + // } + + // const deleteArticle = async (article_id) => { + // // ✨ implement + + // setMessage('') + // setSpinnerOn(true) + + // const token = localStorage.getItem('token') + // if(!token){ + // redirectToLogin() + // return; + // } + + // try{ + // const response = await fetch(`${articlesUrl}/${article_id}`, { + // method: 'DELETE', + // headers: { + // 'Authorization': token, + // }, + // //body: JSON.stringify(article), + // }); + // const data = await response.json() + + // if (response.ok){ + // setArticles(prevArticles => prevArticles.filter(art => art.article_id !== article_id)) + // setMessage(data.message) + // } else{ + // if (response.status === 401){ + // redirectToLogin() + // } else { + // setMessage(data.message) + // } + // } + + // } + // catch (error){ + // setMessage('Error Deleting....') + // } + // setSpinnerOn(false) + // } + + // useEffect(() => { + // console.log("Current Article ID:", currentArticleId) + // }, [currentArticleId]) + return ( // ✨ fix the JSX: `Spinner`, `Message`, `LoginForm`, `ArticleForm` and `Articles` expect props ❗ <> - - + + Logout from app {/* <-- do not change this line */} Advanced Web Applications @@ -78,11 +378,30 @@ export default function App() { Articles - } /> + } /> - - + + art.article_id == currentArticleId)} + setCurrentArticleId={setCurrentArticleId} + postArticle={postArticle} + updateArticle={updateArticle} + /> + + + {/* art.articles_id == currentArticleId)} + setCurrentArticleId={setCurrentArticleId} /> + + */} > } /> @@ -90,4 +409,4 @@ export default function App() { > ) -} +} \ No newline at end of file diff --git a/frontend/components/ArticleForm.js b/frontend/components/ArticleForm.js index 3b8d1afcd4..cabb68520d 100644 --- a/frontend/components/ArticleForm.js +++ b/frontend/components/ArticleForm.js @@ -3,7 +3,7 @@ import PT from 'prop-types' const initialFormValues = { title: '', text: '', topic: '' } -export default function ArticleForm(props) { +export default function ArticleForm({postArticle, updateArticle, setCurrentArticleId, currentArticle}) { const [values, setValues] = useState(initialFormValues) // ✨ where are my props? Destructure them here @@ -12,7 +12,18 @@ export default function ArticleForm(props) { // Every time the `currentArticle` prop changes, we should check it for truthiness: // if it's truthy, we should set its title, text and topic into the corresponding // values of the form. If it's not, we should reset the form back to initial values. - }) + if(currentArticle){ + setValues({ + title: currentArticle.title, + text: currentArticle.text, + topic: currentArticle.topic, + }) + } else { + setValues(initialFormValues) + } + //console.log("Current Article:", currentArticle) + + }, [currentArticle]) const onChange = evt => { const { id, value } = evt.target @@ -24,18 +35,52 @@ export default function ArticleForm(props) { // ✨ implement // We must submit a new post or update an existing one, // depending on the truthyness of the `currentArticle` prop. + // if(currentArticle){ + // updateArticle({article_id: currentArticle.article_id, article: values}) + // //updateArticle({...currentArticle, ...values}) + // } else { + // postArticle(values) + // } + + // setValues(initialFormValues) + //setCurrentArticleId(null) + + const article = { + + text: values.text, + + topic: values.topic, + + title: values.title + + } + + currentArticle ? updateArticle({article, article_id: currentArticle.article_id}) : postArticle(article) + setCurrentArticleId() + setValues(initialFormValues) } const isDisabled = () => { // ✨ implement // Make sure the inputs have some values + //const {title, text, topic} = values + //return !(title.trim() && text.trim() && topic) + //return !(values.title && values.text && values.topic) + return Object.values(values).some(value => !value.trim().length) } + // const onCancel = () => { + // setValues(initialFormValues) + // setCurrentArticleId(null) + // } + return ( // ✨ fix the JSX: make the heading display either "Edit" or "Create" // and replace Function.prototype with the correct function - Create Article + {currentArticle + ? 'Edit Article' : 'Create Article' + } Submit - Cancel edit + { currentArticle && setCurrentArticleId()}>Cancel edit} ) @@ -75,4 +120,4 @@ ArticleForm.propTypes = { text: PT.string.isRequired, topic: PT.string.isRequired, }) -} +} \ No newline at end of file diff --git a/frontend/components/Articles.js b/frontend/components/Articles.js index 78336919e8..32db731b62 100644 --- a/frontend/components/Articles.js +++ b/frontend/components/Articles.js @@ -2,15 +2,24 @@ import React, { useEffect } from 'react' import { Navigate } from 'react-router-dom' import PT from 'prop-types' -export default function Articles(props) { +export default function Articles({articles, getArticles, deleteArticle, setCurrentArticleId, currentArticleId}) { // ✨ where are my props? Destructure them here // ✨ implement conditional logic: if no token exists // we should render a Navigate to login screen (React Router v.6) + if(!localStorage.getItem('token')){ + return + } useEffect(() => { // ✨ grab the articles here, on first render only - }) + getArticles() + }, []) + + const handleEdit = (article_id) => { + console.log('Edit Article ID:', article_id) + setCurrentArticleId(article_id) + } return ( // ✨ fix the JSX: replace `Function.prototype` with actual functions @@ -18,26 +27,24 @@ export default function Articles(props) { Articles { - ![].length - ? 'No articles yet' - : [].map(art => { - return ( - + articles.length === 0 ? 'No articles yet' + : articles.map(art => ( + {art.title} {art.text} Topic: {art.topic} - Edit - Delete + handleEdit(art.article_id)}>Edit + deleteArticle(art.article_id)}>Delete - ) - }) + )) } - ) + ); + } // 🔥 No touchy: Articles expects the following props exactly: @@ -52,4 +59,4 @@ Articles.propTypes = { deleteArticle: PT.func.isRequired, setCurrentArticleId: PT.func.isRequired, currentArticleId: PT.number, // can be undefined or null -} +} \ No newline at end of file diff --git a/frontend/components/LoginForm.js b/frontend/components/LoginForm.js index f7702ea1ad..e10f1f57d9 100644 --- a/frontend/components/LoginForm.js +++ b/frontend/components/LoginForm.js @@ -5,7 +5,7 @@ const initialFormValues = { username: '', password: '', } -export default function LoginForm(props) { +export default function LoginForm({login}) { const [values, setValues] = useState(initialFormValues) // ✨ where are my props? Destructure them here @@ -17,6 +17,7 @@ export default function LoginForm(props) { const onSubmit = evt => { evt.preventDefault() // ✨ implement + login(values) } const isDisabled = () => { @@ -24,6 +25,8 @@ export default function LoginForm(props) { // Trimmed username must be >= 3, and // trimmed password must be >= 8 for // the button to become enabled + const {username, password} = values + return !(username.trim().length >= 3 && password.trim().length >=8) } return ( @@ -51,4 +54,4 @@ export default function LoginForm(props) { // 🔥 No touchy: LoginForm expects the following props exactly: LoginForm.propTypes = { login: PT.func.isRequired, -} +} \ No newline at end of file diff --git a/frontend/components/Message.js b/frontend/components/Message.js index 03713f865f..3997ad3882 100644 --- a/frontend/components/Message.js +++ b/frontend/components/Message.js @@ -20,5 +20,5 @@ export default function Message({ message }) { } Message.propTypes = { - message: PT.string.isRequired, -} + message: PT.string.isRequired, +} \ No newline at end of file diff --git a/frontend/components/Spinner.js b/frontend/components/Spinner.js index e928351db4..ddc411f5f1 100644 --- a/frontend/components/Spinner.js +++ b/frontend/components/Spinner.js @@ -1,16 +1,16 @@ -import React from 'react' -import styled, { keyframes } from 'styled-components' -import PT from 'prop-types' +import React from "react"; +import styled, { keyframes } from "styled-components"; +import PT from "prop-types"; const rotation = keyframes` from { transform: rotate(0deg); } to { transform: rotate(359deg); } -` +`; const opacity = keyframes` from { opacity: 0.2; } to { opacity: 1; } -` +`; const StyledSpinner = styled.div` animation: ${opacity} 1s infinite linear; @@ -19,17 +19,17 @@ const StyledSpinner = styled.div` transform-origin: center center; animation: ${rotation} 1s infinite linear; } -` +`; export default function Spinner({ on }) { - if (!on) return null + if (!on) return null; return ( - + . Please wait... - ) + ); } Spinner.propTypes = { on: PT.bool.isRequired, -} +}; \ No newline at end of file diff --git a/frontend/components/Spinner.test.js b/frontend/components/Spinner.test.js index 9a5773d2de..463c4b396b 100644 --- a/frontend/components/Spinner.test.js +++ b/frontend/components/Spinner.test.js @@ -1,5 +1,28 @@ // Import the Spinner component into this file and test // that it renders what it should for the different props it can take. -test('sanity', () => { - expect(true).toBe(false) -}) +import React from "react"; +import { render, screen } from "@testing-library/react"; +import "@testing-library/jest-dom"; +import Spinner from "./Spinner"; + +describe("Spinner Component", () => { + test("renders nothing when on prop is false", () => { + render(); + const spinnerElement = screen.queryByTestId("spinner"); + expect(spinnerElement).not.toBeInTheDocument(); + }); + + test("renders spinner when on prop is true", () => { + render(); + const spinnerElement = screen.getByTestId("spinner"); + expect(spinnerElement).toBeInTheDocument(); + expect(spinnerElement).toHaveTextContent("Please wait..."); + }); + + test("spinner has correct styling", () => { + render(); + const spinnerElement = screen.getByTestId("spinner"); + const styles = window.getComputedStyle(spinnerElement); + expect(styles.animation).toMatch(/1s infinite linear/); + }); +}); \ No newline at end of file diff --git a/frontend/index.js b/frontend/index.js index 35fe36c232..83908c1c4c 100644 --- a/frontend/index.js +++ b/frontend/index.js @@ -2,14 +2,15 @@ // 👉 DO NOT CHANGE THIS FILE 👈 // 👉 DO NOT CHANGE THIS FILE 👈 import React from 'react' -import { render } from 'react-dom' +import { createRoot } from 'react-dom/client' import { BrowserRouter } from 'react-router-dom' import App from './components/App' import './styles/reset.css' import './styles/styles.css' -render( +const root = createRoot(document.getElementById('root')) +root.render( - , document.getElementById('root')) +) \ No newline at end of file diff --git a/index.js b/index.js index 18b8dc328e..83908c1c4c 100644 --- a/index.js +++ b/index.js @@ -1,7 +1,16 @@ -const server = require('./backend/server') +// 👉 DO NOT CHANGE THIS FILE 👈 +// 👉 DO NOT CHANGE THIS FILE 👈 +// 👉 DO NOT CHANGE THIS FILE 👈 +import React from 'react' +import { createRoot } from 'react-dom/client' +import { BrowserRouter } from 'react-router-dom' +import App from './components/App' +import './styles/reset.css' +import './styles/styles.css' -const PORT = process.env.PORT || 9000 - -server.listen(PORT, () => { - console.log(`API listening on http://localhost:${PORT}`) -}) +const root = createRoot(document.getElementById('root')) +root.render( + + + +) \ No newline at end of file diff --git a/jest.config.js b/jest.config.js index af2c1f2167..409ad95bb0 100644 --- a/jest.config.js +++ b/jest.config.js @@ -197,4 +197,4 @@ const config = { // watchman: true, }; -module.exports = config; +module.exports = config; \ No newline at end of file diff --git a/jest.globals.js b/jest.globals.js index e5e87a5618..e3c90dd774 100644 --- a/jest.globals.js +++ b/jest.globals.js @@ -2,4 +2,4 @@ const nodeFetch = require('node-fetch') globalThis.fetch = nodeFetch globalThis.Request = nodeFetch.Request -globalThis.Response = nodeFetch.Response +globalThis.Response = nodeFetch.Response \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index e780dc3152..b817ae5fe2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,13 +11,14 @@ "axios": "1.6.8", "cors": "2.8.5", "express": "4.19.2", + "express-rate-limit": "^7.3.1", "jsonwebtoken": "9.0.2", "prop-types": "15.8.1", - "react": "18.2.0", + "react": "18.3.1", "react-dom": "18.2.0", "react-router-dom": "6.22.3", "redux": "5.0.1", - "styled-components": "6.1.8", + "styled-components": "6.1.11", "yup": "1.4.0" }, "devDependencies": { @@ -26,26 +27,27 @@ "@babel/plugin-transform-runtime": "7.24.3", "@babel/preset-env": "7.24.4", "@babel/preset-react": "7.24.1", - "@testing-library/jest-dom": "6.4.2", + "@testing-library/jest-dom": "^6.4.2", "@testing-library/react": "15.0.1", + "@types/cors": "^2.8.17", "@types/jest": "29.5.12", "babel-loader": "9.1.3", "babel-plugin-styled-components": "2.1.4", "concurrently": "8.2.2", "cross-env": "7.0.3", - "css-loader": "7.1.1", - "eslint": "8.57.0", - "eslint-plugin-react": "7.34.1", + "css-loader": "7.1.2", + "eslint": "^8.57.0", + "eslint-plugin-react": "^7.34.1", "fkill-cli": "8.0.0", "html-loader": "5.0.0", "html-webpack-plugin": "5.6.0", "jest": "29.7.0", "jest-environment-jsdom": "29.7.0", "msw": "1.3.3", - "nodemon": "3.1.0", + "nodemon": "3.1.4", "string-replace-loader": "3.1.0", "style-loader": "4.0.0", - "webpack": "5.91.0", + "webpack": "5.92.1", "webpack-cli": "5.1.4", "webpack-dev-server": "5.0.4" }, @@ -1916,9 +1918,9 @@ } }, "node_modules/@emotion/is-prop-valid": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.1.tgz", - "integrity": "sha512-61Mf7Ufx4aDxx1xlDeOm8aFFigGHE4z+0sKCa+IHCeZKiyP9RLD0Mmx7m8b9/Cf37f7NAvQOOJAbQQGVr5uERw==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.2.tgz", + "integrity": "sha512-uNsoYd37AFmaCdXlg6EYD1KaPOaRWRByMCYzbKUX4+hhMfrxdVSelShywL4JVaAeM/eHUOSprYBQls+/neX3pw==", "dependencies": { "@emotion/memoize": "^0.8.1" } @@ -1929,9 +1931,9 @@ "integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==" }, "node_modules/@emotion/unitless": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.0.tgz", - "integrity": "sha512-VINS5vEYAscRl2ZUDiT3uMPlrFQupiKgHz5AA4bCH1miKBg4qtwkim1qPmJj/4WG6TreYMY111rEFsjupcOKHw==" + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.1.tgz", + "integrity": "sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==" }, "node_modules/@eslint-community/eslint-utils": { "version": "4.4.0", @@ -3213,6 +3215,7 @@ "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.4.2.tgz", "integrity": "sha512-CzqH0AFymEMG48CpzXFriYYkOjk6ZGPCLMhW9e9jg3KMCn5OfJecF8GtGW7yGfR/IgCe3SX8BSwjdzI6BBbZLw==", "dev": true, + "license": "MIT", "dependencies": { "@adobe/css-tools": "^4.3.2", "@babel/runtime": "^7.9.2", @@ -3438,6 +3441,16 @@ "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==", "dev": true }, + "node_modules/@types/cors": { + "version": "2.8.17", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", + "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/debug": { "version": "4.1.12", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", @@ -3744,9 +3757,9 @@ "dev": true }, "node_modules/@types/stylis": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@types/stylis/-/stylis-4.2.0.tgz", - "integrity": "sha512-n4sx2bqL0mW1tvDf/loQ+aMX7GQD3lc3fkCMC55VFNDu/vBOabO+LTIeXKM14xK0ppk5TUGcWRjiSpIlUpghKw==" + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@types/stylis/-/stylis-4.2.5.tgz", + "integrity": "sha512-1Xve+NMN7FWjY14vLoY5tL3BVEQ/n42YLwaqJIPYhotZ9uBHt87VceMwWQpzmdEt2TNXIorIFG+YeCUUW7RInw==" }, "node_modules/@types/tough-cookie": { "version": "4.0.5", @@ -4049,10 +4062,10 @@ "acorn-walk": "^8.0.2" } }, - "node_modules/acorn-import-assertions": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz", - "integrity": "sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==", + "node_modules/acorn-import-attributes": { + "version": "1.9.5", + "resolved": "https://registry.npmjs.org/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz", + "integrity": "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==", "dev": true, "peerDependencies": { "acorn": "^8" @@ -4794,12 +4807,12 @@ } }, "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, "dependencies": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" }, "engines": { "node": ">=8" @@ -5728,9 +5741,9 @@ } }, "node_modules/css-loader": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-7.1.1.tgz", - "integrity": "sha512-OxIR5P2mjO1PSXk44bWuQ8XtMK4dpEqpIyERCx3ewOo3I8EmbcxMPUc5ScLtQfgXtOojoMv57So4V/C02HQLsw==", + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-7.1.2.tgz", + "integrity": "sha512-6WvYYn7l/XEGN8Xu2vWFt9nVzrCn39vKyTEFf/ExEyoksJjjSZV/0/35XPlMbpnr6VGhZIUg5yJrL8tGfes/FA==", "dev": true, "dependencies": { "icss-utils": "^5.1.0", @@ -5878,8 +5891,7 @@ "node_modules/csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", - "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "dev": true + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" }, "node_modules/data-urls": { "version": "3.0.2", @@ -6463,9 +6475,9 @@ } }, "node_modules/enhanced-resolve": { - "version": "5.16.0", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.16.0.tgz", - "integrity": "sha512-O+QWCviPNSSLAD9Ucn8Awv+poAkqn3T1XY5/N7kR7rQO9yfSGWkYZDwpJ+iKF7B8rxaQKWngSqACpgzeapSyoA==", + "version": "5.17.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.0.tgz", + "integrity": "sha512-dwDPwZL0dmye8Txp2gzFmA6sxALaSvdRDjPH0viLcKrtlOL3tw62nWWweVD1SdILDTJrbrL6tdWVN58Wo6U3eA==", "dev": true, "dependencies": { "graceful-fs": "^4.2.4", @@ -6731,6 +6743,7 @@ "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", "dev": true, + "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -6786,6 +6799,7 @@ "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.34.1.tgz", "integrity": "sha512-N97CxlouPT1AHt8Jn0mhhN2RrADlUAsk1/atcT2KyA/l9Q/E6ll7OIGwNumFmWfZ9skV3XXccYS19h80rHtgkw==", "dev": true, + "license": "MIT", "dependencies": { "array-includes": "^3.1.7", "array.prototype.findlast": "^1.2.4", @@ -7163,6 +7177,21 @@ "node": ">= 0.10.0" } }, + "node_modules/express-rate-limit": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.3.1.tgz", + "integrity": "sha512-BbaryvkY4wEgDqLgD18/NSy2lDO2jTuT9Y8c1Mpx0X63Yz0sYd5zN6KPe7UvpuSVvV33T6RaE1o1IVZQjHMYgw==", + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": "4 || 5 || ^5.0.0-beta.1" + } + }, "node_modules/express/node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -7275,9 +7304,9 @@ } }, "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, "dependencies": { "to-regex-range": "^5.0.1" @@ -12282,9 +12311,9 @@ "dev": true }, "node_modules/nodemon": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.0.tgz", - "integrity": "sha512-xqlktYlDMCepBJd43ZQhjWwMw2obW/JRvkrLxq5RCNcuDDX1DbcPT+qT1IlIIdf+DhnWs90JpTMe+Y5KxOchvA==", + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.4.tgz", + "integrity": "sha512-wjPBbFhtpJwmIeY2yP7QF+UKzPfltVGtfce1g/bB15/8vCGZj8uxD62b/b9M9/WVgme0NZudpownKN+c0plXlQ==", "dev": true, "dependencies": { "chokidar": "^3.5.2", @@ -13065,7 +13094,6 @@ "version": "8.4.38", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz", "integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==", - "dev": true, "funding": [ { "type": "opencollective", @@ -13403,9 +13431,9 @@ } }, "node_modules/react": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", - "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "dependencies": { "loose-envify": "^1.1.0" }, @@ -14780,19 +14808,19 @@ } }, "node_modules/styled-components": { - "version": "6.1.8", - "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-6.1.8.tgz", - "integrity": "sha512-PQ6Dn+QxlWyEGCKDS71NGsXoVLKfE1c3vApkvDYS5KAK+V8fNWGhbSUEo9Gg2iaID2tjLXegEW3bZDUGpofRWw==", + "version": "6.1.11", + "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-6.1.11.tgz", + "integrity": "sha512-Ui0jXPzbp1phYij90h12ksljKGqF8ncGx+pjrNPsSPhbUUjWT2tD1FwGo2LF6USCnbrsIhNngDfodhxbegfEOA==", "dependencies": { - "@emotion/is-prop-valid": "1.2.1", - "@emotion/unitless": "0.8.0", - "@types/stylis": "4.2.0", + "@emotion/is-prop-valid": "1.2.2", + "@emotion/unitless": "0.8.1", + "@types/stylis": "4.2.5", "css-to-react-native": "3.2.0", - "csstype": "3.1.2", - "postcss": "8.4.31", + "csstype": "3.1.3", + "postcss": "8.4.38", "shallowequal": "1.1.0", - "stylis": "4.3.1", - "tslib": "2.5.0" + "stylis": "4.3.2", + "tslib": "2.6.2" }, "engines": { "node": ">= 16" @@ -14806,47 +14834,10 @@ "react-dom": ">= 16.8.0" } }, - "node_modules/styled-components/node_modules/csstype": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz", - "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==" - }, - "node_modules/styled-components/node_modules/postcss": { - "version": "8.4.31", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", - "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "dependencies": { - "nanoid": "^3.3.6", - "picocolors": "^1.0.0", - "source-map-js": "^1.0.2" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/styled-components/node_modules/tslib": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", - "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==" - }, "node_modules/stylis": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.1.tgz", - "integrity": "sha512-EQepAV+wMsIaGVGX1RECzgrcqRRU/0sYOHkeLsZ3fzHaHXZy4DaOOX0vOlGQdlsjkh3mFHAIlVimpwAs4dslyQ==" + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.2.tgz", + "integrity": "sha512-bhtUjWd/z6ltJiQwg0dUfxEJ+W+jdqQd8TbWLWyeIJHlnsqmGLRFFd8e5mA0AZi/zx90smXRlN66YMTcaSFifg==" }, "node_modules/supports-color": { "version": "5.5.0", @@ -15216,8 +15207,7 @@ "node_modules/tslib": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", - "dev": true + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" }, "node_modules/type-check": { "version": "0.4.0", @@ -15608,9 +15598,9 @@ } }, "node_modules/webpack": { - "version": "5.91.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.91.0.tgz", - "integrity": "sha512-rzVwlLeBWHJbmgTC/8TvAcu5vpJNII+MelQpylD4jNERPwpBJOE2lEcko1zJX3QJeLjTTAnQxn/OJ8bjDzVQaw==", + "version": "5.92.1", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.92.1.tgz", + "integrity": "sha512-JECQ7IwJb+7fgUFBlrJzbyu3GEuNBcdqr1LD7IbSzwkSmIevTm8PF+wej3Oxuz/JFBUZ6O1o43zsPkwm1C4TmA==", "dev": true, "dependencies": { "@types/eslint-scope": "^3.7.3", @@ -15619,10 +15609,10 @@ "@webassemblyjs/wasm-edit": "^1.12.1", "@webassemblyjs/wasm-parser": "^1.12.1", "acorn": "^8.7.1", - "acorn-import-assertions": "^1.9.0", + "acorn-import-attributes": "^1.9.5", "browserslist": "^4.21.10", "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.16.0", + "enhanced-resolve": "^5.17.0", "es-module-lexer": "^1.2.1", "eslint-scope": "5.1.1", "events": "^3.2.0", @@ -16274,9 +16264,9 @@ "dev": true }, "node_modules/ws": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.16.0.tgz", - "integrity": "sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==", + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", "dev": true, "engines": { "node": ">=10.0.0" diff --git a/package.json b/package.json index c3309648de..a2bc5a37a4 100644 --- a/package.json +++ b/package.json @@ -16,26 +16,27 @@ "@babel/plugin-transform-runtime": "7.24.3", "@babel/preset-env": "7.24.4", "@babel/preset-react": "7.24.1", - "@testing-library/jest-dom": "6.4.2", + "@testing-library/jest-dom": "^6.4.2", "@testing-library/react": "15.0.1", + "@types/cors": "^2.8.17", "@types/jest": "29.5.12", "babel-loader": "9.1.3", "babel-plugin-styled-components": "2.1.4", "concurrently": "8.2.2", "cross-env": "7.0.3", - "css-loader": "7.1.1", - "eslint": "8.57.0", - "eslint-plugin-react": "7.34.1", + "css-loader": "7.1.2", + "eslint": "^8.57.0", + "eslint-plugin-react": "^7.34.1", "fkill-cli": "8.0.0", "html-loader": "5.0.0", "html-webpack-plugin": "5.6.0", "jest": "29.7.0", "jest-environment-jsdom": "29.7.0", "msw": "1.3.3", - "nodemon": "3.1.0", + "nodemon": "3.1.4", "string-replace-loader": "3.1.0", "style-loader": "4.0.0", - "webpack": "5.91.0", + "webpack": "5.92.1", "webpack-cli": "5.1.4", "webpack-dev-server": "5.0.4" }, @@ -43,13 +44,14 @@ "axios": "1.6.8", "cors": "2.8.5", "express": "4.19.2", + "express-rate-limit": "^7.3.1", "jsonwebtoken": "9.0.2", "prop-types": "15.8.1", - "react": "18.2.0", + "react": "18.3.1", "react-dom": "18.2.0", "react-router-dom": "6.22.3", "redux": "5.0.1", - "styled-components": "6.1.8", + "styled-components": "6.1.11", "yup": "1.4.0" }, "engines": {
{art.text}
Topic: {art.topic}