1
\$\begingroup\$

In my React learning journey, I wanted to create a Note app following the SOLID principles and use as many hooks as possible to grasp things in React. If you can review my code and improve it, it would help me a lot.

App

//App.jsx import { useEffect, useState, useReducer } from "react"; import { initialNotes, notesReducer, NotesContext, NotesDispatchContext, } from "./components/notesContext"; import CreateNote from "./components/CreateNote"; import SearchBar from "./sections/SearchBar"; import Header from "./sections/Header"; import FilteredNotes from "./components/FilteredNotes"; export default function App() { //Handles notes reducer, query state for search bar, theme state for theme change and displays the components.// //----------------------------------------------------------------// /* States & Reducers */ const [notes, dispatch] = useReducer(notesReducer, initialNotes); const [query, setQuery] = useState(""); const [theme, setTheme] = useState(() => { const initialTheme = localStorage.getItem("theme"); return initialTheme ? initialTheme : "light"; }); //----------------------------------------------------------------// //----------------------------------------------------------------// /* Local Storage */ useEffect(() => { getNotesFromLocalStorage(); }, [notes]); useEffect(() => { getThemeFromLocalStorage(); }, [theme]); function getNotesFromLocalStorage() { localStorage.setItem("notes", JSON.stringify(notes)); } function getThemeFromLocalStorage() { const savedTheme = localStorage.getItem("theme"); if (savedTheme) { setTheme(savedTheme); } } //----------------------------------------------------------------// //----------------------------------------------------------------// /* Event Handlers */ function toggleDarkMode() { setTheme((prevTheme) => { const newTheme = prevTheme === "light" ? "dark" : "light"; localStorage.setItem("theme", newTheme); return newTheme; }); } function updateSearchBar(e) { setQuery(e.target.value); } //----------------------------------------------------------------// return ( <div className={`${theme === "dark" ? "dark bg-dark" : "bg-light"} font-dancing-script `} > <main className="max-container min-h-screen p-4 text-text-light dark:text-text-dark"> <section className="flex w-full items-center justify-between py-2"> <Header toggleDarkMode={toggleDarkMode} /> </section> <section> <SearchBar query={query} onChange={updateSearchBar} /> </section> <section className="grid grid-cols-3 gap-2 py-4 max-sm:grid-cols-1 "> <NotesDispatchContext.Provider value={dispatch}> <NotesContext.Provider value={notes}> <FilteredNotes query={query} /> </NotesContext.Provider> <CreateNote /> </NotesDispatchContext.Provider> </section> </main> </div> ); } 

Create Note

//CreateNote.jsx import { useContext, useState } from "react"; import { NotesDispatchContext } from "./notesContext"; export default function CreateNote() { // This component handles the creation of notes.// //----------------------------------------------------------------// const [noteValue, setNoteValue] = useState(""); const dispatch = useContext(NotesDispatchContext); //----------------------------------------------------------------// let currentDate = new Date(); let fullDate = currentDate.getUTCDate() + "/" + currentDate.getUTCMonth() + 1 + "/" + currentDate.getUTCFullYear(); //----------------------------------------------------------------// function updateNote(e) { setNoteValue(e.target.value); } //----------------------------------------------------------------// return ( <div className="flex h-48 flex-col justify-between gap-2 rounded-xl bg-edit-note-light p-4 shadow-xl dark:bg-edit-note-dark"> <textarea aria-label="your note text" placeholder="Type to add a note..." onChange={updateNote} value={noteValue} maxLength={200} className="h-full min-h-6 resize-none bg-transparent text-lg italic placeholder:text-gray-600 focus:outline-none " ></textarea> <div className="flex items-center justify-between h-16"> <p>{200 - noteValue.length} remaining </p> <button aria-label="create-note-button" className="rounded-xl bg-note-light px-6 py-1 dark:bg-button-dark h-full" onClick={() => { dispatch({ type: "create_note", text: noteValue, date: fullDate, }); setNoteValue(""); }} > Save </button> </div> </div> ); } 

Current Note

