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.


    19. Add OneListItem component

    Objective: Create a component for nicely displaying a single article.

    In this step we will be creating the <OneListItem> componet which will be a card that displays information either about the feed or the bundle. Just like with the <ItemList> component, we will make this flexible enough to handle either a bundle or feed.

    In order to give the full effect of the component, we will stub out some of the components that we will be adding later as paragraph tags with text.

    components/oneListItem.tsx

    tsx

    import Link from 'next/link'
    import { BundleObject, FeedObject, ItemType } from '../utils/types'
    import { useFetchUser } from '../utils/user'
    import { WaitingClock } from './svg'
    export const OneListItem = ({
    item,
    type,
    }: {
    type: ItemType
    item: FeedObject | BundleObject
    }) => {
    const isFeed = type === ItemType.FeedType
    const { user, loading } = useFetchUser()
    if (loading) {
    return <WaitingClock className="h-10 w-10 text-gray-500 m-auto" />
    }
    return (
    <Link href={`/${isFeed ? `feed` : `bundle`}/${item.id}`}>
    <div>
    <div
    className={`cursor-pointer grid grid-cols-6 p-4 rounded-lg border-b-4 border-t-4 border-l-4 border-r-4 ${`border-${
    isFeed ? 'green' : 'purple'
    }-400`}`}
    >
    <div className="col-span-4">
    <h4 className="font-bold">{item.name}</h4>
    {!isFeed ? <p>{item['description']}</p> : null}
    </div>
    <div className="col-span-2 flex justify-end">
    <p>actions</p>
    </div>
    <div className="flex col-span-6 py-0 space-x-2">
    {item.author ? <p>profile pic</p> : null}
    </div>
    <div className="col-span-6 py-2">
    <h3>Tags</h3>
    <div className="grid grid-cols-3 gap-2">
    <p>tags...</p>
    </div>
    </div>
    <div className="col-span-6 py-2">
    <h3>{isFeed ? 'Bundles' : 'Feeds'}</h3>
    <div className="grid grid-cols-3 gap-2">
    <p>child items...</p>
    </div>
    </div>
    </div>
    </div>
    </Link>
    )
    }

    Now let's use it in our <ItemList> component.

    components/itemList.tsx

    tsx

    import { useQuery } from '@apollo/client'
    import { BUNDLES_QUERY, FEEDS_QUERY } from '../utils/api/graphql/queries'
    import { BundleObject, FeedObject, ItemType } from '../utils/types'
    import { NotifyError } from './notifyError'
    import { NotifyLoading } from './notifyLoading'
    import { OneListItem } from './oneListItem'
    export const ItemList = ({ type }: { type: ItemType }) => {
    const isFeed = type === ItemType.FeedType
    const { loading, error, data } = useQuery(
    isFeed ? FEEDS_QUERY : BUNDLES_QUERY
    )
    const { feeds, bundles } = data || {}
    const itemList = isFeed ? feeds : bundles
    if (loading) {
    return <NotifyLoading />
    }
    if (error || !itemList) {
    return <NotifyError />
    }
    return (
    <>
    <div className="grid lg:grid-cols-3 md:grid-cols-2 gap-4">
    {itemList.length > 0 ? (
    itemList.map((item: FeedObject | BundleObject) => (
    <OneListItem item={item} type={type} key={item.id} />
    ))
    ) : (
    <p>None are present. Why not add one?</p>
    )}
    </div>
    </>
    )
    }

    Now that we have our item card working, let's add functionality so we can select one of them. We will use this to determine which set of articles get loaded and also it will allow us to edit or delete the currently selected one. We will be storing that state within the pages/index/tsx component:

    pages/index.tsx

    tsx

    import { useState } from 'react'
    import { ItemList } from '../components/itemList'
    import { Layout } from '../components/layout'
    import { ItemType, SelectedFeedState } from '../utils/types'
    const IndexPage = () => {
    const initialSelected: SelectedFeedState = {
    id: null,
    feeds: [],
    editMode: false,
    newMode: false,
    }
    const [selected, setSelected] = useState(initialSelected)
    return (
    <Layout>
    <h3 className="grid-cols-1 justify-start flex text-lg font-medium py-4">
    Home Page
    </h3>
    <ItemList
    type={ItemType.BundleType}
    useSelected={true}
    selected={selected}
    setSelected={setSelected}
    />
    </Layout>
    )
    }
    export default IndexPage

    We are creating an object that will store an id of the item, a list of feeds (which will either be the feed itself if it is a feed or if it is a bundle it will be the list of feeds that belong to it. Finally, there are two booleans that we will later use to turn on new/edit modes when we get to that point.

    Let's turn to the types file and add the SelectedFeedState that we are now importing:

    utils/types.ts

    ts

    import { Feed } from '@prisma/client'
    // other types
    export type SelectedFeedState = {
    id: string
    feeds: Feed[]
    editMode: boolean
    newMode: boolean
    }

    Next, we need to update our <ItemList> to add three new props useSelected and selected and setSelected. The useSelected is a boolean which we will use to turn on or off whether we want this selectibility between other items in the itemList component. This will be nice because on the feed or bundle detail, we will want to use this oneListItem component but not have it in a list, so we have to disable the functionality to able to select a particular item. The selected and setSelected are just the current state and function to set the state.

    components/oneListItem.tsx

    tsx

    import { useQuery } from '@apollo/client'
    import { Dispatch, SetStateAction } from 'react'
    import { BUNDLES_QUERY, FEEDS_QUERY } from '../utils/api/graphql/queries'
    import {
    BundleObject,
    FeedObject,
    ItemType,
    SelectedFeedState,
    } from '../utils/types'
    import { NotifyError } from './notifyError'
    import { NotifyLoading } from './notifyLoading'
    import { OneListItem } from './oneListItem'
    export const ItemList = ({
    type,
    selected,
    setSelected,
    useSelected = false,
    allowEdits = false,
    }: {
    type: ItemType
    selected?: SelectedFeedState
    setSelected?: Dispatch<SetStateAction<SelectedFeedState>>
    useSelected?: boolean
    allowEdits?: boolean
    }) => {
    const isFeed = type === ItemType.FeedType
    const { loading, error, data } = useQuery(
    isFeed ? FEEDS_QUERY : BUNDLES_QUERY
    )
    const { feeds, bundles } = data || {}
    const itemList = isFeed ? feeds : bundles
    if (loading) {
    return <NotifyLoading />
    }
    if (error || !itemList) {
    return <NotifyError />
    }
    return (
    <>
    <div className="grid lg:grid-cols-3 md:grid-cols-2 gap-4">
    {itemList.length > 0 ? (
    itemList.map((item: FeedObject | BundleObject) => (
    <OneListItem
    item={item}
    type={type}
    key={item.id}
    useSelected={useSelected}
    allowEdits={allowEdits}
    selected={selected}
    setSelected={setSelected}
    />
    ))
    ) : (
    <p>None are present. Why not add one?</p>
    )}
    </div>
    </>
    )
    }

    In addition to the 3 props that we just mentioned adding, we will also add a allowEdits prop which we will default to false along with useSelected.

    Now, we need to go into the <OneListItem> component and add the new props that we are passing into it. We will also add a new div at the bottom of the list item that will either show a double arrow right or down depending on whether it is selected. On clicking if it isn't selected, it will set the selected item as the one that was selected and we change the border of the list item to go from gray to either purple or green depending on whether it is a bundle or feed, respectively.

    components/oneListItem.tsx

    tsx

    import Link from 'next/link'
    import {
    BundleObject,
    FeedObject,
    ItemType,
    SelectedFeedState,
    } from '../utils/types'
    import { useFetchUser } from '../utils/user'
    import { Dispatch, SetStateAction } from 'react'
    import { DoubleArrowDown, DoubleArrowRight, WaitingClock } from './svg'
    export const OneListItem = ({
    item,
    type,
    selected,
    setSelected,
    useSelected = false,
    allowEdits = false,
    }: {
    type: ItemType
    item: FeedObject | BundleObject
    selected?: SelectedFeedState
    setSelected?: Dispatch<SetStateAction<SelectedFeedState>>
    useSelected?: boolean
    allowEdits?: boolean
    }) => {
    const isFeed = type === ItemType.FeedType
    const isSelected = useSelected && selected && selected.id === item.id
    const { user, loading } = useFetchUser()
    if (loading) {
    return <WaitingClock className="h-10 w-10 text-gray-500 m-auto" />
    }
    return (
    <Link href={`/${isFeed ? `feed` : `bundle`}/${item.id}`}>
    <div>
    <div
    className={`cursor-pointer grid grid-cols-6 p-4 rounded-lg ${
    useSelected ? 'rounded-b-none' : 'border-b-4'
    } border-t-4 border-l-4 border-r-4 ${
    isSelected
    ? `border-${isFeed ? 'green' : 'purple'}-400`
    : `border-gray-300`
    }`}
    >
    <div className="col-span-4">
    <h4 className="font-bold">{item.name}</h4>
    {!isFeed ? <p>{item['description']}</p> : null}
    </div>
    <div className="col-span-2 flex justify-end">
    <p>actions</p>
    </div>
    <div className="flex col-span-6 py-0 space-x-2">
    {item.author ? <p>profile pic</p> : null}
    </div>
    <div className="col-span-6 py-2">
    <h3>Tags</h3>
    <div className="grid grid-cols-3 gap-2">
    <p>tags...</p>
    </div>
    </div>
    <div className="col-span-6 py-2">
    <h3>{isFeed ? 'Bundles' : 'Feeds'}</h3>
    <div className="grid grid-cols-3 gap-2">
    <p>child items...</p>
    </div>
    </div>
    </div>
    {useSelected ? (
    <>
    {isSelected ? (
    <p
    onClick={e => {
    e.preventDefault()
    }}
    className={`flex rounded-lg rounded-t-none align-middle
    ${
    isSelected
    ? `bg-${isFeed ? 'green' : 'purple'}-400`
    : `bg-gray-300`
    }
    p-4 z-10 text-white cursor-pointer`}
    >
    <DoubleArrowDown className="h-5 w-5 text-white-500 mr-2 mt-1" />
    {` Hide ${isFeed ? `Feed` : `Bundle`} Articles`}
    </p>
    ) : (
    <p
    onClick={e => {
    e.preventDefault()
    setSelected({
    id: item.id,
    feeds: isFeed ? [item] : item['feeds'],
    editMode: false,
    newMode: false,
    })
    }}
    className={`flex rounded-lg rounded-t-none align-middle
    ${
    isSelected
    ? `bg-${isFeed ? 'green' : 'purple'}-400`
    : `bg-gray-300`
    }
    p-4 z-10 text-white cursor-pointer`}
    >
    <DoubleArrowRight className="h-5 w-5 text-white-500 mr-2 mt-1" />
    {` Show ${isFeed ? `Feed` : `Bundle`} Articles`}
    </p>
    )}
    </>
    ) : null}
    </div>
    </Link>
    )
    }

    We also need to add the <DoubleArrowDown> and <DoubleArrowRight> to the list of svgs:

    components/svg.tsx

    tsx

    // other svgs
    export const DoubleArrowDown = ({ 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="M19 13l-7 7-7-7m14-8l-7 7-7-7"
    />
    </svg>
    )
    export const DoubleArrowRight = ({ 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="M13 5l7 7-7 7M5 5l7 7-7 7"
    />
    </svg>
    )

    If you try this component out, you will see that it mostly works. You can click from bundle to bundle and the outline should go from gray to purple. The only trouble here is that initially no items will be selected. We can fix this by adding an effect to the <ItemList> component which will automatically select one if there isn't one selected already.

    components/itemList.tsx

    tsx

    import { Dispatch, SetStateAction, useEffect } from 'react'
    // more code above
    const itemList = isFeed ? feeds : bundles
    useEffect(() => {
    ;(async () => {
    if (
    useSelected &&
    itemList &&
    itemList.length > 0 &&
    selected.id === null
    ) {
    const firstItem = itemList[0]
    await setSelected({
    id: firstItem.id,
    feeds: isFeed ? [firstItem] : firstItem['feeds'],
    editMode: false,
    newMode: false,
    })
    }
    })()
    })
    if (loading) {
    return <NotifyLoading />
    }
    // more code below

    How this effect works is that it will be called as props change and in the event that we are using this component in the mode where useSelected = true, there is an itemList that is greater than 0 and the selected id hasn't been set yet, take the first item in the list and set that to the selected item. The feeds array will either be the feed itself or the list of feeds in the case of a bundle.

    Now when we reload the page, we should see that the first item automatically gets selected, but we can still switch to others when we click them.