From f045fe4a8a69783b497b5c480d1af87b58a1c479 Mon Sep 17 00:00:00 2001 From: Erik Tiekstra Date: Thu, 6 Mar 2025 13:31:37 +0000 Subject: [PATCH] Merged in feat/SW-1555-jobylon-integration (pull request #1484) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Feat/SW-1555 jobylon integration * feat(SW-1555): Added jobylon feed query * feat(SW-1555): Added jobylon feed component Approved-by: Fredrik Thorsson Approved-by: Matilda Landström --- .../JobylonFeed/JobylonCard/index.tsx | 57 +++++++++ .../JobylonCard/jobylonCard.module.css | 21 +++ .../DynamicContent/JobylonFeed/index.tsx | 60 +++++++++ .../JobylonFeed/jobylonFeed.module.css | 10 ++ .../Blocks/DynamicContent/index.tsx | 4 + apps/scandic-web/i18n/dictionaries/da.json | 6 +- apps/scandic-web/i18n/dictionaries/de.json | 6 +- apps/scandic-web/i18n/dictionaries/en.json | 6 +- apps/scandic-web/i18n/dictionaries/fi.json | 6 +- apps/scandic-web/i18n/dictionaries/no.json | 6 +- apps/scandic-web/i18n/dictionaries/sv.json | 6 +- apps/scandic-web/lib/dt.ts | 2 + .../lib/trpc/memoizedRequests/index.ts | 4 + .../server/routers/partners/index.ts | 6 +- .../server/routers/partners/jobylon/output.ts | 121 ++++++++++++++++++ .../server/routers/partners/jobylon/query.ts | 94 ++++++++++++++ .../routers/partners/jobylon/telemetry.ts | 10 ++ .../scandic-web/types/enums/dynamicContent.ts | 10 +- .../types/trpc/routers/jobylon/index.ts | 5 + 19 files changed, 429 insertions(+), 11 deletions(-) create mode 100644 apps/scandic-web/components/Blocks/DynamicContent/JobylonFeed/JobylonCard/index.tsx create mode 100644 apps/scandic-web/components/Blocks/DynamicContent/JobylonFeed/JobylonCard/jobylonCard.module.css create mode 100644 apps/scandic-web/components/Blocks/DynamicContent/JobylonFeed/index.tsx create mode 100644 apps/scandic-web/components/Blocks/DynamicContent/JobylonFeed/jobylonFeed.module.css create mode 100644 apps/scandic-web/server/routers/partners/jobylon/output.ts create mode 100644 apps/scandic-web/server/routers/partners/jobylon/query.ts create mode 100644 apps/scandic-web/server/routers/partners/jobylon/telemetry.ts create mode 100644 apps/scandic-web/types/trpc/routers/jobylon/index.ts diff --git a/apps/scandic-web/components/Blocks/DynamicContent/JobylonFeed/JobylonCard/index.tsx b/apps/scandic-web/components/Blocks/DynamicContent/JobylonFeed/JobylonCard/index.tsx new file mode 100644 index 000000000..883ddfac0 --- /dev/null +++ b/apps/scandic-web/components/Blocks/DynamicContent/JobylonFeed/JobylonCard/index.tsx @@ -0,0 +1,57 @@ +import { OpenInNewSmallIcon } from "@/components/Icons" +import Button from "@/components/TempDesignSystem/Button" +import Caption from "@/components/TempDesignSystem/Text/Caption" +import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" +import { getIntl } from "@/i18n" +import { getLang } from "@/i18n/serverContext" + +import styles from "./jobylonCard.module.css" + +import type { JobylonItem } from "@/types/trpc/routers/jobylon" + +interface JobylonCardProps { + job: JobylonItem +} + +export default async function JobylonCard({ job }: JobylonCardProps) { + const intl = await getIntl() + const lang = getLang() + const deadlineText = job.toDate + ? intl.formatMessage( + { id: "Deadline: {date}" }, + { date: job.toDate.locale(lang).format("Do MMMM") } + ) + : intl.formatMessage({ id: "Open for application" }) + + return ( +
+ +

{job.title}

+
+ +
+
+ {job.categories.map((cat) => cat.text).join(", ")} + + {job.locations + .map((loc) => `${loc.city}, ${loc.country}`) + .join(" | ")} + + {deadlineText} +
+ +
+
+ ) +} diff --git a/apps/scandic-web/components/Blocks/DynamicContent/JobylonFeed/JobylonCard/jobylonCard.module.css b/apps/scandic-web/components/Blocks/DynamicContent/JobylonFeed/JobylonCard/jobylonCard.module.css new file mode 100644 index 000000000..148122cd9 --- /dev/null +++ b/apps/scandic-web/components/Blocks/DynamicContent/JobylonFeed/JobylonCard/jobylonCard.module.css @@ -0,0 +1,21 @@ +.jobylonCard { + display: grid; + width: 100%; + padding: var(--Spacing-x2); + background-color: var(--Base-Surface-Primary-light-Normal); + border: 1px solid var(--Base-Border-Subtle); + border-radius: var(--Corner-radius-Medium); +} + +.contentWrapper { + display: grid; + gap: var(--Spacing-x1); +} + +@media screen and (min-width: 768px) { + .contentWrapper { + grid-template-columns: 1fr auto; + gap: var(--Spacing-x2); + align-items: end; + } +} diff --git a/apps/scandic-web/components/Blocks/DynamicContent/JobylonFeed/index.tsx b/apps/scandic-web/components/Blocks/DynamicContent/JobylonFeed/index.tsx new file mode 100644 index 000000000..897671117 --- /dev/null +++ b/apps/scandic-web/components/Blocks/DynamicContent/JobylonFeed/index.tsx @@ -0,0 +1,60 @@ +import { getJobylonFeed } from "@/lib/trpc/memoizedRequests" + +import SectionContainer from "@/components/Section/Container" +import SectionHeader from "@/components/Section/Header" +import SectionLink from "@/components/Section/Link" +import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" +import { getIntl } from "@/i18n" + +import JobylonCard from "./JobylonCard" + +import styles from "./jobylonFeed.module.css" + +interface JobylonFeedProps { + title?: string + subtitle?: string + link?: { href: string; text: string } +} + +export default async function JobylonFeed({ + title, + subtitle, + link, +}: JobylonFeedProps) { + const intl = await getIntl() + const allJobs = await getJobylonFeed() + + if (!allJobs) { + return null + } + + return ( + + +
+ + {intl.formatMessage( + { + id: "{count, plural, one {{count} Result} other {{count} Results}}", + }, + { count: allJobs.length } + )} + +
    + {allJobs.map((job) => ( +
  • + +
  • + ))} +
