Compare commits
27 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 089bbe7c4f | |||
| c62999879f | |||
| 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,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
@@ -33,7 +33,6 @@
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* Styles for new empty upcoming stays design */
|
||||
.emptyUpcomingStaysContainer {
|
||||
display: flex;
|
||||
padding: var(--Space-x6);
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { notFound } from "next/navigation"
|
||||
import { Suspense } from "react"
|
||||
|
||||
import { LinkChips } from "@scandic-hotels/design-system/LinkChips"
|
||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||
import { TrackingSDK } from "@scandic-hotels/tracking/TrackingSDK"
|
||||
|
||||
import { getCampaignOverviewPage } from "@/lib/trpc/memoizedRequests"
|
||||
|
||||
import { TopCampaign } from "@/components/ContentType/CampaignOverviewPage/TopCampaign"
|
||||
import LinkChips from "@/components/TempDesignSystem/LinkChips"
|
||||
|
||||
import Blocks from "./Blocks"
|
||||
import CampaignOverviewPageSkeleton from "./CampaignOverviewPageSkeleton"
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
} from "@scandic-hotels/design-system/Breadcrumbs"
|
||||
import ButtonLink from "@scandic-hotels/design-system/ButtonLink"
|
||||
import Image from "@scandic-hotels/design-system/Image"
|
||||
import { LinkChips } from "@scandic-hotels/design-system/LinkChips"
|
||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||
import { TrackingSDK } from "@scandic-hotels/tracking/TrackingSDK"
|
||||
|
||||
@@ -17,7 +18,6 @@ import { Breadcrumbs } from "@/components/Breadcrumbs"
|
||||
import HeaderDynamicContent from "@/components/Headers/DynamicContent"
|
||||
import { HeroVideo } from "@/components/HeroVideo"
|
||||
import MeetingPackageWidget from "@/components/MeetingPackageWidget"
|
||||
import LinkChips from "@/components/TempDesignSystem/LinkChips"
|
||||
|
||||
import styles from "./collectionPage.module.css"
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Suspense } from "react"
|
||||
|
||||
import { BreadcrumbsSkeleton } from "@scandic-hotels/design-system/Breadcrumbs"
|
||||
import ButtonLink from "@scandic-hotels/design-system/ButtonLink"
|
||||
import { LinkChips } from "@scandic-hotels/design-system/LinkChips"
|
||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||
import { TrackingSDK } from "@scandic-hotels/tracking/TrackingSDK"
|
||||
|
||||
@@ -16,7 +17,6 @@ import { HeroVideo } from "@/components/HeroVideo"
|
||||
import Sidebar from "@/components/Sidebar"
|
||||
import SidebarSkeleton from "@/components/Sidebar/SidebarSkeleton"
|
||||
import StickyMeetingPackageWidget from "@/components/StickyMeetingPackageWidget"
|
||||
import LinkChips from "@/components/TempDesignSystem/LinkChips"
|
||||
|
||||
import styles from "./contentPage.module.css"
|
||||
|
||||
|
||||
+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) {
|
||||
|
||||
@@ -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
@@ -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"
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import { ChipLink } from "@scandic-hotels/design-system/ChipLink"
|
||||
import { Chips } from "@scandic-hotels/design-system/Chips"
|
||||
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
|
||||
|
||||
interface LinkChipsProps {
|
||||
chips: {
|
||||
url: string
|
||||
title: string
|
||||
}[]
|
||||
}
|
||||
|
||||
export default function LinkChips({ chips }: LinkChipsProps) {
|
||||
if (!chips.length) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<Chips>
|
||||
{chips.map(({ title, url }) => (
|
||||
<ChipLink key={`${title}-${url}`} href={url}>
|
||||
{title}
|
||||
<MaterialIcon icon="chevron_right" size={20} color="CurrentColor" />
|
||||
</ChipLink>
|
||||
))}
|
||||
</Chips>
|
||||
)
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -52,7 +52,7 @@ import { getPaymentHeadingConfig } from "./utils"
|
||||
|
||||
import styles from "./payment.module.css"
|
||||
|
||||
import type { CreateBookingInput } from "@scandic-hotels/trpc/routers/booking/mutation/create/schema"
|
||||
import type { CreateBookingInput } from "@scandic-hotels/trpc/routers/booking/input"
|
||||
import type { CreditCard } from "@scandic-hotels/trpc/types/user"
|
||||
|
||||
import type { PriceChangeData } from "../PriceChangeData"
|
||||
@@ -128,45 +128,46 @@ export default function PaymentClient({
|
||||
|
||||
const initiateBooking = trpc.booking.create.useMutation({
|
||||
onSuccess: (result) => {
|
||||
if (result) {
|
||||
if ("error" in result) {
|
||||
const queryParams = new URLSearchParams(searchParams.toString())
|
||||
queryParams.set("errorCode", result.cause)
|
||||
window.history.replaceState(
|
||||
{},
|
||||
"",
|
||||
`${pathname}?${queryParams.toString()}`
|
||||
)
|
||||
handlePaymentError(result.cause)
|
||||
return
|
||||
}
|
||||
|
||||
const { booking } = result
|
||||
const mainRoom = booking.rooms[0]
|
||||
|
||||
if (booking.reservationStatus == BookingStatusEnum.BookingCompleted) {
|
||||
clearBookingWidgetState()
|
||||
// Cookie is used by Booking Confirmation page to validate that the user came from payment callback
|
||||
// eslint-disable-next-line react-hooks/immutability
|
||||
document.cookie = `bcsig=${result.sig}; Path=/; Max-Age=60; Secure; SameSite=Strict`
|
||||
const confirmationUrl = `${bookingConfirmation(lang)}?RefId=${encodeURIComponent(mainRoom.refId)}`
|
||||
router.push(confirmationUrl)
|
||||
return
|
||||
}
|
||||
|
||||
setRefId(mainRoom.refId)
|
||||
|
||||
const hasPriceChange = booking.rooms.some((r) => r.priceChangedMetadata)
|
||||
if (hasPriceChange) {
|
||||
const priceChangeData = booking.rooms
|
||||
.map((room) => room.priceChangedMetadata || null)
|
||||
.filter(isNotNull)
|
||||
setPriceChangeData(priceChangeData)
|
||||
} else {
|
||||
setIsPollingForBookingStatus(true)
|
||||
}
|
||||
} else {
|
||||
if (!result) {
|
||||
handlePaymentError("No confirmation number")
|
||||
return
|
||||
}
|
||||
|
||||
if ("error" in result) {
|
||||
const queryParams = new URLSearchParams(searchParams.toString())
|
||||
queryParams.set("errorCode", result.cause)
|
||||
window.history.replaceState(
|
||||
{},
|
||||
"",
|
||||
`${pathname}?${queryParams.toString()}`
|
||||
)
|
||||
handlePaymentError(result.cause)
|
||||
return
|
||||
}
|
||||
|
||||
const { booking } = result
|
||||
const mainRoom = booking.rooms[0]
|
||||
|
||||
if (booking.reservationStatus == BookingStatusEnum.BookingCompleted) {
|
||||
clearBookingWidgetState()
|
||||
// Cookie is used by Booking Confirmation page to validate that the user came from payment callback
|
||||
// eslint-disable-next-line react-hooks/immutability
|
||||
document.cookie = `bcsig=${result.sig}; Path=/; Max-Age=60; Secure; SameSite=Strict`
|
||||
const confirmationUrl = `${bookingConfirmation(lang)}?RefId=${encodeURIComponent(mainRoom.refId)}`
|
||||
router.push(confirmationUrl)
|
||||
return
|
||||
}
|
||||
|
||||
setRefId(mainRoom.refId)
|
||||
|
||||
const hasPriceChange = booking.rooms.some((r) => r.priceChangedMetadata)
|
||||
if (hasPriceChange) {
|
||||
const priceChangeData = booking.rooms
|
||||
.map((room) => room.priceChangedMetadata || null)
|
||||
.filter(isNotNull)
|
||||
setPriceChangeData(priceChangeData)
|
||||
} else {
|
||||
setIsPollingForBookingStatus(true)
|
||||
}
|
||||
},
|
||||
onError: (error) => {
|
||||
@@ -419,6 +420,7 @@ export default function PaymentClient({
|
||||
}
|
||||
),
|
||||
}
|
||||
|
||||
initiateBooking.mutate(payload)
|
||||
},
|
||||
[
|
||||
|
||||
+1
-1
@@ -35,7 +35,7 @@ export default function TermsAndConditions({
|
||||
<span>
|
||||
{intl.formatMessage({
|
||||
id: "booking.acceptBookingTerms",
|
||||
defaultMessage: "I accept the booking and cancellation terms.",
|
||||
defaultMessage: "I accept the booking and cancellation terms",
|
||||
})}
|
||||
</span>
|
||||
</Typography>
|
||||
|
||||
@@ -6,7 +6,6 @@ import { useIntl } from "react-intl"
|
||||
|
||||
import { selectRate } from "@scandic-hotels/common/constants/routes/hotelReservation"
|
||||
import { Button } from "@scandic-hotels/design-system/Button"
|
||||
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 { getHotelRoom } from "@scandic-hotels/trpc/routers/booking/helpers"
|
||||
@@ -47,20 +46,14 @@ export default function SelectedRoom() {
|
||||
<div className={styles.wrapper} data-available={room.isAvailable}>
|
||||
<div className={styles.main}>
|
||||
<div className={styles.headerContainer}>
|
||||
<Footnote
|
||||
className={styles.title}
|
||||
asChild
|
||||
textTransform="uppercase"
|
||||
type="label"
|
||||
color="uiTextHighContrast"
|
||||
>
|
||||
<h2>
|
||||
<Typography variant="Title/Overline/sm">
|
||||
<h2 className={styles.title}>
|
||||
{intl.formatMessage({
|
||||
id: "common.room",
|
||||
defaultMessage: "Room",
|
||||
})}
|
||||
</h2>
|
||||
</Footnote>
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="Title/Subtitle/md"
|
||||
className={styles.description}
|
||||
|
||||
+1
-1
@@ -9,7 +9,7 @@
|
||||
}
|
||||
|
||||
.facilities {
|
||||
font-family: var(--typography-Body-Bold-fontFamily);
|
||||
font-family: var(--Body-Paragraph-Font-family), var(--Body-Paragraph-Font-fallback);
|
||||
padding-bottom: var(--Space-x3);
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -177,12 +177,12 @@ export function SelectHotelMapContent({
|
||||
>
|
||||
<MaterialIcon icon="close" size={20} color="CurrentColor" />
|
||||
<Typography variant="Body/Supporting text (caption)/smBold">
|
||||
<p>
|
||||
<span>
|
||||
{intl.formatMessage({
|
||||
id: "selectHotel.closeMap",
|
||||
defaultMessage: "Close the map",
|
||||
})}
|
||||
</p>
|
||||
</span>
|
||||
</Typography>
|
||||
</Link>
|
||||
</Button>
|
||||
|
||||
+1
-1
@@ -29,7 +29,7 @@
|
||||
.link {
|
||||
display: flex;
|
||||
gap: var(--Space-x05);
|
||||
align-items: baseline;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.bookingCodeFilter {
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { formatPrice } from "@scandic-hotels/common/utils/numberFormatting"
|
||||
import Footnote from "@scandic-hotels/design-system/Footnote"
|
||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||
|
||||
import { useBookingFlowConfig } from "../../bookingFlowConfig/bookingFlowConfigContext"
|
||||
@@ -37,9 +36,11 @@ export default function SignupPromoDesktop({
|
||||
data-testid="signup-promo-desktop"
|
||||
>
|
||||
{badgeContent && <span className={styles.badge}>{badgeContent}</span>}
|
||||
<Footnote color="burgundy">
|
||||
<Message price={price} isEnterDetailsPage={isEnterDetailsPage} />
|
||||
</Footnote>
|
||||
<Typography variant="Body/Supporting text (caption)/smRegular">
|
||||
<p>
|
||||
<Message price={price} isEnterDetailsPage={isEnterDetailsPage} />
|
||||
</p>
|
||||
</Typography>
|
||||
</div>
|
||||
) : null
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import Footnote from "@scandic-hotels/design-system/Footnote"
|
||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||
|
||||
import { useBookingFlowConfig } from "../../bookingFlowConfig/bookingFlowConfigContext"
|
||||
|
||||
@@ -14,9 +14,11 @@ export default function SignupPromoMobile() {
|
||||
data-footer-spacing-signup
|
||||
className={styles.memberDiscountBannerMobile}
|
||||
>
|
||||
<Footnote color="burgundy">
|
||||
<Message />
|
||||
</Footnote>
|
||||
<Typography variant="Body/Supporting text (caption)/smRegular">
|
||||
<p>
|
||||
<Message />
|
||||
</p>
|
||||
</Typography>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user