End to End React with Prisma 2

Start at the beginning and work your way through this project. The code for each step as well as the finished project can be found in the Github repository.


    22. Start the NewEditItem component

    Objective: Start a component for making new items.

    In this step we will be creating a component that we will be able to use for creating new or editing existing items. For now we will just focus on the new capability, but after we will turn to setting up the edit functionality.

    We will set up a state that will store an empty feed or bundle in it which we then will update as people write text in the fields. We will store this state as currentItem and set it using setState. Our visual component will be a form that has a number of things inside- we are blocking out

    components/newEditItem.tsx

    tsx

    import { useMutation } from '@apollo/client'
    import { useState } from 'react'
    import {
    CREATE_BUNDLE_MUTATION,
    CREATE_FEED_MUTATION,
    } from '../utils/api/graphql/mutations'
    import {
    ActionType,
    BadgeFieldName,
    BundleState,
    FeedState,
    ItemType,
    NewItemState,
    } from '../utils/types'
    import { BadgeList } from './badgeList'
    import { ErrorSign, WaitingClock } from './svg'
    export const NewEditItem = ({ type }: { type: ItemType }) => {
    const isFeed = type === ItemType.FeedType
    const initialFeed: FeedState = { name: '', url: '', tags: [] }
    const initialBundle: BundleState = {
    name: '',
    description: '',
    tags: [],
    feeds: [],
    }
    const initialState: NewItemState = isFeed ? initialFeed : initialBundle
    const inputFields = isFeed ? ['name', 'url'] : ['name', 'description']
    const [currentItem, setItem] = useState<NewItemState>(initialState)
    const [
    createItemMutation,
    { loading: createLoading, error: createError },
    ] = useMutation(isFeed ? CREATE_FEED_MUTATION : CREATE_BUNDLE_MUTATION)
    if (createLoading) {
    return <WaitingClock className="my-20 h-10 w-10 text-gray-500 m-auto" />
    }
    if (createError) {
    return <ErrorSign className="my-20 h-10 w-10 text-gray-500 m-auto" />
    }
    return (
    <>
    <form
    onSubmit={e => {
    e.preventDefault()
    }}
    >
    <div className="grid grid-cols-12 gap-4 rounded-md border-4 my-4 py-2 px-4">
    <h3 className="col-span-12 text-lg font-medium py-2">
    {isFeed ? `New Feed` : `New Bundle`}
    </h3>
    <div className="col-span-6">
    {inputFields.map(name => (
    <p key={name}>{name}</p>
    ))}
    <div className={`py-4 ${isFeed ? null : `pt-28`}`}>
    <input
    className={`py-4 ${`bg-${
    isFeed ? 'green' : 'purple'
    }-400`} hover:bg-${
    isFeed ? 'green' : 'purple'
    }-700 text-white font-bold px-12 rounded`}
    type="submit"
    />
    </div>
    </div>
    <div className="col-span-6">
    <div className="py-2">
    <label className="block py-2">Tags:</label>
    <div className="grid grid-cols-3 gap-2">
    <BadgeList
    fieldName={BadgeFieldName.tags}
    action={ActionType.CREATE}
    setItem={setItem}
    item={currentItem}
    />
    </div>
    </div>
    <div className="py-2">
    <label className="block py-2">Add New Tag:</label>
    </div>
    {isFeed ? null : (
    <>
    <div className="py-2">
    <label className="block py-2">Feeds:</label>
    <div className="grid grid-cols-3 gap-2">
    <BadgeList
    fieldName={BadgeFieldName.feeds}
    action={ActionType.CREATE}
    setItem={setItem}
    item={currentItem}
    />
    </div>
    </div>
    <div className="py-2">
    <label className="block py-2">Add New Feed:</label>
    </div>
    </>
    )}
    </div>
    </div>
    </form>
    </>
    )
    }

    We have to make sure to add the NewItemState to our types file- it is just either a FeedState or BundleState.

    utils/types.ts

    ts

    // other types above
    export type NewItemState = FeedState | BundleState

    Let's modify our bundles and feeds page. These changes will be similar between the two- we use the useFetchUser hook to get the user object and if the user is logged in, we will show a div that has a + sign and either feed or bundle depending on which page it is. When you click on it, it will switch to a - sign and we will display the <NewEditItem> component that we just created.

    pages/bundles.tsx

    tsx

    import { useState } from 'react'
    import { NewEditItem } from '../components/newEditItem'
    import { Layout } from '../components/layout'
    import { ItemType, SelectedFeedState } from '../utils/types'
    import { ItemList } from '../components/itemList'
    import { useFetchUser } from '../utils/user'
    import { Minus, Plus } from '../components/svg'
    const BundlesPage = () => {
    const { user, loading } = useFetchUser()
    const initialSelected: SelectedFeedState = {
    id: null,
    feeds: [],
    editMode: false,
    newMode: false,
    }
    const [selected, setSelected] = useState(initialSelected)
    return (
    <Layout>
    <div className="grid grid-cols-2">
    <h3 className="grid-cols-1 justify-start flex text-lg font-medium py-4">
    Bundles Page
    </h3>
    {user ? (
    <div
    onClick={e => {
    e.persist()
    setSelected(currState => ({
    ...currState,
    newMode: !currState.newMode,
    editMode: false,
    }))
    }}
    className="flex grid-cols-1 justify-end cursor-pointer"
    >
    {selected.newMode ? (
    <Minus
    className={`h-6 w-6 text-${
    selected.newMode ? `gray` : `blue`
    }-500 mt-4`}
    />
    ) : (
    <Plus
    className={`h-6 w-6 text-${
    selected.newMode ? `gray` : `blue`
    }-500 mt-4`}
    />
    )}
    <h3
    className={`grid-cols-1 justify-start flex text-lg font-medium py-4 text-${
    selected.newMode ? `gray` : `blue`
    }-500`}
    >
    New Bundle
    </h3>
    </div>
    ) : null}
    </div>
    {(selected.editMode || selected.newMode) && user ? (
    <NewEditItem type={ItemType.BundleType} />
    ) : null}
    <ItemList
    type={ItemType.BundleType}
    useSelected={true}
    allowEdits={true}
    selected={selected}
    setSelected={setSelected}
    />
    </Layout>
    )
    }
    export default BundlesPage

    pages/feeds.tsx

    tsx

    import { ItemList } from '../components/itemList'
    import { NewEditItem } from '../components/newEditItem'
    import { Layout } from '../components/layout'
    import { ItemType, SelectedFeedState } from '../utils/types'
    import { useFetchUser } from '../utils/user'
    import { useState } from 'react'
    import { Minus, Plus } from '../components/svg'
    const FeedsPage = () => {
    const { user, loading } = useFetchUser()
    const initialSelected: SelectedFeedState = {
    id: null,
    feeds: [],
    editMode: false,
    newMode: false,
    }
    const [selected, setSelected] = useState(initialSelected)
    return (
    <Layout>
    <div className="grid grid-cols-2">
    <h3 className="grid-cols-1 justify-start flex text-lg font-medium py-4">
    Feeds Page
    </h3>
    {user ? (
    <div
    onClick={e => {
    e.persist()
    setSelected(currState => ({
    ...currState,
    newMode: !currState.newMode,
    editMode: false,
    }))
    }}
    className="flex grid-cols-1 justify-end"
    >
    {selected.newMode ? (
    <Minus
    className={`h-6 w-6 text-${
    selected.newMode ? `gray` : `blue`
    }-500 mt-4`}
    />
    ) : (
    <Plus
    className={`h-6 w-6 text-${
    selected.newMode ? `gray` : `blue`
    }-500 mt-4`}
    />
    )}
    <h3
    className={`grid-cols-1 justify-start flex text-lg font-medium py-4 text-${
    selected.newMode ? `gray` : `blue`
    }-500`}
    >
    New Feed
    </h3>
    </div>
    ) : null}
    </div>
    {(selected.editMode || selected.newMode) && user ? (
    <NewEditItem type={ItemType.FeedType} />
    ) : null}
    <ItemList
    type={ItemType.FeedType}
    useSelected={true}
    allowEdits={true}
    selected={selected}
    setSelected={setSelected}
    />
    </Layout>
    )
    }
    export default FeedsPage

    We also need to add + or - svg in our svg.tsx file:

    components/svg.tsx

    tsx

    // other svg components above
    export const Plus = ({ className }) => (
    <svg
    xmlns="http://www.w3.org/2000/svg"
    fill="none"
    viewBox="0 0 24 24"
    stroke="currentColor"
    className={className}
    >
    <path
    strokeLinecap="round"
    strokeLinejoin="round"
    strokeWidth={2}
    d="M12 6v6m0 0v6m0-6h6m-6 0H6"
    />
    </svg>
    )
    export const Minus = ({ className }) => (
    <svg
    xmlns="http://www.w3.org/2000/svg"
    fill="none"
    viewBox="0 0 24 24"
    stroke="currentColor"
    className={className}
    >
    <path
    strokeLinecap="round"
    strokeLinejoin="round"
    strokeWidth={2}
    d="M20 12H4"
    />
    </svg>
    )

    Now test it out and make sure it works- when you click it should pop open the new item box with a submit button and a bunch of stubbed out areas that we will add fields to. The first one is the <GenerateInputField> component.

    components/generateInputField.tsx

    tsx

    import { Dispatch, SetStateAction } from 'react'
    import { BundleState, FeedState } from '../utils/types'
    export const GenerateInputField = ({
    currentItem,
    name,
    changeHandler,
    }: {
    name: string
    currentItem: FeedState | BundleState
    changeHandler: Dispatch<SetStateAction<FeedState | BundleState>>
    }) => (
    <div className="py-2">
    <label className="block py-2">
    {name.charAt(0).toUpperCase() + name.slice(1)}:
    </label>
    <input
    className="border-4 rounded w-full py-2 px-3"
    value={currentItem[name]}
    onChange={e => {
    e.persist()
    changeHandler(curr => ({ ...curr, [name]: e.target.value }))
    }}
    />
    </div>
    )

    This component takes a name component which will be the title of the property that it is in charge of, the currentItem state that controls the entire form and the changeHandler which is in charge of updating the field. We display a div that has a label and input in it. When the input changes we will call the change handler and selectively update that particular field while leaving everything else alone.

    Now we can update the inputFields.map to include this <GenerateInputField> component that we just created:

    components/newEditItem.tsx

    tsx

    import { GenerateInputField } from './generateInputField'
    // more of the component here
    {
    inputFields.map(name => (
    <GenerateInputField
    key={`${type}-${name}`}
    currentItem={currentItem}
    name={name}
    changeHandler={setItem}
    />
    ))
    }
    // the rest of the component here

    Now when we open up the new item section we will see fields for the name and either description or url. Try it out! It should actually work- as we type the new contents of the field will get saved to the local state on the page.

    The missing piece before we add our create mutation to the mix is to get our tags and feeds under control, and we will do that next.