+
+ +
+ ) +} diff --git a/apps/scandic-web/components/Blocks/DynamicContent/JobylonFeed/jobylonFeed.module.css b/apps/scandic-web/components/Blocks/DynamicContent/JobylonFeed/jobylonFeed.module.css new file mode 100644 index 000000000..b72fc5592 --- /dev/null +++ b/apps/scandic-web/components/Blocks/DynamicContent/JobylonFeed/jobylonFeed.module.css @@ -0,0 +1,10 @@ +.list { + list-style: none; + display: grid; + gap: var(--Spacing-x2); +} + +.content { + display: grid; + gap: var(--Spacing-x2); +} diff --git a/apps/scandic-web/components/Blocks/DynamicContent/index.tsx b/apps/scandic-web/components/Blocks/DynamicContent/index.tsx index 97bc876bb..0cf1ec991 100644 --- a/apps/scandic-web/components/Blocks/DynamicContent/index.tsx +++ b/apps/scandic-web/components/Blocks/DynamicContent/index.tsx @@ -19,6 +19,8 @@ import SoonestStays from "@/components/Blocks/DynamicContent/Stays/Soonest" import UpcomingStays from "@/components/Blocks/DynamicContent/Stays/Upcoming" import LoadingSpinner from "@/components/LoadingSpinner" +import JobylonFeed from "./JobylonFeed" + import type { DynamicContentProps } from "@/types/components/blocks/dynamicContent" import { DynamicContentEnum } from "@/types/enums/dynamicContent" @@ -45,6 +47,8 @@ function DynamicContentBlocks(props: DynamicContentProps) { return ( ) + case DynamicContentEnum.Blocks.components.jobylon_feed: + return case DynamicContentEnum.Blocks.components.loyalty_levels: return ( Included (based on availability)": "Inkluderet (baseret på tilgængelighed)", "Total price (incl VAT)": "Samlet pris (inkl. moms)", @@ -56,6 +55,7 @@ "Are you sure you want to cancel your stay at {hotel} from {checkInDate} to {checkOutDate}? This can't be reversed.": "Er du sikker på, at du vil annullere dit ophold hos {hotel} fra {checkInDate} til {checkOutDate}? Dette kan ikke gendannes.", "Are you sure you want to continue with the cancellation?": "Er du sikker på, at du vil fortsætte med annullereringen?", "Are you sure you want to remove the card ending with {lastFourDigits} from your member profile?": "Er du sikker på, at du vil fjerne kortet, der slutter me {lastFourDigits} fra din medlemsprofil?", + "Are you sure you want to remove this product?": "Er du sikker på, at du vil fjerne dette produkt?", "Arrival date": "Ankomstdato", "As our Close Friend": "Som vores nære ven", "As our {level}": "Som vores {level}", @@ -182,6 +182,7 @@ "Date of Birth": "Fødselsdato", "Date of birth not matching": "Date of birth not matching", "Day": "Dag", + "Deadline: {date}": "Deadline: {date}", "Delivered at:": "Leveret til:", "Delivery between {deliveryTime}. Payment will be made on check-in": "Levering mellem {deliveryTime}. Betaling vil ske ved check-in.", "Description": "Beskrivelse", @@ -459,6 +460,7 @@ "On your journey": "På din rejse", "Oops! Something went wrong while showing your surprise. Please refresh the page or try again later. If the issue persists, contact the support.": "Ups! Noget gik galt under visningen af din overraskelse. Opdater siden, eller prøv igen senere. Hvis problemet fortsætter, skal du kontakte supporten.", "Open": "Åben", + "Open for application": "Åben for ansøgning", "Open image gallery": "Åbn billedgalleri", "Open language menu": "Åbn sprogmenuen", "Open menu": "Åbn menuen", @@ -681,6 +683,7 @@ "VAT {vat}%": "Moms {vat}%", "Valid through {expirationDate}": "Gyldig til og med {expirationDate}", "Verification code": "Verification code", + "View & apply": "Se og anvend", "View all": "Vis alle", "View all hotels in {country}": "Se alle hoteller i {country}", "View and buy add-ons": "View and buy add-ons", @@ -798,6 +801,7 @@ "{checkOutDate} from {checkOutTime}": "{checkOutDate} fra {checkOutTime}", "{count, plural, one {{count} Hotel} other {{count} Hotels}}": "{count, plural, one {# hotel} other {# hoteller}}", "{count, plural, one {{count} Location} other {{count} Locations}}": "{count, plural, one {# sted} other {# steder}}", + "{count, plural, one {{count} Result} other {{count} Results}}": "{count, plural, one {{count} Result} other {{count} Results}}", "{count} destinations": "{count} destinationer", "{count} hotels": "{count} hotels", "{count} lowercase letter": "{count} lille bogstav", diff --git a/apps/scandic-web/i18n/dictionaries/de.json b/apps/scandic-web/i18n/dictionaries/de.json index 4287e927f..30443958f 100644 --- a/apps/scandic-web/i18n/dictionaries/de.json +++ b/apps/scandic-web/i18n/dictionaries/de.json @@ -1,6 +1,5 @@ { "+46 8 517 517 00": "+46 8 517 517 00", - "Are you sure you want to remove this product?": "Möchten Sie dieses Produkt wirklich entfernen?", "/night per adult": "/Nacht pro Erwachsenem", "Included (based on availability)": "Inbegriffen (je nach Verfügbarkeit)", "Total price (incl VAT)": "Gesamtpreis (inkl. MwSt.)", @@ -56,6 +55,7 @@ "Are you sure you want to cancel your stay at {hotel} from {checkInDate} to {checkOutDate}? This can't be reversed.": "Sind Sie sicher, dass Sie Ihren Aufenthalt bei {hotel} vom {checkInDate} bis {checkOutDate} stornieren möchten? Dies kann nicht rückgängig gemacht werden.", "Are you sure you want to continue with the cancellation?": "Sind Sie sicher, dass Sie mit der Stornierung fortfahren möchten?", "Are you sure you want to remove the card ending with {lastFourDigits} from your member profile?": "Möchten Sie die Karte mit der Endung {lastFourDigits} wirklich aus Ihrem Mitgliedsprofil entfernen?", + "Are you sure you want to remove this product?": "Möchten Sie dieses Produkt wirklich entfernen?", "Arrival date": "Ankunftsdatum", "As our Close Friend": "Als unser enger Freund", "As our {level}": "Als unser {level}", @@ -183,6 +183,7 @@ "Date of Birth": "Geburtsdatum", "Date of birth not matching": "Date of birth not matching", "Day": "Tag", + "Deadline: {date}": "Deadline: {date}", "Delivered at:": "Geliefert bei:", "Delivery between {deliveryTime}. Payment will be made on check-in": "Lieferung zwischen {deliveryTime}. Die Zahlung erfolgt beim Check-in.", "Description": "Beschreibung", @@ -460,6 +461,7 @@ "On your journey": "Auf deiner Reise", "Oops! Something went wrong while showing your surprise. Please refresh the page or try again later. If the issue persists, contact the support.": "Ups! Beim Anzeigen Ihrer Überraschung ist ein Fehler aufgetreten. Bitte aktualisieren Sie die Seite oder versuchen Sie es später erneut. Wenn das Problem weiterhin besteht, kontaktieren Sie den Support.", "Open": "Offen", + "Open for application": "Offen für Bewerbungen", "Open image gallery": "Bildergalerie öffnen", "Open language menu": "Sprachmenü öffnen", "Open menu": "Menü öffnen", @@ -679,6 +681,7 @@ "VAT {vat}%": "MwSt. {vat}%", "Valid through {expirationDate}": "Gültig bis {expirationDate}", "Verification code": "Verification code", + "View & apply": "Ansehen & bewerben", "View all": "Alle anzeigen", "View all hotels in {country}": "Alle Hotels in {country} anzeigen", "View and buy add-ons": "View and buy add-ons", @@ -796,6 +799,7 @@ "{checkOutDate} from {checkOutTime}": "{checkOutDate} aus {checkOutTime}", "{count, plural, one {{count} Hotel} other {{count} Hotels}}": "{count, plural, one {{count} Hotel} other {{count} Hotels}}}", "{count, plural, one {{count} Location} other {{count} Locations}}": "{count, plural, one {# Standort} other {# Standorte}}", + "{count, plural, one {{count} Result} other {{count} Results}}": "{count, plural, one {{count} Ergebnis} other {{count} Ergebnisse}}", "{count} destinations": "{count} Ziele", "{count} hotels": "{count} hotels", "{count} lowercase letter": "{count} Kleinbuchstabe", diff --git a/apps/scandic-web/i18n/dictionaries/en.json b/apps/scandic-web/i18n/dictionaries/en.json index eef6cacb0..eb3b456bb 100644 --- a/apps/scandic-web/i18n/dictionaries/en.json +++ b/apps/scandic-web/i18n/dictionaries/en.json @@ -1,5 +1,4 @@ { - "Are you sure you want to remove this product?": "Are you sure you want to remove this product?", "+46 8 517 517 00": "+46 8 517 517 00", "/night per adult": "/night per adult", "Included (based on availability)": "Included (based on availability)", @@ -55,6 +54,7 @@ "Are you sure you want to cancel your stay at {hotel} from {checkInDate} to {checkOutDate}? This can't be reversed.": "Are you sure you want to cancel your stay at {hotel} from {checkInDate} to {checkOutDate}? This can't be reversed.", "Are you sure you want to continue with the cancellation?": "Are you sure you want to continue with the cancellation?", "Are you sure you want to remove the card ending with {lastFourDigits} from your member profile?": "Are you sure you want to remove the card ending with {lastFourDigits} from your member profile?", + "Are you sure you want to remove this product?": "Are you sure you want to remove this product?", "Arrival date": "Arrival date", "As our Close Friend": "As our Close Friend", "As our {level}": "As our {level}", @@ -181,6 +181,7 @@ "Date of Birth": "Date of Birth", "Date of birth not matching": "Date of birth not matching", "Day": "Day", + "Deadline: {date}": "Deadline: {date}", "Delivered at:": "Delivered at:", "Delivery between {deliveryTime}. Payment will be made on check-in": "Delivery between {deliveryTime}. Payment will be made on check-in.", "Description": "Description", @@ -458,6 +459,7 @@ "On your journey": "On your journey", "Oops! Something went wrong while showing your surprise. Please refresh the page or try again later. If the issue persists, contact the support.": "Oops! Something went wrong while showing your surprise. Please refresh the page or try again later. If the issue persists, contact the support.", "Open": "Open", + "Open for application": "Open for application", "Open image gallery": "Open image gallery", "Open language menu": "Open language menu", "Open menu": "Open menu", @@ -677,6 +679,7 @@ "VAT {vat}%": "VAT {vat}%", "Valid through {expirationDate}": "Valid through {expirationDate}", "Verification code": "Verification code", + "View & apply": "View & apply", "View all": "View all", "View all hotels in {country}": "View all hotels in {country}", "View and buy add-ons": "View and buy add-ons", @@ -791,6 +794,7 @@ "{checkOutDate} from {checkOutTime}": "{checkOutDate} from {checkOutTime}", "{count, plural, one {{count} Hotel} other {{count} Hotels}}": "{count, plural, one {{count} Hotel} other {{count} Hotels}}", "{count, plural, one {{count} Location} other {{count} Locations}}": "{count, plural, one {{count} Location} other {{count} Locations}}", + "{count, plural, one {{count} Result} other {{count} Results}}": "{count, plural, one {{count} Result} other {{count} Results}}", "{count} destinations": "{count} destinations", "{count} hotels": "{count} hotels", "{count} lowercase letter": "{count} lowercase letter", diff --git a/apps/scandic-web/i18n/dictionaries/fi.json b/apps/scandic-web/i18n/dictionaries/fi.json index 2a1752d80..836438ded 100644 --- a/apps/scandic-web/i18n/dictionaries/fi.json +++ b/apps/scandic-web/i18n/dictionaries/fi.json @@ -1,6 +1,5 @@ { "+46 8 517 517 00": "+46 8 517 517 00", - "Are you sure you want to remove this product?": "Haluatko varmasti poistaa tämän tuotteen?", "/night per adult": "/yötä aikuista kohti", "Included (based on availability)": "Sisältyy (saatavuuden mukaan)", "Total price (incl VAT)": "Kokonaishinta (sis. ALV)", @@ -55,6 +54,7 @@ "Are you sure you want to cancel your stay at {hotel} from {checkInDate} to {checkOutDate}? This can't be reversed.": "Oletko varmasti haluamassa peruuttaa majoituksesi hoteleissa {hotel} alkaen {checkInDate} asti {checkOutDate}? Tätä ei voi kumota.", "Are you sure you want to continue with the cancellation?": "Oletko varmasti haluamassa jatkaa peruuttamista?", "Are you sure you want to remove the card ending with {lastFourDigits} from your member profile?": "Haluatko varmasti poistaa kortin, joka päättyy numeroon {lastFourDigits} jäsenprofiilistasi?", + "Are you sure you want to remove this product?": "Haluatko varmasti poistaa tämän tuotteen?", "Arrival date": "Saapumispäivä", "As our Close Friend": "Läheisenä ystävänämme", "As our {level}": "{level}-etu", @@ -182,6 +182,7 @@ "Date of Birth": "Syntymäaika", "Date of birth not matching": "Date of birth not matching", "Day": "Päivä", + "Deadline: {date}": "Määräaika: {date}", "Delivered at:": "Toimitettu:", "Delivery between {deliveryTime}. Payment will be made on check-in": "Toimitus välillä {deliveryTime}. Maksu suoritetaan sisäänkirjautumisen yhteydessä.", "Description": "Kuvaus", @@ -459,6 +460,7 @@ "On your journey": "Matkallasi", "Oops! Something went wrong while showing your surprise. Please refresh the page or try again later. If the issue persists, contact the support.": "Hups! Jotain meni pieleen yllätyksesi näyttämisessä. Päivitä sivu tai yritä myöhemmin uudelleen. Jos ongelma jatkuu, ota yhteyttä tukeen.", "Open": "Avata", + "Open for application": "Avoinna hakemuksille", "Open image gallery": "Avaa kuvagalleria", "Open language menu": "Avaa kielivalikko", "Open menu": "Avaa valikko", @@ -679,6 +681,7 @@ "VAT {vat}%": "ALV {vat}%", "Valid through {expirationDate}": "Voimassa {expirationDate} asti", "Verification code": "Verification code", + "View & apply": "Näytä ja käytä", "View all": "Näytä kaikki", "View all hotels in {country}": "Näytä kaikki hotellit maassa {country}", "View and buy add-ons": "View and buy add-ons", @@ -796,6 +799,7 @@ "{checkOutDate} from {checkOutTime}": "{checkOutDate} alkaen {checkOutTime}", "{count, plural, one {{count} Hotel} other {{count} Hotels}}": "{count, plural, one {# hotelli} other {# hotellit}}", "{count, plural, one {{count} Location} other {{count} Locations}}": "{count, plural, one {# sijainti} other {# sijainnit}}", + "{count, plural, one {{count} Result} other {{count} Results}}": "{count, plural, one {{count} tulos} other {{count} tulosta}}", "{count} destinations": "{count} kohdetta", "{count} hotels": "{count} hotels", "{count} lowercase letter": "{count} pien kirjain", diff --git a/apps/scandic-web/i18n/dictionaries/no.json b/apps/scandic-web/i18n/dictionaries/no.json index 8924ae6d2..79fffb3d5 100644 --- a/apps/scandic-web/i18n/dictionaries/no.json +++ b/apps/scandic-web/i18n/dictionaries/no.json @@ -1,6 +1,5 @@ { "+46 8 517 517 00": "+46 8 517 517 00", - "Are you sure you want to remove this product?": "Er du sikker på at du vil fjerne dette produktet?", "/night per adult": "/natt per voksen", "Included (based on availability)": "Inkludert (basert på tilgjengelighet)", "Total price (incl VAT)": "Totalpris (inkl. mva)", @@ -55,6 +54,7 @@ "Are you sure you want to cancel your stay at {hotel} from {checkInDate} to {checkOutDate}? This can't be reversed.": "Er du sikker på, at du vil annullere dit ophold hos {hotel} fra {checkInDate} til {checkOutDate}? Dette kan ikke gendannes.", "Are you sure you want to continue with the cancellation?": "Er du sikker på, at du vil fortsætte med annullereringen?", "Are you sure you want to remove the card ending with {lastFourDigits} from your member profile?": "Er du sikker på at du vil fjerne kortet som slutter på {lastFourDigits} fra medlemsprofilen din?", + "Are you sure you want to remove this product?": "Er du sikker på at du vil fjerne dette produktet?", "Arrival date": "Ankomstdato", "As our Close Friend": "Som vår nære venn", "As our {level}": "Som vår {level}", @@ -181,6 +181,7 @@ "Date of Birth": "Fødselsdato", "Date of birth not matching": "Date of birth not matching", "Day": "Dag", + "Deadline: {date}": "Frist: {date}", "Delivered at:": "Delivered at:", "Delivery between {deliveryTime}. Payment will be made on check-in": "Levering mellom {deliveryTime}. Betaling vil skje ved innsjekking.", "Description": "Beskrivelse", @@ -458,6 +459,7 @@ "On your journey": "På reisen din", "Oops! Something went wrong while showing your surprise. Please refresh the page or try again later. If the issue persists, contact the support.": "Beklager! Noe gikk galt under visningen av overraskelsen din. Oppdater siden eller prøv igjen senere. Hvis problemet vedvarer, kontakt brukerstøtten.", "Open": "Åpen", + "Open for application": "Åpen for søknad", "Open image gallery": "Åpne bildegalleri", "Open language menu": "Åpne språkmenyen", "Open menu": "Åpne menyen", @@ -677,6 +679,7 @@ "VAT {vat}%": "mva {vat}%", "Valid through {expirationDate}": "Gyldig til og med {expirationDate}", "Verification code": "Verification code", + "View & apply": "Se og bruk", "View all": "Vis alle", "View all hotels in {country}": "Se alle hotellene i {country}", "View and buy add-ons": "View and buy add-ons", @@ -794,6 +797,7 @@ "{checkOutDate} from {checkOutTime}": "{checkOutDate} fra {checkOutTime}", "{count, plural, one {{count} Hotel} other {{count} Hotels}}": "{count, plural, one {# hotell} other {# hoteller}}", "{count, plural, one {{count} Location} other {{count} Locations}}": "{count, plural, one {# sted} other {# steder}}", + "{count, plural, one {{count} Result} other {{count} Results}}": "{count, plural, one {{count} Result} other {{count} Results}}", "{count} destinations": "{count} destinasjoner", "{count} hotels": "{count} hotels", "{count} lowercase letter": "{count} liten bokstav", diff --git a/apps/scandic-web/i18n/dictionaries/sv.json b/apps/scandic-web/i18n/dictionaries/sv.json index 8fe7f8252..53efa67f7 100644 --- a/apps/scandic-web/i18n/dictionaries/sv.json +++ b/apps/scandic-web/i18n/dictionaries/sv.json @@ -1,6 +1,5 @@ { "+46 8 517 517 00": "+46 8 517 517 00", - "Are you sure you want to remove this product?": "Är du säker på att du vill ta bort den här produkten?", "/night per adult": "/natt per vuxen", "Included (based on availability)": "Ingår (baserat på tillgänglighet)", "Total price (incl VAT)": "Totalpris (inkl moms)", @@ -55,6 +54,7 @@ "Are you sure you want to cancel your stay at {hotel} from {checkInDate} to {checkOutDate}? This can't be reversed.": "Är du säker på att du vill avboka din vistelse hos {hotel} från {checkInDate} till {checkOutDate}? Detta kan inte ångras.", "Are you sure you want to continue with the cancellation?": "Är du säker på att du vill fortsätta med avbokningen?", "Are you sure you want to remove the card ending with {lastFourDigits} from your member profile?": "Är du säker på att du vill ta bort kortet som slutar med {lastFourDigits} från din medlemsprofil?", + "Are you sure you want to remove this product?": "Är du säker på att du vill ta bort den här produkten?", "Arrival date": "Ankomstdatum", "As our Close Friend": "Som vår nära vän", "As our {level}": "Som vår {level}", @@ -181,6 +181,7 @@ "Date of Birth": "Födelsedatum", "Date of birth not matching": "Date of birth not matching", "Day": "Dag", + "Deadline: {date}": "Deadline: {date}", "Delivered at:": "Levereras vid:", "Delivery between {deliveryTime}. Payment will be made on check-in": "Leverans mellan {deliveryTime}. Betalning kommer att göras vid incheckning.", "Description": "Beskrivning", @@ -458,6 +459,7 @@ "On your journey": "På din resa", "Oops! Something went wrong while showing your surprise. Please refresh the page or try again later. If the issue persists, contact the support.": "Hoppsan! Något gick fel när din överraskning visades. Uppdatera sidan eller försök igen senare. Om problemet kvarstår, kontakta supporten.", "Open": "Öppna", + "Open for application": "Öppen för ansökan", "Open image gallery": "Öppna bildgalleri", "Open language menu": "Öppna språkmenyn", "Open menu": "Öppna menyn", @@ -677,6 +679,7 @@ "VAT {vat}%": "Moms {vat}%", "Valid through {expirationDate}": "Gäller till och med {expirationDate}", "Verification code": "Verification code", + "View & apply": "Visa & ansök", "View all": "Visa alla", "View all hotels in {country}": "Visa alla hotell i {country}", "View and buy add-ons": "View and buy add-ons", @@ -796,6 +799,7 @@ "{checkOutDate} from {checkOutTime}": "{checkOutDate} från {checkOutTime}", "{count, plural, one {{count} Hotel} other {{count} Hotels}}": "{count, plural, one {# hotell} other {# hotell}}", "{count, plural, one {{count} Location} other {{count} Locations}}": "{count, plural, one {# plats} other {# platser}}", + "{count, plural, one {{count} Result} other {{count} Results}}": "{count, plural, one {{count} Result} other {{count} Results}}", "{count} destinations": "{count} destinationer", "{count} hotels": "{count} hotels", "{count} lowercase letter": "{count} liten bokstav", diff --git a/apps/scandic-web/lib/dt.ts b/apps/scandic-web/lib/dt.ts index 275fdd582..46c0dd927 100644 --- a/apps/scandic-web/lib/dt.ts +++ b/apps/scandic-web/lib/dt.ts @@ -7,6 +7,7 @@ import d from "dayjs" import advancedFormat from "dayjs/plugin/advancedFormat" import duration from "dayjs/plugin/duration" import isSameOrAfter from "dayjs/plugin/isSameOrAfter" +import isSameOrBefore from "dayjs/plugin/isSameOrBefore" import isToday from "dayjs/plugin/isToday" import relativeTime from "dayjs/plugin/relativeTime" import timezone from "dayjs/plugin/timezone" @@ -65,6 +66,7 @@ d.extend(relativeTime) d.extend(timezone) d.extend(utc) d.extend(isSameOrAfter) +d.extend(isSameOrBefore) d.extend(duration) export const dt = d diff --git a/apps/scandic-web/lib/trpc/memoizedRequests/index.ts b/apps/scandic-web/lib/trpc/memoizedRequests/index.ts index f7ba0eac5..39cf0306b 100644 --- a/apps/scandic-web/lib/trpc/memoizedRequests/index.ts +++ b/apps/scandic-web/lib/trpc/memoizedRequests/index.ts @@ -259,3 +259,7 @@ export const isBookingWidgetHidden = cache( return false } ) + +export const getJobylonFeed = cache(async function getMemoizedJobylonFeed() { + return serverClient().partner.jobylon.feed.get() +}) diff --git a/apps/scandic-web/server/routers/partners/index.ts b/apps/scandic-web/server/routers/partners/index.ts index 3f26ae6a3..f1679a468 100644 --- a/apps/scandic-web/server/routers/partners/index.ts +++ b/apps/scandic-web/server/routers/partners/index.ts @@ -1,5 +1,9 @@ import { router } from "@/server/trpc" +import { jobylonQueryRouter } from "./jobylon/query" import { sasRouter } from "./sas" -export const partnerRouter = router({ sas: sasRouter }) +export const partnerRouter = router({ + sas: sasRouter, + jobylon: jobylonQueryRouter, +}) diff --git a/apps/scandic-web/server/routers/partners/jobylon/output.ts b/apps/scandic-web/server/routers/partners/jobylon/output.ts new file mode 100644 index 000000000..94695b49d --- /dev/null +++ b/apps/scandic-web/server/routers/partners/jobylon/output.ts @@ -0,0 +1,121 @@ +import { z } from "zod" + +import { dt } from "@/lib/dt" + +const categoriesSchema = z.array( + z + .object({ category: z.object({ id: z.number(), text: z.string() }) }) + .transform(({ category }) => { + return { + id: category.id, + text: category.text, + } + }) +) + +const departmentsSchema = z + .array( + z + .object({ + department: z.object({ + id: z.number(), + name: z.string(), + }), + }) + .transform(({ department }) => { + if (!department.id || !department.name) { + return null + } + return { + id: department.id, + name: department.name, + } + }) + ) + .transform((departments) => + departments.filter( + (department): department is NonNullable => !!department + ) + ) + +const locationsSchema = z + .array( + z + .object({ + location: z.object({ + city: z.string().nullish(), + country: z.string().nullish(), + place_id: z.string().nullish(), + country_short: z.string().nullish(), + }), + }) + .transform(({ location }) => { + if (!location.city || !location.country) { + return null + } + return { + city: location.city, + country: location.country, + countryShort: location.country_short ?? null, + placeId: location.place_id ?? null, + } + }) + ) + .transform((locations) => + locations.filter( + (location): location is NonNullable => !!location + ) + ) + +const urlsSchema = z + .object({ + apply: z.string(), + ad: z.string(), + }) + .transform(({ ad }) => ad) + +export const jobylonItemSchema = z + .object({ + id: z.number(), + title: z.string(), + from_date: z.string().nullish(), + to_date: z.string().nullish(), + categories: categoriesSchema, + departments: departmentsSchema, + locations: locationsSchema, + urls: urlsSchema, + }) + .transform( + ({ + id, + from_date, + to_date, + title, + categories, + departments, + locations, + urls, + }) => { + const now = dt.utc() + const fromDate = from_date ? dt(from_date) : null + const toDate = to_date ? dt(to_date) : null + + return { + id, + title, + isActive: + fromDate && + now.isSameOrAfter(fromDate) && + (!toDate || now.isSameOrBefore(toDate)), + categories, + departments, + toDate, + locations, + url: urls, + } + } + ) + +export const jobylonFeedSchema = z + .array(jobylonItemSchema) + .transform((jobs) => jobs.filter((job) => job.isActive)) diff --git a/apps/scandic-web/server/routers/partners/jobylon/query.ts b/apps/scandic-web/server/routers/partners/jobylon/query.ts new file mode 100644 index 000000000..9613aed2f --- /dev/null +++ b/apps/scandic-web/server/routers/partners/jobylon/query.ts @@ -0,0 +1,94 @@ +import { publicProcedure, router } from "@/server/trpc" + +import { jobylonFeedSchema } from "./output" +import { + getJobylonFeedCounter, + getJobylonFeedFailCounter, + getJobylonFeedSuccessCounter, +} from "./telemetry" + +export const TWENTYFOUR_HOURS = 60 * 60 * 24 + +// The URL for the Jobylon feed including the hash for the specific feed. +// The URL and hash are generated by Jobylon. Documentation: https://developer.jobylon.com/feed-api +const feedUrl = + "https://feed.jobylon.com/feeds/cc04ba19-f0bd-4412-8b9b-d1d1fcbf0800" + +export const jobylonQueryRouter = router({ + feed: router({ + get: publicProcedure.query(async function () { + const url = new URL(feedUrl) + url.search = new URLSearchParams({ + format: "json", + }).toString() + const urlString = url.toString() + + getJobylonFeedCounter.add(1, { url: urlString }) + console.info( + "jobylon.feed start", + JSON.stringify({ query: { url: urlString } }) + ) + + const response = await fetch(url, { + cache: "force-cache", + next: { + revalidate: TWENTYFOUR_HOURS, + }, + }) + + if (!response.ok) { + const text = await response.text() + const error = { + status: response.status, + statusText: response.statusText, + text, + } + getJobylonFeedFailCounter.add(1, { + url: urlString, + error_type: "http_error", + error: JSON.stringify(error), + }) + console.error( + "jobylon.feed error", + JSON.stringify({ + query: { url: urlString }, + error, + }) + ) + return null + } + + const responseJson = await response.json() + const validatedResponse = jobylonFeedSchema.safeParse(responseJson) + + if (!validatedResponse.success) { + getJobylonFeedFailCounter.add(1, { + urlString, + error_type: "validation_error", + error: JSON.stringify(validatedResponse.error), + }) + + console.error( + "jobylon.feed error", + JSON.stringify({ + query: { url: urlString }, + error: validatedResponse.error, + }) + ) + return null + } + + getJobylonFeedSuccessCounter.add(1, { + url: urlString, + }) + console.info( + "jobylon.feed success", + JSON.stringify({ + query: { url: urlString }, + }) + ) + + return validatedResponse.data + }), + }), +}) diff --git a/apps/scandic-web/server/routers/partners/jobylon/telemetry.ts b/apps/scandic-web/server/routers/partners/jobylon/telemetry.ts new file mode 100644 index 000000000..4434effb3 --- /dev/null +++ b/apps/scandic-web/server/routers/partners/jobylon/telemetry.ts @@ -0,0 +1,10 @@ +import { metrics } from "@opentelemetry/api" + +const meter = metrics.getMeter("trpc.booking") +export const getJobylonFeedCounter = meter.createCounter("trpc.jobylon-feed") +export const getJobylonFeedSuccessCounter = meter.createCounter( + "trpc.jobylon-feed-success" +) +export const getJobylonFeedFailCounter = meter.createCounter( + "trpc.jobylon-feed-fail" +) diff --git a/apps/scandic-web/types/enums/dynamicContent.ts b/apps/scandic-web/types/enums/dynamicContent.ts index 2497b4b68..87c12681b 100644 --- a/apps/scandic-web/types/enums/dynamicContent.ts +++ b/apps/scandic-web/types/enums/dynamicContent.ts @@ -5,6 +5,7 @@ export namespace DynamicContentEnum { earn_and_burn = "earn_and_burn", expiring_points = "expiring_points", how_it_works = "how_it_works", + jobylon_feed = "jobylon_feed", loyalty_levels = "loyalty_levels", membership_overview = "membership_overview", my_points = "my_points", @@ -12,12 +13,12 @@ export namespace DynamicContentEnum { overview_table = "overview_table", points_overview = "points_overview", previous_stays = "previous_stays", + sas_linked_account = "sas_linked_account", + sas_tier_comparison = "sas_tier_comparison", sign_up_form = "sign_up_form", sign_up_verification = "sign_up_verification", soonest_stays = "soonest_stays", upcoming_stays = "upcoming_stays", - sas_linked_account = "sas_linked_account", - sas_tier_comparison = "sas_tier_comparison", } /** Type needed to satisfy zod enum type */ @@ -26,6 +27,7 @@ export namespace DynamicContentEnum { components.earn_and_burn, components.expiring_points, components.how_it_works, + components.jobylon_feed, components.loyalty_levels, components.membership_overview, components.my_points, @@ -33,12 +35,12 @@ export namespace DynamicContentEnum { components.overview_table, components.points_overview, components.previous_stays, + components.sas_linked_account, + components.sas_tier_comparison, components.sign_up_form, components.sign_up_verification, components.soonest_stays, components.upcoming_stays, - components.sas_linked_account, - components.sas_tier_comparison, ] } diff --git a/apps/scandic-web/types/trpc/routers/jobylon/index.ts b/apps/scandic-web/types/trpc/routers/jobylon/index.ts new file mode 100644 index 000000000..524a7ab70 --- /dev/null +++ b/apps/scandic-web/types/trpc/routers/jobylon/index.ts @@ -0,0 +1,5 @@ +import type { z } from "zod" + +import type { jobylonItemSchema } from "@/server/routers/partners/jobylon/output" + +export interface JobylonItem extends z.output {}