Skip to content
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

Merged
merged 14 commits into from Jun 24, 2021
Merged

Added Reputation, Number of Posts, and User Icons #12

merged 14 commits into from Jun 24, 2021

Conversation

OlexG
Copy link
Owner

@OlexG OlexG commented Jun 14, 2021

No description provided.

@OlexG
Copy link
Owner Author

OlexG commented Jun 14, 2021

forumPR12.mp4

@@ -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';

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}")` }}

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?

Copy link
Owner Author

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);

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();

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 (
<>

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
<>

Comment on lines 10 to 15
.editIcon{
position: absolute;
bottom:90%;
left:0;
color: #ffffff;
}

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.

Suggested change
.editIcon{
position: absolute;
bottom:90%;
left:0;
color: #ffffff;
}
.editIcon {
position: absolute;
bottom: 90%;
left: 0;
color: #ffffff;
}

Comment on lines 52 to 59
// 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));

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.

Comment on lines 31 to 33
fileFilter: function(req, file, cb) {
validateFile(file, cb);
}

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.

Suggested change
fileFilter: function(req, file, cb) {
validateFile(file, cb);
}
fileFilter: validateFile;

Comment on lines 63 to 64
const refreshToken = req.cookies.refreshToken;
const username = await userManager.findRefreshToken(refreshToken);

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.

Copy link

@irenlian irenlian left a 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';

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.' });

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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);

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")` }}

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?

Copy link
Owner Author

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.

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.

Comment on lines 8 to 13
api.sendReactionsRequest(username).then((res) => {
setReactions(res.data);
setLoading(false);
}).catch((error) => {
setError(error);
});

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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);
});

Copy link
Owner Author

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

@@ -17,6 +19,11 @@ export interface FilterObject {
search: string;
}

export interface PublicUserData{

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.

Suggested change
export interface PublicUserData{
export interface PublicUserData {

Copy link
Owner Author

@OlexG OlexG Jun 22, 2021

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.

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?

Copy link
Owner Author

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"

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.

@@ -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)) {
Copy link

@irenlian irenlian Jun 22, 2021

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 in if clause
  • add comments

I would prefer the first option.

});
return 'success';
}

async changeIconPath(username: string, path: string): Promise<string> {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps, update suits better?

Suggested change
async changeIconPath(username: string, path: string): Promise<string> {
async updateIconPath(username: string, path: string): Promise<string> {

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));

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.

Suggested change
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));

Comment on lines +9 to +10
"plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended"

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.

Copy link
Owner Author

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.

Comment on lines 12 to 20
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');
}

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?

Copy link
Owner Author

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

Copy link
Owner Author

@OlexG OlexG Jun 23, 2021

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

}
}
fetchData();
// eslint-disable-next-line react-hooks/exhaustive-deps

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?

Copy link
Owner Author

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

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.

Copy link

@irenlian irenlian left a 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! 😍

@OlexG OlexG merged commit 3577ae7 into main Jun 24, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants