Appointment Planner

Hello everyone. I’ve finished this project and wanted to leave my solution hanging in case someone is having some problems!

App.js

import React from "react";
import { Switch, Route, Redirect, NavLink } from "react-router-dom";
import { useState } from 'react';
import { AppointmentsPage } from "./containers/appointmentsPage/AppointmentsPage";
import { ContactsPage } from "./containers/contactsPage/ContactsPage";

function App() {
  // state variables for contacts and appointments 
  const [contacts, setContacts] =  useState([]);
  const [appointments, setAppointments] = useState([]);

  const ROUTES = {
    CONTACTS: "/contacts",
    APPOINTMENTS: "/appointments",
  };

  // functions to add data to contacts and appointments
  const addContact = (name, phone, email ) => {
    setContacts( prev => [...prev, {name, phone, email}]);
  };
  const addAppointment = (title, contact, date, time) => {
    setAppointments( prev =>  [...prev, {title, contact, date, time}]);
  };

  return (
    <>
      <nav>
        <NavLink to={ROUTES.CONTACTS} activeClassName="active">
          Contacts
        </NavLink>
        <NavLink to={ROUTES.APPOINTMENTS} activeClassName="active">
          Appointments
        </NavLink>
      </nav>
      <main>
        <Switch>
          <Route exact path="/">
            <Redirect to={ROUTES.CONTACTS} />
          </Route>
          <Route path={ROUTES.CONTACTS}>
             {/* Add props to ContactsPage */}
            <ContactsPage 
              addContact={addContact}
              contacts={contacts}
              />
          </Route>
          <Route path={ROUTES.APPOINTMENTS}>
            {/* Add props to AppointmentsPage */}
            <AppointmentsPage 
              addAppointment={addAppointment}
              appointments={appointments}
              contacts={contacts}
              />
          </Route>
        </Switch>
      </main>
    </>
  );
}

export default App;

AppointmentsPage.js

import React from "react";
import { useState, useEffect } from 'react';
import { AppointmentForm } from '../../components/appointmentForm/AppointmentForm';
import { TileList } from '../../components/tileList/TileList';

export const AppointmentsPage = (props) => {
  // Define state variables for appointment info
  const appointments = props.appointments;
  const contacts = props.contacts;
  const addAppointment = props.addAppointment;
  // local States
  const [title, setTitle] = useState('');
  const [contact, setContact] = useState('');
  const [date, setDate] = useState('');
  const [time, setTime] = useState('');

  const handleSubmit = (e) => {
    e.preventDefault();
    // Add contact info and clear data 
    addAppointment(title, contact, date, time);
    // reseting values
    setTitle('');
    setContact('');
    setDate('');
    setTime('');
};

  return (
    <div>
      <section>
        <h2>Add Appointment</h2>
        <AppointmentForm 
          title={title}
          setTitle={setTitle}
          contact={contact}
          setContact={setContact}
          date={date}
          setDate={setDate}
          time={time}
          setTime={setTime}
          handleSubmit={handleSubmit}
          contacts={contacts}
        />
      </section>
      <hr />
      <section>
        <h2>Appointments</h2>
        <TileList objArr={appointments}/>
      </section>
    </div>
  );
};

AppointmentForm.js

import React from "react";
import {ContactPicker} from '../contactPicker/ContactPicker';
export const AppointmentForm = ({
  contacts,
  title,
  setTitle,
  contact,
  setContact,
  date,
  setDate,
  time,
  setTime,
  handleSubmit
}) => {
  const getTodayString = () => {
    const [month, day, year] = new Date()
      .toLocaleDateString("en-US")
      .split("/");
    return `${year}-${month.padStart(2, "0")}-${day.padStart(2, "0")}`;
  };

  return (
    <form onSubmit={handleSubmit}>
      <input 
        type="text"
        value={title}
        onChange={({target}) => setTitle(target.value)}
      />
      <input 
        type="date"
        value={date}
        min={getTodayString()}
        onChange={({target}) => setDate(target.value)}
      />
      <input 
        type="time"
        value={time}
        onChange={({target}) => setTime(target.value)}
      />
      <ContactPicker 
        contacts={contacts}
        value={contact}
        onChange={({target}) => setContact(target.value)}
      />
      <input type="submit"/>
    </form>
  );
};

