Skip to content

Commit 8c355d9

Browse files
commit 1
1 parent 12be9f8 commit 8c355d9

File tree

9 files changed

+344
-118
lines changed

9 files changed

+344
-118
lines changed

frontend/components/Api.js

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import axios from 'axios';
2+
3+
const BASE_URL = 'http://localhost:9000/api';
4+
5+
// Login: Authenticates the user and returns a token
6+
export const login = async ({ username, password }) => {
7+
try {
8+
const payload = { username: username.trim(), password: password.trim() };
9+
const response = await axios.post(`${BASE_URL}/login`, payload);
10+
return response.data; // Expected to include the token
11+
} catch (error) {
12+
console.error('Error logging in:', error);
13+
throw error.response.data; // Rethrow for handling in components
14+
}
15+
};
16+
17+
// Fetch all articles
18+
export const getArticles = async (token) => {
19+
try {
20+
const response = await axios.get(`${BASE_URL}/articles`, {
21+
headers: { Authorization: token },
22+
});
23+
return response.data; // Returns the list of articles
24+
} catch (error) {
25+
console.error('Error fetching articles:', error);
26+
throw error.response.data;
27+
}
28+
};
29+
30+
// Create a new article
31+
export const createArticle = async (token, { title, text, topic }) => {
32+
try {
33+
const payload = {
34+
title: title.trim(),
35+
text: text.trim(),
36+
topic,
37+
};
38+
const response = await axios.post(`${BASE_URL}/articles`, payload, {
39+
headers: { Authorization: token },
40+
});
41+
return response.data; // Returns the created article
42+
} catch (error) {
43+
console.error('Error creating article:', error);
44+
throw error.response.data;
45+
}
46+
};
47+
48+
// Update an existing article
49+
export const updateArticle = async (token, articleId, { title, text, topic }) => {
50+
try {
51+
const payload = {
52+
title: title.trim(),
53+
text: text.trim(),
54+
topic,
55+
};
56+
const response = await axios.put(`${BASE_URL}/articles/${articleId}`, payload, {
57+
headers: { Authorization: token },
58+
});
59+
return response.data; // Returns the updated article
60+
} catch (error) {
61+
console.error('Error updating article:', error);
62+
throw error.response.data;
63+
}
64+
};
65+
66+
// Delete an article
67+
export const deleteArticle = async (token, articleId) => {
68+
try {
69+
const response = await axios.delete(`${BASE_URL}/articles/${articleId}`, {
70+
headers: { Authorization: token },
71+
});
72+
return response.data; // Returns success message
73+
} catch (error) {
74+
console.error('Error deleting article:', error);
75+
throw error.response.data;
76+
}
77+
};

frontend/components/App.js

Lines changed: 129 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -18,76 +18,163 @@ export default function App() {
1818

1919
// ✨ Research `useNavigate` in React Router v.6
2020
const navigate = useNavigate()
21-
const redirectToLogin = () => { /* ✨ implement */ }
22-
const redirectToArticles = () => { /* ✨ implement */ }
21+
const redirectToLogin = () => { navigate ('/');
2322

24-
const logout = () => {
25-
// ✨ implement
26-
// If a token is in local storage it should be removed,
27-
// and a message saying "Goodbye!" should be set in its proper state.
28-
// In any case, we should redirect the browser back to the login screen,
29-
// using the helper above.
30-
}
23+
}
24+
const redirectToArticles = () => {
25+
navigate ('/articles');
26+
};
3127

28+
const logout = () => {
29+
localStorage.removeItem('token');
30+
setMessage('Goodbye!');
31+
redirectToLogin();
32+
};
33+
3234
const login = ({ username, password }) => {
33-
// ✨ implement
34-
// We should flush the message state, turn on the spinner
35-
// and launch a request to the proper endpoint.
36-
// On success, we should set the token to local storage in a 'token' key,
37-
// put the server success message in its proper state, and redirect
38-
// to the Articles screen. Don't forget to turn off the spinner!
35+
setMessage ('');
36+
setSpinnerOn(true);
37+
axios.post (loginURL, {username, password })
38+
.then(response => {
39+
const token = response.data.token;
40+
localStorage.setItem('token', token);
41+
setMessage('Login successful!');
42+
redirectToArticles();
43+
})
44+
.catch (error => {
45+
setMessage (' Login failed. Please check your credentials.');
46+
})
47+
.finally(() => {
48+
setSpinnerOn(false);
49+
})
3950
}
4051

4152
const getArticles = () => {
42-
// ✨ implement
43-
// We should flush the message state, turn on the spinner
44-
// and launch an authenticated request to the proper endpoint.
45-
// On success, we should set the articles in their proper state and
46-
// put the server success message in its proper state.
47-
// If something goes wrong, check the status of the response:
48-
// if it's a 401 the token might have gone bad, and we should redirect to login.
49-
// Don't forget to turn off the spinner!
50-
}
53+
setMessage(''); // Clear any previous messages
54+
setSpinnerOn(true); // Show the spinner while fetching articles
55+
56+
const token = localStorage.getItem('token'); // Retrieve the token from local storage
57+
58+
axios.get(articlesUrl, {
59+
headers: { Authorization: token } // Pass the token in the Authorization header
60+
})
61+
.then(response => {
62+
setArticles(response.data); // Update the articles state with the response data
63+
setMessage('Articles retrieved successfully!'); // Set a success message
64+
})
65+
.catch(error => {
66+
if (error.response?.status === 401) {
67+
setMessage('Session expired. Please log in again.'); // Set a message for unauthorized access
68+
redirectToLogin(); // Redirect to the login page
69+
} else {
70+
setMessage('Failed to retrieve articles. Please try again.'); // General error message
71+
}
72+
})
73+
.finally(() => {
74+
setSpinnerOn(false); // Hide the spinner after the request is complete
75+
});
76+
};
77+
5178

