Compare commits
29 Commits
5a90d454ce
..
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 1dffeb6be7 | |||
| 1f1ed2e4f3 | |||
| bc9eaf6706 | |||
| 549265cd34 | |||
| 989b18527e | |||
| 0cda37808e | |||
| b3c4761ae5 | |||
| dd65467573 | |||
| eb45e6b294 | |||
| 6553fcf685 | |||
| c2cf6b03a7 | |||
| 310ad7bc7f | |||
| fbdbd35813 | |||
| d6b94376b0 | |||
| 7a604f1250 | |||
| 13fd8f81c9 | |||
| 16cc26632e | |||
| 8ac2c4ba22 | |||
| 65e5d90fee | |||
| 5f55687239 | |||
| e30ce9ac30 | |||
| 61c024dbda | |||
| 2f73fce6f2 | |||
| 76ee5e97bf | |||
| fd38542863 | |||
| cc60cf2903 | |||
| 13b0d976ac | |||
| 77eabac038 | |||
| 0919134f88 |
@@ -1,3 +1,4 @@
|
||||
.layout {
|
||||
font-family: var(--typography-Body-Regular-fontFamily);
|
||||
font-family:
|
||||
var(--Body-Paragraph-Font-family), var(--Body-Paragraph-Font-fallback);
|
||||
}
|
||||
|
||||
@@ -158,6 +158,11 @@ body.partner-sas {
|
||||
white-space: nowrap;
|
||||
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) {
|
||||
:root {
|
||||
|
||||
@@ -231,6 +231,12 @@
|
||||
"value": "Særlige ønsker (valgfrit)"
|
||||
}
|
||||
],
|
||||
"ancillaries.label.quantity": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Antal"
|
||||
}
|
||||
],
|
||||
"ancillaries.unableToDisplayBreakfastPrices": [
|
||||
{
|
||||
"type": 0,
|
||||
@@ -249,6 +255,18 @@
|
||||
"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": [
|
||||
{
|
||||
"type": 0,
|
||||
@@ -953,6 +971,16 @@
|
||||
"value": "Tilføj kode"
|
||||
}
|
||||
],
|
||||
"bookingWidget.bookingCode.readMore": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Læs mere om brug af "
|
||||
},
|
||||
{
|
||||
"type": 1,
|
||||
"value": "codeVoucher"
|
||||
}
|
||||
],
|
||||
"bookingWidget.bookingCode.remember": [
|
||||
{
|
||||
"type": 0,
|
||||
@@ -1061,6 +1089,16 @@
|
||||
"value": "details"
|
||||
}
|
||||
],
|
||||
"bookingWidget.reward.readMore": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Læs mere om booking med "
|
||||
},
|
||||
{
|
||||
"type": 1,
|
||||
"value": "reward"
|
||||
}
|
||||
],
|
||||
"bookingWidget.reward.rewardNight": [
|
||||
{
|
||||
"type": 0,
|
||||
@@ -2510,6 +2548,44 @@
|
||||
"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": [
|
||||
{
|
||||
"type": 0,
|
||||
@@ -5783,7 +5859,7 @@
|
||||
"myPages.myStay.ancillaries.reachedMaxItemsStepperMessage": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Maksimal antal nået for denne vare."
|
||||
"value": "Maksimalt antal nået for denne vare."
|
||||
}
|
||||
],
|
||||
"myPages.myStay.ancillaries.reachedMaxPointsMessage": [
|
||||
@@ -7569,6 +7645,58 @@
|
||||
"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": [
|
||||
{
|
||||
"offset": 0,
|
||||
@@ -8629,6 +8757,12 @@
|
||||
"value": "). Se tilgængelige priser nedenfor."
|
||||
}
|
||||
],
|
||||
"selectRate.alert.reservationPolicies": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Se prisdetaljer"
|
||||
}
|
||||
],
|
||||
"selectRate.availableRooms.all": [
|
||||
{
|
||||
"offset": 0,
|
||||
|
||||
@@ -231,6 +231,12 @@
|
||||
"value": "Sonderwünsche (optional)"
|
||||
}
|
||||
],
|
||||
"ancillaries.label.quantity": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Menge"
|
||||
}
|
||||
],
|
||||
"ancillaries.unableToDisplayBreakfastPrices": [
|
||||
{
|
||||
"type": 0,
|
||||
@@ -249,6 +255,18 @@
|
||||
"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": [
|
||||
{
|
||||
"type": 0,
|
||||
@@ -949,6 +967,16 @@
|
||||
"value": "Code hinzufügen"
|
||||
}
|
||||
],
|
||||
"bookingWidget.bookingCode.readMore": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Mehr erfahren über die Verwendung von "
|
||||
},
|
||||
{
|
||||
"type": 1,
|
||||
"value": "codeVoucher"
|
||||
}
|
||||
],
|
||||
"bookingWidget.bookingCode.remember": [
|
||||
{
|
||||
"type": 0,
|
||||
@@ -1057,6 +1085,20 @@
|
||||
"value": "details"
|
||||
}
|
||||
],
|
||||
"bookingWidget.reward.readMore": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Mehr über Buchungen mit "
|
||||
},
|
||||
{
|
||||
"type": 1,
|
||||
"value": "reward"
|
||||
},
|
||||
{
|
||||
"type": 0,
|
||||
"value": "erfahren"
|
||||
}
|
||||
],
|
||||
"bookingWidget.reward.rewardNight": [
|
||||
{
|
||||
"type": 0,
|
||||
@@ -2522,6 +2564,44 @@
|
||||
"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": [
|
||||
{
|
||||
"type": 0,
|
||||
@@ -7570,6 +7650,58 @@
|
||||
"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": [
|
||||
{
|
||||
"offset": 0,
|
||||
@@ -8634,6 +8766,12 @@
|
||||
"value": "). Verfügbare Übernachtungspreise sind unten zu sehen."
|
||||
}
|
||||
],
|
||||
"selectRate.alert.reservationPolicies": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Tarifdetails anzeigen"
|
||||
}
|
||||
],
|
||||
"selectRate.availableRooms.all": [
|
||||
{
|
||||
"offset": 0,
|
||||
|
||||
@@ -231,6 +231,12 @@
|
||||
"value": "Special requests (optional)"
|
||||
}
|
||||
],
|
||||
"ancillaries.label.quantity": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Quantity"
|
||||
}
|
||||
],
|
||||
"ancillaries.unableToDisplayBreakfastPrices": [
|
||||
{
|
||||
"type": 0,
|
||||
@@ -249,6 +255,18 @@
|
||||
"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": [
|
||||
{
|
||||
"type": 0,
|
||||
@@ -337,6 +355,12 @@
|
||||
"value": "Change or cancel"
|
||||
}
|
||||
],
|
||||
"booking.changeTitle": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Change"
|
||||
}
|
||||
],
|
||||
"booking.codeVoucher": [
|
||||
{
|
||||
"type": 0,
|
||||
@@ -953,6 +977,16 @@
|
||||
"value": "Add code"
|
||||
}
|
||||
],
|
||||
"bookingWidget.bookingCode.readMore": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Read more about using "
|
||||
},
|
||||
{
|
||||
"type": 1,
|
||||
"value": "codeVoucher"
|
||||
}
|
||||
],
|
||||
"bookingWidget.bookingCode.remember": [
|
||||
{
|
||||
"type": 0,
|
||||
@@ -1061,6 +1095,16 @@
|
||||
"value": "details"
|
||||
}
|
||||
],
|
||||
"bookingWidget.reward.readMore": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Read more about booking with "
|
||||
},
|
||||
{
|
||||
"type": 1,
|
||||
"value": "reward"
|
||||
}
|
||||
],
|
||||
"bookingWidget.reward.rewardNight": [
|
||||
{
|
||||
"type": 0,
|
||||
@@ -2198,6 +2242,16 @@
|
||||
"value": " points"
|
||||
}
|
||||
],
|
||||
"common.pointsInLine": [
|
||||
{
|
||||
"type": 1,
|
||||
"value": "points"
|
||||
},
|
||||
{
|
||||
"type": 0,
|
||||
"value": " points"
|
||||
}
|
||||
],
|
||||
"common.pointsToSpend": [
|
||||
{
|
||||
"type": 0,
|
||||
@@ -2510,6 +2564,44 @@
|
||||
"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": [
|
||||
{
|
||||
"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."
|
||||
}
|
||||
],
|
||||
"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": [
|
||||
{
|
||||
"type": 0,
|
||||
@@ -3344,6 +3442,40 @@
|
||||
"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": [
|
||||
{
|
||||
"type": 0,
|
||||
@@ -5669,6 +5801,24 @@
|
||||
"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": [
|
||||
{
|
||||
"type": 0,
|
||||
@@ -6057,6 +6207,88 @@
|
||||
"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": [
|
||||
{
|
||||
"type": 0,
|
||||
@@ -7546,6 +7778,58 @@
|
||||
"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": [
|
||||
{
|
||||
"offset": 0,
|
||||
@@ -8610,6 +8894,12 @@
|
||||
"value": "). See available rates below."
|
||||
}
|
||||
],
|
||||
"selectRate.alert.reservationPolicies": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "See rate details"
|
||||
}
|
||||
],
|
||||
"selectRate.availableRooms.all": [
|
||||
{
|
||||
"offset": 0,
|
||||
|
||||
@@ -231,6 +231,12 @@
|
||||
"value": "Erityistoiveet (valinnainen)"
|
||||
}
|
||||
],
|
||||
"ancillaries.label.quantity": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Määrä"
|
||||
}
|
||||
],
|
||||
"ancillaries.unableToDisplayBreakfastPrices": [
|
||||
{
|
||||
"type": 0,
|
||||
@@ -249,6 +255,18 @@
|
||||
"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": [
|
||||
{
|
||||
"type": 0,
|
||||
@@ -953,6 +971,20 @@
|
||||
"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": [
|
||||
{
|
||||
"type": 0,
|
||||
@@ -1061,6 +1093,20 @@
|
||||
"value": "details"
|
||||
}
|
||||
],
|
||||
"bookingWidget.reward.readMore": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Lue lisää "
|
||||
},
|
||||
{
|
||||
"type": 1,
|
||||
"value": "reward"
|
||||
},
|
||||
{
|
||||
"type": 0,
|
||||
"value": " varaamisesta"
|
||||
}
|
||||
],
|
||||
"bookingWidget.reward.rewardNight": [
|
||||
{
|
||||
"type": 0,
|
||||
@@ -2514,6 +2560,44 @@
|
||||
"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": [
|
||||
{
|
||||
"type": 0,
|
||||
@@ -7586,6 +7670,58 @@
|
||||
"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": [
|
||||
{
|
||||
"offset": 0,
|
||||
@@ -8658,6 +8794,12 @@
|
||||
"value": "). Katso saatavilla olevat hinnat alla."
|
||||
}
|
||||
],
|
||||
"selectRate.alert.reservationPolicies": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Katso hinnan tiedot"
|
||||
}
|
||||
],
|
||||
"selectRate.availableRooms.all": [
|
||||
{
|
||||
"offset": 0,
|
||||
|
||||
@@ -231,6 +231,12 @@
|
||||
"value": "Spesielle ønsker (valgfritt)"
|
||||
}
|
||||
],
|
||||
"ancillaries.label.quantity": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Antall"
|
||||
}
|
||||
],
|
||||
"ancillaries.unableToDisplayBreakfastPrices": [
|
||||
{
|
||||
"type": 0,
|
||||
@@ -249,6 +255,18 @@
|
||||
"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": [
|
||||
{
|
||||
"type": 0,
|
||||
@@ -953,6 +971,16 @@
|
||||
"value": "Legg til kode"
|
||||
}
|
||||
],
|
||||
"bookingWidget.bookingCode.readMore": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Les mer om bruk av "
|
||||
},
|
||||
{
|
||||
"type": 1,
|
||||
"value": "codeVoucher"
|
||||
}
|
||||
],
|
||||
"bookingWidget.bookingCode.remember": [
|
||||
{
|
||||
"type": 0,
|
||||
@@ -1061,6 +1089,16 @@
|
||||
"value": "details"
|
||||
}
|
||||
],
|
||||
"bookingWidget.reward.readMore": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Les mer om bestilling med "
|
||||
},
|
||||
{
|
||||
"type": 1,
|
||||
"value": "reward"
|
||||
}
|
||||
],
|
||||
"bookingWidget.reward.rewardNight": [
|
||||
{
|
||||
"type": 0,
|
||||
@@ -2510,6 +2548,44 @@
|
||||
"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": [
|
||||
{
|
||||
"type": 0,
|
||||
@@ -7582,6 +7658,58 @@
|
||||
"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": [
|
||||
{
|
||||
"offset": 0,
|
||||
@@ -8654,6 +8782,12 @@
|
||||
"value": " ). Se tilgjengelige priser nedenfor."
|
||||
}
|
||||
],
|
||||
"selectRate.alert.reservationPolicies": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Se prisdetaljer"
|
||||
}
|
||||
],
|
||||
"selectRate.availableRooms.all": [
|
||||
{
|
||||
"offset": 0,
|
||||
|
||||
@@ -231,6 +231,12 @@
|
||||
"value": "Särskilda önskemål (valfritt)"
|
||||
}
|
||||
],
|
||||
"ancillaries.label.quantity": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Antal"
|
||||
}
|
||||
],
|
||||
"ancillaries.unableToDisplayBreakfastPrices": [
|
||||
{
|
||||
"type": 0,
|
||||
@@ -249,6 +255,18 @@
|
||||
"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": [
|
||||
{
|
||||
"type": 0,
|
||||
@@ -953,6 +971,16 @@
|
||||
"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": [
|
||||
{
|
||||
"type": 0,
|
||||
@@ -1061,6 +1089,16 @@
|
||||
"value": "details"
|
||||
}
|
||||
],
|
||||
"bookingWidget.reward.readMore": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Läs mer om bokning med "
|
||||
},
|
||||
{
|
||||
"type": 1,
|
||||
"value": "reward"
|
||||
}
|
||||
],
|
||||
"bookingWidget.reward.rewardNight": [
|
||||
{
|
||||
"type": 0,
|
||||
@@ -2510,6 +2548,44 @@
|
||||
"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": [
|
||||
{
|
||||
"type": 0,
|
||||
@@ -3845,7 +3921,7 @@
|
||||
"findMyBooking.findYourStay": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Hitta ditt hotell"
|
||||
"value": "Hitta din bokning"
|
||||
}
|
||||
],
|
||||
"findMyBooking.manageBooking": [
|
||||
@@ -7566,6 +7642,58 @@
|
||||
"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": [
|
||||
{
|
||||
"offset": 0,
|
||||
@@ -8626,6 +8754,12 @@
|
||||
"value": "). Se tillgängliga priser nedan."
|
||||
}
|
||||
],
|
||||
"selectRate.alert.reservationPolicies": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Se bokningsvillkor"
|
||||
}
|
||||
],
|
||||
"selectRate.availableRooms.all": [
|
||||
{
|
||||
"offset": 0,
|
||||
|
||||
@@ -22,13 +22,13 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@formatjs/intl": "^3.1.6",
|
||||
"@netlify/plugin-nextjs": "^5.15.1",
|
||||
"@netlify/plugin-nextjs": "^5.15.7",
|
||||
"@scandic-hotels/booking-flow": "workspace:*",
|
||||
"@scandic-hotels/design-system": "workspace:*",
|
||||
"@scandic-hotels/tracking": "workspace:*",
|
||||
"@scandic-hotels/trpc": "workspace:*",
|
||||
"@sentry/nextjs": "^10.33.0",
|
||||
"@swc/plugin-formatjs": "^3.2.2",
|
||||
"@swc/plugin-formatjs": "^8.1.0",
|
||||
"@t3-oss/env-nextjs": "^0.13.4",
|
||||
"@tanstack/react-query": "^5.75.5",
|
||||
"@tanstack/react-query-devtools": "^5.75.5",
|
||||
@@ -36,11 +36,11 @@
|
||||
"@trpc/server": "^11.1.2",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"iron-session": "^8.0.4",
|
||||
"next": "16.0.10",
|
||||
"next": "16.1.6",
|
||||
"next-auth": "5.0.0-beta.29",
|
||||
"react": "19.2.1",
|
||||
"react": "19.2.4",
|
||||
"react-aria-components": "1.8.0",
|
||||
"react-dom": "19.2.1",
|
||||
"react-dom": "19.2.4",
|
||||
"react-intl": "^7.1.11",
|
||||
"server-only": "^0.0.1",
|
||||
"usehooks-ts": "3.1.1",
|
||||
|
||||
@@ -52,3 +52,4 @@ DTMC_ENTRA_ID_CLIENT=""
|
||||
DTMC_ENTRA_ID_ISSUER=""
|
||||
DTMC_ENTRA_ID_SECRET=""
|
||||
|
||||
NEXT_PUBLIC_NEW_POINTCLAIMS="true"
|
||||
|
||||
@@ -16,6 +16,18 @@ yarn workspace @scandic-hotels/design-system build
|
||||
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.
|
||||
|
||||
### Caching
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
|
||||
.layout {
|
||||
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;
|
||||
min-height: 100dvh;
|
||||
max-width: var(--max-width-page);
|
||||
|
||||
@@ -1,12 +1,7 @@
|
||||
import { TrackingSDK } from "@scandic-hotels/tracking/TrackingSDK"
|
||||
import {
|
||||
getEurobonusMembership,
|
||||
scandicMembershipTypes,
|
||||
} from "@scandic-hotels/trpc/routers/user/helpers"
|
||||
import { getEurobonusMembership } from "@scandic-hotels/trpc/routers/user/helpers"
|
||||
|
||||
import { env } from "@/env/server"
|
||||
import {
|
||||
getBasicProfileSafely,
|
||||
getProfileSafely,
|
||||
getProfilingConsent,
|
||||
} from "@/lib/trpc/memoizedRequests"
|
||||
@@ -26,15 +21,7 @@ type MyPagesLayoutProps = React.PropsWithChildren<{
|
||||
breadcrumbs: React.ReactNode
|
||||
}>
|
||||
|
||||
export default async function MyPagesLayout(props: MyPagesLayoutProps) {
|
||||
if (env.ENABLE_PROFILE_CONSENT) {
|
||||
return <MyPagesLayoutWithConsent {...props} />
|
||||
}
|
||||
|
||||
return <MyPagesLayoutBase {...props} />
|
||||
}
|
||||
|
||||
async function MyPagesLayoutWithConsent({
|
||||
export default async function MyPagesLayout({
|
||||
breadcrumbs,
|
||||
children,
|
||||
}: MyPagesLayoutProps) {
|
||||
@@ -84,25 +71,3 @@ async function MyPagesLayoutWithConsent({
|
||||
</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 { env } from "@/env/server"
|
||||
import { getProfile } from "@/lib/trpc/memoizedRequests"
|
||||
import { serverClient } from "@/lib/trpc/server"
|
||||
|
||||
import { ProfilingConsent } from "@/components/Forms/ProfilingConsent"
|
||||
import { getLang } from "@/i18n/serverContext"
|
||||
|
||||
import styles from "./page.module.css"
|
||||
|
||||
export default async function ProfilingConsentSlot() {
|
||||
const lang = await getLang()
|
||||
|
||||
if (!env.ENABLE_PROFILE_CONSENT) {
|
||||
redirect(profile[lang])
|
||||
}
|
||||
|
||||
const caller = await serverClient()
|
||||
const accountPage = await caller.contentstack.accountPage.get()
|
||||
const user = await getProfile()
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
.layout {
|
||||
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;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
.layout {
|
||||
font-family: var(--typography-Body-Regular-fontFamily);
|
||||
font-family:
|
||||
var(--Body-Paragraph-Font-family), var(--Body-Paragraph-Font-fallback);
|
||||
}
|
||||
|
||||
+1
-2
@@ -5,6 +5,7 @@ import { useTransition } from "react"
|
||||
import { FormProvider, useForm } from "react-hook-form"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { sasPartnershipTermsAndConditions } from "@scandic-hotels/common/constants/routes/customerService"
|
||||
import { profileEdit } from "@scandic-hotels/common/constants/routes/myPages"
|
||||
import { Button } from "@scandic-hotels/design-system/Button"
|
||||
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 { Typography } from "@scandic-hotels/design-system/Typography"
|
||||
|
||||
import { sasPartnershipTermsAndConditions } from "@/constants/webHrefs"
|
||||
|
||||
import styles from "./link-sas.module.css"
|
||||
|
||||
import type { LangParams } from "@/types/params"
|
||||
|
||||
+19
-23
@@ -1,9 +1,8 @@
|
||||
import { redirect } from "next/navigation"
|
||||
import { z } from "zod"
|
||||
|
||||
import Footnote from "@scandic-hotels/design-system/Footnote"
|
||||
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 { env } from "@/env/server"
|
||||
@@ -94,27 +93,24 @@ export default async function SASxScandicLoginPage(
|
||||
{intentDescriptions[parsedParams.intent]}
|
||||
</p>
|
||||
</Typography>
|
||||
<Footnote textAlign="center">
|
||||
{intl.formatMessage(
|
||||
{
|
||||
id: "linkEuroBonusAccount.manualRedirectLinkMessage",
|
||||
defaultMessage:
|
||||
"If you are not redirected automatically, please <loginLink>click here</loginLink>.",
|
||||
},
|
||||
{
|
||||
loginLink: (str) => (
|
||||
<Link
|
||||
href={loginLink}
|
||||
color="red"
|
||||
size="tiny"
|
||||
textDecoration="underline"
|
||||
>
|
||||
{str}
|
||||
</Link>
|
||||
),
|
||||
}
|
||||
)}
|
||||
</Footnote>
|
||||
<Typography variant="Body/Supporting text (caption)/smRegular">
|
||||
<p style={{ textAlign: "center" }}>
|
||||
{intl.formatMessage(
|
||||
{
|
||||
id: "linkEuroBonusAccount.manualRedirectLinkMessage",
|
||||
defaultMessage:
|
||||
"If you are not redirected automatically, please <loginLink>click here</loginLink>.",
|
||||
},
|
||||
{
|
||||
loginLink: (str) => (
|
||||
<TextLink typography="Link/sm" href={loginLink}>
|
||||
{str}
|
||||
</TextLink>
|
||||
),
|
||||
}
|
||||
)}
|
||||
</p>
|
||||
</Typography>
|
||||
</SASModal>
|
||||
)
|
||||
}
|
||||
|
||||
+2
-1
@@ -42,7 +42,8 @@
|
||||
width: 34px;
|
||||
height: 0px;
|
||||
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-radius: var(--Corner-Radius-md);
|
||||
text-align: center;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
.layout {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -88,6 +88,12 @@ body:has([data-booking-widget-open="true"]) #kindly-chat-api {
|
||||
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) {
|
||||
:root {
|
||||
--max-width-single-spacing: var(--Layout-Tablet-Margin-Margin-min);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
.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 { getAlertPhoneContactData } from "@scandic-hotels/trpc/routers/contentstack/base/utils"
|
||||
|
||||
import { serverClient } from "@/lib/trpc/server"
|
||||
|
||||
import type { AlertBlock } from "@scandic-hotels/trpc/types/blocks"
|
||||
|
||||
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) {
|
||||
return null
|
||||
}
|
||||
|
||||
return <Alert {...alert} />
|
||||
const phoneContact =
|
||||
alert.phoneContact && contactConfig
|
||||
? getAlertPhoneContactData(alert, contactConfig)
|
||||
: null
|
||||
|
||||
return (
|
||||
<Alert
|
||||
{...alert}
|
||||
phoneContact={phoneContact}
|
||||
sidepeekCtaText={alert.sidepeekButton?.cta_text}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
-4
@@ -16,18 +16,14 @@
|
||||
|
||||
.iconTh {
|
||||
padding: var(--Space-x5) var(--Space-x2) var(--Space-x2);
|
||||
font-weight: var(--typography-Caption-Regular-fontWeight);
|
||||
vertical-align: bottom;
|
||||
}
|
||||
|
||||
.summaryTh {
|
||||
font-size: var(--typography-Caption-Regular-fontSize);
|
||||
font-weight: var(--typography-Caption-Regular-fontWeight);
|
||||
padding: 0 var(--Space-x2) var(--Space-x2);
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.select {
|
||||
font-weight: var(--typography-Caption-Regular-fontWeight);
|
||||
padding: 0 var(--Space-x2) var(--Space-x2);
|
||||
}
|
||||
|
||||
+9
-5
@@ -1,3 +1,5 @@
|
||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||
|
||||
import MembershipLevelIcon from "@/components/Levels/Icon"
|
||||
|
||||
import LevelSummary from "../../LevelSummary"
|
||||
@@ -37,12 +39,14 @@ export default function DesktopHeader({
|
||||
<th />
|
||||
{levels.map((level, idx) => {
|
||||
return (
|
||||
<th
|
||||
key={"summary" + level.level_id + idx}
|
||||
className={styles.summaryTh}
|
||||
<Typography
|
||||
variant="Body/Supporting text (caption)/smRegular"
|
||||
key={"name" + level.level_id + idx}
|
||||
>
|
||||
<LevelSummary level={level} />
|
||||
</th>
|
||||
<th className={styles.summaryTh}>
|
||||
<LevelSummary level={level} />
|
||||
</th>
|
||||
</Typography>
|
||||
)
|
||||
})}
|
||||
</tr>
|
||||
|
||||
+6
-4
@@ -82,10 +82,12 @@ function RewardTableHeader({ name, description }: RewardTableHeaderProps) {
|
||||
</span>
|
||||
</hgroup>
|
||||
</summary>
|
||||
<p
|
||||
className={styles.rewardDescription}
|
||||
dangerouslySetInnerHTML={{ __html: description }}
|
||||
/>
|
||||
<Typography variant="Body/Supporting text (caption)/smRegular">
|
||||
<p
|
||||
className={styles.rewardDescription}
|
||||
dangerouslySetInnerHTML={{ __html: description }}
|
||||
/>
|
||||
</Typography>
|
||||
</details>
|
||||
)
|
||||
}
|
||||
|
||||
-3
@@ -15,14 +15,11 @@
|
||||
}
|
||||
|
||||
.td {
|
||||
font-size: var(--typography-Footnote-Regular-fontSize);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.rewardTh {
|
||||
padding: var(--Space-x3) var(--Space-x2);
|
||||
font-size: var(--typography-Caption-Regular-fontSize);
|
||||
font-weight: var(--typography-Caption-Regular-fontWeight);
|
||||
}
|
||||
|
||||
.details[open] .chevron {
|
||||
|
||||
+5
-1
@@ -1,5 +1,7 @@
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||
|
||||
import styles from "./levelSummary.module.css"
|
||||
|
||||
import type { LevelSummaryProps } from "@/types/components/overviewTable"
|
||||
@@ -32,7 +34,9 @@ export default function LevelSummary({
|
||||
|
||||
return (
|
||||
<div className={styles.levelSummary}>
|
||||
<span className={styles.levelRequirements}>{pointsMsg}</span>
|
||||
<Typography variant="Label/xsRegular">
|
||||
<span className={styles.levelRequirements}>{pointsMsg}</span>
|
||||
</Typography>
|
||||
{showDescription && (
|
||||
<p className={styles.levelSummaryText}>{level.description}</p>
|
||||
)}
|
||||
|
||||
+2
-13
@@ -8,16 +8,14 @@
|
||||
|
||||
.levelRequirements {
|
||||
border-radius: var(--Corner-Radius-md);
|
||||
background-color: var(--Scandic-Brand-Pale-Peach);
|
||||
color: var(--Scandic-Peach-80);
|
||||
background-color: var(--Surface-Brand-Primary-1-Default);
|
||||
color: var(--Text-Interactive-Secondary);
|
||||
padding: var(--Space-x05) var(--Space-x1);
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.levelSummaryText {
|
||||
font-size: var(--typography-Caption-Regular-fontSize);
|
||||
line-height: var(--typography-Body-Regular-lineHeight);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@@ -26,12 +24,3 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
+6
-4
@@ -27,10 +27,12 @@ export default function RewardCard({
|
||||
</span>
|
||||
</hgroup>
|
||||
</summary>
|
||||
<p
|
||||
className={styles.rewardCardDescription}
|
||||
dangerouslySetInnerHTML={{ __html: description }}
|
||||
/>
|
||||
<Typography variant="Body/Supporting text (caption)/smRegular">
|
||||
<p
|
||||
className={styles.rewardCardDescription}
|
||||
dangerouslySetInnerHTML={{ __html: description }}
|
||||
/>
|
||||
</Typography>
|
||||
</details>
|
||||
</div>
|
||||
<div className={styles.rewardComparison}>
|
||||
|
||||
-2
@@ -12,8 +12,6 @@
|
||||
}
|
||||
|
||||
.rewardCardDescription {
|
||||
font-size: var(--typography-Caption-Regular-fontSize);
|
||||
line-height: 150%;
|
||||
padding-right: var(--Space-x4);
|
||||
}
|
||||
|
||||
|
||||
+4
-3
@@ -1,6 +1,7 @@
|
||||
import { Minus } from "react-feather"
|
||||
|
||||
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
|
||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||
|
||||
import styles from "./rewardValue.module.css"
|
||||
|
||||
@@ -21,8 +22,8 @@ export default function RewardValue({ reward }: RewardValueProps) {
|
||||
)
|
||||
}
|
||||
return (
|
||||
<div className={styles.rewardValueContainer}>
|
||||
<span className={styles.rewardValue}>{reward.value}</span>
|
||||
</div>
|
||||
<Typography variant="Body/Paragraph/mdBold">
|
||||
<div className={styles.rewardValueContainer}>{reward.value}</div>
|
||||
</Typography>
|
||||
)
|
||||
}
|
||||
|
||||
-11
@@ -7,17 +7,6 @@
|
||||
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 {
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
+430
@@ -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 can’t 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>
|
||||
)
|
||||
}
|
||||
+97
@@ -6,3 +6,100 @@
|
||||
gap: var(--Space-x2);
|
||||
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"
|
||||
|
||||
import { useEffect, useState } from "react"
|
||||
import { Dialog } from "react-aria-components"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { Button } from "@scandic-hotels/design-system/Button"
|
||||
import ButtonLink from "@scandic-hotels/design-system/ButtonLink"
|
||||
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 { missingPoints } from "@/constants/missingPointsHrefs"
|
||||
import { env } from "@/env/client"
|
||||
|
||||
import useLang from "@/hooks/useLang"
|
||||
|
||||
import { ClaimPointsWizard } from "./ClaimPointsWizard"
|
||||
|
||||
import styles from "./claimPoints.module.css"
|
||||
|
||||
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'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 lang = useLang()
|
||||
|
||||
|
||||
+49
-31
@@ -26,14 +26,14 @@ export function PointTransactionRow({
|
||||
focusRef: React.Ref<HTMLAnchorElement>
|
||||
}) {
|
||||
const intl = useIntl()
|
||||
|
||||
const { confirmationNumber, bookingUrl, checkinDate, awardPoints } =
|
||||
const { confirmationNumber, bookingUrl, transactionDate, awardPoints } =
|
||||
transaction.attributes
|
||||
const balfwd = confirmationNumber === BALFWD
|
||||
const nonTransactional = confirmationNumber === NON_TRANSACTIONAL
|
||||
|
||||
const day = checkinDate.split("-")[2].replace(/^0/, "")
|
||||
const month = dt(checkinDate.split("-")[1]).locale(lang).format("MMM")
|
||||
const date = dt.utc(transactionDate).locale(lang)
|
||||
const day = date.format("D")
|
||||
const month = date.format("MMM")
|
||||
|
||||
const formattedPoints = intl.formatNumber(Math.abs(awardPoints))
|
||||
const calculatedPoints =
|
||||
@@ -41,7 +41,12 @@ export function PointTransactionRow({
|
||||
? 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)
|
||||
|
||||
@@ -93,26 +98,29 @@ export function PointTransactionRow({
|
||||
|
||||
function getDescription(transaction: Transaction, intl: IntlShape) {
|
||||
const hotelInformation = transaction.attributes.hotelInformation
|
||||
const balfwd = transaction.attributes.confirmationNumber === BALFWD
|
||||
const nonTransactional =
|
||||
const isBalfwd = transaction.attributes.confirmationNumber === BALFWD
|
||||
const isNonTransactional =
|
||||
transaction.attributes.confirmationNumber === NON_TRANSACTIONAL
|
||||
|
||||
if (isNonTransactional && transaction.attributes.nights === 0) {
|
||||
return intl.formatMessage({
|
||||
id: "earnAndBurn.journeyTable.pointsActivity",
|
||||
defaultMessage: "Point activity",
|
||||
})
|
||||
}
|
||||
switch (transaction.type) {
|
||||
case Transactions.rewardType.stay:
|
||||
return nonTransactional && transaction.attributes.nights === 0
|
||||
? intl.formatMessage({
|
||||
id: "myPoints.pointTransactions.pointsActivity",
|
||||
defaultMessage: "Point activity",
|
||||
})
|
||||
: hotelInformation?.name
|
||||
? intl.formatMessage(
|
||||
{
|
||||
id: "myPoints.pointTransactions.stayAt",
|
||||
defaultMessage: "Stay at {hotelName}",
|
||||
},
|
||||
{ hotelName: hotelInformation?.name }
|
||||
)
|
||||
: ""
|
||||
|
||||
if (hotelInformation?.name) {
|
||||
return intl.formatMessage(
|
||||
{
|
||||
id: "earnAndBurn.journeyTable.stayAt",
|
||||
defaultMessage: "Stay at {hotelName}",
|
||||
},
|
||||
{ hotelName: hotelInformation.name }
|
||||
)
|
||||
} else {
|
||||
return ""
|
||||
}
|
||||
case Transactions.rewardType.stayAdj:
|
||||
if (transaction.attributes.hotelOperaId === "ORS") {
|
||||
return intl.formatMessage({
|
||||
@@ -120,42 +128,52 @@ function getDescription(transaction: Transaction, intl: IntlShape) {
|
||||
defaultMessage: "Former Scandic Hotel",
|
||||
})
|
||||
}
|
||||
if (balfwd) {
|
||||
if (isBalfwd) {
|
||||
return intl.formatMessage({
|
||||
id: "myPoints.pointTransactions.pointsEarnedPriorMay2021",
|
||||
id: "earnAndBurn.journeyTable.pointsEarnedPriorMay2021",
|
||||
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:
|
||||
return intl.formatMessage({
|
||||
id: "myPoints.pointTransactions.extrasToBooking",
|
||||
id: "earnAndBurn.journeyTable.extrasToBooking",
|
||||
defaultMessage: "Extras to your booking",
|
||||
})
|
||||
|
||||
case Transactions.rewardType.enrollment:
|
||||
return intl.formatMessage({
|
||||
id: "myPoints.pointTransactions.signUpBonus",
|
||||
id: "earnAndBurn.journeyTable.signUpBonus",
|
||||
defaultMessage: "Sign up bonus",
|
||||
})
|
||||
|
||||
case Transactions.rewardType.mastercard_points:
|
||||
return intl.formatMessage({
|
||||
id: "myPoints.pointTransactions.scandicFriendsMastercard",
|
||||
id: "earnAndBurn.journeyTable.scandicFriendsMastercard",
|
||||
defaultMessage: "Scandic Friends Mastercard",
|
||||
})
|
||||
|
||||
case Transactions.rewardType.tui_points:
|
||||
return intl.formatMessage({
|
||||
id: "myPoints.pointTransactions.tuiPoints",
|
||||
id: "earnAndBurn.journeyTable.tuiPoints",
|
||||
defaultMessage: "TUI Points",
|
||||
})
|
||||
|
||||
case Transactions.rewardType.pointShop:
|
||||
return intl.formatMessage({
|
||||
id: "myPoints.pointTransactions.pointShop",
|
||||
id: "earnAndBurn.journeyTable.pointShop",
|
||||
defaultMessage: "Scandic Friends Point Shop",
|
||||
})
|
||||
default:
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
+3
-2
@@ -2,6 +2,7 @@
|
||||
import { Fragment, useCallback, useRef } from "react"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { dt } from "@scandic-hotels/common/dt"
|
||||
import useStickyPosition from "@scandic-hotels/common/hooks/useStickyPosition"
|
||||
import { StickyElementNameEnum } from "@scandic-hotels/common/stores/sticky-position"
|
||||
import { Divider } from "@scandic-hotels/design-system/Divider"
|
||||
@@ -40,9 +41,9 @@ export function PointTransactionList() {
|
||||
.flatMap((page) => page.data)
|
||||
|
||||
const groupedTransactions =
|
||||
transactions?.reduce<Record<number, typeof transactions>>(
|
||||
transactions?.reduce<Record<string, typeof transactions>>(
|
||||
(acc, transaction) => {
|
||||
const year = new Date(transaction.attributes.checkinDate).getFullYear()
|
||||
const year = dt.utc(transaction.attributes.transactionDate).year()
|
||||
if (!acc[year]) acc[year] = []
|
||||
acc[year].push(transaction)
|
||||
return acc
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { env } from "@/env/server"
|
||||
|
||||
import SignupForm from "@/components/Forms/Signup"
|
||||
|
||||
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({
|
||||
dynamic_content,
|
||||
}: SignupFormWrapperProps) {
|
||||
return (
|
||||
<SignupForm
|
||||
{...dynamic_content}
|
||||
enableProfileConsent={env.ENABLE_PROFILE_CONSENT}
|
||||
/>
|
||||
)
|
||||
return <SignupForm {...dynamic_content} />
|
||||
}
|
||||
|
||||
-1
@@ -33,7 +33,6 @@
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* Styles for new empty upcoming stays design */
|
||||
.emptyUpcomingStaysContainer {
|
||||
display: flex;
|
||||
padding: var(--Space-x6);
|
||||
|
||||
+5
-1
@@ -10,6 +10,7 @@ import { useMediaQuery } from "usehooks-ts"
|
||||
|
||||
import { useMarkerHover } from "@scandic-hotels/common/hooks/map/useMarkerHover"
|
||||
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"
|
||||
|
||||
@@ -79,7 +80,10 @@ export default function CityClusterMarker({
|
||||
})}
|
||||
anchorPoint={AdvancedMarkerAnchorPoint.CENTER}
|
||||
>
|
||||
<span className={styles.count}>{sizeAsText}</span>
|
||||
<Typography variant="Title/Subtitle/md">
|
||||
<span>{sizeAsText}</span>
|
||||
</Typography>
|
||||
|
||||
{isDesktop && isHovered ? (
|
||||
<InfoWindow
|
||||
position={position}
|
||||
|
||||
-6
@@ -20,9 +20,3 @@
|
||||
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.rangeEnd,
|
||||
td.rangeStart {
|
||||
font-family: var(--typography-Body-Bold-fontFamily);
|
||||
font-size: var(--typography-Body-Bold-fontSize);
|
||||
font-weight: 500;
|
||||
letter-spacing: var(--typography-Body-Bold-letterSpacing);
|
||||
line-height: var(--typography-Body-Bold-lineHeight);
|
||||
text-decoration: var(--typography-Body-Bold-textDecoration);
|
||||
font-family:
|
||||
var(--Body-Paragraph-Font-family), var(--Body-Paragraph-Font-fallback);
|
||||
font-size: var(--Body-Paragraph-Size);
|
||||
font-weight: var(--Body-Paragraph-Font-weight-2);
|
||||
letter-spacing: var(--Body-Paragraph-Letter-spacing);
|
||||
line-height: 1.5;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
td.rangeEnd,
|
||||
@@ -90,14 +91,16 @@ td.day[data-outside="true"] ~ td.day[data-disabled="true"] button.dayButton,
|
||||
}
|
||||
|
||||
.weekDay {
|
||||
color: var(--UI-Text-Placeholder);
|
||||
font-family: var(--typography-Footnote-Labels-fontFamily);
|
||||
font-size: var(--typography-Footnote-Labels-fontSize);
|
||||
font-weight: var(--typography-Footnote-Labels-fontWeight);
|
||||
letter-spacing: var(--typography-Footnote-Labels-letterSpacing);
|
||||
line-height: var(--typography-Footnote-Labels-lineHeight);
|
||||
text-decoration: var(--typography-Footnote-Labels-textDecoration);
|
||||
text-transform: uppercase;
|
||||
color: var(--Text-Tertiary);
|
||||
font-family:
|
||||
var(--Title-Overline-sm-Font-family), var(--Title-Overline-sm-Font-fallback);
|
||||
font-size: var(--Title-Overline-sm-Size);
|
||||
font-style: normal;
|
||||
font-weight: var(--Title-Overline-sm-Font-weight);
|
||||
line-height: 1.5;
|
||||
letter-spacing: var(--Title-Overline-sm-Letter-spacing);
|
||||
text-transform: var(--Title-Overline-sm-Text-Transform);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.footer {
|
||||
|
||||
@@ -89,12 +89,13 @@ div.months {
|
||||
td.day,
|
||||
td.rangeEnd,
|
||||
td.rangeStart {
|
||||
font-family: var(--typography-Body-Bold-fontFamily);
|
||||
font-size: var(--typography-Body-Bold-fontSize);
|
||||
font-weight: 500;
|
||||
letter-spacing: var(--typography-Body-Bold-letterSpacing);
|
||||
line-height: var(--typography-Body-Bold-lineHeight);
|
||||
text-decoration: var(--typography-Body-Bold-textDecoration);
|
||||
font-family:
|
||||
var(--Body-Paragraph-Font-family), var(--Body-Paragraph-Font-fallback);
|
||||
font-size: var(--Body-Paragraph-Size);
|
||||
font-weight: var(--Body-Paragraph-Font-weight-2);
|
||||
letter-spacing: var(--Body-Paragraph-Letter-spacing);
|
||||
line-height: 1.5;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
td.rangeEnd,
|
||||
@@ -156,14 +157,16 @@ td.day[data-outside="true"] ~ td.day[data-disabled="true"] button.dayButton,
|
||||
}
|
||||
|
||||
.weekDay {
|
||||
color: var(--UI-Text-Placeholder);
|
||||
font-family: var(--typography-Caption-Labels-fontFamily);
|
||||
font-size: var(--typography-Caption-Labels-fontSize);
|
||||
font-weight: var(--typography-Caption-Labels-fontWeight);
|
||||
letter-spacing: var(--typography-Caption-Labels-letterSpacing);
|
||||
line-height: var(--typography-Caption-Labels-lineHeight);
|
||||
text-decoration: var(--typography-Caption-Labels-textDecoration);
|
||||
text-transform: uppercase;
|
||||
color: var(--Text-Tertiary);
|
||||
font-family:
|
||||
var(--Title-Overline-sm-Font-family), var(--Title-Overline-sm-Font-fallback);
|
||||
font-size: var(--Title-Overline-sm-Size);
|
||||
font-style: normal;
|
||||
font-weight: var(--Title-Overline-sm-Font-weight);
|
||||
line-height: 1.5;
|
||||
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) {
|
||||
|
||||
@@ -107,7 +107,6 @@ export default function Form({ user }: EditFormProps) {
|
||||
} else {
|
||||
router.push(profile[lang])
|
||||
}
|
||||
router.refresh() // Can be removed on NextJs 15
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
@@ -48,14 +48,9 @@ import styles from "./form.module.css"
|
||||
|
||||
interface SignUpFormProps {
|
||||
title: string
|
||||
enableProfileConsent?: boolean
|
||||
}
|
||||
|
||||
export default function SignupForm({
|
||||
title,
|
||||
// Handled as a prop rather than a client env var due to limits in Netlify env var size.
|
||||
enableProfileConsent = false,
|
||||
}: SignUpFormProps) {
|
||||
export default function SignupForm({ title }: SignUpFormProps) {
|
||||
const intl = useIntl()
|
||||
const router = useRouter()
|
||||
const lang = useLang()
|
||||
@@ -140,7 +135,7 @@ export default function SignupForm({
|
||||
|
||||
return (
|
||||
<div className={styles.formWrapper}>
|
||||
{enableProfileConsent && <ProfilingConsentModalReadOnly />}
|
||||
<ProfilingConsentModalReadOnly />
|
||||
{title ? (
|
||||
<Typography variant="Title/md">
|
||||
<h2>{title}</h2>
|
||||
@@ -293,41 +288,39 @@ export default function SignupForm({
|
||||
/>
|
||||
</section>
|
||||
|
||||
{enableProfileConsent && (
|
||||
<section className={styles.personalization}>
|
||||
<header>
|
||||
<Typography variant="Title/Subtitle/md">
|
||||
<h3>
|
||||
{intl.formatMessage({
|
||||
id: "signup.UnlockYourPersonalizedExperience",
|
||||
defaultMessage: "Unlock your personalized experience!",
|
||||
})}
|
||||
</h3>
|
||||
</Typography>
|
||||
</header>
|
||||
<Checkbox
|
||||
name="profilingConsent"
|
||||
registerOptions={{ required: true }}
|
||||
>
|
||||
{intl.formatMessage({
|
||||
id: "signup.yesConsent",
|
||||
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.",
|
||||
})}
|
||||
</Checkbox>
|
||||
<TextLinkButton
|
||||
typography="Link/sm"
|
||||
color="Primary"
|
||||
className={styles.personalizationButton}
|
||||
onClick={openPersonalizationModal}
|
||||
>
|
||||
{intl.formatMessage({
|
||||
id: "signup.ReadMoreAboutPersonalization",
|
||||
defaultMessage: "Read more about personalization at Scandic",
|
||||
})}
|
||||
</TextLinkButton>
|
||||
</section>
|
||||
)}
|
||||
<section className={styles.personalization}>
|
||||
<header>
|
||||
<Typography variant="Title/Subtitle/md">
|
||||
<h3>
|
||||
{intl.formatMessage({
|
||||
id: "signup.UnlockYourPersonalizedExperience",
|
||||
defaultMessage: "Unlock your personalized experience!",
|
||||
})}
|
||||
</h3>
|
||||
</Typography>
|
||||
</header>
|
||||
<Checkbox
|
||||
name="profilingConsent"
|
||||
registerOptions={{ required: true }}
|
||||
>
|
||||
{intl.formatMessage({
|
||||
id: "signup.yesConsent",
|
||||
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.",
|
||||
})}
|
||||
</Checkbox>
|
||||
<TextLinkButton
|
||||
typography="Link/sm"
|
||||
color="Primary"
|
||||
className={styles.personalizationButton}
|
||||
onClick={openPersonalizationModal}
|
||||
>
|
||||
{intl.formatMessage({
|
||||
id: "signup.ReadMoreAboutPersonalization",
|
||||
defaultMessage: "Read more about personalization at Scandic",
|
||||
})}
|
||||
</TextLinkButton>
|
||||
</section>
|
||||
|
||||
<section className={styles.terms}>
|
||||
<header>
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
.menuButton {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: var(--Space-x05);
|
||||
cursor: pointer;
|
||||
width: 100%;
|
||||
background-color: transparent;
|
||||
color: var(--Text-Interactive-Default);
|
||||
border-width: 0;
|
||||
padding: var(--Space-x05) 0;
|
||||
@layer component {
|
||||
.menuButton {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: var(--Space-x05);
|
||||
cursor: pointer;
|
||||
width: 100%;
|
||||
background-color: transparent;
|
||||
color: var(--Text-Interactive-Default);
|
||||
border-width: 0;
|
||||
padding: var(--Space-x05) 0;
|
||||
|
||||
&.loading {
|
||||
cursor: progress;
|
||||
&.loading {
|
||||
cursor: progress;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
"use client"
|
||||
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { usePathname, useRouter } from "next/navigation"
|
||||
import { useState } from "react"
|
||||
@@ -25,7 +26,7 @@ import ModifyContact from "../ModifyContact"
|
||||
|
||||
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 {
|
||||
type ModifyContactSchema,
|
||||
@@ -34,9 +35,9 @@ import {
|
||||
import { MODAL_STEPS } from "@/types/components/hotelReservation/myStay/myStay"
|
||||
import type { SafeUser } from "@/types/user"
|
||||
|
||||
interface GuestDetailsProps {
|
||||
type GuestDetailsProps = {
|
||||
refId: string
|
||||
guest: Guest
|
||||
guest: BookingConfirmation["booking"]["guest"]
|
||||
isCancelled: boolean
|
||||
user: SafeUser
|
||||
}
|
||||
@@ -76,6 +77,7 @@ export default function GuestDetails({
|
||||
const isFirstStep = currentStep === MODAL_STEPS.INITIAL
|
||||
|
||||
const isMemberBooking =
|
||||
!!user?.membership?.membershipNumber &&
|
||||
guest.membershipNumber === user?.membership?.membershipNumber
|
||||
|
||||
const updateGuest = trpc.booking.update.useMutation({
|
||||
@@ -196,7 +198,7 @@ export default function GuestDetails({
|
||||
{guest.firstName} {guest.lastName}
|
||||
</p>
|
||||
</Typography>
|
||||
{isMemberBooking && user.membership && (
|
||||
{isMemberBooking && user?.membership && (
|
||||
<Typography variant="Body/Paragraph/mdRegular">
|
||||
<p className={styles.memberNumber} data-hj-suppress>
|
||||
{intl.formatMessage(
|
||||
|
||||
+1
@@ -23,6 +23,7 @@
|
||||
text-decoration-skip-ink: none;
|
||||
text-decoration-thickness: auto;
|
||||
text-underline-offset: auto;
|
||||
text-align: center;
|
||||
text-underline-position: from-font;
|
||||
}
|
||||
|
||||
|
||||
+8
-2
@@ -4,6 +4,7 @@ import { useIntl } from "react-intl"
|
||||
|
||||
import { sumPackages } from "@scandic-hotels/booking-flow/utils/SelectRate"
|
||||
import { CurrencyEnum } from "@scandic-hotels/common/constants/currency"
|
||||
import { dt } from "@scandic-hotels/common/dt"
|
||||
import { formatPrice } from "@scandic-hotels/common/utils/numberFormatting"
|
||||
import { trpc } from "@scandic-hotels/trpc/client"
|
||||
|
||||
@@ -66,9 +67,14 @@ export default function Steps({ closeModal }: ChangeDatesStepsProps) {
|
||||
|
||||
setDates({ fromDate, toDate })
|
||||
|
||||
const numberOfNights = dt(toDate).diff(dt(fromDate), "days")
|
||||
|
||||
const pkgsSum = sumPackages(packages)
|
||||
const extraPrice =
|
||||
pkgsSum.price + ((breakfast && breakfast.localPrice.totalPrice) || 0)
|
||||
const breakfastPrice = !!breakfast
|
||||
? breakfast.localPrice.price * numberOfNights
|
||||
: 0
|
||||
|
||||
const extraPrice = pkgsSum.price + breakfastPrice
|
||||
if (isLoggedIn && "member" in data.product && data.product.member) {
|
||||
const { currency, pricePerStay } = data.product.member.localPrice
|
||||
setNewPrice(formatPrice(intl, pricePerStay + extraPrice, currency))
|
||||
|
||||
+22
-16
@@ -1,11 +1,11 @@
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { CancellationRuleEnum } from "@scandic-hotels/common/constants/booking"
|
||||
import { changeOrCancelDateFormat } from "@scandic-hotels/common/constants/dateFormats"
|
||||
import { dt } from "@scandic-hotels/common/dt"
|
||||
|
||||
import { useMyStayStore } from "@/stores/my-stay"
|
||||
|
||||
import { hasModifiableRate } from "@/components/HotelReservation/MyStay/utils"
|
||||
import useLang from "@/hooks/useLang"
|
||||
|
||||
import Row from "./Row"
|
||||
@@ -14,14 +14,19 @@ export default function ModifyBy() {
|
||||
const intl = useIntl()
|
||||
const lang = useLang()
|
||||
|
||||
const { checkInDate, isModifyable } = useMyStayStore((state) => ({
|
||||
checkInDate: state.bookedRoom.checkInDate,
|
||||
isModifyable: hasModifiableRate(
|
||||
state.bookedRoom.rateDefinition.cancellationRule
|
||||
),
|
||||
}))
|
||||
const { checkInDate, isFlexBooking, isChangeBooking } = useMyStayStore(
|
||||
(state) => ({
|
||||
checkInDate: state.bookedRoom.checkInDate,
|
||||
isFlexBooking:
|
||||
state.bookedRoom.rateDefinition.cancellationRule ===
|
||||
CancellationRuleEnum.CancellableBefore6PM,
|
||||
isChangeBooking:
|
||||
state.bookedRoom.rateDefinition.cancellationRule ===
|
||||
CancellationRuleEnum.Changeable,
|
||||
})
|
||||
)
|
||||
|
||||
if (!isModifyable) {
|
||||
if (!isFlexBooking && !isChangeBooking) {
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -38,14 +43,15 @@ export default function ModifyBy() {
|
||||
}
|
||||
)
|
||||
|
||||
return (
|
||||
<Row
|
||||
icon="refresh"
|
||||
text={text}
|
||||
title={intl.formatMessage({
|
||||
const title = isChangeBooking
|
||||
? intl.formatMessage({
|
||||
id: "booking.changeTitle",
|
||||
defaultMessage: "Change",
|
||||
})
|
||||
: intl.formatMessage({
|
||||
id: "booking.changeOrCancel",
|
||||
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 type { PriceType as _PriceType } from "@/types/components/hotelReservation/myStay/myStay"
|
||||
|
||||
export default function TotalPrice() {
|
||||
const { bookedRoom, totalPrice } = useMyStayStore((state) => ({
|
||||
bookedRoom: state.bookedRoom,
|
||||
totalPrice: state.totalPrice,
|
||||
}))
|
||||
const { bookedRoom, totalPrice, allRoomsAreCancelled } = useMyStayStore(
|
||||
(state) => ({
|
||||
bookedRoom: state.bookedRoom,
|
||||
totalPrice: state.totalPrice,
|
||||
allRoomsAreCancelled: state.allRoomsAreCancelled,
|
||||
})
|
||||
)
|
||||
|
||||
return (
|
||||
<Price
|
||||
isCancelled={bookedRoom.isCancelled}
|
||||
isCancelled={allRoomsAreCancelled}
|
||||
isMember={bookedRoom.rateDefinition.isMemberRate}
|
||||
price={totalPrice}
|
||||
/>
|
||||
|
||||
@@ -8,7 +8,7 @@ import accessBooking, {
|
||||
} from "./accessBooking"
|
||||
|
||||
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"
|
||||
|
||||
@@ -201,7 +201,7 @@ const badAuthenticatedUser: SafeUser = {
|
||||
profilingConsentUpdateDate: undefined,
|
||||
}
|
||||
|
||||
const loggedOutGuest: Guest = {
|
||||
const loggedOutGuest: BookingConfirmation["booking"]["guest"] = {
|
||||
email: "logged+out@scandichotels.com",
|
||||
firstName: "Anonymous",
|
||||
lastName: "Booking",
|
||||
@@ -210,7 +210,7 @@ const loggedOutGuest: Guest = {
|
||||
countryCode: "SE",
|
||||
}
|
||||
|
||||
const loggedInGuest: Guest = {
|
||||
const loggedInGuest: BookingConfirmation["booking"]["guest"] = {
|
||||
email: "logged+in@scandichotels.com",
|
||||
firstName: "Authenticated",
|
||||
lastName: "Booking",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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"
|
||||
|
||||
@@ -15,7 +15,7 @@ export {
|
||||
* Whether a request can access a confirmed booking or not.
|
||||
*/
|
||||
function accessBooking(
|
||||
guest: Guest,
|
||||
guest: BookingConfirmation["booking"]["guest"],
|
||||
lastName: string,
|
||||
user: SafeUser | null,
|
||||
cookie: string = ""
|
||||
|
||||
@@ -27,6 +27,7 @@ import AdditionalInfoForm from "@/components/HotelReservation/FindMyBooking/Addi
|
||||
import accessBooking, {
|
||||
ACCESS_GRANTED,
|
||||
ERROR_BAD_REQUEST,
|
||||
ERROR_NOT_FOUND,
|
||||
ERROR_UNAUTHORIZED,
|
||||
} from "@/components/HotelReservation/MyStay/accessBooking"
|
||||
import { Ancillaries } from "@/components/HotelReservation/MyStay/Ancillaries"
|
||||
@@ -74,39 +75,23 @@ async function MyStay(props: {
|
||||
notFound()
|
||||
}
|
||||
|
||||
const { confirmationNumber, lastName } = parseRefId(refId)
|
||||
|
||||
const isLoggedIn = await isLoggedInUser()
|
||||
|
||||
const cookieStore = await cookies()
|
||||
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) {
|
||||
bookingConfirmation = await findBooking(
|
||||
confirmationNumber,
|
||||
lastName,
|
||||
firstName,
|
||||
email
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<RenderAdditionalInfoForm
|
||||
confirmationNumber={confirmationNumber}
|
||||
lastName={lastName}
|
||||
/>
|
||||
)
|
||||
}
|
||||
} else {
|
||||
const { confirmationNumber, lastName } = parseRefId(refId)
|
||||
const isLoggedIn = await isLoggedInUser()
|
||||
const [{ error, bookingConfirmation }, user] = await Promise.all([
|
||||
getOrFindBookingConfirmation({
|
||||
refId,
|
||||
isLoggedIn,
|
||||
confirmationNumber,
|
||||
lastName,
|
||||
bv,
|
||||
}),
|
||||
getProfileSafely(),
|
||||
])
|
||||
|
||||
if (error === "MISSING_INFO") {
|
||||
return (
|
||||
<RenderAdditionalInfoForm
|
||||
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)
|
||||
|
||||
if (access === ACCESS_GRANTED) {
|
||||
const fromDate = dt(booking.checkInDate).format("YYYY-MM-DD")
|
||||
const toDate = dt(booking.checkOutDate).format("YYYY-MM-DD")
|
||||
|
||||
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
|
||||
return (
|
||||
<RenderFindMyBookingForm
|
||||
bv={bv}
|
||||
lastName={lastName}
|
||||
confirmationNumber={confirmationNumber}
|
||||
/>
|
||||
)
|
||||
}
|
||||
let ancillaryPackagesPromise = null
|
||||
if (booking.showAncillaries) {
|
||||
ancillaryPackagesPromise = getAncillaryPackages({
|
||||
default:
|
||||
const _exhaustiveCheck: never = code
|
||||
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,
|
||||
hotelId: hotel.operaId,
|
||||
toDate,
|
||||
})
|
||||
}
|
||||
: null
|
||||
|
||||
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 = {
|
||||
...bookingConfirmation,
|
||||
booking: {
|
||||
...bookingConfirmation.booking,
|
||||
guest: {
|
||||
...bookingConfirmation.booking.guest,
|
||||
email: maskValue.email(bookingConfirmation.booking.guest.email),
|
||||
phoneNumber: maskValue.phone(
|
||||
bookingConfirmation.booking.guest.phoneNumber ?? ""
|
||||
),
|
||||
},
|
||||
},
|
||||
} 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,
|
||||
}}
|
||||
return (
|
||||
<MyStayProvider
|
||||
bookingConfirmation={maskedBookingConfirmation}
|
||||
breakfastPackages={breakfastPackages}
|
||||
lang={lang}
|
||||
linkedReservationsPromise={linkedReservationsPromise}
|
||||
refId={booking.refId}
|
||||
roomCategories={roomCategories}
|
||||
savedCreditCards={savedCreditCards}
|
||||
isLoggedIn={!!user}
|
||||
>
|
||||
<main className={styles.main}>
|
||||
<div className={styles.imageContainer}>
|
||||
<div className={styles.blurOverlay} />
|
||||
{imageSrc && (
|
||||
<Image
|
||||
className={styles.image}
|
||||
src={imageSrc}
|
||||
alt={hotelWithFilteredAlerts.name}
|
||||
fill
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.content}>
|
||||
<div className={styles.headerContainer}>
|
||||
<Header
|
||||
cityName={hotelWithFilteredAlerts.cityName}
|
||||
name={hotelWithFilteredAlerts.name}
|
||||
/>
|
||||
<ReferenceCard />
|
||||
</div>
|
||||
</main>
|
||||
)
|
||||
} else {
|
||||
}
|
||||
}
|
||||
{booking.showAncillaries && ancillaryPackagesPromise && (
|
||||
<Ancillaries
|
||||
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({
|
||||
@@ -343,3 +346,75 @@ function RenderAdditionalInfoForm({
|
||||
</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)
|
||||
}
|
||||
|
||||
-7
@@ -4,13 +4,6 @@
|
||||
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-style: none;
|
||||
}
|
||||
|
||||
@@ -16,8 +16,8 @@
|
||||
cursor: pointer;
|
||||
height: 32px;
|
||||
width: 32px;
|
||||
font-size: var(--typography-Body-Bold-fontSize);
|
||||
font-weight: var(--typography-Body-Bold-fontWeight);
|
||||
font-size: var(--Body-Paragraph-Size);
|
||||
font-weight: var(--Body-Paragraph-Font-weight-2);
|
||||
padding: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { env } from "@/env/server"
|
||||
|
||||
import { getIntl } from "@/i18n"
|
||||
|
||||
import { Section } from "../Section"
|
||||
@@ -17,7 +15,7 @@ export async function CommunicationSettings() {
|
||||
})}
|
||||
>
|
||||
<EmailSlot />
|
||||
{env.ENABLE_PROFILE_CONSENT && <PersonalizationSlot />}
|
||||
<PersonalizationSlot />
|
||||
</Section>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||
|
||||
import { env } from "@/env/server"
|
||||
import { getProfile, getProfilingConsent } from "@/lib/trpc/memoizedRequests"
|
||||
|
||||
import { GetMainIconByCSIdentifier, userHasConsent } from "../utils"
|
||||
@@ -9,8 +8,6 @@ import { BannerButton } from "./Button"
|
||||
import styles from "./profilingConsentBanner.module.css"
|
||||
|
||||
export async function ProfilingConsentBanner() {
|
||||
if (!env.ENABLE_PROFILE_CONSENT) return null
|
||||
|
||||
const user = await getProfile()
|
||||
if (!user || userHasConsent(user?.profilingConsent)) return null
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# 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
|
||||
|
||||
@@ -121,11 +121,9 @@ Replace `<memberKey>` with the actual `membershipNumber` or `profileId`.
|
||||
Required content for the feature:
|
||||
|
||||
1. **Profiling Consent (config)**
|
||||
|
||||
- Config needs to be created and published in each language
|
||||
|
||||
2. **/consent (account page)**
|
||||
|
||||
- Page needs to be created and published in each language
|
||||
|
||||
3. **/overview (account page)**
|
||||
|
||||
+1
@@ -2,6 +2,7 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Space-x05);
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.link {
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
import Footnote from "@scandic-hotels/design-system/Footnote"
|
||||
import {
|
||||
MaterialIcon,
|
||||
type MaterialIconSetIconProps,
|
||||
} 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 { getValueFromContactConfig } from "@scandic-hotels/trpc/utils/contactConfig"
|
||||
|
||||
import { serverClient } from "@/lib/trpc/server"
|
||||
|
||||
// import { getValueFromContactConfig } from "@/utils/contactConfig"
|
||||
import styles from "./contactRow.module.css"
|
||||
|
||||
import type { ContactRowProps } from "@/types/components/sidebar/joinLoyaltyContact"
|
||||
@@ -46,22 +44,27 @@ export default async function ContactRow({ contact }: ContactRowProps) {
|
||||
|
||||
return (
|
||||
<div className={styles.wrapper}>
|
||||
<Typography
|
||||
variant="Body/Paragraph/mdBold"
|
||||
className={styles.displayText}
|
||||
>
|
||||
<p>{contact.display_text}</p>
|
||||
</Typography>
|
||||
<Link
|
||||
{contact.display_text ? (
|
||||
<Typography
|
||||
variant="Body/Paragraph/mdBold"
|
||||
className={styles.displayText}
|
||||
>
|
||||
<p>{contact.display_text}</p>
|
||||
</Typography>
|
||||
) : null}
|
||||
<TextLink
|
||||
typography="Link/sm"
|
||||
className={styles.link}
|
||||
href={openableLink}
|
||||
textDecoration="underline"
|
||||
size="small"
|
||||
>
|
||||
{Icon ? <Icon size={20} color="Icon/Interactive/Default" /> : null}
|
||||
{val}
|
||||
</Link>
|
||||
{footnote && <Footnote color="burgundy">{footnote}</Footnote>}
|
||||
</TextLink>
|
||||
{footnote && (
|
||||
<Typography variant="Body/Supporting text (caption)/smRegular">
|
||||
<p>{footnote}</p>
|
||||
</Typography>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -13,18 +13,9 @@
|
||||
gap: var(--Space-x15);
|
||||
}
|
||||
|
||||
.contact > div {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1367px) {
|
||||
.contactContainer {
|
||||
align-items: start;
|
||||
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 Link from "@scandic-hotels/design-system/OldDSLink"
|
||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||
|
||||
@@ -2,15 +2,6 @@ import { Lang } from "@scandic-hotels/common/constants/language"
|
||||
|
||||
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 = {
|
||||
da: `/${Lang.da}/scandic-friends/hjalp-og-service/faq`,
|
||||
de: `/${Lang.de}/scandic-friends/hilfe-und-service/faq`,
|
||||
|
||||
Vendored
+6
@@ -16,6 +16,11 @@ export const env = createEnv({
|
||||
.transform((s) =>
|
||||
getSemver("scandic-web", s, process.env.BRANCH || "development")
|
||||
),
|
||||
NEXT_PUBLIC_NEW_POINTCLAIMS: z
|
||||
.string()
|
||||
.optional()
|
||||
.default("false")
|
||||
.transform((s) => s === "true"),
|
||||
},
|
||||
emptyStringAsUndefined: true,
|
||||
runtimeEnv: {
|
||||
@@ -26,5 +31,6 @@ export const env = createEnv({
|
||||
process.env.NEXT_PUBLIC_SENTRY_CLIENT_SAMPLERATE,
|
||||
NEXT_PUBLIC_PUBLIC_URL: process.env.NEXT_PUBLIC_PUBLIC_URL,
|
||||
NEXT_PUBLIC_RELEASE_TAG: process.env.NEXT_PUBLIC_RELEASE_TAG,
|
||||
NEXT_PUBLIC_NEW_POINTCLAIMS: process.env.NEXT_PUBLIC_NEW_POINTCLAIMS,
|
||||
},
|
||||
})
|
||||
|
||||
Vendored
-6
@@ -96,11 +96,6 @@ export const env = createEnv({
|
||||
.refine((s) => s === "1" || s === "0")
|
||||
.transform((s) => s === "1")
|
||||
.default("0"),
|
||||
ENABLE_PROFILE_CONSENT: z
|
||||
.string()
|
||||
.refine((s) => s === "true" || s === "false")
|
||||
.transform((s) => s === "true")
|
||||
.default("false"),
|
||||
RELEASE_TAG: z
|
||||
.string()
|
||||
.optional()
|
||||
@@ -160,7 +155,6 @@ export const env = createEnv({
|
||||
DTMC_ENTRA_ID_SECRET: process.env.DTMC_ENTRA_ID_SECRET,
|
||||
CHATBOT_LIVE_LANGS: process.env.CHATBOT_LIVE_LANGS,
|
||||
SEO_INERT: process.env.SEO_INERT,
|
||||
ENABLE_PROFILE_CONSENT: process.env.ENABLE_PROFILE_CONSENT,
|
||||
RELEASE_TAG: process.env.NEXT_PUBLIC_RELEASE_TAG,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -231,6 +231,12 @@
|
||||
"value": "Særlige ønsker (valgfrit)"
|
||||
}
|
||||
],
|
||||
"ancillaries.label.quantity": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Antal"
|
||||
}
|
||||
],
|
||||
"ancillaries.unableToDisplayBreakfastPrices": [
|
||||
{
|
||||
"type": 0,
|
||||
@@ -249,6 +255,18 @@
|
||||
"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": [
|
||||
{
|
||||
"type": 0,
|
||||
@@ -953,6 +971,16 @@
|
||||
"value": "Tilføj kode"
|
||||
}
|
||||
],
|
||||
"bookingWidget.bookingCode.readMore": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Læs mere om brug af "
|
||||
},
|
||||
{
|
||||
"type": 1,
|
||||
"value": "codeVoucher"
|
||||
}
|
||||
],
|
||||
"bookingWidget.bookingCode.remember": [
|
||||
{
|
||||
"type": 0,
|
||||
@@ -1061,6 +1089,16 @@
|
||||
"value": "details"
|
||||
}
|
||||
],
|
||||
"bookingWidget.reward.readMore": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Læs mere om booking med "
|
||||
},
|
||||
{
|
||||
"type": 1,
|
||||
"value": "reward"
|
||||
}
|
||||
],
|
||||
"bookingWidget.reward.rewardNight": [
|
||||
{
|
||||
"type": 0,
|
||||
@@ -2510,6 +2548,44 @@
|
||||
"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": [
|
||||
{
|
||||
"type": 0,
|
||||
@@ -5783,7 +5859,7 @@
|
||||
"myPages.myStay.ancillaries.reachedMaxItemsStepperMessage": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Maksimal antal nået for denne vare."
|
||||
"value": "Maksimalt antal nået for denne vare."
|
||||
}
|
||||
],
|
||||
"myPages.myStay.ancillaries.reachedMaxPointsMessage": [
|
||||
@@ -7569,6 +7645,58 @@
|
||||
"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": [
|
||||
{
|
||||
"offset": 0,
|
||||
@@ -8629,6 +8757,12 @@
|
||||
"value": "). Se tilgængelige priser nedenfor."
|
||||
}
|
||||
],
|
||||
"selectRate.alert.reservationPolicies": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Se prisdetaljer"
|
||||
}
|
||||
],
|
||||
"selectRate.availableRooms.all": [
|
||||
{
|
||||
"offset": 0,
|
||||
|
||||
@@ -231,6 +231,12 @@
|
||||
"value": "Sonderwünsche (optional)"
|
||||
}
|
||||
],
|
||||
"ancillaries.label.quantity": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Menge"
|
||||
}
|
||||
],
|
||||
"ancillaries.unableToDisplayBreakfastPrices": [
|
||||
{
|
||||
"type": 0,
|
||||
@@ -249,6 +255,18 @@
|
||||
"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": [
|
||||
{
|
||||
"type": 0,
|
||||
@@ -949,6 +967,16 @@
|
||||
"value": "Code hinzufügen"
|
||||
}
|
||||
],
|
||||
"bookingWidget.bookingCode.readMore": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Mehr erfahren über die Verwendung von "
|
||||
},
|
||||
{
|
||||
"type": 1,
|
||||
"value": "codeVoucher"
|
||||
}
|
||||
],
|
||||
"bookingWidget.bookingCode.remember": [
|
||||
{
|
||||
"type": 0,
|
||||
@@ -1057,6 +1085,20 @@
|
||||
"value": "details"
|
||||
}
|
||||
],
|
||||
"bookingWidget.reward.readMore": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Mehr über Buchungen mit "
|
||||
},
|
||||
{
|
||||
"type": 1,
|
||||
"value": "reward"
|
||||
},
|
||||
{
|
||||
"type": 0,
|
||||
"value": "erfahren"
|
||||
}
|
||||
],
|
||||
"bookingWidget.reward.rewardNight": [
|
||||
{
|
||||
"type": 0,
|
||||
@@ -2522,6 +2564,44 @@
|
||||
"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": [
|
||||
{
|
||||
"type": 0,
|
||||
@@ -7570,6 +7650,58 @@
|
||||
"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": [
|
||||
{
|
||||
"offset": 0,
|
||||
@@ -8634,6 +8766,12 @@
|
||||
"value": "). Verfügbare Übernachtungspreise sind unten zu sehen."
|
||||
}
|
||||
],
|
||||
"selectRate.alert.reservationPolicies": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Tarifdetails anzeigen"
|
||||
}
|
||||
],
|
||||
"selectRate.availableRooms.all": [
|
||||
{
|
||||
"offset": 0,
|
||||
|
||||
@@ -231,6 +231,12 @@
|
||||
"value": "Special requests (optional)"
|
||||
}
|
||||
],
|
||||
"ancillaries.label.quantity": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Quantity"
|
||||
}
|
||||
],
|
||||
"ancillaries.unableToDisplayBreakfastPrices": [
|
||||
{
|
||||
"type": 0,
|
||||
@@ -249,6 +255,18 @@
|
||||
"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": [
|
||||
{
|
||||
"type": 0,
|
||||
@@ -337,6 +355,12 @@
|
||||
"value": "Change or cancel"
|
||||
}
|
||||
],
|
||||
"booking.changeTitle": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Change"
|
||||
}
|
||||
],
|
||||
"booking.codeVoucher": [
|
||||
{
|
||||
"type": 0,
|
||||
@@ -953,6 +977,16 @@
|
||||
"value": "Add code"
|
||||
}
|
||||
],
|
||||
"bookingWidget.bookingCode.readMore": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Read more about using "
|
||||
},
|
||||
{
|
||||
"type": 1,
|
||||
"value": "codeVoucher"
|
||||
}
|
||||
],
|
||||
"bookingWidget.bookingCode.remember": [
|
||||
{
|
||||
"type": 0,
|
||||
@@ -1061,6 +1095,16 @@
|
||||
"value": "details"
|
||||
}
|
||||
],
|
||||
"bookingWidget.reward.readMore": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Read more about booking with "
|
||||
},
|
||||
{
|
||||
"type": 1,
|
||||
"value": "reward"
|
||||
}
|
||||
],
|
||||
"bookingWidget.reward.rewardNight": [
|
||||
{
|
||||
"type": 0,
|
||||
@@ -2198,6 +2242,16 @@
|
||||
"value": " points"
|
||||
}
|
||||
],
|
||||
"common.pointsInLine": [
|
||||
{
|
||||
"type": 1,
|
||||
"value": "points"
|
||||
},
|
||||
{
|
||||
"type": 0,
|
||||
"value": " points"
|
||||
}
|
||||
],
|
||||
"common.pointsToSpend": [
|
||||
{
|
||||
"type": 0,
|
||||
@@ -2510,6 +2564,44 @@
|
||||
"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": [
|
||||
{
|
||||
"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."
|
||||
}
|
||||
],
|
||||
"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": [
|
||||
{
|
||||
"type": 0,
|
||||
@@ -3344,6 +3442,40 @@
|
||||
"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": [
|
||||
{
|
||||
"type": 0,
|
||||
@@ -5669,6 +5801,24 @@
|
||||
"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": [
|
||||
{
|
||||
"type": 0,
|
||||
@@ -6057,6 +6207,88 @@
|
||||
"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": [
|
||||
{
|
||||
"type": 0,
|
||||
@@ -7546,6 +7778,58 @@
|
||||
"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": [
|
||||
{
|
||||
"offset": 0,
|
||||
@@ -8610,6 +8894,12 @@
|
||||
"value": "). See available rates below."
|
||||
}
|
||||
],
|
||||
"selectRate.alert.reservationPolicies": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "See rate details"
|
||||
}
|
||||
],
|
||||
"selectRate.availableRooms.all": [
|
||||
{
|
||||
"offset": 0,
|
||||
|
||||
@@ -231,6 +231,12 @@
|
||||
"value": "Erityistoiveet (valinnainen)"
|
||||
}
|
||||
],
|
||||
"ancillaries.label.quantity": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Määrä"
|
||||
}
|
||||
],
|
||||
"ancillaries.unableToDisplayBreakfastPrices": [
|
||||
{
|
||||
"type": 0,
|
||||
@@ -249,6 +255,18 @@
|
||||
"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": [
|
||||
{
|
||||
"type": 0,
|
||||
@@ -953,6 +971,20 @@
|
||||
"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": [
|
||||
{
|
||||
"type": 0,
|
||||
@@ -1061,6 +1093,20 @@
|
||||
"value": "details"
|
||||
}
|
||||
],
|
||||
"bookingWidget.reward.readMore": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Lue lisää "
|
||||
},
|
||||
{
|
||||
"type": 1,
|
||||
"value": "reward"
|
||||
},
|
||||
{
|
||||
"type": 0,
|
||||
"value": " varaamisesta"
|
||||
}
|
||||
],
|
||||
"bookingWidget.reward.rewardNight": [
|
||||
{
|
||||
"type": 0,
|
||||
@@ -2514,6 +2560,44 @@
|
||||
"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": [
|
||||
{
|
||||
"type": 0,
|
||||
@@ -7586,6 +7670,58 @@
|
||||
"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": [
|
||||
{
|
||||
"offset": 0,
|
||||
@@ -8658,6 +8794,12 @@
|
||||
"value": "). Katso saatavilla olevat hinnat alla."
|
||||
}
|
||||
],
|
||||
"selectRate.alert.reservationPolicies": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Katso hinnan tiedot"
|
||||
}
|
||||
],
|
||||
"selectRate.availableRooms.all": [
|
||||
{
|
||||
"offset": 0,
|
||||
|
||||
@@ -231,6 +231,12 @@
|
||||
"value": "Spesielle ønsker (valgfritt)"
|
||||
}
|
||||
],
|
||||
"ancillaries.label.quantity": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Antall"
|
||||
}
|
||||
],
|
||||
"ancillaries.unableToDisplayBreakfastPrices": [
|
||||
{
|
||||
"type": 0,
|
||||
@@ -249,6 +255,18 @@
|
||||
"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": [
|
||||
{
|
||||
"type": 0,
|
||||
@@ -953,6 +971,16 @@
|
||||
"value": "Legg til kode"
|
||||
}
|
||||
],
|
||||
"bookingWidget.bookingCode.readMore": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Les mer om bruk av "
|
||||
},
|
||||
{
|
||||
"type": 1,
|
||||
"value": "codeVoucher"
|
||||
}
|
||||
],
|
||||
"bookingWidget.bookingCode.remember": [
|
||||
{
|
||||
"type": 0,
|
||||
@@ -1061,6 +1089,16 @@
|
||||
"value": "details"
|
||||
}
|
||||
],
|
||||
"bookingWidget.reward.readMore": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Les mer om bestilling med "
|
||||
},
|
||||
{
|
||||
"type": 1,
|
||||
"value": "reward"
|
||||
}
|
||||
],
|
||||
"bookingWidget.reward.rewardNight": [
|
||||
{
|
||||
"type": 0,
|
||||
@@ -2510,6 +2548,44 @@
|
||||
"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": [
|
||||
{
|
||||
"type": 0,
|
||||
@@ -7582,6 +7658,58 @@
|
||||
"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": [
|
||||
{
|
||||
"offset": 0,
|
||||
@@ -8654,6 +8782,12 @@
|
||||
"value": " ). Se tilgjengelige priser nedenfor."
|
||||
}
|
||||
],
|
||||
"selectRate.alert.reservationPolicies": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Se prisdetaljer"
|
||||
}
|
||||
],
|
||||
"selectRate.availableRooms.all": [
|
||||
{
|
||||
"offset": 0,
|
||||
|
||||
@@ -231,6 +231,12 @@
|
||||
"value": "Särskilda önskemål (valfritt)"
|
||||
}
|
||||
],
|
||||
"ancillaries.label.quantity": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Antal"
|
||||
}
|
||||
],
|
||||
"ancillaries.unableToDisplayBreakfastPrices": [
|
||||
{
|
||||
"type": 0,
|
||||
@@ -249,6 +255,18 @@
|
||||
"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": [
|
||||
{
|
||||
"type": 0,
|
||||
@@ -953,6 +971,16 @@
|
||||
"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": [
|
||||
{
|
||||
"type": 0,
|
||||
@@ -1061,6 +1089,16 @@
|
||||
"value": "details"
|
||||
}
|
||||
],
|
||||
"bookingWidget.reward.readMore": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Läs mer om bokning med "
|
||||
},
|
||||
{
|
||||
"type": 1,
|
||||
"value": "reward"
|
||||
}
|
||||
],
|
||||
"bookingWidget.reward.rewardNight": [
|
||||
{
|
||||
"type": 0,
|
||||
@@ -2510,6 +2548,44 @@
|
||||
"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": [
|
||||
{
|
||||
"type": 0,
|
||||
@@ -3845,7 +3921,7 @@
|
||||
"findMyBooking.findYourStay": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Hitta ditt hotell"
|
||||
"value": "Hitta din bokning"
|
||||
}
|
||||
],
|
||||
"findMyBooking.manageBooking": [
|
||||
@@ -7566,6 +7642,58 @@
|
||||
"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": [
|
||||
{
|
||||
"offset": 0,
|
||||
@@ -8626,6 +8754,12 @@
|
||||
"value": "). Se tillgängliga priser nedan."
|
||||
}
|
||||
],
|
||||
"selectRate.alert.reservationPolicies": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Se bokningsvillkor"
|
||||
}
|
||||
],
|
||||
"selectRate.availableRooms.all": [
|
||||
{
|
||||
"offset": 0,
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
"@internationalized/date": "^3.8.0",
|
||||
"@netlify/blobs": "^8.1.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",
|
||||
"@scandic-hotels/booking-flow": "workspace:*",
|
||||
"@scandic-hotels/common": "workspace:*",
|
||||
@@ -38,7 +38,7 @@
|
||||
"@scandic-hotels/tracking": "workspace:*",
|
||||
"@scandic-hotels/trpc": "workspace:*",
|
||||
"@sentry/nextjs": "^10.33.0",
|
||||
"@swc/plugin-formatjs": "^3.2.2",
|
||||
"@swc/plugin-formatjs": "^8.1.0",
|
||||
"@t3-oss/env-nextjs": "^0.13.4",
|
||||
"@tanstack/react-query": "^5.75.5",
|
||||
"@tanstack/react-query-devtools": "^5.75.5",
|
||||
@@ -66,12 +66,12 @@
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"md5": "^2.3.0",
|
||||
"motion": "^12.10.0",
|
||||
"next": "16.0.10",
|
||||
"next": "16.1.6",
|
||||
"next-auth": "5.0.0-beta.29",
|
||||
"react": "19.2.1",
|
||||
"react": "19.2.4",
|
||||
"react-aria-components": "1.8.0",
|
||||
"react-day-picker": "^9.6.7",
|
||||
"react-dom": "19.2.1",
|
||||
"react-dom": "19.2.4",
|
||||
"react-feather": "^2.0.10",
|
||||
"react-focus-lock": "^2.13.6",
|
||||
"react-hook-form": "^7.56.2",
|
||||
|
||||
@@ -13,22 +13,20 @@ import { MyStaySkeleton } from "@/components/HotelReservation/MyStay/myStaySkele
|
||||
import { MyStayContext } from "@/contexts/MyStay"
|
||||
|
||||
import type { Lang } from "@scandic-hotels/common/constants/language"
|
||||
import type {
|
||||
BookingConfirmation,
|
||||
BookingConfirmationSchema,
|
||||
} from "@scandic-hotels/trpc/types/bookingConfirmation"
|
||||
import type { BookingConfirmation } from "@scandic-hotels/trpc/types/bookingConfirmation"
|
||||
import type { RoomCategories } from "@scandic-hotels/trpc/types/hotel"
|
||||
import type { CreditCard } from "@scandic-hotels/trpc/types/user"
|
||||
|
||||
import type { Packages } from "@/types/components/myPages/myStay/ancillaries"
|
||||
import type { MyStayStore } from "@/types/contexts/my-stay"
|
||||
import type { getLinkedReservations } from "@/lib/trpc/memoizedRequests"
|
||||
|
||||
interface MyStayProviderProps {
|
||||
bookingConfirmation: BookingConfirmation
|
||||
breakfastPackages: Packages | null
|
||||
isLoggedIn?: boolean
|
||||
lang: Lang
|
||||
linkedReservationsPromise: Promise<BookingConfirmationSchema[]>
|
||||
linkedReservationsPromise: ReturnType<typeof getLinkedReservations>
|
||||
refId: string
|
||||
roomCategories: RoomCategories
|
||||
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("")
|
||||
})
|
||||
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", () => {
|
||||
|
||||
@@ -22,6 +22,13 @@ export function getTimeAgoText(checkoutDate: string, intl: IntlShape): string {
|
||||
return ""
|
||||
}
|
||||
|
||||
if (daysDiff === 0) {
|
||||
return intl.formatMessage({
|
||||
id: "nextStay.today",
|
||||
defaultMessage: "Today",
|
||||
})
|
||||
}
|
||||
|
||||
if (daysDiff <= 30) {
|
||||
// 1-30 days
|
||||
return intl.formatMessage(
|
||||
|
||||
@@ -198,10 +198,15 @@ export function Room({
|
||||
{isFlexBooking || isChangeBooking ? (
|
||||
<li className={styles.listItem}>
|
||||
<p className={styles.label}>
|
||||
{intl.formatMessage({
|
||||
id: "booking.changeOrCancel",
|
||||
defaultMessage: "Change or cancel",
|
||||
})}
|
||||
{isChangeBooking
|
||||
? intl.formatMessage({
|
||||
id: "booking.changeTitle",
|
||||
defaultMessage: "Change",
|
||||
})
|
||||
: intl.formatMessage({
|
||||
id: "booking.changeOrCancel",
|
||||
defaultMessage: "Change or cancel",
|
||||
})}
|
||||
</p>
|
||||
<p>
|
||||
{intl.formatMessage(
|
||||
|
||||
+1
-1
@@ -9,7 +9,7 @@
|
||||
overflow-y: auto;
|
||||
padding: var(--Space-x2) var(--Space-x3);
|
||||
position: fixed;
|
||||
top: calc(140px + max(var(--sitewide-alert-sticky-height), 25px));
|
||||
top: calc(140px + max(var(--sitewide-alert-sticky-height), 15px));
|
||||
width: 100%;
|
||||
height: calc(100% - 200px);
|
||||
z-index: 10010;
|
||||
|
||||
+4
-6
@@ -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"
|
||||
|
||||
export default function Label({ children }: React.PropsWithChildren) {
|
||||
return (
|
||||
<li className={styles.label}>
|
||||
<Footnote color="uiTextPlaceholder" textTransform="uppercase">
|
||||
{children}
|
||||
</Footnote>
|
||||
</li>
|
||||
<Typography variant="Title/Overline/sm">
|
||||
<li className={styles.label}>{children}</li>
|
||||
</Typography>
|
||||
)
|
||||
}
|
||||
|
||||
+2
-1
@@ -5,5 +5,6 @@
|
||||
}
|
||||
|
||||
.label {
|
||||
padding: 0 var(--Space-x1);
|
||||
padding: 0 var(--Space-x1) var(--Space-x05);
|
||||
color: var(--Text-Tertiary);
|
||||
}
|
||||
|
||||
+16
-17
@@ -6,7 +6,6 @@ import { useIntl } from "react-intl"
|
||||
import { useDebounceValue } from "usehooks-ts"
|
||||
|
||||
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 { Typography } from "@scandic-hotels/design-system/Typography"
|
||||
import { trpc } from "@scandic-hotels/trpc/client"
|
||||
@@ -192,16 +191,14 @@ export default function SearchList({
|
||||
{typeFilteredSearchHistory && typeFilteredSearchHistory.length > 0 && (
|
||||
<>
|
||||
<Divider className={styles.noResultsDivider} />
|
||||
<Footnote
|
||||
className={styles.text}
|
||||
color="uiTextPlaceholder"
|
||||
textTransform="uppercase"
|
||||
>
|
||||
{intl.formatMessage({
|
||||
id: "bookingWidget.searchList.latestSearches",
|
||||
defaultMessage: "Latest searches",
|
||||
})}
|
||||
</Footnote>
|
||||
<Typography variant="Title/Overline/sm">
|
||||
<p className={styles.text}>
|
||||
{intl.formatMessage({
|
||||
id: "bookingWidget.searchList.latestSearches",
|
||||
defaultMessage: "Latest searches",
|
||||
})}
|
||||
</p>
|
||||
</Typography>
|
||||
<List
|
||||
getItemProps={getItemProps}
|
||||
highlightedIndex={highlightedIndex}
|
||||
@@ -226,12 +223,14 @@ export default function SearchList({
|
||||
if (displaySearchHistory) {
|
||||
return (
|
||||
<Dialog getMenuProps={getMenuProps}>
|
||||
<Footnote color="uiTextPlaceholder" textTransform="uppercase">
|
||||
{intl.formatMessage({
|
||||
id: "bookingWidget.searchList.latestSearches",
|
||||
defaultMessage: "Latest searches",
|
||||
})}
|
||||
</Footnote>
|
||||
<Typography variant="Title/Overline/sm">
|
||||
<p className={styles.text}>
|
||||
{intl.formatMessage({
|
||||
id: "bookingWidget.searchList.latestSearches",
|
||||
defaultMessage: "Latest searches",
|
||||
})}
|
||||
</p>
|
||||
</Typography>
|
||||
<List
|
||||
getItemProps={getItemProps}
|
||||
highlightedIndex={highlightedIndex}
|
||||
|
||||
+2
@@ -33,6 +33,8 @@
|
||||
|
||||
.text {
|
||||
padding: 0 var(--Space-x1);
|
||||
color: var(--Text-Tertiary);
|
||||
white-space: normal;
|
||||
}
|
||||
.textPlaceholderColor {
|
||||
color: var(--UI-Text-Placeholder);
|
||||
|
||||
@@ -5,6 +5,7 @@ import { cx } from "class-variance-authority"
|
||||
import { useSearchParams } from "next/navigation"
|
||||
import { use, useEffect, useRef, useState } from "react"
|
||||
import { FormProvider, useForm } from "react-hook-form"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { dt } from "@scandic-hotels/common/dt"
|
||||
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 { debounce } from "@scandic-hotels/common/utils/debounce"
|
||||
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 { SEARCH_TYPE_REDEMPTION } from "@scandic-hotels/trpc/constants/booking"
|
||||
|
||||
@@ -66,6 +67,7 @@ export default function BookingWidgetClient({
|
||||
autoLock: false,
|
||||
})
|
||||
const shouldFetchAutoComplete = !!data.hotelId || !!data.city
|
||||
const intl = useIntl()
|
||||
|
||||
const { data: destinationsData, isPending } =
|
||||
trpc.autocomplete.destinations.useQuery(
|
||||
@@ -257,13 +259,17 @@ export default function BookingWidgetClient({
|
||||
/>
|
||||
<div className={styles.backdrop} onClick={closeMobileSearch} />
|
||||
<div className={formContainerClassNames}>
|
||||
<button
|
||||
<IconButton
|
||||
className={styles.close}
|
||||
onClick={closeMobileSearch}
|
||||
type="button"
|
||||
>
|
||||
<MaterialIcon icon="close" />
|
||||
</button>
|
||||
variant="Muted"
|
||||
emphasis
|
||||
aria-label={intl.formatMessage({
|
||||
id: "common.close",
|
||||
defaultMessage: "Close",
|
||||
})}
|
||||
onPress={closeMobileSearch}
|
||||
iconName="close"
|
||||
/>
|
||||
<Form
|
||||
type={type}
|
||||
onClose={closeMobileSearch}
|
||||
|
||||
@@ -65,7 +65,7 @@ export default function DatePickerRangeDesktop({
|
||||
range_start: styles.rangeStart,
|
||||
root: `${classNames.root} ${styles.container}`,
|
||||
week: styles.week,
|
||||
weekday: `${classNames.weekday} ${styles.weekDay}`,
|
||||
weekday: styles.weekDay,
|
||||
nav: `${classNames.nav} ${styles.nav}`,
|
||||
button_next: `${classNames.button_next} ${styles.button_next}`,
|
||||
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 { dt } from "@scandic-hotels/common/dt"
|
||||
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 useLang from "../../../../hooks/useLang"
|
||||
@@ -72,9 +72,17 @@ export default function DatePickerRangeMobile({
|
||||
return (
|
||||
<div className={styles.container} ref={monthsRef}>
|
||||
<header className={styles.header}>
|
||||
<button className={styles.close} onClick={close} type="button">
|
||||
<MaterialIcon icon="close" />
|
||||
</button>
|
||||
<IconButton
|
||||
className={styles.close}
|
||||
variant="Muted"
|
||||
emphasis
|
||||
aria-label={intl.formatMessage({
|
||||
id: "common.close",
|
||||
defaultMessage: "Close",
|
||||
})}
|
||||
onPress={close}
|
||||
iconName="close"
|
||||
/>
|
||||
</header>
|
||||
<DayPicker
|
||||
classNames={{
|
||||
@@ -90,7 +98,7 @@ export default function DatePickerRangeMobile({
|
||||
range_start: styles.rangeStart,
|
||||
root: `${classNames.root} ${styles.root}`,
|
||||
week: styles.week,
|
||||
weekday: `${classNames.weekday} ${styles.weekDay}`,
|
||||
weekday: styles.weekDay,
|
||||
}}
|
||||
disabled={[
|
||||
{ from: lastDayOfPreviousMonth, to: yesterday },
|
||||
|
||||
+15
-14
@@ -20,12 +20,12 @@ div.months {
|
||||
td.day,
|
||||
td.rangeEnd,
|
||||
td.rangeStart {
|
||||
font-family: var(--typography-Body-Bold-fontFamily);
|
||||
font-size: var(--typography-Body-Bold-fontSize);
|
||||
font-weight: 500;
|
||||
letter-spacing: var(--typography-Body-Bold-letterSpacing);
|
||||
line-height: var(--typography-Body-Bold-lineHeight);
|
||||
text-decoration: var(--typography-Body-Bold-textDecoration);
|
||||
font-family: var(--Body-Paragraph-Font-family), var(--Body-Paragraph-Font-fallback);
|
||||
font-size: var(--Body-Paragraph-Size);
|
||||
font-weight: var(--Body-Paragraph-Font-weight-2);
|
||||
letter-spacing: var(--Body-Paragraph-Letter-spacing);
|
||||
line-height: 1.5;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
td.rangeEnd,
|
||||
@@ -92,14 +92,15 @@ td.day[data-outside="true"] ~ td.day[data-disabled="true"] button.dayButton,
|
||||
}
|
||||
|
||||
.weekDay {
|
||||
color: var(--UI-Text-Placeholder);
|
||||
font-family: var(--typography-Footnote-Labels-fontFamily);
|
||||
font-size: var(--typography-Footnote-Labels-fontSize);
|
||||
font-weight: var(--typography-Footnote-Labels-fontWeight);
|
||||
letter-spacing: var(--typography-Footnote-Labels-letterSpacing);
|
||||
line-height: var(--typography-Footnote-Labels-lineHeight);
|
||||
text-decoration: var(--typography-Footnote-Labels-textDecoration);
|
||||
text-transform: uppercase;
|
||||
color: var(--Text-Tertiary);
|
||||
font-family: var(--Title-Overline-sm-Font-family), var(--Title-Overline-sm-Font-fallback);
|
||||
font-size: var(--Title-Overline-sm-Size);
|
||||
font-style: normal;
|
||||
font-weight: var(--Title-Overline-sm-Font-weight);
|
||||
line-height: 1.5;
|
||||
letter-spacing: var(--Title-Overline-sm-Letter-spacing);
|
||||
text-transform: var(--Title-Overline-sm-Text-Transform);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.footer {
|
||||
|
||||
+15
-15
@@ -97,12 +97,12 @@ div.months {
|
||||
td.day,
|
||||
td.rangeEnd,
|
||||
td.rangeStart {
|
||||
font-family: var(--typography-Body-Bold-fontFamily);
|
||||
font-size: var(--typography-Body-Bold-fontSize);
|
||||
font-weight: 500;
|
||||
letter-spacing: var(--typography-Body-Bold-letterSpacing);
|
||||
line-height: var(--typography-Body-Bold-lineHeight);
|
||||
text-decoration: var(--typography-Body-Bold-textDecoration);
|
||||
font-family: var(--Body-Paragraph-Font-family), var(--Body-Paragraph-Font-fallback);
|
||||
font-size: var(--Body-Paragraph-Size);
|
||||
font-weight: var(--Body-Paragraph-Font-weight-2);
|
||||
letter-spacing: var(--Body-Paragraph-Letter-spacing);
|
||||
line-height: 1.5;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
td.rangeEnd,
|
||||
@@ -165,15 +165,15 @@ td.day[data-outside="true"] ~ td.day[data-disabled="true"] button.dayButton,
|
||||
}
|
||||
|
||||
.weekDay {
|
||||
color: var(--Base-Text-Medium-contrast);
|
||||
opacity: 1;
|
||||
font-family: var(--typography-Caption-Labels-fontFamily);
|
||||
font-size: var(--typography-Caption-Labels-fontSize);
|
||||
font-weight: var(--typography-Caption-Labels-fontWeight);
|
||||
letter-spacing: var(--typography-Caption-Labels-letterSpacing);
|
||||
line-height: var(--typography-Caption-Labels-lineHeight);
|
||||
text-decoration: var(--typography-Caption-Labels-textDecoration);
|
||||
text-transform: uppercase;
|
||||
color: var(--Text-Tertiary);
|
||||
font-family: var(--Title-Overline-sm-Font-family), var(--Title-Overline-sm-Font-fallback);
|
||||
font-size: var(--Title-Overline-sm-Size);
|
||||
font-style: normal;
|
||||
font-weight: var(--Title-Overline-sm-Font-weight);
|
||||
line-height: 1.5;
|
||||
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) {
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useFormContext, useWatch } from "react-hook-form"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
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 { Typography } from "@scandic-hotels/design-system/Typography"
|
||||
import { SEARCH_TYPE_REDEMPTION } from "@scandic-hotels/trpc/constants/booking"
|
||||
@@ -114,9 +115,17 @@ export default function GuestsRoomsPickerDialog({
|
||||
<>
|
||||
<section className={styles.contentWrapper}>
|
||||
<header className={styles.header}>
|
||||
<button type="button" className={styles.close} onClick={onClose}>
|
||||
<MaterialIcon icon="close" />
|
||||
</button>
|
||||
<IconButton
|
||||
className={styles.close}
|
||||
variant="Muted"
|
||||
emphasis
|
||||
aria-label={intl.formatMessage({
|
||||
id: "common.close",
|
||||
defaultMessage: "Close",
|
||||
})}
|
||||
onPress={onClose}
|
||||
iconName="close"
|
||||
/>
|
||||
</header>
|
||||
|
||||
<div className={styles.contentContainer}>
|
||||
|
||||
+8
-10
@@ -4,9 +4,8 @@ import { useIntl } from "react-intl"
|
||||
|
||||
import { CurrencyEnum } from "@scandic-hotels/common/constants/currency"
|
||||
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 Link from "@scandic-hotels/design-system/OldDSLink"
|
||||
import { TextLink } from "@scandic-hotels/design-system/TextLink"
|
||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||
|
||||
import { useBookingFlowConfig } from "../../../../../bookingFlowConfig/bookingFlowConfigContext"
|
||||
@@ -83,8 +82,8 @@ export default function JoinScandicFriendsCard({
|
||||
</Typography>
|
||||
</Checkbox>
|
||||
|
||||
<div className={styles.terms}>
|
||||
<Footnote color="uiTextPlaceholder">
|
||||
<Typography variant="Body/Supporting text (caption)/smRegular">
|
||||
<p className={styles.terms}>
|
||||
{intl.formatMessage(
|
||||
{
|
||||
id: "enterDetails.joinScandicFriendsCard.terms",
|
||||
@@ -93,19 +92,18 @@ export default function JoinScandicFriendsCard({
|
||||
},
|
||||
{
|
||||
termsAndConditionsLink: (str) => (
|
||||
<Link
|
||||
textDecoration="underline"
|
||||
size="tiny"
|
||||
<TextLink
|
||||
typography="Link/sm"
|
||||
target="_blank"
|
||||
href={routes.membershipTermsAndConditions[lang]}
|
||||
>
|
||||
{str}
|
||||
</Link>
|
||||
</TextLink>
|
||||
),
|
||||
}
|
||||
)}
|
||||
</Footnote>
|
||||
</div>
|
||||
</p>
|
||||
</Typography>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
+1
@@ -28,6 +28,7 @@
|
||||
|
||||
.terms {
|
||||
grid-area: terms;
|
||||
color: var(--Text-Secondary);
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
|
||||
+8
-10
@@ -4,9 +4,8 @@ import { useIntl } from "react-intl"
|
||||
|
||||
import { CurrencyEnum } from "@scandic-hotels/common/constants/currency"
|
||||
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 Link from "@scandic-hotels/design-system/OldDSLink"
|
||||
import { TextLink } from "@scandic-hotels/design-system/TextLink"
|
||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||
|
||||
import { useBookingFlowConfig } from "../../../../../bookingFlowConfig/bookingFlowConfigContext"
|
||||
@@ -97,8 +96,8 @@ export function PartnerSASJoinScandicFriendsCard({
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.terms}>
|
||||
<Footnote color="uiTextPlaceholder">
|
||||
<Typography variant="Body/Supporting text (caption)/smRegular">
|
||||
<p className={styles.terms}>
|
||||
{intl.formatMessage(
|
||||
{
|
||||
id: "enterDetails.joinScandicFriendsCard.terms",
|
||||
@@ -107,19 +106,18 @@ export function PartnerSASJoinScandicFriendsCard({
|
||||
},
|
||||
{
|
||||
termsAndConditionsLink: (str) => (
|
||||
<Link
|
||||
textDecoration="underline"
|
||||
size="tiny"
|
||||
<TextLink
|
||||
typography="Link/sm"
|
||||
target="_blank"
|
||||
href={routes.membershipTermsAndConditions[lang]}
|
||||
>
|
||||
{str}
|
||||
</Link>
|
||||
</TextLink>
|
||||
),
|
||||
}
|
||||
)}
|
||||
</Footnote>
|
||||
</div>
|
||||
</p>
|
||||
</Typography>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
+1
@@ -31,6 +31,7 @@
|
||||
|
||||
.terms {
|
||||
grid-area: terms;
|
||||
color: var(--Text-Secondary);
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
|
||||
+8
-10
@@ -4,10 +4,9 @@ import { useIntl } from "react-intl"
|
||||
import { CurrencyEnum } from "@scandic-hotels/common/constants/currency"
|
||||
import { useLazyPathname } from "@scandic-hotels/common/hooks/useLazyPathname"
|
||||
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 { 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 { trackEvent } from "@scandic-hotels/tracking/base"
|
||||
import { trackLoginClick } from "@scandic-hotels/tracking/navigation"
|
||||
@@ -101,8 +100,8 @@ export function JoinScandicFriendsCard({ name = "join" }: Props) {
|
||||
})}
|
||||
</LoginButton>
|
||||
|
||||
<div className={styles.terms}>
|
||||
<Footnote color="uiTextPlaceholder">
|
||||
<Typography variant="Body/Supporting text (caption)/smRegular">
|
||||
<p className={styles.terms}>
|
||||
{intl.formatMessage(
|
||||
{
|
||||
id: "enterDetails.joinScandicFriendsCard.terms",
|
||||
@@ -111,19 +110,18 @@ export function JoinScandicFriendsCard({ name = "join" }: Props) {
|
||||
},
|
||||
{
|
||||
termsAndConditionsLink: (str) => (
|
||||
<Link
|
||||
textDecoration="underline"
|
||||
size="tiny"
|
||||
<TextLink
|
||||
typography="Link/sm"
|
||||
target="_blank"
|
||||
href={routes.membershipTermsAndConditions[lang]}
|
||||
>
|
||||
{str}
|
||||
</Link>
|
||||
</TextLink>
|
||||
),
|
||||
}
|
||||
)}
|
||||
</Footnote>
|
||||
</div>
|
||||
</p>
|
||||
</Typography>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
+1
@@ -34,6 +34,7 @@
|
||||
|
||||
.terms {
|
||||
grid-area: terms;
|
||||
color: var(--Text-Secondary);
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
|
||||
+8
-10
@@ -4,9 +4,8 @@ import { useIntl } from "react-intl"
|
||||
|
||||
import { CurrencyEnum } from "@scandic-hotels/common/constants/currency"
|
||||
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 Link from "@scandic-hotels/design-system/OldDSLink"
|
||||
import { TextLink } from "@scandic-hotels/design-system/TextLink"
|
||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||
import { trpc } from "@scandic-hotels/trpc/client"
|
||||
|
||||
@@ -94,8 +93,8 @@ export function PartnerSASJoinScandicFriendsCard({
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.terms}>
|
||||
<Footnote color="uiTextPlaceholder">
|
||||
<Typography variant="Body/Supporting text (caption)/smRegular">
|
||||
<p className={styles.terms}>
|
||||
{intl.formatMessage(
|
||||
{
|
||||
id: "enterDetails.joinScandicFriendsCard.terms",
|
||||
@@ -104,19 +103,18 @@ export function PartnerSASJoinScandicFriendsCard({
|
||||
},
|
||||
{
|
||||
termsAndConditionsLink: (str) => (
|
||||
<Link
|
||||
textDecoration="underline"
|
||||
size="tiny"
|
||||
<TextLink
|
||||
typography="Link/sm"
|
||||
target="_blank"
|
||||
href={routes.membershipTermsAndConditions[lang]}
|
||||
>
|
||||
{str}
|
||||
</Link>
|
||||
</TextLink>
|
||||
),
|
||||
}
|
||||
)}
|
||||
</Footnote>
|
||||
</div>
|
||||
</p>
|
||||
</Typography>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
+1
@@ -31,6 +31,7 @@
|
||||
|
||||
.terms {
|
||||
grid-area: terms;
|
||||
color: var(--Text-Secondary);
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user