New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Added Reputation, Number of Posts, and User Icons #12
Conversation
forumPR12.mp4 |
client/src/Components/App/App.js
Outdated
@@ -10,6 +10,8 @@ import PostMenu from '../PostMenu/PostMenu.js'; | |||
import { POSTS_PER_PAGE } from '../../constants.js'; | |||
import './styles.css'; | |||
import useReactionsFetch from '../../Hooks/useReactionsFetch.js'; | |||
import UserDashboard from '../UserDashboard/UserDashboard.js'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To avoid such type of import naming as /UserDashboard/UserDashboard.js
, you can put index.js
in the folder from where you do an export (like proxy export). And in this case, you may leave here only import UserDashboard from '../UserDashboard';
@@ -33,6 +33,10 @@ const PostPage = ({ match }) => { | |||
( | |||
<div className='post card mb-2' style={{ 'margin': '1em' }}> | |||
<div className='card-body'> | |||
<div className={styles.imgContainer} | |||
style={{ 'background-image': `url("/api/v1/users/icon?username=${data.author}")` }} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What about changing API to /api/v1/users/:id/icon
. It's like we're based on users -> getting the specific user -> the icon is related to this user.
I guess it would be more in line with REST naming than using params. What do you think?
Or you just don't have the id and only the name?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here I only have the information about the post which only includes the author's name.
async function handleChange (e) { | ||
const formData = new FormData(); | ||
formData.append('image', e.target.files[0]); | ||
await api.sendChangeIconRequest(formData); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What if this request fails?
|
||
function handleClick () { | ||
// trigger the click event of the hidden "choose image" input | ||
hiddenInput.current.click(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is the smart approach. But I'd suggest checking if the current
exists.
hiddenInput.current.click(); | ||
} | ||
return ( | ||
<> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
<> |
.editIcon{ | ||
position: absolute; | ||
bottom:90%; | ||
left:0; | ||
color: #ffffff; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please, pay attention to these details. I'd suggest using the pre-commit hook with eslint and typescript checks.
.editIcon{ | |
position: absolute; | |
bottom:90%; | |
left:0; | |
color: #ffffff; | |
} | |
.editIcon { | |
position: absolute; | |
bottom: 90%; | |
left: 0; | |
color: #ffffff; | |
} |
server/routes/users/index.ts
Outdated
// get user data such as reputation and number of posts | ||
app.get('/api/v1/users', validateRefreshJWT, wrap(userController.getUserData)); | ||
|
||
// change the user image | ||
app.post('/api/v1/users/change-icon', validateRefreshJWT, upload.single('image'), wrap(userController.changeUserIcon)); | ||
|
||
// get the icon of a user | ||
app.get('/api/v1/users/icon', wrap(userController.getUserIcon)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here you have several APIs and all of them are around a single user. And it is not clear from the route.
When you work with a single object you may specify the specific object in a route as users/:id/...
.
Because how would you create an endpoint to get the list of all users right now?
And one more topic related is where do you use cookies. You may use it for validation - if this user has the right to perform a particular action. But you can't make an assumption that the user wants to perform an action under his own instance.
It could be a situation where you may have the catalog of all users of the public user page, where I can go to the link and get your icon, post number, and reputation.
server/routes/users/index.ts
Outdated
fileFilter: function(req, file, cb) { | ||
validateFile(file, cb); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can adjust the function to use it directly and specify its type according to multer as well.
fileFilter: function(req, file, cb) { | |
validateFile(file, cb); | |
} | |
fileFilter: validateFile; |
server/controllers/userController.ts
Outdated
const refreshToken = req.cookies.refreshToken; | ||
const username = await userManager.findRefreshToken(refreshToken); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would highly recommend looking closer at these two lines which you use everywhere. Maybe, you can figure out how to make it nicer.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good job and just a few comments to change.
And let's add tests for some of the code. For instance, for middlewares.
import NavbarComponent from '../Navbar/Navbar.js'; | ||
import NavbarComponent from '../Navbar'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just want to comment additionally that you actually don't need to rename Navbar.js
to index.js
. But you should create index.js
with something like:
export { default } from './Navbar';
if (res.status === 200) { | ||
window.location.reload(); | ||
} else if (res.status === 401) { | ||
setPopup({ 'message': 'Something went wrong. Please re-login again.' }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
setPopup({ 'message': 'Something went wrong. Please re-login again.' }); | |
setPopup({ message: 'Something went wrong. Please re-login again.' }); |
async function handleChange (e) { | ||
const formData = new FormData(); | ||
formData.append('image', e.target.files[0]); | ||
const res = await api.sendChangeIconRequest(formData); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please, take into account that axios.post
may throw an error and UI won't show an error properly.
Look for such cases, since it's really easy to catch them in the front-end and they may not be critical. But if you leave a possible error unhandled it could lead to an issue that may influence a lot and could be hard to find and debug.
And that's why TDD is a good practise which helps you to think about edge cases first.
<p className={styles.editIcon} onClick={handleClick}>Edit Image</p> | ||
<input ref={hiddenInput} type='file' id='fileField' name='file' accept='image/*' hidden='true' onChange={handleChange}/> | ||
<div className={`mx-auto ${styles.imgContainer}`} | ||
style={{ 'background-image': `url("/api/v1/users/${Cookies.get('username')}/icon")` }} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What if the cookie wasn't found? How the link would look like?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe that this component will only be loaded if the cookie exists.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@OlexG Actually, Cookies.get('username')
will return undefined. And the link would look like
/api/v1/users/undefined/icon`. But I suppose here should be a default icon in this case.
api.sendReactionsRequest(username).then((res) => { | ||
setReactions(res.data); | ||
setLoading(false); | ||
}).catch((error) => { | ||
setError(error); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
api.sendReactionsRequest(username).then((res) => { | |
setReactions(res.data); | |
setLoading(false); | |
}).catch((error) => { | |
setError(error); | |
}); | |
api.sendReactionsRequest(username).then((res) => { | |
setReactions(res.data); | |
}).catch((error) => { | |
setError(error); | |
}).finally(() => { | |
setLoading(false); | |
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I decided to switch to an await/async with try-catch model here
server/db/dbManager.ts
Outdated
@@ -17,6 +19,11 @@ export interface FilterObject { | |||
search: string; | |||
} | |||
|
|||
export interface PublicUserData{ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This type of mistake means that eslint doesn't cover this file.
export interface PublicUserData{ | |
export interface PublicUserData { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, it looks like the "space-before-blocks" rule doesn't apply to interfaces because, for other blocks in the file, eslint throws an error if there is no space. I am not sure why this is the case I will try to find a fix.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I assume the tslint can fix it, not eslint. Am I right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
looks like the eslint-typescript plugin does not have a fix for this typescript-eslint/typescript-eslint#1606. I will use prettier in addition to eslint.
package.json
Outdated
"lint-staged": { | ||
"**/*.{js, css}": [ | ||
"npx stylelint \"**/*.css\"", | ||
"eslint **/*.js --fix-dry-run" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This doesn't cover server's typescript files.
server/db/dbManager.ts
Outdated
@@ -173,13 +180,14 @@ export class PostManager { | |||
} | |||
|
|||
async upvotePost(postID: string, username: string, userManager: UserManager): Promise<boolean> { | |||
if (await userManager.removePostDownvote(postID, username)) { | |||
const { author } = await this.model.findOne({ _id: new ObjectId(postID) }).exec(); | |||
if (await userManager.removePostDownvote(postID, username, author)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually when you add the verb
method in the if
clause (like, if remove then update and if add then update
), it makes the code less readable. And I can't understand what is the logic behind these steps.
So you have several options (in the order of preference):
- change the result type from
boolean
to{ hasBeenChanged/madeChange/removed/executed/...: boolean }
and when you use this variable in the if clause it makes sense that you check whether the action has been actually performed - rename the
verb
method (I don't have suggestions unfortunately) - make
removePostDownvote
and get the result to a variable and use it inif
clause - add comments
I would prefer the first option.
server/db/dbManager.ts
Outdated
}); | ||
return 'success'; | ||
} | ||
|
||
async changeIconPath(username: string, path: string): Promise<string> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perhaps, update
suits better?
async changeIconPath(username: string, path: string): Promise<string> { | |
async updateIconPath(username: string, path: string): Promise<string> { |
server/routes/users/index.ts
Outdated
app.get('/api/v1/users/:username', validateUsernameParam, wrap(userController.getUserData)); | ||
|
||
// change the user image | ||
app.post('/api/v1/users/change-icon', validateRefreshJWT, validateUsernameCookie, upload.single('image'), wrap(userController.changeUserIcon)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is the beauty of REST. You can and should use the same route for GET and POST of the same resource.
And this will help you to be consistent with validation as well.
app.post('/api/v1/users/change-icon', validateRefreshJWT, validateUsernameCookie, upload.single('image'), wrap(userController.changeUserIcon)); | |
app.post('/api/v1/users/:username/icon', validateRefreshJWT, validateUsernameCookie, upload.single('image'), wrap(userController.changeUserIcon)); |
"plugin:@typescript-eslint/eslint-recommended", | ||
"plugin:@typescript-eslint/recommended" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Something to look at: the most popular Eslint configuration is from Airbnb.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see, I decided to go with the eslint standard configuration and I think the rules I have now are good but I will look at the airbnb configuration as well.
try { | ||
const res = await api.sendReactionsRequest(username); | ||
if (res.status === 200) { | ||
setReactions(res.data); | ||
} | ||
setLoading(false); | ||
} catch (e) { | ||
setPopup('Something went wrong when fetching reactions'); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The similar question: what if an error happens, what would be with loading state?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if there is an error I don't want to set the state to false as then the UI will try to display the reactions when there is none
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
but I see the issue, loading, in this case, should be false since the request is done
client/src/Hooks/useCommentsFetch.js
Outdated
} | ||
} | ||
fetchData(); | ||
// eslint-disable-next-line react-hooks/exhaustive-deps |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What dependencies have been omitted and why?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the setPopup function as this function will not change
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since this is the props, so you should add it in the dependency anyway. And if you're sure, that it won't be changed, it won't influence the behaviour. But if it did change, it would save you from bigger bugs.
And I would check inside the useEffect if comments exist already. It depends on what you're going to do if setPopup is undefined.
By the way, this issue won't be so important at all if you have Typescript. Because right now you might really reuse this function with the changing setPopup (initially undefined and them populated). And no one will catch this. You must write the code imaging that some other person gonna make changes without deeply reading the logic.
Right it is not so critical. But I really ask you to verify all your code in the future using this rule: QA will gonna break your code eventually. Don't let this happen, this is your reputation and this is how you will grow as a specialist.
And your future colleagues will reuse your code and make mistakes and spend hours to debug.
This is the example of bug I fixed yesterday - someone forgot to add error (one of the output that you should save, not only data from API) to the dependencies and fetching never stopped if there was an error. And I relied on this code supposing it is safe to use, but it is hard to debug the edge cases.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How nice to see the code after prettier! 😍
No description provided.