ContactsPage.js

import React from "react";
import { useState, useEffect } from 'react';
import { ContactForm } from '../../components/contactForm/ContactForm';
import { TileList } from '../../components/tileList/TileList';

export const ContactsPage = (props) => {
  // Define state variables for contact info and duplicate check
  const contacts = props.contacts;
  const addContact = props.addContact;
  // local states
  const [name, setName] = useState('');
  const [phone, setPhone] = useState('');
  const [email, setEmail] = useState('');
  const [duplicate, setDuplicate] = useState(false);

  const handleSubmit = (e) => {
    e.preventDefault();
    // Add contact info and clear data if the contact name is not a duplicate
    if (!duplicate) {

      addContact(name,phone, email);
      // reseting values
      setName('');
      setPhone('');
      setEmail('');
    }
  };


  // check for contact name in the contacts array variable in props
  useEffect( () => {
    for (const contact of contacts) {
      if (name === contact.name) {
        setDuplicate(true);
      }

      return;
    }
  });

  return (
    <div>
      <section>
        <h2>Add Contact</h2>
        <ContactForm 
          name={name}
          phone={phone}
          email={email}
          setName={setName}
          setPhone={setPhone}
          setEmail={setEmail}
          handleSubmit={handleSubmit}
        />
      </section>
      <hr />
      <section>
        <h2>Contacts</h2>
        <TileList
          objArr={props.contacts}
        />
      </section>
    </div>
  );
};

ContactForm.js

import React from "react";

export const ContactForm = ({
  name,
  setName,
  phone,
  setPhone,
  email,
  setEmail,
  handleSubmit
}) => {
  return (
    <form onSubmit={handleSubmit}>
      <input 
        type="text" 
        value={name}
        onChange={({target}) => {setName(target.value)}}
        required
      />
      <input 
        type="tel"
        value={phone}
        pattern="(^\+[0-9]{2}|^\+[0-9]{2}\(0\)|^\(\+[0-9]{2}\)\(0\)|^00[0-9]{2}|^0)([0-9]{9}$|[0-9\-\s]{10}$)"
        onChange={({target}) => {setPhone(target.value)}}
        required
      />
      <input 
        type="email"
        value={email}
        onChange={({target}) => {setEmail(target.value)}}
        required
      />
      <input
        type="submit"
      />
    </form>
  );
};

ContactPicker.js

import React from "react";

export const ContactPicker = (props) => {
  const contacts = props.contacts;
  const onChange = props.onChange;
  
  return (
    <select onChange={onChange}>
      <option value="">Select a contact</option>
      {contacts.map( contact => <option value={contact.name}>{contact.name}</option>)}
    </select>
  
  );
};

TileList.js

import React from "react";
import { Tile } from '../tile/Tile';


export const TileList = ({objArr}) => {
  
  return (
    <div>
      {objArr.map( (value, index) => <Tile value={value} key={index}/>)}
    </div>
  );
};

Tile.js

import React from "react";

export const Tile = ({value}) => {
  const array = Object.values(value);
  return (
    <div className="tile-container">
      {array.map( (data, index) => {
        if (index === 0 ) {
          return <p className="tile-title" key={index}>{data}</p>;
        }
          return <p className="tile" key={index}>{data}</p>;
        })
      }
    </div>
  );
};

Good coding!

7 Likes

thank you, I got stuck it how to make TileList render both appointments and contacts, you saved my life!

2 Likes

Couldn’t have done it without your help on it also!

