For this experience, you will add the ability to save timestamped notes that document conversations you’ve had with your contacts. When finished, each Contact card will provide the ability to add a Note, and display all the Notes associated with this contact:
The conceptual goal of this experience is to learn how to design and implement multiple, inter-related tables. In essence, this experience illustrates the creation of a simple data model.
Let’s discuss how to implement a set of one or more Notes, each associated with a specific Contact. Your first instinct might be to expand the Contacts table with a Notes field that contains an array of objects. This would work, but becomes unwieldy as soon as you want to expand the system to support more sophisticated operations (such as, for example, listing all notes across all contacts in chronological order).
The better way to implement Notes functionality is to create a new table where each row contains a single Note, and where each row also provides fields to indicate which Contact (and Owner) this Note belongs to. In database terms, these fields are known as foreign keys. Once you get the hang of it, it is simple and fast to implement separate tables for each entity in your system along with foreign keys to indicate their interdependencies.
To make it clear how this works, let’s look at the tables we have so far. Here is a representation of the User table:
Each user actually has two unique IDs: the id field, which is generated by Postgres, and the email field, which is guaranteed to be unique by the schema.prisma
.
model User {
id Int @id @default(autoincrement())
email String @unique
password String
role Role @default(USER)
}
Let’s now look at the Contact table:
As with all Postgres tables, the id field contains a unique ID for each Contact row. In addition, the owner field must contain an email from the Users table. We could have used the id value for a User just as easily.
And now let’s look at the way we’ll implement the Notes table. Remember that a given Contact can have multiple Notes associated with it, and each Note is associated with exactly one Contact.
As always, the id field is a unique ID for each Note. The note and createdAt fields provide the contents of the note and the time it was created. The contactId field contains the id value of the Contact row the Note is associated with. So, the first Note in this table is associated with the Contact “Philip Johnson”, the second with “Kim Binsted” and the last Note is associated with the Contact “Scott Robertson”.
You might wonder why we also provide an owner field with each Note row. We could infer the owner from the contactId, but if we add the owner field explicitly, it makes it trivial to create a query for Notes associated with the current user (just the same way we do it for Contacts). For those of you with a database background, this is an example of denormalization.
When you are designing your own data models, it can be helpful to create a set of tables like this that indicate the fields associated with each table and how you will connect them together. It only takes a few minutes and can make your implementation process much easier.
First, I suggest you watch my screencast before trying this WOD for the first time. I have not been able to find suitable resources for you to read prior to doing this WOD to cover all of the nextjs concepts I will cover. So, it’s best for you to watch the solution one time through to orient yourself.
At the conclusion of the last WOD, your contact forms mockup was in a branch called contact-forms-1 (and/or contact-forms-2).
Quit Next.js if it is running.
Now switch to the main branch in GitHub Desktop, and merge one of your contact-forms branches into it. Push your main branch to GitHub after the merge completes.
Start Next.js using npm run dev
, and check http://localhost:3000 (and the console) to ensure that the merge worked correctly.
Pro Tip: run your app within the Terminal window in VSCode, and keep Chrome Dev Tools open during development.
Now you’re ready to do this practice WOD. There are two rounds to this WOD, each timed individually.
Start your timer.
1. Create a branch to hold your work.
Create a branch called notes-1 using GitHub Desktop, and publish this branch to GitHub.
If necessary, start up your application using npm run dev
. Check to see that it’s running at http://localhost:3000. Take a look at the console to be sure there are no errors.
2. Create the Note table.
Update the prisma/schema.prisma
file to include a model for the Note. The model should specify the fields “note”, “contactId”, and “owner” as String, and createdAt as a Date. Also update the Contact model to include a field called “notes” that is an array of Note objects. Here’s an example of what the schema might look like:
model Note {
id Int @id @default(autoincrement())
contactId Int
note String
owner String
createdAt DateTime @default(now())
Contact Contact @relation(fields: [contactId], references: [id])
}
model Contact {
id Int @id @default(autoincrement())
firstName String
lastName String
address String
image String
description String
notes Note[]
owner String
}
Run npx prisma migrate dev --name note
to create the migration and apply it.
3. Create the NoteItem Component.
Make a copy of src/app/components/ContactCard.jsx
called NoteItem.jsx
. This will display each Note. One way to render a Note is by using the Bootstrap ListGroup Component, where each NoteItem is
a ListGroup.Item. Here’s some code you can use:
<ListGroup.Item>
<p className="fw-lighter">{note.createdAt.toLocaleDateString('en-US')}</p>
<p>{note.note}</p>
</ListGroup.Item>
This code requires that each NoteItem component is passed a prisma Note as a parameter called note.
4. Update ContactCard and ContactCardAdmin to have a second parameter notes
.
We need to pass in all the notes associated with the contact to the ContactCard and ContactCardAdmin components. Update the ContactCard and ContactCardAdmin components to accept a second parameter called notes. This parameter should be an array of Note objects. You can do this by changing the parameter list of the ContactCard and ContactCardAdmin functions to include notes. Here’s an example of what the function signature might look like:
const ContactCard = ({ contact, notes }: { contact: Contact; notes: Note[] }) => {
You need to display all the Notes associated with a Contact. Edit ContactCard.tsx
and ContactCardAdmin.tsx
to include the following section at the bottom of each Card above the Edit link:
<ListGroup variant="flush">
{notes.map((note) => <NoteItem key={note.id} note={note}/>)}
</ListGroup>
You’ll need to import ListGroup from react-bootstrap.
5. Update src/app/list/page.tsx
to pass each Contact its associated Notes.
Now, let’s edit src/app/list/page.tsx
to pass each Contact its Notes. This involves:
prisma
to get all the Notes associated with the current user.ContactCard
or ContactCardAdmin
.In ListContacts, you’ll need to update the call to the Contact component to pass in the associated notes. Here’s one way to do it:
<ContactCard key={index}
contact={contact}
notes={notes.filter(note => (note.contactId === contact.id))}/>
The idea is to take notes, which contains all the Notes for all the contacts associated with this user, and filter that list down to only those Notes where the Note’s contactId matches this contact’s ID.
If you’ve implemented everything correctly so far, you should be able to display the ListContacts page without any errors in the page or console. You won’t have any Notes, because we haven’t defined any yet, but the page should look like this:
You can see that at the bottom of the contact, below the Edit link, there is new section about 5px high that is empty whitespace. That’s the area containing the (empty) ListGroup.
6. Implement AddNote.
First, edit src/lib/dbActions.ts
to include a function called addNote
that takes a Note object as a parameter and adds it to the Notes table. You can use the prisma.note.create
function to do this. Here’s an example of what the function might look like:
export async function addNote(note: { note: string; contactId: number, owner: string }) {
await prisma.note.create({
data: {
note: note.note,
contactId: note.contactId,
owner: note.owner,
},
});
redirect('/list');
}
Second, edit src/lib/validationSchemas.ts
to include a schema for adding a Note. This schema should contain a single field for the note. Here’s an example of what the schema might look like:
export const AddNoteSchema = Yup.object({
note: Yup.string().required(),
contactId: Yup.number().required(),
owner: Yup.string().required(),
});
Third, create a copy of src/components/AddContactForm.tsx
called AddNoteForm.tsx
. This component requires the contactId
. This form should contain a single field for the note, and a submit button. The form should also contain hidden fields for the owner and contactId. The owner should be the email of the current user, and the contactId should be the id of the current contact. The user only fills in the note field. All the other fields are provided automatically.
7. Add AddNoteForm to ContactCard.
Now let’s add a call to AddNoteForm in our ContactCard component.
<ListGroup varient="flush">
{notes.map((note: Note) => (
<NoteItem key={note.id} note={note} />
))}
</ListGroup>
<AddNoteForm contactId={contact.id} />
Note that we pass the contactId into the AddNoteForm component.
If you’ve implemented this correctly, then the Contact component should now look like this:
And after adding a note and pressing submit, you should see something like this:
Note that your Note is only added to a single Contact.
8. Check for ESLint errors.
As a last step, run npm run lint
to ensure that there are no ESLint errors anywhere in your application.
9. Commit your finished work.
When your page renders correctly,
To ensure that you understand this material, you must do this WOD a second time.
Switch back to the main branch. This will revert your local repo to its state just before starting the WOD.
Create a new branch called notes-2.
Go through the WOD again. When you’re done, commit and push your notes-2 branch to GitHub with the message ‘notes-2 finished in NN minutes.’, where NN is the number of minutes it took you to do it according to your timer.
Your TA will check whether both branches are in GitHub.
Rx: <32 min Av: 32-42 min Sd: 42-50 min DNF: 50+ min
Here’s a screencast of me working through this problem. You can watch this prior to attempting it for the first time yourself.
By the time and date indicated in Laulima, submit this assignment via Laulima.
You must grant read access to this repo to the TA for your section. To do this:
Retrieve your repository in a browser, then click on “Settings”. Depending upon the GitHub UI you are provided, you’ll then click on either “Collaborators” or “Manage Access”. Let us know if you can’t find either of these.
Your submission should contain:
You will receive full credit for this practice WOD as long as you have attempted it the required number of times and submitted the email with all required data before the due date. Your code does not have to run perfectly for you to receive full credit. However, if you do not repeat each practice WOD until you can finish it successfully in at least AV time, you are unlikely to do well on the in-class WOD. To reduce the stress associated with this course, I recommend that you repeat each practice WOD as many times as necessary to achieve at least AV before its due date.