From 67180fdbe68cee6d515f00a8a962c784092f4753 Mon Sep 17 00:00:00 2001 From: Brandon Notowitz Date: Sun, 18 Aug 2024 18:02:22 -0400 Subject: [PATCH 1/6] Main --- frontend/components/App.js | 120 ++++++++++++++++++++++++---- frontend/components/ArticleForm.js | 49 ++++++++++-- frontend/components/Articles.js | 42 +++++++--- frontend/components/LoginForm.js | 25 ++++-- frontend/components/Message.js | 4 +- frontend/components/Spinner.test.js | 19 ++++- frontend/index.js | 28 +++---- index.js | 21 +++-- package-lock.json | 25 +++--- 9 files changed, 265 insertions(+), 68 deletions(-) diff --git a/frontend/components/App.js b/frontend/components/App.js index c4b6d6ce5c..2715cc6846 100644 --- a/frontend/components/App.js +++ b/frontend/components/App.js @@ -1,10 +1,12 @@ -import React, { useState } from 'react' +import React, { useState, useEffect } 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 axiosWithAuth from '../axios/index' +import axios from 'axios' const articlesUrl = 'http://localhost:9000/api/articles' const loginUrl = 'http://localhost:9000/api/login' @@ -18,34 +20,64 @@ 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 // If a token is in local storage it should be removed, + if (localStorage.getItem('token')) { + localStorage.removeItem('token') + } // and a message saying "Goodbye!" should be set in its proper state. + setMessage('Goodbye!') // In any case, we should redirect the browser back to the login screen, // using the helper above. + redirectToLogin(); } const login = ({ username, password }) => { // ✨ implement // We should flush the message state, turn on the spinner + setMessage(''); + setSpinnerOn(true); // and launch a request to the proper endpoint. + axios.post(loginUrl, {"username": username, "password": password}) // 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! + .then((res) => { + localStorage.setItem('token', res.data.token) + // put the server success message in its proper state, and redirect + // to the Articles screen. Don't forget to turn off the spinner! + setMessage(res.data.message) + redirectToArticles(); + }) + setSpinnerOn(false) } const getArticles = () => { // ✨ implement // We should flush the message state, turn on the spinner + setSpinnerOn(true); + setMessage(''); // and launch an authenticated request to the proper endpoint. + axiosWithAuth().get(articlesUrl) // On success, we should set the articles in their proper state and // put the server success message in its proper state. + .then((res) => { + console.log(res) + setArticles(res.data.articles) + setMessage(res.data.message) + setSpinnerOn(false) + }) // 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. + .catch((err) => { + console.log(err) + if (err.response.status === 401) { + redirectToLogin(); + } + setSpinnerOn(false) + }) // Don't forget to turn off the spinner! } @@ -54,22 +86,70 @@ 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. + setMessage(''); + setSpinnerOn(true); + axiosWithAuth().post(`http://localhost:9000/api/articles`, article) + .then((res) => { + setArticles([...articles, res.data.article]) + setMessage(res.data.message) + setSpinnerOn(false) + }) + .catch((err) => { + console.log(err) + setSpinnerOn(false) + }) } - const updateArticle = ({ article_id, article }) => { + const updateArticle = (articleId, updatedArticle) => { // ✨ implement // You got this! + console.log("BOOP! UPDATED") + const jsondArticle = JSON.stringify(updatedArticle) + setMessage(''); + setSpinnerOn(true); + axiosWithAuth().put(`http://localhost:9000/api/articles/${articleId}`, jsondArticle) + .then((res) => { + console.log(res) + // Get all but the updated article + const articlesSansUpdated = articles.filter((article) => { + return article.article_id !== articleId; + }) + articlesSansUpdated.push(res.data.article) + setArticles(articlesSansUpdated) + setMessage(res.data.message); + setSpinnerOn(false); + }) + .catch((err) => { + console.log(err) + setSpinnerOn(false); + }) } - const deleteArticle = article_id => { + const deleteArticle = (articleId) => { // ✨ implement + console.log("BOOP DELETED: " + articleId) + // Here's how to do it in state: + setSpinnerOn(true) + axiosWithAuth().delete(`http://localhost:9000/api/articles/${articleId}`) + .then((res) => { + const allButDeleted = articles.filter((article) => { + return article.article_id !== articleId; + }) + setArticles(allButDeleted) + setMessage(res.data.message) + setSpinnerOn(false) + }) + .catch((err) => { + console.log(err) + setSpinnerOn(false) + }) } return ( // ✨ fix the JSX: `Spinner`, `Message`, `LoginForm`, `ArticleForm` and `Articles` expect props ❗ <> - - + +
{/* <-- do not change this line */}

Advanced Web Applications

