Working, now work on responsiveness
This commit is contained in:
parent
f49b59a582
commit
fb330d94a1
9 changed files with 176 additions and 60 deletions
|
@ -1,10 +1,21 @@
|
||||||
import { render, screen } from "@testing-library/react"
|
import { render, screen } from "@testing-library/react"
|
||||||
import { describe, expect } from "vitest"
|
import { BookingForm } from "./components/BookingForm"
|
||||||
import { BookingForm } from "./components/BookingForm.jsx"
|
import "@testing-library/jest-dom"
|
||||||
|
import { initializeTimes, updateTimes } from "./components/Main"
|
||||||
|
|
||||||
describe("Renders the BookingForm heading", () => {
|
test("Renders the BookingForm heading", () => {
|
||||||
const availableTimes = ["17:00"]
|
const availableTimes = ["17:00"]
|
||||||
render(<BookingForm availableTimes={availableTimes} />)
|
render(<BookingForm availableTimes={availableTimes} />)
|
||||||
const headingElement = screen.getByText("Choose date")
|
const headingElement = screen.getByText("Choose date")
|
||||||
expect(headingElement).toBeInTheDocument()
|
expect(headingElement).toBeInTheDocument()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("Returns a correct array of times", () => {
|
||||||
|
expect(initializeTimes()).toHaveLength(6)
|
||||||
|
expect(initializeTimes()[0]).toMatch(/\d{2}:\d{2}/)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("Returns the same value provided in the state", () => {
|
||||||
|
const times = initializeTimes()
|
||||||
|
expect(updateTimes(times, "2022-02-04")).toBe(times)
|
||||||
|
})
|
||||||
|
|
|
@ -1,43 +1,84 @@
|
||||||
import { useState } from "react"
|
import { useState } from "react"
|
||||||
import "../styles/BookingForm.css"
|
import "../styles/BookingForm.css"
|
||||||
|
|
||||||
export const BookingForm = ({ availableTimes, dispatch }) => {
|
export const BookingForm = ({
|
||||||
|
availableTimes,
|
||||||
|
dispatch,
|
||||||
|
submitForm,
|
||||||
|
reservationError
|
||||||
|
}) => {
|
||||||
const [date, setDate] = useState(new Date().toISOString().slice(0, 10))
|
const [date, setDate] = useState(new Date().toISOString().slice(0, 10))
|
||||||
const [time, setTime] = useState(availableTimes[0])
|
const [time, setTime] = useState(availableTimes[0])
|
||||||
const [guests, setGuests] = useState(1)
|
const [guests, setGuests] = useState(1)
|
||||||
const [occasion, setOccasion] = useState("None")
|
const [occasion, setOccasion] = useState("Other")
|
||||||
|
|
||||||
|
const [dateError, setDateError] = useState("")
|
||||||
|
const [timeError, setTimeError] = useState("")
|
||||||
|
const [guestsError, setGuestsError] = useState("")
|
||||||
|
const [occasionError, setOccasionError] = useState("")
|
||||||
|
|
||||||
|
const isFormInvalid = () =>
|
||||||
|
guestsError !== "" ||
|
||||||
|
dateError !== "" ||
|
||||||
|
timeError !== "" ||
|
||||||
|
occasionError !== ""
|
||||||
|
|
||||||
|
const onDateChange = ({ target }) => {
|
||||||
|
let error = ""
|
||||||
|
if (target.value === "") error = "Field is required"
|
||||||
|
setDateError(error)
|
||||||
|
setDate(target.value)
|
||||||
|
if (!error) dispatch(target.valueAsDate)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onTimeChange = ({ target }) => {
|
||||||
|
let error = ""
|
||||||
|
if (target.value === "") error = "Field is required"
|
||||||
|
setTimeError(error)
|
||||||
|
setTime(target.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onGuestChange = ({ target }) => {
|
||||||
|
let error = ""
|
||||||
|
if (target.value === "") error = "Field is required"
|
||||||
|
else if (target.value < 1 || target.value > 10)
|
||||||
|
error = "Guests must be between 1 and 10."
|
||||||
|
setGuestsError(error)
|
||||||
|
setGuests(target.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onOccasionChange = ({ target }) => {
|
||||||
|
let error = ""
|
||||||
|
if (target.value === "") error = "Field is required"
|
||||||
|
else if (
|
||||||
|
target.value !== "Other" &&
|
||||||
|
target.value !== "Birthday" &&
|
||||||
|
target.value !== "Anniversary"
|
||||||
|
)
|
||||||
|
error = "Invalid occasion."
|
||||||
|
setOccasionError(error)
|
||||||
|
setOccasion(target.value)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form
|
<form className="booking-form" onSubmit={submitForm}>
|
||||||
className="booking-form"
|
<label htmlFor="date">Choose date</label>
|
||||||
onSubmit={(event) => {
|
|
||||||
event.preventDefault()
|
|
||||||
console.log(date, time, guests, occasion)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<label htmlFor="res-date">Choose date</label>
|
|
||||||
<input
|
<input
|
||||||
value={date}
|
value={date}
|
||||||
required
|
required
|
||||||
onChange={(event) => {
|
onChange={onDateChange}
|
||||||
setDate(event.target.value)
|
|
||||||
dispatch(event.target.value)
|
|
||||||
}}
|
|
||||||
type="date"
|
type="date"
|
||||||
id="res-date"
|
id="date"
|
||||||
/>
|
/>
|
||||||
|
{dateError !== "" && <span className="error">{dateError}</span>}
|
||||||
|
|
||||||
<label htmlFor="time">Choose time</label>
|
<label htmlFor="time">Choose time</label>
|
||||||
<select
|
<select value={time} required onChange={onTimeChange} id="time">
|
||||||
value={time}
|
|
||||||
required
|
|
||||||
onChange={(event) => setTime(event.target.value)}
|
|
||||||
id="time"
|
|
||||||
>
|
|
||||||
{availableTimes.map((time) => (
|
{availableTimes.map((time) => (
|
||||||
<option key={time}>{time}</option>
|
<option key={time}>{time}</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
|
{timeError !== "" && <span className="error">{timeError}</span>}
|
||||||
|
|
||||||
<label htmlFor="guests">Number of guests</label>
|
<label htmlFor="guests">Number of guests</label>
|
||||||
<input
|
<input
|
||||||
|
@ -47,25 +88,29 @@ export const BookingForm = ({ availableTimes, dispatch }) => {
|
||||||
max="10"
|
max="10"
|
||||||
id="guests"
|
id="guests"
|
||||||
value={guests}
|
value={guests}
|
||||||
onChange={(event) => setGuests(event.target.value)}
|
onChange={onGuestChange}
|
||||||
/>
|
/>
|
||||||
|
{guestsError !== "" && <span className="error">{guestsError}</span>}
|
||||||
|
|
||||||
<label htmlFor="occasion">Occasion</label>
|
<label htmlFor="occasion">Occasion</label>
|
||||||
<select
|
<select id="occasion" value={occasion} onChange={onOccasionChange}>
|
||||||
id="occasion"
|
<option>Other</option>
|
||||||
value={occasion}
|
|
||||||
onChange={(event) => setOccasion(event.target.value)}
|
|
||||||
>
|
|
||||||
<option>None</option>
|
|
||||||
<option>Birthday</option>
|
<option>Birthday</option>
|
||||||
<option>Anniversary</option>
|
<option>Anniversary</option>
|
||||||
</select>
|
</select>
|
||||||
|
{occasionError !== "" && (
|
||||||
|
<span className="error">{occasionError}</span>
|
||||||
|
)}
|
||||||
|
|
||||||
<input
|
<input
|
||||||
type="submit"
|
type="submit"
|
||||||
className="submit"
|
className="submit"
|
||||||
|
disabled={isFormInvalid()}
|
||||||
value="Confirm reservation"
|
value="Confirm reservation"
|
||||||
/>
|
/>
|
||||||
|
{reservationError !== "" && (
|
||||||
|
<span className="error">{reservationError}</span>
|
||||||
|
)}
|
||||||
</form>
|
</form>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,24 +6,24 @@ export const Footer = () => (
|
||||||
<img src="/images/footer-logo.png" alt="Little Lemon Logo" />
|
<img src="/images/footer-logo.png" alt="Little Lemon Logo" />
|
||||||
<section>
|
<section>
|
||||||
<h3>Doormat Navigation</h3>
|
<h3>Doormat Navigation</h3>
|
||||||
<a href="#">Home</a>
|
<a href="/">Home</a>
|
||||||
<a href="#">About</a>
|
<a href="/">About</a>
|
||||||
<a href="#">Menu</a>
|
<a href="/">Menu</a>
|
||||||
<Link to="/book">Reserve a table</Link>
|
<Link to="/book">Reserve a table</Link>
|
||||||
<a href="#">Order Online</a>
|
<a href="/">Order online</a>
|
||||||
<a href="#">Login</a>
|
<a href="/">Login</a>
|
||||||
</section>
|
</section>
|
||||||
<section>
|
<section>
|
||||||
<h3>Contact</h3>
|
<h3>Contact</h3>
|
||||||
<a href="#">Address</a>
|
<a href="/">Address</a>
|
||||||
<a href="#">Phone number</a>
|
<a href="/">Phone number</a>
|
||||||
<a href="#">Email</a>
|
<a href="/">Email</a>
|
||||||
</section>
|
</section>
|
||||||
<section>
|
<section>
|
||||||
<h3>Social Media Links</h3>
|
<h3>Social Media Links</h3>
|
||||||
<a href="#">Twitter</a>
|
<a href="/">Twitter</a>
|
||||||
<a href="#">Instagram</a>
|
<a href="/">Instagram</a>
|
||||||
<a href="#">Threads</a>
|
<a href="/">Threads</a>
|
||||||
</section>
|
</section>
|
||||||
</footer>
|
</footer>
|
||||||
)
|
)
|
||||||
|
|
|
@ -2,10 +2,11 @@ import { Route } from "react-router-dom"
|
||||||
import { Routes } from "react-router-dom"
|
import { Routes } from "react-router-dom"
|
||||||
import { HomePage } from "../routes/HomePage"
|
import { HomePage } from "../routes/HomePage"
|
||||||
import { BookingPage } from "../routes/BookingPage"
|
import { BookingPage } from "../routes/BookingPage"
|
||||||
import { useReducer } from "react"
|
import { useReducer, useState } from "react"
|
||||||
|
import { ConfirmedBookingPage } from "../routes/ConfirmedBookingPage"
|
||||||
|
import { useNavigate } from "react-router-dom"
|
||||||
|
|
||||||
export const Main = () => {
|
export const initializeTimes = () => [
|
||||||
const initializeTimes = () => [
|
|
||||||
"17:00",
|
"17:00",
|
||||||
"18:00",
|
"18:00",
|
||||||
"19:00",
|
"19:00",
|
||||||
|
@ -14,27 +15,52 @@ export const Main = () => {
|
||||||
"22:00"
|
"22:00"
|
||||||
]
|
]
|
||||||
|
|
||||||
const updateTimes = (times, date) => {
|
export const updateTimes = (_, date) => {
|
||||||
console.log(times, date)
|
// API provided returns 404, so unable to post data. Mocking return instead.
|
||||||
return times
|
const day = date.getDay()
|
||||||
|
const initialTimes = initializeTimes()
|
||||||
|
if (day === 0) return initialTimes.slice(0, 3)
|
||||||
|
if (day === 1) return initialTimes.slice(2, 5)
|
||||||
|
if (day === 2) return initialTimes.slice(4, 5)
|
||||||
|
if (day === 3) return initialTimes.slice(3, 5)
|
||||||
|
if (day === 4) return initialTimes.slice(1, 5)
|
||||||
|
if (day === 5) return initialTimes.slice(3, 3)
|
||||||
|
if (day === 6) return initialTimes.slice(1, 5)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const Main = () => {
|
||||||
|
const navigate = useNavigate()
|
||||||
const [availableTimes, dispatch] = useReducer(
|
const [availableTimes, dispatch] = useReducer(
|
||||||
updateTimes,
|
updateTimes,
|
||||||
initializeTimes()
|
initializeTimes()
|
||||||
)
|
)
|
||||||
|
const [reservationError, setReservationError] = useState("")
|
||||||
|
|
||||||
|
const submitForm = (event) => {
|
||||||
|
event.preventDefault()
|
||||||
|
// API provided returns 404, so unable to post data. Mocking return instead.
|
||||||
|
if (Math.random() > 0.8)
|
||||||
|
setReservationError("Unable to book reservation. Please try again.")
|
||||||
|
else navigate("/confirmed")
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<main>
|
<main>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route index element={<HomePage />} />
|
<Route index element={<HomePage />} />
|
||||||
|
<Route
|
||||||
|
path="/confirmed"
|
||||||
|
element={<ConfirmedBookingPage />}
|
||||||
|
/>
|
||||||
<Route
|
<Route
|
||||||
path="/book"
|
path="/book"
|
||||||
element={
|
element={
|
||||||
<BookingPage
|
<BookingPage
|
||||||
availableTimes={availableTimes}
|
availableTimes={availableTimes}
|
||||||
dispatch={dispatch}
|
dispatch={dispatch}
|
||||||
|
submitForm={submitForm}
|
||||||
|
reservationError={reservationError}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -3,7 +3,7 @@ import "../styles/Special.css"
|
||||||
|
|
||||||
export const Special = ({ image, title, price, children }) => (
|
export const Special = ({ image, title, price, children }) => (
|
||||||
<section className="column">
|
<section className="column">
|
||||||
<img src={image} alt="Picture of Food" />
|
<img src={image} alt="Food" />
|
||||||
<article>
|
<article>
|
||||||
<section className="row specialRow">
|
<section className="row specialRow">
|
||||||
<h3>{title}</h3>
|
<h3>{title}</h3>
|
||||||
|
|
|
@ -1,10 +1,20 @@
|
||||||
import { BookingForm } from "../components/BookingForm"
|
import { BookingForm } from "../components/BookingForm"
|
||||||
import "../styles/BookingPage.css"
|
import "../styles/BookingPage.css"
|
||||||
|
|
||||||
export const BookingPage = ({ availableTimes, dispatch }) => (
|
export const BookingPage = ({
|
||||||
|
availableTimes,
|
||||||
|
dispatch,
|
||||||
|
submitForm,
|
||||||
|
reservationError
|
||||||
|
}) => (
|
||||||
<section className="booking">
|
<section className="booking">
|
||||||
<h1>Book a Table</h1>
|
<h1>Book a Table</h1>
|
||||||
<hr />
|
<hr />
|
||||||
<BookingForm availableTimes={availableTimes} dispatch={dispatch} />
|
<BookingForm
|
||||||
|
availableTimes={availableTimes}
|
||||||
|
dispatch={dispatch}
|
||||||
|
submitForm={submitForm}
|
||||||
|
reservationError={reservationError}
|
||||||
|
/>
|
||||||
</section>
|
</section>
|
||||||
)
|
)
|
||||||
|
|
10
src/routes/ConfirmedBookingPage.jsx
Normal file
10
src/routes/ConfirmedBookingPage.jsx
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
import { Link } from "react-router-dom"
|
||||||
|
import "../styles/ConfirmedBookingPage.css"
|
||||||
|
|
||||||
|
export const ConfirmedBookingPage = () => (
|
||||||
|
<article className="confirmed">
|
||||||
|
<h1>Booking Confirmed</h1>
|
||||||
|
<p>Your booking has been confirmed.</p>
|
||||||
|
<Link to="/">Back to home</Link>
|
||||||
|
</article>
|
||||||
|
)
|
|
@ -2,6 +2,7 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 0.2rem;
|
gap: 0.2rem;
|
||||||
|
min-width: 20rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.booking-form :is(label, .submit) {
|
.booking-form :is(label, .submit) {
|
||||||
|
@ -19,3 +20,9 @@
|
||||||
.booking-form .submit {
|
.booking-form .submit {
|
||||||
background-color: #fbdabb;
|
background-color: #fbdabb;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.booking-form .error {
|
||||||
|
color: rgb(255, 152, 152);
|
||||||
|
font-weight: bold;
|
||||||
|
margin: 0.5rem 0;
|
||||||
|
}
|
||||||
|
|
7
src/styles/ConfirmedBookingPage.css
Normal file
7
src/styles/ConfirmedBookingPage.css
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
.confirmed {
|
||||||
|
background-color: var(--primary-1);
|
||||||
|
border-radius: 2rem;
|
||||||
|
margin: auto;
|
||||||
|
padding: 2rem 3rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
Reference in a new issue