Merged in feat/SW-1555-jobylon-integration (pull request #1484)

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
This commit is contained in:
Erik Tiekstra
2025-03-06 13:31:37 +00:00
parent 7f5085f855
commit f045fe4a8a
19 changed files with 429 additions and 11 deletions

View File

@@ -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 (
<div className={styles.jobylonCard}>
<Subtitle asChild>
<h3>{job.title}</h3>
</Subtitle>
<div className={styles.contentWrapper}>
<div className={styles.content}>
<Caption>{job.categories.map((cat) => cat.text).join(", ")}</Caption>
<Caption>
{job.locations
.map((loc) => `${loc.city}, ${loc.country}`)
.join(" | ")}
</Caption>
<Caption color="uiTextPlaceholder">{deadlineText}</Caption>
</div>
<Button
theme="base"
size="small"
intent="tertiary"
variant="icon"
asChild
>
<a href={job.url} target="_blank" rel="noopener noreferrer">
{intl.formatMessage({ id: "View & apply" })}
<OpenInNewSmallIcon />
</a>
</Button>
</div>
</div>
)
}

View File

@@ -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;
}
}

View File

@@ -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 (
<SectionContainer>
<SectionHeader
link={link}
preamble={subtitle}
title={title}
headingAs="h3"
headingLevel="h2"
/>
<div className={styles.content}>
<Subtitle type="two">
{intl.formatMessage(
{
id: "{count, plural, one {{count} Result} other {{count} Results}}",
},
{ count: allJobs.length }
)}
</Subtitle>
<ul className={styles.list}>
{allJobs.map((job) => (
<li key={job.id}>
<JobylonCard job={job} />
</li>
))}
</ul>
</div>
<SectionLink link={link} variant="mobile" />
</SectionContainer>
)
}

View File

@@ -0,0 +1,10 @@
.list {
list-style: none;
display: grid;
gap: var(--Spacing-x2);
}
.content {
display: grid;
gap: var(--Spacing-x2);
}

View File

