ByteGrad / Professional-React-and-Next.js-Course

This repo contains everything you need as a student of the Professional React & Next.js Course by ByteGrad.com
https://bytegrad.com/courses/professional-react-nextjs
111 stars 58 forks source link

A few improvements in CorpComment project #17

Open Rope-a-dope opened 1 month ago

Rope-a-dope commented 1 month ago
  1. Since we use Zustand to manage the state, there is no need to pass props to FeedbackForm and HashtagItem, we can just use Zustand in them directly.
  2. It's always a good idea to use custom hooks as explained in https://tkdodo.eu/blog/working-with-zustand and your video https://www.youtube.com/watch?v=I7dwJxGuGYQ&t=69s&ab_channel=ByteGrad. It's the same for other tools as for useContext. But because we drive the state companList and FilteredFeedbackItems inside the store, we must export getCompanyList() and getFilteredFeedbackItems() with () to make sure they are executed inside the store.

feedbackItemsStore.ts

import { create } from "zustand";
import { TFeedbackItem } from "../lib/types";

type Store = {
  feedbackItems: TFeedbackItem[];
  isLoading: boolean;
  errorMessage: string;
  selectedCompany: string;
  actions: {
    getCompanyList: () => string[];
    getFilteredFeedbackItems: () => TFeedbackItem[];
    addItemToList: (text: string) => Promise<void>;
    selectCompany: (company: string) => void;
    fetchFeedbackItems: () => Promise<void>;
  };
};

export const useFeedbackItemsStore = create<Store>((set, get) => ({
  feedbackItems: [],
  isLoading: false,
  errorMessage: "",
  selectedCompany: "",
  actions: {
    getCompanyList: () => {
      const state = get();
      return state 
        .feedbackItems.map((item) => item.company)
        .filter((company, index, array) => {
          return array.indexOf(company) === index;
        });
    },
    getFilteredFeedbackItems: () => {
      const state = get();

      return state.selectedCompany
        ? state.feedbackItems.filter(
            (feedbackItem) => feedbackItem.company === state.selectedCompany
          )
        : state.feedbackItems;
    },
    addItemToList: async (text: string) => {
      const companyName = text
        .split(" ")
        .find((word) => word.includes("#"))!
        .substring(1);

      const newItem: TFeedbackItem = {
        id: new Date().getTime(),
        text: text,
        upvoteCount: 0,
        daysAgo: 0,
        company: companyName,
        badgeLetter: companyName.substring(0, 1).toUpperCase(),
      };

      set((state) => ({
        feedbackItems: [...state.feedbackItems, newItem],
      }));

      await fetch(
        "https://bytegrad.com/course-assets/projects/corpcomment/api/feedbacks",
        {
          method: "POST",
          body: JSON.stringify(newItem),
          headers: {
            Accept: "application/json",
            "Content-Type": "application/json",
          },
        }
      );
    },
    selectCompany: (company: string) => {
      set(() => ({
        selectedCompany: company,
      }));
    },
    fetchFeedbackItems: async () => {
      set(() => ({
        isLoading: true,
      }));

      try {
        const response = await fetch(
          "https://bytegrad.com/course-assets/projects/corpcomment/api/feedbacks"
        );

        if (!response.ok) {
          throw new Error();
        }

        const data = await response.json();
        set(() => ({
          feedbackItems: data.feedbacks,
        }));
      } catch (error) {
        set(() => ({
          errorMessage: "Something went wrong. Please try again later.",
        }));
      }

      set(() => ({
        isLoading: false,
      }));
    },
  },
}));

export const useFeedbackItems = () => useFeedbackItemsStore((state) => state.feedbackItems);
export const useIsLoading = () => useFeedbackItemsStore((state) => state.isLoading);
export const useErrorMessage = () => useFeedbackItemsStore((state) => state.errorMessage);
export const useSelectedCompany = () => useFeedbackItemsStore((state) => state.selectedCompany);
export const useFeedbackItemActions = () => useFeedbackItemsStore((state) => state.actions);
export const useCompanyList = () => useFeedbackItemsStore((state) => state.actions.getCompanyList()); //Make sure it executes inside Zustand store.
export const useFilteredFeedbackItems = () => useFeedbackItemsStore((state) => state.actions.getFilteredFeedbackItems()); //Make sure it executes inside Zustand store.