Hello everyone,
Here is my completed appointment planner app, I customized it a little.
Any feedback is greatly appriciated!
appointment-planner

Howdy!

Here is my appointment planner build.
https://gist.github.com/2862558287fffd83c10243244529b4aa.

I customized the forms a wee bit, making each input to have a designated label so I could utilize default values in the form.

Any feedback is welcomed! If you want to know why I did somethings a certain way, feel free to ask and I will try my best to answer.

2 Likes

I wrote down my methods and thought process in completing this project - I learned A TON from therealguah and I sincerely appreciate their hard work in helping me to deeply understand this project.

My Git project

If you find this useful, please STAR my project (and give me a follow on Git :grimacing:) as I’d really appreciate it.

Guide to Appointment Planner

Download the template folder and run the project locally. Run npm install, npm audit fix, and finally npm start on the project’s root directory.

1a Run npm install in terminal for the project’s root folder.

npm install

1b. Now run npm audit fix as recommended.

npm audit fix

1c. Now we can finally run npm start

npm start

In App.js

2a. “Keep track of the contacts and appointments data, each being an array of objects”
( Define state variables for contacts and appointments )
In App.js, setState for contacts and appointments with the following:

const [contacts, setContacts] = useState([]);

You will see the warning in your browser, at localhost, that ‘useState’ is not defined.
At the top of App.js, insert the following:

import React, { useState } from "react";

Now, the page will load up properly again. For appointments, we can setState with the following:

const [appointments, setAppointments] = useState([]);

2b. “Define a callback function that, given a name, phone number, and email, ADDS a new contact object with that data to the array of contacts”
Note: I am adding one individual contact and one individual appointment to the contacts and appointments array. In the App.js file, add the following function in the App component.

const addContact = (name, phone, email ) => {
  setContacts( prev => [...prev, {name, phone, email}]);
};

2c. “Define a callback function that, given a title, contact, date, and time, adds a new appointment object with that data to the array of appointments.”
Note: I am adding one individual appointment with my callback function. In the App.js file, beneath the previous function, add the following function

const addAppointment = (title, contact, date, time) => {
  setAppointments( prev => [...prev, {title, contact, date, time}]);
}

2d. “Pass the array of contacts and the appropriate callback function as props to the ContactsPage component”
“Array of contacts” is:

contacts={contacts}

“Appropriate callback function” is:

addContact={addContact}

In App.js, add the following properties in App.js’s ContactsPage component (underneath the {/* Add props to ContactsPage */} comment):

<ContactsPage 
  contacts={contacts}
  addContact={addContact}
/>

2e. “Pass the appointments array, contacts array, and the add appointment function as props to the AppointmentsPage component”
In App.js, add the following properties in App.js’s AppointmentsPage component (underneath the {/* Add props to AppointmentsPage */} comment):

<AppointmentsPage
  addAppointment={addAppointment}
  appointments={appointments}
  contacts={contacts}
/>

Based on the given requirements, implement ContactsPage.js as a stateful component to handle the logic for adding new contacts and listing current contacts. ContactsPage.js

3a. "Receive two props:

  • The current list of contacts
  • A callback function for adding a new contact
    Note: In ContactsPage.js, inside the ContactsPage component, add the following:
  // Current list of contacts
  const contacts = props.contacts;
  // Callback function for adding new contact
  const addContact = props.addContact;

Now, the page will not currently load - the error message of ‘props is not defined’ can be fixed by adding props as an input to the ContactsPage component. Change the function definition as follows:

  export const ContactsPage = (props) => {
    ...
  }

The page should load properly again.

3b. “Keep track of three local state values: the current name, phone, and email entered into the form”
Inside the ContactsPage.js ContactsPage component, add the following:

  const [name, setName] = useState('');

The error message from the inability to load can be cleared with the following:

  import React, { useState } from 'react';

