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: ItemTypeitem: FeedObject | BundleObject}) => {const isFeed = type === ItemType.FeedTypeconst { 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><divclassName={`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.FeedTypeconst { loading, error, data } = useQuery(isFeed ? FEEDS_QUERY : BUNDLES_QUERY)const { feeds, bundles } = data || {}const itemList = isFeed ? feeds : bundlesif (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><ItemListtype={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 typesexport type SelectedFeedState = {id: stringfeeds: Feed[]editMode: booleannewMode: 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: ItemTypeselected?: SelectedFeedStatesetSelected?: Dispatch<SetStateAction<SelectedFeedState>>useSelected?: booleanallowEdits?: boolean}) => {const isFeed = type === ItemType.FeedTypeconst { loading, error, data } = useQuery(isFeed ? FEEDS_QUERY : BUNDLES_QUERY)const { feeds, bundles } = data || {}const itemList = isFeed ? feeds : bundlesif (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) => (<OneListItemitem={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: ItemTypeitem: FeedObject | BundleObjectselected?: SelectedFeedStatesetSelected?: Dispatch<SetStateAction<SelectedFeedState>>useSelected?: booleanallowEdits?: boolean}) => {const isFeed = type === ItemType.FeedTypeconst isSelected = useSelected && selected && selected.id === item.idconst { 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><divclassName={`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 ? (<ponClick={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>) : (<ponClick={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 svgsexport const DoubleArrowDown = ({ className }) => (<svgxmlns="http://www.w3.org/2000/svg"fill="none"viewBox="0 0 24 24"stroke="currentColor"className={className}><pathstrokeLinecap="round"strokeLinejoin="round"strokeWidth={2}d="M19 13l-7 7-7-7m14-8l-7 7-7-7"/></svg>)export const DoubleArrowRight = ({ className }) => (<svgxmlns="http://www.w3.org/2000/svg"fill="none"viewBox="0 0 24 24"stroke="currentColor"className={className}><pathstrokeLinecap="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 aboveconst itemList = isFeed ? feeds : bundlesuseEffect(() => {;(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.