feat(SW-864): add to calendar functionality
This commit is contained in:
@@ -39,4 +39,4 @@
|
||||
display: grid;
|
||||
grid-area: receipt;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,8 +23,6 @@ export default async function BookingConfirmationPage({
|
||||
}: PageArgs<LangParams, { confirmationNumber: string }>) {
|
||||
setLang(params.lang)
|
||||
void getBookingConfirmation(searchParams.confirmationNumber)
|
||||
const { confirmationNumber } = searchParams
|
||||
|
||||
return (
|
||||
<main className={styles.main}>
|
||||
<Suspense fallback={<LoadingSpinner fullPage />}>
|
||||
|
||||
@@ -4,7 +4,6 @@ import {
|
||||
BOOKING_CONFIRMATION_NUMBER,
|
||||
PaymentErrorCodeEnum,
|
||||
} from "@/constants/booking"
|
||||
import { Lang } from "@/constants/languages"
|
||||
import {
|
||||
bookingConfirmation,
|
||||
payment,
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
"use client"
|
||||
import { createEvent } from "ics"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { dt } from "@/lib/dt"
|
||||
|
||||
import { CalendarAddIcon } from "@/components/Icons"
|
||||
import Button from "@/components/TempDesignSystem/Button"
|
||||
import useLang from "@/hooks/useLang"
|
||||
|
||||
import type { AddToCalendarProps } from "@/types/components/hotelReservation/bookingConfirmation/actions/addToCalendar"
|
||||
|
||||
export default function AddToCalendar({
|
||||
checkInDate,
|
||||
event,
|
||||
hotelName,
|
||||
}: AddToCalendarProps) {
|
||||
const intl = useIntl()
|
||||
const lang = useLang()
|
||||
|
||||
async function downloadBooking() {
|
||||
const d = dt(checkInDate).locale(lang).format("YYYY-MM-DD")
|
||||
const filename = `${hotelName.toLowerCase().split(" ").join("_")}-${d}.ics`
|
||||
|
||||
const file: Blob = await new Promise((resolve, reject) => {
|
||||
createEvent(event, (error, value) => {
|
||||
if (error) {
|
||||
reject(error)
|
||||
}
|
||||
|
||||
resolve(new File([value], filename, { type: "text/calendar" }))
|
||||
})
|
||||
})
|
||||
|
||||
const url = URL.createObjectURL(file)
|
||||
|
||||
const anchor = document.createElement("a")
|
||||
anchor.href = url
|
||||
anchor.download = filename
|
||||
|
||||
document.body.appendChild(anchor)
|
||||
anchor.click()
|
||||
document.body.removeChild(anchor)
|
||||
|
||||
URL.revokeObjectURL(url)
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
intent="text"
|
||||
onPress={downloadBooking}
|
||||
size="small"
|
||||
theme="base"
|
||||
variant="icon"
|
||||
wrapping
|
||||
>
|
||||
<CalendarAddIcon />
|
||||
{intl.formatMessage({ id: "Add to calendar" })}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
"use client"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { DownloadIcon } from "@/components/Icons"
|
||||
import Button from "@/components/TempDesignSystem/Button"
|
||||
|
||||
export default function DownloadInvoice() {
|
||||
const intl = useIntl()
|
||||
|
||||
function downloadBooking() {
|
||||
window.print()
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
intent="text"
|
||||
onPress={downloadBooking}
|
||||
size="small"
|
||||
theme="base"
|
||||
variant="icon"
|
||||
wrapping
|
||||
>
|
||||
<DownloadIcon />
|
||||
{intl.formatMessage({ id: "Download invoice" })}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
"use client"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { EditIcon } from "@/components/Icons"
|
||||
import Button from "@/components/TempDesignSystem/Button"
|
||||
|
||||
export default function ManageBooking() {
|
||||
const intl = useIntl()
|
||||
return (
|
||||
<Button intent="text" size="small" theme="base" variant="icon" wrapping>
|
||||
<EditIcon />
|
||||
{intl.formatMessage({ id: "Manage booking" })}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
.actions {
|
||||
border-radius: var(--Corner-radius-Medium);
|
||||
display: grid;
|
||||
grid-area: actions;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
.actions {
|
||||
gap: var(--Spacing-x3);
|
||||
grid-auto-columns: auto;
|
||||
grid-auto-flow: column;
|
||||
grid-template-columns: auto;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { dt } from "@/lib/dt"
|
||||
|
||||
import type { DateTime } from "ics"
|
||||
|
||||
export function generateDateTime(d: Date): DateTime {
|
||||
const _d = dt(d).utc()
|
||||
return [
|
||||
_d.year(),
|
||||
// Need to add +1 since month is 0 based
|
||||
_d.month() + 1,
|
||||
_d.date(),
|
||||
_d.hour(),
|
||||
_d.minute(),
|
||||
]
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
import { CalendarAddIcon, DownloadIcon, EditIcon } from "@/components/Icons"
|
||||
import Button from "@/components/TempDesignSystem/Button"
|
||||
import { getIntl } from "@/i18n"
|
||||
|
||||
import styles from "./actions.module.css"
|
||||
|
||||
export default async function Actions() {
|
||||
const intl = await getIntl()
|
||||
return (
|
||||
<div className={styles.actions}>
|
||||
<Button intent="text" size="small" theme="base" variant="icon" wrapping>
|
||||
<CalendarAddIcon />
|
||||
{intl.formatMessage({ id: "Add to calendar" })}
|
||||
</Button>
|
||||
<Button intent="text" size="small" theme="base" variant="icon" wrapping>
|
||||
<EditIcon />
|
||||
{intl.formatMessage({ id: "Manage booking" })}
|
||||
</Button>
|
||||
<Button intent="text" size="small" theme="base" variant="icon" wrapping>
|
||||
<DownloadIcon />
|
||||
{intl.formatMessage({ id: "Download invoice" })}
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -17,6 +17,22 @@
|
||||
max-width: 720px;
|
||||
}
|
||||
|
||||
.actions {
|
||||
border-radius: var(--Corner-radius-Medium);
|
||||
display: grid;
|
||||
grid-area: actions;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
.actions {
|
||||
gap: var(--Spacing-x3);
|
||||
grid-auto-columns: auto;
|
||||
grid-auto-flow: column;
|
||||
grid-template-columns: auto;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1367px) {
|
||||
.header {
|
||||
padding-bottom: var(--Spacing-x4);
|
||||
|
||||
@@ -5,17 +5,22 @@ import Body from "@/components/TempDesignSystem/Text/Body"
|
||||
import Title from "@/components/TempDesignSystem/Text/Title"
|
||||
import { getIntl } from "@/i18n"
|
||||
|
||||
import Actions from "./Actions"
|
||||
import AddToCalendar from "./Actions/AddToCalendar"
|
||||
import DownloadInvoice from "./Actions/DownloadInvoice"
|
||||
import { generateDateTime } from "./Actions/helpers"
|
||||
import ManageBooking from "./Actions/ManageBooking"
|
||||
|
||||
import styles from "./header.module.css"
|
||||
|
||||
import type { EventAttributes } from "ics"
|
||||
|
||||
import type { BookingConfirmationProps } from "@/types/components/hotelReservation/bookingConfirmation/bookingConfirmation"
|
||||
|
||||
export default async function Header({
|
||||
confirmationNumber,
|
||||
}: BookingConfirmationProps) {
|
||||
const intl = await getIntl()
|
||||
const { hotel } = await getBookingConfirmation(confirmationNumber)
|
||||
const { booking, hotel } = await getBookingConfirmation(confirmationNumber)
|
||||
|
||||
const text = intl.formatMessage<React.ReactNode>(
|
||||
{ id: "booking.confirmation.text" },
|
||||
@@ -28,6 +33,25 @@ export default async function Header({
|
||||
}
|
||||
)
|
||||
|
||||
const event: EventAttributes = {
|
||||
busyStatus: "FREE",
|
||||
categories: ["booking", "hotel", "stay"],
|
||||
created: generateDateTime(booking.createDateTime),
|
||||
description: hotel.hotelContent.texts.descriptions.medium,
|
||||
end: generateDateTime(booking.checkOutDate),
|
||||
endInputType: "utc",
|
||||
geo: {
|
||||
lat: hotel.location.latitude,
|
||||
lon: hotel.location.longitude,
|
||||
},
|
||||
location: `${hotel.address.streetAddress}, ${hotel.address.zipCode} ${hotel.address.city} ${hotel.address.country}`,
|
||||
start: generateDateTime(booking.checkInDate),
|
||||
startInputType: "utc",
|
||||
status: "CONFIRMED",
|
||||
title: hotel.name,
|
||||
url: hotel.contactInformation.websiteUrl,
|
||||
}
|
||||
|
||||
return (
|
||||
<header className={styles.header}>
|
||||
<hgroup className={styles.hgroup}>
|
||||
@@ -39,7 +63,15 @@ export default async function Header({
|
||||
</Title>
|
||||
</hgroup>
|
||||
<Body className={styles.body}>{text}</Body>
|
||||
<Actions />
|
||||
<div className={styles.actions}>
|
||||
<AddToCalendar
|
||||
checkInDate={booking.checkInDate}
|
||||
event={event}
|
||||
hotelName={hotel.name}
|
||||
/>
|
||||
<ManageBooking />
|
||||
<DownloadInvoice />
|
||||
</div>
|
||||
</header>
|
||||
)
|
||||
}
|
||||
|
||||
53
package-lock.json
generated
53
package-lock.json
generated
@@ -41,6 +41,7 @@
|
||||
"graphql": "^16.8.1",
|
||||
"graphql-request": "^6.1.0",
|
||||
"graphql-tag": "^2.12.6",
|
||||
"ics": "^3.8.1",
|
||||
"immer": "10.1.1",
|
||||
"json-stable-stringify-without-jsonify": "^1.0.1",
|
||||
"libphonenumber-js": "^1.10.60",
|
||||
@@ -11794,6 +11795,16 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ics": {
|
||||
"version": "3.8.1",
|
||||
"resolved": "https://registry.npmjs.org/ics/-/ics-3.8.1.tgz",
|
||||
"integrity": "sha512-UqQlfkajfhrS4pUGQfGIJMYz/Jsl/ob3LqcfEhUmLbwumg+ZNkU0/6S734Vsjq3/FYNpEcZVKodLBoe+zBM69g==",
|
||||
"dependencies": {
|
||||
"nanoid": "^3.1.23",
|
||||
"runes2": "^1.1.2",
|
||||
"yup": "^1.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/icss-utils": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz",
|
||||
@@ -16840,6 +16851,11 @@
|
||||
"react-is": "^16.13.1"
|
||||
}
|
||||
},
|
||||
"node_modules/property-expr": {
|
||||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npmjs.org/property-expr/-/property-expr-2.0.6.tgz",
|
||||
"integrity": "sha512-SVtmxhRE/CGkn3eZY1T6pC8Nln6Fr/lu1mKSgRud0eC73whjGfoAogbn78LkD8aFL0zz3bAFerKSnOl7NlErBA=="
|
||||
},
|
||||
"node_modules/proto-list": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz",
|
||||
@@ -17859,6 +17875,11 @@
|
||||
"queue-microtask": "^1.2.2"
|
||||
}
|
||||
},
|
||||
"node_modules/runes2": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/runes2/-/runes2-1.1.4.tgz",
|
||||
"integrity": "sha512-LNPnEDPOOU4ehF71m5JoQyzT2yxwD6ZreFJ7MxZUAoMKNMY1XrAo60H1CUoX5ncSm0rIuKlqn9JZNRrRkNou2g=="
|
||||
},
|
||||
"node_modules/rxjs": {
|
||||
"version": "7.8.1",
|
||||
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz",
|
||||
@@ -19048,6 +19069,11 @@
|
||||
"integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/tiny-case": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/tiny-case/-/tiny-case-1.0.3.tgz",
|
||||
"integrity": "sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q=="
|
||||
},
|
||||
"node_modules/tmp": {
|
||||
"version": "0.2.3",
|
||||
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz",
|
||||
@@ -19103,6 +19129,11 @@
|
||||
"node": ">=0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/toposort": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz",
|
||||
"integrity": "sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg=="
|
||||
},
|
||||
"node_modules/tough-cookie": {
|
||||
"version": "4.1.3",
|
||||
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.3.tgz",
|
||||
@@ -20348,6 +20379,28 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/yup": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/yup/-/yup-1.4.0.tgz",
|
||||
"integrity": "sha512-wPbgkJRCqIf+OHyiTBQoJiP5PFuAXaWiJK6AmYkzQAh5/c2K9hzSApBZG5wV9KoKSePF7sAxmNSvh/13YHkFDg==",
|
||||
"dependencies": {
|
||||
"property-expr": "^2.0.5",
|
||||
"tiny-case": "^1.0.3",
|
||||
"toposort": "^2.0.2",
|
||||
"type-fest": "^2.19.0"
|
||||
}
|
||||
},
|
||||
"node_modules/yup/node_modules/type-fest": {
|
||||
"version": "2.19.0",
|
||||
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz",
|
||||
"integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==",
|
||||
"engines": {
|
||||
"node": ">=12.20"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/zod": {
|
||||
"version": "3.22.4",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.22.4.tgz",
|
||||
|
||||
@@ -56,6 +56,7 @@
|
||||
"graphql": "^16.8.1",
|
||||
"graphql-request": "^6.1.0",
|
||||
"graphql-tag": "^2.12.6",
|
||||
"ics": "^3.8.1",
|
||||
"immer": "10.1.1",
|
||||
"json-stable-stringify-without-jsonify": "^1.0.1",
|
||||
"libphonenumber-js": "^1.10.60",
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import type { EventAttributes } from "ics"
|
||||
|
||||
import type { RouterOutput } from "@/lib/trpc/client"
|
||||
|
||||
export interface AddToCalendarProps {
|
||||
checkInDate: RouterOutput["booking"]["confirmation"]["booking"]["checkInDate"]
|
||||
event: EventAttributes
|
||||
hotelName: RouterOutput["booking"]["confirmation"]["hotel"]["name"]
|
||||
}
|
||||
Reference in New Issue
Block a user