@@ -78,16 +158,28 @@ export default function App() { Articles - } /> + } /> - - + + } /> -
Bloom Institute of Technology 2024
+
Bloom Institute of Technology 2022
) -} +} \ No newline at end of file diff --git a/frontend/components/ArticleForm.js b/frontend/components/ArticleForm.js index 3b8d1afcd4..5e5d136801 100644 --- a/frontend/components/ArticleForm.js +++ b/frontend/components/ArticleForm.js @@ -1,5 +1,6 @@ import React, { useEffect, useState } from 'react' import PT from 'prop-types' +import axiosWithAuth from '../axios'; const initialFormValues = { title: '', text: '', topic: '' } @@ -7,35 +8,71 @@ export default function ArticleForm(props) { const [values, setValues] = useState(initialFormValues) // ✨ where are my props? Destructure them here + const { articles, currentArticleId, postArticle, updateArticle, setCurrentArticleId} = props; + + // Fill inputs with current editing article useEffect(() => { // ✨ implement // Every time the `currentArticle` prop changes, we should check it for truthiness: + if (currentArticleId) { + const [currentArticle] = articles.filter((article) => { + return article.article_id === currentArticleId + }) + console.log(currentArticle) + setValues({ + title: currentArticle.title, + text: currentArticle.text, + topic: currentArticle.topic + }) + } else { + setValues(initialFormValues) + } // 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. - }) + }, [currentArticleId]) const onChange = evt => { const { id, value } = evt.target setValues({ ...values, [id]: value }) } + const onCancel = () => { + setValues(initialFormValues) + setCurrentArticleId('') + } + const onSubmit = evt => { evt.preventDefault() // ✨ implement // We must submit a new post or update an existing one, // depending on the truthyness of the `currentArticle` prop. + if (currentArticleId) { + updateArticle( currentArticleId, values ) + setValues(initialFormValues) + } else { + postArticle(values); + setValues(initialFormValues); + } + } + // Submit button validation const isDisabled = () => { // ✨ implement // Make sure the inputs have some values + if (Object.values(values).every((value) => value.trim())) { + console.log('Submit/Edit box validated!') + return false + } else { + return true; + } } return ( // ✨ fix the JSX: make the heading display either "Edit" or "Create" // and replace Function.prototype with the correct function
-

Create Article

+

{currentArticleId ? 'Edit' : 'Create'} Article

Node
- - + + {currentArticleId ? : ''}
) } -// 🔥 No touchy: ArticleForm expects the following props exactly: +// 🔥 No touchy: LoginForm expects the following props exactly: ArticleForm.propTypes = { postArticle: PT.func.isRequired, updateArticle: PT.func.isRequired, @@ -75,4 +112,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..884b784313 100644 --- a/frontend/components/Articles.js +++ b/frontend/components/Articles.js @@ -1,16 +1,38 @@ import React, { useEffect } from 'react' import { Navigate } from 'react-router-dom' -import PT from 'prop-types' +import PT from 'prop-types'; +import axios from 'axios'; export default function Articles(props) { // ✨ where are my props? Destructure them here + const { redirectToLogin, updateArticle, articles, getArticles, deleteArticle, setCurrentArticleId, currentArticleId } = props; // ✨ implement conditional logic: if no token exists - // we should render a Navigate to login screen (React Router v.6) + if (!localStorage.getItem('token')) { + // we should render a Navigate to login screen (React Router v.6) + console.log("TOKEN NOT HERE") + return ( + + ) + } + // ✨ grab the articles here, on first render only useEffect(() => { - // ✨ grab the articles here, on first render only - }) + getArticles() + }, []) + + // clickHandler for edit and delete buttons + const clickHandler = (e) => { + const selectedArticleTitle = e.target.parentElement.previousSibling.childNodes[0].textContent + const [ selectedArticle ] = articles.filter((article) => { + return article.title === selectedArticleTitle; + }) + if (e.target.textContent === 'Delete') { + deleteArticle(selectedArticle.article_id); + } else if (e.target.textContent === 'Edit') { + setCurrentArticleId(selectedArticle.article_id) + } + } return ( // ✨ fix the JSX: replace `Function.prototype` with actual functions @@ -18,9 +40,9 @@ export default function Articles(props) {

Articles

