Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down
180 changes: 138 additions & 42 deletions frontend/components/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -18,76 +19,171 @@ 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();
getArticles()
})
.catch (() => {
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) => {
// 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) => {
console.error('Error fetching articles:', error);
if (error.response?.status === 401) {
setMessage('Session expired. Please log in again.');
redirectToLogin();
} else {
setMessage('Failed to retrieve articles. Please try again.');
}
})
.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 => {
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 (() => {
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.article : art // Replace updated article
));
setMessage(response.data.message); // Set a success message
})
.catch(()=> {
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((response) => {
setArticles(articles.filter(art => art.article_id !== article_id)); // Remove the deleted article
setMessage(response.data.message); // Set a success message
})
.catch(() => {
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 ❗
<>
<Spinner />
<Message />
<Spinner on={spinnerOn} />
<Message message={message} />
<button id="logout" onClick={logout}>Logout from app</button>
<div id="wrapper" style={{ opacity: spinnerOn ? "0.25" : "1" }}> {/* <-- do not change this line */}
<div id="wrapper" style={{ opacity: spinnerOn ? "0.25" : "1" }}>
<h1>Advanced Web Applications</h1>
<nav>
<NavLink id="loginScreen" to="/">Login</NavLink>
<NavLink id="articlesScreen" to="/articles">Articles</NavLink>
</nav>
<Routes>
<Route path="/" element={<LoginForm />} />
<Route path="/" element={<LoginForm login={login} />} />
<Route path="articles" element={
<>
<ArticleForm />
<Articles />
<ArticleForm
currentArticle = {articles.find(art=> art.article_id == currentArticleId)}
postArticle={postArticle}
updateArticle={updateArticle}
setCurrentArticleId={setCurrentArticleId}
/>
<Articles
articles={articles}
getArticles={getArticles}
deleteArticle={deleteArticle}
setCurrentArticleId={setCurrentArticleId}
currentArticleId={currentArticleId}
/>
</>
} />
</Routes>
<footer>Bloom Institute of Technology 2024</footer>
</div>
</>
)
);
}
48 changes: 34 additions & 14 deletions frontend/components/ArticleForm.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -21,21 +27,35 @@ 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();
};

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.text || !values.topic) {
// 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
<form id="form" onSubmit={onSubmit}>
<h2>Create Article</h2>
<h2>{currentArticle ? "Edit Article" : "Create Article"}</h2>

<input
maxLength={50}
onChange={onChange}
Expand All @@ -58,7 +78,7 @@ export default function ArticleForm(props) {
</select>
<div className="button-group">
<button disabled={isDisabled()} id="submitArticle">Submit</button>
<button onClick={Function.prototype}>Cancel edit</button>
<button onClick={ () => setCurrentArticleId(null)}>Cancel edit</button>
</div>
</form>
)
Expand Down
68 changes: 33 additions & 35 deletions frontend/components/Articles.js
Original file line number Diff line number Diff line change
@@ -1,48 +1,46 @@
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, currentArticleId}) {
const token = localStorage.getItem('token');

// ✨ implement conditional logic: if no token exists
// we should render a Navigate to login screen (React Router v.6)
// Ensure token validation
if (!token || token === 'undefined' || token === '') {
return <Navigate to="/" />;
}

useEffect(() => {
// ✨ grab the articles here, on first render only
})
if (token) {
getArticles();
}
}, [token]);

console.log (articles);

return (
// ✨ fix the JSX: replace `Function.prototype` with actual functions
// and use the articles prop to generate articles
<div className="articles">
<h2>Articles</h2>
{
![].length
? 'No articles yet'
: [].map(art => {
return (
<div className="article" key={art.article_id}>
<div>
<h3>{art.title}</h3>
<p>{art.text}</p>
<p>Topic: {art.topic}</p>
</div>
<div>
<button disabled={true} onClick={Function.prototype}>Edit</button>
<button disabled={true} onClick={Function.prototype}>Delete</button>
</div>
</div>
)
})
}
{articles.length === 0 ? (
<p>No articles yet</p>
) : (
articles.map((art) => (
<div key={art.article_id} className="article">
<h3>{art.title}</h3>
<p>{art.text}</p>
<p>Topic: {art.topic}</p>
<button onClick={() => setCurrentArticleId(art.article_id)}>Edit</button>
<button onClick={() => deleteArticle(art.article_id)}>Delete</button>
</div>
))
)}
</div>
)
);
}

// 🔥 No touchy: Articles expects the following props exactly:

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,
Expand All @@ -51,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,
};
Loading