Skip to content

Instantly share code, notes, and snippets.

@xiaoyunyang
Created July 18, 2023 19:18
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save xiaoyunyang/2a2d4c17fbff247b0e9cd1c6ff62f69c to your computer and use it in GitHub Desktop.
Save xiaoyunyang/2a2d4c17fbff247b0e9cd1c6ff62f69c to your computer and use it in GitHub Desktop.
User Story for Comments Feed

Comments App

Overview and Goals

Background

We want to create a frontend app to display, send, and delete comments. A comment has this shape:

interface Comment {
  id: number
  name: string
  created: datetime
  message: string
}

User Story

As a user, I can

  1. Enter my name in a text input and my message in a textarea.
  2. Click the "Comment" button.
  3. See my comment appear at the top of the list of comments and my name and comment box inputs clear.
  4. Scroll through a list of comments posted by me or other users. Each comment has a date displayed indicating when it was posted.
  5. Try posting a comment while offline.
  6. See an error message display under the comment button and the name and comment I typed persist in the UI.

Goals

A breakdown of the sub-problems we need to solve are:

  • Add a comment
  • Retrieve all comments
  • Display a comment

The implementation strategy will be discussed in the Implementation Overview section.

Non-Goals

Pagination

In a real comments app, the total number of comments can be huge and we don't want to retrieve all of them in a single call because that will result in network timeout.

We can implement pagination to retrieve comments in batches. However, this is not a requested feature from the PRD and looking at the server and api code, pagination is not implemented server-side.

Frontend trigger for deleting all the comments

We are not building a button that deletes all the comments. This looks like a user-facing app, not an internal tooling. We don't want any user to delete comments for other users. If we want to call /deleteComments to purge data, we can do that in Postman.

Single message deletion

This is an common feature for a comments app. As a user, I want to be able to click a "Delete" button next to a comment I previously posted to delete that comment.

However, this is not a requested feature from feature is not requested in the PRD and . It's not clear whether this is a feature that is not needed or it's a feature that is not needed for the MVP.

Deletion Confirmation Modal

Users can easily click the delete button by accident. We should add a confirmation modal to prevent accidental deletion because it would be a bad user experience that that the UI does not guard against irreversible actions due to user error.

However, for the sake of speed in completing this takehome project, I will not implement the confirmation modal, but I will talk about it here how I would implement it:

  1. Create a ConfirmationModal component that takes a onConfirm callback prop and a onClose callback prop. It also has a isVisible prop that controls whether the modal is visible or not.
  2. The parent component keeps the isModalVisible in React state. When the delete button is clicked, it changes that state to true. It passes a onClose props to ConfirmationModal that changes the isModalVisible state to false.
  3. In ConfirmationModal, there's a close button which triggers onClose. The onClose is also called when the user clicks confirm to delete the comment and the request was successfully processed.
  4. When the user clicks confirm to delete the comment, onConfirm is called.
  5. In the parent component, onConfirm is used to update the component state where all the comments are kept.
  6. ConfirmationModal is responsible for making the deleteComment API call and handling errors from that API call.

API

Before diving into development, we need to establish the client-server contract.

The API exposes a few services to the client but it's not clear from the PRD what the shapes of those responses are, but we can figure this out by looking at the server and api code and testing the API endpoints using Postman.

Here is a few ways we can generate the success and error responses:

  • Use Postman to test the API endpoints by calling it with valid args to create the success response and invalid args to create error response.
  • Changing the server or api code to always reject with error.

Once we have the responses for all the endpoints, we can create mock data for frontend development. API contract/mock data is a great way to enables parallelization in frontend and backend development.

De-scoped

re-render comment posted date after posting

When you first post a comment, it gets inserted to the top of the comments list with the label {Your Name} Just Now!. But if you let this view sit for a while without refreshing the page, this Just Now label should update to Today at 12:45 PM. This requires some sort of timer to update the label every minute or so based on the new value for time now. We can do this with setInterval or setTimeout and pass now as an optional parameter into getConditionalDateString.

Making a CommentBox shared component

I thought about making a CommentBox component that can be used to display a comment or to add a comment. It can be useful if in the future, we want to enable editing of an older comment in the list. However, I decided against it because it's pre-mature optimization. If there's a need to edit an older comment in the future, we can add that feature later on and do some refactoring.

Accessibility and Internationalization

Important for real projects, especially if we are building a consumer application. There's a blueprint for how to successfully implement a11y and i18n in React apps and I've had a lot of experience at OkCupid and Smartling working on both.

For the sake of velocity in completing this takehome project building a MVP comments app, I will not implement fancy a11y and i18n capabilities but I will create the MVP with a11y and i18n best practices so that it can easily extended to add fancy a11y and i18n capabilities later on.

There are simple things we can do now in an MVP to avoid having to refactor later on when we are more focused on a11y and i18n:

  • a11y: Use semantic HTML elements. For example, use a for links and button for buttons. Don't try to build your own links and buttons using div with onClick handlers.
  • i18n: Since we are using toLocaleDateString to format the date string, we can accept a locale parameter and set it to English by default for now. We shouldn't spend time now testing all the other locales. Another easy thing we can do is to build reusable components that can be easily evolved for i18n (for instance, don't pass string as props. Pass children). Reusable components that are not originally built to accept arbitrary React nodes would require a huge refactor to the frontend codebase when we want to add i18n later on.

a11y is a big topic with a lot of best practices you have to be aware of. For instance it requires a lot of extra props like aria-label to be added to the jsx. There are tools like the a11y eslint plugin to guide you to add the right props. There are so many other tools and libraries like focus-trap-react and react-aria that provides accessible UI primitives for your design system.

