[Project Update] Smults a.k.a Steem MULti Tags Search - New Features

in #utopian-io6 years ago

What's up fellow Steemians!! This is my update for Smults, after nearly one month of launching it. Smults (or Steem MULti Tags Search) is a web application that allows users to search multiple tags (up to 5 tags) at a time. User can also mark the 'First tag as category' checkbox, such that all returned results will be in the same category as the first tag. And thanks to yours truly, now users can even search posts filtered by author!

In this update, I have added ES-linting, unit test cases and the cool feature to filter by author, search for author and subsequently return posts by said author. User can also further filter the author's posts by using the multi tags search.

URL link

https://smults-search.appspot.com

Repository

https://github.com/Alvin-Voo/smults

Pull requests

https://github.com/Alvin-Voo/smults/pull/10
https://github.com/Alvin-Voo/smults/pull/8
https://github.com/Alvin-Voo/smults/pull/7
https://github.com/Alvin-Voo/smults/pull/5
https://github.com/Alvin-Voo/smults/pull/3

New Features

Filter by authors

PR #7 and PR #10 implemented the filter by authors feature, as seen below:
sc-filter-by-author-1

By choosing the filter 'Author', a dropdown input box will pop up to the right, where user can search for author's name instantaneously, much like the search box of busy.org. While user searches, suggestions of user names will appear in the dropdown.

sc-filter-by-author-2

The search will return all posts by this said author if multiple tags search field is empty. User can also further filter the posts list using the existing multi tags search input box.

The were 2 challenges in implementing this feature:

  1. Getting the author names search suggestion
  2. Re-implement a new fetch posts action with author's name

For challenge No. 1, the dispatch action is quite straight-forward (at this commit):

export function lookupAuthors(search) {
  const request = client.call('condenser_api', 'get_account_reputations', [search, MAX_AUTHORS]);
 //MAX_AUTHORS here is 20
  return {
    type: LOOKUP_AUTHORS,
    payload: request, // this goes to redux promise
  };
}

whereby this will return an array of 20 authors, with their reputation scores. All I have to do after that was to filter away (at this commit) those authors with zero reputation scores and I get the valid authors.

At the Search page (pages/search.js), I wanted the input of user to be throttled or debounced (at this commit):

  asyncLookupAuthors = async (query) => {
    const { lookupAuthors } = this.props;
    await lookupAuthors(query);
    const { authorsOptions } = this.props;
    this.setState({ authorsOptions });
  }

  // since debounce and throttle functions *returns* the executable function
  // they are declared here
  lookupAuthorsDebouncer = debounce(this.asyncLookupAuthors, 300);

  lookupAuthorsThrottler = throttle(this.asyncLookupAuthors, 300);

  authorSearchChange = async (e, d) => {
    const query = d.searchQuery;
    if (query.length < 5) this.lookupAuthorsThrottler(query);
    else this.lookupAuthorsDebouncer(query);
  }

I put a delay of 300ms for both debounce and throttle functions of lodash, and to check that the async lookupAuthors function will be called consistently (i.e. throttled) if search string is short while it will only be called once at the end (i.e. debounced) if search string is longer (than 5).

For the second challenge, since I already have the posts reducer with the magic function to filter out posts with specific tags. I just need an API which can return the exact same raw posts data, which I can then supply to my posts reducer. Luckily, steemit's appbase API got me covered (at this commit):

async function fetchFilterByAuthor(author) {
  const blogs = await client.call('condenser_api', 'get_discussions_by_blog', [{ tag: author, limit: MAX_POSTS }]);

  const posts = blogs.map((blog) => {
    const retBlog = { ...blog };
    if (retBlog.author !== author) retBlog.resteemed = true;
    return retBlog;
  });

  return {
    type: FETCH_AND_FILTER_POSTS,
    payload: posts,
  };
}

Here, the 'get_discussion_by_blog' API call returns the exact raw posts results as per 'get_discussions_by_filtername'. Once the posts data return, I need to check whether they are created by this said author, if they are not, means they were re-steemed (or re-blogged). After that, the data goes into the posts reducer. The posts reducer will then be able to further filter down the posts based on any tag(s) entered by the user.

Other minor features (improvements)

Searching with no tag by default will return full search results

Prior to this, the application would not search if there were no tag supplied by user, but I needed a way to return all results for an author, so the only logical solution is to remove this constraint. Now, by selecting a filter and then clicking 'Search' will return all posts for that filter (just like how steemit does it). Searching by author with no search tags will return all his/her posts.

Customized next.js 404 page with back to home page's link

not_found.png

Able to show fetch posts error (if any)

page-error

I have written a single Notify component to display any error or custom messages (at this and this commit).

Able to show a customized message

no-post

