29 Commits

Author SHA1 Message Date
Anton Gunnarsson 1dffeb6be7 Merged in fix/loy-603-fix-edit-profile-redirect (pull request #3543)
fix(LOY-603): Remove refresh preventing redirect

* Remove refresh preventing redirect


Approved-by: Matilda Landström
2026-02-05 07:45:19 +00:00
Anton Gunnarsson 1f1ed2e4f3 Merged in chore/update-sas-banner-image (pull request #3545)
fix(LOY-621): Replace scandic banner image and reduce size

* Replace scandic banner image and reduce size


Approved-by: Emma Zettervall
Approved-by: Matilda Landström
2026-02-05 07:44:54 +00:00
Linus Flood bc9eaf6706 Merged in fix/robots-txt (pull request #3547)
feat(robots): fixed robots.txt file

* feat(robots): fixed robots.txt file
2026-02-05 07:38:15 +00:00
Matilda Landström 549265cd34 Merged in feat/LOY-615-cleanup-env-prof-consent (pull request #3537)
feat(LOY-615): cleanup profiling consent env var

* feat(LOY-615): cleanup profiling consent env var


Approved-by: Anton Gunnarsson
2026-02-04 16:51:06 +00:00
Matilda Landström 989b18527e Merged in fix/STAY-138-center-text (pull request #3538)
fix(STAY-138): center text

* fix(STAY-138): center text


Approved-by: Emma Zettervall
2026-02-04 13:09:12 +00:00
Erik Tiekstra 0cda37808e Merged in fix/BOOK-755-alert-content (pull request #3523)
fix(BOOK-755, BOOK-787): Fixed issue for phone number and sidepeeks not showing

* fix(BOOK-755): Fixed issue for phone number and sidepeeks not showing

* fix(BOOK-755): fix issue phonenumber alert

* fix(BOOK-755): fix issue phonenumber


Approved-by: Matilda Landström
2026-02-03 15:28:23 +00:00
Erik Tiekstra b3c4761ae5 Merged in chore/BOOK-773-replace-old-typography-variables (pull request #3515)
Chore/BOOK-773 replace old typography variables

* chore(BOOK-773): Replaced body typography

* chore(BOOK-773): Replaced caption typography

* chore(BOOK-773): Replaced footnote typography

* chore(BOOK-773): Replaced subtitle typography


Approved-by: Bianca Widstam
2026-02-03 15:07:18 +00:00
Linus Flood dd65467573 Merged in fix/close-map-text (pull request #3536)
feat(map): fixed close map text alignment

* feat(map): fixed close map text alignment
2026-02-03 14:07:40 +00:00
Joakim Jäderberg eb45e6b294 Merged in fix/LOY-606 (pull request #3535)
fix(LOY-606): breakfast price now considers number of nights in MyStay/ChangeDates

* fix(LOY-606): breakfast price now considers number of nights in MyStay/ChangeDates


Approved-by: Linus Flood
2026-02-03 13:51:45 +00:00
Emma Zettervall 6553fcf685 Merged in fix/use-old-loacalize-keys (pull request #3534)
fix(LOY-391): changed back localize keys to the old ones to make less work for content

* fix(LOY-391): changed back localize keys to the old ones to make less work for content


Approved-by: Anton Gunnarsson
2026-02-03 13:42:37 +00:00
Anton Gunnarsson c2cf6b03a7 Merged in feat/loy-291-new-claim-points-flow (pull request #3508)
feat(LOY-291): New claim points flow for logged in users

* wip new flow

* More wip

* More wip

* Wip styling

* wip with a mutation

* Actually fetch booking data

* More styling wip

* Fix toast duration

* fix loading a11y maybe

* More stuff

* Add feature flag

* Add invalid state

* Clean up

* Add fields for missing user info

* Restructure files

* Add todos

* Disable warning

* Fix icon and border radius


Approved-by: Emma Zettervall
Approved-by: Matilda Landström
2026-02-03 13:27:24 +00:00
Linus Flood 310ad7bc7f Merged in fix/book-785-hotelfilters-cache (pull request #3533)
fix(BOOK-785): fix incorrect cache keys for contact config and hotel filters

* fix(BOOK-785): fix incorrect cache keys for contact config and hotel filters
2026-02-03 13:18:23 +00:00
Joakim Jäderberg fbdbd35813 Merged in fix/mainmenu-button-css-cascade (pull request #3532)
Fix/mainmenu button css cascade

* fix(MainMenuButton): add @layer to make css cascading work

* reset
2026-02-03 12:36:27 +00:00
Matilda Landström d6b94376b0 Merged in fix/STAY-23-room-price (pull request #3529)
fix(STAY-23): don't strikethrough price if only one room is cancelled (multiroom)

* fix(STAY-23): don't strikethrough price if only room is cancelled (multiroom)


Approved-by: Anton Gunnarsson
2026-02-03 07:58:52 +00:00
Linus Flood 7a604f1250 Merged in chore/nextjs-version (pull request #3530)
Chore/nextjs version

* chore(next/react): update to latest versions

* Correct swc version

* Readme

* No turbopack

* test

* test

* test
2026-02-03 07:52:04 +00:00
Joakim Jäderberg 13fd8f81c9 Merged in revert-nextjs-upgrade (pull request #3528)
revert nextjs upgrade

* revert nextjs upgrade

* Fix revert


Approved-by: Anton Gunnarsson
2026-02-02 15:15:42 +00:00
Joakim Jäderberg 16cc26632e Merged in chore/refactor-trpc-booking-routes (pull request #3510)
feat(BOOK-750): refactor booking endpoints

* WIP

* wip

* wip

* parse dates in UTC

* wip

* no more errors

* Merge branch 'master' of bitbucket.org:scandic-swap/web into chore/refactor-trpc-booking-routes

* .

* cleanup

* import named z from zod

* fix(BOOK-750): updateBooking api endpoint expects dateOnly, we passed ISO date


Approved-by: Anton Gunnarsson
2026-02-02 14:28:14 +00:00
Linus Flood 8ac2c4ba22 Merged in chore/nextjs-version (pull request #3527)
chore(next/react): update to latest versions

* chore(next/react): update to latest versions

* Correct swc version


Approved-by: Anton Gunnarsson
2026-02-02 14:21:39 +00:00
Emma Zettervall 65e5d90fee Merged in fix/point-transaction-link (pull request #3526)
Fix/point transaction link

* fix: only make stay transactions links

* fix: only make transaction of type stay and reward nights with a bookingUrl as links


Approved-by: Matilda Landström
2026-02-02 12:44:08 +00:00
Linus Flood 5f55687239 Merged in feat/lokalise-sync-020226 (pull request #3525)
Lokalise sync

* Lokalise sync
2026-02-02 10:35:04 +00:00
Matilda Haneling e30ce9ac30 Merged in fix/book-769-booking-widget-ui-bugs (pull request #3524)
Fix/book 769 booking widget ui bugs

* fixed text in serachList not wrapping properly

* fixed spacing on mobile searchList

* fixed close button icon color

* fix for issues with fixed vs sticky elements on scroll lock

Approved-by: Linus Flood
2026-02-02 10:26:48 +00:00
Matilda Landström 61c024dbda Merged in fix/correct-policies-link (pull request #3522)
Fix/correct policies link

* fix: add correct link

* fix: move route


Approved-by: Emma Zettervall
2026-02-02 09:22:08 +00:00
Emma Zettervall 2f73fce6f2 Merged in fix/LOY-391-fix-for-transaction-details-not-available (pull request #3513)
fix(LOY-391): uses transaction date if checkinDate does not exist

* fix(LOY-391): uses transaction date if checkinDate does not exist

* fix(LOY-391): changed date to datejs and fixed description for reward night and reward gift

* fix(LOY-391):use year transaction date

* fix(LOY-391): fixed date timezone and dt() instead of datejs()

* fix(LOY-391): small improv with year

* fix(LOY-391): utc fix

* fix(LOY-391): changed sorting order on transaction date

* fix(LOY-391): filter out transactions with 0 award points


Approved-by: Joakim Jäderberg
2026-01-30 14:59:14 +00:00
Anton Gunnarsson 76ee5e97bf Merged in chore/improve-my-stay-load-times (pull request #3514)
chore: Improve My Stay load times

* Restructure My Stay page to avoid data fetching waterfalls


Approved-by: Linus Flood
Approved-by: Matilda Landström
2026-01-30 14:29:49 +00:00
Anton Gunnarsson fd38542863 Merged in fix/sw-3703-fix-focus-in-lightbox-gallery (pull request #3521)
fix(SW-3703): Fix focus management in lightbox gallery

* Add keys to fix focus


Approved-by: Matilda Landström
2026-01-30 12:55:46 +00:00
Anton Gunnarsson cc60cf2903 Merged in chore/change-icons-to-rounded (pull request #3520)
chore: Change default icon style to rounded

* Change default icon style to rounded


Approved-by: Linus Flood
2026-01-30 10:13:03 +00:00
Erik Tiekstra 13b0d976ac fix(BOOK-391): Fixed typo in path to Futura PT font
* fix(BOOK-391): Now using correct Futura PT fonts

Approved-by: Linus Flood
2026-01-30 09:58:50 +00:00
Anton Gunnarsson 77eabac038 Merged in fix/loy-589-update-cancellation-labels (pull request #3495)
fix(LOY-589): Split cancellation label into two

* Split cancellation label into two

* Fix copy


Approved-by: Bianca Widstam
2026-01-30 08:29:35 +00:00
Matilda Landström 0919134f88 Merged in fix/LOY-597-upcoming-today (pull request #3517)
fix(LOY-597): Display "Today" for upcoming stay today

* fix(LOY-597): Display "Today" for upcoming stay today


Approved-by: Anton Gunnarsson
2026-01-30 08:03:28 +00:00
575 changed files with 5883 additions and 2951 deletions
@@ -1,3 +1,4 @@
.layout { .layout {
font-family: var(--typography-Body-Regular-fontFamily); font-family:
var(--Body-Paragraph-Font-family), var(--Body-Paragraph-Font-fallback);
} }
+5
View File
@@ -158,6 +158,11 @@ body.partner-sas {
white-space: nowrap; white-space: nowrap;
border-width: 0; border-width: 0;
} }
/* When a select in the booking widget is open, react-aria sets overflow:hidden
which breaks sticky positioning. Override with clip which doesn't break sticky. */
body:has([data-booking-widget-open] [data-open]) {
overflow: clip !important;
}
@media screen and (min-width: 768px) { @media screen and (min-width: 768px) {
:root { :root {
+135 -1
View File
@@ -231,6 +231,12 @@
"value": "Særlige ønsker (valgfrit)" "value": "Særlige ønsker (valgfrit)"
} }
], ],
"ancillaries.label.quantity": [
{
"type": 0,
"value": "Antal"
}
],
"ancillaries.unableToDisplayBreakfastPrices": [ "ancillaries.unableToDisplayBreakfastPrices": [
{ {
"type": 0, "type": 0,
@@ -249,6 +255,18 @@
"value": "Jeg accepterer booking- og annulleringsbetingelserne" "value": "Jeg accepterer booking- og annulleringsbetingelserne"
} }
], ],
"booking.alert.extraBeds.text": [
{
"type": 0,
"value": "Ved booking for mere end 2 gæster vil der blive opkrævet et ekstra gebyr pr. person. Se link for detaljer."
}
],
"booking.alert.extraguests": [
{
"type": 0,
"value": "Ekstra gæst(er)"
}
],
"booking.approx": [ "booking.approx": [
{ {
"type": 0, "type": 0,
@@ -953,6 +971,16 @@
"value": "Tilføj kode" "value": "Tilføj kode"
} }
], ],
"bookingWidget.bookingCode.readMore": [
{
"type": 0,
"value": "Læs mere om brug af "
},
{
"type": 1,
"value": "codeVoucher"
}
],
"bookingWidget.bookingCode.remember": [ "bookingWidget.bookingCode.remember": [
{ {
"type": 0, "type": 0,
@@ -1061,6 +1089,16 @@
"value": "details" "value": "details"
} }
], ],
"bookingWidget.reward.readMore": [
{
"type": 0,
"value": "Læs mere om booking med "
},
{
"type": 1,
"value": "reward"
}
],
"bookingWidget.reward.rewardNight": [ "bookingWidget.reward.rewardNight": [
{ {
"type": 0, "type": 0,
@@ -2510,6 +2548,44 @@
"value": "Datoer" "value": "Datoer"
} }
], ],
"designSystem.stepper.ariaLabel.currentCount": [
{
"type": 0,
"value": "Nuværende "
},
{
"type": 1,
"value": "label"
},
{
"type": 0,
"value": " "
},
{
"type": 1,
"value": "count"
}
],
"designSystem.stepper.ariaLabel.decrease": [
{
"type": 0,
"value": "Reducer "
},
{
"type": 1,
"value": "label"
}
],
"designSystem.stepper.ariaLabel.increase": [
{
"type": 0,
"value": "Øg "
},
{
"type": 1,
"value": "label"
}
],
"destination.backToCities": [ "destination.backToCities": [
{ {
"type": 0, "type": 0,
@@ -5783,7 +5859,7 @@
"myPages.myStay.ancillaries.reachedMaxItemsStepperMessage": [ "myPages.myStay.ancillaries.reachedMaxItemsStepperMessage": [
{ {
"type": 0, "type": 0,
"value": "Maksimal antal nået for denne vare." "value": "Maksimalt antal nået for denne vare."
} }
], ],
"myPages.myStay.ancillaries.reachedMaxPointsMessage": [ "myPages.myStay.ancillaries.reachedMaxPointsMessage": [
@@ -7569,6 +7645,58 @@
"value": "Samlede ophold" "value": "Samlede ophold"
} }
], ],
"price.numberOfEuroBonusPoints": [
{
"offset": 0,
"options": {
"one": {
"value": [
{
"type": 0,
"value": "EB-point"
}
]
},
"other": {
"value": [
{
"type": 0,
"value": "EB-point"
}
]
}
},
"pluralType": "cardinal",
"type": 6,
"value": "numberOfEuroBonusPoints"
}
],
"price.numberOfScandicPoints": [
{
"offset": 0,
"options": {
"one": {
"value": [
{
"type": 0,
"value": "Point"
}
]
},
"other": {
"value": [
{
"type": 0,
"value": "Point"
}
]
}
},
"pluralType": "cardinal",
"type": 6,
"value": "numberOfScandicPoints"
}
],
"price.numberOfVouchers": [ "price.numberOfVouchers": [
{ {
"offset": 0, "offset": 0,
@@ -8629,6 +8757,12 @@
"value": "). Se tilgængelige priser nedenfor." "value": "). Se tilgængelige priser nedenfor."
} }
], ],
"selectRate.alert.reservationPolicies": [
{
"type": 0,
"value": "Se prisdetaljer"
}
],
"selectRate.availableRooms.all": [ "selectRate.availableRooms.all": [
{ {
"offset": 0, "offset": 0,
+138
View File
@@ -231,6 +231,12 @@
"value": "Sonderwünsche (optional)" "value": "Sonderwünsche (optional)"
} }
], ],
"ancillaries.label.quantity": [
{
"type": 0,
"value": "Menge"
}
],
"ancillaries.unableToDisplayBreakfastPrices": [ "ancillaries.unableToDisplayBreakfastPrices": [
{ {
"type": 0, "type": 0,
@@ -249,6 +255,18 @@
"value": "Ich akzeptiere die Buchungs- und Stornierungsbedingungen" "value": "Ich akzeptiere die Buchungs- und Stornierungsbedingungen"
} }
], ],
"booking.alert.extraBeds.text": [
{
"type": 0,
"value": "Bei Buchungen für mehr als 2 Gäste fällt eine zusätzliche Gebühr pro Person an. Siehe Link für Details."
}
],
"booking.alert.extraguests": [
{
"type": 0,
"value": "Zusätzliche Gäste"
}
],
"booking.approx": [ "booking.approx": [
{ {
"type": 0, "type": 0,
@@ -949,6 +967,16 @@
"value": "Code hinzufügen" "value": "Code hinzufügen"
} }
], ],
"bookingWidget.bookingCode.readMore": [
{
"type": 0,
"value": "Mehr erfahren über die Verwendung von "
},
{
"type": 1,
"value": "codeVoucher"
}
],
"bookingWidget.bookingCode.remember": [ "bookingWidget.bookingCode.remember": [
{ {
"type": 0, "type": 0,
@@ -1057,6 +1085,20 @@
"value": "details" "value": "details"
} }
], ],
"bookingWidget.reward.readMore": [
{
"type": 0,
"value": "Mehr über Buchungen mit "
},
{
"type": 1,
"value": "reward"
},
{
"type": 0,
"value": "erfahren"
}
],
"bookingWidget.reward.rewardNight": [ "bookingWidget.reward.rewardNight": [
{ {
"type": 0, "type": 0,
@@ -2522,6 +2564,44 @@
"value": "Daten" "value": "Daten"
} }
], ],
"designSystem.stepper.ariaLabel.currentCount": [
{
"type": 0,
"value": "Aktuell "
},
{
"type": 1,
"value": "label"
},
{
"type": 0,
"value": " "
},
{
"type": 1,
"value": "count"
}
],
"designSystem.stepper.ariaLabel.decrease": [
{
"type": 1,
"value": "label"
},
{
"type": 0,
"value": "verringern"
}
],
"designSystem.stepper.ariaLabel.increase": [
{
"type": 0,
"value": "Erhöhen "
},
{
"type": 1,
"value": "label"
}
],
"destination.backToCities": [ "destination.backToCities": [
{ {
"type": 0, "type": 0,
@@ -7570,6 +7650,58 @@
"value": "Aufenthalte insgesamt" "value": "Aufenthalte insgesamt"
} }
], ],
"price.numberOfEuroBonusPoints": [
{
"offset": 0,
"options": {
"one": {
"value": [
{
"type": 0,
"value": "EB-Punkt"
}
]
},
"other": {
"value": [
{
"type": 0,
"value": "EB-Punkte"
}
]
}
},
"pluralType": "cardinal",
"type": 6,
"value": "numberOfEuroBonusPoints"
}
],
"price.numberOfScandicPoints": [
{
"offset": 0,
"options": {
"one": {
"value": [
{
"type": 0,
"value": "Punkt"
}
]
},
"other": {
"value": [
{
"type": 0,
"value": "Punkte"
}
]
}
},
"pluralType": "cardinal",
"type": 6,
"value": "numberOfScandicPoints"
}
],
"price.numberOfVouchers": [ "price.numberOfVouchers": [
{ {
"offset": 0, "offset": 0,
@@ -8634,6 +8766,12 @@
"value": "). Verfügbare Übernachtungspreise sind unten zu sehen." "value": "). Verfügbare Übernachtungspreise sind unten zu sehen."
} }
], ],
"selectRate.alert.reservationPolicies": [
{
"type": 0,
"value": "Tarifdetails anzeigen"
}
],
"selectRate.availableRooms.all": [ "selectRate.availableRooms.all": [
{ {
"offset": 0, "offset": 0,
+290
View File
@@ -231,6 +231,12 @@
"value": "Special requests (optional)" "value": "Special requests (optional)"
} }
], ],
"ancillaries.label.quantity": [
{
"type": 0,
"value": "Quantity"
}
],
"ancillaries.unableToDisplayBreakfastPrices": [ "ancillaries.unableToDisplayBreakfastPrices": [
{ {
"type": 0, "type": 0,
@@ -249,6 +255,18 @@
"value": "I accept the booking and cancellation terms" "value": "I accept the booking and cancellation terms"
} }
], ],
"booking.alert.extraBeds.text": [
{
"type": 0,
"value": "When booking for more than 2 guests, an additional fee will apply per person. See link for details."
}
],
"booking.alert.extraguests": [
{
"type": 0,
"value": "Extra guest(s)"
}
],
"booking.approx": [ "booking.approx": [
{ {
"type": 0, "type": 0,
@@ -337,6 +355,12 @@
"value": "Change or cancel" "value": "Change or cancel"
} }
], ],
"booking.changeTitle": [
{
"type": 0,
"value": "Change"
}
],
"booking.codeVoucher": [ "booking.codeVoucher": [
{ {
"type": 0, "type": 0,
@@ -953,6 +977,16 @@
"value": "Add code" "value": "Add code"
} }
], ],
"bookingWidget.bookingCode.readMore": [
{
"type": 0,
"value": "Read more about using "
},
{
"type": 1,
"value": "codeVoucher"
}
],
"bookingWidget.bookingCode.remember": [ "bookingWidget.bookingCode.remember": [
{ {
"type": 0, "type": 0,
@@ -1061,6 +1095,16 @@
"value": "details" "value": "details"
} }
], ],
"bookingWidget.reward.readMore": [
{
"type": 0,
"value": "Read more about booking with "
},
{
"type": 1,
"value": "reward"
}
],
"bookingWidget.reward.rewardNight": [ "bookingWidget.reward.rewardNight": [
{ {
"type": 0, "type": 0,
@@ -2198,6 +2242,16 @@
"value": " points" "value": " points"
} }
], ],
"common.pointsInLine": [
{
"type": 1,
"value": "points"
},
{
"type": 0,
"value": " points"
}
],
"common.pointsToSpend": [ "common.pointsToSpend": [
{ {
"type": 0, "type": 0,
@@ -2510,6 +2564,44 @@
"value": "Dates" "value": "Dates"
} }
], ],
"designSystem.stepper.ariaLabel.currentCount": [
{
"type": 0,
"value": "Current "
},
{
"type": 1,
"value": "label"
},
{
"type": 0,
"value": " "
},
{
"type": 1,
"value": "count"
}
],
"designSystem.stepper.ariaLabel.decrease": [
{
"type": 0,
"value": "Decrease "
},
{
"type": 1,
"value": "label"
}
],
"designSystem.stepper.ariaLabel.increase": [
{
"type": 0,
"value": "Increase "
},
{
"type": 1,
"value": "label"
}
],
"destination.backToCities": [ "destination.backToCities": [
{ {
"type": 0, "type": 0,
@@ -3060,6 +3152,12 @@
"value": "When you confirm the booking the room will be guaranteed for late arrival. If you fail to arrive without cancelling in advance or if you cancel after 18:00 local time, you will be charged for one reward night." "value": "When you confirm the booking the room will be guaranteed for late arrival. If you fail to arrive without cancelling in advance or if you cancel after 18:00 local time, you will be charged for one reward night."
} }
], ],
"enterDetails.confirmBooking.rewardNightGuaranteeInfo": [
{
"type": 0,
"value": "When you complete the booking the room will be guaranteed for late arrival. The hotel will hold your booking, even if you arrive after 18:00. In case of a no-show, you will be charged for one reward night."
}
],
"enterDetails.details.description": [ "enterDetails.details.description": [
{ {
"type": 0, "type": 0,
@@ -3344,6 +3442,40 @@
"value": "Select payment method" "value": "Select payment method"
} }
], ],
"enterDetails.paymentStep.flexBookingTermsAndConditions": [
{
"type": 0,
"value": "To complete your booking, please accept the general "
},
{
"children": [
{
"type": 0,
"value": "Booking & Cancellation Terms"
}
],
"type": 8,
"value": "termsAndConditionsLink"
},
{
"type": 0,
"value": ", and acknowledge that your data will be processed in accordance with Scandic's "
},
{
"children": [
{
"type": 0,
"value": "Privacy policy"
}
],
"type": 8,
"value": "privacyPolicyLink"
},
{
"type": 0,
"value": "."
}
],
"enterDetails.priceChangeDialog.acceptButton": [ "enterDetails.priceChangeDialog.acceptButton": [
{ {
"type": 0, "type": 0,
@@ -5669,6 +5801,24 @@
"value": "!" "value": "!"
} }
], ],
"myPages.l6progress.modal.title": [
{
"type": 0,
"value": "Level upgrade and membership year"
}
],
"myPages.l6progress.modal.youCanAlsoReach": [
{
"type": 0,
"value": "You can also reach Best Friend, our highest membership level, by staying 100 nights with us within a membership year."
}
],
"myPages.l6progress.modal.yourLevelDuring": [
{
"type": 0,
"value": "Your level during the current and next period is based on the points you earn during this 12-month period."
}
],
"myPages.leftToLevelUp": [ "myPages.leftToLevelUp": [
{ {
"type": 0, "type": 0,
@@ -6057,6 +6207,88 @@
"value": "Your membership" "value": "Your membership"
} }
], ],
"myPoints.pointTransactions.extrasToBooking": [
{
"type": 0,
"value": "Extras to your booking"
}
],
"myPoints.pointTransactions.formerScandicHotel": [
{
"type": 0,
"value": "Former Scandic Hotel"
}
],
"myPoints.pointTransactions.noTransactions": [
{
"type": 0,
"value": "No transactions available"
}
],
"myPoints.pointTransactions.pointShop": [
{
"type": 0,
"value": "Scandic Friends Point Shop"
}
],
"myPoints.pointTransactions.pointsActivity": [
{
"type": 0,
"value": "Point activity"
}
],
"myPoints.pointTransactions.pointsEarnedPriorMay2021": [
{
"type": 0,
"value": "Points earned prior to May 1, 2021"
}
],
"myPoints.pointTransactions.redGift": [
{
"type": 0,
"value": "Reward Gift"
}
],
"myPoints.pointTransactions.rewardNight": [
{
"type": 0,
"value": "Reward Night"
}
],
"myPoints.pointTransactions.scandicFriendsMastercard": [
{
"type": 0,
"value": "Scandic Friends Mastercard"
}
],
"myPoints.pointTransactions.showMoreTransactions": [
{
"type": 0,
"value": "Show more transactions"
}
],
"myPoints.pointTransactions.signUpBonus": [
{
"type": 0,
"value": "Sign up bonus"
}
],
"myPoints.pointTransactions.stayAt": [
{
"type": 0,
"value": "Stay at "
},
{
"type": 1,
"value": "hotelName"
}
],
"myPoints.pointTransactions.tuiPoints": [
{
"type": 0,
"value": "TUI Points"
}
],
"myStay.accessDenied.bookingNotFound": [ "myStay.accessDenied.bookingNotFound": [
{ {
"type": 0, "type": 0,
@@ -7546,6 +7778,58 @@
"value": "Total stays" "value": "Total stays"
} }
], ],
"price.numberOfEuroBonusPoints": [
{
"offset": 0,
"options": {
"one": {
"value": [
{
"type": 0,
"value": "EB Point"
}
]
},
"other": {
"value": [
{
"type": 0,
"value": "EB Points"
}
]
}
},
"pluralType": "cardinal",
"type": 6,
"value": "numberOfEuroBonusPoints"
}
],
"price.numberOfScandicPoints": [
{
"offset": 0,
"options": {
"one": {
"value": [
{
"type": 0,
"value": "Point"
}
]
},
"other": {
"value": [
{
"type": 0,
"value": "Points"
}
]
}
},
"pluralType": "cardinal",
"type": 6,
"value": "numberOfScandicPoints"
}
],
"price.numberOfVouchers": [ "price.numberOfVouchers": [
{ {
"offset": 0, "offset": 0,
@@ -8610,6 +8894,12 @@
"value": "). See available rates below." "value": "). See available rates below."
} }
], ],
"selectRate.alert.reservationPolicies": [
{
"type": 0,
"value": "See rate details"
}
],
"selectRate.availableRooms.all": [ "selectRate.availableRooms.all": [
{ {
"offset": 0, "offset": 0,
+142
View File
@@ -231,6 +231,12 @@
"value": "Erityistoiveet (valinnainen)" "value": "Erityistoiveet (valinnainen)"
} }
], ],
"ancillaries.label.quantity": [
{
"type": 0,
"value": "Määrä"
}
],
"ancillaries.unableToDisplayBreakfastPrices": [ "ancillaries.unableToDisplayBreakfastPrices": [
{ {
"type": 0, "type": 0,
@@ -249,6 +255,18 @@
"value": "Hyväksyn varaus- ja peruutusehdot" "value": "Hyväksyn varaus- ja peruutusehdot"
} }
], ],
"booking.alert.extraBeds.text": [
{
"type": 0,
"value": "Kun varaat yli 2 vieraalle, lisämaksu veloitetaan per henkilö. Katso lisätietoja linkistä."
}
],
"booking.alert.extraguests": [
{
"type": 0,
"value": "Ylimääräinen vieras/vieraat"
}
],
"booking.approx": [ "booking.approx": [
{ {
"type": 0, "type": 0,
@@ -953,6 +971,20 @@
"value": "Lisää koodi" "value": "Lisää koodi"
} }
], ],
"bookingWidget.bookingCode.readMore": [
{
"type": 0,
"value": "Lue lisää "
},
{
"type": 1,
"value": "codeVoucher"
},
{
"type": 0,
"value": ":n käytöstä"
}
],
"bookingWidget.bookingCode.remember": [ "bookingWidget.bookingCode.remember": [
{ {
"type": 0, "type": 0,
@@ -1061,6 +1093,20 @@
"value": "details" "value": "details"
} }
], ],
"bookingWidget.reward.readMore": [
{
"type": 0,
"value": "Lue lisää "
},
{
"type": 1,
"value": "reward"
},
{
"type": 0,
"value": " varaamisesta"
}
],
"bookingWidget.reward.rewardNight": [ "bookingWidget.reward.rewardNight": [
{ {
"type": 0, "type": 0,
@@ -2514,6 +2560,44 @@
"value": "Päivämäärät" "value": "Päivämäärät"
} }
], ],
"designSystem.stepper.ariaLabel.currentCount": [
{
"type": 0,
"value": "Nykyinen "
},
{
"type": 1,
"value": "label"
},
{
"type": 0,
"value": " "
},
{
"type": 1,
"value": "count"
}
],
"designSystem.stepper.ariaLabel.decrease": [
{
"type": 0,
"value": "Vähennä "
},
{
"type": 1,
"value": "label"
}
],
"designSystem.stepper.ariaLabel.increase": [
{
"type": 0,
"value": "Lisää "
},
{
"type": 1,
"value": "label"
}
],
"destination.backToCities": [ "destination.backToCities": [
{ {
"type": 0, "type": 0,
@@ -7586,6 +7670,58 @@
"value": "Majoitukset yhteensä" "value": "Majoitukset yhteensä"
} }
], ],
"price.numberOfEuroBonusPoints": [
{
"offset": 0,
"options": {
"one": {
"value": [
{
"type": 0,
"value": "EB-piste"
}
]
},
"other": {
"value": [
{
"type": 0,
"value": "EB-pistettä"
}
]
}
},
"pluralType": "cardinal",
"type": 6,
"value": "numberOfEuroBonusPoints"
}
],
"price.numberOfScandicPoints": [
{
"offset": 0,
"options": {
"one": {
"value": [
{
"type": 0,
"value": "Piste"
}
]
},
"other": {
"value": [
{
"type": 0,
"value": "pistettä"
}
]
}
},
"pluralType": "cardinal",
"type": 6,
"value": "numberOfScandicPoints"
}
],
"price.numberOfVouchers": [ "price.numberOfVouchers": [
{ {
"offset": 0, "offset": 0,
@@ -8658,6 +8794,12 @@
"value": "). Katso saatavilla olevat hinnat alla." "value": "). Katso saatavilla olevat hinnat alla."
} }
], ],
"selectRate.alert.reservationPolicies": [
{
"type": 0,
"value": "Katso hinnan tiedot"
}
],
"selectRate.availableRooms.all": [ "selectRate.availableRooms.all": [
{ {
"offset": 0, "offset": 0,
+134
View File
@@ -231,6 +231,12 @@
"value": "Spesielle ønsker (valgfritt)" "value": "Spesielle ønsker (valgfritt)"
} }
], ],
"ancillaries.label.quantity": [
{
"type": 0,
"value": "Antall"
}
],
"ancillaries.unableToDisplayBreakfastPrices": [ "ancillaries.unableToDisplayBreakfastPrices": [
{ {
"type": 0, "type": 0,
@@ -249,6 +255,18 @@
"value": "Jeg godtar bestillings- og avbestillingsvilkårene" "value": "Jeg godtar bestillings- og avbestillingsvilkårene"
} }
], ],
"booking.alert.extraBeds.text": [
{
"type": 0,
"value": "Ved bestilling for mer enn 2 gjester vil det påløpe et tilleggsgebyr per person. Se lenke for detaljer."
}
],
"booking.alert.extraguests": [
{
"type": 0,
"value": "Ekstra gjest(er)"
}
],
"booking.approx": [ "booking.approx": [
{ {
"type": 0, "type": 0,
@@ -953,6 +971,16 @@
"value": "Legg til kode" "value": "Legg til kode"
} }
], ],
"bookingWidget.bookingCode.readMore": [
{
"type": 0,
"value": "Les mer om bruk av "
},
{
"type": 1,
"value": "codeVoucher"
}
],
"bookingWidget.bookingCode.remember": [ "bookingWidget.bookingCode.remember": [
{ {
"type": 0, "type": 0,
@@ -1061,6 +1089,16 @@
"value": "details" "value": "details"
} }
], ],
"bookingWidget.reward.readMore": [
{
"type": 0,
"value": "Les mer om bestilling med "
},
{
"type": 1,
"value": "reward"
}
],
"bookingWidget.reward.rewardNight": [ "bookingWidget.reward.rewardNight": [
{ {
"type": 0, "type": 0,
@@ -2510,6 +2548,44 @@
"value": "Datoer" "value": "Datoer"
} }
], ],
"designSystem.stepper.ariaLabel.currentCount": [
{
"type": 0,
"value": "Gjeldende "
},
{
"type": 1,
"value": "label"
},
{
"type": 0,
"value": " "
},
{
"type": 1,
"value": "count"
}
],
"designSystem.stepper.ariaLabel.decrease": [
{
"type": 0,
"value": "Reduser "
},
{
"type": 1,
"value": "label"
}
],
"designSystem.stepper.ariaLabel.increase": [
{
"type": 0,
"value": "Øk "
},
{
"type": 1,
"value": "label"
}
],
"destination.backToCities": [ "destination.backToCities": [
{ {
"type": 0, "type": 0,
@@ -7582,6 +7658,58 @@
"value": "Totalt antall opphold" "value": "Totalt antall opphold"
} }
], ],
"price.numberOfEuroBonusPoints": [
{
"offset": 0,
"options": {
"one": {
"value": [
{
"type": 0,
"value": "EB-poeng"
}
]
},
"other": {
"value": [
{
"type": 0,
"value": "EB-poeng"
}
]
}
},
"pluralType": "cardinal",
"type": 6,
"value": "numberOfEuroBonusPoints"
}
],
"price.numberOfScandicPoints": [
{
"offset": 0,
"options": {
"one": {
"value": [
{
"type": 0,
"value": "Poeng"
}
]
},
"other": {
"value": [
{
"type": 0,
"value": "Poeng"
}
]
}
},
"pluralType": "cardinal",
"type": 6,
"value": "numberOfScandicPoints"
}
],
"price.numberOfVouchers": [ "price.numberOfVouchers": [
{ {
"offset": 0, "offset": 0,
@@ -8654,6 +8782,12 @@
"value": " ). Se tilgjengelige priser nedenfor." "value": " ). Se tilgjengelige priser nedenfor."
} }
], ],
"selectRate.alert.reservationPolicies": [
{
"type": 0,
"value": "Se prisdetaljer"
}
],
"selectRate.availableRooms.all": [ "selectRate.availableRooms.all": [
{ {
"offset": 0, "offset": 0,
+135 -1
View File
@@ -231,6 +231,12 @@
"value": "Särskilda önskemål (valfritt)" "value": "Särskilda önskemål (valfritt)"
} }
], ],
"ancillaries.label.quantity": [
{
"type": 0,
"value": "Antal"
}
],
"ancillaries.unableToDisplayBreakfastPrices": [ "ancillaries.unableToDisplayBreakfastPrices": [
{ {
"type": 0, "type": 0,
@@ -249,6 +255,18 @@
"value": "Jag accepterar boknings- och avbokningsvillkoren" "value": "Jag accepterar boknings- och avbokningsvillkoren"
} }
], ],
"booking.alert.extraBeds.text": [
{
"type": 0,
"value": "När du bokar för fler än 2 gäster tillkommer en extra avgift per person. Se länk för detaljer."
}
],
"booking.alert.extraguests": [
{
"type": 0,
"value": "Extra gäst(er)"
}
],
"booking.approx": [ "booking.approx": [
{ {
"type": 0, "type": 0,
@@ -953,6 +971,16 @@
"value": "Lägg till kod" "value": "Lägg till kod"
} }
], ],
"bookingWidget.bookingCode.readMore": [
{
"type": 0,
"value": "Läs mer om att använda "
},
{
"type": 1,
"value": "codeVoucher"
}
],
"bookingWidget.bookingCode.remember": [ "bookingWidget.bookingCode.remember": [
{ {
"type": 0, "type": 0,
@@ -1061,6 +1089,16 @@
"value": "details" "value": "details"
} }
], ],
"bookingWidget.reward.readMore": [
{
"type": 0,
"value": "Läs mer om bokning med "
},
{
"type": 1,
"value": "reward"
}
],
"bookingWidget.reward.rewardNight": [ "bookingWidget.reward.rewardNight": [
{ {
"type": 0, "type": 0,
@@ -2510,6 +2548,44 @@
"value": "Datum" "value": "Datum"
} }
], ],
"designSystem.stepper.ariaLabel.currentCount": [
{
"type": 0,
"value": "Aktuell "
},
{
"type": 1,
"value": "label"
},
{
"type": 0,
"value": " "
},
{
"type": 1,
"value": "count"
}
],
"designSystem.stepper.ariaLabel.decrease": [
{
"type": 0,
"value": "Minska "
},
{
"type": 1,
"value": "label"
}
],
"designSystem.stepper.ariaLabel.increase": [
{
"type": 0,
"value": "Öka "
},
{
"type": 1,
"value": "label"
}
],
"destination.backToCities": [ "destination.backToCities": [
{ {
"type": 0, "type": 0,
@@ -3845,7 +3921,7 @@
"findMyBooking.findYourStay": [ "findMyBooking.findYourStay": [
{ {
"type": 0, "type": 0,
"value": "Hitta ditt hotell" "value": "Hitta din bokning"
} }
], ],
"findMyBooking.manageBooking": [ "findMyBooking.manageBooking": [
@@ -7566,6 +7642,58 @@
"value": "Totalt antal vistelser" "value": "Totalt antal vistelser"
} }
], ],
"price.numberOfEuroBonusPoints": [
{
"offset": 0,
"options": {
"one": {
"value": [
{
"type": 0,
"value": "EB-poäng"
}
]
},
"other": {
"value": [
{
"type": 0,
"value": "EB-poäng"
}
]
}
},
"pluralType": "cardinal",
"type": 6,
"value": "numberOfEuroBonusPoints"
}
],
"price.numberOfScandicPoints": [
{
"offset": 0,
"options": {
"one": {
"value": [
{
"type": 0,
"value": "Punkt"
}
]
},
"other": {
"value": [
{
"type": 0,
"value": "Poäng"
}
]
}
},
"pluralType": "cardinal",
"type": 6,
"value": "numberOfScandicPoints"
}
],
"price.numberOfVouchers": [ "price.numberOfVouchers": [
{ {
"offset": 0, "offset": 0,
@@ -8626,6 +8754,12 @@
"value": "). Se tillgängliga priser nedan." "value": "). Se tillgängliga priser nedan."
} }
], ],
"selectRate.alert.reservationPolicies": [
{
"type": 0,
"value": "Se bokningsvillkor"
}
],
"selectRate.availableRooms.all": [ "selectRate.availableRooms.all": [
{ {
"offset": 0, "offset": 0,
+5 -5
View File
@@ -22,13 +22,13 @@
}, },
"dependencies": { "dependencies": {
"@formatjs/intl": "^3.1.6", "@formatjs/intl": "^3.1.6",
"@netlify/plugin-nextjs": "^5.15.1", "@netlify/plugin-nextjs": "^5.15.7",
"@scandic-hotels/booking-flow": "workspace:*", "@scandic-hotels/booking-flow": "workspace:*",
"@scandic-hotels/design-system": "workspace:*", "@scandic-hotels/design-system": "workspace:*",
"@scandic-hotels/tracking": "workspace:*", "@scandic-hotels/tracking": "workspace:*",
"@scandic-hotels/trpc": "workspace:*", "@scandic-hotels/trpc": "workspace:*",
"@sentry/nextjs": "^10.33.0", "@sentry/nextjs": "^10.33.0",
"@swc/plugin-formatjs": "^3.2.2", "@swc/plugin-formatjs": "^8.1.0",
"@t3-oss/env-nextjs": "^0.13.4", "@t3-oss/env-nextjs": "^0.13.4",
"@tanstack/react-query": "^5.75.5", "@tanstack/react-query": "^5.75.5",
"@tanstack/react-query-devtools": "^5.75.5", "@tanstack/react-query-devtools": "^5.75.5",
@@ -36,11 +36,11 @@
"@trpc/server": "^11.1.2", "@trpc/server": "^11.1.2",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"iron-session": "^8.0.4", "iron-session": "^8.0.4",
"next": "16.0.10", "next": "16.1.6",
"next-auth": "5.0.0-beta.29", "next-auth": "5.0.0-beta.29",
"react": "19.2.1", "react": "19.2.4",
"react-aria-components": "1.8.0", "react-aria-components": "1.8.0",
"react-dom": "19.2.1", "react-dom": "19.2.4",
"react-intl": "^7.1.11", "react-intl": "^7.1.11",
"server-only": "^0.0.1", "server-only": "^0.0.1",
"usehooks-ts": "3.1.1", "usehooks-ts": "3.1.1",
+1
View File
@@ -52,3 +52,4 @@ DTMC_ENTRA_ID_CLIENT=""
DTMC_ENTRA_ID_ISSUER="" DTMC_ENTRA_ID_ISSUER=""
DTMC_ENTRA_ID_SECRET="" DTMC_ENTRA_ID_SECRET=""
NEXT_PUBLIC_NEW_POINTCLAIMS="true"
+12
View File
@@ -16,6 +16,18 @@ yarn workspace @scandic-hotels/design-system build
yarn dev yarn dev
``` ```
To run only scandic web
```bash
yarn dev:web
```
To run only partner sas
```bash
yarn dev:sas
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
### Caching ### Caching
@@ -4,7 +4,8 @@
.layout { .layout {
display: grid; display: grid;
font-family: var(--typography-Body-Regular-fontFamily); font-family:
var(--Body-Paragraph-Font-family), var(--Body-Paragraph-Font-fallback);
grid-template-rows: auto 1fr; grid-template-rows: auto 1fr;
min-height: 100dvh; min-height: 100dvh;
max-width: var(--max-width-page); max-width: var(--max-width-page);
@@ -1,12 +1,7 @@
import { TrackingSDK } from "@scandic-hotels/tracking/TrackingSDK" import { TrackingSDK } from "@scandic-hotels/tracking/TrackingSDK"
import { import { getEurobonusMembership } from "@scandic-hotels/trpc/routers/user/helpers"
getEurobonusMembership,
scandicMembershipTypes,
} from "@scandic-hotels/trpc/routers/user/helpers"
import { env } from "@/env/server"
import { import {
getBasicProfileSafely,
getProfileSafely, getProfileSafely,
getProfilingConsent, getProfilingConsent,
} from "@/lib/trpc/memoizedRequests" } from "@/lib/trpc/memoizedRequests"
@@ -26,15 +21,7 @@ type MyPagesLayoutProps = React.PropsWithChildren<{
breadcrumbs: React.ReactNode breadcrumbs: React.ReactNode
}> }>
export default async function MyPagesLayout(props: MyPagesLayoutProps) { export default async function MyPagesLayout({
if (env.ENABLE_PROFILE_CONSENT) {
return <MyPagesLayoutWithConsent {...props} />
}
return <MyPagesLayoutBase {...props} />
}
async function MyPagesLayoutWithConsent({
breadcrumbs, breadcrumbs,
children, children,
}: MyPagesLayoutProps) { }: MyPagesLayoutProps) {
@@ -84,25 +71,3 @@ async function MyPagesLayoutWithConsent({
</ProfilingConsentAlertProvider> </ProfilingConsentAlertProvider>
) )
} }
async function MyPagesLayoutBase({
breadcrumbs,
children,
}: MyPagesLayoutProps) {
const profile = await getBasicProfileSafely()
const eurobonusMembership = profile?.loyalty?.memberships?.find(
(m) => m.membershipType === scandicMembershipTypes.SAS_EB
)
return (
<div className={styles.container}>
<div className={styles.layout}>
{breadcrumbs}
<div className={styles.content}>{children}</div>
</div>
{eurobonusMembership && <SASLevelUpgradeCheck />}
<Surprises />
</div>
)
}
@@ -1,24 +1,13 @@
import { redirect } from "next/navigation"
import { profile } from "@scandic-hotels/common/constants/routes/myPages"
import { TrackingSDK } from "@scandic-hotels/tracking/TrackingSDK" import { TrackingSDK } from "@scandic-hotels/tracking/TrackingSDK"
import { env } from "@/env/server"
import { getProfile } from "@/lib/trpc/memoizedRequests" import { getProfile } from "@/lib/trpc/memoizedRequests"
import { serverClient } from "@/lib/trpc/server" import { serverClient } from "@/lib/trpc/server"
import { ProfilingConsent } from "@/components/Forms/ProfilingConsent" import { ProfilingConsent } from "@/components/Forms/ProfilingConsent"
import { getLang } from "@/i18n/serverContext"
import styles from "./page.module.css" import styles from "./page.module.css"
export default async function ProfilingConsentSlot() { export default async function ProfilingConsentSlot() {
const lang = await getLang()
if (!env.ENABLE_PROFILE_CONSENT) {
redirect(profile[lang])
}
const caller = await serverClient() const caller = await serverClient()
const accountPage = await caller.contentstack.accountPage.get() const accountPage = await caller.contentstack.accountPage.get()
const user = await getProfile() const user = await getProfile()
@@ -1,6 +1,7 @@
.layout { .layout {
display: grid; display: grid;
font-family: var(--typography-Body-Regular-fontFamily); font-family:
var(--Body-Paragraph-Font-family), var(--Body-Paragraph-Font-fallback);
grid-template-rows: auto 1fr; grid-template-rows: auto 1fr;
position: relative; position: relative;
} }
@@ -1,3 +1,4 @@
.layout { .layout {
font-family: var(--typography-Body-Regular-fontFamily); font-family:
var(--Body-Paragraph-Font-family), var(--Body-Paragraph-Font-fallback);
} }
@@ -5,6 +5,7 @@ import { useTransition } from "react"
import { FormProvider, useForm } from "react-hook-form" import { FormProvider, useForm } from "react-hook-form"
import { useIntl } from "react-intl" import { useIntl } from "react-intl"
import { sasPartnershipTermsAndConditions } from "@scandic-hotels/common/constants/routes/customerService"
import { profileEdit } from "@scandic-hotels/common/constants/routes/myPages" import { profileEdit } from "@scandic-hotels/common/constants/routes/myPages"
import { Button } from "@scandic-hotels/design-system/Button" import { Button } from "@scandic-hotels/design-system/Button"
import Checkbox from "@scandic-hotels/design-system/Form/Checkbox" import Checkbox from "@scandic-hotels/design-system/Form/Checkbox"
@@ -13,8 +14,6 @@ import Image from "@scandic-hotels/design-system/Image"
import Link from "@scandic-hotels/design-system/OldDSLink" import Link from "@scandic-hotels/design-system/OldDSLink"
import { Typography } from "@scandic-hotels/design-system/Typography" import { Typography } from "@scandic-hotels/design-system/Typography"
import { sasPartnershipTermsAndConditions } from "@/constants/webHrefs"
import styles from "./link-sas.module.css" import styles from "./link-sas.module.css"
import type { LangParams } from "@/types/params" import type { LangParams } from "@/types/params"
@@ -1,9 +1,8 @@
import { redirect } from "next/navigation" import { redirect } from "next/navigation"
import { z } from "zod" import { z } from "zod"
import Footnote from "@scandic-hotels/design-system/Footnote"
import Image from "@scandic-hotels/design-system/Image" import Image from "@scandic-hotels/design-system/Image"
import Link from "@scandic-hotels/design-system/OldDSLink" import { TextLink } from "@scandic-hotels/design-system/TextLink"
import { Typography } from "@scandic-hotels/design-system/Typography" import { Typography } from "@scandic-hotels/design-system/Typography"
import { env } from "@/env/server" import { env } from "@/env/server"
@@ -94,27 +93,24 @@ export default async function SASxScandicLoginPage(
{intentDescriptions[parsedParams.intent]} {intentDescriptions[parsedParams.intent]}
</p> </p>
</Typography> </Typography>
<Footnote textAlign="center"> <Typography variant="Body/Supporting text (caption)/smRegular">
{intl.formatMessage( <p style={{ textAlign: "center" }}>
{ {intl.formatMessage(
id: "linkEuroBonusAccount.manualRedirectLinkMessage", {
defaultMessage: id: "linkEuroBonusAccount.manualRedirectLinkMessage",
"If you are not redirected automatically, please <loginLink>click here</loginLink>.", defaultMessage:
}, "If you are not redirected automatically, please <loginLink>click here</loginLink>.",
{ },
loginLink: (str) => ( {
<Link loginLink: (str) => (
href={loginLink} <TextLink typography="Link/sm" href={loginLink}>
color="red" {str}
size="tiny" </TextLink>
textDecoration="underline" ),
> }
{str} )}
</Link> </p>
), </Typography>
}
)}
</Footnote>
</SASModal> </SASModal>
) )
} }
@@ -42,7 +42,8 @@
width: 34px; width: 34px;
height: 0px; height: 0px;
padding: var(--Space-x3) 0; padding: var(--Space-x3) 0;
font-family: var(--typography-Body-Regular-fontFamily); font-family:
var(--Body-Paragraph-Font-family), var(--Body-Paragraph-Font-fallback);
border: 1px solid var(--Base-Border-Normal); border: 1px solid var(--Base-Border-Normal);
border-radius: var(--Corner-Radius-md); border-radius: var(--Corner-Radius-md);
text-align: center; text-align: center;
@@ -1,5 +1,6 @@
.layout { .layout {
background-color: var(--Background-Primary); background-color: var(--Background-Primary);
font-family: var(--typography-Body-Regular-fontFamily); font-family:
var(--Body-Paragraph-Font-family), var(--Body-Paragraph-Font-fallback);
min-height: 100dvh; min-height: 100dvh;
} }
+6
View File
@@ -88,6 +88,12 @@ body:has([data-booking-widget-open="true"]) #kindly-chat-api {
z-index: -1 !important; z-index: -1 !important;
} }
/* When a select in the booking widget is open, react-aria sets overflow:hidden
which breaks sticky positioning. Override with clip which doesn't break sticky. */
body:has([data-booking-widget-open] [data-open]) {
overflow: clip !important;
}
@media screen and (min-width: 768px) { @media screen and (min-width: 768px) {
:root { :root {
--max-width-single-spacing: var(--Layout-Tablet-Margin-Margin-min); --max-width-single-spacing: var(--Layout-Tablet-Margin-Margin-min);
+2 -1
View File
@@ -1,3 +1,4 @@
.layout { .layout {
font-family: var(--typography-Body-Regular-fontFamily); font-family:
var(--Body-Paragraph-Font-family), var(--Body-Paragraph-Font-fallback);
} }
@@ -1,13 +1,29 @@
import { Alert } from "@scandic-hotels/design-system/Alert" import { Alert } from "@scandic-hotels/design-system/Alert"
import { getAlertPhoneContactData } from "@scandic-hotels/trpc/routers/contentstack/base/utils"
import { serverClient } from "@/lib/trpc/server"
import type { AlertBlock } from "@scandic-hotels/trpc/types/blocks" import type { AlertBlock } from "@scandic-hotels/trpc/types/blocks"
interface AlertBlockProps extends Pick<AlertBlock, "alert"> {} interface AlertBlockProps extends Pick<AlertBlock, "alert"> {}
export function AlertBlock({ alert }: AlertBlockProps) { export async function AlertBlock({ alert }: AlertBlockProps) {
const caller = await serverClient()
const contactConfig = await caller.contentstack.base.contact()
if (!alert) { if (!alert) {
return null return null
} }
return <Alert {...alert} /> const phoneContact =
alert.phoneContact && contactConfig
? getAlertPhoneContactData(alert, contactConfig)
: null
return (
<Alert
{...alert}
phoneContact={phoneContact}
sidepeekCtaText={alert.sidepeekButton?.cta_text}
/>
)
} }
@@ -16,18 +16,14 @@
.iconTh { .iconTh {
padding: var(--Space-x5) var(--Space-x2) var(--Space-x2); padding: var(--Space-x5) var(--Space-x2) var(--Space-x2);
font-weight: var(--typography-Caption-Regular-fontWeight);
vertical-align: bottom; vertical-align: bottom;
} }
.summaryTh { .summaryTh {
font-size: var(--typography-Caption-Regular-fontSize);
font-weight: var(--typography-Caption-Regular-fontWeight);
padding: 0 var(--Space-x2) var(--Space-x2); padding: 0 var(--Space-x2) var(--Space-x2);
vertical-align: top; vertical-align: top;
} }
.select { .select {
font-weight: var(--typography-Caption-Regular-fontWeight);
padding: 0 var(--Space-x2) var(--Space-x2); padding: 0 var(--Space-x2) var(--Space-x2);
} }
@@ -1,3 +1,5 @@
import { Typography } from "@scandic-hotels/design-system/Typography"
import MembershipLevelIcon from "@/components/Levels/Icon" import MembershipLevelIcon from "@/components/Levels/Icon"
import LevelSummary from "../../LevelSummary" import LevelSummary from "../../LevelSummary"
@@ -37,12 +39,14 @@ export default function DesktopHeader({
<th /> <th />
{levels.map((level, idx) => { {levels.map((level, idx) => {
return ( return (
<th <Typography
key={"summary" + level.level_id + idx} variant="Body/Supporting text (caption)/smRegular"
className={styles.summaryTh} key={"name" + level.level_id + idx}
> >
<LevelSummary level={level} /> <th className={styles.summaryTh}>
</th> <LevelSummary level={level} />
</th>
</Typography>
) )
})} })}
</tr> </tr>
@@ -82,10 +82,12 @@ function RewardTableHeader({ name, description }: RewardTableHeaderProps) {
</span> </span>
</hgroup> </hgroup>
</summary> </summary>
<p <Typography variant="Body/Supporting text (caption)/smRegular">
className={styles.rewardDescription} <p
dangerouslySetInnerHTML={{ __html: description }} className={styles.rewardDescription}
/> dangerouslySetInnerHTML={{ __html: description }}
/>
</Typography>
</details> </details>
) )
} }
@@ -15,14 +15,11 @@
} }
.td { .td {
font-size: var(--typography-Footnote-Regular-fontSize);
text-align: center; text-align: center;
} }
.rewardTh { .rewardTh {
padding: var(--Space-x3) var(--Space-x2); padding: var(--Space-x3) var(--Space-x2);
font-size: var(--typography-Caption-Regular-fontSize);
font-weight: var(--typography-Caption-Regular-fontWeight);
} }
.details[open] .chevron { .details[open] .chevron {
@@ -1,5 +1,7 @@
import { useIntl } from "react-intl" import { useIntl } from "react-intl"
import { Typography } from "@scandic-hotels/design-system/Typography"
import styles from "./levelSummary.module.css" import styles from "./levelSummary.module.css"
import type { LevelSummaryProps } from "@/types/components/overviewTable" import type { LevelSummaryProps } from "@/types/components/overviewTable"
@@ -32,7 +34,9 @@ export default function LevelSummary({
return ( return (
<div className={styles.levelSummary}> <div className={styles.levelSummary}>
<span className={styles.levelRequirements}>{pointsMsg}</span> <Typography variant="Label/xsRegular">
<span className={styles.levelRequirements}>{pointsMsg}</span>
</Typography>
{showDescription && ( {showDescription && (
<p className={styles.levelSummaryText}>{level.description}</p> <p className={styles.levelSummaryText}>{level.description}</p>
)} )}
@@ -8,16 +8,14 @@
.levelRequirements { .levelRequirements {
border-radius: var(--Corner-Radius-md); border-radius: var(--Corner-Radius-md);
background-color: var(--Scandic-Brand-Pale-Peach); background-color: var(--Surface-Brand-Primary-1-Default);
color: var(--Scandic-Peach-80); color: var(--Text-Interactive-Secondary);
padding: var(--Space-x05) var(--Space-x1); padding: var(--Space-x05) var(--Space-x1);
text-align: center; text-align: center;
width: 100%; width: 100%;
} }
.levelSummaryText { .levelSummaryText {
font-size: var(--typography-Caption-Regular-fontSize);
line-height: var(--typography-Body-Regular-lineHeight);
margin: 0; margin: 0;
} }
@@ -26,12 +24,3 @@
padding: var(--Space-x05) var(--Space-x1); padding: var(--Space-x05) var(--Space-x1);
} }
} }
@media screen and (min-width: 1367px) {
.levelRequirements {
font-size: var(--typography-Footnote-Regular-fontSize);
}
.levelSummaryText {
font-size: var(--typography-Caption-Regular-fontSize);
}
}
@@ -27,10 +27,12 @@ export default function RewardCard({
</span> </span>
</hgroup> </hgroup>
</summary> </summary>
<p <Typography variant="Body/Supporting text (caption)/smRegular">
className={styles.rewardCardDescription} <p
dangerouslySetInnerHTML={{ __html: description }} className={styles.rewardCardDescription}
/> dangerouslySetInnerHTML={{ __html: description }}
/>
</Typography>
</details> </details>
</div> </div>
<div className={styles.rewardComparison}> <div className={styles.rewardComparison}>
@@ -12,8 +12,6 @@
} }
.rewardCardDescription { .rewardCardDescription {
font-size: var(--typography-Caption-Regular-fontSize);
line-height: 150%;
padding-right: var(--Space-x4); padding-right: var(--Space-x4);
} }
@@ -1,6 +1,7 @@
import { Minus } from "react-feather" import { Minus } from "react-feather"
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon" import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
import { Typography } from "@scandic-hotels/design-system/Typography"
import styles from "./rewardValue.module.css" import styles from "./rewardValue.module.css"
@@ -21,8 +22,8 @@ export default function RewardValue({ reward }: RewardValueProps) {
) )
} }
return ( return (
<div className={styles.rewardValueContainer}> <Typography variant="Body/Paragraph/mdBold">
<span className={styles.rewardValue}>{reward.value}</span> <div className={styles.rewardValueContainer}>{reward.value}</div>
</div> </Typography>
) )
} }
@@ -7,17 +7,6 @@
text-wrap: balance; text-wrap: balance;
} }
.rewardValue {
font-size: var(--typography-Body-Bold-fontSize);
font-weight: var(--typography-Body-Bold-fontWeight);
}
.rewardValueDetails {
font-size: var(--typography-Footnote-Regular-fontSize);
text-align: center;
color: var(--UI-Grey-80);
}
.checkIcon { .checkIcon {
display: inline-flex; display: inline-flex;
} }
@@ -0,0 +1,430 @@
/* eslint-disable formatjs/no-literal-string-in-jsx */
/* TODO remove disable and add i18n */
/* TODO add analytics */
import { zodResolver } from "@hookform/resolvers/zod"
import { cx } from "class-variance-authority"
import { useState } from "react"
import { FormProvider, useForm, useWatch } from "react-hook-form"
import { useIntl } from "react-intl"
import z from "zod"
import { dt } from "@scandic-hotels/common/dt"
import { Button } from "@scandic-hotels/design-system/Button"
import { FormInput } from "@scandic-hotels/design-system/Form/FormInput"
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
import { LoadingSpinner } from "@scandic-hotels/design-system/LoadingSpinner"
import { MessageBanner } from "@scandic-hotels/design-system/MessageBanner"
import { Typography } from "@scandic-hotels/design-system/Typography"
import { trpc } from "@scandic-hotels/trpc/client"
import useLang from "@/hooks/useLang"
import styles from "./claimPoints.module.css"
type PointClaimBookingInfo = {
from: string
to: string
city: string
hotel: string
}
export function ClaimPointsWizard({
onSuccess,
onClose,
}: {
onSuccess: () => void
onClose: () => void
}) {
const [state, setState] = useState<
"initial" | "loading" | "invalid" | "form"
>("initial")
const [bookingDetails, setBookingDetails] =
useState<PointClaimBookingInfo | null>(null)
const { data, isLoading } = trpc.user.getSafely.useQuery()
if (state === "invalid") {
return <InvalidBooking onClose={onClose} />
}
if (state === "form") {
if (isLoading) {
return null
}
return (
<ClaimPointsForm
onSuccess={onSuccess}
initialData={{
...bookingDetails,
firstName: data?.firstName ?? "",
lastName: data?.lastName ?? "",
email: data?.email ?? "",
phone: data?.phoneNumber ?? "",
}}
/>
)
}
const handleBookingNumberEvent = (event: BookingNumberEvent) => {
switch (event.type) {
case "submit":
setState("loading")
break
case "error":
setState("initial")
break
case "invalid":
setState("invalid")
break
case "success":
setBookingDetails(event.data)
setState("form")
break
}
}
return (
<div className={styles.introWrapper}>
{state === "loading" && (
<div
className={styles.spinner}
aria-live="polite"
aria-label="Loading booking details, please wait.."
>
<LoadingSpinner />
</div>
)}
<div
className={cx(styles.options, { [styles.hidden]: state === "loading" })}
>
<section className={styles.sectionCard}>
<div className={styles.sectionInfo}>
<Typography variant="Body/Paragraph/mdBold">
<h4>Claim points with booking number</h4>
</Typography>
<Typography variant="Body/Supporting text (caption)/smRegular">
<p>
Enter a valid booking number to load booking details
automatically.
</p>
</Typography>
</div>
<BookingNumberInput onEvent={handleBookingNumberEvent} />
</section>
<Divider />
<section className={styles.sectionCard}>
<div className={styles.sectionInfo}>
<Typography variant="Body/Paragraph/mdBold">
<h4>Claim points without booking number</h4>
</Typography>
<Typography variant="Body/Supporting text (caption)/smRegular">
<p>You need to add booking details in a form.</p>
</Typography>
</div>
<Button variant="Secondary" onPress={() => setState("form")}>
Fill form to claim points
</Button>
</section>
</div>
<MessageBanner
type="info"
text="Points can be claimed up to 6 months back if you were a member at the time of your stay."
/>
</div>
)
}
type BookingNumberFormData = {
bookingNumber: string
}
type BookingNumberEvent =
| { type: "submit" }
| { type: "success"; data: PointClaimBookingInfo }
| { type: "error" }
| { type: "invalid" }
function BookingNumberInput({
onEvent,
}: {
onEvent: (event: BookingNumberEvent) => void
}) {
const lang = useLang()
const form = useForm<BookingNumberFormData>({
resolver: zodResolver(
z.object({
bookingNumber: z
.string()
// TODO Check UX for validation as different environments have different lengths
.min(9, { message: "Booking number must be 10 digits" })
.max(10, { message: "Booking number must be 10 digits" }),
})
),
defaultValues: {
bookingNumber: "",
},
})
const confirmationNumber = useWatch({
name: "bookingNumber",
control: form.control,
})
const { refetch, isFetching } =
trpc.booking.findBookingForCurrentUser.useQuery(
{
confirmationNumber,
lang,
},
{ enabled: false }
)
const handleSubmit = async () => {
onEvent({ type: "submit" })
const result = await refetch()
if (!result.data) {
onEvent({ type: "error" })
form.setError("bookingNumber", {
type: "manual",
message:
"We could not find a booking with this number registered in your name.",
})
return
}
const data = result.data
// TODO validate if this should be check out or check in date
const checkOutDate = dt(data.booking.checkOutDate)
const sixMonthsAgo = dt().subtract(6, "months")
if (checkOutDate.isBefore(sixMonthsAgo, "day")) {
onEvent({ type: "invalid" })
return
}
onEvent({
type: "success",
data: {
from: data.booking.checkInDate,
to: data.booking.checkOutDate,
city: data.hotel.cityName,
hotel: data.hotel.name,
},
})
}
return (
<FormProvider {...form}>
<form onSubmit={form.handleSubmit(handleSubmit)}>
<FormInput
name="bookingNumber"
label="Booking number"
leftIcon={<MaterialIcon icon="edit_document" />}
description="Enter your 10-digit booking number"
maxLength={10}
showClearContentIcon
disabled={isFetching}
autoFocus
autoComplete="off"
onChange={(e) => {
const value = e.target.value
if (value.length !== 10) return
form.handleSubmit(handleSubmit)()
}}
/>
</form>
</FormProvider>
)
}
function InvalidBooking({ onClose }: { onClose: () => void }) {
return (
<div className={styles.invalidWrapper}>
<Typography variant="Body/Paragraph/mdRegular">
<p>
We cant add these points to your account as it has been longer than 6
months since your stay.
</p>
</Typography>
<Button variant="Primary" fullWidth onPress={onClose}>
Close
</Button>
</div>
)
}
type PointClaimUserInfo = {
firstName: string
lastName: string
email: string
phone: string
}
function ClaimPointsForm({
onSuccess,
initialData,
}: {
onSuccess: () => void
initialData: Partial<PointClaimBookingInfo & PointClaimUserInfo> | null
}) {
const form = useForm({
resolver: zodResolver(
z.object({
from: z.string().min(1, { message: "Arrival date is required" }),
to: z.string().min(1, { message: "Departure date is required" }),
city: z.string().min(1, { message: "City is required" }),
hotel: z.string().min(1, { message: "Hotel is required" }),
firstName: z.string().min(1, { message: "First name is required" }),
lastName: z.string().min(1, { message: "Last name is required" }),
email: z
.string()
.email("Enter a valid email")
.min(1, { message: "Email is required" }),
phone: z.string().min(1, { message: "Phone is required" }),
})
),
defaultValues: {
from: initialData?.from || "",
to: initialData?.to || "",
city: initialData?.city || "",
hotel: initialData?.hotel || "",
firstName: initialData?.firstName || "",
lastName: initialData?.lastName || "",
email: initialData?.email || "",
phone: initialData?.phone || "",
},
mode: "all",
})
const { mutate, isPending } = trpc.user.claimPoints.useMutation({
onSuccess,
})
const autoFocusField = getAutoFocus(initialData)
return (
<FormProvider {...form}>
<form
className={styles.form}
onSubmit={form.handleSubmit((data) => mutate(data))}
>
<div className={styles.formInputs}>
{!initialData?.firstName && (
<FormInput
name="firstName"
label="First name"
autoFocus={autoFocusField === "firstName"}
readOnly={isPending}
registerOptions={{ required: true }}
/>
)}
{!initialData?.lastName && (
<FormInput
name="lastName"
label="Last name"
autoFocus={autoFocusField === "lastName"}
readOnly={isPending}
registerOptions={{ required: true }}
/>
)}
{!initialData?.email && (
<FormInput
name="email"
label="Email"
type="email"
autoFocus={autoFocusField === "email"}
readOnly={isPending}
registerOptions={{ required: true }}
/>
)}
{!initialData?.phone && (
<FormInput
name="phone"
label="Phone"
type="tel"
autoFocus={autoFocusField === "phone"}
readOnly={isPending}
registerOptions={{ required: true }}
/>
)}
<FormInput
name="from"
label="Arrival (YYYY-MM-DD)"
leftIcon={<MaterialIcon icon="calendar_today" />}
autoFocus={autoFocusField === "from"}
readOnly={isPending}
registerOptions={{ required: true }}
/>
<FormInput
name="to"
label="Departure (YYYY-MM-DD)"
leftIcon={<MaterialIcon icon="calendar_today" />}
readOnly={isPending}
registerOptions={{ required: true }}
/>
<FormInput
name="city"
label="City"
readOnly={isPending}
registerOptions={{ required: true }}
/>
<FormInput
name="hotel"
label="Hotel"
readOnly={isPending}
registerOptions={{ required: true }}
/>
</div>
<MessageBanner
type="info"
text="Points can be claimed up to 6 months back if you were a member at the time of your stay."
/>
<Button
type="submit"
variant="Primary"
fullWidth
isDisabled={!form.formState.isValid}
isPending={isPending}
className={styles.formSubmit}
>
Send points claim
</Button>
</form>
</FormProvider>
)
}
function getAutoFocus(userInfo: Partial<PointClaimUserInfo> | null) {
if (!userInfo?.firstName) {
return "firstName"
}
if (!userInfo?.lastName) {
return "lastName"
}
if (!userInfo?.email) {
return "email"
}
if (!userInfo?.phone) {
return "phone"
}
return "from"
}
function Divider() {
const intl = useIntl()
return (
<div className={styles.divider}>
<Typography variant="Body/Paragraph/mdRegular">
<span>
{intl.formatMessage({
id: "common.or",
defaultMessage: "or",
})}
</span>
</Typography>
</div>
)
}
@@ -6,3 +6,100 @@
gap: var(--Space-x2); gap: var(--Space-x2);
white-space: nowrap; white-space: nowrap;
} }
.dialog {
max-width: 560px;
}
.introWrapper {
position: relative;
display: flex;
flex-direction: column;
gap: var(--Space-x3);
}
.options {
display: flex;
flex-direction: column;
gap: var(--Space-x1);
}
.sectionCard {
display: flex;
flex-direction: column;
gap: var(--Space-x3);
padding: var(--Space-x2);
background-color: var(--Surface-Primary-OnSurface-Default);
border-radius: var(--Corner-Radius-md);
}
.sectionInfo {
display: flex;
flex-direction: column;
gap: var(--Space-x1);
}
.spinner {
background-color: var(--Base-Surface-Primary-light-Normal);
position: absolute;
inset: 0;
z-index: 1;
display: flex;
justify-content: center;
align-items: center;
}
.bookingInputDescription {
display: flex;
align-items: center;
gap: var(--Space-x05);
}
.form {
display: flex;
flex-direction: column;
gap: var(--Space-x3);
}
.formInputs {
display: flex;
flex-direction: column;
gap: var(--Space-x2);
}
.formSubmit {
margin-top: auto;
}
.divider {
width: 100%;
position: relative;
display: flex;
justify-content: center;
& > span {
position: relative;
padding: 0 var(--Space-x2);
background-color: white;
}
&::before {
position: absolute;
bottom: calc(50% - 1px);
content: "";
display: block;
height: 1px;
width: 100%;
background-color: var(--Border-Default);
}
}
.hidden {
visibility: hidden;
}
.invalidWrapper {
display: flex;
flex-direction: column;
gap: var(--Space-x3);
}
@@ -1,18 +1,110 @@
/* eslint-disable formatjs/no-literal-string-in-jsx */
/* TODO remove disable and add i18n */
"use client" "use client"
import { useEffect, useState } from "react"
import { Dialog } from "react-aria-components"
import { useIntl } from "react-intl" import { useIntl } from "react-intl"
import { Button } from "@scandic-hotels/design-system/Button"
import ButtonLink from "@scandic-hotels/design-system/ButtonLink" import ButtonLink from "@scandic-hotels/design-system/ButtonLink"
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon" import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
import Modal from "@scandic-hotels/design-system/Modal"
import { toast } from "@scandic-hotels/design-system/Toast"
import { Typography } from "@scandic-hotels/design-system/Typography" import { Typography } from "@scandic-hotels/design-system/Typography"
import { missingPoints } from "@/constants/missingPointsHrefs" import { missingPoints } from "@/constants/missingPointsHrefs"
import { env } from "@/env/client"
import useLang from "@/hooks/useLang" import useLang from "@/hooks/useLang"
import { ClaimPointsWizard } from "./ClaimPointsWizard"
import styles from "./claimPoints.module.css" import styles from "./claimPoints.module.css"
export default function ClaimPoints() { export default function ClaimPoints() {
const intl = useIntl()
const [openModal, setOpenModal] = useLinkableModalState("claim-points")
const useNewFlow = env.NEXT_PUBLIC_NEW_POINTCLAIMS
if (!useNewFlow) {
return <OldClaimPointsLink />
}
return (
<>
<div className={styles.claim}>
<Typography variant="Body/Supporting text (caption)/smRegular">
<p>
{intl.formatMessage({
id: "points.claimPoints.missingPreviousStay",
defaultMessage: "Missing a previous stay?",
})}
</p>
</Typography>
<Button variant="Text" size="sm" onPress={() => setOpenModal(true)}>
{intl.formatMessage({
id: "points.claimPoints.cta",
defaultMessage: "Claim points",
})}
</Button>
</div>
<Modal
title="Add missing points"
isOpen={openModal}
onToggle={(open) => setOpenModal(open)}
>
<Dialog aria-label="TODO" className={styles.dialog}>
{({ close }) => (
<ClaimPointsWizard
onSuccess={() => {
toast.info(
<>
<Typography variant="Body/Paragraph/mdBold">
<p>We&apos;re on it!</p>
</Typography>
<Typography variant="Body/Paragraph/mdRegular">
<p>
If your points have not been added to your account
within 2 weeks, please contact us.
</p>
</Typography>
</>,
{
duration: Infinity,
}
)
close()
}}
onClose={close}
/>
)}
</Dialog>
</Modal>
</>
)
}
function useLinkableModalState(target: string) {
const [openModal, setOpenModal] = useState(false)
useEffect(() => {
const params = new URLSearchParams(window.location.search)
const claimPoints = params.get("target") === target
if (claimPoints) {
params.delete("target")
const newUrl = `${window.location.pathname}?${params.toString()}`
window.history.replaceState({}, "", newUrl)
// eslint-disable-next-line react-hooks/set-state-in-effect
setOpenModal(true)
}
}, [target])
return [openModal, setOpenModal] as const
}
function OldClaimPointsLink() {
const intl = useIntl() const intl = useIntl()
const lang = useLang() const lang = useLang()
@@ -26,14 +26,14 @@ export function PointTransactionRow({
focusRef: React.Ref<HTMLAnchorElement> focusRef: React.Ref<HTMLAnchorElement>
}) { }) {
const intl = useIntl() const intl = useIntl()
const { confirmationNumber, bookingUrl, transactionDate, awardPoints } =
const { confirmationNumber, bookingUrl, checkinDate, awardPoints } =
transaction.attributes transaction.attributes
const balfwd = confirmationNumber === BALFWD const balfwd = confirmationNumber === BALFWD
const nonTransactional = confirmationNumber === NON_TRANSACTIONAL const nonTransactional = confirmationNumber === NON_TRANSACTIONAL
const day = checkinDate.split("-")[2].replace(/^0/, "") const date = dt.utc(transactionDate).locale(lang)
const month = dt(checkinDate.split("-")[1]).locale(lang).format("MMM") const day = date.format("D")
const month = date.format("MMM")
const formattedPoints = intl.formatNumber(Math.abs(awardPoints)) const formattedPoints = intl.formatNumber(Math.abs(awardPoints))
const calculatedPoints = const calculatedPoints =
@@ -41,7 +41,12 @@ export function PointTransactionRow({
? formattedPoints ? formattedPoints
: `${awardPoints > 0 ? "+" : "-"} ${formattedPoints}` : `${awardPoints > 0 ? "+" : "-"} ${formattedPoints}`
const canLinkBookingUrl = !balfwd && !nonTransactional const canLinkBookingUrl =
!balfwd &&
!nonTransactional &&
!!transaction.attributes.bookingUrl &&
(transaction.type === Transactions.rewardType.stay ||
transaction.type === Transactions.rewardType.rewardNight)
const description = getDescription(transaction, intl) const description = getDescription(transaction, intl)
@@ -93,26 +98,29 @@ export function PointTransactionRow({
function getDescription(transaction: Transaction, intl: IntlShape) { function getDescription(transaction: Transaction, intl: IntlShape) {
const hotelInformation = transaction.attributes.hotelInformation const hotelInformation = transaction.attributes.hotelInformation
const balfwd = transaction.attributes.confirmationNumber === BALFWD const isBalfwd = transaction.attributes.confirmationNumber === BALFWD
const nonTransactional = const isNonTransactional =
transaction.attributes.confirmationNumber === NON_TRANSACTIONAL transaction.attributes.confirmationNumber === NON_TRANSACTIONAL
if (isNonTransactional && transaction.attributes.nights === 0) {
return intl.formatMessage({
id: "earnAndBurn.journeyTable.pointsActivity",
defaultMessage: "Point activity",
})
}
switch (transaction.type) { switch (transaction.type) {
case Transactions.rewardType.stay: case Transactions.rewardType.stay:
return nonTransactional && transaction.attributes.nights === 0 if (hotelInformation?.name) {
? intl.formatMessage({ return intl.formatMessage(
id: "myPoints.pointTransactions.pointsActivity", {
defaultMessage: "Point activity", id: "earnAndBurn.journeyTable.stayAt",
}) defaultMessage: "Stay at {hotelName}",
: hotelInformation?.name },
? intl.formatMessage( { hotelName: hotelInformation.name }
{ )
id: "myPoints.pointTransactions.stayAt", } else {
defaultMessage: "Stay at {hotelName}", return ""
}, }
{ hotelName: hotelInformation?.name }
)
: ""
case Transactions.rewardType.stayAdj: case Transactions.rewardType.stayAdj:
if (transaction.attributes.hotelOperaId === "ORS") { if (transaction.attributes.hotelOperaId === "ORS") {
return intl.formatMessage({ return intl.formatMessage({
@@ -120,42 +128,52 @@ function getDescription(transaction: Transaction, intl: IntlShape) {
defaultMessage: "Former Scandic Hotel", defaultMessage: "Former Scandic Hotel",
}) })
} }
if (balfwd) { if (isBalfwd) {
return intl.formatMessage({ return intl.formatMessage({
id: "myPoints.pointTransactions.pointsEarnedPriorMay2021", id: "earnAndBurn.journeyTable.pointsEarnedPriorMay2021",
defaultMessage: "Points earned prior to May 1, 2021", defaultMessage: "Points earned prior to May 1, 2021",
}) })
} }
break case Transactions.rewardType.redgift:
return intl.formatMessage({
id: "earnAndBurn.journeyTable.redGift",
defaultMessage: "Reward Gift",
})
case Transactions.rewardType.rewardNight:
return intl.formatMessage({
id: "earnAndBurn.journeyTable.rewardNight",
defaultMessage: "Reward Night",
})
case Transactions.rewardType.ancillary: case Transactions.rewardType.ancillary:
return intl.formatMessage({ return intl.formatMessage({
id: "myPoints.pointTransactions.extrasToBooking", id: "earnAndBurn.journeyTable.extrasToBooking",
defaultMessage: "Extras to your booking", defaultMessage: "Extras to your booking",
}) })
case Transactions.rewardType.enrollment: case Transactions.rewardType.enrollment:
return intl.formatMessage({ return intl.formatMessage({
id: "myPoints.pointTransactions.signUpBonus", id: "earnAndBurn.journeyTable.signUpBonus",
defaultMessage: "Sign up bonus", defaultMessage: "Sign up bonus",
}) })
case Transactions.rewardType.mastercard_points: case Transactions.rewardType.mastercard_points:
return intl.formatMessage({ return intl.formatMessage({
id: "myPoints.pointTransactions.scandicFriendsMastercard", id: "earnAndBurn.journeyTable.scandicFriendsMastercard",
defaultMessage: "Scandic Friends Mastercard", defaultMessage: "Scandic Friends Mastercard",
}) })
case Transactions.rewardType.tui_points: case Transactions.rewardType.tui_points:
return intl.formatMessage({ return intl.formatMessage({
id: "myPoints.pointTransactions.tuiPoints", id: "earnAndBurn.journeyTable.tuiPoints",
defaultMessage: "TUI Points", defaultMessage: "TUI Points",
}) })
case Transactions.rewardType.pointShop: case Transactions.rewardType.pointShop:
return intl.formatMessage({ return intl.formatMessage({
id: "myPoints.pointTransactions.pointShop", id: "earnAndBurn.journeyTable.pointShop",
defaultMessage: "Scandic Friends Point Shop", defaultMessage: "Scandic Friends Point Shop",
}) })
default:
return undefined
} }
} }
@@ -2,6 +2,7 @@
import { Fragment, useCallback, useRef } from "react" import { Fragment, useCallback, useRef } from "react"
import { useIntl } from "react-intl" import { useIntl } from "react-intl"
import { dt } from "@scandic-hotels/common/dt"
import useStickyPosition from "@scandic-hotels/common/hooks/useStickyPosition" import useStickyPosition from "@scandic-hotels/common/hooks/useStickyPosition"
import { StickyElementNameEnum } from "@scandic-hotels/common/stores/sticky-position" import { StickyElementNameEnum } from "@scandic-hotels/common/stores/sticky-position"
import { Divider } from "@scandic-hotels/design-system/Divider" import { Divider } from "@scandic-hotels/design-system/Divider"
@@ -40,9 +41,9 @@ export function PointTransactionList() {
.flatMap((page) => page.data) .flatMap((page) => page.data)
const groupedTransactions = const groupedTransactions =
transactions?.reduce<Record<number, typeof transactions>>( transactions?.reduce<Record<string, typeof transactions>>(
(acc, transaction) => { (acc, transaction) => {
const year = new Date(transaction.attributes.checkinDate).getFullYear() const year = dt.utc(transaction.attributes.transactionDate).year()
if (!acc[year]) acc[year] = [] if (!acc[year]) acc[year] = []
acc[year].push(transaction) acc[year].push(transaction)
return acc return acc
@@ -1,5 +1,3 @@
import { env } from "@/env/server"
import SignupForm from "@/components/Forms/Signup" import SignupForm from "@/components/Forms/Signup"
import type { SignupFormWrapperProps } from "@/types/components/blocks/dynamicContent" import type { SignupFormWrapperProps } from "@/types/components/blocks/dynamicContent"
@@ -7,10 +5,5 @@ import type { SignupFormWrapperProps } from "@/types/components/blocks/dynamicCo
export default async function SignupFormWrapper({ export default async function SignupFormWrapper({
dynamic_content, dynamic_content,
}: SignupFormWrapperProps) { }: SignupFormWrapperProps) {
return ( return <SignupForm {...dynamic_content} />
<SignupForm
{...dynamic_content}
enableProfileConsent={env.ENABLE_PROFILE_CONSENT}
/>
)
} }
@@ -33,7 +33,6 @@
align-items: center; align-items: center;
} }
/* Styles for new empty upcoming stays design */
.emptyUpcomingStaysContainer { .emptyUpcomingStaysContainer {
display: flex; display: flex;
padding: var(--Space-x6); padding: var(--Space-x6);
@@ -10,6 +10,7 @@ import { useMediaQuery } from "usehooks-ts"
import { useMarkerHover } from "@scandic-hotels/common/hooks/map/useMarkerHover" import { useMarkerHover } from "@scandic-hotels/common/hooks/map/useMarkerHover"
import { InfoWindow } from "@scandic-hotels/design-system/Map/InfoWindow" import { InfoWindow } from "@scandic-hotels/design-system/Map/InfoWindow"
import { Typography } from "@scandic-hotels/design-system/Typography"
import { useDestinationPageCitiesMapStore } from "@/stores/destination-page-cities-map" import { useDestinationPageCitiesMapStore } from "@/stores/destination-page-cities-map"
@@ -79,7 +80,10 @@ export default function CityClusterMarker({
})} })}
anchorPoint={AdvancedMarkerAnchorPoint.CENTER} anchorPoint={AdvancedMarkerAnchorPoint.CENTER}
> >
<span className={styles.count}>{sizeAsText}</span> <Typography variant="Title/Subtitle/md">
<span>{sizeAsText}</span>
</Typography>
{isDesktop && isHovered ? ( {isDesktop && isHovered ? (
<InfoWindow <InfoWindow
position={position} position={position}
@@ -20,9 +20,3 @@
height: 46px !important; height: 46px !important;
} }
} }
.count {
font-family: var(--typography-Body-Regular-fontFamily);
font-size: var(--typography-Subtitle-2-fontSize);
font-weight: var(--typography-Subtitle-2-fontWeight);
}
@@ -19,12 +19,13 @@ div.months {
td.day, td.day,
td.rangeEnd, td.rangeEnd,
td.rangeStart { td.rangeStart {
font-family: var(--typography-Body-Bold-fontFamily); font-family:
font-size: var(--typography-Body-Bold-fontSize); var(--Body-Paragraph-Font-family), var(--Body-Paragraph-Font-fallback);
font-weight: 500; font-size: var(--Body-Paragraph-Size);
letter-spacing: var(--typography-Body-Bold-letterSpacing); font-weight: var(--Body-Paragraph-Font-weight-2);
line-height: var(--typography-Body-Bold-lineHeight); letter-spacing: var(--Body-Paragraph-Letter-spacing);
text-decoration: var(--typography-Body-Bold-textDecoration); line-height: 1.5;
text-decoration: none;
} }
td.rangeEnd, td.rangeEnd,
@@ -90,14 +91,16 @@ td.day[data-outside="true"] ~ td.day[data-disabled="true"] button.dayButton,
} }
.weekDay { .weekDay {
color: var(--UI-Text-Placeholder); color: var(--Text-Tertiary);
font-family: var(--typography-Footnote-Labels-fontFamily); font-family:
font-size: var(--typography-Footnote-Labels-fontSize); var(--Title-Overline-sm-Font-family), var(--Title-Overline-sm-Font-fallback);
font-weight: var(--typography-Footnote-Labels-fontWeight); font-size: var(--Title-Overline-sm-Size);
letter-spacing: var(--typography-Footnote-Labels-letterSpacing); font-style: normal;
line-height: var(--typography-Footnote-Labels-lineHeight); font-weight: var(--Title-Overline-sm-Font-weight);
text-decoration: var(--typography-Footnote-Labels-textDecoration); line-height: 1.5;
text-transform: uppercase; letter-spacing: var(--Title-Overline-sm-Letter-spacing);
text-transform: var(--Title-Overline-sm-Text-Transform);
text-decoration: none;
} }
.footer { .footer {
@@ -89,12 +89,13 @@ div.months {
td.day, td.day,
td.rangeEnd, td.rangeEnd,
td.rangeStart { td.rangeStart {
font-family: var(--typography-Body-Bold-fontFamily); font-family:
font-size: var(--typography-Body-Bold-fontSize); var(--Body-Paragraph-Font-family), var(--Body-Paragraph-Font-fallback);
font-weight: 500; font-size: var(--Body-Paragraph-Size);
letter-spacing: var(--typography-Body-Bold-letterSpacing); font-weight: var(--Body-Paragraph-Font-weight-2);
line-height: var(--typography-Body-Bold-lineHeight); letter-spacing: var(--Body-Paragraph-Letter-spacing);
text-decoration: var(--typography-Body-Bold-textDecoration); line-height: 1.5;
text-decoration: none;
} }
td.rangeEnd, td.rangeEnd,
@@ -156,14 +157,16 @@ td.day[data-outside="true"] ~ td.day[data-disabled="true"] button.dayButton,
} }
.weekDay { .weekDay {
color: var(--UI-Text-Placeholder); color: var(--Text-Tertiary);
font-family: var(--typography-Caption-Labels-fontFamily); font-family:
font-size: var(--typography-Caption-Labels-fontSize); var(--Title-Overline-sm-Font-family), var(--Title-Overline-sm-Font-fallback);
font-weight: var(--typography-Caption-Labels-fontWeight); font-size: var(--Title-Overline-sm-Size);
letter-spacing: var(--typography-Caption-Labels-letterSpacing); font-style: normal;
line-height: var(--typography-Caption-Labels-lineHeight); font-weight: var(--Title-Overline-sm-Font-weight);
text-decoration: var(--typography-Caption-Labels-textDecoration); line-height: 1.5;
text-transform: uppercase; letter-spacing: var(--Title-Overline-sm-Letter-spacing);
text-transform: var(--Title-Overline-sm-Text-Transform);
text-decoration: none;
} }
@media screen and (min-width: 1367px) { @media screen and (min-width: 1367px) {
@@ -107,7 +107,6 @@ export default function Form({ user }: EditFormProps) {
} else { } else {
router.push(profile[lang]) router.push(profile[lang])
} }
router.refresh() // Can be removed on NextJs 15
} }
break break
} }
@@ -48,14 +48,9 @@ import styles from "./form.module.css"
interface SignUpFormProps { interface SignUpFormProps {
title: string title: string
enableProfileConsent?: boolean
} }
export default function SignupForm({ export default function SignupForm({ title }: SignUpFormProps) {
title,
// Handled as a prop rather than a client env var due to limits in Netlify env var size.
enableProfileConsent = false,
}: SignUpFormProps) {
const intl = useIntl() const intl = useIntl()
const router = useRouter() const router = useRouter()
const lang = useLang() const lang = useLang()
@@ -140,7 +135,7 @@ export default function SignupForm({
return ( return (
<div className={styles.formWrapper}> <div className={styles.formWrapper}>
{enableProfileConsent && <ProfilingConsentModalReadOnly />} <ProfilingConsentModalReadOnly />
{title ? ( {title ? (
<Typography variant="Title/md"> <Typography variant="Title/md">
<h2>{title}</h2> <h2>{title}</h2>
@@ -293,41 +288,39 @@ export default function SignupForm({
/> />
</section> </section>
{enableProfileConsent && ( <section className={styles.personalization}>
<section className={styles.personalization}> <header>
<header> <Typography variant="Title/Subtitle/md">
<Typography variant="Title/Subtitle/md"> <h3>
<h3> {intl.formatMessage({
{intl.formatMessage({ id: "signup.UnlockYourPersonalizedExperience",
id: "signup.UnlockYourPersonalizedExperience", defaultMessage: "Unlock your personalized experience!",
defaultMessage: "Unlock your personalized experience!", })}
})} </h3>
</h3> </Typography>
</Typography> </header>
</header> <Checkbox
<Checkbox name="profilingConsent"
name="profilingConsent" registerOptions={{ required: true }}
registerOptions={{ required: true }} >
> {intl.formatMessage({
{intl.formatMessage({ id: "signup.yesConsent",
id: "signup.yesConsent", defaultMessage:
defaultMessage: "I consent to Scandic using my information to give me even more personalized travel inspiration and offers from Scandic and trusted Scandic Friends partners. This means Scandic may use information about my interactions with Scandic Friends partners, and share details of my interactions with Scandic with those partners, to make the experience even more relevant to me.",
"I consent to Scandic using my information to give me even more personalized travel inspiration and offers from Scandic and trusted Scandic Friends partners. This means Scandic may use information about my interactions with Scandic Friends partners, and share details of my interactions with Scandic with those partners, to make the experience even more relevant to me.", })}
})} </Checkbox>
</Checkbox> <TextLinkButton
<TextLinkButton typography="Link/sm"
typography="Link/sm" color="Primary"
color="Primary" className={styles.personalizationButton}
className={styles.personalizationButton} onClick={openPersonalizationModal}
onClick={openPersonalizationModal} >
> {intl.formatMessage({
{intl.formatMessage({ id: "signup.ReadMoreAboutPersonalization",
id: "signup.ReadMoreAboutPersonalization", defaultMessage: "Read more about personalization at Scandic",
defaultMessage: "Read more about personalization at Scandic", })}
})} </TextLinkButton>
</TextLinkButton> </section>
</section>
)}
<section className={styles.terms}> <section className={styles.terms}>
<header> <header>
@@ -1,16 +1,18 @@
.menuButton { @layer component {
display: flex; .menuButton {
align-items: center; display: flex;
justify-content: center; align-items: center;
gap: var(--Space-x05); justify-content: center;
cursor: pointer; gap: var(--Space-x05);
width: 100%; cursor: pointer;
background-color: transparent; width: 100%;
color: var(--Text-Interactive-Default); background-color: transparent;
border-width: 0; color: var(--Text-Interactive-Default);
padding: var(--Space-x05) 0; border-width: 0;
padding: var(--Space-x05) 0;
&.loading { &.loading {
cursor: progress; cursor: progress;
}
} }
} }
@@ -1,4 +1,5 @@
"use client" "use client"
import { zodResolver } from "@hookform/resolvers/zod" import { zodResolver } from "@hookform/resolvers/zod"
import { usePathname, useRouter } from "next/navigation" import { usePathname, useRouter } from "next/navigation"
import { useState } from "react" import { useState } from "react"
@@ -25,7 +26,7 @@ import ModifyContact from "../ModifyContact"
import styles from "./guestDetails.module.css" import styles from "./guestDetails.module.css"
import type { Guest } from "@scandic-hotels/trpc/routers/booking/output" import type { BookingConfirmation } from "@scandic-hotels/trpc/types/bookingConfirmation"
import { import {
type ModifyContactSchema, type ModifyContactSchema,
@@ -34,9 +35,9 @@ import {
import { MODAL_STEPS } from "@/types/components/hotelReservation/myStay/myStay" import { MODAL_STEPS } from "@/types/components/hotelReservation/myStay/myStay"
import type { SafeUser } from "@/types/user" import type { SafeUser } from "@/types/user"
interface GuestDetailsProps { type GuestDetailsProps = {
refId: string refId: string
guest: Guest guest: BookingConfirmation["booking"]["guest"]
isCancelled: boolean isCancelled: boolean
user: SafeUser user: SafeUser
} }
@@ -76,6 +77,7 @@ export default function GuestDetails({
const isFirstStep = currentStep === MODAL_STEPS.INITIAL const isFirstStep = currentStep === MODAL_STEPS.INITIAL
const isMemberBooking = const isMemberBooking =
!!user?.membership?.membershipNumber &&
guest.membershipNumber === user?.membership?.membershipNumber guest.membershipNumber === user?.membership?.membershipNumber
const updateGuest = trpc.booking.update.useMutation({ const updateGuest = trpc.booking.update.useMutation({
@@ -196,7 +198,7 @@ export default function GuestDetails({
{guest.firstName} {guest.lastName} {guest.firstName} {guest.lastName}
</p> </p>
</Typography> </Typography>
{isMemberBooking && user.membership && ( {isMemberBooking && user?.membership && (
<Typography variant="Body/Paragraph/mdRegular"> <Typography variant="Body/Paragraph/mdRegular">
<p className={styles.memberNumber} data-hj-suppress> <p className={styles.memberNumber} data-hj-suppress>
{intl.formatMessage( {intl.formatMessage(
@@ -23,6 +23,7 @@
text-decoration-skip-ink: none; text-decoration-skip-ink: none;
text-decoration-thickness: auto; text-decoration-thickness: auto;
text-underline-offset: auto; text-underline-offset: auto;
text-align: center;
text-underline-position: from-font; text-underline-position: from-font;
} }
@@ -4,6 +4,7 @@ import { useIntl } from "react-intl"
import { sumPackages } from "@scandic-hotels/booking-flow/utils/SelectRate" import { sumPackages } from "@scandic-hotels/booking-flow/utils/SelectRate"
import { CurrencyEnum } from "@scandic-hotels/common/constants/currency" import { CurrencyEnum } from "@scandic-hotels/common/constants/currency"
import { dt } from "@scandic-hotels/common/dt"
import { formatPrice } from "@scandic-hotels/common/utils/numberFormatting" import { formatPrice } from "@scandic-hotels/common/utils/numberFormatting"
import { trpc } from "@scandic-hotels/trpc/client" import { trpc } from "@scandic-hotels/trpc/client"
@@ -66,9 +67,14 @@ export default function Steps({ closeModal }: ChangeDatesStepsProps) {
setDates({ fromDate, toDate }) setDates({ fromDate, toDate })
const numberOfNights = dt(toDate).diff(dt(fromDate), "days")
const pkgsSum = sumPackages(packages) const pkgsSum = sumPackages(packages)
const extraPrice = const breakfastPrice = !!breakfast
pkgsSum.price + ((breakfast && breakfast.localPrice.totalPrice) || 0) ? breakfast.localPrice.price * numberOfNights
: 0
const extraPrice = pkgsSum.price + breakfastPrice
if (isLoggedIn && "member" in data.product && data.product.member) { if (isLoggedIn && "member" in data.product && data.product.member) {
const { currency, pricePerStay } = data.product.member.localPrice const { currency, pricePerStay } = data.product.member.localPrice
setNewPrice(formatPrice(intl, pricePerStay + extraPrice, currency)) setNewPrice(formatPrice(intl, pricePerStay + extraPrice, currency))
@@ -1,11 +1,11 @@
import { useIntl } from "react-intl" import { useIntl } from "react-intl"
import { CancellationRuleEnum } from "@scandic-hotels/common/constants/booking"
import { changeOrCancelDateFormat } from "@scandic-hotels/common/constants/dateFormats" import { changeOrCancelDateFormat } from "@scandic-hotels/common/constants/dateFormats"
import { dt } from "@scandic-hotels/common/dt" import { dt } from "@scandic-hotels/common/dt"
import { useMyStayStore } from "@/stores/my-stay" import { useMyStayStore } from "@/stores/my-stay"
import { hasModifiableRate } from "@/components/HotelReservation/MyStay/utils"
import useLang from "@/hooks/useLang" import useLang from "@/hooks/useLang"
import Row from "./Row" import Row from "./Row"
@@ -14,14 +14,19 @@ export default function ModifyBy() {
const intl = useIntl() const intl = useIntl()
const lang = useLang() const lang = useLang()
const { checkInDate, isModifyable } = useMyStayStore((state) => ({ const { checkInDate, isFlexBooking, isChangeBooking } = useMyStayStore(
checkInDate: state.bookedRoom.checkInDate, (state) => ({
isModifyable: hasModifiableRate( checkInDate: state.bookedRoom.checkInDate,
state.bookedRoom.rateDefinition.cancellationRule isFlexBooking:
), state.bookedRoom.rateDefinition.cancellationRule ===
})) CancellationRuleEnum.CancellableBefore6PM,
isChangeBooking:
state.bookedRoom.rateDefinition.cancellationRule ===
CancellationRuleEnum.Changeable,
})
)
if (!isModifyable) { if (!isFlexBooking && !isChangeBooking) {
return null return null
} }
@@ -38,14 +43,15 @@ export default function ModifyBy() {
} }
) )
return ( const title = isChangeBooking
<Row ? intl.formatMessage({
icon="refresh" id: "booking.changeTitle",
text={text} defaultMessage: "Change",
title={intl.formatMessage({ })
: intl.formatMessage({
id: "booking.changeOrCancel", id: "booking.changeOrCancel",
defaultMessage: "Change or cancel", defaultMessage: "Change or cancel",
})} })
/>
) return <Row icon="refresh" text={text} title={title} />
} }
@@ -4,16 +4,18 @@ import { useMyStayStore } from "@/stores/my-stay"
import Price from "../PriceType/Price" import Price from "../PriceType/Price"
import type { PriceType as _PriceType } from "@/types/components/hotelReservation/myStay/myStay"
export default function TotalPrice() { export default function TotalPrice() {
const { bookedRoom, totalPrice } = useMyStayStore((state) => ({ const { bookedRoom, totalPrice, allRoomsAreCancelled } = useMyStayStore(
bookedRoom: state.bookedRoom, (state) => ({
totalPrice: state.totalPrice, bookedRoom: state.bookedRoom,
})) totalPrice: state.totalPrice,
allRoomsAreCancelled: state.allRoomsAreCancelled,
})
)
return ( return (
<Price <Price
isCancelled={bookedRoom.isCancelled} isCancelled={allRoomsAreCancelled}
isMember={bookedRoom.rateDefinition.isMemberRate} isMember={bookedRoom.rateDefinition.isMemberRate}
price={totalPrice} price={totalPrice}
/> />
@@ -8,7 +8,7 @@ import accessBooking, {
} from "./accessBooking" } from "./accessBooking"
import type { AdditionalInfoCookieValue } from "@scandic-hotels/booking-flow/types/components/findMyBooking/additionalInfoCookieValue" import type { AdditionalInfoCookieValue } from "@scandic-hotels/booking-flow/types/components/findMyBooking/additionalInfoCookieValue"
import type { Guest } from "@scandic-hotels/trpc/routers/booking/output" import type { BookingConfirmation } from "@scandic-hotels/trpc/types/bookingConfirmation"
import type { SafeUser } from "@/types/user" import type { SafeUser } from "@/types/user"
@@ -201,7 +201,7 @@ const badAuthenticatedUser: SafeUser = {
profilingConsentUpdateDate: undefined, profilingConsentUpdateDate: undefined,
} }
const loggedOutGuest: Guest = { const loggedOutGuest: BookingConfirmation["booking"]["guest"] = {
email: "logged+out@scandichotels.com", email: "logged+out@scandichotels.com",
firstName: "Anonymous", firstName: "Anonymous",
lastName: "Booking", lastName: "Booking",
@@ -210,7 +210,7 @@ const loggedOutGuest: Guest = {
countryCode: "SE", countryCode: "SE",
} }
const loggedInGuest: Guest = { const loggedInGuest: BookingConfirmation["booking"]["guest"] = {
email: "logged+in@scandichotels.com", email: "logged+in@scandichotels.com",
firstName: "Authenticated", firstName: "Authenticated",
lastName: "Booking", lastName: "Booking",
@@ -1,5 +1,5 @@
import type { AdditionalInfoCookieValue } from "@scandic-hotels/booking-flow/types/components/findMyBooking/additionalInfoCookieValue" import type { AdditionalInfoCookieValue } from "@scandic-hotels/booking-flow/types/components/findMyBooking/additionalInfoCookieValue"
import type { Guest } from "@scandic-hotels/trpc/routers/booking/output" import type { BookingConfirmation } from "@scandic-hotels/trpc/types/bookingConfirmation"
import type { SafeUser } from "@/types/user" import type { SafeUser } from "@/types/user"
@@ -15,7 +15,7 @@ export {
* Whether a request can access a confirmed booking or not. * Whether a request can access a confirmed booking or not.
*/ */
function accessBooking( function accessBooking(
guest: Guest, guest: BookingConfirmation["booking"]["guest"],
lastName: string, lastName: string,
user: SafeUser | null, user: SafeUser | null,
cookie: string = "" cookie: string = ""
@@ -27,6 +27,7 @@ import AdditionalInfoForm from "@/components/HotelReservation/FindMyBooking/Addi
import accessBooking, { import accessBooking, {
ACCESS_GRANTED, ACCESS_GRANTED,
ERROR_BAD_REQUEST, ERROR_BAD_REQUEST,
ERROR_NOT_FOUND,
ERROR_UNAUTHORIZED, ERROR_UNAUTHORIZED,
} from "@/components/HotelReservation/MyStay/accessBooking" } from "@/components/HotelReservation/MyStay/accessBooking"
import { Ancillaries } from "@/components/HotelReservation/MyStay/Ancillaries" import { Ancillaries } from "@/components/HotelReservation/MyStay/Ancillaries"
@@ -74,39 +75,23 @@ async function MyStay(props: {
notFound() notFound()
} }
const { confirmationNumber, lastName } = parseRefId(refId)
const isLoggedIn = await isLoggedInUser()
const cookieStore = await cookies() const cookieStore = await cookies()
const bv = cookieStore.get("bv")?.value const bv = cookieStore.get("bv")?.value
let bookingConfirmation
if (isLoggedIn) {
bookingConfirmation = await getBookingConfirmation(refId)
} else if (bv) {
logger.debug(`MyStay: bv`, bv)
const {
firstName,
email,
confirmationNumber: bvConfirmationNo,
} = JSON.parse(bv) as AdditionalInfoCookieValue
if (firstName && email && bvConfirmationNo === confirmationNumber) { const { confirmationNumber, lastName } = parseRefId(refId)
bookingConfirmation = await findBooking( const isLoggedIn = await isLoggedInUser()
confirmationNumber, const [{ error, bookingConfirmation }, user] = await Promise.all([
lastName, getOrFindBookingConfirmation({
firstName, refId,
email isLoggedIn,
) confirmationNumber,
} else { lastName,
return ( bv,
<RenderAdditionalInfoForm }),
confirmationNumber={confirmationNumber} getProfileSafely(),
lastName={lastName} ])
/>
) if (error === "MISSING_INFO") {
}
} else {
return ( return (
<RenderAdditionalInfoForm <RenderAdditionalInfoForm
confirmationNumber={confirmationNumber} confirmationNumber={confirmationNumber}
@@ -121,208 +106,226 @@ async function MyStay(props: {
) )
} }
const { additionalData, booking, hotel, roomCategories } = bookingConfirmation const { booking } = bookingConfirmation
const user = await getProfileSafely() const { code } = accessBooking(booking.guest, lastName, user, bv)
const intl = await getIntl() switch (code) {
case ACCESS_GRANTED.code:
return (
<MyStayPage
bookingConfirmation={bookingConfirmation}
user={user}
lang={lang}
isWebview={!!isWebview}
/>
)
case ERROR_NOT_FOUND.code:
return notFound()
case ERROR_BAD_REQUEST.code:
return (
<RenderAdditionalInfoForm
confirmationNumber={confirmationNumber}
lastName={lastName}
/>
)
case ERROR_UNAUTHORIZED.code: {
if (!bv) return notFound()
const access = accessBooking(booking.guest, lastName, user, bv) return (
<RenderFindMyBookingForm
if (access === ACCESS_GRANTED) { bv={bv}
const fromDate = dt(booking.checkInDate).format("YYYY-MM-DD") lastName={lastName}
const toDate = dt(booking.checkOutDate).format("YYYY-MM-DD") confirmationNumber={confirmationNumber}
/>
const linkedReservationsPromise = getLinkedReservations(booking.refId)
const packagesInput = {
adults: booking.adults,
children: booking.childrenAges.length,
endDate: toDate,
hotelId: hotel.operaId,
lang,
startDate: fromDate,
packageCodes: [
BreakfastPackageEnum.ANCILLARY_REGULAR_BREAKFAST,
BreakfastPackageEnum.ANCILLARY_CHILD_PAYING_BREAKFAST,
BreakfastPackageEnum.FREE_CHILD_BREAKFAST,
],
}
const supportedCards = hotel.merchantInformationData.cards
const savedPaymentCardsInput = { supportedCards }
const hasBreakfastPackage = booking.packages.find(
(pkg) => pkg.code === BreakfastPackageEnum.REGULAR_BREAKFAST
)
const breakfastIncluded = booking.rateDefinition.breakfastIncluded
const shouldFetchBreakfastPackages =
!hasBreakfastPackage && !breakfastIncluded
if (shouldFetchBreakfastPackages) {
void getPackages(packagesInput)
}
const isOwnBooking = user?.email === booking.guest.email
if (user && isOwnBooking) {
void getSavedPaymentCardsSafely(savedPaymentCardsInput)
}
let breakfastPackages = null
if (shouldFetchBreakfastPackages) {
breakfastPackages = await getPackages(packagesInput)
}
let savedCreditCards = null
if (user && isOwnBooking) {
savedCreditCards = await getSavedPaymentCardsSafely(
savedPaymentCardsInput
) )
} }
let ancillaryPackagesPromise = null default:
if (booking.showAncillaries) { const _exhaustiveCheck: never = code
ancillaryPackagesPromise = getAncillaryPackages({ throw new Error(`Unknown access code: ${code}`)
}
}
async function MyStayPage({
bookingConfirmation,
user,
lang,
isWebview,
}: {
bookingConfirmation: BookingConfirmation
user: SafeUser | null
lang: Lang
isWebview: boolean
}) {
const intl = await getIntl()
const { additionalData, booking, hotel, roomCategories } = bookingConfirmation
const fromDate = dt(booking.checkInDate).format("YYYY-MM-DD")
const toDate = dt(booking.checkOutDate).format("YYYY-MM-DD")
const packagesInput = {
adults: booking.adults,
children: booking.childrenAges.length,
endDate: toDate,
hotelId: hotel.operaId,
lang,
startDate: fromDate,
packageCodes: [
BreakfastPackageEnum.ANCILLARY_REGULAR_BREAKFAST,
BreakfastPackageEnum.ANCILLARY_CHILD_PAYING_BREAKFAST,
BreakfastPackageEnum.FREE_CHILD_BREAKFAST,
],
}
const supportedCards = hotel.merchantInformationData.cards
const savedPaymentCardsInput = { supportedCards }
const hasBreakfastPackage = booking.packages.find(
(pkg) => pkg.code === BreakfastPackageEnum.REGULAR_BREAKFAST
)
const breakfastIncluded = booking.rateDefinition.breakfastIncluded
const shouldFetchBreakfastPackages =
!hasBreakfastPackage && !breakfastIncluded
const isOwnBooking = user?.email === booking.guest.email
const shouldGetCards = user && isOwnBooking
const [breakfastPackages, savedCreditCards] = await Promise.all([
shouldFetchBreakfastPackages ? getPackages(packagesInput) : noop(),
shouldGetCards
? getSavedPaymentCardsSafely(savedPaymentCardsInput)
: noop(),
])
const imageSrc =
hotel.hotelContent.images.src ||
additionalData.gallery?.heroImages[0]?.src ||
hotel.galleryImages[0]?.src
const baseUrl = env.PUBLIC_URL || "https://www.scandichotels.com"
const promoUrl = new URL(`${baseUrl}/${lang}/`)
const hotelUrl = new URL(`${baseUrl}${bookingConfirmation.url}/`)
promoUrl.searchParams.set("hotel", hotel.operaId)
const maskedBookingConfirmation = maskBookingConfirmation(bookingConfirmation)
const maskedUser = isOwnBooking ? maskUser(user) : null
const hotelWithFilteredAlerts = {
...hotel,
specialAlerts: filterOverlappingDates(
hotel.specialAlerts,
dt.utc(fromDate),
dt.utc(toDate)
),
}
const linkedReservationsPromise = getLinkedReservations(booking.refId)
const ancillaryPackagesPromise = booking.showAncillaries
? getAncillaryPackages({
fromDate, fromDate,
hotelId: hotel.operaId, hotelId: hotel.operaId,
toDate, toDate,
}) })
} : null
const imageSrc = return (
hotel.hotelContent.images.src || <MyStayProvider
additionalData.gallery?.heroImages[0]?.src || bookingConfirmation={maskedBookingConfirmation}
hotel.galleryImages[0]?.src breakfastPackages={breakfastPackages}
lang={lang}
const baseUrl = env.PUBLIC_URL || "https://www.scandichotels.com" linkedReservationsPromise={linkedReservationsPromise}
const promoUrl = new URL(`${baseUrl}/${lang}/`) refId={booking.refId}
const hotelUrl = new URL(`${baseUrl}${bookingConfirmation.url}/`) roomCategories={roomCategories}
savedCreditCards={savedCreditCards}
promoUrl.searchParams.set("hotel", hotel.operaId) isLoggedIn={!!user}
>
const maskedBookingConfirmation = { <main className={styles.main}>
...bookingConfirmation, <div className={styles.imageContainer}>
booking: { <div className={styles.blurOverlay} />
...bookingConfirmation.booking, {imageSrc && (
guest: { <Image
...bookingConfirmation.booking.guest, className={styles.image}
email: maskValue.email(bookingConfirmation.booking.guest.email), src={imageSrc}
phoneNumber: maskValue.phone( alt={hotelWithFilteredAlerts.name}
bookingConfirmation.booking.guest.phoneNumber ?? "" fill
),
},
},
} satisfies BookingConfirmation
const maskedUser =
user && isOwnBooking
? ({
...user,
email: maskValue.email(user.email),
phoneNumber: maskValue.phone(user.phoneNumber ?? ""),
} satisfies SafeUser)
: null
hotel.specialAlerts = filterOverlappingDates(
hotel.specialAlerts,
dt.utc(fromDate),
dt.utc(toDate)
)
return (
<MyStayProvider
bookingConfirmation={maskedBookingConfirmation}
breakfastPackages={breakfastPackages}
lang={lang}
linkedReservationsPromise={linkedReservationsPromise}
refId={booking.refId}
roomCategories={roomCategories}
savedCreditCards={savedCreditCards}
isLoggedIn={isLoggedIn}
>
<main className={styles.main}>
<div className={styles.imageContainer}>
<div className={styles.blurOverlay} />
{imageSrc && (
<Image
className={styles.image}
src={imageSrc}
alt={hotel.name}
fill
/>
)}
</div>
<div className={styles.content}>
<div className={styles.headerContainer}>
<Header cityName={hotel.cityName} name={hotel.name} />
<ReferenceCard />
</div>
{booking.showAncillaries && ancillaryPackagesPromise && (
<Ancillaries
ancillariesPromise={ancillaryPackagesPromise}
packages={breakfastPackages}
user={maskedUser}
savedCreditCards={savedCreditCards}
/>
)}
<SingleRoom user={maskedUser} />
<MultiRoom user={maskedUser} />
<BookingSummary hotelUrl={hotelUrl.toString()} hotel={hotel} />
{!isWebview && (
<Promo
title={intl.formatMessage({
id: "booking.bookNextStay.title",
defaultMessage: "Book your next stay",
})}
text={intl.formatMessage({
id: "booking.bookAnotherStayDescription",
defaultMessage:
"Get inspired and start dreaming beyond your next trip. Explore more Scandic destinations.",
})}
buttonText={intl.formatMessage({
id: "myStay.promo.bookNextStay.buttonText",
defaultMessage: "Explore Scandic hotels",
})}
href={promoUrl.toString()}
image={hotel.hotelContent.images}
/>
)}
</div>
</main>
</MyStayProvider>
)
}
if (access === ERROR_BAD_REQUEST) {
return (
<RenderAdditionalInfoForm
confirmationNumber={confirmationNumber}
lastName={lastName}
/>
)
}
if (access === ERROR_UNAUTHORIZED) {
if (bv) {
const { firstName, email } = JSON.parse(bv) as AdditionalInfoCookieValue
return (
<main className={styles.main}>
<div className={styles.form}>
<FindMyBooking
error={FindMyBookingErrorEnum.BOOKING_ACCESS_DENIED}
defaultValues={{
firstName,
lastName,
confirmationNumber,
email,
}}
/> />
)}
</div>
<div className={styles.content}>
<div className={styles.headerContainer}>
<Header
cityName={hotelWithFilteredAlerts.cityName}
name={hotelWithFilteredAlerts.name}
/>
<ReferenceCard />
</div> </div>
</main> {booking.showAncillaries && ancillaryPackagesPromise && (
) <Ancillaries
} else { ancillariesPromise={ancillaryPackagesPromise}
} packages={breakfastPackages}
} user={maskedUser}
savedCreditCards={savedCreditCards}
/>
)}
return notFound() <SingleRoom user={maskedUser} />
<MultiRoom user={maskedUser} />
<BookingSummary
hotelUrl={hotelUrl.toString()}
hotel={hotelWithFilteredAlerts}
/>
{!isWebview && (
<Promo
title={intl.formatMessage({
id: "booking.bookNextStay.title",
defaultMessage: "Book your next stay",
})}
text={intl.formatMessage({
id: "booking.bookAnotherStayDescription",
defaultMessage:
"Get inspired and start dreaming beyond your next trip. Explore more Scandic destinations.",
})}
buttonText={intl.formatMessage({
id: "myStay.promo.bookNextStay.buttonText",
defaultMessage: "Explore Scandic hotels",
})}
href={promoUrl.toString()}
image={hotelWithFilteredAlerts.hotelContent.images}
/>
)}
</div>
</main>
</MyStayProvider>
)
}
function RenderFindMyBookingForm({
bv,
lastName,
confirmationNumber,
}: {
bv: string
lastName: string
confirmationNumber: string
}) {
const { firstName, email } = JSON.parse(bv) as AdditionalInfoCookieValue
return (
<main className={styles.main}>
<div className={styles.form}>
<FindMyBooking
error={FindMyBookingErrorEnum.BOOKING_ACCESS_DENIED}
defaultValues={{
firstName,
lastName,
confirmationNumber,
email,
}}
/>
</div>
</main>
)
} }
function RenderAdditionalInfoForm({ function RenderAdditionalInfoForm({
@@ -343,3 +346,75 @@ function RenderAdditionalInfoForm({
</main> </main>
) )
} }
function maskUser(user: SafeUser | null): SafeUser | null {
if (!user) return null
return {
...user,
email: maskValue.email(user.email),
phoneNumber: maskValue.phone(user.phoneNumber ?? ""),
}
}
function maskBookingConfirmation(
bookingConfirmation: BookingConfirmation
): BookingConfirmation {
return {
...bookingConfirmation,
booking: {
...bookingConfirmation.booking,
guest: {
...bookingConfirmation.booking.guest,
email: maskValue.email(bookingConfirmation.booking.guest.email),
phoneNumber: maskValue.phone(
bookingConfirmation.booking.guest.phoneNumber ?? ""
),
},
},
}
}
async function getOrFindBookingConfirmation({
refId,
confirmationNumber,
lastName,
isLoggedIn,
bv,
}: {
refId: string
confirmationNumber: string
lastName: string
isLoggedIn: boolean
bv?: string
}) {
if (isLoggedIn)
return { bookingConfirmation: await getBookingConfirmation(refId) } as const
if (!bv) return { error: "MISSING_INFO", bookingConfirmation: null } as const
logger.debug(`MyStay: bv`, bv)
const {
firstName,
email,
confirmationNumber: bvConfirmationNo,
} = JSON.parse(bv) as AdditionalInfoCookieValue
if (!firstName || !email || bvConfirmationNo !== confirmationNumber) {
return { error: "MISSING_INFO", bookingConfirmation: null } as const
}
return {
bookingConfirmation: await findBooking(
confirmationNumber,
lastName,
firstName,
email
),
} as const
}
// Helper function to handle conditional Promise.all calls
async function noop() {
return Promise.resolve(null)
}
@@ -4,13 +4,6 @@
padding: var(--Space-x3) var(--Space-x2); padding: var(--Space-x3) var(--Space-x2);
} }
.subtitle {
font-family: var(--typography-Subtitle-2-fontFamily);
font-size: var(--typography-Subtitle-2-Mobile-fontSize);
font-weight: var(--typography-Subtitle-2-fontWeight);
color: var(--Base-Text-High-contrast);
}
.list { .list {
list-style: none; list-style: none;
} }
@@ -16,8 +16,8 @@
cursor: pointer; cursor: pointer;
height: 32px; height: 32px;
width: 32px; width: 32px;
font-size: var(--typography-Body-Bold-fontSize); font-size: var(--Body-Paragraph-Size);
font-weight: var(--typography-Body-Bold-fontWeight); font-weight: var(--Body-Paragraph-Font-weight-2);
padding: 0; padding: 0;
display: flex; display: flex;
align-items: center; align-items: center;
@@ -1,5 +1,3 @@
import { env } from "@/env/server"
import { getIntl } from "@/i18n" import { getIntl } from "@/i18n"
import { Section } from "../Section" import { Section } from "../Section"
@@ -17,7 +15,7 @@ export async function CommunicationSettings() {
})} })}
> >
<EmailSlot /> <EmailSlot />
{env.ENABLE_PROFILE_CONSENT && <PersonalizationSlot />} <PersonalizationSlot />
</Section> </Section>
) )
} }
@@ -1,6 +1,5 @@
import { Typography } from "@scandic-hotels/design-system/Typography" import { Typography } from "@scandic-hotels/design-system/Typography"
import { env } from "@/env/server"
import { getProfile, getProfilingConsent } from "@/lib/trpc/memoizedRequests" import { getProfile, getProfilingConsent } from "@/lib/trpc/memoizedRequests"
import { GetMainIconByCSIdentifier, userHasConsent } from "../utils" import { GetMainIconByCSIdentifier, userHasConsent } from "../utils"
@@ -9,8 +8,6 @@ import { BannerButton } from "./Button"
import styles from "./profilingConsentBanner.module.css" import styles from "./profilingConsentBanner.module.css"
export async function ProfilingConsentBanner() { export async function ProfilingConsentBanner() {
if (!env.ENABLE_PROFILE_CONSENT) return null
const user = await getProfile() const user = await getProfile()
if (!user || userHasConsent(user?.profilingConsent)) return null if (!user || userHasConsent(user?.profilingConsent)) return null
@@ -1,6 +1,6 @@
# Profiling Consent # Profiling Consent
Profiling consent allows users to opt in/out of personalized experiences. The feature is controlled by the `ENABLE_PROFILE_CONSENT` environment variable. Profiling consent allows users to opt in/out of personalized experiences.
## User Journey ## User Journey
@@ -121,11 +121,9 @@ Replace `<memberKey>` with the actual `membershipNumber` or `profileId`.
Required content for the feature: Required content for the feature:
1. **Profiling Consent (config)** 1. **Profiling Consent (config)**
- Config needs to be created and published in each language - Config needs to be created and published in each language
2. **/consent (account page)** 2. **/consent (account page)**
- Page needs to be created and published in each language - Page needs to be created and published in each language
3. **/overview (account page)** 3. **/overview (account page)**
@@ -2,6 +2,7 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: var(--Space-x05); gap: var(--Space-x05);
align-items: flex-start;
} }
.link { .link {
@@ -1,15 +1,13 @@
import Footnote from "@scandic-hotels/design-system/Footnote"
import { import {
MaterialIcon, MaterialIcon,
type MaterialIconSetIconProps, type MaterialIconSetIconProps,
} from "@scandic-hotels/design-system/Icons/MaterialIcon" } from "@scandic-hotels/design-system/Icons/MaterialIcon"
import Link from "@scandic-hotels/design-system/OldDSLink" import { TextLink } from "@scandic-hotels/design-system/TextLink"
import { Typography } from "@scandic-hotels/design-system/Typography" import { Typography } from "@scandic-hotels/design-system/Typography"
import { getValueFromContactConfig } from "@scandic-hotels/trpc/utils/contactConfig" import { getValueFromContactConfig } from "@scandic-hotels/trpc/utils/contactConfig"
import { serverClient } from "@/lib/trpc/server" import { serverClient } from "@/lib/trpc/server"
// import { getValueFromContactConfig } from "@/utils/contactConfig"
import styles from "./contactRow.module.css" import styles from "./contactRow.module.css"
import type { ContactRowProps } from "@/types/components/sidebar/joinLoyaltyContact" import type { ContactRowProps } from "@/types/components/sidebar/joinLoyaltyContact"
@@ -46,22 +44,27 @@ export default async function ContactRow({ contact }: ContactRowProps) {
return ( return (
<div className={styles.wrapper}> <div className={styles.wrapper}>
<Typography {contact.display_text ? (
variant="Body/Paragraph/mdBold" <Typography
className={styles.displayText} variant="Body/Paragraph/mdBold"
> className={styles.displayText}
<p>{contact.display_text}</p> >
</Typography> <p>{contact.display_text}</p>
<Link </Typography>
) : null}
<TextLink
typography="Link/sm"
className={styles.link} className={styles.link}
href={openableLink} href={openableLink}
textDecoration="underline"
size="small"
> >
{Icon ? <Icon size={20} color="Icon/Interactive/Default" /> : null} {Icon ? <Icon size={20} color="Icon/Interactive/Default" /> : null}
{val} {val}
</Link> </TextLink>
{footnote && <Footnote color="burgundy">{footnote}</Footnote>} {footnote && (
<Typography variant="Body/Supporting text (caption)/smRegular">
<p>{footnote}</p>
</Typography>
)}
</div> </div>
) )
} }
@@ -13,18 +13,9 @@
gap: var(--Space-x15); gap: var(--Space-x15);
} }
.contact > div {
display: flex;
justify-content: center;
}
@media screen and (min-width: 1367px) { @media screen and (min-width: 1367px) {
.contactContainer { .contactContainer {
align-items: start; align-items: start;
padding-top: var(--Space-x2); padding-top: var(--Space-x2);
} }
.contact > div {
justify-content: start;
}
} }
@@ -1,4 +1,4 @@
import { scandicFriends } from "@scandic-hotels/common/constants/routes/myPages" import { scandicFriends } from "@scandic-hotels/common/constants/routes/customerService"
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon" import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
import Link from "@scandic-hotels/design-system/OldDSLink" import Link from "@scandic-hotels/design-system/OldDSLink"
import { Typography } from "@scandic-hotels/design-system/Typography" import { Typography } from "@scandic-hotels/design-system/Typography"
-9
View File
@@ -2,15 +2,6 @@ import { Lang } from "@scandic-hotels/common/constants/language"
import type { LangRoute } from "@scandic-hotels/common/constants/routes/langRoute" import type { LangRoute } from "@scandic-hotels/common/constants/routes/langRoute"
export const sasPartnershipTermsAndConditions: LangRoute = {
da: `/${Lang.da}/kundeservice/politikker/scandic-friends-betingelser/sas-eurobonus`,
de: `/${Lang.de}/kundenbetreuung/richtlinien/scandic-friends-allgemeine-geschaeftsbedingungen/sas-eurobonus`,
en: `/${Lang.en}/customer-service/policies/scandic-friends/sas-eurobonus`,
fi: `/${Lang.fi}/asiakaspalvelu/ehdot/scandic-friends/sas-eurobonus`,
no: `/${Lang.no}/kundeservice/betingelser/scandic-friends-betingelser/sas-eurobonus`,
sv: `/${Lang.sv}/kundservice/villkor/scandic-friends/sas-eurobonus`,
}
export const faq: LangRoute = { export const faq: LangRoute = {
da: `/${Lang.da}/scandic-friends/hjalp-og-service/faq`, da: `/${Lang.da}/scandic-friends/hjalp-og-service/faq`,
de: `/${Lang.de}/scandic-friends/hilfe-und-service/faq`, de: `/${Lang.de}/scandic-friends/hilfe-und-service/faq`,
+6
View File
@@ -16,6 +16,11 @@ export const env = createEnv({
.transform((s) => .transform((s) =>
getSemver("scandic-web", s, process.env.BRANCH || "development") getSemver("scandic-web", s, process.env.BRANCH || "development")
), ),
NEXT_PUBLIC_NEW_POINTCLAIMS: z
.string()
.optional()
.default("false")
.transform((s) => s === "true"),
}, },
emptyStringAsUndefined: true, emptyStringAsUndefined: true,
runtimeEnv: { runtimeEnv: {
@@ -26,5 +31,6 @@ export const env = createEnv({
process.env.NEXT_PUBLIC_SENTRY_CLIENT_SAMPLERATE, process.env.NEXT_PUBLIC_SENTRY_CLIENT_SAMPLERATE,
NEXT_PUBLIC_PUBLIC_URL: process.env.NEXT_PUBLIC_PUBLIC_URL, NEXT_PUBLIC_PUBLIC_URL: process.env.NEXT_PUBLIC_PUBLIC_URL,
NEXT_PUBLIC_RELEASE_TAG: process.env.NEXT_PUBLIC_RELEASE_TAG, NEXT_PUBLIC_RELEASE_TAG: process.env.NEXT_PUBLIC_RELEASE_TAG,
NEXT_PUBLIC_NEW_POINTCLAIMS: process.env.NEXT_PUBLIC_NEW_POINTCLAIMS,
}, },
}) })
-6
View File
@@ -96,11 +96,6 @@ export const env = createEnv({
.refine((s) => s === "1" || s === "0") .refine((s) => s === "1" || s === "0")
.transform((s) => s === "1") .transform((s) => s === "1")
.default("0"), .default("0"),
ENABLE_PROFILE_CONSENT: z
.string()
.refine((s) => s === "true" || s === "false")
.transform((s) => s === "true")
.default("false"),
RELEASE_TAG: z RELEASE_TAG: z
.string() .string()
.optional() .optional()
@@ -160,7 +155,6 @@ export const env = createEnv({
DTMC_ENTRA_ID_SECRET: process.env.DTMC_ENTRA_ID_SECRET, DTMC_ENTRA_ID_SECRET: process.env.DTMC_ENTRA_ID_SECRET,
CHATBOT_LIVE_LANGS: process.env.CHATBOT_LIVE_LANGS, CHATBOT_LIVE_LANGS: process.env.CHATBOT_LIVE_LANGS,
SEO_INERT: process.env.SEO_INERT, SEO_INERT: process.env.SEO_INERT,
ENABLE_PROFILE_CONSENT: process.env.ENABLE_PROFILE_CONSENT,
RELEASE_TAG: process.env.NEXT_PUBLIC_RELEASE_TAG, RELEASE_TAG: process.env.NEXT_PUBLIC_RELEASE_TAG,
}, },
}) })
+135 -1
View File
@@ -231,6 +231,12 @@
"value": "Særlige ønsker (valgfrit)" "value": "Særlige ønsker (valgfrit)"
} }
], ],
"ancillaries.label.quantity": [
{
"type": 0,
"value": "Antal"
}
],
"ancillaries.unableToDisplayBreakfastPrices": [ "ancillaries.unableToDisplayBreakfastPrices": [
{ {
"type": 0, "type": 0,
@@ -249,6 +255,18 @@
"value": "Jeg accepterer booking- og annulleringsbetingelserne" "value": "Jeg accepterer booking- og annulleringsbetingelserne"
} }
], ],
"booking.alert.extraBeds.text": [
{
"type": 0,
"value": "Ved booking for mere end 2 gæster vil der blive opkrævet et ekstra gebyr pr. person. Se link for detaljer."
}
],
"booking.alert.extraguests": [
{
"type": 0,
"value": "Ekstra gæst(er)"
}
],
"booking.approx": [ "booking.approx": [
{ {
"type": 0, "type": 0,
@@ -953,6 +971,16 @@
"value": "Tilføj kode" "value": "Tilføj kode"
} }
], ],
"bookingWidget.bookingCode.readMore": [
{
"type": 0,
"value": "Læs mere om brug af "
},
{
"type": 1,
"value": "codeVoucher"
}
],
"bookingWidget.bookingCode.remember": [ "bookingWidget.bookingCode.remember": [
{ {
"type": 0, "type": 0,
@@ -1061,6 +1089,16 @@
"value": "details" "value": "details"
} }
], ],
"bookingWidget.reward.readMore": [
{
"type": 0,
"value": "Læs mere om booking med "
},
{
"type": 1,
"value": "reward"
}
],
"bookingWidget.reward.rewardNight": [ "bookingWidget.reward.rewardNight": [
{ {
"type": 0, "type": 0,
@@ -2510,6 +2548,44 @@
"value": "Datoer" "value": "Datoer"
} }
], ],
"designSystem.stepper.ariaLabel.currentCount": [
{
"type": 0,
"value": "Nuværende "
},
{
"type": 1,
"value": "label"
},
{
"type": 0,
"value": " "
},
{
"type": 1,
"value": "count"
}
],
"designSystem.stepper.ariaLabel.decrease": [
{
"type": 0,
"value": "Reducer "
},
{
"type": 1,
"value": "label"
}
],
"designSystem.stepper.ariaLabel.increase": [
{
"type": 0,
"value": "Øg "
},
{
"type": 1,
"value": "label"
}
],
"destination.backToCities": [ "destination.backToCities": [
{ {
"type": 0, "type": 0,
@@ -5783,7 +5859,7 @@
"myPages.myStay.ancillaries.reachedMaxItemsStepperMessage": [ "myPages.myStay.ancillaries.reachedMaxItemsStepperMessage": [
{ {
"type": 0, "type": 0,
"value": "Maksimal antal nået for denne vare." "value": "Maksimalt antal nået for denne vare."
} }
], ],
"myPages.myStay.ancillaries.reachedMaxPointsMessage": [ "myPages.myStay.ancillaries.reachedMaxPointsMessage": [
@@ -7569,6 +7645,58 @@
"value": "Samlede ophold" "value": "Samlede ophold"
} }
], ],
"price.numberOfEuroBonusPoints": [
{
"offset": 0,
"options": {
"one": {
"value": [
{
"type": 0,
"value": "EB-point"
}
]
},
"other": {
"value": [
{
"type": 0,
"value": "EB-point"
}
]
}
},
"pluralType": "cardinal",
"type": 6,
"value": "numberOfEuroBonusPoints"
}
],
"price.numberOfScandicPoints": [
{
"offset": 0,
"options": {
"one": {
"value": [
{
"type": 0,
"value": "Point"
}
]
},
"other": {
"value": [
{
"type": 0,
"value": "Point"
}
]
}
},
"pluralType": "cardinal",
"type": 6,
"value": "numberOfScandicPoints"
}
],
"price.numberOfVouchers": [ "price.numberOfVouchers": [
{ {
"offset": 0, "offset": 0,
@@ -8629,6 +8757,12 @@
"value": "). Se tilgængelige priser nedenfor." "value": "). Se tilgængelige priser nedenfor."
} }
], ],
"selectRate.alert.reservationPolicies": [
{
"type": 0,
"value": "Se prisdetaljer"
}
],
"selectRate.availableRooms.all": [ "selectRate.availableRooms.all": [
{ {
"offset": 0, "offset": 0,
+138
View File
@@ -231,6 +231,12 @@
"value": "Sonderwünsche (optional)" "value": "Sonderwünsche (optional)"
} }
], ],
"ancillaries.label.quantity": [
{
"type": 0,
"value": "Menge"
}
],
"ancillaries.unableToDisplayBreakfastPrices": [ "ancillaries.unableToDisplayBreakfastPrices": [
{ {
"type": 0, "type": 0,
@@ -249,6 +255,18 @@
"value": "Ich akzeptiere die Buchungs- und Stornierungsbedingungen" "value": "Ich akzeptiere die Buchungs- und Stornierungsbedingungen"
} }
], ],
"booking.alert.extraBeds.text": [
{
"type": 0,
"value": "Bei Buchungen für mehr als 2 Gäste fällt eine zusätzliche Gebühr pro Person an. Siehe Link für Details."
}
],
"booking.alert.extraguests": [
{
"type": 0,
"value": "Zusätzliche Gäste"
}
],
"booking.approx": [ "booking.approx": [
{ {
"type": 0, "type": 0,
@@ -949,6 +967,16 @@
"value": "Code hinzufügen" "value": "Code hinzufügen"
} }
], ],
"bookingWidget.bookingCode.readMore": [
{
"type": 0,
"value": "Mehr erfahren über die Verwendung von "
},
{
"type": 1,
"value": "codeVoucher"
}
],
"bookingWidget.bookingCode.remember": [ "bookingWidget.bookingCode.remember": [
{ {
"type": 0, "type": 0,
@@ -1057,6 +1085,20 @@
"value": "details" "value": "details"
} }
], ],
"bookingWidget.reward.readMore": [
{
"type": 0,
"value": "Mehr über Buchungen mit "
},
{
"type": 1,
"value": "reward"
},
{
"type": 0,
"value": "erfahren"
}
],
"bookingWidget.reward.rewardNight": [ "bookingWidget.reward.rewardNight": [
{ {
"type": 0, "type": 0,
@@ -2522,6 +2564,44 @@
"value": "Daten" "value": "Daten"
} }
], ],
"designSystem.stepper.ariaLabel.currentCount": [
{
"type": 0,
"value": "Aktuell "
},
{
"type": 1,
"value": "label"
},
{
"type": 0,
"value": " "
},
{
"type": 1,
"value": "count"
}
],
"designSystem.stepper.ariaLabel.decrease": [
{
"type": 1,
"value": "label"
},
{
"type": 0,
"value": "verringern"
}
],
"designSystem.stepper.ariaLabel.increase": [
{
"type": 0,
"value": "Erhöhen "
},
{
"type": 1,
"value": "label"
}
],
"destination.backToCities": [ "destination.backToCities": [
{ {
"type": 0, "type": 0,
@@ -7570,6 +7650,58 @@
"value": "Aufenthalte insgesamt" "value": "Aufenthalte insgesamt"
} }
], ],
"price.numberOfEuroBonusPoints": [
{
"offset": 0,
"options": {
"one": {
"value": [
{
"type": 0,
"value": "EB-Punkt"
}
]
},
"other": {
"value": [
{
"type": 0,
"value": "EB-Punkte"
}
]
}
},
"pluralType": "cardinal",
"type": 6,
"value": "numberOfEuroBonusPoints"
}
],
"price.numberOfScandicPoints": [
{
"offset": 0,
"options": {
"one": {
"value": [
{
"type": 0,
"value": "Punkt"
}
]
},
"other": {
"value": [
{
"type": 0,
"value": "Punkte"
}
]
}
},
"pluralType": "cardinal",
"type": 6,
"value": "numberOfScandicPoints"
}
],
"price.numberOfVouchers": [ "price.numberOfVouchers": [
{ {
"offset": 0, "offset": 0,
@@ -8634,6 +8766,12 @@
"value": "). Verfügbare Übernachtungspreise sind unten zu sehen." "value": "). Verfügbare Übernachtungspreise sind unten zu sehen."
} }
], ],
"selectRate.alert.reservationPolicies": [
{
"type": 0,
"value": "Tarifdetails anzeigen"
}
],
"selectRate.availableRooms.all": [ "selectRate.availableRooms.all": [
{ {
"offset": 0, "offset": 0,
+290
View File
@@ -231,6 +231,12 @@
"value": "Special requests (optional)" "value": "Special requests (optional)"
} }
], ],
"ancillaries.label.quantity": [
{
"type": 0,
"value": "Quantity"
}
],
"ancillaries.unableToDisplayBreakfastPrices": [ "ancillaries.unableToDisplayBreakfastPrices": [
{ {
"type": 0, "type": 0,
@@ -249,6 +255,18 @@
"value": "I accept the booking and cancellation terms" "value": "I accept the booking and cancellation terms"
} }
], ],
"booking.alert.extraBeds.text": [
{
"type": 0,
"value": "When booking for more than 2 guests, an additional fee will apply per person. See link for details."
}
],
"booking.alert.extraguests": [
{
"type": 0,
"value": "Extra guest(s)"
}
],
"booking.approx": [ "booking.approx": [
{ {
"type": 0, "type": 0,
@@ -337,6 +355,12 @@
"value": "Change or cancel" "value": "Change or cancel"
} }
], ],
"booking.changeTitle": [
{
"type": 0,
"value": "Change"
}
],
"booking.codeVoucher": [ "booking.codeVoucher": [
{ {
"type": 0, "type": 0,
@@ -953,6 +977,16 @@
"value": "Add code" "value": "Add code"
} }
], ],
"bookingWidget.bookingCode.readMore": [
{
"type": 0,
"value": "Read more about using "
},
{
"type": 1,
"value": "codeVoucher"
}
],
"bookingWidget.bookingCode.remember": [ "bookingWidget.bookingCode.remember": [
{ {
"type": 0, "type": 0,
@@ -1061,6 +1095,16 @@
"value": "details" "value": "details"
} }
], ],
"bookingWidget.reward.readMore": [
{
"type": 0,
"value": "Read more about booking with "
},
{
"type": 1,
"value": "reward"
}
],
"bookingWidget.reward.rewardNight": [ "bookingWidget.reward.rewardNight": [
{ {
"type": 0, "type": 0,
@@ -2198,6 +2242,16 @@
"value": " points" "value": " points"
} }
], ],
"common.pointsInLine": [
{
"type": 1,
"value": "points"
},
{
"type": 0,
"value": " points"
}
],
"common.pointsToSpend": [ "common.pointsToSpend": [
{ {
"type": 0, "type": 0,
@@ -2510,6 +2564,44 @@
"value": "Dates" "value": "Dates"
} }
], ],
"designSystem.stepper.ariaLabel.currentCount": [
{
"type": 0,
"value": "Current "
},
{
"type": 1,
"value": "label"
},
{
"type": 0,
"value": " "
},
{
"type": 1,
"value": "count"
}
],
"designSystem.stepper.ariaLabel.decrease": [
{
"type": 0,
"value": "Decrease "
},
{
"type": 1,
"value": "label"
}
],
"designSystem.stepper.ariaLabel.increase": [
{
"type": 0,
"value": "Increase "
},
{
"type": 1,
"value": "label"
}
],
"destination.backToCities": [ "destination.backToCities": [
{ {
"type": 0, "type": 0,
@@ -3060,6 +3152,12 @@
"value": "When you confirm the booking the room will be guaranteed for late arrival. If you fail to arrive without cancelling in advance or if you cancel after 18:00 local time, you will be charged for one reward night." "value": "When you confirm the booking the room will be guaranteed for late arrival. If you fail to arrive without cancelling in advance or if you cancel after 18:00 local time, you will be charged for one reward night."
} }
], ],
"enterDetails.confirmBooking.rewardNightGuaranteeInfo": [
{
"type": 0,
"value": "When you complete the booking the room will be guaranteed for late arrival. The hotel will hold your booking, even if you arrive after 18:00. In case of a no-show, you will be charged for one reward night."
}
],
"enterDetails.details.description": [ "enterDetails.details.description": [
{ {
"type": 0, "type": 0,
@@ -3344,6 +3442,40 @@
"value": "Select payment method" "value": "Select payment method"
} }
], ],
"enterDetails.paymentStep.flexBookingTermsAndConditions": [
{
"type": 0,
"value": "To complete your booking, please accept the general "
},
{
"children": [
{
"type": 0,
"value": "Booking & Cancellation Terms"
}
],
"type": 8,
"value": "termsAndConditionsLink"
},
{
"type": 0,
"value": ", and acknowledge that your data will be processed in accordance with Scandic's "
},
{
"children": [
{
"type": 0,
"value": "Privacy policy"
}
],
"type": 8,
"value": "privacyPolicyLink"
},
{
"type": 0,
"value": "."
}
],
"enterDetails.priceChangeDialog.acceptButton": [ "enterDetails.priceChangeDialog.acceptButton": [
{ {
"type": 0, "type": 0,
@@ -5669,6 +5801,24 @@
"value": "!" "value": "!"
} }
], ],
"myPages.l6progress.modal.title": [
{
"type": 0,
"value": "Level upgrade and membership year"
}
],
"myPages.l6progress.modal.youCanAlsoReach": [
{
"type": 0,
"value": "You can also reach Best Friend, our highest membership level, by staying 100 nights with us within a membership year."
}
],
"myPages.l6progress.modal.yourLevelDuring": [
{
"type": 0,
"value": "Your level during the current and next period is based on the points you earn during this 12-month period."
}
],
"myPages.leftToLevelUp": [ "myPages.leftToLevelUp": [
{ {
"type": 0, "type": 0,
@@ -6057,6 +6207,88 @@
"value": "Your membership" "value": "Your membership"
} }
], ],
"myPoints.pointTransactions.extrasToBooking": [
{
"type": 0,
"value": "Extras to your booking"
}
],
"myPoints.pointTransactions.formerScandicHotel": [
{
"type": 0,
"value": "Former Scandic Hotel"
}
],
"myPoints.pointTransactions.noTransactions": [
{
"type": 0,
"value": "No transactions available"
}
],
"myPoints.pointTransactions.pointShop": [
{
"type": 0,
"value": "Scandic Friends Point Shop"
}
],
"myPoints.pointTransactions.pointsActivity": [
{
"type": 0,
"value": "Point activity"
}
],
"myPoints.pointTransactions.pointsEarnedPriorMay2021": [
{
"type": 0,
"value": "Points earned prior to May 1, 2021"
}
],
"myPoints.pointTransactions.redGift": [
{
"type": 0,
"value": "Reward Gift"
}
],
"myPoints.pointTransactions.rewardNight": [
{
"type": 0,
"value": "Reward Night"
}
],
"myPoints.pointTransactions.scandicFriendsMastercard": [
{
"type": 0,
"value": "Scandic Friends Mastercard"
}
],
"myPoints.pointTransactions.showMoreTransactions": [
{
"type": 0,
"value": "Show more transactions"
}
],
"myPoints.pointTransactions.signUpBonus": [
{
"type": 0,
"value": "Sign up bonus"
}
],
"myPoints.pointTransactions.stayAt": [
{
"type": 0,
"value": "Stay at "
},
{
"type": 1,
"value": "hotelName"
}
],
"myPoints.pointTransactions.tuiPoints": [
{
"type": 0,
"value": "TUI Points"
}
],
"myStay.accessDenied.bookingNotFound": [ "myStay.accessDenied.bookingNotFound": [
{ {
"type": 0, "type": 0,
@@ -7546,6 +7778,58 @@
"value": "Total stays" "value": "Total stays"
} }
], ],
"price.numberOfEuroBonusPoints": [
{
"offset": 0,
"options": {
"one": {
"value": [
{
"type": 0,
"value": "EB Point"
}
]
},
"other": {
"value": [
{
"type": 0,
"value": "EB Points"
}
]
}
},
"pluralType": "cardinal",
"type": 6,
"value": "numberOfEuroBonusPoints"
}
],
"price.numberOfScandicPoints": [
{
"offset": 0,
"options": {
"one": {
"value": [
{
"type": 0,
"value": "Point"
}
]
},
"other": {
"value": [
{
"type": 0,
"value": "Points"
}
]
}
},
"pluralType": "cardinal",
"type": 6,
"value": "numberOfScandicPoints"
}
],
"price.numberOfVouchers": [ "price.numberOfVouchers": [
{ {
"offset": 0, "offset": 0,
@@ -8610,6 +8894,12 @@
"value": "). See available rates below." "value": "). See available rates below."
} }
], ],
"selectRate.alert.reservationPolicies": [
{
"type": 0,
"value": "See rate details"
}
],
"selectRate.availableRooms.all": [ "selectRate.availableRooms.all": [
{ {
"offset": 0, "offset": 0,
+142
View File
@@ -231,6 +231,12 @@
"value": "Erityistoiveet (valinnainen)" "value": "Erityistoiveet (valinnainen)"
} }
], ],
"ancillaries.label.quantity": [
{
"type": 0,
"value": "Määrä"
}
],
"ancillaries.unableToDisplayBreakfastPrices": [ "ancillaries.unableToDisplayBreakfastPrices": [
{ {
"type": 0, "type": 0,
@@ -249,6 +255,18 @@
"value": "Hyväksyn varaus- ja peruutusehdot" "value": "Hyväksyn varaus- ja peruutusehdot"
} }
], ],
"booking.alert.extraBeds.text": [
{
"type": 0,
"value": "Kun varaat yli 2 vieraalle, lisämaksu veloitetaan per henkilö. Katso lisätietoja linkistä."
}
],
"booking.alert.extraguests": [
{
"type": 0,
"value": "Ylimääräinen vieras/vieraat"
}
],
"booking.approx": [ "booking.approx": [
{ {
"type": 0, "type": 0,
@@ -953,6 +971,20 @@
"value": "Lisää koodi" "value": "Lisää koodi"
} }
], ],
"bookingWidget.bookingCode.readMore": [
{
"type": 0,
"value": "Lue lisää "
},
{
"type": 1,
"value": "codeVoucher"
},
{
"type": 0,
"value": ":n käytöstä"
}
],
"bookingWidget.bookingCode.remember": [ "bookingWidget.bookingCode.remember": [
{ {
"type": 0, "type": 0,
@@ -1061,6 +1093,20 @@
"value": "details" "value": "details"
} }
], ],
"bookingWidget.reward.readMore": [
{
"type": 0,
"value": "Lue lisää "
},
{
"type": 1,
"value": "reward"
},
{
"type": 0,
"value": " varaamisesta"
}
],
"bookingWidget.reward.rewardNight": [ "bookingWidget.reward.rewardNight": [
{ {
"type": 0, "type": 0,
@@ -2514,6 +2560,44 @@
"value": "Päivämäärät" "value": "Päivämäärät"
} }
], ],
"designSystem.stepper.ariaLabel.currentCount": [
{
"type": 0,
"value": "Nykyinen "
},
{
"type": 1,
"value": "label"
},
{
"type": 0,
"value": " "
},
{
"type": 1,
"value": "count"
}
],
"designSystem.stepper.ariaLabel.decrease": [
{
"type": 0,
"value": "Vähennä "
},
{
"type": 1,
"value": "label"
}
],
"designSystem.stepper.ariaLabel.increase": [
{
"type": 0,
"value": "Lisää "
},
{
"type": 1,
"value": "label"
}
],
"destination.backToCities": [ "destination.backToCities": [
{ {
"type": 0, "type": 0,
@@ -7586,6 +7670,58 @@
"value": "Majoitukset yhteensä" "value": "Majoitukset yhteensä"
} }
], ],
"price.numberOfEuroBonusPoints": [
{
"offset": 0,
"options": {
"one": {
"value": [
{
"type": 0,
"value": "EB-piste"
}
]
},
"other": {
"value": [
{
"type": 0,
"value": "EB-pistettä"
}
]
}
},
"pluralType": "cardinal",
"type": 6,
"value": "numberOfEuroBonusPoints"
}
],
"price.numberOfScandicPoints": [
{
"offset": 0,
"options": {
"one": {
"value": [
{
"type": 0,
"value": "Piste"
}
]
},
"other": {
"value": [
{
"type": 0,
"value": "pistettä"
}
]
}
},
"pluralType": "cardinal",
"type": 6,
"value": "numberOfScandicPoints"
}
],
"price.numberOfVouchers": [ "price.numberOfVouchers": [
{ {
"offset": 0, "offset": 0,
@@ -8658,6 +8794,12 @@
"value": "). Katso saatavilla olevat hinnat alla." "value": "). Katso saatavilla olevat hinnat alla."
} }
], ],
"selectRate.alert.reservationPolicies": [
{
"type": 0,
"value": "Katso hinnan tiedot"
}
],
"selectRate.availableRooms.all": [ "selectRate.availableRooms.all": [
{ {
"offset": 0, "offset": 0,
+134
View File
@@ -231,6 +231,12 @@
"value": "Spesielle ønsker (valgfritt)" "value": "Spesielle ønsker (valgfritt)"
} }
], ],
"ancillaries.label.quantity": [
{
"type": 0,
"value": "Antall"
}
],
"ancillaries.unableToDisplayBreakfastPrices": [ "ancillaries.unableToDisplayBreakfastPrices": [
{ {
"type": 0, "type": 0,
@@ -249,6 +255,18 @@
"value": "Jeg godtar bestillings- og avbestillingsvilkårene" "value": "Jeg godtar bestillings- og avbestillingsvilkårene"
} }
], ],
"booking.alert.extraBeds.text": [
{
"type": 0,
"value": "Ved bestilling for mer enn 2 gjester vil det påløpe et tilleggsgebyr per person. Se lenke for detaljer."
}
],
"booking.alert.extraguests": [
{
"type": 0,
"value": "Ekstra gjest(er)"
}
],
"booking.approx": [ "booking.approx": [
{ {
"type": 0, "type": 0,
@@ -953,6 +971,16 @@
"value": "Legg til kode" "value": "Legg til kode"
} }
], ],
"bookingWidget.bookingCode.readMore": [
{
"type": 0,
"value": "Les mer om bruk av "
},
{
"type": 1,
"value": "codeVoucher"
}
],
"bookingWidget.bookingCode.remember": [ "bookingWidget.bookingCode.remember": [
{ {
"type": 0, "type": 0,
@@ -1061,6 +1089,16 @@
"value": "details" "value": "details"
} }
], ],
"bookingWidget.reward.readMore": [
{
"type": 0,
"value": "Les mer om bestilling med "
},
{
"type": 1,
"value": "reward"
}
],
"bookingWidget.reward.rewardNight": [ "bookingWidget.reward.rewardNight": [
{ {
"type": 0, "type": 0,
@@ -2510,6 +2548,44 @@
"value": "Datoer" "value": "Datoer"
} }
], ],
"designSystem.stepper.ariaLabel.currentCount": [
{
"type": 0,
"value": "Gjeldende "
},
{
"type": 1,
"value": "label"
},
{
"type": 0,
"value": " "
},
{
"type": 1,
"value": "count"
}
],
"designSystem.stepper.ariaLabel.decrease": [
{
"type": 0,
"value": "Reduser "
},
{
"type": 1,
"value": "label"
}
],
"designSystem.stepper.ariaLabel.increase": [
{
"type": 0,
"value": "Øk "
},
{
"type": 1,
"value": "label"
}
],
"destination.backToCities": [ "destination.backToCities": [
{ {
"type": 0, "type": 0,
@@ -7582,6 +7658,58 @@
"value": "Totalt antall opphold" "value": "Totalt antall opphold"
} }
], ],
"price.numberOfEuroBonusPoints": [
{
"offset": 0,
"options": {
"one": {
"value": [
{
"type": 0,
"value": "EB-poeng"
}
]
},
"other": {
"value": [
{
"type": 0,
"value": "EB-poeng"
}
]
}
},
"pluralType": "cardinal",
"type": 6,
"value": "numberOfEuroBonusPoints"
}
],
"price.numberOfScandicPoints": [
{
"offset": 0,
"options": {
"one": {
"value": [
{
"type": 0,
"value": "Poeng"
}
]
},
"other": {
"value": [
{
"type": 0,
"value": "Poeng"
}
]
}
},
"pluralType": "cardinal",
"type": 6,
"value": "numberOfScandicPoints"
}
],
"price.numberOfVouchers": [ "price.numberOfVouchers": [
{ {
"offset": 0, "offset": 0,
@@ -8654,6 +8782,12 @@
"value": " ). Se tilgjengelige priser nedenfor." "value": " ). Se tilgjengelige priser nedenfor."
} }
], ],
"selectRate.alert.reservationPolicies": [
{
"type": 0,
"value": "Se prisdetaljer"
}
],
"selectRate.availableRooms.all": [ "selectRate.availableRooms.all": [
{ {
"offset": 0, "offset": 0,
+135 -1
View File
@@ -231,6 +231,12 @@
"value": "Särskilda önskemål (valfritt)" "value": "Särskilda önskemål (valfritt)"
} }
], ],
"ancillaries.label.quantity": [
{
"type": 0,
"value": "Antal"
}
],
"ancillaries.unableToDisplayBreakfastPrices": [ "ancillaries.unableToDisplayBreakfastPrices": [
{ {
"type": 0, "type": 0,
@@ -249,6 +255,18 @@
"value": "Jag accepterar boknings- och avbokningsvillkoren" "value": "Jag accepterar boknings- och avbokningsvillkoren"
} }
], ],
"booking.alert.extraBeds.text": [
{
"type": 0,
"value": "När du bokar för fler än 2 gäster tillkommer en extra avgift per person. Se länk för detaljer."
}
],
"booking.alert.extraguests": [
{
"type": 0,
"value": "Extra gäst(er)"
}
],
"booking.approx": [ "booking.approx": [
{ {
"type": 0, "type": 0,
@@ -953,6 +971,16 @@
"value": "Lägg till kod" "value": "Lägg till kod"
} }
], ],
"bookingWidget.bookingCode.readMore": [
{
"type": 0,
"value": "Läs mer om att använda "
},
{
"type": 1,
"value": "codeVoucher"
}
],
"bookingWidget.bookingCode.remember": [ "bookingWidget.bookingCode.remember": [
{ {
"type": 0, "type": 0,
@@ -1061,6 +1089,16 @@
"value": "details" "value": "details"
} }
], ],
"bookingWidget.reward.readMore": [
{
"type": 0,
"value": "Läs mer om bokning med "
},
{
"type": 1,
"value": "reward"
}
],
"bookingWidget.reward.rewardNight": [ "bookingWidget.reward.rewardNight": [
{ {
"type": 0, "type": 0,
@@ -2510,6 +2548,44 @@
"value": "Datum" "value": "Datum"
} }
], ],
"designSystem.stepper.ariaLabel.currentCount": [
{
"type": 0,
"value": "Aktuell "
},
{
"type": 1,
"value": "label"
},
{
"type": 0,
"value": " "
},
{
"type": 1,
"value": "count"
}
],
"designSystem.stepper.ariaLabel.decrease": [
{
"type": 0,
"value": "Minska "
},
{
"type": 1,
"value": "label"
}
],
"designSystem.stepper.ariaLabel.increase": [
{
"type": 0,
"value": "Öka "
},
{
"type": 1,
"value": "label"
}
],
"destination.backToCities": [ "destination.backToCities": [
{ {
"type": 0, "type": 0,
@@ -3845,7 +3921,7 @@
"findMyBooking.findYourStay": [ "findMyBooking.findYourStay": [
{ {
"type": 0, "type": 0,
"value": "Hitta ditt hotell" "value": "Hitta din bokning"
} }
], ],
"findMyBooking.manageBooking": [ "findMyBooking.manageBooking": [
@@ -7566,6 +7642,58 @@
"value": "Totalt antal vistelser" "value": "Totalt antal vistelser"
} }
], ],
"price.numberOfEuroBonusPoints": [
{
"offset": 0,
"options": {
"one": {
"value": [
{
"type": 0,
"value": "EB-poäng"
}
]
},
"other": {
"value": [
{
"type": 0,
"value": "EB-poäng"
}
]
}
},
"pluralType": "cardinal",
"type": 6,
"value": "numberOfEuroBonusPoints"
}
],
"price.numberOfScandicPoints": [
{
"offset": 0,
"options": {
"one": {
"value": [
{
"type": 0,
"value": "Punkt"
}
]
},
"other": {
"value": [
{
"type": 0,
"value": "Poäng"
}
]
}
},
"pluralType": "cardinal",
"type": 6,
"value": "numberOfScandicPoints"
}
],
"price.numberOfVouchers": [ "price.numberOfVouchers": [
{ {
"offset": 0, "offset": 0,
@@ -8626,6 +8754,12 @@
"value": "). Se tillgängliga priser nedan." "value": "). Se tillgängliga priser nedan."
} }
], ],
"selectRate.alert.reservationPolicies": [
{
"type": 0,
"value": "Se bokningsvillkor"
}
],
"selectRate.availableRooms.all": [ "selectRate.availableRooms.all": [
{ {
"offset": 0, "offset": 0,
+5 -5
View File
@@ -30,7 +30,7 @@
"@internationalized/date": "^3.8.0", "@internationalized/date": "^3.8.0",
"@netlify/blobs": "^8.1.0", "@netlify/blobs": "^8.1.0",
"@netlify/functions": "^3.0.0", "@netlify/functions": "^3.0.0",
"@netlify/plugin-nextjs": "^5.15.1", "@netlify/plugin-nextjs": "^5.15.7",
"@radix-ui/react-slot": "^1.2.2", "@radix-ui/react-slot": "^1.2.2",
"@scandic-hotels/booking-flow": "workspace:*", "@scandic-hotels/booking-flow": "workspace:*",
"@scandic-hotels/common": "workspace:*", "@scandic-hotels/common": "workspace:*",
@@ -38,7 +38,7 @@
"@scandic-hotels/tracking": "workspace:*", "@scandic-hotels/tracking": "workspace:*",
"@scandic-hotels/trpc": "workspace:*", "@scandic-hotels/trpc": "workspace:*",
"@sentry/nextjs": "^10.33.0", "@sentry/nextjs": "^10.33.0",
"@swc/plugin-formatjs": "^3.2.2", "@swc/plugin-formatjs": "^8.1.0",
"@t3-oss/env-nextjs": "^0.13.4", "@t3-oss/env-nextjs": "^0.13.4",
"@tanstack/react-query": "^5.75.5", "@tanstack/react-query": "^5.75.5",
"@tanstack/react-query-devtools": "^5.75.5", "@tanstack/react-query-devtools": "^5.75.5",
@@ -66,12 +66,12 @@
"jsonwebtoken": "^9.0.2", "jsonwebtoken": "^9.0.2",
"md5": "^2.3.0", "md5": "^2.3.0",
"motion": "^12.10.0", "motion": "^12.10.0",
"next": "16.0.10", "next": "16.1.6",
"next-auth": "5.0.0-beta.29", "next-auth": "5.0.0-beta.29",
"react": "19.2.1", "react": "19.2.4",
"react-aria-components": "1.8.0", "react-aria-components": "1.8.0",
"react-day-picker": "^9.6.7", "react-day-picker": "^9.6.7",
"react-dom": "19.2.1", "react-dom": "19.2.4",
"react-feather": "^2.0.10", "react-feather": "^2.0.10",
"react-focus-lock": "^2.13.6", "react-focus-lock": "^2.13.6",
"react-hook-form": "^7.56.2", "react-hook-form": "^7.56.2",
+3 -5
View File
@@ -13,22 +13,20 @@ import { MyStaySkeleton } from "@/components/HotelReservation/MyStay/myStaySkele
import { MyStayContext } from "@/contexts/MyStay" import { MyStayContext } from "@/contexts/MyStay"
import type { Lang } from "@scandic-hotels/common/constants/language" import type { Lang } from "@scandic-hotels/common/constants/language"
import type { import type { BookingConfirmation } from "@scandic-hotels/trpc/types/bookingConfirmation"
BookingConfirmation,
BookingConfirmationSchema,
} from "@scandic-hotels/trpc/types/bookingConfirmation"
import type { RoomCategories } from "@scandic-hotels/trpc/types/hotel" import type { RoomCategories } from "@scandic-hotels/trpc/types/hotel"
import type { CreditCard } from "@scandic-hotels/trpc/types/user" import type { CreditCard } from "@scandic-hotels/trpc/types/user"
import type { Packages } from "@/types/components/myPages/myStay/ancillaries" import type { Packages } from "@/types/components/myPages/myStay/ancillaries"
import type { MyStayStore } from "@/types/contexts/my-stay" import type { MyStayStore } from "@/types/contexts/my-stay"
import type { getLinkedReservations } from "@/lib/trpc/memoizedRequests"
interface MyStayProviderProps { interface MyStayProviderProps {
bookingConfirmation: BookingConfirmation bookingConfirmation: BookingConfirmation
breakfastPackages: Packages | null breakfastPackages: Packages | null
isLoggedIn?: boolean isLoggedIn?: boolean
lang: Lang lang: Lang
linkedReservationsPromise: Promise<BookingConfirmationSchema[]> linkedReservationsPromise: ReturnType<typeof getLinkedReservations>
refId: string refId: string
roomCategories: RoomCategories roomCategories: RoomCategories
savedCreditCards: CreditCard[] | null savedCreditCards: CreditCard[] | null
Binary file not shown.

Before

Width:  |  Height:  |  Size: 847 KiB

After

Width:  |  Height:  |  Size: 301 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 MiB

After

Width:  |  Height:  |  Size: 208 KiB

@@ -212,6 +212,12 @@ describe("getTimeAgoText", () => {
expect(result).toBe("") expect(result).toBe("")
}) })
it("should return Today for todays date", () => {
const todaysDate = dt().format("YYYY-MM-DD")
const result = getTimeAgoText(todaysDate, mockIntl)
expect(result).toBe("Today")
})
}) })
describe("boundary transitions", () => { describe("boundary transitions", () => {
+7
View File
@@ -22,6 +22,13 @@ export function getTimeAgoText(checkoutDate: string, intl: IntlShape): string {
return "" return ""
} }
if (daysDiff === 0) {
return intl.formatMessage({
id: "nextStay.today",
defaultMessage: "Today",
})
}
if (daysDiff <= 30) { if (daysDiff <= 30) {
// 1-30 days // 1-30 days
return intl.formatMessage( return intl.formatMessage(
@@ -198,10 +198,15 @@ export function Room({
{isFlexBooking || isChangeBooking ? ( {isFlexBooking || isChangeBooking ? (
<li className={styles.listItem}> <li className={styles.listItem}>
<p className={styles.label}> <p className={styles.label}>
{intl.formatMessage({ {isChangeBooking
id: "booking.changeOrCancel", ? intl.formatMessage({
defaultMessage: "Change or cancel", id: "booking.changeTitle",
})} defaultMessage: "Change",
})
: intl.formatMessage({
id: "booking.changeOrCancel",
defaultMessage: "Change or cancel",
})}
</p> </p>
<p> <p>
{intl.formatMessage( {intl.formatMessage(
@@ -9,7 +9,7 @@
overflow-y: auto; overflow-y: auto;
padding: var(--Space-x2) var(--Space-x3); padding: var(--Space-x2) var(--Space-x3);
position: fixed; position: fixed;
top: calc(140px + max(var(--sitewide-alert-sticky-height), 25px)); top: calc(140px + max(var(--sitewide-alert-sticky-height), 15px));
width: 100%; width: 100%;
height: calc(100% - 200px); height: calc(100% - 200px);
z-index: 10010; z-index: 10010;
@@ -1,13 +1,11 @@
import Footnote from "@scandic-hotels/design-system/Footnote" import { Typography } from "@scandic-hotels/design-system/Typography"
import styles from "./list.module.css" import styles from "./list.module.css"
export default function Label({ children }: React.PropsWithChildren) { export default function Label({ children }: React.PropsWithChildren) {
return ( return (
<li className={styles.label}> <Typography variant="Title/Overline/sm">
<Footnote color="uiTextPlaceholder" textTransform="uppercase"> <li className={styles.label}>{children}</li>
{children} </Typography>
</Footnote>
</li>
) )
} }
@@ -5,5 +5,6 @@
} }
.label { .label {
padding: 0 var(--Space-x1); padding: 0 var(--Space-x1) var(--Space-x05);
color: var(--Text-Tertiary);
} }
@@ -6,7 +6,6 @@ import { useIntl } from "react-intl"
import { useDebounceValue } from "usehooks-ts" import { useDebounceValue } from "usehooks-ts"
import { Divider } from "@scandic-hotels/design-system/Divider" import { Divider } from "@scandic-hotels/design-system/Divider"
import Footnote from "@scandic-hotels/design-system/Footnote"
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon" import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
import { Typography } from "@scandic-hotels/design-system/Typography" import { Typography } from "@scandic-hotels/design-system/Typography"
import { trpc } from "@scandic-hotels/trpc/client" import { trpc } from "@scandic-hotels/trpc/client"
@@ -192,16 +191,14 @@ export default function SearchList({
{typeFilteredSearchHistory && typeFilteredSearchHistory.length > 0 && ( {typeFilteredSearchHistory && typeFilteredSearchHistory.length > 0 && (
<> <>
<Divider className={styles.noResultsDivider} /> <Divider className={styles.noResultsDivider} />
<Footnote <Typography variant="Title/Overline/sm">
className={styles.text} <p className={styles.text}>
color="uiTextPlaceholder" {intl.formatMessage({
textTransform="uppercase" id: "bookingWidget.searchList.latestSearches",
> defaultMessage: "Latest searches",
{intl.formatMessage({ })}
id: "bookingWidget.searchList.latestSearches", </p>
defaultMessage: "Latest searches", </Typography>
})}
</Footnote>
<List <List
getItemProps={getItemProps} getItemProps={getItemProps}
highlightedIndex={highlightedIndex} highlightedIndex={highlightedIndex}
@@ -226,12 +223,14 @@ export default function SearchList({
if (displaySearchHistory) { if (displaySearchHistory) {
return ( return (
<Dialog getMenuProps={getMenuProps}> <Dialog getMenuProps={getMenuProps}>
<Footnote color="uiTextPlaceholder" textTransform="uppercase"> <Typography variant="Title/Overline/sm">
{intl.formatMessage({ <p className={styles.text}>
id: "bookingWidget.searchList.latestSearches", {intl.formatMessage({
defaultMessage: "Latest searches", id: "bookingWidget.searchList.latestSearches",
})} defaultMessage: "Latest searches",
</Footnote> })}
</p>
</Typography>
<List <List
getItemProps={getItemProps} getItemProps={getItemProps}
highlightedIndex={highlightedIndex} highlightedIndex={highlightedIndex}
@@ -33,6 +33,8 @@
.text { .text {
padding: 0 var(--Space-x1); padding: 0 var(--Space-x1);
color: var(--Text-Tertiary);
white-space: normal;
} }
.textPlaceholderColor { .textPlaceholderColor {
color: var(--UI-Text-Placeholder); color: var(--UI-Text-Placeholder);
@@ -5,6 +5,7 @@ import { cx } from "class-variance-authority"
import { useSearchParams } from "next/navigation" import { useSearchParams } from "next/navigation"
import { use, useEffect, useRef, useState } from "react" import { use, useEffect, useRef, useState } from "react"
import { FormProvider, useForm } from "react-hook-form" import { FormProvider, useForm } from "react-hook-form"
import { useIntl } from "react-intl"
import { dt } from "@scandic-hotels/common/dt" import { dt } from "@scandic-hotels/common/dt"
import { useScrollLock } from "@scandic-hotels/common/hooks/useScrollLock" import { useScrollLock } from "@scandic-hotels/common/hooks/useScrollLock"
@@ -12,7 +13,7 @@ import useStickyPosition from "@scandic-hotels/common/hooks/useStickyPosition"
import { StickyElementNameEnum } from "@scandic-hotels/common/stores/sticky-position" import { StickyElementNameEnum } from "@scandic-hotels/common/stores/sticky-position"
import { debounce } from "@scandic-hotels/common/utils/debounce" import { debounce } from "@scandic-hotels/common/utils/debounce"
import isValidJson from "@scandic-hotels/common/utils/isValidJson" import isValidJson from "@scandic-hotels/common/utils/isValidJson"
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon" import { IconButton } from "@scandic-hotels/design-system/IconButton"
import { trpc } from "@scandic-hotels/trpc/client" import { trpc } from "@scandic-hotels/trpc/client"
import { SEARCH_TYPE_REDEMPTION } from "@scandic-hotels/trpc/constants/booking" import { SEARCH_TYPE_REDEMPTION } from "@scandic-hotels/trpc/constants/booking"
@@ -66,6 +67,7 @@ export default function BookingWidgetClient({
autoLock: false, autoLock: false,
}) })
const shouldFetchAutoComplete = !!data.hotelId || !!data.city const shouldFetchAutoComplete = !!data.hotelId || !!data.city
const intl = useIntl()
const { data: destinationsData, isPending } = const { data: destinationsData, isPending } =
trpc.autocomplete.destinations.useQuery( trpc.autocomplete.destinations.useQuery(
@@ -257,13 +259,17 @@ export default function BookingWidgetClient({
/> />
<div className={styles.backdrop} onClick={closeMobileSearch} /> <div className={styles.backdrop} onClick={closeMobileSearch} />
<div className={formContainerClassNames}> <div className={formContainerClassNames}>
<button <IconButton
className={styles.close} className={styles.close}
onClick={closeMobileSearch} variant="Muted"
type="button" emphasis
> aria-label={intl.formatMessage({
<MaterialIcon icon="close" /> id: "common.close",
</button> defaultMessage: "Close",
})}
onPress={closeMobileSearch}
iconName="close"
/>
<Form <Form
type={type} type={type}
onClose={closeMobileSearch} onClose={closeMobileSearch}
@@ -65,7 +65,7 @@ export default function DatePickerRangeDesktop({
range_start: styles.rangeStart, range_start: styles.rangeStart,
root: `${classNames.root} ${styles.container}`, root: `${classNames.root} ${styles.container}`,
week: styles.week, week: styles.week,
weekday: `${classNames.weekday} ${styles.weekDay}`, weekday: styles.weekDay,
nav: `${classNames.nav} ${styles.nav}`, nav: `${classNames.nav} ${styles.nav}`,
button_next: `${classNames.button_next} ${styles.button_next}`, button_next: `${classNames.button_next} ${styles.button_next}`,
button_previous: `${classNames.button_previous} ${styles.button_previous}`, button_previous: `${classNames.button_previous} ${styles.button_previous}`,
@@ -6,7 +6,7 @@ import { useIntl } from "react-intl"
import { Lang } from "@scandic-hotels/common/constants/language" import { Lang } from "@scandic-hotels/common/constants/language"
import { dt } from "@scandic-hotels/common/dt" import { dt } from "@scandic-hotels/common/dt"
import { Button } from "@scandic-hotels/design-system/Button" import { Button } from "@scandic-hotels/design-system/Button"
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon" import { IconButton } from "@scandic-hotels/design-system/IconButton"
import { Typography } from "@scandic-hotels/design-system/Typography" import { Typography } from "@scandic-hotels/design-system/Typography"
import useLang from "../../../../hooks/useLang" import useLang from "../../../../hooks/useLang"
@@ -72,9 +72,17 @@ export default function DatePickerRangeMobile({
return ( return (
<div className={styles.container} ref={monthsRef}> <div className={styles.container} ref={monthsRef}>
<header className={styles.header}> <header className={styles.header}>
<button className={styles.close} onClick={close} type="button"> <IconButton
<MaterialIcon icon="close" /> className={styles.close}
</button> variant="Muted"
emphasis
aria-label={intl.formatMessage({
id: "common.close",
defaultMessage: "Close",
})}
onPress={close}
iconName="close"
/>
</header> </header>
<DayPicker <DayPicker
classNames={{ classNames={{
@@ -90,7 +98,7 @@ export default function DatePickerRangeMobile({
range_start: styles.rangeStart, range_start: styles.rangeStart,
root: `${classNames.root} ${styles.root}`, root: `${classNames.root} ${styles.root}`,
week: styles.week, week: styles.week,
weekday: `${classNames.weekday} ${styles.weekDay}`, weekday: styles.weekDay,
}} }}
disabled={[ disabled={[
{ from: lastDayOfPreviousMonth, to: yesterday }, { from: lastDayOfPreviousMonth, to: yesterday },
@@ -20,12 +20,12 @@ div.months {
td.day, td.day,
td.rangeEnd, td.rangeEnd,
td.rangeStart { td.rangeStart {
font-family: var(--typography-Body-Bold-fontFamily); font-family: var(--Body-Paragraph-Font-family), var(--Body-Paragraph-Font-fallback);
font-size: var(--typography-Body-Bold-fontSize); font-size: var(--Body-Paragraph-Size);
font-weight: 500; font-weight: var(--Body-Paragraph-Font-weight-2);
letter-spacing: var(--typography-Body-Bold-letterSpacing); letter-spacing: var(--Body-Paragraph-Letter-spacing);
line-height: var(--typography-Body-Bold-lineHeight); line-height: 1.5;
text-decoration: var(--typography-Body-Bold-textDecoration); text-decoration: none;
} }
td.rangeEnd, td.rangeEnd,
@@ -92,14 +92,15 @@ td.day[data-outside="true"] ~ td.day[data-disabled="true"] button.dayButton,
} }
.weekDay { .weekDay {
color: var(--UI-Text-Placeholder); color: var(--Text-Tertiary);
font-family: var(--typography-Footnote-Labels-fontFamily); font-family: var(--Title-Overline-sm-Font-family), var(--Title-Overline-sm-Font-fallback);
font-size: var(--typography-Footnote-Labels-fontSize); font-size: var(--Title-Overline-sm-Size);
font-weight: var(--typography-Footnote-Labels-fontWeight); font-style: normal;
letter-spacing: var(--typography-Footnote-Labels-letterSpacing); font-weight: var(--Title-Overline-sm-Font-weight);
line-height: var(--typography-Footnote-Labels-lineHeight); line-height: 1.5;
text-decoration: var(--typography-Footnote-Labels-textDecoration); letter-spacing: var(--Title-Overline-sm-Letter-spacing);
text-transform: uppercase; text-transform: var(--Title-Overline-sm-Text-Transform);
text-decoration: none;
} }
.footer { .footer {
@@ -97,12 +97,12 @@ div.months {
td.day, td.day,
td.rangeEnd, td.rangeEnd,
td.rangeStart { td.rangeStart {
font-family: var(--typography-Body-Bold-fontFamily); font-family: var(--Body-Paragraph-Font-family), var(--Body-Paragraph-Font-fallback);
font-size: var(--typography-Body-Bold-fontSize); font-size: var(--Body-Paragraph-Size);
font-weight: 500; font-weight: var(--Body-Paragraph-Font-weight-2);
letter-spacing: var(--typography-Body-Bold-letterSpacing); letter-spacing: var(--Body-Paragraph-Letter-spacing);
line-height: var(--typography-Body-Bold-lineHeight); line-height: 1.5;
text-decoration: var(--typography-Body-Bold-textDecoration); text-decoration: none;
} }
td.rangeEnd, td.rangeEnd,
@@ -165,15 +165,15 @@ td.day[data-outside="true"] ~ td.day[data-disabled="true"] button.dayButton,
} }
.weekDay { .weekDay {
color: var(--Base-Text-Medium-contrast); color: var(--Text-Tertiary);
opacity: 1; font-family: var(--Title-Overline-sm-Font-family), var(--Title-Overline-sm-Font-fallback);
font-family: var(--typography-Caption-Labels-fontFamily); font-size: var(--Title-Overline-sm-Size);
font-size: var(--typography-Caption-Labels-fontSize); font-style: normal;
font-weight: var(--typography-Caption-Labels-fontWeight); font-weight: var(--Title-Overline-sm-Font-weight);
letter-spacing: var(--typography-Caption-Labels-letterSpacing); line-height: 1.5;
line-height: var(--typography-Caption-Labels-lineHeight); letter-spacing: var(--Title-Overline-sm-Letter-spacing);
text-decoration: var(--typography-Caption-Labels-textDecoration); text-transform: var(--Title-Overline-sm-Text-Transform);
text-transform: uppercase; text-decoration: none;
} }
@media screen and (min-width: 1367px) { @media screen and (min-width: 1367px) {
@@ -5,6 +5,7 @@ import { useFormContext, useWatch } from "react-hook-form"
import { useIntl } from "react-intl" import { useIntl } from "react-intl"
import { Button } from "@scandic-hotels/design-system/Button" import { Button } from "@scandic-hotels/design-system/Button"
import { IconButton } from "@scandic-hotels/design-system/IconButton"
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon" import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
import { Typography } from "@scandic-hotels/design-system/Typography" import { Typography } from "@scandic-hotels/design-system/Typography"
import { SEARCH_TYPE_REDEMPTION } from "@scandic-hotels/trpc/constants/booking" import { SEARCH_TYPE_REDEMPTION } from "@scandic-hotels/trpc/constants/booking"
@@ -114,9 +115,17 @@ export default function GuestsRoomsPickerDialog({
<> <>
<section className={styles.contentWrapper}> <section className={styles.contentWrapper}>
<header className={styles.header}> <header className={styles.header}>
<button type="button" className={styles.close} onClick={onClose}> <IconButton
<MaterialIcon icon="close" /> className={styles.close}
</button> variant="Muted"
emphasis
aria-label={intl.formatMessage({
id: "common.close",
defaultMessage: "Close",
})}
onPress={onClose}
iconName="close"
/>
</header> </header>
<div className={styles.contentContainer}> <div className={styles.contentContainer}>
@@ -4,9 +4,8 @@ import { useIntl } from "react-intl"
import { CurrencyEnum } from "@scandic-hotels/common/constants/currency" import { CurrencyEnum } from "@scandic-hotels/common/constants/currency"
import { formatPrice } from "@scandic-hotels/common/utils/numberFormatting" import { formatPrice } from "@scandic-hotels/common/utils/numberFormatting"
import Footnote from "@scandic-hotels/design-system/Footnote"
import Checkbox from "@scandic-hotels/design-system/Form/Checkbox" import Checkbox from "@scandic-hotels/design-system/Form/Checkbox"
import Link from "@scandic-hotels/design-system/OldDSLink" import { TextLink } from "@scandic-hotels/design-system/TextLink"
import { Typography } from "@scandic-hotels/design-system/Typography" import { Typography } from "@scandic-hotels/design-system/Typography"
import { useBookingFlowConfig } from "../../../../../bookingFlowConfig/bookingFlowConfigContext" import { useBookingFlowConfig } from "../../../../../bookingFlowConfig/bookingFlowConfigContext"
@@ -83,8 +82,8 @@ export default function JoinScandicFriendsCard({
</Typography> </Typography>
</Checkbox> </Checkbox>
<div className={styles.terms}> <Typography variant="Body/Supporting text (caption)/smRegular">
<Footnote color="uiTextPlaceholder"> <p className={styles.terms}>
{intl.formatMessage( {intl.formatMessage(
{ {
id: "enterDetails.joinScandicFriendsCard.terms", id: "enterDetails.joinScandicFriendsCard.terms",
@@ -93,19 +92,18 @@ export default function JoinScandicFriendsCard({
}, },
{ {
termsAndConditionsLink: (str) => ( termsAndConditionsLink: (str) => (
<Link <TextLink
textDecoration="underline" typography="Link/sm"
size="tiny"
target="_blank" target="_blank"
href={routes.membershipTermsAndConditions[lang]} href={routes.membershipTermsAndConditions[lang]}
> >
{str} {str}
</Link> </TextLink>
), ),
} }
)} )}
</Footnote> </p>
</div> </Typography>
</div> </div>
) )
} }
@@ -28,6 +28,7 @@
.terms { .terms {
grid-area: terms; grid-area: terms;
color: var(--Text-Secondary);
} }
@media screen and (min-width: 768px) { @media screen and (min-width: 768px) {
@@ -4,9 +4,8 @@ import { useIntl } from "react-intl"
import { CurrencyEnum } from "@scandic-hotels/common/constants/currency" import { CurrencyEnum } from "@scandic-hotels/common/constants/currency"
import { formatPrice } from "@scandic-hotels/common/utils/numberFormatting" import { formatPrice } from "@scandic-hotels/common/utils/numberFormatting"
import Footnote from "@scandic-hotels/design-system/Footnote"
import Checkbox from "@scandic-hotels/design-system/Form/Checkbox" import Checkbox from "@scandic-hotels/design-system/Form/Checkbox"
import Link from "@scandic-hotels/design-system/OldDSLink" import { TextLink } from "@scandic-hotels/design-system/TextLink"
import { Typography } from "@scandic-hotels/design-system/Typography" import { Typography } from "@scandic-hotels/design-system/Typography"
import { useBookingFlowConfig } from "../../../../../bookingFlowConfig/bookingFlowConfigContext" import { useBookingFlowConfig } from "../../../../../bookingFlowConfig/bookingFlowConfigContext"
@@ -97,8 +96,8 @@ export function PartnerSASJoinScandicFriendsCard({
/> />
</div> </div>
<div className={styles.terms}> <Typography variant="Body/Supporting text (caption)/smRegular">
<Footnote color="uiTextPlaceholder"> <p className={styles.terms}>
{intl.formatMessage( {intl.formatMessage(
{ {
id: "enterDetails.joinScandicFriendsCard.terms", id: "enterDetails.joinScandicFriendsCard.terms",
@@ -107,19 +106,18 @@ export function PartnerSASJoinScandicFriendsCard({
}, },
{ {
termsAndConditionsLink: (str) => ( termsAndConditionsLink: (str) => (
<Link <TextLink
textDecoration="underline" typography="Link/sm"
size="tiny"
target="_blank" target="_blank"
href={routes.membershipTermsAndConditions[lang]} href={routes.membershipTermsAndConditions[lang]}
> >
{str} {str}
</Link> </TextLink>
), ),
} }
)} )}
</Footnote> </p>
</div> </Typography>
</div> </div>
) )
} }
@@ -31,6 +31,7 @@
.terms { .terms {
grid-area: terms; grid-area: terms;
color: var(--Text-Secondary);
} }
@media screen and (min-width: 768px) { @media screen and (min-width: 768px) {
@@ -4,10 +4,9 @@ import { useIntl } from "react-intl"
import { CurrencyEnum } from "@scandic-hotels/common/constants/currency" import { CurrencyEnum } from "@scandic-hotels/common/constants/currency"
import { useLazyPathname } from "@scandic-hotels/common/hooks/useLazyPathname" import { useLazyPathname } from "@scandic-hotels/common/hooks/useLazyPathname"
import { formatPrice } from "@scandic-hotels/common/utils/numberFormatting" import { formatPrice } from "@scandic-hotels/common/utils/numberFormatting"
import Footnote from "@scandic-hotels/design-system/Footnote"
import Checkbox from "@scandic-hotels/design-system/Form/Checkbox" import Checkbox from "@scandic-hotels/design-system/Form/Checkbox"
import { LoginButton } from "@scandic-hotels/design-system/LoginButton" import { LoginButton } from "@scandic-hotels/design-system/LoginButton"
import Link from "@scandic-hotels/design-system/OldDSLink" import { TextLink } from "@scandic-hotels/design-system/TextLink"
import { Typography } from "@scandic-hotels/design-system/Typography" import { Typography } from "@scandic-hotels/design-system/Typography"
import { trackEvent } from "@scandic-hotels/tracking/base" import { trackEvent } from "@scandic-hotels/tracking/base"
import { trackLoginClick } from "@scandic-hotels/tracking/navigation" import { trackLoginClick } from "@scandic-hotels/tracking/navigation"
@@ -101,8 +100,8 @@ export function JoinScandicFriendsCard({ name = "join" }: Props) {
})} })}
</LoginButton> </LoginButton>
<div className={styles.terms}> <Typography variant="Body/Supporting text (caption)/smRegular">
<Footnote color="uiTextPlaceholder"> <p className={styles.terms}>
{intl.formatMessage( {intl.formatMessage(
{ {
id: "enterDetails.joinScandicFriendsCard.terms", id: "enterDetails.joinScandicFriendsCard.terms",
@@ -111,19 +110,18 @@ export function JoinScandicFriendsCard({ name = "join" }: Props) {
}, },
{ {
termsAndConditionsLink: (str) => ( termsAndConditionsLink: (str) => (
<Link <TextLink
textDecoration="underline" typography="Link/sm"
size="tiny"
target="_blank" target="_blank"
href={routes.membershipTermsAndConditions[lang]} href={routes.membershipTermsAndConditions[lang]}
> >
{str} {str}
</Link> </TextLink>
), ),
} }
)} )}
</Footnote> </p>
</div> </Typography>
</div> </div>
) )
} }
@@ -34,6 +34,7 @@
.terms { .terms {
grid-area: terms; grid-area: terms;
color: var(--Text-Secondary);
} }
@media screen and (min-width: 768px) { @media screen and (min-width: 768px) {
@@ -4,9 +4,8 @@ import { useIntl } from "react-intl"
import { CurrencyEnum } from "@scandic-hotels/common/constants/currency" import { CurrencyEnum } from "@scandic-hotels/common/constants/currency"
import { formatPrice } from "@scandic-hotels/common/utils/numberFormatting" import { formatPrice } from "@scandic-hotels/common/utils/numberFormatting"
import Footnote from "@scandic-hotels/design-system/Footnote"
import Checkbox from "@scandic-hotels/design-system/Form/Checkbox" import Checkbox from "@scandic-hotels/design-system/Form/Checkbox"
import Link from "@scandic-hotels/design-system/OldDSLink" import { TextLink } from "@scandic-hotels/design-system/TextLink"
import { Typography } from "@scandic-hotels/design-system/Typography" import { Typography } from "@scandic-hotels/design-system/Typography"
import { trpc } from "@scandic-hotels/trpc/client" import { trpc } from "@scandic-hotels/trpc/client"
@@ -94,8 +93,8 @@ export function PartnerSASJoinScandicFriendsCard({
/> />
</div> </div>
<div className={styles.terms}> <Typography variant="Body/Supporting text (caption)/smRegular">
<Footnote color="uiTextPlaceholder"> <p className={styles.terms}>
{intl.formatMessage( {intl.formatMessage(
{ {
id: "enterDetails.joinScandicFriendsCard.terms", id: "enterDetails.joinScandicFriendsCard.terms",
@@ -104,19 +103,18 @@ export function PartnerSASJoinScandicFriendsCard({
}, },
{ {
termsAndConditionsLink: (str) => ( termsAndConditionsLink: (str) => (
<Link <TextLink
textDecoration="underline" typography="Link/sm"
size="tiny"
target="_blank" target="_blank"
href={routes.membershipTermsAndConditions[lang]} href={routes.membershipTermsAndConditions[lang]}
> >
{str} {str}
</Link> </TextLink>
), ),
} }
)} )}
</Footnote> </p>
</div> </Typography>
</div> </div>
) )
} }
@@ -31,6 +31,7 @@
.terms { .terms {
grid-area: terms; grid-area: terms;
color: var(--Text-Secondary);
} }
@media screen and (min-width: 768px) { @media screen and (min-width: 768px) {

Some files were not shown because too many files have changed in this diff Show More