29 Commits

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

* Remove refresh preventing redirect


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

* Replace scandic banner image and reduce size


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

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

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


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

* fix(STAY-138): center text


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

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

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

* fix(BOOK-755): fix issue phonenumber


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

* chore(BOOK-773): Replaced body typography

* chore(BOOK-773): Replaced caption typography

* chore(BOOK-773): Replaced footnote typography

* chore(BOOK-773): Replaced subtitle typography


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

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

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


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

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


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

* wip new flow

* More wip

* More wip

* Wip styling

* wip with a mutation

* Actually fetch booking data

* More styling wip

* Fix toast duration

* fix loading a11y maybe

* More stuff

* Add feature flag

* Add invalid state

* Clean up

* Add fields for missing user info

* Restructure files

* Add todos

* Disable warning

* Fix icon and border radius


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

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

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

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

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


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

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

* Correct swc version

* Readme

* No turbopack

* test

* test

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

* revert nextjs upgrade

* Fix revert


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

* WIP

* wip

* wip

* parse dates in UTC

* wip

* no more errors

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

* .

* cleanup

* import named z from zod

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


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

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

* Correct swc version


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

* fix: only make stay transactions links

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


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

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

* fixed text in serachList not wrapping properly

* fixed spacing on mobile searchList

* fixed close button icon color

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

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

* fix: add correct link

* fix: move route


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

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

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

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

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

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

* fix(LOY-391): utc fix

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

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


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

* Restructure My Stay page to avoid data fetching waterfalls


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

* Add keys to fix focus


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

* Change default icon style to rounded


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

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

* Split cancellation label into two

* Fix copy


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

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


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

Before

Width:  |  Height:  |  Size: 847 KiB

After

Width:  |  Height:  |  Size: 301 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 MiB

After

Width:  |  Height:  |  Size: 208 KiB

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

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