Merged in fix/SW-2534-booking-widget-fixes (pull request #2129)

fix(SW-2534): Added validation error and fixed initial month in date picker for BW

* Merged in revert-version (pull request #2128)

revert including version in layouts and api responses

* revert including version in layouts and api responses


Approved-by: Linus Flood

* feat: prevent users from selecting the same room when there is no vacancy for it

* Merged in fix/no-my-stay-for-external-bookings (pull request #2126)

fix: Only link web and app bookings to my stay

Approved-by: Joakim Jäderberg

* Merged in fix/bookingwidget-fixes (pull request #2135)

Fix: (#SW-2663) bookingwidget mobile - Space around, border-radius and correct color on date field

* fix: booking widget mobile - padding around booking widget and date color

* Fixed rounded corners

* Reduced minimum size of column


Approved-by: Joakim Jäderberg

* Merged in fix/LOY-105-signupform-error-messages (pull request #2121)

feat(LOY-105): update signup form validation messages

* feat(LOY-105): improve signup form validation messages


Approved-by: Erik Tiekstra

* fix(LOY-199): add missing benefits link

* Merged in feat/SW-2340-aa-tracking-my-stay-pageview- (pull request #2133)

feat: SW-2340 Implemented tracking on my-stay, webview my-stay and receipt page

* feat: SW-2340 Implemented tracking on my-stay, webview my-stay and receipt page

* feat: SW-2340 Updated webview tracking

* feat: SW-2340 Updated receipt tracking


Approved-by: Linus Flood

* Merged in fix/SW-2804-missing-meeting-rooms (pull request #2138)

fix: return [] when we get a 404 for meeting rooms for a hotel

* fix: return [] when we get a 404 for meeting rooms for a hotel


Approved-by: Linus Flood

* feat(auth): limit output in session endpoint

* fix(SW-2376): Vertically centered previous/next buttons inside carousel cards

Approved-by: Matilda Landström

* fix(SW-2055): Surrounded ul inside JsonToHtml with a typography component

Approved-by: Matilda Landström

* fix(SW-2621): Breaking too long words on heading inside destination city pages

Approved-by: Matilda Landström

* Merged in fix/sw-2763-external-scripts (pull request #2104)

fix: try/catch external scripts to avoid them breaking our page #sw-2763

* fix: try/catch external scripts to avoid them breaking our page #sw-2763


Approved-by: Joakim Jäderberg

* fix: handle non loaded surprises in case they're returned as null from server

* feat(SW-2806): booking widget should not be blocked by sitewide alert

* Merged in fix/remove-on-error (pull request #2142)

fix: revert onError on the Script component

* fix: revert onError on the Script component

* Merged in fix/alert-icon (pull request #2139)

fix(SW-2807): alert icons

* fix(SW-2807): fix incorrect icon color on sitewide alert

* fix(SW-2807): change error icon


Approved-by: Erik Tiekstra
Approved-by: Linus Flood

* Merged in feat/SW-2760-SW-552-wellness-openinghours (pull request #2112)

fix(SW-2760, SW-2552): fix opening hours wellness sidepeek

* fix(SW-2760, SW-2552): fix opening hours wellness sidepeek


Approved-by: Erik Tiekstra

* Merged in feat/SW-1749-sidepeek-hotel-cta (pull request #2123)

feat(SW-1749): add link to hotel page in sidepeek

* feat(SW-1749): add link to hotel page in sidepeek


Approved-by: Matilda Landström

* fix(SW-2811): suggest list should follow where-to-field

* fix(SW-2451): placement of suggest list

* Merged in fix/SW-2684-booking-widget-text-overflow (pull request #2048)

fix(SW-2684): truncate overflowing text in booking widget

* fix: truncate overflowing text in booking widget

* fix: change Body to Typography and css selector fix


Approved-by: Hrishikesh Vaipurkar

* Merged in feat/SW-2800-lightbox-history-state (pull request #2147)

feat(SW-2800): closing image gallery and lightbox on using browser navigation

* feat(SW-2800): closing image gallery and lightbox on using browser navigation


Approved-by: Linus Flood

* Merged in fix/enter-details-footer-margin (pull request #2150)

fix: margin to footer on enter details

* fix: margin to footer on enter details

* Merged in fix/SW-2822-missing-meetingroom-images (pull request #2151)

fix: meeting rooms with missing images

* fix: meeting rooms with missing images


Approved-by: Linus Flood


Approved-by: Bianca Widstam
This commit is contained in:
Tobias Johansson
2025-05-20 06:49:27 +00:00
parent 3e50132bc9
commit b641f8387e
8 changed files with 190 additions and 97 deletions

View File

@@ -29,7 +29,7 @@ export default function DatePickerRangeDesktop({
}: DatePickerRangeProps) {
const lang = useLang()
const intl = useIntl()
const [month, setMonth] = useState(new Date())
const [month, setMonth] = useState(selectedRange?.from ?? new Date())
/** English is default language and doesn't need to be imported */
const locale = lang === Lang.en ? undefined : locales[lang]

View File

@@ -74,6 +74,7 @@ export default function DatePickerRangeMobile({
lang={lang}
locale={locale}
mode="range"
defaultMonth={selectedRange?.from}
/** Showing full year or what's left of it */
numberOfMonths={13}
onSelect={handleOnSelect}

View File

@@ -225,7 +225,7 @@ export function SearchSkeleton() {
const intl = useIntl()
return (
<div className={styles.container}>
<div className={styles.label}>
<div className={`${styles.label} ${styles.red}`}>
<Typography variant="Body/Supporting text (caption)/smBold">
<span>{intl.formatMessage({ defaultMessage: "Where to?" })}</span>
</Typography>

View File

@@ -0,0 +1,32 @@
import { useIntl } from "react-intl"
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
import Caption from "@/components/TempDesignSystem/Text/Caption"
import styles from "./validationError.module.css"
export default function ValidationError() {
const intl = useIntl()
return (
<div className={styles.container}>
<Caption className={styles.title} color="red" type="bold">
<MaterialIcon
icon="error_circle_rounded"
color="Icon/Feedback/Error"
size={20}
/>
{intl.formatMessage({
defaultMessage: "Enter destination or hotel",
})}
</Caption>
<Caption className={styles.message} type="regular">
{intl.formatMessage({
defaultMessage:
"A destination or hotel name is needed to be able to search for a hotel room.",
})}
</Caption>
</div>
)
}

View File

@@ -0,0 +1,36 @@
.container {
position: absolute;
top: calc(100% + var(--Space-x2));
background: var(--Surface-Primary-Default);
border-radius: var(--Corner-radius-lg);
box-shadow: 0px 0px 14px 6px rgba(0, 0, 0, 0.1);
padding: var(--Space-x15);
max-width: min(100vw, calc(360px - var(--Space-x2)));
width: 360px;
display: flex;
flex-direction: column;
padding: var(--Space-x15);
align-items: flex-start;
gap: var(--Space-x05);
z-index: var(--dialog-z-index);
}
.title {
display: flex;
align-items: center;
gap: var(--Space-x1);
}
.message {
text-wrap: auto;
}
@media screen and (min-width: 1367px) {
.container {
top: calc(100% + var(--Space-x1) + var(--Space-x2));
left: calc(var(--Space-x1) * -1);
padding: var(--Space-x2);
max-width: 360px;
}
}

View File

@@ -11,6 +11,7 @@
display: none;
}
.where,
.rooms,
.when {
position: relative;
@@ -25,6 +26,10 @@
display: none;
}
.label {
color: var(--Text-Accent-Primary);
}
@media screen and (max-width: 767px) {
.voucherContainer {
padding: var(--Spacing-x2) 0 var(--Spacing-x4);

View File

@@ -4,6 +4,7 @@ import { useFormContext, useWatch } from "react-hook-form"
import { useIntl } from "react-intl"
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
import { Typography } from "@scandic-hotels/design-system/Typography"
import { dt } from "@/lib/dt"
@@ -11,10 +12,10 @@ import DatePicker from "@/components/DatePicker"
import GuestsRoomsPickerForm from "@/components/GuestsRoomsPicker"
import SkeletonShimmer from "@/components/SkeletonShimmer"
import Button from "@/components/TempDesignSystem/Button"
import Caption from "@/components/TempDesignSystem/Text/Caption"
import { RemoveExtraRooms } from "./BookingCode"
import { Search, SearchSkeleton } from "./Search"
import ValidationError from "./ValidationError"
import Voucher, { VoucherSkeleton } from "./Voucher"
import styles from "./formContent.module.css"
@@ -37,19 +38,23 @@ export default function FormContent({
const nights = dt(selectedDate.toDate).diff(dt(selectedDate.fromDate), "days")
return (
<>
<div className={styles.input}>
<div className={styles.inputContainer}>
<div className={styles.where}>
<Search
handlePressEnter={onSubmit}
selectOnBlur={true}
inputName="search"
includeTypes={["cities", "hotels"]}
/>
</div>
<div className={styles.when}>
<Caption color="red" type="bold">
<div className={styles.input}>
<div className={styles.inputContainer}>
<div className={styles.where}>
<Search
handlePressEnter={onSubmit}
selectOnBlur={true}
inputName="search"
includeTypes={["cities", "hotels"]}
/>
{errors.search && <ValidationError />}
</div>
<div className={styles.when}>
<Typography
variant="Body/Supporting text (caption)/smBold"
className={styles.label}
>
<label htmlFor="date">
{nights > 0
? intl.formatMessage(
{
@@ -61,73 +66,72 @@ export default function FormContent({
: intl.formatMessage({
defaultMessage: "Check in",
})}
</Caption>
<DatePicker />
</div>
<div className={styles.rooms}>
<label>
<Caption color="red" type="bold" asChild>
<span>
{intl.formatMessage({
defaultMessage: "Rooms & Guests",
})}
</span>
</Caption>
</label>
<GuestsRoomsPickerForm />
</div>
</Typography>
<DatePicker name="date" />
</div>
<div className={`${styles.buttonContainer} ${styles.showOnTablet}`}>
<Button
className={styles.button}
form={formId}
intent="primary"
theme="base"
type="submit"
<div className={styles.rooms}>
<Typography
variant="Body/Supporting text (caption)/smBold"
className={styles.label}
>
<span className={styles.icon}>
<MaterialIcon icon="search" color="Icon/Inverted" size={28} />
</span>
</Button>
</div>
<div className={`${styles.voucherContainer} ${styles.voucherRow}`}>
<Voucher />
</div>
<div className={`${styles.buttonContainer} ${styles.hideOnTablet}`}>
{errors.bookingCode?.value?.message?.indexOf("Multi-room") === 0 ? (
<RemoveExtraRooms
size="medium"
fullWidth
className={styles.showOnMobile}
/>
) : null}
<Button
className={styles.button}
form={formId}
intent="primary"
theme="base"
type="submit"
disabled={isSearching}
>
<Caption
color="white"
type="bold"
className={styles.buttonText}
asChild
>
<span>
{intl.formatMessage({
defaultMessage: "Search",
})}
</span>
</Caption>
<span className={styles.icon}>
<MaterialIcon icon="search" color="Icon/Inverted" size={28} />
</span>
</Button>
<label id="rooms-and-guests">
{intl.formatMessage({
defaultMessage: "Rooms & Guests",
})}
</label>
</Typography>
<GuestsRoomsPickerForm ariaLabelledBy="rooms-and-guests" />
</div>
</div>
</>
<div className={`${styles.buttonContainer} ${styles.showOnTablet}`}>
<Button
className={styles.button}
form={formId}
intent="primary"
theme="base"
type="submit"
>
<span className={styles.icon}>
<MaterialIcon icon="search" color="Icon/Inverted" size={28} />
</span>
</Button>
</div>
<div className={`${styles.voucherContainer} ${styles.voucherRow}`}>
<Voucher />
</div>
<div className={`${styles.buttonContainer} ${styles.hideOnTablet}`}>
{errors.bookingCode?.value?.message?.indexOf("Multi-room") === 0 ? (
<RemoveExtraRooms
size="medium"
fullWidth
className={styles.showOnMobile}
/>
) : null}
<Button
className={styles.button}
form={formId}
intent="primary"
theme="base"
type="submit"
disabled={isSearching}
>
<Typography
variant="Body/Supporting text (caption)/smBold"
className={styles.buttonText}
>
<span>
{intl.formatMessage({
defaultMessage: "Search",
})}
</span>
</Typography>
<span className={styles.icon}>
<MaterialIcon icon="search" color="Icon/Inverted" size={28} />
</span>
</Button>
</div>
</div>
)
}
@@ -141,25 +145,33 @@ export function BookingWidgetFormContentSkeleton() {
<SearchSkeleton />
</div>
<div className={styles.when}>
<Caption color="red" type="bold">
{intl.formatMessage(
{
defaultMessage:
"{totalNights, plural, one {# night} other {# nights}}",
},
{ totalNights: 0 }
)}
</Caption>
<Typography
variant="Body/Supporting text (caption)/smBold"
className={styles.label}
>
<label>
{intl.formatMessage(
{
defaultMessage:
"{totalNights, plural, one {# night} other {# nights}}",
},
{ totalNights: 0 }
)}
</label>
</Typography>
<SkeletonShimmer width={"100%"} display={"block"} />
</div>
<div className={styles.rooms}>
<Caption color="red" type="bold" asChild>
<span>
<Typography
variant="Body/Supporting text (caption)/smBold"
className={styles.label}
>
<label id="rooms-and-guests">
{intl.formatMessage({
defaultMessage: "Rooms & Guests",
})}
</span>
</Caption>
</label>
</Typography>
<SkeletonShimmer width={"100%"} display={"block"} />
</div>
</div>
@@ -174,18 +186,17 @@ export function BookingWidgetFormContentSkeleton() {
type="submit"
disabled
>
<Caption
color="white"
type="bold"
<Typography
variant="Body/Supporting text (caption)/smBold"
className={styles.buttonText}
asChild
>
<span>
{intl.formatMessage({
defaultMessage: "Search",
})}
</span>
</Caption>
</Typography>
<span className={styles.icon}>
<MaterialIcon icon="search" color="Icon/Inverted" size={28} />
</span>

View File

@@ -21,7 +21,11 @@ import styles from "./guests-rooms-picker.module.css"
import type { BookingWidgetSchema } from "@/types/components/bookingWidget"
import type { GuestsRoom } from "@/types/components/bookingWidget/guestsRoomsPicker"
export default function GuestsRoomsPickerForm() {
export default function GuestsRoomsPickerForm({
ariaLabelledBy,
}: {
ariaLabelledBy?: string
}) {
const { trigger } = useFormContext<BookingWidgetSchema>()
const rooms = useWatch<BookingWidgetSchema, "rooms">({ name: "rooms" })
@@ -115,6 +119,7 @@ export default function GuestsRoomsPickerForm() {
triggerFn={() => {
setIsOpen(true)
}}
ariaLabelledBy={ariaLabelledBy}
/>
<Modal>
<Dialog className={styles.pickerContainerMobile}>
@@ -129,10 +134,12 @@ function Trigger({
rooms,
className,
triggerFn,
ariaLabelledBy,
}: {
rooms: GuestsRoom[]
className: string
triggerFn?: () => void
ariaLabelledBy?: string
}) {
const intl = useIntl()
@@ -173,6 +180,7 @@ function Trigger({
className={`${className} ${styles.btn}`}
type="button"
onPress={triggerFn}
aria-labelledby={ariaLabelledBy}
>
<Typography variant="Body/Paragraph/mdRegular">
<span>{parts.join(", ")}</span>