FeedbackForm.tsx

import { useState } from "react";
import { MAX_CHARACTERS } from "../../lib/constants";
import { useFeedbackItemActions } from "../../stores/feedbackItemsStore";

export default function FeedbackForm() {
  const {addItemToList} = useFeedbackItemActions();
  const [text, setText] = useState("");
  const [showValidIndicator, setShowValidIndicator] = useState(false);
  const [showInvalidIndicator, setShowInvalidIndicator] = useState(false);
  const charCount = MAX_CHARACTERS - text.length;

  const handleChange = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
    const newText = event.target.value;
    if (newText.length > MAX_CHARACTERS) {
      return;
    }
    setText(newText);
  };

  const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
    event.preventDefault();

    // basic validation
    if (text.includes("#") && text.length >= 5) {
      setShowValidIndicator(true);
      setTimeout(() => setShowValidIndicator(false), 2000);
    } else {
      setShowInvalidIndicator(true);
      setTimeout(() => setShowInvalidIndicator(false), 2000);
      return;
    }

    addItemToList(text);
    setText("");
  };

  return (
    <form
      onSubmit={handleSubmit}
      className={`form ${showValidIndicator && "form--valid"} ${
        showInvalidIndicator && "form--invalid"
      }`}
    >
      <textarea
        value={text}
        onChange={handleChange}
        id="feedback-textarea"
        placeholder="blabla"
        spellCheck={false}
      />

      <label htmlFor="feedback-textarea">
        Enter your feedback here, remember to #hashtag the company
      </label>

      <div>
        <p className="u-italic">{charCount}</p>
        <button>
          <span>Submit</span>
        </button>
      </div>
    </form>
  );
}

FeedbackList.tsx

import {
  useErrorMessage,
  useFilteredFeedbackItems,
  useIsLoading
} from "../../stores/feedbackItemsStore";
import ErrorMessage from "../ErrorMessage";
import Spinner from "../Spinner";
import FeedbackItem from "./FeedbackItem";

export default function FeedbackList() {
  const isLoading = useIsLoading();
  const errorMessage = useErrorMessage();
  const filteredFeedbackItems = useFilteredFeedbackItems();

  return (
    <ol className="feedback-list">
      {isLoading && <Spinner />}

      {errorMessage && <ErrorMessage message={errorMessage} />}

      {filteredFeedbackItems.map((feedbackItem) => (
        <FeedbackItem key={feedbackItem.id} feedbackItem={feedbackItem} />
      ))}
    </ol>
  );
}

HashtagItem.tsx

import { useFeedbackItemActions } from "../../stores/feedbackItemsStore";

type HashtagItemProps = {
  company: string;
};

export default function HashtagItem({
  company,
}: HashtagItemProps) {
  const { selectCompany } = useFeedbackItemActions(); 
  return (
    <li key={company}>
      <button onClick={() => selectCompany(company)}>#{company}</button>
    </li>
  );
}

HashtagList.tsx

import { useCompanyList } from "../../stores/feedbackItemsStore";
import HashtagItem from "./HashtagItem";

export default function HashtagList() {
  const companyList = useCompanyList();

  return (
    <ul className="hashtags">
      {companyList.map((company) => (
        <HashtagItem
          key={company}
          company={company}
        />
      ))}
    </ul>
  );
}

App.tsx

import { useEffect } from "react";
import { useFeedbackItemActions } from "../stores/feedbackItemsStore";
import HashtagList from "./hashtag/HashtagList";
import Container from "./layout/Container";
import Footer from "./layout/Footer";

function App() {
  const { fetchFeedbackItems } = useFeedbackItemActions();

  useEffect(() => {
    fetchFeedbackItems();
  }, [fetchFeedbackItems]);

  return (
    <div className="app">
      <Footer />

      <Container />

      <HashtagList />
    </div>
  );
}

export default App;