{ - ![].length + !articles.length ? 'No articles yet' - : [].map(art => { + : articles.map(art => { return (
@@ -29,8 +51,8 @@ export default function Articles(props) {

Topic: {art.topic}

- - + +
) @@ -51,5 +73,5 @@ Articles.propTypes = { getArticles: PT.func.isRequired, deleteArticle: PT.func.isRequired, setCurrentArticleId: PT.func.isRequired, - currentArticleId: PT.number, // can be undefined or null -} + currentArticleId: PT.number, +} \ No newline at end of file diff --git a/frontend/components/LoginForm.js b/frontend/components/LoginForm.js index f7702ea1ad..a627214a28 100644 --- a/frontend/components/LoginForm.js +++ b/frontend/components/LoginForm.js @@ -1,4 +1,4 @@ -import React, { useState } from 'react' +import React, { useState, useEffect } from 'react' import PT from 'prop-types' const initialFormValues = { @@ -7,7 +7,10 @@ const initialFormValues = { } export default function LoginForm(props) { const [values, setValues] = useState(initialFormValues) + const [disabled, setDisabled] = useState(true) // ✨ where are my props? Destructure them here + + const { login } = props const onChange = evt => { const { id, value } = evt.target @@ -17,15 +20,28 @@ export default function LoginForm(props) { const onSubmit = evt => { evt.preventDefault() // ✨ implement + login( values ) } + const isDisabled = () => { // ✨ implement // Trimmed username must be >= 3, and + const username = values.username.trim().length; // trimmed password must be >= 8 for - // the button to become enabled + const password = values.password.trim().length; + if (username >= 3 && password >= 8) { + // the button to become enabled + setDisabled(false) + } else { + setDisabled(true) + } } + useEffect(() => { + isDisabled() + }, [values]) + return (

Login

@@ -43,12 +59,11 @@ export default function LoginForm(props) { placeholder="Enter password" id="password" /> - +
) } -// 🔥 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.test.js b/frontend/components/Spinner.test.js index 9a5773d2de..cd7fecf83a 100644 --- a/frontend/components/Spinner.test.js +++ b/frontend/components/Spinner.test.js @@ -1,5 +1,22 @@ // Import the Spinner component into this file and test // that it renders what it should for the different props it can take. +import React from 'react' +import Spinner from './Spinner'; +import { render, screen } from '@testing-library/react'; + test('sanity', () => { - expect(true).toBe(false) + expect(true).toBe(true) }) +describe('spinner render tests', () => { + + test('spinner renders when passed true value', () => { + render() + screen.debug() + expect(screen.queryByText('Please wait...')).not.toBeNull() + }) + test('spinner does not render when passed a false value', () => { + render() + screen.debug() + expect(screen.queryByText('Please wait...')).toBeNull(); + }) +}) \ No newline at end of file diff --git a/frontend/index.js b/frontend/index.js index 35fe36c232..5dad201a85 100644 --- a/frontend/index.js +++ b/frontend/index.js @@ -1,15 +1,15 @@ -// 👉 DO NOT CHANGE THIS FILE 👈 -// 👉 DO NOT CHANGE THIS FILE 👈 -// 👉 DO NOT CHANGE THIS FILE 👈 -import React from 'react' -import { render } from 'react-dom' -import { BrowserRouter } from 'react-router-dom' -import App from './components/App' -import './styles/reset.css' -import './styles/styles.css' +// ✨ implement axiosWithAuth +import axios from 'axios'; -render( - - - - , document.getElementById('root')) + +const axiosWithAuth = () => { + const token = localStorage.getItem('token'); + return axios.create({ + headers: { + 'Authorization': token, + 'Content-Type': 'application/json' + } + }); +} + +export default axiosWithAuth; \ No newline at end of file diff --git a/index.js b/index.js index 18b8dc328e..dad9edd1ec 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 { render } from 'react-dom' +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}`) -}) +render( + + + + , document.getElementById('root')) + \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index e780dc3152..fc15cb1da9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4794,12 +4794,13 @@ } }, "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, + "license": "MIT", "dependencies": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" }, "engines": { "node": ">=8" @@ -7275,10 +7276,11 @@ } }, "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, + "license": "MIT", "dependencies": { "to-regex-range": "^5.0.1" }, @@ -8839,6 +8841,7 @@ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.12.0" } @@ -15145,6 +15148,7 @@ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", "dev": true, + "license": "MIT", "dependencies": { "is-number": "^7.0.0" }, @@ -16274,10 +16278,11 @@ "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.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", "dev": true, + "license": "MIT", "engines": { "node": ">=10.0.0" }, From e45c2249532e1d2e38910803fbdef903979b22bc Mon Sep 17 00:00:00 2001 From: Brandon Notowitz Date: Sun, 18 Aug 2024 18:37:59 -0400 Subject: [PATCH 2/6] new --- backend/server.js | 2 +- frontend/components/App.js | 2 +- frontend/components/ArticleForm.js | 98 +++++++--------- frontend/components/Articles.js | 86 +++++++------- frontend/components/LoginForm.js | 62 +++++----- frontend/components/Spinner.js | 20 ++-- frontend/components/Spinner.test.js | 44 ++++--- frontend/index.js | 18 +-- index.js | 8 +- jest.config.js | 2 +- jest.globals.js | 2 +- package-lock.json | 176 ++++++++++++---------------- package.json | 17 +-- 13 files changed, 244 insertions(+), 293 deletions(-) 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/frontend/components/App.js b/frontend/components/App.js index 2715cc6846..e3f89ae3f8 100644 --- a/frontend/components/App.js +++ b/frontend/components/App.js @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from 'react' +import React, { useState } from 'react' import { NavLink, Routes, Route, useNavigate } from 'react-router-dom' import Articles from './Articles' import LoginForm from './LoginForm' diff --git a/frontend/components/ArticleForm.js b/frontend/components/ArticleForm.js index 5e5d136801..a2b2a6d8ef 100644 --- a/frontend/components/ArticleForm.js +++ b/frontend/components/ArticleForm.js @@ -1,78 +1,63 @@ -import React, { useEffect, useState } from 'react' -import PT from 'prop-types' -import axiosWithAuth from '../axios'; +import React, { useEffect, useState } from "react"; +import PT from "prop-types"; -const initialFormValues = { title: '', text: '', topic: '' } +const initialFormValues = { title: "", text: "", topic: "" }; -export default function ArticleForm(props) { - const [values, setValues] = useState(initialFormValues) +export default function ArticleForm({ + postArticle, + updateArticle, + setCurrentArticleId, + currentArticle, +}) { + const [values, setValues] = useState(initialFormValues); // ✨ where are my props? Destructure them here - const { articles, currentArticleId, postArticle, updateArticle, setCurrentArticleId} = props; - - // Fill inputs with current editing article useEffect(() => { // ✨ implement // Every time the `currentArticle` prop changes, we should check it for truthiness: - if (currentArticleId) { - const [currentArticle] = articles.filter((article) => { - return article.article_id === currentArticleId - }) - console.log(currentArticle) - setValues({ + // 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 - }) + topic: currentArticle.topic, + }); } else { - setValues(initialFormValues) + setValues(initialFormValues); } - // 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. - }, [currentArticleId]) - - const onChange = evt => { - const { id, value } = evt.target - setValues({ ...values, [id]: value }) - } + }, [currentArticle]); - const onCancel = () => { - setValues(initialFormValues) - setCurrentArticleId('') - } + const onChange = (evt) => { + const { id, value } = evt.target; + setValues({ ...values, [id]: value }); + }; - const onSubmit = evt => { - evt.preventDefault() + const onSubmit = (evt) => { + evt.preventDefault(); // ✨ implement // We must submit a new post or update an existing one, // depending on the truthyness of the `currentArticle` prop. - if (currentArticleId) { - updateArticle( currentArticleId, values ) - setValues(initialFormValues) + if (currentArticle) { + updateArticle({ article_id: currentArticle.article_id, article: values }); } else { postArticle(values); - setValues(initialFormValues); } - - } + setValues(initialFormValues); + setCurrentArticleId(null); + }; - // Submit button validation const isDisabled = () => { // ✨ implement // Make sure the inputs have some values - if (Object.values(values).every((value) => value.trim())) { - console.log('Submit/Edit box validated!') - return false - } else { - return true; - } - } + return !values.title.trim() || !values.text.trim() || !values.topic; + }; return ( // ✨ fix the JSX: make the heading display either "Edit" or "Create" // and replace Function.prototype with the correct function
-

{currentArticleId ? 'Edit' : 'Create'} Article

+

{currentArticle ? "Edit" : "Create"} Article

Node
- - {currentArticleId ? : ''} + + {currentArticle && ( + + )}
- ) + ); } -// 🔥 No touchy: LoginForm expects the following props exactly: +// 🔥 No touchy: ArticleForm expects the following props exactly: ArticleForm.propTypes = { postArticle: PT.func.isRequired, updateArticle: PT.func.isRequired, setCurrentArticleId: PT.func.isRequired, - currentArticle: PT.shape({ // can be null or undefined, meaning "create" mode (as opposed to "update") + currentArticle: PT.shape({ + // can be null or undefined, meaning "create" mode (as opposed to "update") article_id: PT.number.isRequired, title: PT.string.isRequired, text: PT.string.isRequired, topic: PT.string.isRequired, - }) -} \ No newline at end of file + }), +}; \ No newline at end of file diff --git a/frontend/components/Articles.js b/frontend/components/Articles.js index 884b784313..ef9458c0ee 100644 --- a/frontend/components/Articles.js +++ b/frontend/components/Articles.js @@ -1,37 +1,28 @@ -import React, { useEffect } from 'react' -import { Navigate } from 'react-router-dom' -import PT from 'prop-types'; -import axios from 'axios'; +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, +}) { // ✨ where are my props? Destructure them here - const { redirectToLogin, updateArticle, articles, getArticles, deleteArticle, setCurrentArticleId, currentArticleId } = props; // ✨ implement conditional logic: if no token exists - if (!localStorage.getItem('token')) { - // we should render a Navigate to login screen (React Router v.6) - console.log("TOKEN NOT HERE") - return ( - - ) - } + // we should render a Navigate to login screen (React Router v.6) + const token = localStorage.getItem("token"); - // ✨ grab the articles here, on first render only useEffect(() => { - getArticles() - }, []) - - // clickHandler for edit and delete buttons - const clickHandler = (e) => { - const selectedArticleTitle = e.target.parentElement.previousSibling.childNodes[0].textContent - const [ selectedArticle ] = articles.filter((article) => { - return article.title === selectedArticleTitle; - }) - if (e.target.textContent === 'Delete') { - deleteArticle(selectedArticle.article_id); - } else if (e.target.textContent === 'Edit') { - setCurrentArticleId(selectedArticle.article_id) + // ✨ grab the articles here, on first render only + if (token) { + getArticles(); } + }, []); + + if (!token) { + return ; } return ( @@ -39,10 +30,9 @@ export default function Articles(props) { // and use the articles prop to generate articles

Articles

- { - !articles.length - ? 'No articles yet' - : articles.map(art => { + {!articles.length + ? "No articles yet" + : articles.map((art) => { return (
@@ -51,27 +41,33 @@ export default function Articles(props) {

Topic: {art.topic}

- - + +
- ) - }) - } + ); + })}
- ) + ); } // 🔥 No touchy: Articles expects the following props exactly: Articles.propTypes = { - articles: PT.arrayOf(PT.shape({ // the array can be empty - article_id: PT.number.isRequired, - title: PT.string.isRequired, - text: PT.string.isRequired, - topic: PT.string.isRequired, - })).isRequired, + articles: PT.arrayOf( + PT.shape({ + // the array can be empty + article_id: PT.number.isRequired, + title: PT.string.isRequired, + text: PT.string.isRequired, + topic: PT.string.isRequired, + }) + ).isRequired, getArticles: PT.func.isRequired, deleteArticle: PT.func.isRequired, setCurrentArticleId: PT.func.isRequired, - currentArticleId: PT.number, -} \ No newline at end of file + 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 a627214a28..811d2bbe30 100644 --- a/frontend/components/LoginForm.js +++ b/frontend/components/LoginForm.js @@ -1,46 +1,34 @@ -import React, { useState, useEffect } from 'react' -import PT from 'prop-types' +import React, { useState } from "react"; +import PT from "prop-types"; const initialFormValues = { - username: '', - password: '', -} -export default function LoginForm(props) { - const [values, setValues] = useState(initialFormValues) - const [disabled, setDisabled] = useState(true) + username: "", + password: "", +}; +export default function LoginForm({ login }) { + const [values, setValues] = useState(initialFormValues); // ✨ where are my props? Destructure them here - - const { login } = props - const onChange = evt => { - const { id, value } = evt.target - setValues({ ...values, [id]: value }) - } + const onChange = (evt) => { + const { id, value } = evt.target; + setValues({ ...values, [id]: value }); + }; - const onSubmit = evt => { - evt.preventDefault() + const onSubmit = (evt) => { + evt.preventDefault(); // ✨ implement - login( values ) - } + login(values); + }; - const isDisabled = () => { // ✨ implement // Trimmed username must be >= 3, and - const username = values.username.trim().length; // trimmed password must be >= 8 for - const password = values.password.trim().length; - if (username >= 3 && password >= 8) { - // the button to become enabled - setDisabled(false) - } else { - setDisabled(true) - } - } - - useEffect(() => { - isDisabled() - }, [values]) + // the button to become enabled + return ( + values.username.trim().length < 3 || values.password.trim().length < 8 + ); + }; return (
@@ -54,16 +42,20 @@ 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 +}; \ 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 cd7fecf83a..463c4b396b 100644 --- a/frontend/components/Spinner.test.js +++ b/frontend/components/Spinner.test.js @@ -1,22 +1,28 @@ // Import the Spinner component into this file and test // that it renders what it should for the different props it can take. -import React from 'react' -import Spinner from './Spinner'; -import { render, screen } from '@testing-library/react'; +import React from "react"; +import { render, screen } from "@testing-library/react"; +import "@testing-library/jest-dom"; +import Spinner from "./Spinner"; -test('sanity', () => { - expect(true).toBe(true) -}) -describe('spinner render tests', () => { - - test('spinner renders when passed true value', () => { - render() - screen.debug() - expect(screen.queryByText('Please wait...')).not.toBeNull() - }) - test('spinner does not render when passed a false value', () => { - render() - screen.debug() - expect(screen.queryByText('Please wait...')).toBeNull(); - }) -}) \ No newline at end of file +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 5dad201a85..b628313181 100644 --- a/frontend/index.js +++ b/frontend/index.js @@ -1,15 +1,7 @@ -// ✨ implement axiosWithAuth -import axios from 'axios'; +const server = require('./backend/server') +const PORT = process.env.PORT || 9000 -const axiosWithAuth = () => { - const token = localStorage.getItem('token'); - return axios.create({ - headers: { - 'Authorization': token, - 'Content-Type': 'application/json' - } - }); -} - -export default axiosWithAuth; \ No newline at end of file +server.listen(PORT, () => { + console.log(`API listening on http://localhost:${PORT}`) +}) \ No newline at end of file diff --git a/index.js b/index.js index dad9edd1ec..83908c1c4c 100644 --- a/index.js +++ b/index.js @@ -2,15 +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 +) \ 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 fc15cb1da9..f2b4d9f559 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,26 @@ "@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/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 +1917,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 +1930,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 +3214,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", @@ -3744,9 +3746,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 +4051,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" @@ -4798,7 +4800,6 @@ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, - "license": "MIT", "dependencies": { "fill-range": "^7.1.1" }, @@ -5729,9 +5730,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", @@ -5879,8 +5880,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", @@ -6464,9 +6464,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", @@ -6732,6 +6732,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", @@ -6787,6 +6788,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", @@ -7164,6 +7166,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", @@ -7280,7 +7297,6 @@ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, - "license": "MIT", "dependencies": { "to-regex-range": "^5.0.1" }, @@ -8841,7 +8857,6 @@ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "dev": true, - "license": "MIT", "engines": { "node": ">=0.12.0" } @@ -12285,9 +12300,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", @@ -13068,7 +13083,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", @@ -13406,9 +13420,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" }, @@ -14783,19 +14797,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" @@ -14809,47 +14823,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", @@ -15148,7 +15125,6 @@ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", "dev": true, - "license": "MIT", "dependencies": { "is-number": "^7.0.0" }, @@ -15220,8 +15196,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", @@ -15612,9 +15587,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", @@ -15623,10 +15598,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", @@ -16278,11 +16253,10 @@ "dev": true }, "node_modules/ws": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", - "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", "dev": true, - "license": "MIT", "engines": { "node": ">=10.0.0" }, diff --git a/package.json b/package.json index c3309648de..3f61adea6e 100644 --- a/package.json +++ b/package.json @@ -16,26 +16,26 @@ "@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/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 +43,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": { From d03117c181a6b46b30a041a694388f798f0c987e Mon Sep 17 00:00:00 2001 From: Brandon Notowitz Date: Sun, 18 Aug 2024 18:43:21 -0400 Subject: [PATCH 3/6] newworkspace --- codegrade_mvp.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 7d7f9857437e158658624905e5f894ee0f76112c Mon Sep 17 00:00:00 2001 From: Brandon Notowitz Date: Sun, 18 Aug 2024 18:53:01 -0400 Subject: [PATCH 4/6] newer --- backend/server.js | 18 +++++++++--------- frontend/index.js | 2 +- package-lock.json | 11 +++++++++++ package.json | 1 + 4 files changed, 22 insertions(+), 10 deletions(-) diff --git a/backend/server.js b/backend/server.js index 60b01f9b0c..d932bddd87 100644 --- a/backend/server.js +++ b/backend/server.js @@ -1,14 +1,14 @@ -const express = require('express') -const cors = require('cors') -const path = require('path') +import express from 'express'; +import cors from 'cors'; +import path from 'path'; -const help = require('./helpers') -const delay = 1000 +import help from './helpers'; +const delay = 1000; -const server = express() -server.use(express.json()) -server.use(express.static(path.join(__dirname, '../dist'))) -server.use(cors()) +const server = express(); +server.use(express.json()); +server.use(express.static(path.join(__dirname, '../dist'))); +server.use(cors()); // 1 server.post('/api/login', async (req, res, next) => { try { diff --git a/frontend/index.js b/frontend/index.js index b628313181..c8f23103e8 100644 --- a/frontend/index.js +++ b/frontend/index.js @@ -1,4 +1,4 @@ -const server = require('./backend/server') +import server from './backend/server'; const PORT = process.env.PORT || 9000 diff --git a/package-lock.json b/package-lock.json index f2b4d9f559..b817ae5fe2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29,6 +29,7 @@ "@babel/preset-react": "7.24.1", "@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", @@ -3440,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", diff --git a/package.json b/package.json index 3f61adea6e..a2bc5a37a4 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "@babel/preset-react": "7.24.1", "@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", From 52e0ab3c0ad03359495eee28e9c9bb1ad6fc2221 Mon Sep 17 00:00:00 2001 From: Brandon Notowitz Date: Sun, 18 Aug 2024 19:00:13 -0400 Subject: [PATCH 5/6] Update --- frontend/components/App.js | 417 ++++++++++++++++++++++------- frontend/components/ArticleForm.js | 106 +++++--- frontend/components/Articles.js | 67 ++--- frontend/components/LoginForm.js | 46 ++-- 4 files changed, 433 insertions(+), 203 deletions(-) diff --git a/frontend/components/App.js b/frontend/components/App.js index e3f89ae3f8..2324a97143 100644 --- a/frontend/components/App.js +++ b/frontend/components/App.js @@ -1,11 +1,10 @@ -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 axiosWithAuth from '../axios/index' import axios from 'axios' const articlesUrl = 'http://localhost:9000/api/articles' @@ -20,65 +19,69 @@ export default function App() { // ✨ Research `useNavigate` in React Router v.6 const navigate = useNavigate() - const redirectToLogin = () => {navigate('/')} - const redirectToArticles = () => {navigate('/articles')} + const redirectToLogin = () => { navigate('/')} + const redirectToArticles = () => { navigate('/articles')} const logout = () => { // ✨ implement // If a token is in local storage it should be removed, - if (localStorage.getItem('token')) { - localStorage.removeItem('token') - } // and a message saying "Goodbye!" should be set in its proper state. - setMessage('Goodbye!') // In any case, we should redirect the browser back to the login screen, // using the helper above. - redirectToLogin(); + localStorage.removeItem('token') + setMessage('Goodbye!') + redirectToLogin() } const login = ({ username, password }) => { // ✨ implement // We should flush the message state, turn on the spinner - setMessage(''); - setSpinnerOn(true); // and launch a request to the proper endpoint. - axios.post(loginUrl, {"username": username, "password": password}) // On success, we should set the token to local storage in a 'token' key, - .then((res) => { - localStorage.setItem('token', res.data.token) - // put the server success message in its proper state, and redirect - // to the Articles screen. Don't forget to turn off the spinner! - setMessage(res.data.message) - redirectToArticles(); - }) - setSpinnerOn(false) + // 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 = () => { // ✨ implement // We should flush the message state, turn on the spinner - setSpinnerOn(true); - setMessage(''); // and launch an authenticated request to the proper endpoint. - axiosWithAuth().get(articlesUrl) // On success, we should set the articles in their proper state and // put the server success message in its proper state. - .then((res) => { - console.log(res) - setArticles(res.data.articles) - setMessage(res.data.message) - setSpinnerOn(false) - }) // 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. - .catch((err) => { - console.log(err) - if (err.response.status === 401) { - redirectToLogin(); - } - setSpinnerOn(false) - }) // 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 => { @@ -86,69 +89,286 @@ 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. - setMessage(''); - setSpinnerOn(true); - axiosWithAuth().post(`http://localhost:9000/api/articles`, article) - .then((res) => { - setArticles([...articles, res.data.article]) - setMessage(res.data.message) - setSpinnerOn(false) - }) - .catch((err) => { - console.log(err) - setSpinnerOn(false) - }) + 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 = (articleId, updatedArticle) => { + const updateArticle = ({ article_id, article }) => { // ✨ implement // You got this! - console.log("BOOP! UPDATED") - const jsondArticle = JSON.stringify(updatedArticle) - setMessage(''); - setSpinnerOn(true); - axiosWithAuth().put(`http://localhost:9000/api/articles/${articleId}`, jsondArticle) - .then((res) => { - console.log(res) - // Get all but the updated article - const articlesSansUpdated = articles.filter((article) => { - return article.article_id !== articleId; + 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) }) - articlesSansUpdated.push(res.data.article) - setArticles(articlesSansUpdated) - setMessage(res.data.message); - setSpinnerOn(false); - }) - .catch((err) => { - console.log(err) - setSpinnerOn(false); - }) } - const deleteArticle = (articleId) => { + const deleteArticle = article_id => { // ✨ implement - console.log("BOOP DELETED: " + articleId) - // Here's how to do it in state: + setMessage('') setSpinnerOn(true) - axiosWithAuth().delete(`http://localhost:9000/api/articles/${articleId}`) - .then((res) => { - const allButDeleted = articles.filter((article) => { - return article.article_id !== articleId; + 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) }) - setArticles(allButDeleted) - setMessage(res.data.message) - setSpinnerOn(false) - }) - .catch((err) => { - console.log(err) - 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 ❗ <> - +
{/* <-- do not change this line */} @@ -158,27 +378,34 @@ export default function App() { Articles - } /> + } /> - art.article_id == currentArticleId)} setCurrentArticleId={setCurrentArticleId} - currentArticleId={currentArticleId} - articles={articles}/> - + + /> + + {/* art.articles_id == currentArticleId)} + setCurrentArticleId={setCurrentArticleId} /> + + */} } /> -
Bloom Institute of Technology 2022
+
Bloom Institute of Technology 2024
) diff --git a/frontend/components/ArticleForm.js b/frontend/components/ArticleForm.js index a2b2a6d8ef..cabb68520d 100644 --- a/frontend/components/ArticleForm.js +++ b/frontend/components/ArticleForm.js @@ -1,15 +1,10 @@ -import React, { useEffect, useState } from "react"; -import PT from "prop-types"; - -const initialFormValues = { title: "", text: "", topic: "" }; - -export default function ArticleForm({ - postArticle, - updateArticle, - setCurrentArticleId, - currentArticle, -}) { - const [values, setValues] = useState(initialFormValues); +import React, { useEffect, useState } from 'react' +import PT from 'prop-types' + +const initialFormValues = { title: '', text: '', topic: '' } + +export default function ArticleForm({postArticle, updateArticle, setCurrentArticleId, currentArticle}) { + const [values, setValues] = useState(initialFormValues) // ✨ where are my props? Destructure them here useEffect(() => { @@ -17,47 +12,75 @@ export default function ArticleForm({ // 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) { + if(currentArticle){ setValues({ title: currentArticle.title, text: currentArticle.text, topic: currentArticle.topic, - }); + }) } else { - setValues(initialFormValues); + setValues(initialFormValues) } - }, [currentArticle]); + //console.log("Current Article:", currentArticle) + + }, [currentArticle]) - const onChange = (evt) => { - const { id, value } = evt.target; - setValues({ ...values, [id]: value }); - }; + const onChange = evt => { + const { id, value } = evt.target + setValues({ ...values, [id]: value }) + } - const onSubmit = (evt) => { - evt.preventDefault(); + const onSubmit = evt => { + evt.preventDefault() // ✨ 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 }); - } else { - postArticle(values); - } - setValues(initialFormValues); - setCurrentArticleId(null); - }; + // 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 - return !values.title.trim() || !values.text.trim() || !values.topic; - }; + //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
-

{currentArticle ? "Edit" : "Create"} Article

+

{currentArticle + ? 'Edit Article' : 'Create Article' + }

Node
- - {currentArticle && ( - - )} + + { currentArticle && }
- ); + ) } // 🔥 No touchy: ArticleForm expects the following props exactly: @@ -95,11 +114,10 @@ ArticleForm.propTypes = { postArticle: PT.func.isRequired, updateArticle: PT.func.isRequired, setCurrentArticleId: PT.func.isRequired, - currentArticle: PT.shape({ - // can be null or undefined, meaning "create" mode (as opposed to "update") + currentArticle: PT.shape({ // can be null or undefined, meaning "create" mode (as opposed to "update") article_id: PT.number.isRequired, title: PT.string.isRequired, text: PT.string.isRequired, topic: PT.string.isRequired, - }), -}; \ No newline at end of file + }) +} \ No newline at end of file diff --git a/frontend/components/Articles.js b/frontend/components/Articles.js index ef9458c0ee..32db731b62 100644 --- a/frontend/components/Articles.js +++ b/frontend/components/Articles.js @@ -1,28 +1,24 @@ -import React, { useEffect } from "react"; -import { Navigate } from "react-router-dom"; -import PT from "prop-types"; +import React, { useEffect } from 'react' +import { Navigate } from 'react-router-dom' +import PT from 'prop-types' -export default function Articles({ - articles, - getArticles, - deleteArticle, - setCurrentArticleId, -}) { +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) - const token = localStorage.getItem("token"); + if(!localStorage.getItem('token')){ + return + } useEffect(() => { // ✨ grab the articles here, on first render only - if (token) { - getArticles(); - } - }, []); + getArticles() + }, []) - if (!token) { - return ; + const handleEdit = (article_id) => { + console.log('Edit Article ID:', article_id) + setCurrentArticleId(article_id) } return ( @@ -30,44 +26,37 @@ export default function Articles({ // and use the articles prop to generate articles