i18n is also a big topic. Compared to a11y, there is not as many explicit guidelines for i18n standardization. There are dos and don'ts for i18n but they exist primarily in tribal knowledge form. When I worked on the internalization team at OkCupid, I created an eslint plugin to enforce i18n best practices (eslint-plugin-i18n-lingui) which we open sourced.

Offline support

We are not going to support offline support for this project but if offline support were an objective, I could use this library messages-cache which I built when I was at OkCupid to supports offline-first experience for sending a message in chat.

This library is basically a class that manages a cache. It exposes various methods for updating and retrieving things from the map. The cache is a data structure consisting of a double-ended queue in a Map and supports constant time lookup by message id and constant time insertion and deletion anywhere in the queue.

For OkCupid, I integrated this data structure by putting it in a React memo and synced the cache data with messages React state that drives the re-rendering. The messages-cache exposes a method called syncStateWithCache that the component using the lib can call to sync the cache with the React state.

We don't want to put the cache in the React state directly because it would cause excessive re-rendering when the dequeue is updated. The cache manager can be called to add a bunch of new messages to the end of the queue (on initial load) or beginning of the queue (load older messages). Because the dequeue recursively add nodes, when many nodes are added all at once, this would cause excessive re-rendering and crash the chat app if the cache data structure were stored in a React state.

Typescript and ESLint

In a real project, having TS and better linting will improve maintainability and ease of feature development as the codebase grows. But for this project, since the boilerplate starter is not already set up with TS and linting, it would require extra configuration and setup that is not worth the time for this takehome project.

Implementation Overview

Add a comment

We will build a custom hook usePostComment to make the API call to post a comments. The custom hooks will do all the hard work of creating the request, mapping errors and provide a nice interface to the React component:

The custom hook will be used in the NewComment component, which renders the input, textarea, and comment button.

Retrieve all comments

We will build another custom hook useAllComments to make the API call to retrieve all comments. This hook will used by the App component which renders the list of Comment, the NewComment, and manages a React state comments.

Display a comment

The Comment component will display a comment and be passed the name, created, and message props.

Sorting comments

Comments need to be displayed in descending order based on the date created.There’s a feature I need to implement in which things need to be sorted.

I can do it in the frontend code but it’s better to do it in the backend. This is a frontend takehome but there’s some backend code is provided. Can I update the backend code for this assignment?

Formatting datetime

Let's write a function getConditionalDateString that takes the datetime from the server and the current time and calculates a display string indicates when the comment that can be sent. The display shows "Just now!", "Today at 1:25 PM", "Yesterday at 9:52 PM", "Thursday at 3:16 PM", or "Jan 12 at 3:15 PM". This function can be written to support internationalization right off the bat, although testing it for all these different locales would be de-scoped.

We want to write a function that takes the datetime and outputs the display string.

This function is tricky because datetime from the server is UTC and the current time, calculated from new Date() is in the clients's local timezone.

We have to convert the datetime from the server to the client's local timezone before we do all the time math to calculate the display string.

Invalid Input XSS and SQL Injection

For this project, we are implementing any sanitization of the input to prevent XSS or SQL injection, because:

  • On inspection of the server code to add a new comment, it doesn't look vulnerable to sql injection

    createComment({ name, message }) {
      return this.dataAccessObject.run(
        'INSERT INTO comments (name, message) VALUES (?, ?)',
        [name, message]
      );
    }
  • The input is not rendered as html so it's not vulnerable to XSS. According to the best practice for XSS prevention, we should always try to render data through JSX and let React handle the security concerns for you.

I also double checked by entering some some sql injection strings to delete the comment table and html script to display alert into the message textarea and verified that the table is not deleted and the app is not showing an alert.

For input validation, we don't want the comment to be posted unless both name and message inputs are non-empty and contain at least one non-whitespace character. We can do this by disabling the comment button if the name or message is empty or only contains whitespace.

We also don't trim the spaces before and after the name and message before posting, which is expected for most user-generated contents on social media sites.

Testing Strategy

Unit Tests

Date formatting

A tricky part about testing this function is that it's not a pure function as it calculates its own current time. For unit tests, we have to make sure the test passes consistently every time we run it.

We can do this by mocking the new Date() function to return a fixed date if it's called with no argument.

UI Stress test

We can perform UI stress testing for each of our React components by checking that everything is displayed nicely when the strings are very long. We can also check that the UI is responsive when the window is resized. Verified that:

  • textarea is scrollable
  • comment display area scales with content

You can do these tests without making any database queries or code changes by modifying the DOM directly in Chrome Dev tools.

Responsiveness We can test responsive design by using the Chrome Dev tools to simulate different screen sizes and verify that it looks good on all screen sizes.

Unusual strings

We should also test some unusual strings like empty string, string with only spaces, string with only special characters, string with only emojis, etc.

  1. For our NewComment component, make sure we disable sending if the name is empty or just empty strings
  2. For our Comment component, make sure that it renders strings with special characters correctly. For example, try this string: What's the meaning of "meaning"?

Integration Testing

Error states

We can simulate network error in Chrome Dev tools, in the network tab, set network throttling to offline. Then, test sending a comment when the network is offline. We should see the error message.

We can simulate server error by modifying the server code or the API code to always return the error.

To simulate server outage, we can kill the server locally and test sending a comment. We should see the error message.

Acceptance Testing

Perform all the actions in the user story.

Demo video uploaded to YouTube because the file is too big to upload.

TODOs

  • Mini Tech Design
  • Create mock data for frontend development
  • Build out the frontend components with support for variable size screen
  • Format date based on current time with unit test
  • Integrate frontend with the GET endpoints
  • Integrate frontend with the POST endpoints (Spike: timezone difference)
  • Handle get and post error in the frontend
  • Acceptance testing and creating the demo video
  • Implement form input validation
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment