From 8c355d9151abe95b9a8b5182aed07d68e252b010 Mon Sep 17 00:00:00 2001 From: nabeelmumtaz21 Date: Thu, 5 Dec 2024 10:37:44 -0600 Subject: [PATCH 1/3] commit 1 --- frontend/components/Api.js | 77 +++++++++++++ frontend/components/App.js | 171 +++++++++++++++++++++------- frontend/components/ArticleForm.js | 50 +++++--- frontend/components/Articles.js | 67 ++++++----- frontend/components/LoginForm.js | 55 +++++---- frontend/components/Spinner.js | 2 +- frontend/components/Spinner.test.js | 30 ++++- package-lock.json | 6 +- package.json | 4 +- 9 files changed, 344 insertions(+), 118 deletions(-) create mode 100644 frontend/components/Api.js diff --git a/frontend/components/Api.js b/frontend/components/Api.js new file mode 100644 index 0000000000..76f28e3455 --- /dev/null +++ b/frontend/components/Api.js @@ -0,0 +1,77 @@ +import axios from 'axios'; + +const BASE_URL = 'http://localhost:9000/api'; + +// Login: Authenticates the user and returns a token +export const login = async ({ username, password }) => { + try { + const payload = { username: username.trim(), password: password.trim() }; + const response = await axios.post(`${BASE_URL}/login`, payload); + return response.data; // Expected to include the token + } catch (error) { + console.error('Error logging in:', error); + throw error.response.data; // Rethrow for handling in components + } +}; + +// Fetch all articles +export const getArticles = async (token) => { + try { + const response = await axios.get(`${BASE_URL}/articles`, { + headers: { Authorization: token }, + }); + return response.data; // Returns the list of articles + } catch (error) { + console.error('Error fetching articles:', error); + throw error.response.data; + } +}; + +// Create a new article +export const createArticle = async (token, { title, text, topic }) => { + try { + const payload = { + title: title.trim(), + text: text.trim(), + topic, + }; + const response = await axios.post(`${BASE_URL}/articles`, payload, { + headers: { Authorization: token }, + }); + return response.data; // Returns the created article + } catch (error) { + console.error('Error creating article:', error); + throw error.response.data; + } +}; + +// Update an existing article +export const updateArticle = async (token, articleId, { title, text, topic }) => { + try { + const payload = { + title: title.trim(), + text: text.trim(), + topic, + }; + const response = await axios.put(`${BASE_URL}/articles/${articleId}`, payload, { + headers: { Authorization: token }, + }); + return response.data; // Returns the updated article + } catch (error) { + console.error('Error updating article:', error); + throw error.response.data; + } +}; + +// Delete an article +export const deleteArticle = async (token, articleId) => { + try { + const response = await axios.delete(`${BASE_URL}/articles/${articleId}`, { + headers: { Authorization: token }, + }); + return response.data; // Returns success message + } catch (error) { + console.error('Error deleting article:', error); + throw error.response.data; + } +}; diff --git a/frontend/components/App.js b/frontend/components/App.js index c4b6d6ce5c..db33d582b4 100644 --- a/frontend/components/App.js +++ b/frontend/components/App.js @@ -18,76 +18,163 @@ export default function App() { // ✨ Research `useNavigate` in React Router v.6 const navigate = useNavigate() - const redirectToLogin = () => { /* ✨ implement */ } - const redirectToArticles = () => { /* ✨ implement */ } + const redirectToLogin = () => { navigate ('/'); - const logout = () => { - // ✨ implement - // If a token is in local storage it should be removed, - // 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. - } + } + const redirectToArticles = () => { + navigate ('/articles'); + }; + const logout = () => { + localStorage.removeItem('token'); + setMessage('Goodbye!'); + redirectToLogin(); + }; + const login = ({ 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); + axios.post (loginURL, {username, password }) + .then(response => { + const token = response.data.token; + localStorage.setItem('token', token); + setMessage('Login successful!'); + redirectToArticles(); + }) + .catch (error => { + setMessage (' Login failed. Please check your credentials.'); + }) + .finally(() => { + setSpinnerOn(false); + }) } const getArticles = () => { - // ✨ 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(''); // Clear any previous messages + setSpinnerOn(true); // Show the spinner while fetching articles + + const token = localStorage.getItem('token'); // Retrieve the token from local storage + + axios.get(articlesUrl, { + headers: { Authorization: token } // Pass the token in the Authorization header + }) + .then(response => { + setArticles(response.data); // Update the articles state with the response data + setMessage('Articles retrieved successfully!'); // Set a success message + }) + .catch(error => { + if (error.response?.status === 401) { + setMessage('Session expired. Please log in again.'); // Set a message for unauthorized access + redirectToLogin(); // Redirect to the login page + } else { + setMessage('Failed to retrieve articles. Please try again.'); // General error message + } + }) + .finally(() => { + setSpinnerOn(false); // Hide the spinner after the request is complete + }); + }; + const postArticle = 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(''); // Clear any previous messages + setSpinnerOn(true); // Show the spinner while making the request + + const token = localStorage.getItem('token'); // Retrieve the token + + axios.post(articlesUrl, article, { + headers: { Authorization: token } // Pass the token in the Authorization header + }) + .then(response => { + setArticles([...articles, response.data]); // Add the new article to the state + setMessage('Article successfully added!'); // Set a success message + }) + .catch(error => { + setMessage('Failed to add article. Please try again.'); // Set an error message + }) + .finally(() => { + setSpinnerOn(false); // Hide the spinner after the request is complete + }); + }; + const updateArticle = ({ article_id, article }) => { - // ✨ implement - // You got this! - } + setMessage(''); // Clear any previous messages + setSpinnerOn(true); // Show the spinner while making the request + + const token = localStorage.getItem('token'); // Retrieve the token + + axios.put(`${articlesUrl}/${article_id}`, article, { + headers: { Authorization: token } // Pass the token in the Authorization header + }) + .then(response => { + setArticles(articles.map(art => + art.article_id === article_id ? response.data : art // Replace updated article + )); + setMessage('Article successfully updated!'); // Set a success message + }) + .catch(error => { + setMessage('Failed to update article. Please try again.'); // Set an error message + }) + .finally(() => { + setSpinnerOn(false); // Hide the spinner after the request is complete + }); + }; + const deleteArticle = article_id => { - // ✨ implement - } + setMessage(''); // Clear any previous messages + setSpinnerOn(true); // Show the spinner while making the request + + const token = localStorage.getItem('token'); // Retrieve the token + + axios.delete(`${articlesUrl}/${article_id}`, { + headers: { Authorization: token } // Pass the token in the Authorization header + }) + .then(() => { + setArticles(articles.filter(art => art.article_id !== article_id)); // Remove the deleted article + setMessage('Article successfully deleted!'); // Set a success message + }) + .catch(error => { + setMessage('Failed to delete article. Please try again.'); // Set an error message + }) + .finally(() => { + setSpinnerOn(false); // Hide the spinner after the request is complete + }); + }; + return ( - // ✨ fix the JSX: `Spinner`, `Message`, `LoginForm`, `ArticleForm` and `Articles` expect props ❗ <> - - + + -
{/* <-- do not change this line */} +