The page should now load properly again.
Now add the following below our definition of name state variables (for phone and email):

  const [phone, setPhone] = useState('');
  const [email, setEmail] = useState('');

3c. “Check for duplicates whenever the name in the form changes and indicate the name is a duplicate”
Define state variables for duplicate check (with a default value of false) to the following:

  const [duplicate, setDuplicate] = useState(false);

Checking if name matches contacts.name - if it does match, set setDuplicate state to true.
/* Using hooks, check for contact name in the contacts array variable in props */
Note: I will be using the useEffect hook here so make sure to import it at the top of the file, next to useState if preferred.

  // check for contact name in the contacts array variable in props
  useEffect( () => {
    for (const contact of contacts) {
      if (name === contact.name) {
        setDuplicate(true);
      }
      return;
    }
  });

3d. “Only add a new contact on form submission if it does not duplicate an existing contact’s name”
Now inside the handleSubmit handler, check for duplicate, then add the contact information if it is not a duplicate.

const handleSubmit = (e) => {
  e.preventDefault();
  /*
  Add contact info and clear data
  if the contact name is not a duplicate
  */
  if (!duplicate) {
    addContact(name, phone, email);
    // "A successful submission should clear the form"
    setName('');
    setPhone('');
    setEmail('');
  }
};

3e. "In the Add Contact section, render a ContactForm with the following passed via props:

  • local state variables
  • local state variable setter functions
  • handleSubmit callback function
    Import the ContactForm component with the following:
import { ContactForm } from '../../components/contactForm/ContactForm';

Now, under the h2 Add Contact, import the ContactForm component with the following props:

<ContactForm
  name={name}
  setName={setName}
  phone={phone}
  setPhone={setPhone}
  email={email}
  setEmail={setEmail}
  handleSubmit={handleSubmit}
/>

3f. “In the Contacts section, render a TileList with the contact array passed via props”
Note: Under h2 Contacts, add the TileList component with the prop key to “data”. My first attempt at this was to
use the property key of contacts={contacts}. However, TileList is a shared component between Appointments and Contacts so having
the property set to contacts won’t make sense later on (when I try to implement appointments into the TileList component). My suggestion
is to first try contacts={contacts} and then it will make sense why I changed it to data={contacts} below.

<TileList data={contacts} />

Import TileList component with the following:

import { TileList } from '../../components/tileList/TileList.js';

Implement ContactForm as a stateless component that renders a web form to collect the necessary contact information. ContactForm.js

4a. Render a form with the onSubmit attribute set
In the return statement, add an HTML form tab.

<form onSubmit={handleSubmit}>

</form>

4b. Render a form with 3 controlled elements, one for each piece of contact data.
Inside the form tag, nest the following inputs:
Note: Try onChange={target => setXXXX(target.value)} and watch the code break. Then debug like I did. :crazy_face:

<form onSubmit={handleSubmit}>
  <input
    value={name}
    type="text"
    onChange={({target}) => setName(target.value)}
    required
  />
  <input
    value={phone}
    type="tel"
    // 4d. A pattern attribute to the phone <input> with a regex (USA)
    pattern="^(?:\(\d{3}\)|\d{3})[- ]?\d{3}[- ]?\d{4}$"
    onChange={({target}) => setPhone(target.value)}
    required
  />
  <input
    value={email}
    type="email"
    onChange={({target}) => setEmail(target.value)}
    required
  />
</form>

4c. Render a form with the a submit button
Inside the form, create a submit button.

  <input
    type="submit"
  />

Implement TileList as a stateless component that renders a list of Tile components using an array of objects. TileList.js

5a. Receive one prop:
An array of objects to render as a list.
Note: Try export const TileList = (data) => {

} without the curly braces wrapped around data. Then debug later. Remember, on the first go around, I used contacts={contacts} here so entering contacts, instead of data, would be the first attempt.

export const TileList = ({data}) => {
  ...
};

