Merged in feat/LOY-430-reward-nights (pull request #3295)
Feat/LOY-430 reward nights * chore(LOY-430): add reward nights request and dynamic content * chore(LOY-430): fix Reward Night component * Refactor: use existing endpoint and add rewardNight data to that response instead Approved-by: Linus Flood
This commit is contained in:
@@ -27,7 +27,7 @@ export async function GET(request: NextRequest) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const caller = await serverClient()
|
const caller = await serverClient()
|
||||||
const hotels = await caller.hotel.hotels.getDestinationsMapData({
|
const hotels = await caller.hotel.hotels.getAllHotelData({
|
||||||
lang: parsedLang.data,
|
lang: parsedLang.data,
|
||||||
warmup: true,
|
warmup: true,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -0,0 +1,122 @@
|
|||||||
|
import { cx } from "class-variance-authority"
|
||||||
|
|
||||||
|
import Table from "@scandic-hotels/design-system/Table"
|
||||||
|
import { TextLink } from "@scandic-hotels/design-system/TextLink"
|
||||||
|
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||||
|
|
||||||
|
import { getAllHotelData } from "@/lib/trpc/memoizedRequests"
|
||||||
|
|
||||||
|
import { getIntl } from "@/i18n"
|
||||||
|
|
||||||
|
import styles from "./rewardNights.module.css"
|
||||||
|
|
||||||
|
export async function RewardNights() {
|
||||||
|
const intl = await getIntl()
|
||||||
|
const hotelData = await getAllHotelData()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Table intent="striped" variant="content" style={{ textWrap: "balance" }}>
|
||||||
|
<Table.THead>
|
||||||
|
<Table.TR>
|
||||||
|
<Table.TH>
|
||||||
|
{intl.formatMessage({
|
||||||
|
id: "rewardNights.table.hotel",
|
||||||
|
defaultMessage: "Hotel",
|
||||||
|
})}
|
||||||
|
</Table.TH>
|
||||||
|
<Table.TH>
|
||||||
|
{intl.formatMessage({
|
||||||
|
id: "rewardNights.table.destination",
|
||||||
|
defaultMessage: "Destination",
|
||||||
|
})}
|
||||||
|
</Table.TH>
|
||||||
|
<Table.TH>
|
||||||
|
{intl.formatMessage({
|
||||||
|
id: "common.points",
|
||||||
|
defaultMessage: "Points",
|
||||||
|
})}
|
||||||
|
</Table.TH>
|
||||||
|
</Table.TR>
|
||||||
|
</Table.THead>
|
||||||
|
<Table.TBody>
|
||||||
|
{hotelData.map((data) => {
|
||||||
|
const { hotel } = data
|
||||||
|
const hasCampaign = hotel.rewardNight.campaign.points
|
||||||
|
return (
|
||||||
|
<Table.TR key={hotel.id}>
|
||||||
|
<Table.TD style={{ alignContent: "flex-start" }}>
|
||||||
|
<TextLink href={data.url ?? ""}>{hotel.name}</TextLink>
|
||||||
|
</Table.TD>
|
||||||
|
<Table.TD>
|
||||||
|
{/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */}
|
||||||
|
{`${hotel.address.city}, ${hotel.address.country}`}
|
||||||
|
{hasCampaign ? (
|
||||||
|
<OfferPrice {...hotel.rewardNight.campaign} />
|
||||||
|
) : null}
|
||||||
|
</Table.TD>
|
||||||
|
<Table.TD style={{ alignContent: "flex-start" }}>
|
||||||
|
<div className={cx({ [styles.grid]: hasCampaign })}>
|
||||||
|
{formatPoints(hotel.rewardNight.points)}
|
||||||
|
{hasCampaign ? (
|
||||||
|
<Typography
|
||||||
|
variant="Body/Paragraph/mdBold"
|
||||||
|
className={styles.highlightedText}
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
{formatPoints(hotel.rewardNight.campaign.points)}
|
||||||
|
</span>
|
||||||
|
</Typography>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</Table.TD>
|
||||||
|
</Table.TR>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</Table.TBody>
|
||||||
|
</Table>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
interface OfferPriceProps {
|
||||||
|
points: number
|
||||||
|
start: string
|
||||||
|
end: string
|
||||||
|
}
|
||||||
|
async function OfferPrice(offer: OfferPriceProps) {
|
||||||
|
const intl = await getIntl()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.offerPrice}>
|
||||||
|
<Typography variant="Body/Paragraph/mdBold">
|
||||||
|
<p className={styles.highlightedText}>
|
||||||
|
{intl.formatMessage({
|
||||||
|
id: "rewardNights.offerPrice",
|
||||||
|
defaultMessage: "Offer price",
|
||||||
|
})}
|
||||||
|
</p>
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="Label/xsBold">
|
||||||
|
<p>
|
||||||
|
{intl.formatMessage({
|
||||||
|
id: "rewardNights.stayBetween:",
|
||||||
|
defaultMessage: "Stay between:",
|
||||||
|
})}
|
||||||
|
</p>
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="Label/xsRegular">
|
||||||
|
<time>
|
||||||
|
{/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */}
|
||||||
|
{formatDate(offer.start)} - {formatDate(offer.end)}
|
||||||
|
</time>
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatPoints(number: number) {
|
||||||
|
const format = new Intl.NumberFormat("fr-FR")
|
||||||
|
return format.format(number).replace(/\u202F/g, " ")
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDate(date: string) {
|
||||||
|
return new Date(date).toISOString().split("T")[0]
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
.offerPrice {
|
||||||
|
display: grid;
|
||||||
|
gap: var(--Space-x05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.highlightedText {
|
||||||
|
color: var(--Text-Interactive-Secondary);
|
||||||
|
padding-top: var(--Space-x2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid {
|
||||||
|
display: grid;
|
||||||
|
}
|
||||||
@@ -28,6 +28,7 @@ import { SJWidget } from "@/components/SJWidget"
|
|||||||
import JobylonFeed from "./JobylonFeed"
|
import JobylonFeed from "./JobylonFeed"
|
||||||
|
|
||||||
import type { DynamicContentProps } from "@/types/components/blocks/dynamicContent"
|
import type { DynamicContentProps } from "@/types/components/blocks/dynamicContent"
|
||||||
|
import { RewardNights } from "./RewardNights"
|
||||||
|
|
||||||
export default function DynamicContent(props: DynamicContentProps) {
|
export default function DynamicContent(props: DynamicContentProps) {
|
||||||
return (
|
return (
|
||||||
@@ -91,6 +92,8 @@ function DynamicContentBlocks(props: DynamicContentProps) {
|
|||||||
|
|
||||||
case DynamicContentEnum.Blocks.components.sj_widget:
|
case DynamicContentEnum.Blocks.components.sj_widget:
|
||||||
return <SJWidget />
|
return <SJWidget />
|
||||||
|
case DynamicContentEnum.Blocks.components.reward_nights:
|
||||||
|
return <RewardNights />
|
||||||
default:
|
default:
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { env } from "@/env/server"
|
import { env } from "@/env/server"
|
||||||
import { getDestinationsMapData } from "@/lib/trpc/memoizedRequests"
|
import { getAllHotelData } from "@/lib/trpc/memoizedRequests"
|
||||||
|
|
||||||
import DynamicMap from "../../Map/DynamicMap"
|
import DynamicMap from "../../Map/DynamicMap"
|
||||||
import MapContent from "../../Map/MapContent"
|
import MapContent from "../../Map/MapContent"
|
||||||
@@ -8,7 +8,7 @@ import { getHotelMapMarkers, mapMarkerDataToGeoJson } from "../../Map/utils"
|
|||||||
import ActiveMapCard from "./ActiveMapCard"
|
import ActiveMapCard from "./ActiveMapCard"
|
||||||
|
|
||||||
export default async function OverviewMapContainer() {
|
export default async function OverviewMapContainer() {
|
||||||
const hotelData = await getDestinationsMapData()
|
const hotelData = await getAllHotelData()
|
||||||
|
|
||||||
if (!hotelData) {
|
if (!hotelData) {
|
||||||
return null
|
return null
|
||||||
|
|||||||
@@ -203,12 +203,10 @@ export const getHotelsByCityIdentifier = cache(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
export const getDestinationsMapData = cache(
|
export const getAllHotelData = cache(async function getMemoizedAllHotelData() {
|
||||||
async function getMemoizedDestinationsMapData() {
|
const caller = await serverClient()
|
||||||
const caller = await serverClient()
|
return caller.hotel.hotels.getAllHotelData()
|
||||||
return caller.hotel.hotels.getDestinationsMapData()
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
export const getDestinationCityPage = cache(
|
export const getDestinationCityPage = cache(
|
||||||
async function getMemoizedDestinationCityPage() {
|
async function getMemoizedDestinationCityPage() {
|
||||||
const caller = await serverClient()
|
const caller = await serverClient()
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ export const warmupHotelData =
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const caller = await serverClient()
|
const caller = await serverClient()
|
||||||
await caller.hotel.hotels.getDestinationsMapData({
|
await caller.hotel.hotels.getAllHotelData({
|
||||||
lang,
|
lang,
|
||||||
warmup: true,
|
warmup: true,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ export const tableSchema = z.object({
|
|||||||
),
|
),
|
||||||
data: z.array(z.object({}).catchall(z.string())),
|
data: z.array(z.object({}).catchall(z.string())),
|
||||||
skipReset: z.boolean(),
|
skipReset: z.boolean(),
|
||||||
tableActionEnabled: z.boolean(),
|
tableActionEnabled: z.boolean().optional().default(false),
|
||||||
headerRowAdded: z.boolean().optional().default(false),
|
headerRowAdded: z.boolean().optional().default(false),
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import {
|
|||||||
import { addressSchema } from "./schemas/hotel/address"
|
import { addressSchema } from "./schemas/hotel/address"
|
||||||
import { detailedFacilitiesSchema } from "./schemas/hotel/detailedFacility"
|
import { detailedFacilitiesSchema } from "./schemas/hotel/detailedFacility"
|
||||||
import { locationSchema } from "./schemas/hotel/location"
|
import { locationSchema } from "./schemas/hotel/location"
|
||||||
|
import { rewardNightSchema } from "./schemas/hotel/rewardNight"
|
||||||
import { imageSchema } from "./schemas/image"
|
import { imageSchema } from "./schemas/image"
|
||||||
import { relationshipsSchema } from "./schemas/relationships"
|
import { relationshipsSchema } from "./schemas/relationships"
|
||||||
import { roomConfigurationSchema } from "./schemas/roomAvailability/configuration"
|
import { roomConfigurationSchema } from "./schemas/roomAvailability/configuration"
|
||||||
@@ -600,6 +601,7 @@ export const hotelListingHotelDataSchema = z.object({
|
|||||||
hotelType: z.string(),
|
hotelType: z.string(),
|
||||||
type: z.literal("hotels"), // No enum here but the standard return appears to be "hotels".
|
type: z.literal("hotels"), // No enum here but the standard return appears to be "hotels".
|
||||||
description: z.string().nullable(),
|
description: z.string().nullable(),
|
||||||
|
rewardNight: rewardNightSchema,
|
||||||
}),
|
}),
|
||||||
url: z.string().nullable(),
|
url: z.string().nullable(),
|
||||||
meetingUrl: z.string().nullable(),
|
meetingUrl: z.string().nullable(),
|
||||||
|
|||||||
@@ -229,7 +229,7 @@ export const hotelQueryRouter = router({
|
|||||||
return hotels
|
return hotels
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
getDestinationsMapData: serviceProcedure
|
getAllHotelData: serviceProcedure
|
||||||
.input(getDestinationsMapDataInput)
|
.input(getDestinationsMapDataInput)
|
||||||
.query(async function ({ input, ctx }) {
|
.query(async function ({ input, ctx }) {
|
||||||
const lang = input?.lang ?? ctx.lang
|
const lang = input?.lang ?? ctx.lang
|
||||||
|
|||||||
@@ -94,6 +94,7 @@ export async function getHotelsByHotelIds({
|
|||||||
address: hotel.address,
|
address: hotel.address,
|
||||||
cityIdentifier: cities[0]?.cityIdentifier || null,
|
cityIdentifier: cities[0]?.cityIdentifier || null,
|
||||||
description: content.description || null,
|
description: content.description || null,
|
||||||
|
rewardNight: hotel.rewardNight,
|
||||||
},
|
},
|
||||||
url: content.url,
|
url: content.url,
|
||||||
meetingUrl: additionalData.meetingRooms.meetingOnlineLink || null,
|
meetingUrl: additionalData.meetingRooms.meetingOnlineLink || null,
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ export namespace DynamicContentEnum {
|
|||||||
sas_tier_comparison: "sas_tier_comparison",
|
sas_tier_comparison: "sas_tier_comparison",
|
||||||
manage_cookie_consent: "manage_cookie_consent",
|
manage_cookie_consent: "manage_cookie_consent",
|
||||||
sj_widget: "sj_widget",
|
sj_widget: "sj_widget",
|
||||||
|
reward_nights: "reward_nights",
|
||||||
unknown: "unknown",
|
unknown: "unknown",
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user