Skip to content

09 Card And Search

JP Barbosa edited this page Apr 15, 2023 · 2 revisions

Card And Search

Download Movies Images

mkdir -p ./packages/web/public/img/movies

(cd ./packages/web/public/img/movies && curl http://localhost:3333/movies \
  | jq '.[] | .title | ascii_downcase' \
  | sed "s/['\"]//g" | sed "s/[ \/]/_/g" | sed 's/$/.jpg/' \
  | sed "s_^_https://raw.githubusercontent.com/jpbarbosa/neo4j-crud/main/packages/web/public/img/movies/_" \
  | xargs -n 1 wget -N)

Image File Names

code ./packages/web/src/utils/fileNameFromString.ts
export const fileNameFromString = (str: string): string => {
  return str
    .toLocaleLowerCase()
    .replaceAll(' ', '_')
    .replaceAll('/', '_')
    .replaceAll("'", '')
};

Card

code ./packages/web/src/pages/movies/List/Item.tsx
import { Link } from 'react-router-dom';
import { Movie } from '@neo4j-crud/shared';
import { fileNameFromString } from '../../../utils/fileNameFromString';
import { HighlightedText } from '../../../components';

export type ItemProps = {
  movie: Movie;
  search: string;
};

export const Item: React.FC<ItemProps> = ({ movie, search }) => {
  return (
    <li key={movie.title}>
      <Link to={`${movie.id}/edit`}>
        <img
          src="/img/px.gif"
          style={{
            backgroundImage: `url("/img/movies/${fileNameFromString(
              movie.title
            )}.jpg")`,
          }}
          alt={movie.title}
        />
        <div className="content">
          <div>
            <h3>
              <HighlightedText text={movie.title} search={search} />
            </h3>
            <div className="released">Released: {movie.released}</div>
            <div className="tagline">{movie.tagline}</div>
          </div>
          <div className="action">
            <button className="ghost">Edit</button>
          </div>
        </div>
      </Link>
    </li>
  );
};

HighlightedText Component

code ./packages/web/src/components/HighlightedText.tsx
type HighlightedTextProps = {
  text: string;
  search?: string;
};

export const HighlightedText: React.FC<HighlightedTextProps> = ({
  text,
  search,
}) => {
  if (!search) {
    return <div>{text}</div>;
  }
  const regex = new RegExp(search, 'gi');
  const highlightedText = text.replace(
    regex,
    `<span class="highlight">$&</span>`
  );
  return <div dangerouslySetInnerHTML={{ __html: highlightedText }} />;
};
code ./packages/web/src/components/index.ts
...
export * from './HighlightedText';

Content

code ./packages/web/src/pages/movies/List/index.tsx
import { useState } from 'react';
import { Content } from './Content';

export const List = () => {
  const [search, setSearch] = useState<string>('');

  return (
    <div>
      <div className="actions-bar">
        <h2>Movies</h2>
        <div className="filter">
          <input
            type="text"
            value={search}
            placeholder="Search by movie title or person name..."
            onChange={(e) => setSearch(e.target.value)}
          />
        </div>
      </div>
      <Content search={search} />
    </div>
  );
};
code ./packages/web/src/pages/movies/List/Content.tsx
import { useQuery } from 'react-query';
import axios from 'axios';
import { AxiosCustomError, Movie } from '@neo4j-crud/shared';
import { useDebounce } from '../../../hooks/useDebounce';
import { Item } from './Item';

const url = `${import.meta.env.VITE_API_URL}/movies`;

type ContentProps = {
  search: string;
};

export const Content: React.FC<ContentProps> = ({ search }) => {
  const debouncedSearch = useDebounce(search, 500);

  const { data, error, isLoading } = useQuery<Movie[], AxiosCustomError>(
    ['movies', debouncedSearch],
    () => axios.get<Movie[]>(`${url}?search=${search}`).then((res) => res.data)
  );

  if (error) {
    return <div>{error.message}</div>;
  }

  if (isLoading) {
    return <div>Loading...</div>;
  }

  if (!data) {
    return <div>No data.</div>;
  }

  return (
    <ul className="record-list">
      {data.map((movie) => (
        <Item key={movie.id} movie={movie} search={search} />
      ))}
    </ul>
  );
};

Debounce Hook

code ./packages/web/src/hooks/useDebounce.ts
import { useEffect, useState } from 'react';

export const useDebounce = <T>(value: T, delay: number): T => {
  const [debouncedValue, setDebouncedValue] = useState<T>(value);

  useEffect(() => {
    const handler = setTimeout(() => {
      setDebouncedValue(value);
    }, delay);

    return () => {
      clearTimeout(handler);
    };
  }, [value, delay]);

  return debouncedValue;
};

Test Frontend

open http://localhost:4200
List Search

Commit

git add .
git commit -m "Card And Search"