-1

So I'm creating a web app using react with typescript that calls an api endpoint (it's a yugioh card database https://db.ygoprodeck.com/api-guide/). I should probably also point out that I'm using RTK Query, it might be a caching things I don't know. Specifically for my problem, it's a query that makes a fuzzy name search to see if a user's input text matches any card in the database. The returned array from the api however is not sorted in a way that I desire, so I wanted to fix that with a utility function as I can see myself sorting this way in multiple places.

Given the search "Dark" for example, the api returns all cards where dark appears in the name, regardless of when in the name the word appear. A card could for example be called "Angel of Dark" and it would come before a much more likely search "Dark Magician" because the api returns it in alphabetical order. But I want it to be sorted in a way that cards that starts with the word appears before cards where the word appears later.

I think I've done it in a way that should return it correctly, but alas it doesn't, so I figured the experts could maybe tell me where the code is wrong, if it's in the utility function or if it's with RTKQ or somewhere else where I'm just completely wrong.

Here's the utility function:

import { CardData } from "../models/cardData.interface";

const SortNameBySearch: Function = (search: string, cardData: CardData) => {
  const sortedData = [...cardData.data];
  sortedData.sort((a, b) => {
    // Sort results by matching name with search position in name
    if (
      a.name.toLowerCase().indexOf(search.toLowerCase()) >
      b.name.toLowerCase().indexOf(search.toLowerCase())
    ) {
      return 1;
    } else if (
      a.name.toLowerCase().indexOf(search.toLowerCase()) <
      b.name.toLowerCase().indexOf(search.toLowerCase())
    ) {
      return -1;
    } else {
      if (a.name > b.name) return 1;
      else return -1;
    }
  });
  return sortedData;
};

export default SortNameBySearch;

I've sliced the array to only show the top 3 results and it looks like this, but it's weird because the first one should't even be in this array as it doesn't contain the searched name, which makes me think it might be a caching thing with RTKQ but I'm honestly clueless as how to fix that. shows search result given the input "Dark"

For good measure and better context here's how I'm using it in context

import { FC, useEffect, useRef, useState } from "react";
import { useGetCardsBySearchQuery } from "../../../../services/api";
import { SearchResultsProps } from "./SearchResults.types";
import SortNameBySearch from "../../../../utils/SortNamesBySearch";
import { Card } from "../../../../models/card.interface";

const SearchResults: FC<SearchResultsProps> = ({ search }) => {
  const [searched, setSearched] = useState("");
  const [typing, setTyping] = useState(false);
  const searchRef = useRef(0);
  useEffect(() => {
    clearTimeout(searchRef.current);
    setTyping(true);
    if (search !== null) {
      searchRef.current = window.setTimeout(() => {
        setSearched(search);
        setTyping(false);
      }, 100);
    }
  }, [search]);
  const { data, isLoading, isError, isSuccess } = useGetCardsBySearchQuery(
    searched,
    { skip: typing }
  );

  const results =
    !typing && isSuccess
      ? SortNameBySearch(searched, data)
          .slice(0, 3)
          .map((card: Card) => {
            return (
              <p key={card.id} className="text-white">
                {card.name}
              </p>
            );
          })
      : null;

  if (isError) {
    return <div>No card matching your query was found in the database.</div>;
  }

  return (
    <>
      {isLoading ? (
        "loading..."
      ) : typing ? (
        "loading..."
      ) : (
        <div className="bg-black relative">{results}</div>
      )}
    </>
  );
};

export default SearchResults;

Given that I'm still new to this whole thing there are likely other issues with my code and of course I want to improve, so if there's something in there that just doesn't make sense please do let me know.

Looking forward to see your answers. -Benjamin

Benji
  • 61
  • 6
  • 1
    I would recommend you split your question in 2, as they are quite unrelated topics. Sorting and RTKQ caching. – Renato Jun 26 '22 at 08:57
  • 1
    For the sorting problem, in your short sample, if we ignore the "Master of Chaos", the other 2 are correctly ordered as "Dark Advance" should come before "Dark Alligator". But why is "Master of Chaos" coming first? That is because SortNameBySearch function is not considering the case where the word "dark" is not found. if the word you are searching does not appear on the string, the method "indexOf" will return -1, making it show first in the result. If that scenario should not happen in your sorting function, then you just need to work on your cache. – Renato Jun 26 '22 at 09:02
  • @Renato Thanks for your input if we say I save the potential caching issue for another day. Would it be possible to put the results that doesn't contain the word at the end of the array? – Benji Jun 26 '22 at 14:40
  • 1
    Well, you could filter the results in memory, with something like: `sortedData = sortedData.filter((word) => word.includes('dark'))`. with the proper case handling and using the proper variable of course. – Renato Jun 26 '22 at 15:29
  • @Renato once again thank you for your input, then I'm just back to having an array sorted where the cards where the search is included in the name shows up, for example if the third word in the name is Dark, but the first word starts with an "A" it would take priority. Is there a way to filter so the names that actually start with the search shows up first and then the cards that simply contain the name would show up in the end of the array? – Benji Jun 26 '22 at 16:01
  • 1
    So, if the first word is 'Dark' those should be on the top, and within those should be in alphabetical order? And if it's not the first word (doesn't matter if it's the second word or the tenth word), those should be in the bottom block, and alphabetically ordered between those. if that is what you want, it's possible. But if you want to have multiple blocks, for example: [names with dark as the first word], [names with dark as the second word], [names with dark as the third word]. It's still technically possible, it will add too much complexity to your code, and I recommend against it – Renato Jun 28 '22 at 02:11
  • @Renato yes that's exactly what I want, your first assumption. Can it be done by using split on the name and saying -1 if [0] !== 'dark' ? – Benji Jun 28 '22 at 13:36
  • 1
    did you check my answer? If that helped you, please mark the answer as accepted, if not, let me know what didn't work, and I can still try to help you. – Renato Jun 30 '22 at 01:27

1 Answers1

0

Here is your code changed to do as you said in the last comment. Not fully tested, but it should be something like this, so I guess this should be enough to get you going.

import { CardData } from "../models/cardData.interface";

const SortNameBySearch: Function = (search: string, cardData: CardData) => {
  const sortedData = [...cardData.data];

  const dataWithSearchAtIndex0: string[] = [];
  const dataWithSearchAtOtherIndexes: string[] = [];


  for(const data of sortedData) {
    const searchIndex = data.toLowerCase().indexOf(search.toLowerCase());
    if (searchIndex === 0) {
      dataWithSearchAtIndex0.push(data);
    } else if (searchIndex > 0) {
      dataWithSearchAtOtherIndexes.push(data);
    }
  }

  dataWithSearchAtIndex0.sort((a, b) => sortIgnoringCase(a, b));
  dataWithSearchAtOtherIndexes.sort((a, b) => sortIgnoringCase(a, b));

  return [...dataWithSearchAtIndex0, ...dataWithSearchAtOtherIndexes];
}

const sortIgnoringCase: Function = (a: string, b: string) => {
  const lowerCaseA = a.toLowerCase(); // ignore upper and lowercase
  const lowerCaseB = b.toLowerCase(); // ignore upper and lowercase
  if (lowerCaseA < lowerCaseB) {
    return -1;
  }
  if (lowerCaseA > lowerCaseB) {
    return 1;
  }

  // a and b must be equal
  return 0;
};

export default SortNameBySearch;
Renato
  • 329
  • 1
  • 7