Advanced Web Applications

- } /> + } /> - - + + } />
Bloom Institute of Technology 2024
- ) + ); } diff --git a/frontend/components/ArticleForm.js b/frontend/components/ArticleForm.js index 3b8d1afcd4..d4c0adcaa5 100644 --- a/frontend/components/ArticleForm.js +++ b/frontend/components/ArticleForm.js @@ -5,14 +5,20 @@ const initialFormValues = { title: '', text: '', topic: '' } export default function ArticleForm(props) { const [values, setValues] = useState(initialFormValues) + const { postArticle, updateArticle, setCurrentArticleId, currentArticle } = props; // ✨ where are my props? Destructure them here useEffect(() => { - // ✨ implement - // 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); + } + }, [currentArticle]); const onChange = evt => { const { id, value } = evt.target @@ -21,21 +27,37 @@ export default function ArticleForm(props) { 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); + }; } const isDisabled = () => { - // ✨ implement - // Make sure the inputs have some values - } + // Assuming `values` contains the form data as an object, e.g., + // { title: "some text", body: "some content" } + + if (!values.title || !values.body) { + // If either title or body is empty, disable the form + return true; + } + + // All inputs have values; enable the form + return false; + }; + 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"}

+
- +
) -} + // 🔥 No touchy: ArticleForm expects the following props exactly: ArticleForm.propTypes = { diff --git a/frontend/components/Articles.js b/frontend/components/Articles.js index 78336919e8..a9b096cca4 100644 --- a/frontend/components/Articles.js +++ b/frontend/components/Articles.js @@ -1,46 +1,53 @@ -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(props) { - // ✨ where are my props? Destructure them here +export default function Articles({ + articles, + getArticles, + deleteArticle, + setCurrentArticleId, +}) { + const token = localStorage.getItem('token'); - // ✨ implement conditional logic: if no token exists - // we should render a Navigate to login screen (React Router v.6) + // If no token exists, redirect to login + if (!token) { + return ; + } + // Fetch articles on first render useEffect(() => { - // ✨ grab the articles here, on first render only - }) + getArticles(); + }, [getArticles]); return ( - // ✨ fix the JSX: replace `Function.prototype` with actual functions - // and use the articles prop to generate articles