//CurrentNote.jsx import { useContext, useState } from "react"; import { PiTrashDuotone, PiX } from "react-icons/pi"; import { NotesDispatchContext } from "./notesContext"; export default function CurrentNote(props) { // This component handles the display of the current note. // // ----------------------------------------------------------------// const dispatch = useContext(NotesDispatchContext); const [noteValue, setNoteValue] = useState(props.text ? props.text : ""); const [noteState, setNoteState] = useState("saved"); // ----------------------------------------------------------------// function setNote(e) { setNoteValue(e.target.value); } // ----------------------------------------------------------------// const changeState = (v) => { return () => setNoteState(v); }; // ----------------------------------------------------------------// return noteState === "saved" ? ( // Saved note <div className="flex h-48 w-full flex-col justify-between gap-2 rounded-xl bg-note-light p-4 italic shadow-lg dark:bg-note-dark"> <textarea aria-label="your note text" onChange={setNote} value={props.text} maxLength={200} disabled={true} className="h-full min-h-6 resize-none bg-transparent text-xl italic focus:outline-none" ></textarea> <div className="flex items-center justify-between h-16"> <time>{props.date} </time> <div className="flex w-28 items-center justify-between h-full"> <button aria-label="edit-note" className="rounded-xl bg-button-light px-6 py-1 dark:bg-button-dark h-full" onClick={changeState("typing")} > Edit </button> <PiTrashDuotone className="cursor-pointer w-16" onClick={() => dispatch({ type: "delete_note", id: props.id, }) } /> </div> </div> </div> ) : ( // Edit note <div className="flex h-48 flex-col justify-between rounded-xl gap-2 bg-edit-note-light p-4 shadow-xl dark:bg-edit-note-dark"> <textarea aria-label="your note text" placeholder="Type to add a note..." onChange={setNote} value={noteValue} maxLength={200} className="h-full min-h-6 resize-none bg-transparent text-xl italic focus:outline-none" ></textarea> <div className="flex items-center justify-between h-16 "> <p>{200 - noteValue.length} remaining </p> <div className="flex w-28 items-center justify-between h-full "> <button aria-label="save-note" className="rounded-xl bg-note-light px-6 py-1 dark:bg-button-dark h-full" onClick={() => { dispatch({ type: "edit_note", id: props.id, text: noteValue, }); setNoteState("saved"); }} > Save </button> <PiX className="cursor-pointer w-16" onClick={changeState("saved")}></PiX> </div> </div> </div> ); } 

Filtered Notes

//FilteredNotes.jsx import { useContext, useMemo } from "react"; import CurrentNote from "./CurrentNote"; import { NotesContext } from "./notesContext"; export default function FilteredNotes({ query }) { // This component filters the notes using the query state. Query state comes from the SearchBar via App.js // //----------------------------------------------------------------// const notes = useContext(NotesContext); //----------------------------------------------------------------// const filteredNotes = useMemo(() => { return filterItems(notes, query); }, [notes, query]); function filterItems(notes, query) { query = query.toLowerCase(); return notes.filter((item) => item.text.toLowerCase().includes(query)); } const result = filteredNotes; //----------------------------------------------------------------// //----------------------------------------------------------------// // Creates a CurrentNote component from a list of notes let notesList = []; if (notes.length > 0) { notesList = result.map((note) => { return ( <CurrentNote key={note.id} id={note.id} text={note.text} date={note.date} /> ); }); } else { notesList = []; } // Used on App.js return. return notesList; //----------------------------------------------------------------// } 

Notes Context

//NotesContext.jsx import { createContext } from "react"; //----------------------------------------------------------------// // Get notes from the browser // export const initialNotes = JSON.parse(localStorage.getItem("notes")) || []; //----------------------------------------------------------------// // This context is used on the FilteredNotes component export const NotesContext = createContext(null); export const NotesDispatchContext = createContext(null); //----------------------------------------------------------------// // Reducer to dispatch changes on both child and parent components. App.js notes. export function notesReducer(notes, action) { switch (action.type) { case "create_note": const newNote = { id: notes.length + 1, text: action.text, date: action.date, }; return [...notes, newNote]; case "edit_note": return notes.map((newNote) => { if (newNote.id === action.id) { return { ...newNote, text: action.text, date: newNote.date, }; } else { return newNote; } }); case "delete_note": const newNotes = notes.filter((note) => note.id !== action.id); return newNotes; default: throw Error("Unknown action: " + action.type); } } 

Header

//Header.jsx import { PiMoonBold } from "react-icons/pi"; export default function Header(props) { // This component handles the display of the header. // return ( <> <h1 className="text-[2rem] font-bold text-black dark:text-white"> Notes </h1> <button aria-label="theme-toggle-button" className="rounded-lg bg-button-light px-3 py-1 text-text-light shadow-lg dark:bg-button-dark dark:text-text-dark max-sm:hover:shadow-none " onClick={props.toggleDarkMode} > <PiMoonBold /> </button> </> ); } 

Search Bar

//SearchBar.jsx import { PiMagnifyingGlass } from "react-icons/pi"; export default function SearchBar(props) { // This component handles the display of the search bar. // return ( <div className="flex w-full items-center justify-center rounded-xl bg-search-bar-light p-3 text-text-light shadow-lg dark:bg-search-bar-dark dark:text-text-dark"> <PiMagnifyingGlass className="cursor-pointer" /> <input aria-label="search input" placeholder="Type to search..." value={props.query} onChange={props.onChange} className="w-full bg-search-bar-light px-4 italic focus:outline-none dark:bg-search-bar-dark" ></input> </div> ); } 
\$\endgroup\$

    0

    Start asking to get answers

    Find the answer to your question by asking.

    Ask question

    Explore related questions

    See similar questions with these tags.