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:
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
.list {
|
||||
list-style: none;
|
||||
display: grid;
|
||||
gap: var(--Spacing-x2);
|
||||
}
|
||||
|
||||
.content {
|
||||
display: grid;
|
||||
gap: var(--Spacing-x2);
|
||||
}
|
||||
@@ -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 (
|
||||
<HowItWorks dynamic_content={dynamic_content} firstItem={firstItem} />
|
||||
)
|
||||
case DynamicContentEnum.Blocks.components.jobylon_feed:
|
||||
return <JobylonFeed {...dynamic_content} />
|
||||
case DynamicContentEnum.Blocks.components.loyalty_levels:
|
||||
return (
|
||||
<LoyaltyLevels
|
||||
|
||||
@@ -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 produkt?",
|
||||
"/night per adult": "/nat per voksen",
|
||||
"<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)",
|
||||
@@ -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, <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 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",
|
||||
|
||||
@@ -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",
|
||||
"<b>Included</b> (based on availability)": "<b>Inbegriffen</b> (je nach Verfügbarkeit)",
|
||||
"<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 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, <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 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",
|
||||
|
||||
@@ -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",
|
||||
"<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 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, <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 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",
|
||||
|
||||
@@ -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",
|
||||
"<b>Included</b> (based on availability)": "<b>Sisältyy</b> (saatavuuden mukaan)",
|
||||
"<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 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, <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 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",
|
||||
|
||||
@@ -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",
|
||||
"<b>Included</b> (based on availability)": "<b>Inkludert</b> (basert på tilgjengelighet)",
|
||||
"<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 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, <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 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",
|
||||
|
||||
@@ -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",
|
||||
"<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)",
|
||||
@@ -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, <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 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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -259,3 +259,7 @@ export const isBookingWidgetHidden = cache(
|
||||
return false
|
||||
}
|
||||
)
|
||||
|
||||
export const getJobylonFeed = cache(async function getMemoizedJobylonFeed() {
|
||||
return serverClient().partner.jobylon.feed.get()
|
||||
})
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
121
apps/scandic-web/server/routers/partners/jobylon/output.ts
Normal file
121
apps/scandic-web/server/routers/partners/jobylon/output.ts
Normal 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))
|
||||
94
apps/scandic-web/server/routers/partners/jobylon/query.ts
Normal file
94
apps/scandic-web/server/routers/partners/jobylon/query.ts
Normal 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
|
||||
}),
|
||||
}),
|
||||
})
|
||||
@@ -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"
|
||||
)
|
||||
@@ -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,
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
5
apps/scandic-web/types/trpc/routers/jobylon/index.ts
Normal file
5
apps/scandic-web/types/trpc/routers/jobylon/index.ts
Normal 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> {}
|
||||
Reference in New Issue
Block a user