Articles

{ - ![].length - ? 'No articles yet' - : [].map(art => { - return ( -
-
-

{art.title}

-

{art.text}

-

Topic: {art.topic}

-
-
- - -
+ !articles.length + ?

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, @@ -52,4 +59,4 @@ Articles.propTypes = { deleteArticle: PT.func.isRequired, setCurrentArticleId: PT.func.isRequired, currentArticleId: PT.number, // can be undefined or null -} +}; diff --git a/frontend/components/LoginForm.js b/frontend/components/LoginForm.js index f7702ea1ad..6f73567896 100644 --- a/frontend/components/LoginForm.js +++ b/frontend/components/LoginForm.js @@ -1,30 +1,38 @@ -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(props) { - const [values, setValues] = useState(initialFormValues) - // ✨ where are my props? Destructure them here +}; + +export default function LoginForm({ login }) { + const [values, setValues] = useState(initialFormValues); + + // Handle input changes + const onChange = (evt) => { + const { id, value } = evt.target; + setValues({ ...values, [id]: value }); + }; - const onChange = evt => { - const { id, value } = evt.target - setValues({ ...values, [id]: value }) - } + // Handle form submission + const onSubmit = (evt) => { + evt.preventDefault(); - const onSubmit = evt => { - evt.preventDefault() - // ✨ implement - } + // Trim inputs to ensure no unnecessary spaces + const trimmedUsername = values.username.trim(); + const trimmedPassword = values.password.trim(); + // Call the `login` function with trimmed values + login({ username: trimmedUsername, password: trimmedPassword }); + }; + + // Determine if the submit button should be disabled const isDisabled = () => { - // ✨ implement - // Trimmed username must be >= 3, and - // trimmed password must be >= 8 for - // the button to become enabled - } + const trimmedUsername = values.username.trim(); + const trimmedPassword = values.password.trim(); + return trimmedUsername.length < 3 || trimmedPassword.length < 8; + }; return (
@@ -38,17 +46,20 @@ export default function LoginForm(props) { /> - +
- ) + ); } // 🔥 No touchy: LoginForm expects the following props exactly: LoginForm.propTypes = { login: PT.func.isRequired, -} +}; diff --git a/frontend/components/Spinner.js b/frontend/components/Spinner.js index e928351db4..8de050b1ad 100644 --- a/frontend/components/Spinner.js +++ b/frontend/components/Spinner.js @@ -24,7 +24,7 @@ const StyledSpinner = styled.div` export default function Spinner({ on }) { if (!on) return null return ( - +

 .

   Please wait...
) diff --git a/frontend/components/Spinner.test.js b/frontend/components/Spinner.test.js index 9a5773d2de..9d1948beee 100644 --- a/frontend/components/Spinner.test.js +++ b/frontend/components/Spinner.test.js @@ -1,5 +1,25 @@ -// 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'; // Adjust the import path based on your file structure + +describe('Spinner Component', () => { + test('renders correctly when "on" is true', () => { + render(); + + // Check if the spinner is in the document + const spinner = screen.getByText(/please wait/i); + expect(spinner).toBeInTheDocument(); + + // Check if the spinner has the correct ID + const spinnerContainer = screen.getByTestId('spinner'); + expect(spinnerContainer).toBeInTheDocument(); + }); + + test('does not render when "on" is false', () => { + render(); + + // Ensure the spinner is not in the document + expect(screen.queryByText(/please wait/i)).not.toBeInTheDocument(); + }); +}); diff --git a/package-lock.json b/package-lock.json index e780dc3152..acb2285f96 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,8 +26,8 @@ "@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/react": "15.0.1", + "@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", @@ -3213,6 +3213,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", @@ -3325,6 +3326,7 @@ "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-15.0.1.tgz", "integrity": "sha512-I8u4qqGAuBg7C1/kRB9n7Oz9Pc/UHEkmiQRbJziSG8B13eZfAcAUn8TSP26ZIvfSeb68CngmtZbKKcRqcQKa3g==", "dev": true, + "license": "MIT", "dependencies": { "@babel/runtime": "^7.12.5", "@testing-library/dom": "^10.0.0", diff --git a/package.json b/package.json index c3309648de..75e155a61e 100644 --- a/package.json +++ b/package.json @@ -16,8 +16,8 @@ "@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/react": "15.0.1", + "@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", From f86ed3eaa17e59b44eed1d69a2c51b20a7f1e4f6 Mon Sep 17 00:00:00 2001 From: nabeelmumtaz21 Date: Thu, 26 Dec 2024 11:45:17 -0600 Subject: [PATCH 2/3] initial commit --- frontend/components/Api.js | 77 ------------------------------ frontend/components/App.js | 53 +++++++++++--------- frontend/components/ArticleForm.js | 10 ++-- frontend/components/Articles.js | 61 ++++++++++------------- frontend/components/Message.js | 7 +-- package-lock.json | 63 +++++++++--------------- package.json | 6 +-- 7 files changed, 89 insertions(+), 188 deletions(-) delete mode 100644 frontend/components/Api.js diff --git a/frontend/components/Api.js b/frontend/components/Api.js deleted file mode 100644 index 76f28e3455..0000000000 --- a/frontend/components/Api.js +++ /dev/null @@ -1,77 +0,0 @@ -import axios from 'axios'; - -const BASE_URL = 'http://localhost:9000/api'; - -// Login: Authenticates the user and returns a token -export const login = async ({ username, password }) => { - try { - const payload = { username: username.trim(), password: password.trim() }; - const response = await axios.post(`${BASE_URL}/login`, payload); - return response.data; // Expected to include the token - } catch (error) { - console.error('Error logging in:', error); - throw error.response.data; // Rethrow for handling in components - } -}; - -// Fetch all articles -export const getArticles = async (token) => { - try { - const response = await axios.get(`${BASE_URL}/articles`, { - headers: { Authorization: token }, - }); - return response.data; // Returns the list of articles - } catch (error) { - console.error('Error fetching articles:', error); - throw error.response.data; - } -}; - -// Create a new article -export const createArticle = async (token, { title, text, topic }) => { - try { - const payload = { - title: title.trim(), - text: text.trim(), - topic, - }; - const response = await axios.post(`${BASE_URL}/articles`, payload, { - headers: { Authorization: token }, - }); - return response.data; // Returns the created article - } catch (error) { - console.error('Error creating article:', error); - throw error.response.data; - } -}; - -// Update an existing article -export const updateArticle = async (token, articleId, { title, text, topic }) => { - try { - const payload = { - title: title.trim(), - text: text.trim(), - topic, - }; - const response = await axios.put(`${BASE_URL}/articles/${articleId}`, payload, { - headers: { Authorization: token }, - }); - return response.data; // Returns the updated article - } catch (error) { - console.error('Error updating article:', error); - throw error.response.data; - } -}; - -// Delete an article -export const deleteArticle = async (token, articleId) => { - try { - const response = await axios.delete(`${BASE_URL}/articles/${articleId}`, { - headers: { Authorization: token }, - }); - return response.data; // Returns success message - } catch (error) { - console.error('Error deleting article:', error); - throw error.response.data; - } -}; diff --git a/frontend/components/App.js b/frontend/components/App.js index db33d582b4..7986231398 100644 --- a/frontend/components/App.js +++ b/frontend/components/App.js @@ -5,6 +5,7 @@ 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' @@ -34,15 +35,17 @@ export default function App() { const login = ({ username, password }) => { setMessage (''); setSpinnerOn(true); - axios.post (loginURL, {username, password }) + + axios.post (loginUrl, {username, password }) .then(response => { const token = response.data.token; localStorage.setItem('token', token); setMessage('Login successful!'); redirectToArticles(); + getArticles() }) - .catch (error => { - setMessage (' Login failed. Please check your credentials.'); + .catch (() => { + setMessage ('Login failed. Please check your credentials.'); }) .finally(() => { setSpinnerOn(false); @@ -52,22 +55,24 @@ export default function App() { const getArticles = () => { setMessage(''); // Clear any previous messages setSpinnerOn(true); // Show the spinner while fetching articles - const token = localStorage.getItem('token'); // Retrieve the token from local storage axios.get(articlesUrl, { - headers: { Authorization: token } // Pass the token in the Authorization header + headers: { Authorization: token }, // Pass the token in the Authorization header }) - .then(response => { - setArticles(response.data); // Update the articles state with the response data - setMessage('Articles retrieved successfully!'); // Set a success message + .then((response) => { + // Log the response to debug its structure + console.log('Response data:', response.data); + setArticles(response.data.articles); // Use the array directly + setMessage(response.data.message); }) - .catch(error => { + .catch((error) => { + console.error('Error fetching articles:', error); if (error.response?.status === 401) { - setMessage('Session expired. Please log in again.'); // Set a message for unauthorized access - redirectToLogin(); // Redirect to the login page + setMessage('Session expired. Please log in again.'); + redirectToLogin(); } else { - setMessage('Failed to retrieve articles. Please try again.'); // General error message + setMessage('Failed to retrieve articles. Please try again.'); } }) .finally(() => { @@ -86,10 +91,12 @@ export default function App() { headers: { Authorization: token } // Pass the token in the Authorization header }) .then(response => { - setArticles([...articles, response.data]); // Add the new article to the state - setMessage('Article successfully added!'); // Set a success message + console.log ("postresponse",response); + setArticles([...articles, response.data.article]); // Add the new article to the state + setMessage(response.data.message); // Set a success message + }) - .catch(error => { + .catch (() => { setMessage('Failed to add article. Please try again.'); // Set an error message }) .finally(() => { @@ -109,11 +116,11 @@ export default function App() { }) .then(response => { setArticles(articles.map(art => - art.article_id === article_id ? response.data : art // Replace updated article + art.article_id === article_id ? response.data.article : art // Replace updated article )); - setMessage('Article successfully updated!'); // Set a success message + setMessage(response.data.message); // Set a success message }) - .catch(error => { + .catch(()=> { setMessage('Failed to update article. Please try again.'); // Set an error message }) .finally(() => { @@ -131,11 +138,11 @@ export default function App() { axios.delete(`${articlesUrl}/${article_id}`, { headers: { Authorization: token } // Pass the token in the Authorization header }) - .then(() => { + .then((response) => { setArticles(articles.filter(art => art.article_id !== article_id)); // Remove the deleted article - setMessage('Article successfully deleted!'); // Set a success message + setMessage(response.data.message); // Set a success message }) - .catch(error => { + .catch(() => { setMessage('Failed to delete article. Please try again.'); // Set an error message }) .finally(() => { @@ -160,15 +167,17 @@ export default function App() { art.article_id == currentArticleId)} postArticle={postArticle} updateArticle={updateArticle} setCurrentArticleId={setCurrentArticleId} /> } /> diff --git a/frontend/components/ArticleForm.js b/frontend/components/ArticleForm.js index d4c0adcaa5..8b8aca8b87 100644 --- a/frontend/components/ArticleForm.js +++ b/frontend/components/ArticleForm.js @@ -34,15 +34,13 @@ export default function ArticleForm(props) { postArticle(values); } setValues(initialFormValues); - setCurrentArticleId(null); + setCurrentArticleId(); }; - } const isDisabled = () => { // Assuming `values` contains the form data as an object, e.g., // { title: "some text", body: "some content" } - - if (!values.title || !values.body) { + if (!values.title || !values.text || !values.topic) { // If either title or body is empty, disable the form return true; } @@ -80,11 +78,11 @@ export default function ArticleForm(props) {
- +
) - +} // 🔥 No touchy: ArticleForm expects the following props exactly: ArticleForm.propTypes = { diff --git a/frontend/components/Articles.js b/frontend/components/Articles.js index a9b096cca4..452cc7bc47 100644 --- a/frontend/components/Articles.js +++ b/frontend/components/Articles.js @@ -2,54 +2,45 @@ 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}) { const token = localStorage.getItem('token'); - // If no token exists, redirect to login - if (!token) { - return ; + // Ensure token validation + if (!token || token === 'undefined' || token === '') { + return ; } - // Fetch articles on first render useEffect(() => { - getArticles(); - }, [getArticles]); + if (token) { + getArticles(); + } + }, [token]); + + console.log (articles); return (