@@ -19,6 +19,8 @@ import SoonestStays from "@/components/Blocks/DynamicContent/Stays/Soonest"
import UpcomingStays from "@/components/Blocks/DynamicContent/Stays/Upcoming" import UpcomingStays from "@/components/Blocks/DynamicContent/Stays/Upcoming"
import LoadingSpinner from "@/components/LoadingSpinner" import LoadingSpinner from "@/components/LoadingSpinner"
import JobylonFeed from "./JobylonFeed"
import type { DynamicContentProps } from "@/types/components/blocks/dynamicContent" import type { DynamicContentProps } from "@/types/components/blocks/dynamicContent"
import { DynamicContentEnum } from "@/types/enums/dynamicContent" import { DynamicContentEnum } from "@/types/enums/dynamicContent"
@@ -45,6 +47,8 @@ function DynamicContentBlocks(props: DynamicContentProps) {
return ( return (
<HowItWorks dynamic_content={dynamic_content} firstItem={firstItem} /> <HowItWorks dynamic_content={dynamic_content} firstItem={firstItem} />
) )
case DynamicContentEnum.Blocks.components.jobylon_feed:
return <JobylonFeed {...dynamic_content} />
case DynamicContentEnum.Blocks.components.loyalty_levels: case DynamicContentEnum.Blocks.components.loyalty_levels:
return ( return (
<LoyaltyLevels <LoyaltyLevels

View File

@@ -1,6 +1,5 @@
{ {
"+46 8 517 517 00": "+46 8 517 517 00", "+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 produkt?",
"/night per adult": "/nat per voksen", "/night per adult": "/nat per voksen",
"<b>Included</b> (based on availability)": "<b>Inkluderet</b> (baseret på tilgængelighed)", "<b>Included</b> (based on availability)": "<b>Inkluderet</b> (baseret på tilgængelighed)",
"<b>Total price</b> (incl VAT)": "<b>Samlet pris</b> (inkl. moms)", "<b>Total price</b> (incl VAT)": "<b>Samlet pris</b> (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 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 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 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", "Arrival date": "Ankomstdato",
"As our Close Friend": "Som vores nære ven", "As our Close Friend": "Som vores nære ven",
"As our {level}": "Som vores {level}", "As our {level}": "Som vores {level}",
@@ -182,6 +182,7 @@
"Date of Birth": "Fødselsdato", "Date of Birth": "Fødselsdato",
"Date of birth not matching": "Date of birth not matching", "Date of birth not matching": "Date of birth not matching",
"Day": "Dag", "Day": "Dag",
"Deadline: {date}": "Deadline: {date}",
"Delivered at:": "Leveret til:", "Delivered at:": "Leveret til:",
"Delivery between {deliveryTime}. Payment will be made on check-in": "Levering mellem {deliveryTime}. Betaling vil ske ved check-in.", "Delivery between {deliveryTime}. Payment will be made on check-in": "Levering mellem {deliveryTime}. Betaling vil ske ved check-in.",
"Description": "Beskrivelse", "Description": "Beskrivelse",
@@ -459,6 +460,7 @@
"On your journey": "På din rejse", "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, <link>contact the support.</link>": "Ups! Noget gik galt under visningen af din overraskelse. Opdater siden, eller prøv igen senere. Hvis problemet fortsætter, skal du <link>kontakte supporten.</link>", "Oops! Something went wrong while showing your surprise. Please refresh the page or try again later. If the issue persists, <link>contact the support.</link>": "Ups! Noget gik galt under visningen af din overraskelse. Opdater siden, eller prøv igen senere. Hvis problemet fortsætter, skal du <link>kontakte supporten.</link>",
"Open": "Åben", "Open": "Åben",
"Open for application": "Åben for ansøgning",
"Open image gallery": "Åbn billedgalleri", "Open image gallery": "Åbn billedgalleri",
"Open language menu": "Åbn sprogmenuen", "Open language menu": "Åbn sprogmenuen",
"Open menu": "Åbn menuen", "Open menu": "Åbn menuen",
@@ -681,6 +683,7 @@
"VAT {vat}%": "Moms {vat}%", "VAT {vat}%": "Moms {vat}%",
"Valid through {expirationDate}": "Gyldig til og med {expirationDate}", "Valid through {expirationDate}": "Gyldig til og med {expirationDate}",
"Verification code": "Verification code", "Verification code": "Verification code",
"View & apply": "Se og anvend",
"View all": "Vis alle", "View all": "Vis alle",
"View all hotels in {country}": "Se alle hoteller i {country}", "View all hotels in {country}": "Se alle hoteller i {country}",
"View and buy add-ons": "View and buy add-ons", "View and buy add-ons": "View and buy add-ons",
@@ -798,6 +801,7 @@
"{checkOutDate} from {checkOutTime}": "{checkOutDate} fra {checkOutTime}", "{checkOutDate} from {checkOutTime}": "{checkOutDate} fra {checkOutTime}",
"{count, plural, one {{count} Hotel} other {{count} Hotels}}": "{count, plural, one {# hotel} other {# hoteller}}", "{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} 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} destinations": "{count} destinationer",
"{count} hotels": "{count} hotels", "{count} hotels": "{count} hotels",
"{count} lowercase letter": "{count} lille bogstav", "{count} lowercase letter": "{count} lille bogstav",

View File

@@ -1,6 +1,5 @@
{ {
"+46 8 517 517 00": "+46 8 517 517 00", "+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", "/night per adult": "/Nacht pro Erwachsenem",
"<b>Included</b> (based on availability)": "<b>Inbegriffen</b> (je nach Verfügbarkeit)", "<b>Included</b> (based on availability)": "<b>Inbegriffen</b> (je nach Verfügbarkeit)",
"<b>Total price</b> (incl VAT)": "<b>Gesamtpreis</b> (inkl. MwSt.)", "<b>Total price</b> (incl VAT)": "<b>Gesamtpreis</b> (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 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 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 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", "Arrival date": "Ankunftsdatum",
"As our Close Friend": "Als unser enger Freund", "As our Close Friend": "Als unser enger Freund",
"As our {level}": "Als unser {level}", "As our {level}": "Als unser {level}",
@@ -183,6 +183,7 @@
"Date of Birth": "Geburtsdatum", "Date of Birth": "Geburtsdatum",
"Date of birth not matching": "Date of birth not matching", "Date of birth not matching": "Date of birth not matching",
"Day": "Tag", "Day": "Tag",
"Deadline: {date}": "Deadline: {date}",
"Delivered at:": "Geliefert bei:", "Delivered at:": "Geliefert bei:",
"Delivery between {deliveryTime}. Payment will be made on check-in": "Lieferung zwischen {deliveryTime}. Die Zahlung erfolgt beim Check-in.", "Delivery between {deliveryTime}. Payment will be made on check-in": "Lieferung zwischen {deliveryTime}. Die Zahlung erfolgt beim Check-in.",
"Description": "Beschreibung", "Description": "Beschreibung",
@@ -460,6 +461,7 @@
"On your journey": "Auf deiner Reise", "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, <link>contact the support.</link>": "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, <link>kontaktieren Sie den Support.</link>", "Oops! Something went wrong while showing your surprise. Please refresh the page or try again later. If the issue persists, <link>contact the support.</link>": "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, <link>kontaktieren Sie den Support.</link>",
"Open": "Offen", "Open": "Offen",
"Open for application": "Offen für Bewerbungen",
"Open image gallery": "Bildergalerie öffnen", "Open image gallery": "Bildergalerie öffnen",
"Open language menu": "Sprachmenü öffnen", "Open language menu": "Sprachmenü öffnen",
"Open menu": "Menü öffnen", "Open menu": "Menü öffnen",
@@ -679,6 +681,7 @@
"VAT {vat}%": "MwSt. {vat}%", "VAT {vat}%": "MwSt. {vat}%",
"Valid through {expirationDate}": "Gültig bis {expirationDate}", "Valid through {expirationDate}": "Gültig bis {expirationDate}",
"Verification code": "Verification code", "Verification code": "Verification code",
"View & apply": "Ansehen & bewerben",
"View all": "Alle anzeigen", "View all": "Alle anzeigen",
"View all hotels in {country}": "Alle Hotels in {country} anzeigen", "View all hotels in {country}": "Alle Hotels in {country} anzeigen",
"View and buy add-ons": "View and buy add-ons", "View and buy add-ons": "View and buy add-ons",
@@ -796,6 +799,7 @@
"{checkOutDate} from {checkOutTime}": "{checkOutDate} aus {checkOutTime}", "{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} 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} 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} destinations": "{count} Ziele",
"{count} hotels": "{count} hotels", "{count} hotels": "{count} hotels",
"{count} lowercase letter": "{count} Kleinbuchstabe", "{count} lowercase letter": "{count} Kleinbuchstabe",

View File

@@ -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", "+46 8 517 517 00": "+46 8 517 517 00",
"/night per adult": "/night per adult", "/night per adult": "/night per adult",
"<b>Included</b> (based on availability)": "<b>Included</b> (based on availability)", "<b>Included</b> (based on availability)": "<b>Included</b> (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 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 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 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", "Arrival date": "Arrival date",
"As our Close Friend": "As our Close Friend", "As our Close Friend": "As our Close Friend",
"As our {level}": "As our {level}", "As our {level}": "As our {level}",
@@ -181,6 +181,7 @@
"Date of Birth": "Date of Birth", "Date of Birth": "Date of Birth",
"Date of birth not matching": "Date of birth not matching", "Date of birth not matching": "Date of birth not matching",
"Day": "Day", "Day": "Day",
"Deadline: {date}": "Deadline: {date}",
"Delivered at:": "Delivered at:", "Delivered at:": "Delivered at:",
"Delivery between {deliveryTime}. Payment will be made on check-in": "Delivery between {deliveryTime}. Payment will be made on check-in.", "Delivery between {deliveryTime}. Payment will be made on check-in": "Delivery between {deliveryTime}. Payment will be made on check-in.",
"Description": "Description", "Description": "Description",
@@ -458,6 +459,7 @@
"On your journey": "On your journey", "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, <link>contact the support.</link>": "Oops! Something went wrong while showing your surprise. Please refresh the page or try again later. If the issue persists, <link>contact the support.</link>", "Oops! Something went wrong while showing your surprise. Please refresh the page or try again later. If the issue persists, <link>contact the support.</link>": "Oops! Something went wrong while showing your surprise. Please refresh the page or try again later. If the issue persists, <link>contact the support.</link>",
"Open": "Open", "Open": "Open",
"Open for application": "Open for application",
"Open image gallery": "Open image gallery", "Open image gallery": "Open image gallery",
"Open language menu": "Open language menu", "Open language menu": "Open language menu",
"Open menu": "Open menu", "Open menu": "Open menu",
@@ -677,6 +679,7 @@
"VAT {vat}%": "VAT {vat}%", "VAT {vat}%": "VAT {vat}%",
"Valid through {expirationDate}": "Valid through {expirationDate}", "Valid through {expirationDate}": "Valid through {expirationDate}",
"Verification code": "Verification code", "Verification code": "Verification code",
"View & apply": "View & apply",
"View all": "View all", "View all": "View all",
"View all hotels in {country}": "View all hotels in {country}", "View all hotels in {country}": "View all hotels in {country}",
"View and buy add-ons": "View and buy add-ons", "View and buy add-ons": "View and buy add-ons",
@@ -791,6 +794,7 @@
"{checkOutDate} from {checkOutTime}": "{checkOutDate} from {checkOutTime}", "{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} 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} 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} destinations": "{count} destinations",
"{count} hotels": "{count} hotels", "{count} hotels": "{count} hotels",
"{count} lowercase letter": "{count} lowercase letter", "{count} lowercase letter": "{count} lowercase letter",

View File

@@ -1,6 +1,5 @@
{ {
"+46 8 517 517 00": "+46 8 517 517 00", "+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", "/night per adult": "/yötä aikuista kohti",
"<b>Included</b> (based on availability)": "<b>Sisältyy</b> (saatavuuden mukaan)", "<b>Included</b> (based on availability)": "<b>Sisältyy</b> (saatavuuden mukaan)",
"<b>Total price</b> (incl VAT)": "<b>Kokonaishinta</b> (sis. ALV)", "<b>Total price</b> (incl VAT)": "<b>Kokonaishinta</b> (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 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 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 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ä", "Arrival date": "Saapumispäivä",
"As our Close Friend": "Läheisenä ystävänämme", "As our Close Friend": "Läheisenä ystävänämme",
"As our {level}": "{level}-etu", "As our {level}": "{level}-etu",
@@ -182,6 +182,7 @@
"Date of Birth": "Syntymäaika", "Date of Birth": "Syntymäaika",
"Date of birth not matching": "Date of birth not matching", "Date of birth not matching": "Date of birth not matching",
"Day": "Päivä", "Day": "Päivä",
"Deadline: {date}": "Määräaika: {date}",
"Delivered at:": "Toimitettu:", "Delivered at:": "Toimitettu:",
"Delivery between {deliveryTime}. Payment will be made on check-in": "Toimitus välillä {deliveryTime}. Maksu suoritetaan sisäänkirjautumisen yhteydessä.", "Delivery between {deliveryTime}. Payment will be made on check-in": "Toimitus välillä {deliveryTime}. Maksu suoritetaan sisäänkirjautumisen yhteydessä.",
"Description": "Kuvaus", "Description": "Kuvaus",
@@ -459,6 +460,7 @@
"On your journey": "Matkallasi", "On your journey": "Matkallasi",
"Oops! Something went wrong while showing your surprise. Please refresh the page or try again later. If the issue persists, <link>contact the support.</link>": "Hups! Jotain meni pieleen yllätyksesi näyttämisessä. Päivitä sivu tai yritä myöhemmin uudelleen. Jos ongelma jatkuu, <link>ota yhteyttä tukeen.</link>", "Oops! Something went wrong while showing your surprise. Please refresh the page or try again later. If the issue persists, <link>contact the support.</link>": "Hups! Jotain meni pieleen yllätyksesi näyttämisessä. Päivitä sivu tai yritä myöhemmin uudelleen. Jos ongelma jatkuu, <link>ota yhteyttä tukeen.</link>",
"Open": "Avata", "Open": "Avata",
"Open for application": "Avoinna hakemuksille",
"Open image gallery": "Avaa kuvagalleria", "Open image gallery": "Avaa kuvagalleria",
"Open language menu": "Avaa kielivalikko", "Open language menu": "Avaa kielivalikko",
"Open menu": "Avaa valikko", "Open menu": "Avaa valikko",
@@ -679,6 +681,7 @@
"VAT {vat}%": "ALV {vat}%", "VAT {vat}%": "ALV {vat}%",
"Valid through {expirationDate}": "Voimassa {expirationDate} asti", "Valid through {expirationDate}": "Voimassa {expirationDate} asti",
"Verification code": "Verification code", "Verification code": "Verification code",
"View & apply": "Näytä ja käytä",
"View all": "Näytä kaikki", "View all": "Näytä kaikki",
"View all hotels in {country}": "Näytä kaikki hotellit maassa {country}", "View all hotels in {country}": "Näytä kaikki hotellit maassa {country}",
"View and buy add-ons": "View and buy add-ons", "View and buy add-ons": "View and buy add-ons",
@@ -796,6 +799,7 @@
"{checkOutDate} from {checkOutTime}": "{checkOutDate} alkaen {checkOutTime}", "{checkOutDate} from {checkOutTime}": "{checkOutDate} alkaen {checkOutTime}",
"{count, plural, one {{count} Hotel} other {{count} Hotels}}": "{count, plural, one {# hotelli} other {# hotellit}}", "{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} 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} destinations": "{count} kohdetta",
"{count} hotels": "{count} hotels", "{count} hotels": "{count} hotels",
"{count} lowercase letter": "{count} pien kirjain", "{count} lowercase letter": "{count} pien kirjain",

View File

@@ -1,6 +1,5 @@
{ {
"+46 8 517 517 00": "+46 8 517 517 00", "+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", "/night per adult": "/natt per voksen",
"<b>Included</b> (based on availability)": "<b>Inkludert</b> (basert på tilgjengelighet)", "<b>Included</b> (based on availability)": "<b>Inkludert</b> (basert på tilgjengelighet)",
"<b>Total price</b> (incl VAT)": "<b>Totalpris</b> (inkl. mva)", "<b>Total price</b> (incl VAT)": "<b>Totalpris</b> (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 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 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 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", "Arrival date": "Ankomstdato",
"As our Close Friend": "Som vår nære venn", "As our Close Friend": "Som vår nære venn",
"As our {level}": "Som vår {level}", "As our {level}": "Som vår {level}",
@@ -181,6 +181,7 @@
"Date of Birth": "Fødselsdato", "Date of Birth": "Fødselsdato",
"Date of birth not matching": "Date of birth not matching", "Date of birth not matching": "Date of birth not matching",
"Day": "Dag", "Day": "Dag",
"Deadline: {date}": "Frist: {date}",
"Delivered at:": "Delivered at:", "Delivered at:": "Delivered at:",
"Delivery between {deliveryTime}. Payment will be made on check-in": "Levering mellom {deliveryTime}. Betaling vil skje ved innsjekking.", "Delivery between {deliveryTime}. Payment will be made on check-in": "Levering mellom {deliveryTime}. Betaling vil skje ved innsjekking.",
"Description": "Beskrivelse", "Description": "Beskrivelse",
@@ -458,6 +459,7 @@
"On your journey": "På reisen din", "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, <link>contact the support.</link>": "Beklager! Noe gikk galt under visningen av overraskelsen din. Oppdater siden eller prøv igjen senere. Hvis problemet vedvarer, <link>kontakt brukerstøtten.</link>", "Oops! Something went wrong while showing your surprise. Please refresh the page or try again later. If the issue persists, <link>contact the support.</link>": "Beklager! Noe gikk galt under visningen av overraskelsen din. Oppdater siden eller prøv igjen senere. Hvis problemet vedvarer, <link>kontakt brukerstøtten.</link>",
"Open": "Åpen", "Open": "Åpen",
"Open for application": "Åpen for søknad",
"Open image gallery": "Åpne bildegalleri", "Open image gallery": "Åpne bildegalleri",
"Open language menu": "Åpne språkmenyen", "Open language menu": "Åpne språkmenyen",
"Open menu": "Åpne menyen", "Open menu": "Åpne menyen",
@@ -677,6 +679,7 @@
"VAT {vat}%": "mva {vat}%", "VAT {vat}%": "mva {vat}%",
"Valid through {expirationDate}": "Gyldig til og med {expirationDate}", "Valid through {expirationDate}": "Gyldig til og med {expirationDate}",
"Verification code": "Verification code", "Verification code": "Verification code",
"View & apply": "Se og bruk",
"View all": "Vis alle", "View all": "Vis alle",
"View all hotels in {country}": "Se alle hotellene i {country}", "View all hotels in {country}": "Se alle hotellene i {country}",
"View and buy add-ons": "View and buy add-ons", "View and buy add-ons": "View and buy add-ons",
@@ -794,6 +797,7 @@
"{checkOutDate} from {checkOutTime}": "{checkOutDate} fra {checkOutTime}", "{checkOutDate} from {checkOutTime}": "{checkOutDate} fra {checkOutTime}",
"{count, plural, one {{count} Hotel} other {{count} Hotels}}": "{count, plural, one {# hotell} other {# hoteller}}", "{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} 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} destinations": "{count} destinasjoner",
"{count} hotels": "{count} hotels", "{count} hotels": "{count} hotels",
"{count} lowercase letter": "{count} liten bokstav", "{count} lowercase letter": "{count} liten bokstav",

View File

@@ -1,6 +1,5 @@
{ {
"+46 8 517 517 00": "+46 8 517 517 00", "+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", "/night per adult": "/natt per vuxen",
"<b>Included</b> (based on availability)": "<b>Ingår</b> (baserat på tillgänglighet)", "<b>Included</b> (based on availability)": "<b>Ingår</b> (baserat på tillgänglighet)",
"<b>Total price</b> (incl VAT)": "<b>Totalpris</b> (inkl moms)", "<b>Total price</b> (incl VAT)": "<b>Totalpris</b> (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 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 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 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", "Arrival date": "Ankomstdatum",
"As our Close Friend": "Som vår nära vän", "As our Close Friend": "Som vår nära vän",
"As our {level}": "Som vår {level}", "As our {level}": "Som vår {level}",
@@ -181,6 +181,7 @@
"Date of Birth": "Födelsedatum", "Date of Birth": "Födelsedatum",
"Date of birth not matching": "Date of birth not matching", "Date of birth not matching": "Date of birth not matching",
"Day": "Dag", "Day": "Dag",
"Deadline: {date}": "Deadline: {date}",
"Delivered at:": "Levereras vid:", "Delivered at:": "Levereras vid:",
"Delivery between {deliveryTime}. Payment will be made on check-in": "Leverans mellan {deliveryTime}. Betalning kommer att göras vid incheckning.", "Delivery between {deliveryTime}. Payment will be made on check-in": "Leverans mellan {deliveryTime}. Betalning kommer att göras vid incheckning.",
"Description": "Beskrivning", "Description": "Beskrivning",
@@ -458,6 +459,7 @@
"On your journey": "På din resa", "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, <link>contact the support.</link>": "Hoppsan! Något gick fel när din överraskning visades. Uppdatera sidan eller försök igen senare. Om problemet kvarstår, <link>kontakta supporten.</link>", "Oops! Something went wrong while showing your surprise. Please refresh the page or try again later. If the issue persists, <link>contact the support.</link>": "Hoppsan! Något gick fel när din överraskning visades. Uppdatera sidan eller försök igen senare. Om problemet kvarstår, <link>kontakta supporten.</link>",
"Open": "Öppna", "Open": "Öppna",
"Open for application": "Öppen för ansökan",
"Open image gallery": "Öppna bildgalleri", "Open image gallery": "Öppna bildgalleri",
"Open language menu": "Öppna språkmenyn", "Open language menu": "Öppna språkmenyn",
"Open menu": "Öppna menyn", "Open menu": "Öppna menyn",
@@ -677,6 +679,7 @@
"VAT {vat}%": "Moms {vat}%", "VAT {vat}%": "Moms {vat}%",
"Valid through {expirationDate}": "Gäller till och med {expirationDate}", "Valid through {expirationDate}": "Gäller till och med {expirationDate}",
"Verification code": "Verification code", "Verification code": "Verification code",
"View & apply": "Visa & ansök",
"View all": "Visa alla", "View all": "Visa alla",
"View all hotels in {country}": "Visa alla hotell i {country}", "View all hotels in {country}": "Visa alla hotell i {country}",
"View and buy add-ons": "View and buy add-ons", "View and buy add-ons": "View and buy add-ons",
@@ -796,6 +799,7 @@
"{checkOutDate} from {checkOutTime}": "{checkOutDate} från {checkOutTime}", "{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} 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} 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} destinations": "{count} destinationer",
"{count} hotels": "{count} hotels", "{count} hotels": "{count} hotels",
"{count} lowercase letter": "{count} liten bokstav", "{count} lowercase letter": "{count} liten bokstav",

View File

@@ -7,6 +7,7 @@ import d from "dayjs"
import advancedFormat from "dayjs/plugin/advancedFormat" import advancedFormat from "dayjs/plugin/advancedFormat"
import duration from "dayjs/plugin/duration" import duration from "dayjs/plugin/duration"
import isSameOrAfter from "dayjs/plugin/isSameOrAfter" import isSameOrAfter from "dayjs/plugin/isSameOrAfter"
import isSameOrBefore from "dayjs/plugin/isSameOrBefore"
import isToday from "dayjs/plugin/isToday" import isToday from "dayjs/plugin/isToday"
import relativeTime from "dayjs/plugin/relativeTime" import relativeTime from "dayjs/plugin/relativeTime"
import timezone from "dayjs/plugin/timezone" import timezone from "dayjs/plugin/timezone"
@@ -65,6 +66,7 @@ d.extend(relativeTime)
d.extend(timezone) d.extend(timezone)
d.extend(utc) d.extend(utc)
d.extend(isSameOrAfter) d.extend(isSameOrAfter)
d.extend(isSameOrBefore)
d.extend(duration) d.extend(duration)
export const dt = d export const dt = d

View File

@@ -259,3 +259,7 @@ export const isBookingWidgetHidden = cache(
return false return false
} }
) )
export const getJobylonFeed = cache(async function getMemoizedJobylonFeed() {
return serverClient().partner.jobylon.feed.get()
})

View File

@@ -1,5 +1,9 @@
import { router } from "@/server/trpc" import { router } from "@/server/trpc"
import { jobylonQueryRouter } from "./jobylon/query"
import { sasRouter } from "./sas" import { sasRouter } from "./sas"
export const partnerRouter = router({ sas: sasRouter }) export const partnerRouter = router({
sas: sasRouter,
jobylon: jobylonQueryRouter,
})

View File

@@ -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<typeof department> => !!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<typeof location> => !!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))

View File

@@ -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
}),
}),
})

View File

@@ -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"
)

View File

@@ -5,6 +5,7 @@ export namespace DynamicContentEnum {
earn_and_burn = "earn_and_burn", earn_and_burn = "earn_and_burn",
expiring_points = "expiring_points", expiring_points = "expiring_points",
how_it_works = "how_it_works", how_it_works = "how_it_works",
jobylon_feed = "jobylon_feed",
loyalty_levels = "loyalty_levels", loyalty_levels = "loyalty_levels",
membership_overview = "membership_overview", membership_overview = "membership_overview",
my_points = "my_points", my_points = "my_points",
@@ -12,12 +13,12 @@ export namespace DynamicContentEnum {
overview_table = "overview_table", overview_table = "overview_table",
points_overview = "points_overview", points_overview = "points_overview",
previous_stays = "previous_stays", previous_stays = "previous_stays",
sas_linked_account = "sas_linked_account",
sas_tier_comparison = "sas_tier_comparison",
sign_up_form = "sign_up_form", sign_up_form = "sign_up_form",
sign_up_verification = "sign_up_verification", sign_up_verification = "sign_up_verification",
soonest_stays = "soonest_stays", soonest_stays = "soonest_stays",
upcoming_stays = "upcoming_stays", upcoming_stays = "upcoming_stays",
sas_linked_account = "sas_linked_account",
sas_tier_comparison = "sas_tier_comparison",
} }
/** Type needed to satisfy zod enum type */ /** Type needed to satisfy zod enum type */
@@ -26,6 +27,7 @@ export namespace DynamicContentEnum {
components.earn_and_burn, components.earn_and_burn,
components.expiring_points, components.expiring_points,
components.how_it_works, components.how_it_works,
components.jobylon_feed,
components.loyalty_levels, components.loyalty_levels,
components.membership_overview, components.membership_overview,
components.my_points, components.my_points,
@@ -33,12 +35,12 @@ export namespace DynamicContentEnum {
components.overview_table, components.overview_table,
components.points_overview, components.points_overview,
components.previous_stays, components.previous_stays,
components.sas_linked_account,
components.sas_tier_comparison,
components.sign_up_form, components.sign_up_form,
components.sign_up_verification, components.sign_up_verification,
components.soonest_stays, components.soonest_stays,
components.upcoming_stays, components.upcoming_stays,
components.sas_linked_account,
components.sas_tier_comparison,
] ]
} }

View File

@@ -0,0 +1,5 @@
import type { z } from "zod"
import type { jobylonItemSchema } from "@/server/routers/partners/jobylon/output"
export interface JobylonItem extends z.output<typeof jobylonItemSchema> {}