Merged in feat(SW-1275)-cancel-booking-my-stay (pull request #1376)
Feat(SW-1275) cancel booking my stay * feat(SW-1276) UI implementation Desktop part 1 for MyStay * feat(SW-1276) UI implementation Desktop part 2 for MyStay * feat(SW-1276) UI implementation Mobile part 1 for MyStay * refactor: move files from MyStay/MyStay to MyStay * feat(SW-1276) Sidepeek implementation * feat(SW-1276): Refactoring * feat(SW-1276) UI implementation Mobile part 2 for MyStay * feat(SW-1276): translations * feat(SW-1276) fixed skeleton * feat(SW-1276): Added missing translations * feat(SW-1276) fixed translations * feat(SW-1275) cancel modal * feat(SW-1275): Mutate cancel booking * feat(SW-1275) added translations * feat(SW-1275) match current cancellationReason * feat(SW-1275) Added modal for manage stay * feat(SW-1275) Added missing icon * feat(SW-1275) New Dont cancel button * feat(SW-1275) Added preperation for Cancellation number * feat(SW-1275): added --modal-box-shadow * feat(SW-1718) Add to calendar * feat(SW-1718) general add to calendar Approved-by: Niclas Edenvin
This commit is contained in:
50
components/HotelReservation/AddToCalendar/index.tsx
Normal file
50
components/HotelReservation/AddToCalendar/index.tsx
Normal file
@@ -0,0 +1,50 @@
|
||||
"use client"
|
||||
import { createEvent } from "ics"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { dt } from "@/lib/dt"
|
||||
|
||||
import { toast } from "@/components/TempDesignSystem/Toasts"
|
||||
import useLang from "@/hooks/useLang"
|
||||
|
||||
import type { AddToCalendarProps } from "@/types/components/hotelReservation/bookingConfirmation/actions/addToCalendar"
|
||||
|
||||
export default function AddToCalendar({
|
||||
checkInDate,
|
||||
event,
|
||||
hotelName,
|
||||
renderButton,
|
||||
}: AddToCalendarProps) {
|
||||
const lang = useLang()
|
||||
const intl = useIntl()
|
||||
|
||||
async function downloadBooking() {
|
||||
try {
|
||||
const d = dt(checkInDate).locale(lang).format("YYYY-MM-DD")
|
||||
const filename = `${hotelName.toLowerCase().split(" ").join("_")}-${d}.ics`
|
||||
|
||||
createEvent(event, (error, value) => {
|
||||
if (error) {
|
||||
console.error("ICS Error:", error)
|
||||
toast.error(intl.formatMessage({ id: "Failed to add to calendar" }))
|
||||
return
|
||||
}
|
||||
|
||||
const file = 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)
|
||||
})
|
||||
} catch (error) {
|
||||
console.error("Download error:", error)
|
||||
toast.error(intl.formatMessage({ id: "Failed to add to calendar" }))
|
||||
}
|
||||
}
|
||||
|
||||
return renderButton(downloadBooking)
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
"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,20 @@
|
||||
"use client"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { CalendarAddIcon } from "@/components/Icons"
|
||||
import Button from "@/components/TempDesignSystem/Button"
|
||||
|
||||
export default function AddToCalendarButton({
|
||||
onPress,
|
||||
}: {
|
||||
onPress: () => void
|
||||
}) {
|
||||
const intl = useIntl()
|
||||
|
||||
return (
|
||||
<Button intent="text" size="small" theme="base" wrapping onPress={onPress}>
|
||||
<CalendarAddIcon />
|
||||
{intl.formatMessage({ id: "Add to calendar" })}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
@@ -6,7 +6,8 @@ import Body from "@/components/TempDesignSystem/Text/Body"
|
||||
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
||||
import Title from "@/components/TempDesignSystem/Text/Title"
|
||||
|
||||
import AddToCalendar from "./Actions/AddToCalendar"
|
||||
import AddToCalendar from "../../AddToCalendar"
|
||||
import AddToCalendarButton from "./Actions/AddToCalendarButton"
|
||||
import DownloadInvoice from "./Actions/DownloadInvoice"
|
||||
import { generateDateTime } from "./Actions/helpers"
|
||||
import ManageBooking from "./Actions/ManageBooking"
|
||||
@@ -80,6 +81,7 @@ export default function Header({
|
||||
checkInDate={booking.checkInDate}
|
||||
event={event}
|
||||
hotelName={hotel.name}
|
||||
renderButton={(onPress) => <AddToCalendarButton onPress={onPress} />}
|
||||
/>
|
||||
<ManageBooking
|
||||
confirmationNumber={booking.confirmationNumber}
|
||||
|
||||
@@ -23,11 +23,18 @@ export default async function BookingConfirmation({
|
||||
confirmationNumber,
|
||||
}: BookingConfirmationProps) {
|
||||
const lang = getLang()
|
||||
const { booking, hotel, room } =
|
||||
await getBookingConfirmation(confirmationNumber)
|
||||
const bookingConfirmation = await getBookingConfirmation(confirmationNumber)
|
||||
|
||||
if (!bookingConfirmation) {
|
||||
return notFound()
|
||||
}
|
||||
|
||||
const { booking, hotel, room } = bookingConfirmation
|
||||
|
||||
if (!room) {
|
||||
return notFound()
|
||||
}
|
||||
|
||||
const arrivalDate = new Date(booking.checkInDate)
|
||||
const departureDate = new Date(booking.checkOutDate)
|
||||
|
||||
@@ -62,7 +69,7 @@ export default async function BookingConfirmation({
|
||||
noOfAdults: booking.adults,
|
||||
noOfChildren: booking.childrenAges?.length,
|
||||
ageOfChildren: booking.childrenAges?.join(","),
|
||||
childBedPreference: booking?.extraBedTypes
|
||||
childBedPreference: booking?.childBedPreferences
|
||||
?.flatMap((c) => Array(c.quantity).fill(invertedBedTypeMap[c.bedType]))
|
||||
.join("|"),
|
||||
noOfRooms: 1, // // TODO: Handle multiple rooms
|
||||
|
||||
@@ -51,7 +51,7 @@
|
||||
.dialog {
|
||||
background-color: var(--Scandic-Brand-Pale-Peach);
|
||||
border-radius: var(--Corner-radius-Medium);
|
||||
box-shadow: 0px 4px 24px 0px rgba(38, 32, 30, 0.08);
|
||||
box-shadow: var(--modal-box-shadow);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Spacing-x2);
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
||||
|
||||
import styles from "../cancelStay.module.css"
|
||||
|
||||
import type { Hotel } from "@/types/hotel"
|
||||
import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation"
|
||||
|
||||
interface CancelStayConfirmationProps {
|
||||
hotel: Hotel
|
||||
booking: BookingConfirmation["booking"]
|
||||
stayDetails: {
|
||||
checkInDate: string
|
||||
checkOutDate: string
|
||||
nightsText: string
|
||||
adultsText: string
|
||||
childrenText: string
|
||||
}
|
||||
}
|
||||
|
||||
export function CancelStayConfirmation({
|
||||
hotel,
|
||||
booking,
|
||||
stayDetails,
|
||||
}: CancelStayConfirmationProps) {
|
||||
const intl = useIntl()
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={styles.modalText}>
|
||||
<Body color="uiTextHighContrast">
|
||||
{intl.formatMessage(
|
||||
{
|
||||
id: "Are you sure you want to cancel your stay at {hotel} from {checkInDate} to {checkOutDate}? This can't be reversed.",
|
||||
},
|
||||
{
|
||||
hotel: hotel.name,
|
||||
checkInDate: stayDetails.checkInDate,
|
||||
checkOutDate: stayDetails.checkOutDate,
|
||||
}
|
||||
)}
|
||||
</Body>
|
||||
<Caption color="uiTextHighContrast">
|
||||
{intl.formatMessage({ id: "No charges were made." })}
|
||||
</Caption>
|
||||
</div>
|
||||
<div className={styles.priceContainer}>
|
||||
<div className={styles.info}>
|
||||
<Caption color="uiTextHighContrast" type="bold">
|
||||
{intl.formatMessage({ id: "Cancellation cost" })}
|
||||
</Caption>
|
||||
<Caption color="uiTextHighContrast">
|
||||
{stayDetails.nightsText}, {stayDetails.adultsText}
|
||||
{booking.childrenAges?.length > 0
|
||||
? `, ${stayDetails.childrenText}`
|
||||
: ""}
|
||||
</Caption>
|
||||
</div>
|
||||
<div className={styles.price}>
|
||||
<Subtitle color="burgundy" type="one">
|
||||
0 {booking.currencyCode}
|
||||
</Subtitle>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
||||
|
||||
import styles from "../cancelStay.module.css"
|
||||
|
||||
import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation"
|
||||
|
||||
interface FinalConfirmationProps {
|
||||
booking: BookingConfirmation["booking"]
|
||||
stayDetails: {
|
||||
nightsText: string
|
||||
adultsText: string
|
||||
childrenText: string
|
||||
}
|
||||
}
|
||||
|
||||
export function FinalConfirmation({
|
||||
booking,
|
||||
stayDetails,
|
||||
}: FinalConfirmationProps) {
|
||||
const intl = useIntl()
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={styles.modalText}>
|
||||
<Body color="uiTextHighContrast">
|
||||
{intl.formatMessage({
|
||||
id: "Are you sure you want to continue with the cancellation?",
|
||||
})}
|
||||
</Body>
|
||||
</div>
|
||||
<div className={styles.priceContainer}>
|
||||
<div className={styles.info}>
|
||||
<Caption color="uiTextHighContrast" type="bold">
|
||||
{intl.formatMessage({ id: "Cancellation cost" })}
|
||||
</Caption>
|
||||
<Caption color="uiTextHighContrast">
|
||||
{stayDetails.nightsText}, {stayDetails.adultsText}
|
||||
{booking.childrenAges?.length > 0
|
||||
? `, ${stayDetails.childrenText}`
|
||||
: ""}
|
||||
</Caption>
|
||||
</div>
|
||||
<div className={styles.price}>
|
||||
<Subtitle color="burgundy" type="one">
|
||||
0 {booking.currencyCode}
|
||||
</Subtitle>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
.modalText {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Spacing-x2);
|
||||
}
|
||||
|
||||
.priceContainer {
|
||||
display: flex;
|
||||
padding: var(--Spacing-x2);
|
||||
background-color: var(--Base-Background-Primary-Normal);
|
||||
border-radius: var(--Corner-radius-Medium);
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.info {
|
||||
border-right: 1px solid var(--Base-Border-Subtle);
|
||||
padding-right: var(--Spacing-x2);
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.price {
|
||||
padding-left: var(--Spacing-x2);
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
import { useState } from "react"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { trpc } from "@/lib/trpc/client"
|
||||
|
||||
import { toast } from "@/components/TempDesignSystem/Toasts"
|
||||
import useLang from "@/hooks/useLang"
|
||||
|
||||
import type { CancelStayProps } from ".."
|
||||
|
||||
export default function useCancelStay({
|
||||
booking,
|
||||
setBookingStatus,
|
||||
handleCloseModal,
|
||||
handleBackToManageStay,
|
||||
}: Omit<CancelStayProps, "hotel">) {
|
||||
const intl = useIntl()
|
||||
const lang = useLang()
|
||||
const [currentStep, setCurrentStep] = useState(1)
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
|
||||
const cancelStay = trpc.booking.cancel.useMutation({
|
||||
onMutate: () => setIsLoading(true),
|
||||
onSuccess: (result) => {
|
||||
if (!result) {
|
||||
toast.error(
|
||||
intl.formatMessage({
|
||||
id: "Something went wrong. Please try again later.",
|
||||
})
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
setBookingStatus()
|
||||
toast.success(
|
||||
intl.formatMessage(
|
||||
{
|
||||
id: "Your stay was cancelled. Cancellation cost: 0 {currency}. We’re sorry to see that the plans didn’t work out",
|
||||
},
|
||||
{ currency: booking.currencyCode }
|
||||
)
|
||||
)
|
||||
},
|
||||
onError: () => {
|
||||
toast.error(
|
||||
intl.formatMessage({
|
||||
id: "Something went wrong. Please try again later.",
|
||||
})
|
||||
)
|
||||
},
|
||||
onSettled: () => {
|
||||
handleCloseModal()
|
||||
},
|
||||
})
|
||||
|
||||
function handleCancelStay() {
|
||||
if (!booking.confirmationNumber) {
|
||||
toast.error(
|
||||
intl.formatMessage({
|
||||
id: "Something went wrong. Please try again later.",
|
||||
})
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
cancelStay.mutate({
|
||||
confirmationNumber: booking.confirmationNumber,
|
||||
language: lang,
|
||||
})
|
||||
}
|
||||
|
||||
function handleCloseCancelStay() {
|
||||
setCurrentStep(1)
|
||||
setIsLoading(false)
|
||||
handleBackToManageStay()
|
||||
}
|
||||
|
||||
return {
|
||||
currentStep,
|
||||
isLoading,
|
||||
handleCancelStay,
|
||||
handleCloseCancelStay,
|
||||
handleBack: () => setCurrentStep(1),
|
||||
handleForward: () => setCurrentStep(2),
|
||||
}
|
||||
}
|
||||
87
components/HotelReservation/MyStay/CancelStay/index.tsx
Normal file
87
components/HotelReservation/MyStay/CancelStay/index.tsx
Normal file
@@ -0,0 +1,87 @@
|
||||
"use client"
|
||||
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import useLang from "@/hooks/useLang"
|
||||
|
||||
import { ModalContent } from "../ManageStay/ModalContent"
|
||||
import useCancelStay from "./hooks/useCancelStay"
|
||||
import { CancelStayConfirmation } from "./Confirmation"
|
||||
import { FinalConfirmation } from "./FinalConfirmation"
|
||||
import { formatStayDetails } from "./utils"
|
||||
|
||||
import type { Hotel } from "@/types/hotel"
|
||||
import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation"
|
||||
|
||||
export interface CancelStayProps {
|
||||
booking: BookingConfirmation["booking"]
|
||||
hotel: Hotel
|
||||
setBookingStatus: () => void
|
||||
handleCloseModal: () => void
|
||||
handleBackToManageStay: () => void
|
||||
}
|
||||
|
||||
export default function CancelStay({
|
||||
booking,
|
||||
hotel,
|
||||
setBookingStatus,
|
||||
handleCloseModal,
|
||||
handleBackToManageStay,
|
||||
}: CancelStayProps) {
|
||||
const intl = useIntl()
|
||||
const lang = useLang()
|
||||
const {
|
||||
currentStep,
|
||||
isLoading,
|
||||
handleCancelStay,
|
||||
handleCloseCancelStay,
|
||||
handleForward,
|
||||
} = useCancelStay({
|
||||
booking,
|
||||
setBookingStatus,
|
||||
handleCloseModal,
|
||||
handleBackToManageStay,
|
||||
})
|
||||
|
||||
const stayDetails = formatStayDetails({ booking, lang, intl })
|
||||
const isFirstStep = currentStep === 1
|
||||
|
||||
return (
|
||||
<>
|
||||
<ModalContent
|
||||
title={
|
||||
isFirstStep
|
||||
? intl.formatMessage({ id: "Cancel stay" })
|
||||
: intl.formatMessage({ id: "Confirm cancellation" })
|
||||
}
|
||||
onClose={handleCloseModal}
|
||||
content={
|
||||
isFirstStep ? (
|
||||
<CancelStayConfirmation
|
||||
hotel={hotel}
|
||||
booking={booking}
|
||||
stayDetails={stayDetails}
|
||||
/>
|
||||
) : (
|
||||
<FinalConfirmation booking={booking} stayDetails={stayDetails} />
|
||||
)
|
||||
}
|
||||
primaryAction={{
|
||||
label: isFirstStep
|
||||
? intl.formatMessage({ id: "Cancel stay" })
|
||||
: intl.formatMessage({ id: "Confirm cancellation" }),
|
||||
onClick: isFirstStep ? handleForward : handleCancelStay,
|
||||
intent: isFirstStep ? "secondary" : "primary",
|
||||
isLoading: isLoading,
|
||||
}}
|
||||
secondaryAction={{
|
||||
label: isFirstStep
|
||||
? intl.formatMessage({ id: "Back" })
|
||||
: intl.formatMessage({ id: "Don't cancel" }),
|
||||
onClick: isFirstStep ? handleCloseCancelStay : handleCloseModal,
|
||||
intent: "text",
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
44
components/HotelReservation/MyStay/CancelStay/utils.ts
Normal file
44
components/HotelReservation/MyStay/CancelStay/utils.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { dt } from "@/lib/dt"
|
||||
|
||||
import type { IntlShape } from "react-intl"
|
||||
|
||||
import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation"
|
||||
|
||||
export function formatStayDetails({
|
||||
booking,
|
||||
lang,
|
||||
intl,
|
||||
}: {
|
||||
booking: BookingConfirmation["booking"]
|
||||
lang: string
|
||||
intl: IntlShape
|
||||
}) {
|
||||
const checkInDate = dt(booking.checkInDate)
|
||||
.locale(lang)
|
||||
.format("dddd D MMM YYYY")
|
||||
const checkOutDate = dt(booking.checkOutDate)
|
||||
.locale(lang)
|
||||
.format("dddd D MMM YYYY")
|
||||
const diff = dt(checkOutDate).diff(checkInDate, "days")
|
||||
|
||||
const nightsText = intl.formatMessage(
|
||||
{ id: "{totalNights, plural, one {# night} other {# nights}}" },
|
||||
{ totalNights: diff }
|
||||
)
|
||||
const adultsText = intl.formatMessage(
|
||||
{ id: "{totalAdults, plural, one {# adult} other {# adults}}" },
|
||||
{ totalAdults: booking.adults }
|
||||
)
|
||||
const childrenText = intl.formatMessage(
|
||||
{ id: "{totalChildren, plural, one {# child} other {# children}}" },
|
||||
{ totalChildren: booking.childrenAges?.length }
|
||||
)
|
||||
|
||||
return {
|
||||
checkInDate,
|
||||
checkOutDate,
|
||||
nightsText,
|
||||
adultsText,
|
||||
childrenText,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
"use client"
|
||||
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { CalendarAddIcon } from "@/components/Icons"
|
||||
import Button from "@/components/TempDesignSystem/Button"
|
||||
|
||||
import styles from "../actionPanel.module.css"
|
||||
|
||||
export default function AddToCalendarButton({
|
||||
onPress,
|
||||
}: {
|
||||
onPress: () => void
|
||||
}) {
|
||||
const intl = useIntl()
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant="icon"
|
||||
intent="text"
|
||||
theme="base"
|
||||
className={styles.button}
|
||||
onPress={onPress}
|
||||
>
|
||||
{intl.formatMessage({ id: "Add to calendar" })}
|
||||
<CalendarAddIcon width={24} height={24} color="burgundy" />
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
.actionPanel {
|
||||
display: flex;
|
||||
gap: var(--Spacing-x3);
|
||||
padding: var(--Spacing-x3);
|
||||
}
|
||||
|
||||
.menu {
|
||||
width: 432px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Spacing-x2);
|
||||
}
|
||||
|
||||
.actionPanel .menu .button {
|
||||
width: 100%;
|
||||
color: var(--Scandic-Brand-Burgundy);
|
||||
justify-content: space-between !important;
|
||||
padding: var(--Spacing-x1) 0 !important;
|
||||
}
|
||||
|
||||
.info {
|
||||
width: 256px;
|
||||
background-color: var(--Base-Background-Primary-Normal);
|
||||
padding: var(--Spacing-x3);
|
||||
text-align: right;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Spacing-x2);
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.tag {
|
||||
text-transform: uppercase;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--Main-Red-60);
|
||||
font-family: var(--typography-Caption-Labels-fontFamily);
|
||||
}
|
||||
|
||||
.link {
|
||||
margin-top: auto;
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { customerService } from "@/constants/currentWebHrefs"
|
||||
|
||||
import AddToCalendar from "@/components/HotelReservation/AddToCalendar"
|
||||
import { generateDateTime } from "@/components/HotelReservation/BookingConfirmation/Header/Actions/helpers"
|
||||
import {
|
||||
CalendarIcon,
|
||||
ChevronRightIcon,
|
||||
CreditCard,
|
||||
CrossCircleOutlineIcon,
|
||||
DownloadIcon,
|
||||
} from "@/components/Icons"
|
||||
import Button from "@/components/TempDesignSystem/Button"
|
||||
import Link from "@/components/TempDesignSystem/Link"
|
||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
||||
import useLang from "@/hooks/useLang"
|
||||
|
||||
import AddToCalendarButton from "./Actions/AddToCalendarButton"
|
||||
|
||||
import styles from "./actionPanel.module.css"
|
||||
|
||||
import type { EventAttributes } from "ics"
|
||||
|
||||
import type { Hotel } from "@/types/hotel"
|
||||
import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation"
|
||||
|
||||
export default function ActionPanel({
|
||||
booking,
|
||||
hotel,
|
||||
showCancelButton,
|
||||
onCancelClick,
|
||||
}: {
|
||||
booking: BookingConfirmation["booking"]
|
||||
hotel: Hotel
|
||||
showCancelButton: boolean
|
||||
onCancelClick: () => void
|
||||
}) {
|
||||
const intl = useIntl()
|
||||
const lang = useLang()
|
||||
|
||||
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 (
|
||||
<div className={styles.actionPanel}>
|
||||
<div className={styles.menu}>
|
||||
<Button
|
||||
variant="icon"
|
||||
onClick={onCancelClick}
|
||||
intent="text"
|
||||
className={styles.button}
|
||||
>
|
||||
{intl.formatMessage({ id: "Modify dates" })}
|
||||
<CalendarIcon width={24} height={24} color="burgundy" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="icon"
|
||||
onClick={onCancelClick}
|
||||
intent="text"
|
||||
className={styles.button}
|
||||
>
|
||||
{intl.formatMessage({ id: "Guarantee late arrival" })}
|
||||
<CreditCard width={24} height={24} color="burgundy" />
|
||||
</Button>
|
||||
<AddToCalendar
|
||||
checkInDate={booking.checkInDate}
|
||||
event={event}
|
||||
hotelName={hotel.name}
|
||||
renderButton={(onPress) => <AddToCalendarButton onPress={onPress} />}
|
||||
/>
|
||||
<Button
|
||||
variant="icon"
|
||||
onClick={onCancelClick}
|
||||
intent="text"
|
||||
className={styles.button}
|
||||
>
|
||||
{intl.formatMessage({ id: "Download invoice" })}
|
||||
<DownloadIcon width={24} height={24} color="burgundy" />
|
||||
</Button>
|
||||
{showCancelButton && (
|
||||
<Button
|
||||
variant="icon"
|
||||
onClick={onCancelClick}
|
||||
intent="text"
|
||||
className={styles.button}
|
||||
>
|
||||
{intl.formatMessage({ id: "Cancel stay" })}
|
||||
<CrossCircleOutlineIcon width={24} height={24} color="burgundy" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.info}>
|
||||
<div>
|
||||
<span className={styles.tag}>
|
||||
{intl.formatMessage({ id: "Reference number" })}
|
||||
</span>
|
||||
<Subtitle color="burgundy" textAlign="right">
|
||||
{booking.confirmationNumber}
|
||||
</Subtitle>
|
||||
</div>
|
||||
<div className={styles.hotel}>
|
||||
<Body color="uiTextHighContrast" textAlign="right">
|
||||
{hotel.name}
|
||||
</Body>
|
||||
<Body color="uiTextHighContrast" textAlign="right">
|
||||
{hotel.address.streetAddress}
|
||||
</Body>
|
||||
<Body color="uiTextHighContrast" textAlign="right">
|
||||
{hotel.address.city}
|
||||
</Body>
|
||||
<Body color="uiTextHighContrast" asChild>
|
||||
<Link href={`tel:${hotel.contactInformation.phoneNumber}`}>
|
||||
{hotel.contactInformation.phoneNumber}
|
||||
</Link>
|
||||
</Body>
|
||||
</div>
|
||||
<Link
|
||||
href={customerService[lang]}
|
||||
variant="icon"
|
||||
className={styles.link}
|
||||
>
|
||||
<Caption color="burgundy">
|
||||
{intl.formatMessage({ id: "Customer support" })}
|
||||
</Caption>
|
||||
<ChevronRightIcon width={20} height={20} color="burgundy" />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import { CloseLargeIcon } from "@/components/Icons"
|
||||
import Button from "@/components/TempDesignSystem/Button"
|
||||
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
||||
|
||||
import styles from "./modalContent.module.css"
|
||||
|
||||
import type { ReactNode } from "react"
|
||||
|
||||
interface ModalContentProps {
|
||||
title: string
|
||||
content: ReactNode
|
||||
primaryAction: {
|
||||
label: string
|
||||
onClick: () => void
|
||||
intent?: "primary" | "secondary" | "text"
|
||||
isLoading?: boolean
|
||||
}
|
||||
secondaryAction: {
|
||||
label: string
|
||||
onClick: () => void
|
||||
intent?: "primary" | "secondary" | "text"
|
||||
}
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
export function ModalContent({
|
||||
title,
|
||||
content,
|
||||
primaryAction,
|
||||
secondaryAction,
|
||||
onClose,
|
||||
}: ModalContentProps) {
|
||||
return (
|
||||
<>
|
||||
<header className={styles.header}>
|
||||
<Subtitle color="uiTextHighContrast">{title}</Subtitle>
|
||||
<button onClick={onClose} type="button" className={styles.close}>
|
||||
<CloseLargeIcon color="uiTextMediumContrast" />
|
||||
</button>
|
||||
</header>
|
||||
<div className={styles.content}>{content}</div>
|
||||
<footer className={styles.footer}>
|
||||
<Button
|
||||
theme="base"
|
||||
intent={secondaryAction.intent ?? "text"}
|
||||
color="burgundy"
|
||||
onClick={secondaryAction.onClick}
|
||||
>
|
||||
{secondaryAction.label}
|
||||
</Button>
|
||||
<Button
|
||||
theme="base"
|
||||
intent={primaryAction.intent ?? "secondary"}
|
||||
onClick={primaryAction.onClick}
|
||||
disabled={primaryAction.isLoading}
|
||||
>
|
||||
{primaryAction.label}
|
||||
</Button>
|
||||
</footer>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
.content {
|
||||
width: 640px;
|
||||
max-width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Spacing-x3);
|
||||
padding: var(--Spacing-x1) var(--Spacing-x3) var(--Spacing-x4);
|
||||
}
|
||||
|
||||
.header {
|
||||
position: relative;
|
||||
padding: var(--Spacing-x3) var(--Spacing-x3) 0;
|
||||
}
|
||||
|
||||
.footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
border-top: 1px solid var(--Base-Border-Subtle);
|
||||
padding: var(--Spacing-x3);
|
||||
}
|
||||
|
||||
.close {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0;
|
||||
justify-content: center;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
}
|
||||
136
components/HotelReservation/MyStay/ManageStay/index.tsx
Normal file
136
components/HotelReservation/MyStay/ManageStay/index.tsx
Normal file
@@ -0,0 +1,136 @@
|
||||
"use client"
|
||||
import { motion } from "framer-motion"
|
||||
import { useEffect, useState } from "react"
|
||||
import { Dialog, Modal, ModalOverlay } from "react-aria-components"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { BookingStatusEnum } from "@/constants/booking"
|
||||
|
||||
import { ChevronDownIcon } from "@/components/Icons"
|
||||
import {
|
||||
type AnimationState,
|
||||
AnimationStateEnum,
|
||||
} from "@/components/Modal/modal"
|
||||
import { slideFromTop } from "@/components/Modal/motionVariants"
|
||||
import Button from "@/components/TempDesignSystem/Button"
|
||||
|
||||
import CancelStay from "../CancelStay"
|
||||
import ActionPanel from "./ActionPanel"
|
||||
|
||||
import styles from "./modifyModal.module.css"
|
||||
|
||||
import type { Hotel } from "@/types/hotel"
|
||||
import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation"
|
||||
|
||||
type ActiveView = "actionPanel" | "cancelStay"
|
||||
|
||||
export default function ManageStay({
|
||||
booking,
|
||||
hotel,
|
||||
setBookingStatus,
|
||||
bookingStatus,
|
||||
}: {
|
||||
booking: BookingConfirmation["booking"]
|
||||
hotel: Hotel
|
||||
setBookingStatus: (status: BookingStatusEnum) => void
|
||||
bookingStatus: string | null
|
||||
}) {
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const [animation, setAnimation] = useState<AnimationState>(
|
||||
AnimationStateEnum.visible
|
||||
)
|
||||
const [activeView, setActiveView] = useState<ActiveView>("actionPanel")
|
||||
|
||||
const intl = useIntl()
|
||||
|
||||
const MotionOverlay = motion(ModalOverlay)
|
||||
const MotionModal = motion(Modal)
|
||||
|
||||
const showCancelButton =
|
||||
bookingStatus !== BookingStatusEnum.Cancelled && booking.isCancelable
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof isOpen === "boolean") {
|
||||
setAnimation(
|
||||
isOpen ? AnimationStateEnum.visible : AnimationStateEnum.hidden
|
||||
)
|
||||
}
|
||||
if (isOpen === undefined) {
|
||||
setAnimation(AnimationStateEnum.unmounted)
|
||||
}
|
||||
}, [isOpen])
|
||||
|
||||
function modalStateHandler(newAnimationState: AnimationState) {
|
||||
setAnimation((currentAnimationState) =>
|
||||
newAnimationState === AnimationStateEnum.hidden &&
|
||||
currentAnimationState === AnimationStateEnum.hidden
|
||||
? AnimationStateEnum.unmounted
|
||||
: currentAnimationState
|
||||
)
|
||||
}
|
||||
|
||||
function handleClose() {
|
||||
setIsOpen(false)
|
||||
setActiveView("actionPanel")
|
||||
}
|
||||
function handleBack() {
|
||||
setActiveView("actionPanel")
|
||||
}
|
||||
|
||||
function renderContent() {
|
||||
switch (activeView) {
|
||||
case "cancelStay":
|
||||
return (
|
||||
<CancelStay
|
||||
booking={booking}
|
||||
hotel={hotel}
|
||||
setBookingStatus={() =>
|
||||
setBookingStatus(BookingStatusEnum.Cancelled)
|
||||
}
|
||||
handleCloseModal={handleClose}
|
||||
handleBackToManageStay={handleBack}
|
||||
/>
|
||||
)
|
||||
default:
|
||||
return (
|
||||
<ActionPanel
|
||||
booking={booking}
|
||||
hotel={hotel}
|
||||
onCancelClick={() => setActiveView("cancelStay")}
|
||||
showCancelButton={showCancelButton}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button variant="icon" fullWidth onClick={() => setIsOpen(true)}>
|
||||
{intl.formatMessage({ id: "Manage stay" })}
|
||||
<ChevronDownIcon width={24} height={24} color="burgundy" />
|
||||
</Button>
|
||||
<MotionOverlay
|
||||
isOpen={isOpen}
|
||||
className={styles.overlay}
|
||||
initial={"hidden"}
|
||||
onAnimationComplete={modalStateHandler}
|
||||
onOpenChange={handleClose}
|
||||
isDismissable
|
||||
>
|
||||
<MotionModal
|
||||
className={styles.modal}
|
||||
initial={"hidden"}
|
||||
animate={animation}
|
||||
variants={slideFromTop}
|
||||
>
|
||||
<Dialog
|
||||
className={styles.dialog}
|
||||
aria-label={intl.formatMessage({ id: "Dialog" })}
|
||||
>
|
||||
{renderContent()}
|
||||
</Dialog>
|
||||
</MotionModal>
|
||||
</MotionOverlay>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
.overlay {
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
height: var(--visual-viewport-height);
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
z-index: var(--default-modal-overlay-z-index);
|
||||
}
|
||||
|
||||
.modal {
|
||||
background-color: var(--Base-Surface-Primary-light-Normal);
|
||||
border-radius: var(--Corner-radius-Medium) var(--Corner-radius-Medium) 0 0;
|
||||
box-shadow: var(--modal-box-shadow);
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
z-index: var(--default-modal-z-index);
|
||||
}
|
||||
|
||||
.dialog {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
/* For removing focus outline when modal opens first time */
|
||||
outline: 0 none;
|
||||
|
||||
/* for supporting animations within content */
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.close {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
right: var(--Spacing-x2);
|
||||
width: var(--button-dimension);
|
||||
height: var(--button-dimension);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
.overlay {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.modal {
|
||||
left: auto;
|
||||
bottom: auto;
|
||||
width: auto;
|
||||
border-radius: var(--Corner-radius-Medium);
|
||||
max-width: var(--max-width-page);
|
||||
}
|
||||
}
|
||||
@@ -1,32 +1,46 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { BookingStatusEnum } from "@/constants/booking"
|
||||
import { dt } from "@/lib/dt"
|
||||
|
||||
import CrossCircleIcon from "@/components/Icons/CrossCircle"
|
||||
import Button from "@/components/TempDesignSystem/Button"
|
||||
import Divider from "@/components/TempDesignSystem/Divider"
|
||||
import IconChip from "@/components/TempDesignSystem/IconChip"
|
||||
import Link from "@/components/TempDesignSystem/Link"
|
||||
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
||||
import { getIntl } from "@/i18n"
|
||||
import { getLang } from "@/i18n/serverContext"
|
||||
import useLang from "@/hooks/useLang"
|
||||
import { formatPrice } from "@/utils/numberFormatting"
|
||||
|
||||
import ManageStay from "../ManageStay"
|
||||
|
||||
import styles from "./referenceCard.module.css"
|
||||
|
||||
import type { Hotel } from "@/types/hotel"
|
||||
import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation"
|
||||
|
||||
export async function ReferenceCard({
|
||||
export function ReferenceCard({
|
||||
booking,
|
||||
hotel,
|
||||
}: {
|
||||
booking: BookingConfirmation["booking"]
|
||||
hotel: Hotel
|
||||
}) {
|
||||
const intl = await getIntl()
|
||||
const lang = getLang()
|
||||
const [bookingStatus, setBookingStatus] = useState(booking.reservationStatus)
|
||||
const intl = useIntl()
|
||||
const lang = useLang()
|
||||
|
||||
const fromDate = dt(booking.checkInDate).locale(lang)
|
||||
const toDate = dt(booking.checkOutDate).locale(lang)
|
||||
|
||||
const isCancelled = bookingStatus === BookingStatusEnum.Cancelled
|
||||
|
||||
const showCancelButton = !isCancelled && booking.isCancelable
|
||||
|
||||
const directionsUrl = `https://www.google.com/maps/dir/?api=1&destination=${hotel.location.latitude},${hotel.location.longitude}`
|
||||
|
||||
return (
|
||||
@@ -36,10 +50,15 @@ export async function ReferenceCard({
|
||||
{intl.formatMessage({ id: "Reference" })}
|
||||
</Subtitle>
|
||||
<Subtitle color="uiTextHighContrast" className={styles.titleDesktop}>
|
||||
{intl.formatMessage({ id: "Reference number" })}
|
||||
{isCancelled
|
||||
? intl.formatMessage({ id: "Cancellation number" })
|
||||
: intl.formatMessage({ id: "Reference number" })}
|
||||
</Subtitle>
|
||||
<Subtitle color="uiTextHighContrast">
|
||||
{booking.confirmationNumber}
|
||||
{/* TODO: Implement this: https://scandichotels.atlassian.net/browse/API2-2883 to get correct cancellation number */}
|
||||
{isCancelled
|
||||
? booking.linkedReservations[0]?.cancellationNumber
|
||||
: booking.confirmationNumber}
|
||||
</Subtitle>
|
||||
</div>
|
||||
<Divider color="primaryLightSubtle" className={styles.divider} />
|
||||
@@ -105,8 +124,26 @@ export async function ReferenceCard({
|
||||
{formatPrice(intl, booking.totalPrice, booking.currencyCode)}
|
||||
</Caption>
|
||||
</div>
|
||||
{!showCancelButton && (
|
||||
<div className={styles.referenceRow}>
|
||||
<IconChip
|
||||
color={"red"}
|
||||
icon={<CrossCircleIcon width={20} height={20} color="red" />}
|
||||
>
|
||||
<Caption color={"red"}>
|
||||
<strong>{intl.formatMessage({ id: "Status" })}:</strong>{" "}
|
||||
{intl.formatMessage({ id: "Cancelled" })}
|
||||
</Caption>
|
||||
</IconChip>
|
||||
</div>
|
||||
)}
|
||||
<div className={styles.actionArea}>
|
||||
<Button fullWidth>{intl.formatMessage({ id: "Manage stay" })}</Button>
|
||||
<ManageStay
|
||||
booking={booking}
|
||||
hotel={hotel}
|
||||
setBookingStatus={setBookingStatus}
|
||||
bookingStatus={bookingStatus}
|
||||
/>
|
||||
<Button fullWidth intent="secondary" asChild>
|
||||
<Link href={directionsUrl} target="_blank">
|
||||
{intl.formatMessage({ id: "Get directions" })}
|
||||
|
||||
@@ -134,7 +134,7 @@ export function Room({ booking, room, hotel, user }: RoomProps) {
|
||||
</span>
|
||||
<div className={styles.rowContent}>
|
||||
<Body color="uiTextHighContrast">
|
||||
{booking.rateDefinition.title}
|
||||
{booking.rateDefinition.cancellationText}
|
||||
</Body>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { notFound } from "next/navigation"
|
||||
|
||||
import { homeHrefs } from "@/constants/homeHrefs"
|
||||
import { env } from "@/env/server"
|
||||
import { dt } from "@/lib/dt"
|
||||
@@ -21,7 +23,13 @@ import { Room } from "./Room"
|
||||
import styles from "./myStay.module.css"
|
||||
|
||||
export async function MyStay({ reservationId }: { reservationId: string }) {
|
||||
const { booking, hotel, room } = await getBookingConfirmation(reservationId)
|
||||
const bookingConfirmation = await getBookingConfirmation(reservationId)
|
||||
if (!bookingConfirmation) {
|
||||
return notFound()
|
||||
}
|
||||
|
||||
const { booking, hotel, room } = bookingConfirmation
|
||||
|
||||
const userResponse = await getProfileSafely()
|
||||
const user = userResponse && !("error" in userResponse) ? userResponse : null
|
||||
const intl = await getIntl()
|
||||
@@ -38,14 +46,17 @@ export async function MyStay({ reservationId }: { reservationId: string }) {
|
||||
<main className={styles.main}>
|
||||
<div className={styles.imageContainer}>
|
||||
<div className={styles.blurOverlay} />
|
||||
{hotel.gallery?.heroImages[0].imageSizes.large && (
|
||||
<Image
|
||||
className={styles.image}
|
||||
src={hotel.gallery.heroImages[0].imageSizes.large}
|
||||
alt={hotel.name}
|
||||
fill
|
||||
/>
|
||||
)}
|
||||
|
||||
<Image
|
||||
className={styles.image}
|
||||
src={
|
||||
hotel.gallery?.heroImages[0]?.imageSizes.large ??
|
||||
room?.images[0]?.imageSizes.large ??
|
||||
""
|
||||
}
|
||||
alt={hotel.name}
|
||||
fill
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.content}>
|
||||
<div className={styles.headerContainer}>
|
||||
|
||||
@@ -52,6 +52,13 @@
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.content {
|
||||
width: var(--max-width-content);
|
||||
padding-bottom: 160px;
|
||||
}
|
||||
}
|
||||
|
||||
.headerSkeleton {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
height: calc(100dvh - 20px);
|
||||
background-color: var(--Base-Surface-Primary-light-Normal);
|
||||
border-radius: var(--Corner-radius-Medium);
|
||||
box-shadow: 0px 4px 24px 0px rgba(38, 32, 30, 0.08);
|
||||
box-shadow: var(--modal-box-shadow);
|
||||
width: 100%;
|
||||
|
||||
&[data-entering] {
|
||||
|
||||
Reference in New Issue
Block a user