Articles

- { - !articles.length - ?

No articles yet

- : articles.map(art => ( -
-
-

{art.title}

-

{art.text}

-

Topic: {art.topic}

-
-
- - -
-
- )) - } + {articles.length === 0 ? ( +

No articles yet

+ ) : ( + articles.map((art) => ( +
+

{art.title}

+

{art.text}

+

Topic: {art.topic}

+ + +
+ )) + )}
); } + Articles.propTypes = { - articles: PT.arrayOf(PT.shape({ // the array can be empty + articles: PT.arrayOf(PT.shape({ article_id: PT.number.isRequired, title: PT.string.isRequired, text: PT.string.isRequired, @@ -58,5 +49,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, }; diff --git a/frontend/components/Message.js b/frontend/components/Message.js index 03713f865f..34bf7fc9eb 100644 --- a/frontend/components/Message.js +++ b/frontend/components/Message.js @@ -12,13 +12,10 @@ const StyledMessage = styled.div` ` export default function Message({ message }) { - return ( - - {message} - - ) + return message ? {message} : null; } + Message.propTypes = { message: PT.string.isRequired, } diff --git a/package-lock.json b/package-lock.json index acb2285f96..c1e2006aa7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,8 +26,8 @@ "@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/react": "^15.0.1", + "@testing-library/jest-dom": "^6.6.3", + "@testing-library/react": "^15.0.7", "@types/jest": "29.5.12", "babel-loader": "9.1.3", "babel-plugin-styled-components": "2.1.4", @@ -39,7 +39,7 @@ "fkill-cli": "8.0.0", "html-loader": "5.0.0", "html-webpack-plugin": "5.6.0", - "jest": "29.7.0", + "jest": "^29.7.0", "jest-environment-jsdom": "29.7.0", "msw": "1.3.3", "nodemon": "3.1.0", @@ -64,10 +64,11 @@ } }, "node_modules/@adobe/css-tools": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.3.3.tgz", - "integrity": "sha512-rE0Pygv0sEZ4vBWHlAgJLGDU7Pm8xoO6p3wsEceb7GYAjScrOHpEo8KK/eVkAcnSM+slAEtXjA2JpdjLp4fJQQ==", - "dev": true + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.1.tgz", + "integrity": "sha512-12WGKBQzjUAI4ayyF4IAtfw2QR/IDoqk6jTddXDhtYTJF9ASmoE1zst7cVtP0aL/F1jUJL5r+JxKXKEgHNbEUQ==", + "dev": true, + "license": "MIT" }, "node_modules/@ampproject/remapping": { "version": "2.3.0", @@ -3209,49 +3210,24 @@ } }, "node_modules/@testing-library/jest-dom": { - "version": "6.4.2", - "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.4.2.tgz", - "integrity": "sha512-CzqH0AFymEMG48CpzXFriYYkOjk6ZGPCLMhW9e9jg3KMCn5OfJecF8GtGW7yGfR/IgCe3SX8BSwjdzI6BBbZLw==", + "version": "6.6.3", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.6.3.tgz", + "integrity": "sha512-IteBhl4XqYNkM54f4ejhLRJiZNqcSCoXUOG2CPK7qbD322KjQozM4kHQOfkG2oln9b9HTYqs+Sae8vBATubxxA==", "dev": true, "license": "MIT", "dependencies": { - "@adobe/css-tools": "^4.3.2", - "@babel/runtime": "^7.9.2", + "@adobe/css-tools": "^4.4.0", "aria-query": "^5.0.0", "chalk": "^3.0.0", "css.escape": "^1.5.1", "dom-accessibility-api": "^0.6.3", - "lodash": "^4.17.15", + "lodash": "^4.17.21", "redent": "^3.0.0" }, "engines": { "node": ">=14", "npm": ">=6", "yarn": ">=1" - }, - "peerDependencies": { - "@jest/globals": ">= 28", - "@types/bun": "latest", - "@types/jest": ">= 28", - "jest": ">= 28", - "vitest": ">= 0.32" - }, - "peerDependenciesMeta": { - "@jest/globals": { - "optional": true - }, - "@types/bun": { - "optional": true - }, - "@types/jest": { - "optional": true - }, - "jest": { - "optional": true - }, - "vitest": { - "optional": true - } } }, "node_modules/@testing-library/jest-dom/node_modules/ansi-styles": { @@ -3322,9 +3298,9 @@ } }, "node_modules/@testing-library/react": { - "version": "15.0.1", - "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-15.0.1.tgz", - "integrity": "sha512-I8u4qqGAuBg7C1/kRB9n7Oz9Pc/UHEkmiQRbJziSG8B13eZfAcAUn8TSP26ZIvfSeb68CngmtZbKKcRqcQKa3g==", + "version": "15.0.7", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-15.0.7.tgz", + "integrity": "sha512-cg0RvEdD1TIhhkm1IeYMQxrzy0MtUNfa3minv4MjbgcYzJAZ7yD0i0lwoPOTPr+INtiXFezt2o8xMSnyHhEn2Q==", "dev": true, "license": "MIT", "dependencies": { @@ -3336,8 +3312,14 @@ "node": ">=18" }, "peerDependencies": { + "@types/react": "^18.0.0", "react": "^18.0.0", "react-dom": "^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } } }, "node_modules/@tootallnate/once": { @@ -9243,6 +9225,7 @@ "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", "dev": true, + "license": "MIT", "dependencies": { "@jest/core": "^29.7.0", "@jest/types": "^29.6.3", diff --git a/package.json b/package.json index 75e155a61e..b9452c28df 100644 --- a/package.json +++ b/package.json @@ -16,8 +16,8 @@ "@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/react": "^15.0.1", + "@testing-library/jest-dom": "^6.6.3", + "@testing-library/react": "^15.0.7", "@types/jest": "29.5.12", "babel-loader": "9.1.3", "babel-plugin-styled-components": "2.1.4", @@ -29,7 +29,7 @@ "fkill-cli": "8.0.0", "html-loader": "5.0.0", "html-webpack-plugin": "5.6.0", - "jest": "29.7.0", + "jest": "^29.7.0", "jest-environment-jsdom": "29.7.0", "msw": "1.3.3", "nodemon": "3.1.0", From 2f8ed09b36254fdca5a17b3373c4fbdc65116e36 Mon Sep 17 00:00:00 2001 From: nabeelmumtaz21 Date: Thu, 2 Jan 2025 11:57:40 -0600 Subject: [PATCH 3/3] commit 4 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 22dff41821..6f46fa1abf 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ In this challenge, you will write the logic for [THIS APP](https://advanced-apps ❗ Other configurations might work but haven't been tested. ## Project Setup - +s - Fork, clone, and `npm install`. You won't need to add any extra libraries. - Launch the project in a development server executing `npm run dev`. - Visit your app by navigating Chrome to `http://localhost:3000`.