The 'No post found' message is a bit more trickier to get it to display at the right time. I can only display it when 1) The posts reducer has finished processing and 2) The posts list from redux store is empty. I cannot display it straight away after dispatching the fetch posts action, because at that time, the posts reducer might still be running, hence whatever state I get from redux store would be wrong, so I made use of React's lifecycle componentDidUpdate function (at this commit) .

  componentDidUpdate = (prevProps) => {
    const { postsState, postsLength } = this.props;
    if (postsState !== prevProps.postsState) {
      if (postsState === FETCHED_POSTS && postsLength === 0) {
        this.setState({ notify: { type: MESSAGE, message: 'Opps. No post found!' } });
      }
    }
  }

I added 2 states constants for the posts reducer, one is STORED_TAGS and another FETCHED_POSTS. The store's state constants (or flags) will change from STORED_TAGS to FETCHED_POSTS only when the reducer returns (finished processing). So, only by then the application will check the posts list length and tell the search page to display the message if it's indeed zero.

ESLint

I've only decided to do the proper way and started applying linting in this update, so I did quite a bit of code refactoring in PR #3. I've applied the airbnb eslint preset. Final lint test:

es-lint

For those who are starting with any modern day JS frameworks (React, Angular, Vue or even Riot), I strongly recommend to use ES-linting from the get-go, even if it's just for prototyping. It really did help a lot in my case. Of course, there were a lot of figuring out what all the lint rules are about and configurations (at this commit) done at first, but afterwards it made my life easier, for example:

  1. By using ESLint plugin for editor (in my case VSCode), I was able to pretty format my code with shortcut key and maintain a decent coding style/practice, thus cutting out redundant code.
  2. ESLint enforces PropTypes for React (eslint-plugin-react), which I found tedious at first but afterwards I get it.
  3. ESLint plugin also warns of potential error all the time. Since Javascript is a type-less scripting language which can be molded into different frameworks, this definitely helps to pick out those easily overlooked mistakes/errors.
  4. With respect to above, ESLint helped me discovered a boo-boo that I made which I kinda knew about but forgot (at this commit):
  dropDownAddItem = (e, d) => {
    const value = d.value.trim();
    this.setState((prevState) => {
      const newTags = prevState.tagsOptions;
      newTags.unshift({ value, text: value });
      return { tagsOptions: newTags };
    });
  }

Which is to prevent the use of this.state value in this.setState() in the function above. Value should be taken from previous state instead because this.state might not be referencing to the current state (possibility of race condition due to batch calls of this.setState())

Unit test cases

PR #3 and PR #8 implemented all the unit test cases as shown:

t1
t2
t3

Testing is pretty mundane stuff, but one cool thing I want to mention about Jest is it's mocking ability. It's truly unprecedented.
For example, I'm stumped when I wanted to test my ScrollButton component, the button which lets user scroll all the way up to the top in an evenly speed:
scroll

Luckily, Jest has timer mocks which allows me to test my animation by manipulating time (at this commit look for ScrollButton.test.js). Great Scott!