5279
const postArticle = article => {
53-
// ✨ implement
54-
// The flow is very similar to the `getArticles` function.
55-
// You'll know what to do! Use log statements or breakpoints
56-
// to inspect the response from the server.
57-
}
80+
setMessage(''); // Clear any previous messages
81+
setSpinnerOn(true); // Show the spinner while making the request
82+
83+
const token = localStorage.getItem('token'); // Retrieve the token
84+
85+
axios.post(articlesUrl, article, {
86+
headers: { Authorization: token } // Pass the token in the Authorization header
87+
})
88+
.then(response => {
89+
setArticles([...articles, response.data]); // Add the new article to the state
90+
setMessage('Article successfully added!'); // Set a success message
91+
})
92+
.catch(error => {
93+
setMessage('Failed to add article. Please try again.'); // Set an error message
94+
})
95+
.finally(() => {
96+
setSpinnerOn(false); // Hide the spinner after the request is complete
97+
});
98+
};
99+
58100

59101
const updateArticle = ({ article_id, article }) => {
60-
// ✨ implement
61-
// You got this!
62-
}
102+
setMessage(''); // Clear any previous messages
103+
setSpinnerOn(true); // Show the spinner while making the request
104+
105+
const token = localStorage.getItem('token'); // Retrieve the token
106+
107+
axios.put(`${articlesUrl}/${article_id}`, article, {
108+
headers: { Authorization: token } // Pass the token in the Authorization header
109+
})
110+
.then(response => {
111+
setArticles(articles.map(art =>
112+
art.article_id === article_id ? response.data : art // Replace updated article
113+
));
114+
setMessage('Article successfully updated!'); // Set a success message
115+
})
116+
.catch(error => {
117+
setMessage('Failed to update article. Please try again.'); // Set an error message
118+
})
119+
.finally(() => {
120+
setSpinnerOn(false); // Hide the spinner after the request is complete
121+
});
122+
};
123+
63124

64125
const deleteArticle = article_id => {
65-
// ✨ implement
66-
}
126+
setMessage(''); // Clear any previous messages
127+
setSpinnerOn(true); // Show the spinner while making the request
128+
129+
const token = localStorage.getItem('token'); // Retrieve the token
130+
131+
axios.delete(`${articlesUrl}/${article_id}`, {
132+
headers: { Authorization: token } // Pass the token in the Authorization header
133+
})
134+
.then(() => {
135+
setArticles(articles.filter(art => art.article_id !== article_id)); // Remove the deleted article
136+
setMessage('Article successfully deleted!'); // Set a success message
137+
})
138+
.catch(error => {
139+
setMessage('Failed to delete article. Please try again.'); // Set an error message
140+
})
141+
.finally(() => {
142+
setSpinnerOn(false); // Hide the spinner after the request is complete
143+
});
144+
};
145+
67146

