Merged in feat/SW-858 (pull request #1014)
feat(SW-864): add to calendar functionality Approved-by: Tobias Johansson
This commit is contained in:
@@ -39,4 +39,4 @@
|
|||||||
display: grid;
|
display: grid;
|
||||||
grid-area: receipt;
|
grid-area: receipt;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -23,8 +23,6 @@ export default async function BookingConfirmationPage({
|
|||||||
}: PageArgs<LangParams, { confirmationNumber: string }>) {
|
}: PageArgs<LangParams, { confirmationNumber: string }>) {
|
||||||
setLang(params.lang)
|
setLang(params.lang)
|
||||||
void getBookingConfirmation(searchParams.confirmationNumber)
|
void getBookingConfirmation(searchParams.confirmationNumber)
|
||||||
const { confirmationNumber } = searchParams
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className={styles.main}>
|
<main className={styles.main}>
|
||||||
<Suspense fallback={<LoadingSpinner fullPage />}>
|
<Suspense fallback={<LoadingSpinner fullPage />}>
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import {
|
|||||||
BOOKING_CONFIRMATION_NUMBER,
|
BOOKING_CONFIRMATION_NUMBER,
|
||||||
PaymentErrorCodeEnum,
|
PaymentErrorCodeEnum,
|
||||||
} from "@/constants/booking"
|
} from "@/constants/booking"
|
||||||
import { Lang } from "@/constants/languages"
|
|
||||||
import {
|
import {
|
||||||
bookingConfirmation,
|
bookingConfirmation,
|
||||||
payment,
|
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;
|
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) {
|
@media screen and (min-width: 1367px) {
|
||||||
.header {
|
.header {
|
||||||
padding-bottom: var(--Spacing-x4);
|
padding-bottom: var(--Spacing-x4);
|
||||||
|
|||||||
@@ -5,17 +5,22 @@ import Body from "@/components/TempDesignSystem/Text/Body"
|
|||||||
import Title from "@/components/TempDesignSystem/Text/Title"
|
import Title from "@/components/TempDesignSystem/Text/Title"
|
||||||
import { getIntl } from "@/i18n"
|
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 styles from "./header.module.css"
|
||||||
|
|
||||||
|
import type { EventAttributes } from "ics"
|
||||||
|
|
||||||
import type { BookingConfirmationProps } from "@/types/components/hotelReservation/bookingConfirmation/bookingConfirmation"
|
import type { BookingConfirmationProps } from "@/types/components/hotelReservation/bookingConfirmation/bookingConfirmation"
|
||||||
|
|
||||||
export default async function Header({
|
export default async function Header({
|
||||||
confirmationNumber,
|
confirmationNumber,
|
||||||
}: BookingConfirmationProps) {
|
}: BookingConfirmationProps) {
|
||||||
const intl = await getIntl()
|
const intl = await getIntl()
|
||||||
const { hotel } = await getBookingConfirmation(confirmationNumber)
|
const { booking, hotel } = await getBookingConfirmation(confirmationNumber)
|
||||||
|
|
||||||
const text = intl.formatMessage<React.ReactNode>(
|
const text = intl.formatMessage<React.ReactNode>(
|
||||||
{ id: "booking.confirmation.text" },
|
{ 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 (
|
return (
|
||||||
<header className={styles.header}>
|
<header className={styles.header}>
|
||||||
<hgroup className={styles.hgroup}>
|
<hgroup className={styles.hgroup}>
|
||||||
@@ -39,7 +63,15 @@ export default async function Header({
|
|||||||
</Title>
|
</Title>
|
||||||
</hgroup>
|
</hgroup>
|
||||||
<Body className={styles.body}>{text}</Body>
|
<Body className={styles.body}>{text}</Body>
|
||||||
<Actions />
|
<div className={styles.actions}>
|
||||||
|
<AddToCalendar
|
||||||
|
checkInDate={booking.checkInDate}
|
||||||
|
event={event}
|
||||||
|
hotelName={hotel.name}
|
||||||
|
/>
|
||||||
|
<ManageBooking />
|
||||||
|
<DownloadInvoice />
|
||||||
|
</div>
|
||||||
</header>
|
</header>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
53
package-lock.json
generated
53
package-lock.json
generated
@@ -41,6 +41,7 @@
|
|||||||
"graphql": "^16.8.1",
|
"graphql": "^16.8.1",
|
||||||
"graphql-request": "^6.1.0",
|
"graphql-request": "^6.1.0",
|
||||||
"graphql-tag": "^2.12.6",
|
"graphql-tag": "^2.12.6",
|
||||||
|
"ics": "^3.8.1",
|
||||||
"immer": "10.1.1",
|
"immer": "10.1.1",
|
||||||
"json-stable-stringify-without-jsonify": "^1.0.1",
|
"json-stable-stringify-without-jsonify": "^1.0.1",
|
||||||
"libphonenumber-js": "^1.10.60",
|
"libphonenumber-js": "^1.10.60",
|
||||||
@@ -11794,6 +11795,16 @@
|
|||||||
"node": ">=0.10.0"
|
"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": {
|
"node_modules/icss-utils": {
|
||||||
"version": "5.1.0",
|
"version": "5.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz",
|
||||||
@@ -16840,6 +16851,11 @@
|
|||||||
"react-is": "^16.13.1"
|
"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": {
|
"node_modules/proto-list": {
|
||||||
"version": "1.2.4",
|
"version": "1.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz",
|
||||||
@@ -17859,6 +17875,11 @@
|
|||||||
"queue-microtask": "^1.2.2"
|
"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": {
|
"node_modules/rxjs": {
|
||||||
"version": "7.8.1",
|
"version": "7.8.1",
|
||||||
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz",
|
||||||
@@ -19048,6 +19069,11 @@
|
|||||||
"integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==",
|
"integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==",
|
||||||
"dev": true
|
"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": {
|
"node_modules/tmp": {
|
||||||
"version": "0.2.3",
|
"version": "0.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz",
|
||||||
@@ -19103,6 +19129,11 @@
|
|||||||
"node": ">=0.6"
|
"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": {
|
"node_modules/tough-cookie": {
|
||||||
"version": "4.1.3",
|
"version": "4.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.3.tgz",
|
||||||
@@ -20348,6 +20379,28 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"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": {
|
"node_modules/zod": {
|
||||||
"version": "3.22.4",
|
"version": "3.22.4",
|
||||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.22.4.tgz",
|
"resolved": "https://registry.npmjs.org/zod/-/zod-3.22.4.tgz",
|
||||||
|
|||||||
@@ -56,6 +56,7 @@
|
|||||||
"graphql": "^16.8.1",
|
"graphql": "^16.8.1",
|
||||||
"graphql-request": "^6.1.0",
|
"graphql-request": "^6.1.0",
|
||||||
"graphql-tag": "^2.12.6",
|
"graphql-tag": "^2.12.6",
|
||||||
|
"ics": "^3.8.1",
|
||||||
"immer": "10.1.1",
|
"immer": "10.1.1",
|
||||||
"json-stable-stringify-without-jsonify": "^1.0.1",
|
"json-stable-stringify-without-jsonify": "^1.0.1",
|
||||||
"libphonenumber-js": "^1.10.60",
|
"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