-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
22 changed files
with
433 additions
and
7 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
import styled from 'styled-components'; | ||
|
||
import { Booking, BookingStatus } from '@/types/bookings'; | ||
import Table from '@/ui/Table'; | ||
import { formatPrice, formatDate } from '@/utils/helpers'; | ||
import Badge from '@/ui/Badge'; | ||
|
||
interface BookingRowProps { | ||
booking: Booking; | ||
} | ||
|
||
const bookingStatusToBadgeColorMap: Record<BookingStatus, string> = { | ||
unconfirmed: 'blue', | ||
'checked-in': 'green', | ||
'checked-out': 'silver', | ||
}; | ||
|
||
function BookingRow({ booking }: BookingRowProps) { | ||
const { cabin, guest, start_date, end_date, num_nights, total_price, status } = booking; | ||
return ( | ||
<Table.Row> | ||
<DarkNumericTextCell label="cabin">{cabin?.name}</DarkNumericTextCell> | ||
<Table.Cell label="guest"> | ||
<div> | ||
<div className="fw-semi-bold">{guest?.full_name}</div> | ||
<div>{guest?.email}</div> | ||
</div> | ||
</Table.Cell> | ||
<Table.Cell label="duration"> | ||
<div> | ||
<div className="fw-semi-bold">{num_nights} night stay</div> | ||
{formatDate(start_date)} - {formatDate(end_date)} | ||
</div> | ||
</Table.Cell> | ||
<Table.Cell label="status"> | ||
<Badge type={bookingStatusToBadgeColorMap[status as BookingStatus]}>{status}</Badge> | ||
</Table.Cell> | ||
<DarkNumericTextCell label="price">{formatPrice(total_price)}</DarkNumericTextCell> | ||
</Table.Row> | ||
); | ||
} | ||
|
||
export default BookingRow; | ||
|
||
const DarkNumericTextCell = styled(Table.Cell)` | ||
font-family: 'Sono', monospace; | ||
color: var(--color-grey-600); | ||
`; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
import { Booking } from '@/types/bookings'; | ||
import Table from '@/ui/Table'; | ||
import BookingRow from './BookingRow'; | ||
|
||
interface BookingTableProps { | ||
bookings: Booking[]; | ||
} | ||
|
||
function BookingTable({ bookings }: BookingTableProps) { | ||
return ( | ||
<Table id="bookingTable" caption="Bookings"> | ||
<Table.Head> | ||
<Table.Row> | ||
<Table.HeaderCell>Cabin</Table.HeaderCell> | ||
<Table.HeaderCell>Guest</Table.HeaderCell> | ||
<Table.HeaderCell>Duration</Table.HeaderCell> | ||
<Table.HeaderCell>Status</Table.HeaderCell> | ||
<Table.HeaderCell>Price</Table.HeaderCell> | ||
</Table.Row> | ||
</Table.Head> | ||
<Table.Body> | ||
{bookings.map(booking => ( | ||
<BookingRow key={booking.id} booking={booking} /> | ||
))} | ||
</Table.Body> | ||
</Table> | ||
); | ||
} | ||
|
||
export default BookingTable; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
import { screen, within } from '@testing-library/react'; | ||
|
||
import { renderWithQueryClient } from '@/test/utils'; | ||
import BookingTable from '../BookingTable'; | ||
import { bookings } from '@/test/fixtures/bookings'; | ||
import { formatDate } from '@/utils/helpers'; | ||
|
||
function setup() { | ||
renderWithQueryClient(<BookingTable bookings={bookings} />); | ||
} | ||
|
||
describe('BookingTable', () => { | ||
it('should have all column headers in the correct order', () => { | ||
setup(); | ||
const columnHeaders = screen.getAllByRole('columnheader'); | ||
|
||
expect(columnHeaders[0]).toHaveTextContent(/cabin/i); | ||
expect(columnHeaders[1]).toHaveTextContent(/guest/i); | ||
expect(columnHeaders[2]).toHaveTextContent(/duration/i); | ||
expect(columnHeaders[3]).toHaveTextContent(/status/i); | ||
expect(columnHeaders[4]).toHaveTextContent(/price/i); | ||
}); | ||
|
||
it('should have rows with booking details in the correct order', () => { | ||
setup(); | ||
const rows = screen.getAllByRole('row'); | ||
|
||
// The first row is the column header row | ||
[rows[1], rows[2]].forEach((row, i) => { | ||
const bookingCells = within(row).getAllByRole('cell'); | ||
const booking = bookings[i]; | ||
// Even though the type that Supabase returns indicates that cabin and guest can be nullable, it can't because a booking must be associated with a cabin and a guest | ||
const cabin = booking.cabin!; | ||
const guest = booking.guest!; | ||
|
||
expect(bookingCells[0]).toHaveTextContent(cabin.name); | ||
expect(bookingCells[1]).toHaveTextContent(`${guest.full_name}${guest.email}`); | ||
expect(bookingCells[2]).toHaveTextContent( | ||
`${booking.num_nights} night stay${formatDate(booking.start_date)} - ${formatDate( | ||
booking.end_date | ||
)}` | ||
); | ||
expect(bookingCells[3]).toHaveTextContent(booking.status); | ||
expect(bookingCells[4]).toHaveTextContent(`$${booking.total_price}.00`); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
import { getBookings } from '@/services/apiBookings'; | ||
import { BOOKINGS_QUERY_KEY } from '@/utils/constants'; | ||
import { useQuery } from '@tanstack/react-query'; | ||
|
||
export function useBookings(userId: string | null | undefined) { | ||
return useQuery({ | ||
queryKey: [BOOKINGS_QUERY_KEY, userId], | ||
queryFn: () => getBookings(userId), | ||
staleTime: Infinity, | ||
cacheTime: Infinity, | ||
}); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
import { screen } from '@testing-library/react'; | ||
import { rest } from 'msw'; | ||
|
||
import supabase from '@/services/supabase'; | ||
import { userSession } from '@/test/fixtures/authentication'; | ||
import { renderWithQueryClient } from '@/test/utils'; | ||
import Bookings from '../Bookings'; | ||
import { mockServer } from '@/test/mocks/server'; | ||
import { BOOKINGS_BASE_URL } from '@/utils/constants'; | ||
|
||
function setup() { | ||
renderWithQueryClient(<Bookings />); | ||
} | ||
|
||
function setupWithLogin() { | ||
// Setting logged in user | ||
vi.spyOn(supabase.auth, 'getSession').mockResolvedValue(userSession); | ||
|
||
renderWithQueryClient(<Bookings />); | ||
} | ||
|
||
afterEach(() => { | ||
vi.restoreAllMocks(); | ||
}); | ||
|
||
describe('Bookings', () => { | ||
it('should have page title', () => { | ||
setup(); | ||
const titleElement = screen.getByRole('heading', { name: /bookings/i }); | ||
expect(titleElement).toBeInTheDocument(); | ||
}); | ||
|
||
it('should show an appropriate message and not show booking table if there is no logged in user', async () => { | ||
setup(); | ||
|
||
const noBookingsTextElement = await screen.findByText(/no bookings to show/i); | ||
const bookingTable = screen.queryByRole('table', { name: /bookings/i }); | ||
|
||
expect(noBookingsTextElement).toBeInTheDocument(); | ||
expect(bookingTable).not.toBeInTheDocument(); | ||
}); | ||
|
||
it('should show an appropriate message and not show booking table if there are no bookings', async () => { | ||
mockServer.use( | ||
rest.get(BOOKINGS_BASE_URL, (_req, res, ctx) => { | ||
return res(ctx.json([])); | ||
}) | ||
); | ||
|
||
setupWithLogin(); | ||
|
||
const noBookingsTextElement = await screen.findByText(/no bookings to show/i); | ||
const bookingTable = screen.queryByRole('table', { name: /bookings/i }); | ||
|
||
expect(noBookingsTextElement).toBeInTheDocument(); | ||
expect(bookingTable).not.toBeInTheDocument(); | ||
}); | ||
|
||
it('should show booking table if there is a logged in user and there are bookings', async () => { | ||
setupWithLogin(); | ||
const bookingTable = await screen.findByRole('table', { name: /bookings/i }); | ||
expect(bookingTable).toBeInTheDocument(); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
import supabase from './supabase'; | ||
|
||
export async function getBookings(userId: string | null | undefined) { | ||
if (!userId) return []; | ||
|
||
const { data: bookings, error } = await supabase | ||
.from('booking') | ||
.select( | ||
'id, created_at, start_date, end_date, num_nights, num_guests, status, total_price, cabin(name, id), guest(id, full_name, email)' | ||
) | ||
.eq('user_id', userId); | ||
|
||
if (error) { | ||
console.error(error); | ||
throw new Error('Bookings could not be loaded.'); | ||
} | ||
|
||
return bookings; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
import { Booking } from '@/types/bookings'; | ||
import { cabins } from './cabins'; | ||
import { guests } from './guests'; | ||
|
||
export const bookings: Booking[] = [ | ||
{ | ||
id: 1, | ||
created_at: '2024-02-20T23:44:26.426083+00:00', | ||
start_date: '2024-02-20T18:42:34', | ||
end_date: '2024-02-22T18:42:39', | ||
num_nights: 2, | ||
num_guests: cabins[0].max_capacity, | ||
status: 'checked-out', | ||
total_price: cabins[0].regular_price + 50, | ||
cabin: { | ||
id: cabins[0].id, | ||
name: cabins[0].name, | ||
}, | ||
guest: { | ||
id: guests[0].id, | ||
full_name: guests[0].full_name, | ||
email: guests[0].email, | ||
}, | ||
}, | ||
|
||
{ | ||
id: 2, | ||
created_at: '2024-02-20T23:44:26.426083+00:00', | ||
start_date: '2024-02-20T18:42:34', | ||
end_date: '2024-02-24T18:42:39', | ||
num_nights: 4, | ||
num_guests: cabins[1].max_capacity, | ||
status: 'checked-out', | ||
total_price: cabins[1].regular_price + 100, | ||
cabin: { | ||
id: cabins[1].id, | ||
name: cabins[1].name, | ||
}, | ||
guest: { | ||
id: guests[1].id, | ||
full_name: guests[1].full_name, | ||
email: guests[1].email, | ||
}, | ||
}, | ||
]; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
import { Tables } from '@/types/database'; | ||
import { user } from './authentication'; | ||
|
||
export const guests: Tables<'guest'>[] = [ | ||
{ | ||
id: 1, | ||
full_name: 'John Doe', | ||
email: '[email protected]', | ||
national_id: null, | ||
nationality: 'Canada', | ||
country_flag: null, | ||
user_id: user.id, | ||
created_at: '2024-02-20 20:42:58.03428+00', | ||
}, | ||
|
||
{ | ||
id: 2, | ||
full_name: 'Mary Smith', | ||
email: '[email protected]', | ||
national_id: null, | ||
nationality: 'Canada', | ||
country_flag: null, | ||
user_id: user.id, | ||
created_at: '2024-02-20 20:42:58.03428+00', | ||
}, | ||
]; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,13 +1,20 @@ | ||
import { factory } from '@mswjs/data'; | ||
import Cabin from './models/cabin'; | ||
import { cabins } from '../fixtures/cabins'; | ||
import Booking from './models/booking'; | ||
import { bookings } from '../fixtures/bookings'; | ||
|
||
export const db = factory({ | ||
cabin: Cabin, | ||
booking: Booking, | ||
}); | ||
|
||
// Seed database with initial data | ||
|
||
for (const cabin of cabins) { | ||
db.cabin.create(cabin); | ||
} | ||
|
||
for (const booking of bookings) { | ||
db.booking.create(booking); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
import { rest } from 'msw'; | ||
|
||
import { BOOKINGS_BASE_URL } from '@/utils/constants'; | ||
import { db } from '../db'; | ||
|
||
export const bookingHandlers = [ | ||
rest.get(BOOKINGS_BASE_URL, (_req, res, ctx) => { | ||
const bookings = db.booking.getAll(); | ||
return res(ctx.json(bookings)); | ||
}), | ||
]; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,4 @@ | ||
import { bookingHandlers } from './domains/bookings'; | ||
import { cabinHandlers } from './domains/cabins'; | ||
|
||
export const handlers = [...cabinHandlers]; | ||
export const handlers = [...cabinHandlers, ...bookingHandlers]; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
import { nullable, primaryKey } from '@mswjs/data'; | ||
|
||
const Booking = { | ||
id: primaryKey(Number), | ||
created_at: () => new Date().toISOString(), | ||
start_date: String, | ||
end_date: String, | ||
num_nights: Number, | ||
num_guests: Number, | ||
status: String, | ||
total_price: Number, | ||
// Making it nullable to satisfy the type returned by Supabase | ||
cabin: nullable({ id: Number, name: String }), | ||
guest: nullable({ id: Number, full_name: String, email: String }), | ||
}; | ||
|
||
export default Booking; |
Oops, something went wrong.