5b. Use the array passed via props to iteratively render Tile components, passing each object (Individual contact object) in the array (objectsInArray aka App.js’s contacts array) as a prop to each rendered Tile component.

export const TileList = ({data}) => {
  {data.map( (index, value) => <Tile key={index} value={value} /> )}
};

Note: The requirements for the TileList component are generalized and allow it to be shared by the ContactsPage and AppointmentsPage components. As long as an array of objects with either the contact data or appointments data is passed then the content will be handled appropriately.

Implement Tile as a stateless component that renders the data from an object. Tile.js

6a. In Tile.js, receive one prop: An object.
Note: Take a look at TileList.js’s component. That’s where the input value comes from.

export const Tile = ({value}) => {
  return (
    <div className="tile-container">
      ...  
    </div>
  );
};

6b. Iterate over the values in the object, passed in via props, and render a

element for each value
Note: valueInObject is the singular of valuesInObject here. I used the naming method to match the prompt given above.

export const Tile = ({value}) => {
  const valuesInObject = Object.values(value);
  return (
    <div className="tile-container">
      // Implement Tile as a stateless component that renders the "data" from an object
      {valuesInObject.map( (valueInObject, index) => {
        // Render a <p> element for each value
        return <p key={index}> {valueInObject} </p>
      })}
    </div>
  )
};

6c. Give a className of “tile-title” to the first

element
Inside tile-container class, for the Tile.js file, write the following input statement.

if (index === 0) {
  return <p className="tile-title" key={index}> {valueInObject} </p>
}

6d. Give a className of “tile” to all other

elements

if (index === 0) {
  return <p className="tile-title" key={index}> {valueInObject} </p>
} else {
  return <p className="tile" key={index}> {valueInObject} </p>
}

Note: Somehow, the bold will not show up in our tile-title class, but I found the problem in index.css file. Change .tile.tile-title to just .tile-title. Now the bold styling will show as expected.

Implement AppointmentsPage as a stateful component that handles the logic for adding new appointments and listing current appointments. AppointmentsPage.js

7a. Receive three props:
- The current list of appointments
- The current list of contacts

First, insert props as the input variable for the function.
To retrieve the list of appointments and contacts (from App.js), simply check the ContactsPage.js as an example.

export const AppointmentsPage = (props) => {
  const appointments = props.appointments;
  const contacts = props.contacts;
}

7b. - A callback function for adding a new appointment.
Retrieving the App.js addAppointment callback function.

const addAppointment = props.addAppointment

7c. Keep track of four local state variables, the current title, contact, date, and time entered into the form.
Note: Inside the handleSubmit function, (similar to ContactsPage.js) write the following:

const [title, setTitle] = useState('');
const [contact, setContact] = useState('');
const [date, setDate] = useState('');
const [time, setTime] = useState('');

7d. Add a new appointment to form submission.
Note: Duplicates are accepted here.

addAppointment(title, contact, date, time)

7e. Clear the form on submission
Note: Use the state variables defined above to clear data upon submission of form.

setTitle('');
setContact('');
setDate('');
setTime('')

7f. In the Add Appointment section, render an AppointmentForm with the following passed via props:
local state variables
local state variable setter functions
handleSubmit callback function
Note: Add the contacts variable, referring to the contacts property, defined above.

<AppointmentForm 
  title={title}
  contact={contact}
  date={date}
  time={time}
  setTitle={setTitle}
  setContact={setContact}
  setDate={setDate}
  setTime={setTime}
  handleSubmit={handleSubmit}

  contacts={contacts}
/>

Note: Try to NOT add contacts={contacts} here and keep coding. Eventually, you will see that the const contacts=props.contacts was defined, but not used. This will break the code later, but we will eventually use is, like how it is inserted above, and the code will work again.

7g. In the Appointments section, render a TileList with the appointment array passed via props
Note: Appointments section is referring to

Appointments

