Skip to content

Commit

Permalink
Merge pull request #20 from Ayon95/booking_page
Browse files Browse the repository at this point in the history
Add booking details page
  • Loading branch information
Ayon95 authored Mar 15, 2024
2 parents 6c00b01 + 7317d6e commit 499f4d2
Show file tree
Hide file tree
Showing 21 changed files with 675 additions and 47 deletions.
2 changes: 2 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
import GlobalStyles from '@/styles/GlobalStyles';
import Dashboard from '@/pages/Dashboard';
import Bookings from '@/pages/Bookings';
import Booking from './pages/Booking';
import Cabins from '@/pages/Cabins';
import Settings from '@/pages/Settings';
import Account from '@/pages/Account';
Expand Down Expand Up @@ -35,6 +36,7 @@ function App() {
<Route index element={<Navigate replace to="dashboard" />} />
<Route path="dashboard" element={<Dashboard />} />
<Route path="bookings" element={<Bookings />} />
<Route path="bookings/:bookingId" element={<Booking />} />
<Route path="cabins" element={<Cabins />} />
<Route path="settings" element={<Settings />} />
<Route path="account" element={<Account />} />
Expand Down
2 changes: 1 addition & 1 deletion src/features/authentication/Logout.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { HiOutlineArrowRightOnRectangle } from 'react-icons/hi2';
import { useLogout } from './hooks/useLogout';
import SpinnerMini from '@/ui/spinner/SpinnerMini';
import ButtonIconText from '@/ui/button/ButtonIconText';
import { ButtonIconText } from '@/ui/button/ButtonIconText';

function Logout() {
const { mutate, isLoading } = useLogout();
Expand Down
166 changes: 166 additions & 0 deletions src/features/bookings/BookingDetails.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
import styled from 'styled-components';
import { HiArrowLeft, HiOutlineHomeModern } from 'react-icons/hi2';

import Heading from '@/ui/Heading';
import { LinkButtonIconText } from '@/ui/button/ButtonIconText';
import Badge from '@/ui/Badge';
import { bookingStatusToBadgeColorMap } from '@/utils/constants';
import { Booking, BookingStatus } from '@/types/bookings';
import { formatDate } from '@/utils/helpers';
import BookingGuestDetailsTable from './BookingGuestDetailsTable';
import BookingPaymentSummary from './BookingPaymentSummary';

interface BookingProps {
booking: Booking;
}

const dateFormatOptions: Intl.DateTimeFormatOptions = {
weekday: 'short',
month: 'short',
day: '2-digit',
year: 'numeric',
};

function BookingDetails({ booking }: BookingProps) {
return (
<Container>
<Header>
<HeadingContainer>
<Heading as="h1">Booking #{booking.id}</Heading>
<StyledBadge type={bookingStatusToBadgeColorMap[booking.status as BookingStatus]}>
{booking.status}
</StyledBadge>
</HeadingContainer>
<LinkButtonIconText to="/bookings">
<HiArrowLeft />
<span>Bookings</span>
</LinkButtonIconText>
</Header>
<BookingDate className="text-sm">
Booked on{' '}
<time dateTime={booking.created_at}>
{formatDate(booking.created_at, dateFormatOptions)}
</time>
</BookingDate>
<Grid>
<div>
<BookingDurationDetails>
<div>
<HiOutlineHomeModern />
<p>
<span className="fw-semi-bold">{booking.num_nights}</span>{' '}
{booking.num_nights > 1 ? 'nights' : 'night'} in{' '}
<span className="fw-semi-bold">Cabin {booking.cabin?.name}</span>
</p>
</div>
<div>
<p>Check-in</p>
<time dateTime={booking.start_date} className="fw-semi-bold">
{formatDate(booking.start_date, dateFormatOptions)}
</time>
</div>
<div>
<p>Check-out</p>
<time dateTime={booking.end_date} className="fw-semi-bold">
{formatDate(booking.end_date, dateFormatOptions)}
</time>
</div>
</BookingDurationDetails>
{booking.guest && (
<Section>
<Heading as="h2">Guest Details</Heading>
<BookingGuestDetailsTable
guestDetails={{ num_guests: booking.num_guests, guest: booking.guest }}
/>
</Section>
)}
{booking.observations && (
<Observations>
<Heading as="h2">Observations & Requests</Heading>
<p>{booking.observations}</p>
</Observations>
)}
</div>

<BookingPaymentSummary
paymentInfo={{
cabin_price: booking.cabin_price,
extra_price: booking.extra_price,
total_price: booking.total_price,
has_breakfast: booking.has_breakfast,
is_paid: booking.is_paid,
}}
/>
</Grid>
</Container>
);
}

export default BookingDetails;

const Container = styled.div`
--section-spacing: 4rem;
`;

const Header = styled.div`
display: flex;
flex-wrap: wrap;
justify-content: space-between;
align-items: center;
gap: 1rem;
margin-bottom: 0.6rem;
`;

const HeadingContainer = styled.div`
display: flex;
align-items: center;
gap: 1.6rem;
`;

const BookingDate = styled.p`
margin-bottom: 4rem;
`;

const StyledBadge = styled(Badge)`
transform: translateY(3px);
`;

const Grid = styled.div`
display: grid;
row-gap: var(--section-spacing);
column-gap: 3rem;
@media only screen and (min-width: 75em) {
grid-template-columns: 2.4fr 1fr;
}
`;

const Section = styled.section`
margin-top: var(--section-spacing);
h2 {
margin-bottom: 4px;
}
`;

const BookingDurationDetails = styled.div`
display: grid;
grid-template-columns: repeat(auto-fit, minmax(24rem, 1fr));
align-items: center;
gap: 2rem;
padding: 2rem;
border-radius: var(--border-radius-sm);
text-align: center;
background-color: var(--color-brand-100);
svg {
width: 2.2rem;
height: 2.2rem;
}
`;

const Observations = styled(Section)`
p {
max-width: 75ch;
}
`;
73 changes: 73 additions & 0 deletions src/features/bookings/BookingGuestDetailsTable.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import styled from 'styled-components';

import { Booking } from '@/types/bookings';
import Table from '@/ui/Table';

interface BookingGuestDetailsTableProps {
guestDetails: Pick<Booking, 'num_guests' | 'guest'>;
}

function BookingGuestDetailsTable({ guestDetails }: BookingGuestDetailsTableProps) {
const { num_guests, guest } = guestDetails;

if (!guest) return null;

return (
<StyledTable id="guestDetailsTable" caption="Guest Details">
<Table.Head>
<Table.Row>
<Table.HeaderCell>Name</Table.HeaderCell>
<Table.HeaderCell>Email</Table.HeaderCell>
<Table.HeaderCell>Total Guests</Table.HeaderCell>
<Table.HeaderCell>Country</Table.HeaderCell>
<Table.HeaderCell>National ID</Table.HeaderCell>
</Table.Row>
</Table.Head>
<Table.Body>
<Table.Row>
<Table.Cell label="name" className="fw-semi-bold">
{guest.full_name}
</Table.Cell>
<Table.Cell label="email">{guest.email}</Table.Cell>
<NumericTextCell label="total guests">{num_guests}</NumericTextCell>
<Table.Cell label="country">
<CountryDetails>
{guest.country_flag && <Flag src={guest.country_flag} />}
{guest.nationality && <span>{guest.nationality}</span>}
</CountryDetails>
</Table.Cell>
<NumericTextCell label="national ID">{guest.national_id ?? '-'}</NumericTextCell>
</Table.Row>
</Table.Body>
</StyledTable>
);
}

export default BookingGuestDetailsTable;

const StyledTable = styled(Table)`
th,
td {
padding: 0.5rem;
}
td img {
border-radius: var(--border-radius-tiny);
}
`;

const Flag = styled.img`
width: 3rem;
border-radius: var(--border-radius-tiny);
`;

const CountryDetails = styled.div`
display: flex;
align-items: center;
gap: 1rem;
`;

const NumericTextCell = styled(Table.Cell)`
font-family: var(--fontFamily-numeric);
font-size: 1.5rem;
`;
85 changes: 85 additions & 0 deletions src/features/bookings/BookingPaymentSummary.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { Booking } from '@/types/bookings';
import Badge from '@/ui/Badge';
import Heading from '@/ui/Heading';
import { formatPrice } from '@/utils/helpers';
import styled from 'styled-components';

interface BookingPaymentSummaryProps {
paymentInfo: Pick<
Booking,
'cabin_price' | 'extra_price' | 'total_price' | 'has_breakfast' | 'is_paid'
>;
}

function BookingPaymentSummary({ paymentInfo }: BookingPaymentSummaryProps) {
const { cabin_price, extra_price, total_price, has_breakfast, is_paid } = paymentInfo;
return (
<section>
<HeadingContainer>
<Heading as="h2">Payment Summary</Heading>
<StyledBadge type={is_paid ? 'brand' : 'red'}>{is_paid ? 'paid' : 'not paid'}</StyledBadge>
</HeadingContainer>
<div>
<PriceItemContainer>
<PriceItem>
Cabin: <data value={cabin_price}>{formatPrice(cabin_price)}</data>
</PriceItem>
</PriceItemContainer>
{extra_price && (
<PriceItemContainer>
<PriceItem>
Extra: <data value={extra_price}>{formatPrice(extra_price)}</data>
</PriceItem>
{has_breakfast && <PriceItemExtraInfo>Includes breakfast</PriceItemExtraInfo>}
</PriceItemContainer>
)}
<PriceItemContainer>
<PriceItem>
<strong>Total:</strong>{' '}
<strong>
<data value={total_price}>{formatPrice(total_price)}</data>
</strong>
</PriceItem>
</PriceItemContainer>
</div>
</section>
);
}

export default BookingPaymentSummary;

const HeadingContainer = styled.div`
display: flex;
justify-content: space-between;
align-items: center;
gap: 1rem;
margin-bottom: 1.6rem;
`;

const StyledBadge = styled(Badge)`
flex-shrink: 0;
transform: translateY(3px);
`;

const PriceItemContainer = styled.div`
font-size: calc(var(--fontSize-base) + 0.2rem);
&:not(:last-child) {
margin-bottom: 1.2rem;
}
`;

const PriceItem = styled.p`
display: flex;
justify-content: space-between;
gap: 1rem;
data {
font-family: var(--fontFamily-numeric);
}
`;

const PriceItemExtraInfo = styled.p`
font-size: 1.3rem;
font-weight: 600;
color: var(--color-brand-600);
`;
Loading

0 comments on commit 499f4d2

Please sign in to comment.