Articles

- {!articles.length - ? "No articles yet" - : articles.map((art) => { - return ( -
+ { + articles.length === 0 ? 'No articles yet' + : articles.map(art => ( +

{art.title}

{art.text}

Topic: {art.topic}

- - + +
- ); - })} + )) + }
); + } // 🔥 No touchy: Articles expects the following props exactly: Articles.propTypes = { - articles: PT.arrayOf( - PT.shape({ - // the array can be empty - article_id: PT.number.isRequired, - title: PT.string.isRequired, - text: PT.string.isRequired, - topic: PT.string.isRequired, - }) - ).isRequired, + articles: PT.arrayOf(PT.shape({ // the array can be empty + article_id: PT.number.isRequired, + title: PT.string.isRequired, + text: PT.string.isRequired, + topic: PT.string.isRequired, + })).isRequired, getArticles: PT.func.isRequired, deleteArticle: PT.func.isRequired, setCurrentArticleId: PT.func.isRequired, currentArticleId: PT.number, // can be undefined or null -}; \ No newline at end of file +} \ No newline at end of file diff --git a/frontend/components/LoginForm.js b/frontend/components/LoginForm.js index 811d2bbe30..e10f1f57d9 100644 --- a/frontend/components/LoginForm.js +++ b/frontend/components/LoginForm.js @@ -1,34 +1,33 @@ -import React, { useState } from "react"; -import PT from "prop-types"; +import React, { useState } from 'react' +import PT from 'prop-types' const initialFormValues = { - username: "", - password: "", -}; -export default function LoginForm({ login }) { - const [values, setValues] = useState(initialFormValues); + username: '', + password: '', +} +export default function LoginForm({login}) { + const [values, setValues] = useState(initialFormValues) // ✨ where are my props? Destructure them here - const onChange = (evt) => { - const { id, value } = evt.target; - setValues({ ...values, [id]: value }); - }; + const onChange = evt => { + const { id, value } = evt.target + setValues({ ...values, [id]: value }) + } - const onSubmit = (evt) => { - evt.preventDefault(); + const onSubmit = evt => { + evt.preventDefault() // ✨ implement - login(values); - }; + login(values) + } const isDisabled = () => { // ✨ implement // Trimmed username must be >= 3, and // trimmed password must be >= 8 for // the button to become enabled - return ( - values.username.trim().length < 3 || values.password.trim().length < 8 - ); - }; + const {username, password} = values + return !(username.trim().length >= 3 && password.trim().length >=8) + } return (
@@ -42,20 +41,17 @@ export default function LoginForm({ login }) { /> - +
- ); + ) } // 🔥 No touchy: LoginForm expects the following props exactly: LoginForm.propTypes = { login: PT.func.isRequired, -}; \ No newline at end of file +} \ No newline at end of file From d7f0a189d229a3ece415b016c3da3e348b8aee4c Mon Sep 17 00:00:00 2001 From: Brandon Notowitz Date: Sun, 18 Aug 2024 19:04:50 -0400 Subject: [PATCH 6/6] updated backend/server.js and frontend/index.js --- backend/server.js | 18 +++++++++--------- frontend/index.js | 21 +++++++++++++++------ 2 files changed, 24 insertions(+), 15 deletions(-) diff --git a/backend/server.js b/backend/server.js index d932bddd87..60b01f9b0c 100644 --- a/backend/server.js +++ b/backend/server.js @@ -1,14 +1,14 @@ -import express from 'express'; -import cors from 'cors'; -import path from 'path'; +const express = require('express') +const cors = require('cors') +const path = require('path') -import help from './helpers'; -const delay = 1000; +const help = require('./helpers') +const delay = 1000 -const server = express(); -server.use(express.json()); -server.use(express.static(path.join(__dirname, '../dist'))); -server.use(cors()); +const server = express() +server.use(express.json()) +server.use(express.static(path.join(__dirname, '../dist'))) +server.use(cors()) // 1 server.post('/api/login', async (req, res, next) => { try { diff --git a/frontend/index.js b/frontend/index.js index c8f23103e8..83908c1c4c 100644 --- a/frontend/index.js +++ b/frontend/index.js @@ -1,7 +1,16 @@ -import server from './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}`) -}) \ No newline at end of file +const root = createRoot(document.getElementById('root')) +root.render( + + + +) \ No newline at end of file