. Make sure to import TileList component.
Note: Refer back to TileList.js and the input for the function, which is data. If we refer back to ContactsPage.js and its implementation, contacts={appointments} would not make sense. Furthermore, if we try appointments={appointments}, (which I suggest) this below would break the code. Therefore, I backtracked to change the property of TileList component to data={appointments} (for AppointmentsPage.js) and data={contacts} (for ContactsPage.js) so that they “SHARE” the TileList component.

<TileList data={appointments} />

Implement AppointmentForm as a stateless component that renders a web form to collect the necessary appointment information. AppointmentForm.js

Render a form with:
The onSubmit attribute set to the callback function passed in via props
3 controlled input components, to be used for the title, date and time appointment data
A ContactPicker component with the contacts list passed in via props
A submit button
Use getTodayString() to set the min attribute of the date input

8a. Render a form with:
The onSubmit attribute set to the callback function passed in via props

8b. Render a form with:
3 controlled input components, to be used for the title, date and time appointment data
Note: inside the form tag, nest input tags (FYI: input tags are self closing here)

return (
  <form onSubmit={handleSubmit}>
    <input
      type="text"
      onChange={({target}) => setTitle(target.value)}
      value={title} 
    />
    <input
      type="date"
      value={date}
      onChange={({target}) => setDate(target.value)}
    />
    <input
      type="time"
      value={time} 
      onChange={({target}) => setTime(target.value)}
    />
  </form>
);

8c. Render a form with:
A ContactPicker component with the contacts list passed in via props
Note: Inside the form tag, render the component. Make sure to import the component on the top of the file.

<ContactPicker
  contacts={contacts}
  value={contact}
  onChange={({target}) => setContact(target.value)}
/>

8d. Render a form with:
A submit button
Note: Inside form tags

  <input type="submit" />

8e. Use getTodayString() to set the min attribute of the date input
Note: Inside the date input, call the getTodayString function as follows:

<input 
  type="date"
  ...
  min={getTodayString()}
/>

Implement ContactPicker as a stateless component that renders a drop-down list of all contact names. ContactPicker.js

9a. Receive 2 props: The array of contacts and a callback function to handle when the onChange event is triggered
Note: Before the return statement retrieve contacts and onChange. Make sure the function receives a props variable.

export const ContactPicker = (props) => {
  const contacts = props.contacts
  const onChange= props.onChange
  return (
    ...
  );
};

9b. Render a select element with the onChange attribute set to the callback passed in via props
Note: Check the select html rules I chose not to pass the name or id attribute here.

<select onChange={onChange}>
  
</select>

9c. Have a default first option element that indicates no contact is selected
Note: Option Element Rules
I will pass the value attribute here.

<option value="">
  Choose something 😬
</option>

Note: Yes, emojis work and won’t break “THE ENTIRE PROJECT” aka code.

9d. Iteratively add option elements using the contact names from the array passed in via props
Note: Below the previous option tag, Nested inside select tag only, not the option tag. I am adding another option tag below the default tag via JSX.

{contacts.map( contact => <option value={contact.name}>{contact.name}</option>)}

9e. Check chrome console error messages. Add key attribute to make error message disappear.

{contacts.map( (contact, index) => <option value={contact.name} key={index}>{contact.name}<option/>)}
1 Like

So I was stuck on this for a good while. I had everything compiling and rendering but the form submit was not adding the contacts to the tile page. I was ready to rip my hair out, so this is a part solution for anyone having a similar issue, and part question because I was never aware that this was a thing.

At any rate, when I passed my props to the ContactForm on the ContactsPage, I had name={name} phone={phone} and then email={email}, and then following that I passed the setters in the same order. Finally, by complete chance, I rewrote the whole component and this time I passed name and then setName, phone and then setPhone etc and to my surprise it worked, which tells me the order in which the props are passed is important.

Now I’m just trying to understand why so I know moving forward, can anyone give a simple clarification on that?