Open Rope-a-dope opened 4 months ago
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;
feedbackItemsStore.ts
FeedbackForm.tsx
FeedbackList.tsx
HashtagItem.tsx
HashtagList.tsx
App.tsx