describe('Scroll Up click function tests', () => {
  jest.useFakeTimers();

  test('should trigger scrollStep function correctly', () => {
    // the test case before this has called click once, so clear before hand
    jest.clearAllTimers();
    jest.clearAllMocks();

    window.pageYOffset = 1000;
    window.scroll = jest.fn((initVal, offsetVal) => {
      window.pageYOffset = offsetVal;
    });
    wrapper.simulate('click');

    const intervalId = wrapper.state('intervalId');
    expect(window.scroll).not.toBeCalled();

    // fast forward one timer call
    jest.runOnlyPendingTimers();
    expect(setInterval).toHaveBeenCalledTimes(1);
    expect(window.scroll).toHaveBeenCalledTimes(1);
    expect(window.scroll).toHaveBeenCalledWith(0, 1000 - 50);
    expect(window.pageYOffset).toBe(1000 - 50);
....

This truly amazed me the first time I saw it, we could also advance time by certain milliseconds like such:

jest.advanceTimersByTime(*some miliseconds*);

Other than this, Jest also have the capabilities to mock modules, libraries, component functions.. practically anything can be counterfeited for testing purpose. =D

Ideas??

Well, I have pretty much implemented everything I wanted for the search features now. One idea I have is to enable the route to take tags as parameters as such:
for e.g. https://smults-search.appspot.com/search?tags=tag1,tag2,tag3&filter=trending
or e.g. https://smults-search.appspot.com/search?tags=tag1,tag2,tag3&filter=author&author=johnny
This is enable user to post url links that refer to Smults, that says "look at this bunch of posts at steemit, this is what I'm talking about"

Another thing I thought might be fun to do is to have some sort of data visualizer for a user's followers' demography, including how many bot followers a user has.. Of course, this will be an entirely different thing and will be in a different tab if implemented.

Alright, let me know if you have any ideas or issues in the comment below, or you can hit me up in discord @alvinvoo.

Github Account

https://github.com/Alvin-Voo

Thanks for reading. And until next time, Full Steem Ahead!~!

Sort:  

Thank you for your contribution. I really liked the way you have explained about your code, how you implemented and what issue you faced. The code itself is of high quality, and adding ESLint is a very nice decision. I also liked the way you have created your PR, short and meaningful with full description.

Since you have asked for ideas, I can say you can try to use small machine learning algo over this, so that you can give a suggestion to user most searched user or most searched tags.

Your contribution has been evaluated according to Utopian policies and guidelines, as well as a predefined set of questions pertaining to the category.

To view those questions and the relevant answers related to your post, click here.


Need help? Write a ticket on https://support.utopian.io/.
Chat with us on Discord.
[utopian-moderator]

Wow, great to see some progresss :) I have your app in my browser bookmarks altho I have to say I havent been using it..will for sure start..

As for your request u just sent me, the issue is still there, at least for my Chrome version on Ubuntu (Version 62.0.3202.94 (Official Build) (64-bit))

Here the screencast as you requested :)

https://imgur.com/a/E1SxApG

(Dunno how to include it so the gif shows here haha :D Gotta go now otherwise I'll be late to the bjj practice :D Best of luck with the project!

Edit: I just figured out the issue....if I select it by enter, the text gets deleted...if I select it by left mouse click, the text stays there....btw nice to see a favicon, I missed it :) Okk Im off now :D

Wow @matkodurko Thanks for the fast reply. Yea, I see it! Glad to see another Ubuntu user.
Yea, looks weird, but it doesn't happen to me. Can you try to upgrade your chrome? The latest for Ubuntu should be version 69. Below is my version.

Version 69.0.3497.81 (Official Build) Built on Ubuntu , running on Ubuntu 18.04 (64-bit)

Oh wow I had no idea I'm soo behind haha :D But actually I don't want to upgrade tho as the newest version disables Flash by default on every restart...at least thats what a colleague of mine has written to our Slack. And I sometimes work from this private PC as well and it might cause some head scratching for me :D srryyy, maybe someone else could help u?

Glad to see this developing. I think anything that helps make content exploration easier is going to help us long run. Great work @alvinvoo!

I've been missing (apparently in a literal sense) a useful tool like this! We need to raise awareness of Smults in the STEEM community!

Congratulations @alvinvoo! You received a personal award!

Happy Birthday! - You are on the Steem blockchain for 1 year!

You can view your badges on your Steem Board and compare to others on the Steem Ranking

You can upvote this notification to help all Steem users. Learn how here!

Hi @alvinvoo!

Your post was upvoted by @steem-ua, new Steem dApp, using UserAuthority for algorithmic post curation!
Your post is eligible for our upvote, thanks to our collaboration with @utopian-io!
Feel free to join our @steem-ua Discord server

Congratulations @alvinvoo! You have completed the following achievement on the Steem blockchain and have been rewarded with new badge(s) :

Award for the number of upvotes

Click on the badge to view your Board of Honor.
If you no longer want to receive notifications, reply to this comment with the word STOP

You can upvote this notification to help all Steemit users. Learn why here!

Congratulations @alvinvoo! You have completed the following achievement on the Steem blockchain and have been rewarded with new badge(s) :

Award for the number of upvotes received

Click on the badge to view your Board of Honor.
If you no longer want to receive notifications, reply to this comment with the word STOP

You can upvote this notification to help all Steemit users. Learn why here!

Congratulations @alvinvoo! You have completed the following achievement on the Steem blockchain and have been rewarded with new badge(s) :

Award for the number of upvotes

Click on the badge to view your Board of Honor.
If you no longer want to receive notifications, reply to this comment with the word STOP

Do not miss the last post from @steemitboard:

SteemitBoard Ranking update - Resteem and Resteemed added

You can upvote this notification to help all Steemit users. Learn why here!

Congratulations @alvinvoo! You have completed the following achievement on the Steem blockchain and have been rewarded with new badge(s) :

You made more than 1500 upvotes. Your next target is to reach 1750 upvotes.

Click here to view your Board of Honor
If you no longer want to receive notifications, reply to this comment with the word STOP

Do not miss the last post from @steemitboard:

Trick or Treat - Publish your scariest halloween story and win a new badge
SteemitBoard notifications improved

You can upvote this notification to help all Steemit users. Learn why here!

Coin Marketplace

STEEM 0.27
TRX 0.13
JST 0.032
BTC 64161.84
ETH 2950.76
USDT 1.00
SBD 3.60