+
+
+ )
+}
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 {}