68147
return (
69-
// ✨ fix the JSX: `Spinner`, `Message`, `LoginForm`, `ArticleForm` and `Articles` expect props ❗
70148
<>
71-
<Spinner />
72-
<Message />
149+
<Spinner on={spinnerOn} />
150+
<Message message={message} />
73151
<button id="logout" onClick={logout}>Logout from app</button>
74-
<div id="wrapper" style={{ opacity: spinnerOn ? "0.25" : "1" }}> {/* <-- do not change this line */}
152+
<div id="wrapper" style={{ opacity: spinnerOn ? "0.25" : "1" }}>
75153
<h1>Advanced Web Applications</h1>
76154
<nav>
77155
<NavLink id="loginScreen" to="/">Login</NavLink>
78156
<NavLink id="articlesScreen" to="/articles">Articles</NavLink>
79157
</nav>
80158
<Routes>
81-
<Route path="/" element={<LoginForm />} />
159+
<Route path="/" element={<LoginForm login={login} />} />
82160
<Route path="articles" element={
83161
<>
84-
<ArticleForm />
85-
<Articles />
162+
<ArticleForm
163+
currentArticleId={currentArticleId}
164+
postArticle={postArticle}
165+
updateArticle={updateArticle}
166+
setCurrentArticleId={setCurrentArticleId}
167+
/>
168+
<Articles
169+
articles={articles}
170+
deleteArticle={deleteArticle}
171+
setCurrentArticleId={setCurrentArticleId}
172+
/>
86173
</>
87174
} />
88175
</Routes>
89176
<footer>Bloom Institute of Technology 2024</footer>
90177
</div>
91178
</>
92-
)
179+
);
93180
}

frontend/components/ArticleForm.js

Lines changed: 36 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,20 @@ const initialFormValues = { title: '', text: '', topic: '' }
55

66
export default function ArticleForm(props) {
77
const [values, setValues] = useState(initialFormValues)
8+
const { postArticle, updateArticle, setCurrentArticleId, currentArticle } = props;
89
// ✨ where are my props? Destructure them here
910

1011
useEffect(() => {
11-
// ✨ implement
12-
// Every time the `currentArticle` prop changes, we should check it for truthiness:
13-
// if it's truthy, we should set its title, text and topic into the corresponding
14-
// values of the form. If it's not, we should reset the form back to initial values.
15-
})
12+
if (currentArticle) {
13+
setValues({
14+
title: currentArticle.title,
15+
text: currentArticle.text,
16+
topic: currentArticle.topic,
17+
});
18+
} else {
19+
setValues(initialFormValues);
20+
}
21+
}, [currentArticle]);
1622

1723
const onChange = evt => {
1824
const { id, value } = evt.target
@@ -21,21 +27,37 @@ export default function ArticleForm(props) {
2127

2228
const onSubmit = evt => {
2329
evt.preventDefault()
24-
// ✨ implement
25-
// We must submit a new post or update an existing one,
26-
// depending on the truthyness of the `currentArticle` prop.
30+
if (currentArticle) {
31+
updateArticle({
32+
article_id: currentArticle.article_id, article: values });
33+
} else {
34+
postArticle(values);
35+
}
36+
setValues(initialFormValues);
37+
setCurrentArticleId(null);
38+
};
2739
}
2840

2941
const isDisabled = () => {
30-
// ✨ implement
31-
// Make sure the inputs have some values
32-
}
42+
// Assuming `values` contains the form data as an object, e.g.,
43+
// { title: "some text", body: "some content" }
44+
45+
if (!values.title || !values.body) {
46+
// If either title or body is empty, disable the form
47+
return true;
48+
}
49+
50+
// All inputs have values; enable the form
51+
return false;
52+
};
53+
3354

3455
return (
3556
// ✨ fix the JSX: make the heading display either "Edit" or "Create"
3657
// and replace Function.prototype with the correct function
3758
<form id="form" onSubmit={onSubmit}>
38-
<h2>Create Article</h2>
59+
<h2>{currentArticle ? "Edit Article" : "Create Article"}</h2>
60+
3961
<input
4062
maxLength={50}
4163
onChange={onChange}
@@ -58,11 +80,11 @@ export default function ArticleForm(props) {
5880
</select>
5981
<div className="button-group">
6082
<button disabled={isDisabled()} id="submitArticle">Submit</button>
61-
<button onClick={Function.prototype}>Cancel edit</button>
83+
<button onClick={ () => setCurrentArticleID(null)}>Cancel edit</button>
6284
</div>
6385
</form>
6486
)
65-
}
87+
6688

6789
// 🔥 No touchy: ArticleForm expects the following props exactly:
6890
ArticleForm.propTypes = {

0 commit comments

Comments
 (0)