Merged in fix/SW-978-error-page-gdansk (pull request #1002)
Fix/SW-978 error page gdansk Approved-by: Niclas Edenvin
This commit is contained in:
@@ -14,6 +14,7 @@ import { setLang } from "@/i18n/serverContext"
|
|||||||
|
|
||||||
import { fetchAvailableHotels, getFiltersFromHotels } from "../../utils"
|
import { fetchAvailableHotels, getFiltersFromHotels } from "../../utils"
|
||||||
|
|
||||||
|
import type { HotelData } from "@/types/components/hotelReservation/selectHotel/hotelCardListingProps"
|
||||||
import type { SelectHotelSearchParams } from "@/types/components/hotelReservation/selectHotel/selectHotelSearchParams"
|
import type { SelectHotelSearchParams } from "@/types/components/hotelReservation/selectHotel/selectHotelSearchParams"
|
||||||
import type { LangParams, PageArgs } from "@/types/params"
|
import type { LangParams, PageArgs } from "@/types/params"
|
||||||
|
|
||||||
@@ -52,11 +53,15 @@ export default async function SelectHotelMapPage({
|
|||||||
children,
|
children,
|
||||||
})
|
})
|
||||||
|
|
||||||
const hotelPins = getHotelPins(hotels)
|
const validHotels = hotels.filter(
|
||||||
const filterList = getFiltersFromHotels(hotels)
|
(hotel): hotel is HotelData => hotel !== null
|
||||||
|
)
|
||||||
|
|
||||||
|
const hotelPins = getHotelPins(validHotels)
|
||||||
|
const filterList = getFiltersFromHotels(validHotels)
|
||||||
const cityCoordinates = await getCityCoordinates({
|
const cityCoordinates = await getCityCoordinates({
|
||||||
city: city.name,
|
city: city.name,
|
||||||
hotel: { address: hotels[0].hotelData.address.streetAddress },
|
hotel: { address: hotels?.[0]?.hotelData?.address.streetAddress },
|
||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -65,7 +70,7 @@ export default async function SelectHotelMapPage({
|
|||||||
apiKey={googleMapsApiKey}
|
apiKey={googleMapsApiKey}
|
||||||
hotelPins={hotelPins}
|
hotelPins={hotelPins}
|
||||||
mapId={googleMapId}
|
mapId={googleMapId}
|
||||||
hotels={hotels}
|
hotels={validHotels}
|
||||||
filterList={filterList}
|
filterList={filterList}
|
||||||
cityCoordinates={cityCoordinates}
|
cityCoordinates={cityCoordinates}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ import { setLang } from "@/i18n/serverContext"
|
|||||||
|
|
||||||
import styles from "./page.module.css"
|
import styles from "./page.module.css"
|
||||||
|
|
||||||
|
import type { HotelData } from "@/types/components/hotelReservation/selectHotel/hotelCardListingProps"
|
||||||
import type { SelectHotelSearchParams } from "@/types/components/hotelReservation/selectHotel/selectHotelSearchParams"
|
import type { SelectHotelSearchParams } from "@/types/components/hotelReservation/selectHotel/selectHotelSearchParams"
|
||||||
import { AlertTypeEnum } from "@/types/enums/alert"
|
import { AlertTypeEnum } from "@/types/enums/alert"
|
||||||
import { LangParams, PageArgs } from "@/types/params"
|
import { LangParams, PageArgs } from "@/types/params"
|
||||||
@@ -61,6 +62,14 @@ export default async function SelectHotelPage({
|
|||||||
const selectHotelParams = new URLSearchParams(searchParams)
|
const selectHotelParams = new URLSearchParams(searchParams)
|
||||||
const selectHotelParamsObject =
|
const selectHotelParamsObject =
|
||||||
getHotelReservationQueryParams(selectHotelParams)
|
getHotelReservationQueryParams(selectHotelParams)
|
||||||
|
|
||||||
|
if (
|
||||||
|
!selectHotelParamsObject.room ||
|
||||||
|
selectHotelParamsObject.room.length === 0
|
||||||
|
) {
|
||||||
|
return notFound()
|
||||||
|
}
|
||||||
|
|
||||||
const adults = selectHotelParamsObject.room[0].adults // TODO: Handle multiple rooms
|
const adults = selectHotelParamsObject.room[0].adults // TODO: Handle multiple rooms
|
||||||
const children = selectHotelParamsObject.room[0].child
|
const children = selectHotelParamsObject.room[0].child
|
||||||
? generateChildrenString(selectHotelParamsObject.room[0].child)
|
? generateChildrenString(selectHotelParamsObject.room[0].child)
|
||||||
@@ -74,7 +83,11 @@ export default async function SelectHotelPage({
|
|||||||
children,
|
children,
|
||||||
})
|
})
|
||||||
|
|
||||||
const filterList = getFiltersFromHotels(hotels)
|
const validHotels = hotels.filter(
|
||||||
|
(hotel): hotel is HotelData => hotel !== null
|
||||||
|
)
|
||||||
|
|
||||||
|
const filterList = getFiltersFromHotels(validHotels)
|
||||||
const breadcrumbs = [
|
const breadcrumbs = [
|
||||||
{
|
{
|
||||||
title: intl.formatMessage({ id: "Home" }),
|
title: intl.formatMessage({ id: "Home" }),
|
||||||
@@ -169,7 +182,7 @@ export default async function SelectHotelPage({
|
|||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<HotelCardListing hotelData={hotels} />
|
<HotelCardListing hotelData={validHotels} />
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -4,7 +4,10 @@ import { serverClient } from "@/lib/trpc/server"
|
|||||||
import { getLang } from "@/i18n/serverContext"
|
import { getLang } from "@/i18n/serverContext"
|
||||||
|
|
||||||
import type { AvailabilityInput } from "@/types/components/hotelReservation/selectHotel/availabilityInput"
|
import type { AvailabilityInput } from "@/types/components/hotelReservation/selectHotel/availabilityInput"
|
||||||
import type { HotelData } from "@/types/components/hotelReservation/selectHotel/hotelCardListingProps"
|
import type {
|
||||||
|
HotelData,
|
||||||
|
NullableHotelData,
|
||||||
|
} from "@/types/components/hotelReservation/selectHotel/hotelCardListingProps"
|
||||||
import type {
|
import type {
|
||||||
CategorizedFilters,
|
CategorizedFilters,
|
||||||
Filter,
|
Filter,
|
||||||
@@ -30,10 +33,10 @@ const hotelFacilitiesFilterNames = [
|
|||||||
|
|
||||||
export async function fetchAvailableHotels(
|
export async function fetchAvailableHotels(
|
||||||
input: AvailabilityInput
|
input: AvailabilityInput
|
||||||
): Promise<HotelData[]> {
|
): Promise<NullableHotelData[]> {
|
||||||
const availableHotels = await serverClient().hotel.availability.hotels(input)
|
const availableHotels = await serverClient().hotel.availability.hotels(input)
|
||||||
|
|
||||||
if (!availableHotels) throw new Error()
|
if (!availableHotels) return []
|
||||||
|
|
||||||
const language = getLang()
|
const language = getLang()
|
||||||
|
|
||||||
@@ -43,7 +46,7 @@ export async function fetchAvailableHotels(
|
|||||||
language,
|
language,
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!hotelData) throw new Error()
|
if (!hotelData) return { hotelData: null, price: hotel.productType }
|
||||||
|
|
||||||
return {
|
return {
|
||||||
hotelData: hotelData.data.attributes,
|
hotelData: hotelData.data.attributes,
|
||||||
@@ -55,7 +58,13 @@ export async function fetchAvailableHotels(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getFiltersFromHotels(hotels: HotelData[]): CategorizedFilters {
|
export function getFiltersFromHotels(hotels: HotelData[]): CategorizedFilters {
|
||||||
const filters = hotels.flatMap((hotel) => hotel.hotelData.detailedFacilities)
|
if (hotels.length === 0)
|
||||||
|
return { facilityFilters: [], surroundingsFilters: [] }
|
||||||
|
|
||||||
|
const filters = hotels.flatMap((hotel) => {
|
||||||
|
if (!hotel.hotelData) return []
|
||||||
|
return hotel.hotelData.detailedFacilities
|
||||||
|
})
|
||||||
|
|
||||||
const uniqueFilterIds = [...new Set(filters.map((filter) => filter.id))]
|
const uniqueFilterIds = [...new Set(filters.map((filter) => filter.id))]
|
||||||
const filterList: Filter[] = uniqueFilterIds
|
const filterList: Filter[] = uniqueFilterIds
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
"use client" // Error components must be Client Components
|
"use client" // Error components must be Client Components
|
||||||
|
|
||||||
import { useParams, usePathname } from "next/navigation"
|
import {
|
||||||
import { useEffect } from "react"
|
useParams,
|
||||||
|
usePathname,
|
||||||
|
useRouter,
|
||||||
|
useSearchParams,
|
||||||
|
} from "next/navigation"
|
||||||
|
import { startTransition, useEffect, useRef } from "react"
|
||||||
import { useIntl } from "react-intl"
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
import { login } from "@/constants/routes/handleAuth"
|
import { login } from "@/constants/routes/handleAuth"
|
||||||
@@ -15,11 +20,17 @@ import { LangParams } from "@/types/params"
|
|||||||
|
|
||||||
export default function Error({
|
export default function Error({
|
||||||
error,
|
error,
|
||||||
|
reset,
|
||||||
}: {
|
}: {
|
||||||
error: Error & { digest?: string }
|
error: Error & { digest?: string }
|
||||||
|
reset: () => void
|
||||||
}) {
|
}) {
|
||||||
const intl = useIntl()
|
const intl = useIntl()
|
||||||
const params = useParams<LangParams>()
|
const params = useParams<LangParams>()
|
||||||
|
const router = useRouter()
|
||||||
|
const searchParams = useSearchParams()
|
||||||
|
const currentSearchParamsRef = useRef<string>()
|
||||||
|
const isFirstLoadRef = useRef<boolean>(true)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Log the error to an error reporting service
|
// Log the error to an error reporting service
|
||||||
@@ -31,6 +42,23 @@ export default function Error({
|
|||||||
}
|
}
|
||||||
}, [error, params.lang])
|
}, [error, params.lang])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// This is to reset the error and refresh the page when the search params change, to support the booking widget that is using router.push to navigate to the booking flow page
|
||||||
|
const currentSearchParams = searchParams.toString()
|
||||||
|
|
||||||
|
if (
|
||||||
|
currentSearchParamsRef.current !== currentSearchParams &&
|
||||||
|
!isFirstLoadRef.current
|
||||||
|
) {
|
||||||
|
startTransition(() => {
|
||||||
|
reset()
|
||||||
|
router.refresh()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
isFirstLoadRef.current = false
|
||||||
|
currentSearchParamsRef.current = currentSearchParams
|
||||||
|
}, [searchParams, reset, router])
|
||||||
|
|
||||||
const pathname = usePathname()
|
const pathname = usePathname()
|
||||||
const lang = findLang(pathname)
|
const lang = findLang(pathname)
|
||||||
|
|
||||||
|
|||||||
@@ -41,18 +41,11 @@ function HotelCard({
|
|||||||
const { hotelData } = hotel
|
const { hotelData } = hotel
|
||||||
const { price } = hotel
|
const { price } = hotel
|
||||||
|
|
||||||
const amenities = hotelData.detailedFacilities.slice(0, 5)
|
|
||||||
|
|
||||||
const classNames = hotelCardVariants({
|
|
||||||
type,
|
|
||||||
state,
|
|
||||||
})
|
|
||||||
|
|
||||||
const handleMouseEnter = useCallback(() => {
|
const handleMouseEnter = useCallback(() => {
|
||||||
if (onHotelCardHover) {
|
if (onHotelCardHover && hotelData) {
|
||||||
onHotelCardHover(hotelData.name)
|
onHotelCardHover(hotelData.name)
|
||||||
}
|
}
|
||||||
}, [onHotelCardHover, hotelData.name])
|
}, [onHotelCardHover, hotelData])
|
||||||
|
|
||||||
const handleMouseLeave = useCallback(() => {
|
const handleMouseLeave = useCallback(() => {
|
||||||
if (onHotelCardHover) {
|
if (onHotelCardHover) {
|
||||||
@@ -60,6 +53,15 @@ function HotelCard({
|
|||||||
}
|
}
|
||||||
}, [onHotelCardHover])
|
}, [onHotelCardHover])
|
||||||
|
|
||||||
|
if (!hotel || !hotelData) return null
|
||||||
|
|
||||||
|
const amenities = hotelData.detailedFacilities.slice(0, 5)
|
||||||
|
|
||||||
|
const classNames = hotelCardVariants({
|
||||||
|
type,
|
||||||
|
state,
|
||||||
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<article
|
<article
|
||||||
className={classNames}
|
className={classNames}
|
||||||
@@ -82,8 +84,8 @@ function HotelCard({
|
|||||||
<section className={styles.hotelInformation}>
|
<section className={styles.hotelInformation}>
|
||||||
<div className={styles.titleContainer}>
|
<div className={styles.titleContainer}>
|
||||||
<HotelLogo
|
<HotelLogo
|
||||||
hotelId={hotel.hotelData.operaId}
|
hotelId={hotelData.operaId}
|
||||||
hotelType={hotel.hotelData.hotelType}
|
hotelType={hotelData.hotelType}
|
||||||
/>
|
/>
|
||||||
<Subtitle textTransform="capitalize" color="uiTextHighContrast">
|
<Subtitle textTransform="capitalize" color="uiTextHighContrast">
|
||||||
{hotelData.name}
|
{hotelData.name}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ export default function HotelCardDialogListing({
|
|||||||
activeCard,
|
activeCard,
|
||||||
onActiveCardChange,
|
onActiveCardChange,
|
||||||
}: HotelCardDialogListingProps) {
|
}: HotelCardDialogListingProps) {
|
||||||
const hotelsPinData = getHotelPins(hotels)
|
const hotelsPinData = hotels ? getHotelPins(hotels) : []
|
||||||
const activeCardRef = useRef<HTMLDivElement | null>(null)
|
const activeCardRef = useRef<HTMLDivElement | null>(null)
|
||||||
const observerRef = useRef<IntersectionObserver | null>(null)
|
const observerRef = useRef<IntersectionObserver | null>(null)
|
||||||
const dialogRef = useRef<HTMLDivElement>(null)
|
const dialogRef = useRef<HTMLDivElement>(null)
|
||||||
@@ -77,7 +77,7 @@ export default function HotelCardDialogListing({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.hotelCardDialogListing} ref={dialogRef}>
|
<div className={styles.hotelCardDialogListing} ref={dialogRef}>
|
||||||
{hotelsPinData?.length &&
|
{!!hotelsPinData?.length &&
|
||||||
hotelsPinData.map((data) => {
|
hotelsPinData.map((data) => {
|
||||||
const isActive = data.name === activeCard
|
const isActive = data.name === activeCard
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import type { HotelData } from "@/types/components/hotelReservation/selectHotel/
|
|||||||
import type { HotelPin } from "@/types/components/hotelReservation/selectHotel/map"
|
import type { HotelPin } from "@/types/components/hotelReservation/selectHotel/map"
|
||||||
|
|
||||||
export function getHotelPins(hotels: HotelData[]): HotelPin[] {
|
export function getHotelPins(hotels: HotelData[]): HotelPin[] {
|
||||||
|
if (hotels.length === 0) return []
|
||||||
|
|
||||||
return hotels.map((hotel) => ({
|
return hotels.map((hotel) => ({
|
||||||
coordinates: {
|
coordinates: {
|
||||||
lat: hotel.hotelData.location.latitude,
|
lat: hotel.hotelData.location.latitude,
|
||||||
|
|||||||
@@ -5,6 +5,25 @@ import { getUrlWithSignature } from "@/utils/map"
|
|||||||
|
|
||||||
import { StaticMapProps } from "@/types/components/maps/staticMap"
|
import { StaticMapProps } from "@/types/components/maps/staticMap"
|
||||||
|
|
||||||
|
function getCenter({
|
||||||
|
coordinates,
|
||||||
|
city,
|
||||||
|
country,
|
||||||
|
}: {
|
||||||
|
coordinates?: { lat: number; lng: number }
|
||||||
|
city?: string
|
||||||
|
country?: string
|
||||||
|
}): string | undefined {
|
||||||
|
switch (true) {
|
||||||
|
case !!coordinates:
|
||||||
|
return `${coordinates.lat},${coordinates.lng}`
|
||||||
|
case !!country:
|
||||||
|
return `${city}, ${country}`
|
||||||
|
default:
|
||||||
|
return city
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default function StaticMap({
|
export default function StaticMap({
|
||||||
city,
|
city,
|
||||||
country,
|
country,
|
||||||
@@ -19,9 +38,7 @@ export default function StaticMap({
|
|||||||
const key = env.GOOGLE_STATIC_MAP_KEY
|
const key = env.GOOGLE_STATIC_MAP_KEY
|
||||||
const secret = env.GOOGLE_STATIC_MAP_SIGNATURE_SECRET
|
const secret = env.GOOGLE_STATIC_MAP_SIGNATURE_SECRET
|
||||||
const baseUrl = "https://maps.googleapis.com/maps/api/staticmap"
|
const baseUrl = "https://maps.googleapis.com/maps/api/staticmap"
|
||||||
const center = coordinates
|
const center = getCenter({ coordinates, city, country })
|
||||||
? `${coordinates.lat},${coordinates.lng}`
|
|
||||||
: `${city}, ${country}`
|
|
||||||
|
|
||||||
if (!center) {
|
if (!center) {
|
||||||
return null
|
return null
|
||||||
|
|||||||
@@ -144,7 +144,7 @@ export const getBookingConfirmation = cache(
|
|||||||
export const getCityCoordinates = cache(
|
export const getCityCoordinates = cache(
|
||||||
async function getMemoizedCityCoordinates(input: {
|
async function getMemoizedCityCoordinates(input: {
|
||||||
city: string
|
city: string
|
||||||
hotel: { address: string }
|
hotel: { address: string | undefined }
|
||||||
}) {
|
}) {
|
||||||
return serverClient().hotel.map.city(input)
|
return serverClient().hotel.map.city(input)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -77,6 +77,6 @@ export const getRoomPackagesInputSchema = z.object({
|
|||||||
export const getCityCoordinatesInputSchema = z.object({
|
export const getCityCoordinatesInputSchema = z.object({
|
||||||
city: z.string(),
|
city: z.string(),
|
||||||
hotel: z.object({
|
hotel: z.object({
|
||||||
address: z.string(),
|
address: z.string().optional(),
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -18,3 +18,7 @@ export type HotelData = {
|
|||||||
hotelData: Hotel
|
hotelData: Hotel
|
||||||
price: ProductType
|
price: ProductType
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface NullableHotelData extends Omit<HotelData, "hotelData"> {
|
||||||
|
hotelData: HotelData["hotelData"] | null
|
||||||
|
}
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ export interface HotelCardDialogProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface HotelCardDialogListingProps {
|
export interface HotelCardDialogListingProps {
|
||||||
hotels: HotelData[]
|
hotels: HotelData[] | null
|
||||||
activeCard: string | null | undefined
|
activeCard: string | null | undefined
|
||||||
onActiveCardChange: (hotelName: string | null) => void
|
onActiveCardChange: (hotelName: string | null) => void
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user