Edward's Tech Site

this site made with Next.js 13, see the code

FORAY: Aug 24 - State Management
Notes on creating new-tech showcase site

DOING: convert full site to MUI...

  • background
    • I'll be using a number of new technologies for projects at my new job
    • this is a showcase site to install them, get to know them, see how they work together, etc.
  • EASY PEASY
    • infos
    • Quick Start
    • setting up based CRUD-enabled site with json-server
      • x show flashcards
      • x add json-server
      • x make test.rest
      • x make one start script
      • json-server is set up
    • reading through documentation
      • Introduction
        • abstraction of Redux
        • no configuration is required
        • other solutions
          • React Query
            • if primary requirement for your state management solution is data synchronization with a server (e.g. GET and POST requests)
          • React's Context and/or Hook APIs
            • greater support
            • you'll be surprised with how far these APIs can take you
          • Zustand
            • isn't as rich in features as Easy Peasy
            • can easily meet the global state management requirements for most React applications
          • Redux Toolkit
            • greatly simplifies the specification of a Redux store
        • downsides of easy-peasy
          • abstraction is leaky
            • we are bound to the API and capabilities of Redux under the hood
            • knowledge of Redux would certainly help you have an even stronger understanding of how Easy Peasy works
            • specifically exposed the ability to extend the underlying Redux store
          • we are not an official state management solution
            • 3rd party dependency into your application, always a risk
            • may fall out of maintenance
            • may not evolve with React
          • bump the size of your bundle
          • increase the learning curve for new developers
            • we are nowhere near Redux/MobX level of popularity
      • Quick Start
        • hooks.ts
          • import { createTypedHooks } from 'easy-peasy';
            import { StoreModel } from './store';
             
            const typedHooks = createTypedHooks<StoreModel>();
             
            export const useStoreActions = typedHooks.useStoreActions;
            export const useStoreDispatch = typedHooks.useStoreDispatch;
            export const useStoreState = typedHooks.useStoreState;
        • PageWelcome.tsx
          • import { useStoreState, useStoreActions } from "../hooks";
            import { FaRegTrashCan } from "react-icons/fa6";
             
            export const PageWelcome = () => {
             
            const flashcards = useStoreState((state) => state.flashcards);
            const deleteFlashcard = useStoreActions((actions) => actions.deleteFlashcard);
             
            return (
            <>
            <p>There are {flashcards.length} flashcards.</p>
            <ul className="list-disc ml-6">
            {flashcards.map((flashcard, index) => {
            return (
            <li>
            <div className="mt-2 flex gap-2" key={index}>{flashcard.front} - {flashcard.back} <FaRegTrashCan className="mt-1 hover:text-red-800 cursor-pointer" onClick={() => deleteFlashcard(flashcard.id)}/></div>
            </li>
            )
            })}
            </ul>
            </>
            )
            }
        • store.ts
          • import { Action, action, createStore } from "easy-peasy";
            import { Flashcard } from "./types";
            import _db from "./data/db.json";
             
            const flashcards = _db.flashcards;
             
            export type StoreModel = {
            flashcards: Flashcard[];
            deleteFlashcard: Action<StoreModel, number>;
            };
             
            export const store = createStore<StoreModel>({
            flashcards,
            deleteFlashcard: action((state, payload) => {
            const index = state.flashcards.findIndex((m) => m.id === payload);
            if (index !== -1) {
            state.flashcards.splice(index, 1);
            }
            }),
            });
        • works like this:
      • nnn
    • make full crud with easy-peasy
      • load with easy-peasy
    • implement easy-peasy in site
      • load with store
      • make delete
      • make edit
      • make create
    • video course: React Redux Easy Peasy Complete Course
      • First Steps
        • he makes store/index.ts
        • ok, this is just setting up easy-peasy with store and getting data out on a page
      • Actions
        • makes a simple action
      • Thunks
        • got thunks to work to delete items with a waiting symbol for good UX
          • flashcardModel.ts
            • import { Action, action, Thunk, thunk } from "easy-peasy";
              import { DataFlashcard, Flashcard, ProcessStatus } from "../../types";
              import axios from "axios";
              import * as config from "../../config";
               
              export interface FlashcardModel {
              flashcards: Flashcard[];
              flashcardsLoadingStatus: ProcessStatus;
               
              // actions
              setFlashcards: Action<this, Flashcard[]>;
              setFlashcardsLoadingStatus: Action<this, ProcessStatus>;
              deleteFlashcard: Action<this, Flashcard>;
               
              // thunks
              loadFrontendFlashcardsThunk: Thunk<this>;
              deleteFlashcardThunk: Thunk<this, Flashcard>;
              }
               
              export const flashcardModel: FlashcardModel = {
              flashcards: [],
              flashcardsLoadingStatus: "inProcess",
              setFlashcards: action((state, flashcards) => {
              state.flashcards = structuredClone(flashcards);
              }),
              setFlashcardsLoadingStatus: action((state, loadingStatus) => {
              state.flashcardsLoadingStatus = loadingStatus;
              }),
              deleteFlashcard: action((state, flashcard) => {
              const index = state.flashcards.findIndex(
              (m) => m.dataItem.id === flashcard.dataItem.id
              );
              if (index !== -1) {
              state.flashcards.splice(index, 1);
              }
              }),
              loadFrontendFlashcardsThunk: thunk((actions) => {
              actions.setFlashcardsLoadingStatus("inProcess");
              setTimeout(async () => {
              try {
              const response = await axios.get(
              `http://localhost:3760/flashcards`
              );
              if (response.status === 200) {
              const dataFlashcards: DataFlashcard[] = response.data;
              const flashcards = decorateDataFlascards(dataFlashcards);
              actions.setFlashcards(flashcards);
              }
              actions.setFlashcardsLoadingStatus("finished");
              } catch (e: any) {
              console.log(`ERROR: ${e.message}`);
              actions.setFlashcardsLoadingStatus("failed");
              }
              }, config.uxLoadingSeconds() * 1000);
              }),
              deleteFlashcardThunk: thunk(async (actions, flashcard, { getState }) => {
              const state = getState();
              flashcard.deletingStatus = "inProcess";
              actions.setFlashcards(state.flashcards);
              setTimeout(async () => {
              try {
              const response = await axios.delete(
              `http://localhost:3760/flashcards/${flashcard.dataItem.id}`
              );
              if (response.status === 200) {
              console.log(
              `database deletion of id=${flashcard.dataItem.id} was successful`
              );
              actions.deleteFlashcard(flashcard);
              }
              } catch (e: any) {
              console.log(`ERROR: ${e.message}`);
              flashcard.deletingStatus = "failed";
              }
              }, config.uxLoadingSeconds() * 1000);
              }),
              };
               
              export const decorateDataFlascards = (
              dataFlashcards: DataFlashcard[]
              ): Flashcard[] => {
              const flashcards: Flashcard[] = [];
              for (const dataFlashcard of dataFlashcards) {
              const flashcard: Flashcard = {
              dataItem: dataFlashcard,
              deletingStatus: "notActive",
              };
              flashcards.push(flashcard);
              }
              return flashcards;
              };
               
  • MUI
    • background
    • creating pages
      • x welcome page
      • .. MUI page
    • install base MUI
      • npm install @mui/material @emotion/react @emotion/styled
        • @emotion/react and @emotion/styled are required for styling with Material UI
        • Emotion is the default styling engine for MUI
    • install MUI icons
      • npm install @mui/icons-material
    • my version are higher
    • Button
      • import { Button } from '@mui/material';
         
        <Button variant="contained" color="primary">Test</Button>
    • x make responsive menu
      • x show different for mobile
      • x style in div
      • x generate from menuObject
      • x mobile: show current and icon
        • x first
      • x add icon and make mock clickable
      • x create menu opened
      • x make click work
    • x implement reponsive menu in ../vite-react-menu-createbrowserrouter-contexthttps://github.com/edwardtanguay/vite-react-menu-createbrowserrouter-context
    • x template site: more space and larger
    • .. COURSE: Net Ninja: Material UI (2021)
      • Intro & Setup
      • Typography
        • docs: Typography
        • don't use h1, h2, etc.
        • default is p
        • install Roboto font since it is used by default
          • npm install @fontsource/roboto
          • main.tsx
            • import '@fontsource/roboto/300.css';
              import '@fontsource/roboto/400.css';
              import '@fontsource/roboto/500.css';
              import '@fontsource/roboto/700.css';
      • Buttons
        • contained is the most popular variant
        • my default is primary
        • you can add attribute type="submit" and onClick=""
        • Container
        • a nice button group with darker hover:
          • <ButtonGroup variant='contained' disableRipple>
            <Button
            sx={{
            backgroundColor: 'success.main',
            '&:hover': {
            backgroundColor: 'success.dark',
            },
            }}
            >Save</Button>
            <Button
            sx={{
            backgroundColor: 'error.main',
            '&:hover': {
            backgroundColor: 'error.dark',
            },
            }}
            >Delete</Button>
            <Button
            sx={{
            backgroundColor: 'warning.main',
            '&:hover': {
            backgroundColor: 'warning.dark',
            },
            }}
            >Copy</Button>
            </ButtonGroup>
      • Icons
        • install MUI icons
          • npm install @mui/icons-material
          • I got 6.1.0
            • "@mui/icons-material": "^6.1.0",
              "@mui/material": "^6.1.0",
        • search icons here
        • nnn
      • nnn
        • nnn
      • nnn
        • nnn
      • nnn
        • nnn
      • nnn
        • nnn
      • nnn
        • nnn
      • nnn
        • nnn
    • MUI course:
    • .. convert full site to MUI
  • nnn
    • nnn
  • nnn
    • nnn
  • nnn
    • nnn
  • nnn
    • nnn
  • nnn
    • nnn
  • nnn
    • nnn
  • nnn
    • nnn
  • nnn
    • nnn
  • nnn
    • nnn