Merge branch 'master' of bitbucket.org:scandic-swap/web into fix/loading-rooms-separately
This commit is contained in:
@@ -2,8 +2,8 @@ import { Suspense } from "react"
|
||||
|
||||
import LoadingSpinner from "@/components/LoadingSpinner"
|
||||
import Sidebar from "@/components/MyPages/Sidebar"
|
||||
import Surprises from "@/components/MyPages/Surprises"
|
||||
|
||||
// import Surprises from "@/components/MyPages/Surprises"
|
||||
import styles from "./layout.module.css"
|
||||
|
||||
export default async function MyPagesLayout({
|
||||
@@ -24,9 +24,7 @@ export default async function MyPagesLayout({
|
||||
</section>
|
||||
</section>
|
||||
|
||||
{/* TODO: Waiting on new API stuff
|
||||
<Surprises />
|
||||
*/}
|
||||
<Surprises />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { notFound } from "next/navigation"
|
||||
|
||||
import { env } from "@/env/server"
|
||||
import { getLocations } from "@/lib/trpc/memoizedRequests"
|
||||
import { getCityCoordinates, getLocations } from "@/lib/trpc/memoizedRequests"
|
||||
|
||||
import { getHotelPins } from "@/components/HotelReservation/HotelCardDialogListing/utils"
|
||||
import SelectHotelMap from "@/components/HotelReservation/SelectHotel/SelectHotelMap"
|
||||
@@ -58,6 +58,7 @@ export default async function SelectHotelMapPage({
|
||||
|
||||
const hotelPins = getHotelPins(hotels)
|
||||
const filterList = getFiltersFromHotels(hotels)
|
||||
const cityCoordinates = await getCityCoordinates({ city: city.name })
|
||||
|
||||
return (
|
||||
<MapModal>
|
||||
@@ -67,6 +68,7 @@ export default async function SelectHotelMapPage({
|
||||
mapId={googleMapId}
|
||||
hotels={hotels}
|
||||
filterList={filterList}
|
||||
cityCoordinates={cityCoordinates}
|
||||
/>
|
||||
</MapModal>
|
||||
)
|
||||
|
||||
@@ -9,6 +9,7 @@ import TrpcProvider from "@/lib/trpc/Provider"
|
||||
import TokenRefresher from "@/components/Auth/TokenRefresher"
|
||||
import AdobeSDKScript from "@/components/Current/AdobeSDKScript"
|
||||
import VwoScript from "@/components/Current/VwoScript"
|
||||
import StorageCleaner from "@/components/HotelReservation/EnterDetails/StorageCleaner"
|
||||
import { ToastHandler } from "@/components/TempDesignSystem/Toasts"
|
||||
import { preloadUserTracking } from "@/components/TrackingSDK"
|
||||
import { getIntl } from "@/i18n"
|
||||
@@ -64,6 +65,7 @@ export default async function RootLayout({
|
||||
{footer}
|
||||
<ToastHandler />
|
||||
<TokenRefresher />
|
||||
<StorageCleaner />
|
||||
</TrpcProvider>
|
||||
</ServerIntlProvider>
|
||||
</body>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { about } from "@/constants/routes/hotelPageParams"
|
||||
|
||||
import { ChevronRightSmallIcon } from "@/components/Icons"
|
||||
import TripAdvisorIcon from "@/components/Icons/TripAdvisor"
|
||||
import { ChevronRightSmallIcon, TripAdvisorIcon } from "@/components/Icons"
|
||||
import Link from "@/components/TempDesignSystem/Link"
|
||||
import BiroScript from "@/components/TempDesignSystem/Text/BiroScript"
|
||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||
|
||||
@@ -60,13 +60,20 @@ export default function Sidebar({
|
||||
}
|
||||
}
|
||||
|
||||
function handleMouseEnter(poiName: string) {
|
||||
function handleMouseEnter(poiName: string | undefined) {
|
||||
if (!poiName) return
|
||||
|
||||
if (!isClicking) {
|
||||
onActivePoiChange(poiName)
|
||||
}
|
||||
}
|
||||
|
||||
function handlePoiClick(poiName: string, poiCoordinates: Coordinates) {
|
||||
function handlePoiClick(
|
||||
poiName: string | undefined,
|
||||
poiCoordinates: Coordinates
|
||||
) {
|
||||
if (!poiName || !poiCoordinates) return
|
||||
|
||||
setIsClicking(true)
|
||||
toggleFullScreenSidebar()
|
||||
onActivePoiChange(poiName)
|
||||
|
||||
@@ -113,7 +113,7 @@ export default function DynamicMap({
|
||||
activePoi={activePoi}
|
||||
hotelName={hotelName}
|
||||
pointsOfInterest={pointsOfInterest}
|
||||
onActivePoiChange={setActivePoi}
|
||||
onActivePoiChange={(poi) => setActivePoi(poi ?? null)}
|
||||
coordinates={coordinates}
|
||||
/>
|
||||
<InteractiveMap
|
||||
@@ -121,7 +121,7 @@ export default function DynamicMap({
|
||||
coordinates={coordinates}
|
||||
pointsOfInterest={pointsOfInterest}
|
||||
activePoi={activePoi}
|
||||
onActivePoiChange={setActivePoi}
|
||||
onActivePoiChange={(poi) => setActivePoi(poi ?? null)}
|
||||
mapId={mapId}
|
||||
/>
|
||||
</Dialog>
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
.wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Spacing-x-one-and-half);
|
||||
}
|
||||
|
||||
.information {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: var(--Spacing-x2);
|
||||
grid-template-areas:
|
||||
"address drivingDirections"
|
||||
"contact socials"
|
||||
"email email"
|
||||
"ecoLabel ecoLabel";
|
||||
}
|
||||
|
||||
.address {
|
||||
grid-area: address;
|
||||
}
|
||||
|
||||
.drivingDirections {
|
||||
grid-area: drivingDirections;
|
||||
}
|
||||
|
||||
.contact {
|
||||
grid-area: contact;
|
||||
}
|
||||
|
||||
.socials {
|
||||
grid-area: socials;
|
||||
}
|
||||
|
||||
.socialIcons {
|
||||
display: flex;
|
||||
gap: var(--Spacing-x1);
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.email {
|
||||
grid-area: email;
|
||||
}
|
||||
|
||||
.ecoLabel {
|
||||
grid-area: ecoLabel;
|
||||
display: flex;
|
||||
gap: var(--Spacing-x-one-and-half);
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
import { FacebookIcon, InstagramIcon } from "@/components/Icons"
|
||||
import Image from "@/components/Image"
|
||||
import Link from "@/components/TempDesignSystem/Link"
|
||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
||||
import Title from "@/components/TempDesignSystem/Text/Title"
|
||||
import { getIntl } from "@/i18n"
|
||||
import { getLang } from "@/i18n/serverContext"
|
||||
|
||||
import styles from "./contactInformation.module.css"
|
||||
|
||||
import type { ContactInformationProps } from "@/types/components/hotelPage/sidepeek/contactInformation"
|
||||
|
||||
export default async function ContactInformation({
|
||||
hotelAddress,
|
||||
coordinates,
|
||||
contact,
|
||||
socials,
|
||||
ecoLabels,
|
||||
}: ContactInformationProps) {
|
||||
const intl = await getIntl()
|
||||
const lang = getLang()
|
||||
|
||||
const { latitude, longitude } = coordinates
|
||||
const directionsUrl = `https://www.google.com/maps/dir/?api=1&destination=${latitude},${longitude}`
|
||||
|
||||
return (
|
||||
<div className={styles.wrapper}>
|
||||
<Subtitle color="burgundy" asChild>
|
||||
<Title level="h3">
|
||||
{intl.formatMessage({ id: "Practical information" })}
|
||||
</Title>
|
||||
</Subtitle>
|
||||
<div className={styles.information}>
|
||||
<div className={styles.address}>
|
||||
<Body textTransform="bold">
|
||||
{intl.formatMessage({ id: "Address" })}
|
||||
</Body>
|
||||
<Body color="uiTextHighContrast">{hotelAddress.streetAddress}</Body>
|
||||
<Body color="uiTextHighContrast">{hotelAddress.city}</Body>
|
||||
</div>
|
||||
<div className={styles.drivingDirections}>
|
||||
<Body textTransform="bold">
|
||||
{intl.formatMessage({ id: "Driving directions" })}
|
||||
</Body>
|
||||
<Link
|
||||
href={directionsUrl}
|
||||
target="_blank"
|
||||
color="peach80"
|
||||
textDecoration="underline"
|
||||
>
|
||||
Google Maps
|
||||
</Link>
|
||||
</div>
|
||||
<div className={styles.contact}>
|
||||
<Body textTransform="bold">
|
||||
{intl.formatMessage({ id: "Contact us" })}
|
||||
</Body>
|
||||
<Body>
|
||||
<Link
|
||||
href={`tel:+${contact.phoneNumber}`}
|
||||
color="peach80"
|
||||
textDecoration="underline"
|
||||
>
|
||||
{contact.phoneNumber}
|
||||
</Link>
|
||||
</Body>
|
||||
</div>
|
||||
<div className={styles.socials}>
|
||||
<Body textTransform="bold">
|
||||
{intl.formatMessage({ id: "Follow us" })}
|
||||
</Body>
|
||||
<div className={styles.socialIcons}>
|
||||
{socials.instagram && (
|
||||
<Link href={socials.instagram}>
|
||||
<InstagramIcon color="burgundy" />
|
||||
</Link>
|
||||
)}
|
||||
{socials.facebook && (
|
||||
<Link href={socials.facebook}>
|
||||
<FacebookIcon color="burgundy" />
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.email}>
|
||||
<Body textTransform="bold">
|
||||
{intl.formatMessage({ id: "Email" })}
|
||||
</Body>
|
||||
<Link
|
||||
href={`mailto:${contact.email}`}
|
||||
color="peach80"
|
||||
textDecoration="underline"
|
||||
>
|
||||
{contact.email}
|
||||
</Link>
|
||||
</div>
|
||||
{ecoLabels.nordicEcoLabel && (
|
||||
<div className={styles.ecoLabel}>
|
||||
<Image
|
||||
height={38}
|
||||
width={38}
|
||||
alt={intl.formatMessage({ id: "Nordic Swan Ecolabel" })}
|
||||
src={`/_static/img/icons/swan-eco/swan_eco_dark_${lang}.png`}
|
||||
/>
|
||||
<div>
|
||||
<Caption color="uiTextPlaceholder">
|
||||
{intl.formatMessage({ id: "Nordic Swan Ecolabel" })}
|
||||
</Caption>
|
||||
<Caption color="uiTextPlaceholder">
|
||||
{ecoLabels.svanenEcoLabelCertificateNumber}
|
||||
</Caption>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
.wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Spacing-x3);
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import { about } from "@/constants/routes/hotelPageParams"
|
||||
|
||||
import Divider from "@/components/TempDesignSystem/Divider"
|
||||
import SidePeek from "@/components/TempDesignSystem/SidePeek"
|
||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||
import Preamble from "@/components/TempDesignSystem/Text/Preamble"
|
||||
import { getIntl } from "@/i18n"
|
||||
import { getLang } from "@/i18n/serverContext"
|
||||
|
||||
import ContactInformation from "./ContactInformation"
|
||||
|
||||
import styles from "./aboutTheHotel.module.css"
|
||||
|
||||
import type { AboutTheHotelSidePeekProps } from "@/types/components/hotelPage/sidepeek/aboutTheHotel"
|
||||
|
||||
export default async function AboutTheHotelSidePeek({
|
||||
hotelAddress,
|
||||
coordinates,
|
||||
contact,
|
||||
socials,
|
||||
ecoLabels,
|
||||
descriptions,
|
||||
}: AboutTheHotelSidePeekProps) {
|
||||
const lang = getLang()
|
||||
const intl = await getIntl()
|
||||
|
||||
return (
|
||||
<SidePeek
|
||||
contentKey={about[lang]}
|
||||
title={intl.formatMessage({ id: "About the hotel" })}
|
||||
>
|
||||
<section className={styles.wrapper}>
|
||||
<ContactInformation
|
||||
hotelAddress={hotelAddress}
|
||||
coordinates={coordinates}
|
||||
contact={contact}
|
||||
socials={socials}
|
||||
ecoLabels={ecoLabels}
|
||||
/>
|
||||
<Divider color="baseSurfaceSutbleHover" />
|
||||
<Preamble>{descriptions.descriptions.medium}</Preamble>
|
||||
<Body>{descriptions.facilityInformation}</Body>
|
||||
</section>
|
||||
</SidePeek>
|
||||
)
|
||||
}
|
||||
@@ -16,7 +16,7 @@ export default async function Facility({ data }: FacilityProps) {
|
||||
|
||||
return (
|
||||
<div className={styles.content}>
|
||||
{image.imageSizes.medium && (
|
||||
{image?.imageSizes.medium && (
|
||||
<Image
|
||||
src={image.imageSizes.medium}
|
||||
alt={image.metaData.altText || ""}
|
||||
|
||||
2
components/ContentType/HotelPage/SidePeeks/index.ts
Normal file
2
components/ContentType/HotelPage/SidePeeks/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { default as AboutTheHotelSidePeek } from "./AboutTheHotel"
|
||||
export { default as WellnessAndExerciseSidePeek } from "./WellnessAndExercise"
|
||||
@@ -16,12 +16,12 @@ import MapCard from "./Map/MapCard"
|
||||
import MapWithCardWrapper from "./Map/MapWithCard"
|
||||
import MobileMapToggle from "./Map/MobileMapToggle"
|
||||
import StaticMap from "./Map/StaticMap"
|
||||
import WellnessAndExerciseSidePeek from "./SidePeeks/WellnessAndExercise"
|
||||
import AmenitiesList from "./AmenitiesList"
|
||||
import Facilities from "./Facilities"
|
||||
import IntroSection from "./IntroSection"
|
||||
import PreviewImages from "./PreviewImages"
|
||||
import { Rooms } from "./Rooms"
|
||||
import { AboutTheHotelSidePeek, WellnessAndExerciseSidePeek } from "./SidePeeks"
|
||||
import TabNavigation from "./TabNavigation"
|
||||
|
||||
import styles from "./hotelPage.module.css"
|
||||
@@ -41,7 +41,7 @@ export default async function HotelPage() {
|
||||
const {
|
||||
hotelId,
|
||||
hotelName,
|
||||
hotelDescription,
|
||||
hotelDescriptions,
|
||||
hotelLocation,
|
||||
hotelAddress,
|
||||
hotelRatings,
|
||||
@@ -54,6 +54,9 @@ export default async function HotelPage() {
|
||||
faq,
|
||||
alerts,
|
||||
healthFacilities,
|
||||
contact,
|
||||
socials,
|
||||
ecoLabels,
|
||||
} = hotelData
|
||||
|
||||
const topThreePois = pointsOfInterest.slice(0, 3)
|
||||
@@ -80,7 +83,7 @@ export default async function HotelPage() {
|
||||
<div className={styles.introContainer}>
|
||||
<IntroSection
|
||||
hotelName={hotelName}
|
||||
hotelDescription={hotelDescription}
|
||||
hotelDescription={hotelDescriptions.descriptions.short}
|
||||
location={hotelLocation}
|
||||
address={hotelAddress}
|
||||
tripAdvisor={hotelRatings?.tripAdvisor}
|
||||
@@ -134,12 +137,14 @@ export default async function HotelPage() {
|
||||
{/* TODO: Render amenities as per the design. */}
|
||||
Read more about the amenities here
|
||||
</SidePeek>
|
||||
<SidePeek
|
||||
contentKey={hotelPageParams.about[lang]}
|
||||
title={intl.formatMessage({ id: "Read more about the hotel" })}
|
||||
>
|
||||
Some additional information about the hotel
|
||||
</SidePeek>
|
||||
<AboutTheHotelSidePeek
|
||||
hotelAddress={hotelAddress}
|
||||
coordinates={hotelLocation}
|
||||
contact={contact}
|
||||
socials={socials}
|
||||
ecoLabels={ecoLabels}
|
||||
descriptions={hotelDescriptions}
|
||||
/>
|
||||
<SidePeek
|
||||
contentKey={hotelPageParams.restaurantAndBar[lang]}
|
||||
title={intl.formatMessage({ id: "Restaurant & Bar" })}
|
||||
|
||||
@@ -2,8 +2,7 @@
|
||||
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import FacebookIcon from "@/components/Icons/Facebook"
|
||||
import InstagramIcon from "@/components/Icons/Instagram"
|
||||
import { FacebookIcon, InstagramIcon } from "@/components/Icons"
|
||||
import Image from "@/components/Image"
|
||||
import Link from "@/components/TempDesignSystem/Link"
|
||||
import useLang from "@/hooks/useLang"
|
||||
|
||||
@@ -76,6 +76,9 @@ export default function BedType({ bedTypes }: BedTypeProps) {
|
||||
subtitle={width}
|
||||
title={roomType.description}
|
||||
value={roomType.value}
|
||||
handleSelectedOnClick={
|
||||
bedType === roomType.value ? completeStep : undefined
|
||||
}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
|
||||
@@ -97,6 +97,9 @@ export default function Breakfast({ packages }: BreakfastProps) {
|
||||
})}
|
||||
title={intl.formatMessage({ id: "Breakfast buffet" })}
|
||||
value={pkg.code}
|
||||
handleSelectedOnClick={
|
||||
breakfast === pkg.code ? completeStep : undefined
|
||||
}
|
||||
/>
|
||||
))}
|
||||
<RadioCard
|
||||
@@ -113,6 +116,9 @@ export default function Breakfast({ packages }: BreakfastProps) {
|
||||
})}
|
||||
title={intl.formatMessage({ id: "No breakfast" })}
|
||||
value="false"
|
||||
handleSelectedOnClick={
|
||||
breakfast === "false" ? completeStep : undefined
|
||||
}
|
||||
/>
|
||||
</form>
|
||||
</FormProvider>
|
||||
|
||||
26
components/HotelReservation/EnterDetails/StorageCleaner.tsx
Normal file
26
components/HotelReservation/EnterDetails/StorageCleaner.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
"use client"
|
||||
import { usePathname } from "next/navigation"
|
||||
import { useEffect } from "react"
|
||||
|
||||
import { hotelreservation } from "@/constants/routes/hotelReservation"
|
||||
import { detailsStorageName } from "@/stores/details"
|
||||
|
||||
import useLang from "@/hooks/useLang"
|
||||
|
||||
/**
|
||||
* Cleanup component to make sure no stale data is left
|
||||
* from previous booking when user is not in the booking
|
||||
* flow anymore
|
||||
*/
|
||||
export default function StorageCleaner() {
|
||||
const lang = useLang()
|
||||
const pathname = usePathname()
|
||||
|
||||
useEffect(() => {
|
||||
if (!pathname.startsWith(hotelreservation(lang))) {
|
||||
sessionStorage.removeItem(detailsStorageName)
|
||||
}
|
||||
}, [lang, pathname])
|
||||
|
||||
return null
|
||||
}
|
||||
@@ -7,8 +7,7 @@ import { Lang } from "@/constants/languages"
|
||||
import { selectRate } from "@/constants/routes/hotelReservation"
|
||||
|
||||
import { mapFacilityToIcon } from "@/components/ContentType/HotelPage/data"
|
||||
import { CloseLargeIcon } from "@/components/Icons"
|
||||
import TripAdvisorIcon from "@/components/Icons/TripAdvisor"
|
||||
import { CloseLargeIcon, TripAdvisorIcon } from "@/components/Icons"
|
||||
import Image from "@/components/Image"
|
||||
import Button from "@/components/TempDesignSystem/Button"
|
||||
import Chip from "@/components/TempDesignSystem/Chip"
|
||||
|
||||
@@ -60,7 +60,7 @@ export default function HotelCardDialogListing({
|
||||
const elements = document.querySelectorAll("[data-name]")
|
||||
setTimeout(() => {
|
||||
elements.forEach((el) => observerRef.current?.observe(el))
|
||||
}, 500)
|
||||
}, 1000)
|
||||
}
|
||||
}, [activeCard])
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import { useMediaQuery } from "usehooks-ts"
|
||||
|
||||
import { selectHotel } from "@/constants/routes/hotelReservation"
|
||||
|
||||
import { ArrowUpIcon, CloseIcon, CloseLargeIcon } from "@/components/Icons"
|
||||
import { CloseIcon, CloseLargeIcon } from "@/components/Icons"
|
||||
import InteractiveMap from "@/components/Maps/InteractiveMap"
|
||||
import { BackToTopButton } from "@/components/TempDesignSystem/BackToTopButton"
|
||||
import Button from "@/components/TempDesignSystem/Button"
|
||||
@@ -15,7 +15,6 @@ import useLang from "@/hooks/useLang"
|
||||
|
||||
import FilterAndSortModal from "../FilterAndSortModal"
|
||||
import HotelListing from "./HotelListing"
|
||||
import { getCentralCoordinates } from "./utils"
|
||||
|
||||
import styles from "./selectHotelMap.module.css"
|
||||
|
||||
@@ -27,6 +26,7 @@ export default function SelectHotelMap({
|
||||
mapId,
|
||||
hotels,
|
||||
filterList,
|
||||
cityCoordinates,
|
||||
}: SelectHotelMapProps) {
|
||||
const searchParams = useSearchParams()
|
||||
const router = useRouter()
|
||||
@@ -36,15 +36,13 @@ export default function SelectHotelMap({
|
||||
const [activeHotelPin, setActiveHotelPin] = useState<string | null>(null)
|
||||
const [showBackToTop, setShowBackToTop] = useState<boolean>(false)
|
||||
|
||||
const centralCoordinates = getCentralCoordinates(hotelPins)
|
||||
|
||||
const coordinates = isAboveMobile
|
||||
? centralCoordinates
|
||||
: { ...centralCoordinates, lat: centralCoordinates.lat - 0.006 }
|
||||
|
||||
const selectHotelParams = new URLSearchParams(searchParams.toString())
|
||||
const selectedHotel = selectHotelParams.get("selectedHotel")
|
||||
|
||||
const coordinates = isAboveMobile
|
||||
? cityCoordinates
|
||||
: { ...cityCoordinates, lat: cityCoordinates.lat - 0.006 }
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedHotel) {
|
||||
setActiveHotelPin(selectedHotel)
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
import { HotelPin } from "@/types/components/hotelReservation/selectHotel/map"
|
||||
|
||||
export function getCentralCoordinates(hotels: HotelPin[]) {
|
||||
const centralCoordinates = hotels.reduce(
|
||||
(acc, pin) => {
|
||||
acc.lat += pin.coordinates.lat
|
||||
acc.lng += pin.coordinates.lng
|
||||
return acc
|
||||
},
|
||||
{ lat: 0, lng: 0 }
|
||||
)
|
||||
|
||||
centralCoordinates.lat /= hotels.length
|
||||
centralCoordinates.lng /= hotels.length
|
||||
|
||||
return centralCoordinates
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import TripAdvisorIcon from "@/components/Icons/TripAdvisor"
|
||||
import { TripAdvisorIcon } from "@/components/Icons"
|
||||
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||
|
||||
import styles from "./tripAdvisorChip.module.css"
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
import { FC } from "react"
|
||||
|
||||
import FacebookIcon from "./Facebook"
|
||||
import InstagramIcon from "./Instagram"
|
||||
import TripAdvisorIcon from "./TripAdvisor"
|
||||
import {
|
||||
AccesoriesIcon,
|
||||
AccessibilityIcon,
|
||||
@@ -41,6 +38,7 @@ import {
|
||||
EmailIcon,
|
||||
EyeHideIcon,
|
||||
EyeShowIcon,
|
||||
FacebookIcon,
|
||||
FanIcon,
|
||||
FitnessIcon,
|
||||
FootstoolIcon,
|
||||
@@ -56,6 +54,7 @@ import {
|
||||
HouseIcon,
|
||||
ImageIcon,
|
||||
InfoCircleIcon,
|
||||
InstagramIcon,
|
||||
KayakingIcon,
|
||||
KettleIcon,
|
||||
LampIcon,
|
||||
@@ -93,6 +92,7 @@ import {
|
||||
SwimIcon,
|
||||
ThermostatIcon,
|
||||
TrainIcon,
|
||||
TripAdvisorIcon,
|
||||
TshirtIcon,
|
||||
TshirtWashIcon,
|
||||
TvCastingIcon,
|
||||
|
||||
@@ -55,6 +55,7 @@ export { default as EmailIcon } from "./Email"
|
||||
export { default as ErrorCircleIcon } from "./ErrorCircle"
|
||||
export { default as EyeHideIcon } from "./EyeHide"
|
||||
export { default as EyeShowIcon } from "./EyeShow"
|
||||
export { default as FacebookIcon } from "./Facebook"
|
||||
export { default as FanIcon } from "./Fan"
|
||||
export { default as FilterIcon } from "./Filter"
|
||||
export { default as FitnessIcon } from "./Fitness"
|
||||
@@ -127,6 +128,7 @@ export { default as StreetIcon } from "./Street"
|
||||
export { default as SwimIcon } from "./Swim"
|
||||
export { default as ThermostatIcon } from "./Thermostat"
|
||||
export { default as TrainIcon } from "./Train"
|
||||
export { default as TripAdvisorIcon } from "./TripAdvisor"
|
||||
export { default as TshirtIcon } from "./Tshirt"
|
||||
export { default as TshirtWashIcon } from "./TshirtWash"
|
||||
export { default as TvCastingIcon } from "./TvCasting"
|
||||
|
||||
@@ -2,10 +2,11 @@ import {
|
||||
AdvancedMarker,
|
||||
AdvancedMarkerAnchorPoint,
|
||||
} from "@vis.gl/react-google-maps"
|
||||
import { useState } from "react"
|
||||
import { useRef, useState } from "react"
|
||||
|
||||
import HotelCardDialog from "@/components/HotelReservation/HotelCardDialog"
|
||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||
import useClickOutside from "@/hooks/useClickOutside"
|
||||
|
||||
import HotelMarker from "../../Markers/HotelMarker"
|
||||
|
||||
@@ -19,6 +20,7 @@ export default function HotelListingMapContent({
|
||||
onActiveHotelPinChange,
|
||||
}: HotelListingMapContentProps) {
|
||||
const [hoveredHotelPin, setHoveredHotelPin] = useState<string | null>(null)
|
||||
const dialogRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
function toggleActiveHotelPin(pinName: string | null) {
|
||||
if (onActiveHotelPinChange) {
|
||||
@@ -31,6 +33,10 @@ export default function HotelListingMapContent({
|
||||
return activeHotelPin === pinName || hoveredHotelPin === pinName
|
||||
}
|
||||
|
||||
useClickOutside(dialogRef, isPinActiveOrHovered(activeHotelPin ?? ""), () => {
|
||||
toggleActiveHotelPin(null)
|
||||
})
|
||||
|
||||
return (
|
||||
<div>
|
||||
{hotelPins.map((pin) => {
|
||||
@@ -44,13 +50,13 @@ export default function HotelListingMapContent({
|
||||
zIndex={isActiveOrHovered ? 2 : 0}
|
||||
onMouseEnter={() => setHoveredHotelPin(pin.name)}
|
||||
onMouseLeave={() => setHoveredHotelPin(null)}
|
||||
onClick={() =>
|
||||
onClick={() => {
|
||||
toggleActiveHotelPin(
|
||||
activeHotelPin === pin.name ? null : pin.name
|
||||
)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className={styles.dialogContainer}>
|
||||
<div className={styles.dialogContainer} ref={dialogRef}>
|
||||
<HotelCardDialog
|
||||
isOpen={isActiveOrHovered}
|
||||
handleClose={(event: { stopPropagation: () => void }) => {
|
||||
|
||||
@@ -35,9 +35,9 @@ export default function HotelMapContent({
|
||||
position={poi.coordinates}
|
||||
anchorPoint={AdvancedMarkerAnchorPoint.CENTER}
|
||||
zIndex={activePoi === poi.name ? 2 : 0}
|
||||
onMouseEnter={() => onActivePoiChange?.(poi.name)}
|
||||
onMouseEnter={() => onActivePoiChange?.(poi.name ?? null)}
|
||||
onMouseLeave={() => onActivePoiChange?.(null)}
|
||||
onClick={() => toggleActivePoi(poi.name)}
|
||||
onClick={() => toggleActivePoi(poi.name ?? "")}
|
||||
>
|
||||
<span
|
||||
className={`${styles.poi} ${activePoi === poi.name ? styles.active : ""}`}
|
||||
|
||||
30
components/MyPages/Surprises/Card.tsx
Normal file
30
components/MyPages/Surprises/Card.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import Image from "@/components/Image"
|
||||
import Title from "@/components/TempDesignSystem/Text/Title"
|
||||
|
||||
import styles from "./surprises.module.css"
|
||||
|
||||
import type { CardProps } from "@/types/components/blocks/surprises"
|
||||
|
||||
export default function Card({ title, children }: CardProps) {
|
||||
const intl = useIntl()
|
||||
|
||||
return (
|
||||
<div className={styles.content}>
|
||||
<Image
|
||||
src="/_static/img/loyalty-award.png"
|
||||
width={113}
|
||||
height={125}
|
||||
alt={intl.formatMessage({ id: "Surprise!" })}
|
||||
/>
|
||||
<header>
|
||||
<Title textAlign="center" level="h4">
|
||||
{title}
|
||||
</Title>
|
||||
</header>
|
||||
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
214
components/MyPages/Surprises/Client.tsx
Normal file
214
components/MyPages/Surprises/Client.tsx
Normal file
@@ -0,0 +1,214 @@
|
||||
"use client"
|
||||
|
||||
import { AnimatePresence, motion } from "framer-motion"
|
||||
import { usePathname } from "next/navigation"
|
||||
import React, { useState } from "react"
|
||||
import { Dialog, Modal, ModalOverlay } from "react-aria-components"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { benefits } from "@/constants/routes/myPages"
|
||||
import { trpc } from "@/lib/trpc/client"
|
||||
|
||||
import Link from "@/components/TempDesignSystem/Link"
|
||||
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||
import { toast } from "@/components/TempDesignSystem/Toasts"
|
||||
import useLang from "@/hooks/useLang"
|
||||
|
||||
import confetti from "./confetti"
|
||||
import Header from "./Header"
|
||||
import Initial from "./Initial"
|
||||
import Navigation from "./Navigation"
|
||||
import Slide from "./Slide"
|
||||
|
||||
import styles from "./surprises.module.css"
|
||||
|
||||
import type { SurprisesProps } from "@/types/components/blocks/surprises"
|
||||
|
||||
const MotionModal = motion(Modal)
|
||||
|
||||
export default function SurprisesNotification({
|
||||
surprises,
|
||||
membershipNumber,
|
||||
}: SurprisesProps) {
|
||||
const lang = useLang()
|
||||
const intl = useIntl()
|
||||
const pathname = usePathname()
|
||||
const [open, setOpen] = useState(true)
|
||||
const [[selectedSurprise, direction], setSelectedSurprise] = useState([0, 0])
|
||||
const [showSurprises, setShowSurprises] = useState(false)
|
||||
const unwrap = trpc.contentstack.rewards.unwrap.useMutation({
|
||||
onSuccess: () => {
|
||||
if (pathname.indexOf(benefits[lang]) !== 0) {
|
||||
toast.success(
|
||||
<>
|
||||
{intl.formatMessage(
|
||||
{ id: "Gift(s) added to your benefits" },
|
||||
{ amount: surprises.length }
|
||||
)}
|
||||
<br />
|
||||
<Link href={benefits[lang]} variant="underscored" color="burgundy">
|
||||
{intl.formatMessage({ id: "Go to My Benefits" })}
|
||||
</Link>
|
||||
</>
|
||||
)
|
||||
}
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error("Failed to unwrap surprise", error)
|
||||
},
|
||||
})
|
||||
|
||||
const totalSurprises = surprises.length
|
||||
if (!totalSurprises) {
|
||||
return null
|
||||
}
|
||||
|
||||
const surprise = surprises[selectedSurprise]
|
||||
|
||||
function showSurprise(newDirection: number) {
|
||||
setSelectedSurprise(([currentIndex]) => [
|
||||
currentIndex + newDirection,
|
||||
newDirection,
|
||||
])
|
||||
}
|
||||
|
||||
async function viewRewards() {
|
||||
const updates = surprises
|
||||
.map((surprise) => {
|
||||
const coupons = surprise.coupons
|
||||
?.map((coupon) => {
|
||||
if (coupon?.couponCode) {
|
||||
return {
|
||||
rewardId: surprise.id,
|
||||
couponCode: coupon.couponCode,
|
||||
}
|
||||
}
|
||||
})
|
||||
.filter(
|
||||
(coupon): coupon is { rewardId: string; couponCode: string } =>
|
||||
!!coupon
|
||||
)
|
||||
|
||||
return coupons
|
||||
})
|
||||
.flat()
|
||||
|
||||
unwrap.mutate(updates)
|
||||
}
|
||||
|
||||
return (
|
||||
<ModalOverlay
|
||||
className={styles.overlay}
|
||||
isOpen={open}
|
||||
onOpenChange={setOpen}
|
||||
isKeyboardDismissDisabled
|
||||
>
|
||||
<canvas id="surprise-confetti" className={styles.confetti} />
|
||||
<AnimatePresence mode="wait">
|
||||
{open && (
|
||||
<MotionModal
|
||||
className={styles.modal}
|
||||
initial={{ y: 32, opacity: 0 }}
|
||||
animate={{ y: 0, opacity: 1 }}
|
||||
exit={{ y: 32, opacity: 0 }}
|
||||
transition={{
|
||||
y: { duration: 0.4, ease: "easeInOut" },
|
||||
opacity: { duration: 0.4, ease: "easeInOut" },
|
||||
}}
|
||||
onAnimationComplete={confetti}
|
||||
>
|
||||
<Dialog aria-label="Surprises" className={styles.dialog}>
|
||||
{({ close }) => {
|
||||
return (
|
||||
<>
|
||||
<Header
|
||||
onClose={() => {
|
||||
viewRewards()
|
||||
close()
|
||||
}}
|
||||
>
|
||||
{showSurprises && totalSurprises > 1 && (
|
||||
<Caption type="label" uppercase>
|
||||
{intl.formatMessage(
|
||||
{ id: "{amount} out of {total}" },
|
||||
{
|
||||
amount: selectedSurprise + 1,
|
||||
total: totalSurprises,
|
||||
}
|
||||
)}
|
||||
</Caption>
|
||||
)}
|
||||
</Header>
|
||||
|
||||
{showSurprises ? (
|
||||
<>
|
||||
<AnimatePresence
|
||||
mode="popLayout"
|
||||
initial={false}
|
||||
custom={direction}
|
||||
>
|
||||
<motion.div
|
||||
key={selectedSurprise}
|
||||
custom={direction}
|
||||
variants={variants}
|
||||
initial="enter"
|
||||
animate="center"
|
||||
exit="exit"
|
||||
transition={{
|
||||
x: { type: "ease", duration: 0.5 },
|
||||
opacity: { duration: 0.2 },
|
||||
}}
|
||||
layout
|
||||
>
|
||||
<Slide
|
||||
surprise={surprise}
|
||||
membershipNumber={membershipNumber}
|
||||
/>
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
|
||||
{totalSurprises > 1 && (
|
||||
<Navigation
|
||||
selectedSurprise={selectedSurprise}
|
||||
totalSurprises={totalSurprises}
|
||||
showSurprise={showSurprise}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<Initial
|
||||
totalSurprises={totalSurprises}
|
||||
onOpen={() => {
|
||||
setShowSurprises(true)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}}
|
||||
</Dialog>
|
||||
</MotionModal>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</ModalOverlay>
|
||||
)
|
||||
}
|
||||
|
||||
const variants = {
|
||||
enter: (direction: number) => {
|
||||
return {
|
||||
x: direction > 0 ? 1000 : -1000,
|
||||
opacity: 0,
|
||||
}
|
||||
},
|
||||
center: {
|
||||
x: 0,
|
||||
opacity: 1,
|
||||
},
|
||||
exit: (direction: number) => {
|
||||
return {
|
||||
x: direction < 0 ? 1000 : -1000,
|
||||
opacity: 0,
|
||||
}
|
||||
},
|
||||
}
|
||||
16
components/MyPages/Surprises/Header.tsx
Normal file
16
components/MyPages/Surprises/Header.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import { CloseLargeIcon } from "@/components/Icons"
|
||||
|
||||
import styles from "./surprises.module.css"
|
||||
|
||||
import type { HeaderProps } from "@/types/components/blocks/surprises"
|
||||
|
||||
export default function Header({ onClose, children }: HeaderProps) {
|
||||
return (
|
||||
<div className={styles.top}>
|
||||
{children}
|
||||
<button onClick={onClose} type="button" className={styles.close}>
|
||||
<CloseLargeIcon />
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
62
components/MyPages/Surprises/Initial.tsx
Normal file
62
components/MyPages/Surprises/Initial.tsx
Normal file
@@ -0,0 +1,62 @@
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import Button from "@/components/TempDesignSystem/Button"
|
||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||
|
||||
import Card from "./Card"
|
||||
|
||||
import type { InitialProps } from "@/types/components/blocks/surprises"
|
||||
|
||||
export default function Initial({ totalSurprises, onOpen }: InitialProps) {
|
||||
const intl = useIntl()
|
||||
|
||||
return (
|
||||
<Card title={intl.formatMessage({ id: "Surprise!" })}>
|
||||
<Body textAlign="center">
|
||||
{totalSurprises > 1 ? (
|
||||
<>
|
||||
{intl.formatMessage<React.ReactNode>(
|
||||
{
|
||||
id: "You have <b>#</b> gifts waiting for you!",
|
||||
},
|
||||
{
|
||||
amount: totalSurprises,
|
||||
b: (str) => <b>{str}</b>,
|
||||
}
|
||||
)}
|
||||
<br />
|
||||
{intl.formatMessage({
|
||||
id: "Hurry up and use them before they expire!",
|
||||
})}
|
||||
</>
|
||||
) : (
|
||||
intl.formatMessage({
|
||||
id: "We have a special gift waiting for you!",
|
||||
})
|
||||
)}
|
||||
</Body>
|
||||
<Caption>
|
||||
{intl.formatMessage({
|
||||
id: "You'll find all your gifts in 'My benefits'",
|
||||
})}
|
||||
</Caption>
|
||||
|
||||
<Button
|
||||
intent="primary"
|
||||
onPress={onOpen}
|
||||
size="medium"
|
||||
theme="base"
|
||||
fullWidth
|
||||
autoFocus
|
||||
>
|
||||
{intl.formatMessage(
|
||||
{
|
||||
id: "Open gift(s)",
|
||||
},
|
||||
{ amount: totalSurprises }
|
||||
)}
|
||||
</Button>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
45
components/MyPages/Surprises/Navigation.tsx
Normal file
45
components/MyPages/Surprises/Navigation.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { ChevronRightSmallIcon } from "@/components/Icons"
|
||||
import Button from "@/components/TempDesignSystem/Button"
|
||||
|
||||
import styles from "./surprises.module.css"
|
||||
|
||||
import type { NavigationProps } from "@/types/components/blocks/surprises"
|
||||
|
||||
export default function Navigation({
|
||||
selectedSurprise,
|
||||
totalSurprises,
|
||||
showSurprise,
|
||||
}: NavigationProps) {
|
||||
const intl = useIntl()
|
||||
|
||||
return (
|
||||
<nav className={styles.nav}>
|
||||
<Button
|
||||
variant="icon"
|
||||
intent="tertiary"
|
||||
disabled={selectedSurprise === 0}
|
||||
onPress={() => showSurprise(-1)}
|
||||
size="small"
|
||||
>
|
||||
<ChevronRightSmallIcon
|
||||
className={styles.chevron}
|
||||
width={20}
|
||||
height={20}
|
||||
/>
|
||||
{intl.formatMessage({ id: "Previous" })}
|
||||
</Button>
|
||||
<Button
|
||||
variant="icon"
|
||||
intent="tertiary"
|
||||
disabled={selectedSurprise === totalSurprises - 1}
|
||||
onPress={() => showSurprise(1)}
|
||||
size="small"
|
||||
>
|
||||
{intl.formatMessage({ id: "Next" })}
|
||||
<ChevronRightSmallIcon width={20} height={20} />
|
||||
</Button>
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
49
components/MyPages/Surprises/Slide.tsx
Normal file
49
components/MyPages/Surprises/Slide.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { dt } from "@/lib/dt"
|
||||
|
||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||
import useLang from "@/hooks/useLang"
|
||||
|
||||
import Card from "./Card"
|
||||
|
||||
import styles from "./surprises.module.css"
|
||||
|
||||
import type { SlideProps } from "@/types/components/blocks/surprises"
|
||||
|
||||
export default function Slide({ surprise, membershipNumber }: SlideProps) {
|
||||
const lang = useLang()
|
||||
const intl = useIntl()
|
||||
|
||||
const earliestExpirationDate = surprise.coupons?.reduce(
|
||||
(earliestDate, coupon) => {
|
||||
const expiresAt = dt(coupon.expiresAt)
|
||||
return earliestDate.isBefore(expiresAt) ? earliestDate : expiresAt
|
||||
},
|
||||
dt()
|
||||
)
|
||||
return (
|
||||
<Card title={surprise.label}>
|
||||
<Body textAlign="center">{surprise.description}</Body>
|
||||
<div className={styles.badge}>
|
||||
<Caption>
|
||||
{intl.formatMessage(
|
||||
{ id: "Expires at the earliest" },
|
||||
{
|
||||
date: dt(earliestExpirationDate)
|
||||
.locale(lang)
|
||||
.format("D MMM YYYY"),
|
||||
}
|
||||
)}
|
||||
</Caption>
|
||||
<Caption>
|
||||
{intl.formatMessage({
|
||||
id: "Membership ID",
|
||||
})}{" "}
|
||||
{membershipNumber}
|
||||
</Caption>
|
||||
</div>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -1,247 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import { usePathname } from "next/navigation"
|
||||
import React, { useState } from "react"
|
||||
import { Dialog, Modal, ModalOverlay } from "react-aria-components"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { benefits } from "@/constants/routes/myPages"
|
||||
import { dt } from "@/lib/dt"
|
||||
import { trpc } from "@/lib/trpc/client"
|
||||
|
||||
import { ChevronRightSmallIcon, CloseLargeIcon } from "@/components/Icons"
|
||||
import Image from "@/components/Image"
|
||||
import Button from "@/components/TempDesignSystem/Button"
|
||||
import Link from "@/components/TempDesignSystem/Link"
|
||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||
import Title from "@/components/TempDesignSystem/Text/Title"
|
||||
import { toast } from "@/components/TempDesignSystem/Toasts"
|
||||
import useLang from "@/hooks/useLang"
|
||||
|
||||
import styles from "./surprises.module.css"
|
||||
|
||||
import type { SurprisesProps } from "@/types/components/blocks/surprises"
|
||||
|
||||
export default function SurprisesNotification({
|
||||
surprises,
|
||||
membershipNumber,
|
||||
}: SurprisesProps) {
|
||||
const lang = useLang()
|
||||
const pathname = usePathname()
|
||||
const [open, setOpen] = useState(true)
|
||||
const [selectedSurprise, setSelectedSurprise] = useState(0)
|
||||
const [showSurprises, setShowSurprises] = useState(false)
|
||||
const update = trpc.contentstack.rewards.update.useMutation()
|
||||
const intl = useIntl()
|
||||
|
||||
if (!surprises.length) {
|
||||
return null
|
||||
}
|
||||
|
||||
const surprise = surprises[selectedSurprise]
|
||||
|
||||
function showSurprise(n: number) {
|
||||
setSelectedSurprise((surprise) => surprise + n)
|
||||
}
|
||||
|
||||
function viewRewards() {
|
||||
if (surprise.reward_id) {
|
||||
update.mutate({ id: surprise.reward_id })
|
||||
}
|
||||
}
|
||||
|
||||
function closeModal(close: VoidFunction) {
|
||||
viewRewards()
|
||||
close()
|
||||
|
||||
if (pathname.indexOf(benefits[lang]) !== 0) {
|
||||
toast.success(
|
||||
<>
|
||||
{intl.formatMessage(
|
||||
{ id: "Gift(s) added to your benefits" },
|
||||
{ amount: surprises.length }
|
||||
)}
|
||||
<br />
|
||||
<Link href={benefits[lang]} variant="underscored" color="burgundy">
|
||||
{intl.formatMessage({ id: "Go to My Benefits" })}
|
||||
</Link>
|
||||
</>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<ModalOverlay
|
||||
className={styles.overlay}
|
||||
isOpen={open}
|
||||
onOpenChange={setOpen}
|
||||
isKeyboardDismissDisabled
|
||||
>
|
||||
<Modal className={styles.modal}>
|
||||
<Dialog aria-label="Surprises" className={styles.dialog}>
|
||||
{({ close }) => {
|
||||
return (
|
||||
<>
|
||||
<div className={styles.top}>
|
||||
{surprises.length > 1 && showSurprises && (
|
||||
<Caption type="label" uppercase>
|
||||
{intl.formatMessage(
|
||||
{ id: "{amount} out of {total}" },
|
||||
{
|
||||
amount: selectedSurprise + 1,
|
||||
total: surprises.length,
|
||||
}
|
||||
)}
|
||||
</Caption>
|
||||
)}
|
||||
<button
|
||||
onClick={() => closeModal(close)}
|
||||
type="button"
|
||||
className={styles.close}
|
||||
>
|
||||
<CloseLargeIcon />
|
||||
</button>
|
||||
</div>
|
||||
{showSurprises ? (
|
||||
<>
|
||||
<div className={styles.content}>
|
||||
<Surprise title={surprise.label}>
|
||||
<Body textAlign="center">{surprise.description}</Body>
|
||||
<div className={styles.badge}>
|
||||
<Caption>
|
||||
{intl.formatMessage({ id: "Valid through" })}{" "}
|
||||
{dt(surprise.endsAt)
|
||||
.locale(lang)
|
||||
.format("DD MMM YYYY")}
|
||||
</Caption>
|
||||
<Caption>
|
||||
{intl.formatMessage({ id: "Membership ID" })}{" "}
|
||||
{membershipNumber}
|
||||
</Caption>
|
||||
</div>
|
||||
</Surprise>
|
||||
</div>
|
||||
{surprises.length > 1 && (
|
||||
<>
|
||||
<nav className={styles.nav}>
|
||||
<Button
|
||||
variant="icon"
|
||||
intent="tertiary"
|
||||
disabled={selectedSurprise === 0}
|
||||
onPress={() => showSurprise(-1)}
|
||||
size="small"
|
||||
>
|
||||
<ChevronRightSmallIcon
|
||||
className={styles.chevron}
|
||||
width={20}
|
||||
height={20}
|
||||
/>
|
||||
{intl.formatMessage({ id: "Previous" })}
|
||||
</Button>
|
||||
<Button
|
||||
variant="icon"
|
||||
intent="tertiary"
|
||||
disabled={selectedSurprise === surprises.length - 1}
|
||||
onPress={() => showSurprise(1)}
|
||||
size="small"
|
||||
>
|
||||
{intl.formatMessage({ id: "Next" })}
|
||||
<ChevronRightSmallIcon width={20} height={20} />
|
||||
</Button>
|
||||
</nav>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<div className={styles.content}>
|
||||
{surprises.length > 1 ? (
|
||||
<Surprise title={intl.formatMessage({ id: "Surprise!" })}>
|
||||
<Body textAlign="center">
|
||||
{intl.formatMessage<React.ReactNode>(
|
||||
{
|
||||
id: "You have <b>#</b> gifts waiting for you!",
|
||||
},
|
||||
{
|
||||
amount: surprises.length,
|
||||
b: (str) => <b>{str}</b>,
|
||||
}
|
||||
)}
|
||||
<br />
|
||||
{intl.formatMessage({
|
||||
id: "Hurry up and use them before they expire!",
|
||||
})}
|
||||
</Body>
|
||||
<Caption>
|
||||
{intl.formatMessage({
|
||||
id: "You'll find all your gifts in 'My benefits'",
|
||||
})}
|
||||
</Caption>
|
||||
</Surprise>
|
||||
) : (
|
||||
<Surprise title={intl.formatMessage({ id: "Surprise!" })}>
|
||||
<Body textAlign="center">
|
||||
{intl.formatMessage({
|
||||
id: "We have a special gift waiting for you!",
|
||||
})}
|
||||
</Body>
|
||||
<Caption>
|
||||
{intl.formatMessage({
|
||||
id: "You'll find all your gifts in 'My benefits'",
|
||||
})}
|
||||
</Caption>
|
||||
</Surprise>
|
||||
)}
|
||||
|
||||
<Button
|
||||
intent="primary"
|
||||
onPress={() => {
|
||||
viewRewards()
|
||||
setShowSurprises(true)
|
||||
}}
|
||||
size="medium"
|
||||
theme="base"
|
||||
fullWidth
|
||||
autoFocus
|
||||
>
|
||||
{intl.formatMessage(
|
||||
{
|
||||
id: "Open gift(s)",
|
||||
},
|
||||
{ amount: surprises.length }
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}}
|
||||
</Dialog>
|
||||
</Modal>
|
||||
</ModalOverlay>
|
||||
)
|
||||
}
|
||||
|
||||
function Surprise({
|
||||
title,
|
||||
children,
|
||||
}: {
|
||||
title?: string
|
||||
children?: React.ReactNode
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<Image
|
||||
src="/_static/img/loyalty-award.png"
|
||||
width={113}
|
||||
height={125}
|
||||
alt="Gift"
|
||||
/>
|
||||
<Title textAlign="center" level="h4">
|
||||
{title}
|
||||
</Title>
|
||||
|
||||
{children}
|
||||
</>
|
||||
)
|
||||
}
|
||||
13
components/MyPages/Surprises/confetti.ts
Normal file
13
components/MyPages/Surprises/confetti.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { confetti as particlesConfetti } from "@tsparticles/confetti"
|
||||
|
||||
export default function confetti() {
|
||||
particlesConfetti("surprise-confetti", {
|
||||
count: 300,
|
||||
spread: 150,
|
||||
position: {
|
||||
y: 60,
|
||||
},
|
||||
colors: ["#cd0921", "#4d001b", "#fff"],
|
||||
shapes: ["star", "square", "circle", "polygon"],
|
||||
})
|
||||
}
|
||||
@@ -1,9 +1,14 @@
|
||||
import { env } from "@/env/server"
|
||||
import { getProfile } from "@/lib/trpc/memoizedRequests"
|
||||
import { serverClient } from "@/lib/trpc/server"
|
||||
|
||||
import SurprisesNotification from "./SurprisesNotification"
|
||||
import SurprisesClient from "./Client"
|
||||
|
||||
export default async function Surprises() {
|
||||
if (env.HIDE_FOR_NEXT_RELEASE) {
|
||||
return null
|
||||
}
|
||||
|
||||
const user = await getProfile()
|
||||
|
||||
if (!user || "error" in user) {
|
||||
@@ -17,7 +22,7 @@ export default async function Surprises() {
|
||||
}
|
||||
|
||||
return (
|
||||
<SurprisesNotification
|
||||
<SurprisesClient
|
||||
surprises={surprises}
|
||||
membershipNumber={user.membership?.membershipNumber}
|
||||
/>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@keyframes modal-fade {
|
||||
@keyframes fade {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
@@ -8,16 +8,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slide-up {
|
||||
from {
|
||||
transform: translateY(100%);
|
||||
}
|
||||
|
||||
to {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.overlay {
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
height: var(--visual-viewport-height);
|
||||
@@ -28,10 +18,10 @@
|
||||
z-index: 100;
|
||||
|
||||
&[data-entering] {
|
||||
animation: modal-fade 200ms;
|
||||
animation: fade 400ms ease-in;
|
||||
}
|
||||
&[data-exiting] {
|
||||
animation: modal-fade 200ms reverse ease-in;
|
||||
animation: fade 400ms reverse ease-in;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,6 +33,19 @@
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px) and (prefers-reduced-motion) {
|
||||
.overlay:before {
|
||||
background-image: url("/_static/img/confetti.svg");
|
||||
background-repeat: no-repeat;
|
||||
background-position: center 40%;
|
||||
content: "";
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
animation: fade 400ms ease-in;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.modal {
|
||||
background-color: var(--Base-Surface-Primary-light-Normal);
|
||||
border-radius: var(--Corner-radius-Medium);
|
||||
@@ -51,20 +54,7 @@
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
|
||||
&[data-entering] {
|
||||
animation: slide-up 200ms;
|
||||
}
|
||||
&[data-exiting] {
|
||||
animation: slide-up 200ms reverse ease-in-out;
|
||||
}
|
||||
}
|
||||
|
||||
.dialog {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Spacing-x2);
|
||||
padding-bottom: var(--Spacing-x2);
|
||||
z-index: 102;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
@@ -75,6 +65,17 @@
|
||||
}
|
||||
}
|
||||
|
||||
.dialog {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Spacing-x2);
|
||||
padding-bottom: var(--Spacing-x2);
|
||||
|
||||
/* to hide sliding cards */
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.top {
|
||||
--button-height: 32px;
|
||||
box-sizing: content-box;
|
||||
@@ -90,8 +91,10 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0 var(--Spacing-x3);
|
||||
gap: var(--Spacing-x2);
|
||||
min-height: 350px;
|
||||
}
|
||||
|
||||
.nav {
|
||||
@@ -103,6 +106,8 @@
|
||||
}
|
||||
|
||||
.nav button {
|
||||
user-select: none;
|
||||
|
||||
&:nth-child(1) {
|
||||
padding-left: 0;
|
||||
}
|
||||
@@ -141,3 +146,12 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/*
|
||||
* temporary fix until next version of tsparticles is released
|
||||
* https://github.com/tsparticles/tsparticles/issues/5375
|
||||
*/
|
||||
.confetti {
|
||||
position: relative;
|
||||
z-index: 101;
|
||||
}
|
||||
|
||||
@@ -51,3 +51,7 @@
|
||||
.opacity8 {
|
||||
opacity: 0.08;
|
||||
}
|
||||
|
||||
.baseSurfaceSubtleHover {
|
||||
background-color: var(--Base-Surface-Subtle-Hover);
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ export const dividerVariants = cva(styles.divider, {
|
||||
primaryLightSubtle: styles.primaryLightSubtle,
|
||||
subtle: styles.subtle,
|
||||
white: styles.white,
|
||||
baseSurfaceSutbleHover: styles.baseSurfaceSubtleHover,
|
||||
},
|
||||
opacity: {
|
||||
100: styles.opacity100,
|
||||
|
||||
@@ -12,6 +12,7 @@ interface BaseCardProps
|
||||
title: React.ReactNode
|
||||
type: "checkbox" | "radio"
|
||||
value?: string
|
||||
handleSelectedOnClick?: () => void
|
||||
}
|
||||
|
||||
interface ListCardProps extends BaseCardProps {
|
||||
|
||||
@@ -25,10 +25,22 @@ export default function Card({
|
||||
title,
|
||||
type,
|
||||
value,
|
||||
handleSelectedOnClick,
|
||||
}: CardProps) {
|
||||
const { register } = useFormContext()
|
||||
|
||||
function onLabelClick(event: React.MouseEvent) {
|
||||
// Preventing click event on label elements firing twice: https://github.com/facebook/react/issues/14295
|
||||
event.preventDefault()
|
||||
handleSelectedOnClick?.()
|
||||
}
|
||||
return (
|
||||
<label className={styles.label} data-declined={declined} tabIndex={0}>
|
||||
<label
|
||||
className={styles.label}
|
||||
data-declined={declined}
|
||||
tabIndex={0}
|
||||
onClick={handleSelectedOnClick ? onLabelClick : undefined}
|
||||
>
|
||||
<Caption className={styles.title} color="burgundy" type="label" uppercase>
|
||||
{title}
|
||||
</Caption>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
/**
|
||||
* @param {Lang} lang
|
||||
*/
|
||||
function base(lang) {
|
||||
export function hotelreservation(lang) {
|
||||
return `/${lang}/hotelreservation`
|
||||
}
|
||||
|
||||
@@ -13,47 +13,47 @@ function base(lang) {
|
||||
* @param {Lang} lang
|
||||
*/
|
||||
export function bookingConfirmation(lang) {
|
||||
return `${base(lang)}/booking-confirmation`
|
||||
return `${hotelreservation(lang)}/booking-confirmation`
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Lang} lang
|
||||
*/
|
||||
export function details(lang) {
|
||||
return `${base(lang)}/details`
|
||||
return `${hotelreservation(lang)}/details`
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Lang} lang
|
||||
*/
|
||||
export function payment(lang) {
|
||||
return `${base(lang)}/payment`
|
||||
return `${hotelreservation(lang)}/payment`
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Lang} lang
|
||||
*/
|
||||
export function selectBed(lang) {
|
||||
return `${base(lang)}/select-bed`
|
||||
return `${hotelreservation(lang)}/select-bed`
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Lang} lang
|
||||
*/
|
||||
export function selectHotel(lang) {
|
||||
return `${base(lang)}/select-hotel`
|
||||
return `${hotelreservation(lang)}/select-hotel`
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Lang} lang
|
||||
*/
|
||||
export function selectHotelMap(lang) {
|
||||
return `${base(lang)}/select-hotel/map`
|
||||
return `${hotelreservation(lang)}/select-hotel/map`
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Lang} lang
|
||||
*/
|
||||
export function selectRate(lang) {
|
||||
return `${base(lang)}/select-rate`
|
||||
return `${hotelreservation(lang)}/select-rate`
|
||||
}
|
||||
|
||||
@@ -118,6 +118,7 @@
|
||||
"Enter destination or hotel": "Indtast destination eller hotel",
|
||||
"Enter your details": "Indtast dine oplysninger",
|
||||
"Events that make an impression": "Events that make an impression",
|
||||
"Expires at the earliest": "Udløber tidligst {date}",
|
||||
"Explore all levels and benefits": "Udforsk alle niveauer og fordele",
|
||||
"Explore nearby": "Udforsk i nærheden",
|
||||
"Extras to your booking": "Tillæg til din booking",
|
||||
@@ -380,7 +381,6 @@
|
||||
"Use bonus cheque": "Brug Bonus Cheque",
|
||||
"Use code/voucher": "Brug kode/voucher",
|
||||
"User information": "Brugeroplysninger",
|
||||
"Valid through": "Gyldig igennem",
|
||||
"View as list": "Vis som liste",
|
||||
"View as map": "Vis som kort",
|
||||
"View your booking": "Se din booking",
|
||||
|
||||
@@ -118,6 +118,7 @@
|
||||
"Enter destination or hotel": "Reiseziel oder Hotel eingeben",
|
||||
"Enter your details": "Geben Sie Ihre Daten ein",
|
||||
"Events that make an impression": "Events that make an impression",
|
||||
"Expires at the earliest": "Läuft frühestens am {date} ab",
|
||||
"Explore all levels and benefits": "Entdecken Sie alle Levels und Vorteile",
|
||||
"Explore nearby": "Erkunden Sie die Umgebung",
|
||||
"Extras to your booking": "Extras zu Ihrer Buchung",
|
||||
@@ -378,7 +379,6 @@
|
||||
"Use bonus cheque": "Bonusscheck nutzen",
|
||||
"Use code/voucher": "Code/Gutschein nutzen",
|
||||
"User information": "Nutzerinformation",
|
||||
"Valid through": "Gültig bis",
|
||||
"View as list": "Als Liste anzeigen",
|
||||
"View as map": "Als Karte anzeigen",
|
||||
"View your booking": "Ihre Buchung ansehen",
|
||||
|
||||
@@ -127,6 +127,7 @@
|
||||
"Enter destination or hotel": "Enter destination or hotel",
|
||||
"Enter your details": "Enter your details",
|
||||
"Events that make an impression": "Events that make an impression",
|
||||
"Expires at the earliest": "Expires at the earliest {date}",
|
||||
"Explore all levels and benefits": "Explore all levels and benefits",
|
||||
"Explore nearby": "Explore nearby",
|
||||
"Extras to your booking": "Extras to your booking",
|
||||
@@ -410,7 +411,6 @@
|
||||
"User information": "User information",
|
||||
"VAT": "VAT",
|
||||
"VAT amount": "VAT amount",
|
||||
"Valid through": "Valid through",
|
||||
"View as list": "View as list",
|
||||
"View as map": "View as map",
|
||||
"View terms": "View terms",
|
||||
|
||||
@@ -118,6 +118,7 @@
|
||||
"Enter destination or hotel": "Anna kohde tai hotelli",
|
||||
"Enter your details": "Anna tietosi",
|
||||
"Events that make an impression": "Events that make an impression",
|
||||
"Expires at the earliest": "Päättyy aikaisintaan {date}",
|
||||
"Explore all levels and benefits": "Tutustu kaikkiin tasoihin ja etuihin",
|
||||
"Explore nearby": "Tutustu lähialueeseen",
|
||||
"Extras to your booking": "Varauksessa lisäpalveluita",
|
||||
@@ -380,7 +381,6 @@
|
||||
"Use bonus cheque": "Käytä bonussekkiä",
|
||||
"Use code/voucher": "Käytä koodia/voucheria",
|
||||
"User information": "Käyttäjän tiedot",
|
||||
"Valid through": "Voimassa läpi",
|
||||
"View as list": "Näytä listana",
|
||||
"View as map": "Näytä kartalla",
|
||||
"View your booking": "Näytä varauksesi",
|
||||
|
||||
@@ -117,6 +117,7 @@
|
||||
"Enter destination or hotel": "Skriv inn destinasjon eller hotell",
|
||||
"Enter your details": "Skriv inn detaljene dine",
|
||||
"Events that make an impression": "Events that make an impression",
|
||||
"Expires at the earliest": "Utløper tidligst {date}",
|
||||
"Explore all levels and benefits": "Utforsk alle nivåer og fordeler",
|
||||
"Explore nearby": "Utforsk i nærheten",
|
||||
"Extras to your booking": "Tilvalg til bestillingen din",
|
||||
@@ -377,7 +378,6 @@
|
||||
"Use bonus cheque": "Bruk bonussjekk",
|
||||
"Use code/voucher": "Bruk kode/voucher",
|
||||
"User information": "Brukerinformasjon",
|
||||
"Valid through": "Gyldig gjennom",
|
||||
"View as list": "Vis som liste",
|
||||
"View as map": "Vis som kart",
|
||||
"View your booking": "Se din bestilling",
|
||||
|
||||
@@ -117,6 +117,7 @@
|
||||
"Enter destination or hotel": "Ange destination eller hotell",
|
||||
"Enter your details": "Ange dina uppgifter",
|
||||
"Events that make an impression": "Events that make an impression",
|
||||
"Expires at the earliest": "Löper ut tidigast {date}",
|
||||
"Explore all levels and benefits": "Utforska alla nivåer och fördelar",
|
||||
"Explore nearby": "Utforska i närheten",
|
||||
"Extras to your booking": "Extra tillval till din bokning",
|
||||
@@ -377,7 +378,6 @@
|
||||
"Use bonus cheque": "Använd bonuscheck",
|
||||
"Use code/voucher": "Använd kod/voucher",
|
||||
"User information": "Användarinformation",
|
||||
"Valid through": "Giltig t.o.m.",
|
||||
"View as list": "Visa som lista",
|
||||
"View as map": "Visa som karta",
|
||||
"View your booking": "Visa din bokning",
|
||||
|
||||
@@ -158,3 +158,9 @@ export const getBookingConfirmation = cache(
|
||||
return serverClient().booking.confirmation({ confirmationNumber })
|
||||
}
|
||||
)
|
||||
|
||||
export const getCityCoordinates = cache(
|
||||
async function getMemoizedCityCoordinates(input: { city: string }) {
|
||||
return serverClient().hotel.map.city(input)
|
||||
}
|
||||
)
|
||||
|
||||
274
package-lock.json
generated
274
package-lock.json
generated
@@ -27,6 +27,7 @@
|
||||
"@trpc/client": "^11.0.0-rc.467",
|
||||
"@trpc/react-query": "^11.0.0-rc.467",
|
||||
"@trpc/server": "^11.0.0-rc.467",
|
||||
"@tsparticles/confetti": "^3.5.0",
|
||||
"@vercel/otel": "^1.9.1",
|
||||
"@vis.gl/react-google-maps": "^1.2.0",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
@@ -6402,6 +6403,279 @@
|
||||
"integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@tsparticles/basic": {
|
||||
"version": "3.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@tsparticles/basic/-/basic-3.5.0.tgz",
|
||||
"integrity": "sha512-oty33TxM2aHWrzcwWRic1bQ04KBCdpnvzv8JXEkx5Uyp70vgVegUbtKmwGki3shqKZIt3v2qE4I8NsK6onhLrA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/matteobruni"
|
||||
},
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/tsparticles"
|
||||
},
|
||||
{
|
||||
"type": "buymeacoffee",
|
||||
"url": "https://www.buymeacoffee.com/matteobruni"
|
||||
}
|
||||
],
|
||||
"dependencies": {
|
||||
"@tsparticles/engine": "^3.5.0",
|
||||
"@tsparticles/move-base": "^3.5.0",
|
||||
"@tsparticles/shape-circle": "^3.5.0",
|
||||
"@tsparticles/updater-color": "^3.5.0",
|
||||
"@tsparticles/updater-opacity": "^3.5.0",
|
||||
"@tsparticles/updater-out-modes": "^3.5.0",
|
||||
"@tsparticles/updater-size": "^3.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tsparticles/confetti": {
|
||||
"version": "3.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@tsparticles/confetti/-/confetti-3.5.0.tgz",
|
||||
"integrity": "sha512-wS3nqtanbCvAbNlyAffKJq6lgIPzHFljEOO3JSCDgRD6rG5X/jvidhw2vR3kLrjBTV40c+Xv6MpJgSgTRWkogg==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/matteobruni"
|
||||
},
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/tsparticles"
|
||||
},
|
||||
{
|
||||
"type": "buymeacoffee",
|
||||
"url": "https://www.buymeacoffee.com/matteobruni"
|
||||
}
|
||||
],
|
||||
"dependencies": {
|
||||
"@tsparticles/basic": "^3.5.0",
|
||||
"@tsparticles/engine": "^3.5.0",
|
||||
"@tsparticles/plugin-emitters": "^3.5.0",
|
||||
"@tsparticles/plugin-motion": "^3.5.0",
|
||||
"@tsparticles/shape-cards": "^3.5.0",
|
||||
"@tsparticles/shape-emoji": "^3.5.0",
|
||||
"@tsparticles/shape-heart": "^3.5.0",
|
||||
"@tsparticles/shape-image": "^3.5.0",
|
||||
"@tsparticles/shape-polygon": "^3.5.0",
|
||||
"@tsparticles/shape-square": "^3.5.0",
|
||||
"@tsparticles/shape-star": "^3.5.0",
|
||||
"@tsparticles/updater-life": "^3.5.0",
|
||||
"@tsparticles/updater-roll": "^3.5.0",
|
||||
"@tsparticles/updater-rotate": "^3.5.0",
|
||||
"@tsparticles/updater-tilt": "^3.5.0",
|
||||
"@tsparticles/updater-wobble": "^3.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tsparticles/engine": {
|
||||
"version": "3.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@tsparticles/engine/-/engine-3.5.0.tgz",
|
||||
"integrity": "sha512-RCwrJ2SvSYdhXJ24oUCjSUKEZQ9lXwObOWMvfMC9vS6/bk+Qo0N7Xx8AfumqzP/LebB1YJdlCvuoJMauAon0Pg==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/matteobruni"
|
||||
},
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/tsparticles"
|
||||
},
|
||||
{
|
||||
"type": "buymeacoffee",
|
||||
"url": "https://www.buymeacoffee.com/matteobruni"
|
||||
}
|
||||
],
|
||||
"hasInstallScript": true
|
||||
},
|
||||
"node_modules/@tsparticles/move-base": {
|
||||
"version": "3.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@tsparticles/move-base/-/move-base-3.5.0.tgz",
|
||||
"integrity": "sha512-9oDk7zTxyhUCstj3lHTNTiWAgqIBzWa2o1tVQFK63Qwq+/WxzJCSwZOocC9PAHGM1IP6nA4zYJSfjbMBTrUocA==",
|
||||
"dependencies": {
|
||||
"@tsparticles/engine": "^3.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tsparticles/plugin-emitters": {
|
||||
"version": "3.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@tsparticles/plugin-emitters/-/plugin-emitters-3.5.0.tgz",
|
||||
"integrity": "sha512-8Vg6wAPS75ibkukqtTM7yoC+8NnfXBl8xVUUbTaoeQCE0WDWwztboMf5L4pUgWe9WA52ZgFkWtT/mFH5wk5T9g==",
|
||||
"dependencies": {
|
||||
"@tsparticles/engine": "^3.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tsparticles/plugin-motion": {
|
||||
"version": "3.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@tsparticles/plugin-motion/-/plugin-motion-3.5.0.tgz",
|
||||
"integrity": "sha512-juP8f9ABjlhQmg4SO+tTofLYJwvwLPfKWJYvG8c6HU2rlJxJ/6eeWe9kDpv/T8nun3kXYHtrLhcJAmvWg/b5qA==",
|
||||
"dependencies": {
|
||||
"@tsparticles/engine": "^3.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tsparticles/shape-cards": {
|
||||
"version": "3.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@tsparticles/shape-cards/-/shape-cards-3.5.0.tgz",
|
||||
"integrity": "sha512-rU7rp1Yn1leHpCNA/7vrfY6tcLjvrG6A6sOT11dSanIj2J8zgLNXnbVtRJPtU13x+masft9Ta1tpw3dFRdtHcA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/matteobruni"
|
||||
},
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/tsparticles"
|
||||
},
|
||||
{
|
||||
"type": "buymeacoffee",
|
||||
"url": "https://www.buymeacoffee.com/matteobruni"
|
||||
}
|
||||
],
|
||||
"dependencies": {
|
||||
"@tsparticles/engine": "^3.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tsparticles/shape-circle": {
|
||||
"version": "3.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@tsparticles/shape-circle/-/shape-circle-3.5.0.tgz",
|
||||
"integrity": "sha512-59TmXkeeI6Jzv5vt/D3TkclglabaoEXQi2kbDjSCBK68SXRHzlQu29mSAL41Y5S0Ft5ZJKkAQHX1IqEnm8Hyjg==",
|
||||
"dependencies": {
|
||||
"@tsparticles/engine": "^3.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tsparticles/shape-emoji": {
|
||||
"version": "3.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@tsparticles/shape-emoji/-/shape-emoji-3.5.0.tgz",
|
||||
"integrity": "sha512-cxWHxQxnG5vLDltkoxdo7KS87uKPwQgf4SDWy/WCxW4Psm1TEeeSGYMJPVed+wWPspOKmLb7u8OaEexgE2pHHQ==",
|
||||
"dependencies": {
|
||||
"@tsparticles/engine": "^3.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tsparticles/shape-heart": {
|
||||
"version": "3.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@tsparticles/shape-heart/-/shape-heart-3.5.0.tgz",
|
||||
"integrity": "sha512-MvOxW6X7w1jHH+KRJShvHMDhRZ+bpei2mAqQOFR5HY+2D6KFzaDVgtfGFwoiaX8Pm6oP6OQssQ3QnDtrywLRFw==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/matteobruni"
|
||||
},
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/tsparticles"
|
||||
},
|
||||
{
|
||||
"type": "buymeacoffee",
|
||||
"url": "https://www.buymeacoffee.com/matteobruni"
|
||||
}
|
||||
],
|
||||
"dependencies": {
|
||||
"@tsparticles/engine": "^3.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tsparticles/shape-image": {
|
||||
"version": "3.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@tsparticles/shape-image/-/shape-image-3.5.0.tgz",
|
||||
"integrity": "sha512-lWYg7DTv74dSOnXy+4dr7t1/OSuUmxDpIo12Lbxgx/QBN7A5I/HoqbKcs13TSA0RS1hcuMgtti07BcDTEYW3Dw==",
|
||||
"dependencies": {
|
||||
"@tsparticles/engine": "^3.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tsparticles/shape-polygon": {
|
||||
"version": "3.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@tsparticles/shape-polygon/-/shape-polygon-3.5.0.tgz",
|
||||
"integrity": "sha512-sqYL+YXpnq3nSWcOEGZaJ4Z7Cb7x8M0iORSLpPdNEIvwDKdPczYyQM95D8ep19Pv1CV5L0uRthV36wg7UpnJ9Q==",
|
||||
"dependencies": {
|
||||
"@tsparticles/engine": "^3.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tsparticles/shape-square": {
|
||||
"version": "3.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@tsparticles/shape-square/-/shape-square-3.5.0.tgz",
|
||||
"integrity": "sha512-rPHpA4Pzm1W5DIIow+lQS+VS7D2thSBQQbV9eHxb933Wh0/QC3me3w4vovuq7hdtVANhsUVO04n44Gc/2TgHkw==",
|
||||
"dependencies": {
|
||||
"@tsparticles/engine": "^3.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tsparticles/shape-star": {
|
||||
"version": "3.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@tsparticles/shape-star/-/shape-star-3.5.0.tgz",
|
||||
"integrity": "sha512-EDEJc4MYv3UbOeA3wrZjuJVtZ08PdCzzBij3T/7Tp3HUCf/p9XnfHBd/CPR5Mo6X0xpGfrein8UQN9CjGLHwUA==",
|
||||
"dependencies": {
|
||||
"@tsparticles/engine": "^3.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tsparticles/updater-color": {
|
||||
"version": "3.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@tsparticles/updater-color/-/updater-color-3.5.0.tgz",
|
||||
"integrity": "sha512-TGGgiLixIg37sst2Fj9IV4XbdMwkT6PYanM7qEqyfmv4hJ/RHMQlCznEe6b7OhChQVBg5ov5EMl/BT4/fIWEYw==",
|
||||
"dependencies": {
|
||||
"@tsparticles/engine": "^3.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tsparticles/updater-life": {
|
||||
"version": "3.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@tsparticles/updater-life/-/updater-life-3.5.0.tgz",
|
||||
"integrity": "sha512-jlMEq16dwN+rZmW/UmLdqaCe4W0NFrVdmXkZV8QWYgu06a+Ucslz337nHYaP89/9rZWpNua/uq1JDjDzaVD5Jg==",
|
||||
"dependencies": {
|
||||
"@tsparticles/engine": "^3.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tsparticles/updater-opacity": {
|
||||
"version": "3.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@tsparticles/updater-opacity/-/updater-opacity-3.5.0.tgz",
|
||||
"integrity": "sha512-T2YfqdIFV/f5VOg1JQsXu6/owdi9g9K2wrJlBfgteo+IboVp6Lcuo4PGAfilWVkWrTdp1Nz4mz39NrLHfOce2g==",
|
||||
"dependencies": {
|
||||
"@tsparticles/engine": "^3.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tsparticles/updater-out-modes": {
|
||||
"version": "3.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@tsparticles/updater-out-modes/-/updater-out-modes-3.5.0.tgz",
|
||||
"integrity": "sha512-y6NZe2OSk5SrYdaLwUIQnHICsNEDIdPPJHQ2nAWSvAuPJphlSKjUknc7OaGiFwle6l0OkhWoZZe1rV1ktbw/lA==",
|
||||
"dependencies": {
|
||||
"@tsparticles/engine": "^3.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tsparticles/updater-roll": {
|
||||
"version": "3.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@tsparticles/updater-roll/-/updater-roll-3.5.0.tgz",
|
||||
"integrity": "sha512-K3NfBGqVIu2zyJv72oNPlYLMDQKmUXTaCvnxUjzBEJJCYRdx7KhZPQVjAsfVYLHd7m7D7/+wKlkXmdYYAd67bg==",
|
||||
"dependencies": {
|
||||
"@tsparticles/engine": "^3.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tsparticles/updater-rotate": {
|
||||
"version": "3.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@tsparticles/updater-rotate/-/updater-rotate-3.5.0.tgz",
|
||||
"integrity": "sha512-j4qPHQd1eUmDoGnIJOsVswHLqtTof1je+b2GTOLB3WIoKmlyUpzQYjVc7PNfLMuCEUubwpZCfcd/vC80VZeWkg==",
|
||||
"dependencies": {
|
||||
"@tsparticles/engine": "^3.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tsparticles/updater-size": {
|
||||
"version": "3.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@tsparticles/updater-size/-/updater-size-3.5.0.tgz",
|
||||
"integrity": "sha512-TnWlOChBsVZffT2uO0S4ALGSzxT6UAMIVlhGMGFgSeIlktKMqM+dxDGAPrYa1n8IS2dkVGisiXzsV0Ss6Ceu1A==",
|
||||
"dependencies": {
|
||||
"@tsparticles/engine": "^3.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tsparticles/updater-tilt": {
|
||||
"version": "3.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@tsparticles/updater-tilt/-/updater-tilt-3.5.0.tgz",
|
||||
"integrity": "sha512-ovK6jH4fAmTav1kCC5Z1FW/pPjKxtK+X+w9BZJEddpS5cyBEdWD4FgvNgLnmZYpK0xad/nb+xxqeDkpSu/O51Q==",
|
||||
"dependencies": {
|
||||
"@tsparticles/engine": "^3.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tsparticles/updater-wobble": {
|
||||
"version": "3.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@tsparticles/updater-wobble/-/updater-wobble-3.5.0.tgz",
|
||||
"integrity": "sha512-fpN0XPvAf3dJ5UU++C+ETVDLurpnkzje02w865Ar4ubPBgGpMhowr6AbtFUe37Zl8rFUTYntBOSEoxqNYJAUgQ==",
|
||||
"dependencies": {
|
||||
"@tsparticles/engine": "^3.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/aria-query": {
|
||||
"version": "5.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
|
||||
|
||||
@@ -42,6 +42,7 @@
|
||||
"@trpc/client": "^11.0.0-rc.467",
|
||||
"@trpc/react-query": "^11.0.0-rc.467",
|
||||
"@trpc/server": "^11.0.0-rc.467",
|
||||
"@tsparticles/confetti": "^3.5.0",
|
||||
"@vercel/otel": "^1.9.1",
|
||||
"@vis.gl/react-google-maps": "^1.2.0",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
|
||||
1
public/_static/img/confetti.svg
Normal file
1
public/_static/img/confetti.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 14 KiB |
@@ -18,6 +18,9 @@ export const rewardsCurrentInput = z.object({
|
||||
lang: z.nativeEnum(Lang).optional(),
|
||||
})
|
||||
|
||||
export const rewardsUpdateInput = z.object({
|
||||
id: z.string(),
|
||||
})
|
||||
export const rewardsUpdateInput = z.array(
|
||||
z.object({
|
||||
rewardId: z.string(),
|
||||
couponCode: z.string(),
|
||||
})
|
||||
)
|
||||
|
||||
@@ -4,6 +4,7 @@ import { notFound } from "@/server/errors/trpc"
|
||||
import {
|
||||
contentStackBaseWithProtectedProcedure,
|
||||
contentStackBaseWithServiceProcedure,
|
||||
protectedProcedure,
|
||||
router,
|
||||
} from "@/server/trpc"
|
||||
|
||||
@@ -16,7 +17,6 @@ import {
|
||||
} from "./input"
|
||||
import {
|
||||
Reward,
|
||||
SurpriseReward,
|
||||
validateApiRewardSchema,
|
||||
validateCategorizedRewardsSchema,
|
||||
} from "./output"
|
||||
@@ -34,10 +34,11 @@ import {
|
||||
getCurrentRewardFailCounter,
|
||||
getCurrentRewardSuccessCounter,
|
||||
getUniqueRewardIds,
|
||||
getUnwrapSurpriseCounter,
|
||||
getUnwrapSurpriseFailCounter,
|
||||
getUnwrapSurpriseSuccessCounter,
|
||||
} from "./utils"
|
||||
|
||||
import { Surprise } from "@/types/components/blocks/surprises"
|
||||
|
||||
const ONE_HOUR = 60 * 60
|
||||
|
||||
export const rewardQueryRouter = router({
|
||||
@@ -327,44 +328,100 @@ export const rewardQueryRouter = router({
|
||||
|
||||
getCurrentRewardSuccessCounter.add(1)
|
||||
|
||||
const surprises =
|
||||
validatedApiRewards.data
|
||||
.filter(
|
||||
(reward): reward is SurpriseReward =>
|
||||
reward?.type === "coupon" && reward?.rewardType === "Surprise"
|
||||
const surprises = validatedApiRewards.data
|
||||
// TODO: Add predicates once legacy endpoints are removed
|
||||
.filter((reward) => {
|
||||
if (reward?.rewardType !== "Surprise") {
|
||||
return false
|
||||
}
|
||||
|
||||
if (!("coupon" in reward)) {
|
||||
return false
|
||||
}
|
||||
|
||||
const unwrappedCoupons =
|
||||
reward.coupon?.filter((coupon) => !coupon.unwrapped) || []
|
||||
if (unwrappedCoupons.length === 0) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
.map((surprise) => {
|
||||
const reward = cmsRewards.find(
|
||||
({ reward_id }) => surprise.rewardId === reward_id
|
||||
)
|
||||
.map((surprise) => {
|
||||
const reward = cmsRewards.find(
|
||||
({ reward_id }) => surprise.rewardId === reward_id
|
||||
)
|
||||
|
||||
if (!reward) {
|
||||
return null
|
||||
}
|
||||
if (!reward) {
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
...reward,
|
||||
id: surprise.id,
|
||||
endsAt: surprise.endsAt,
|
||||
}
|
||||
})
|
||||
.filter((surprise): surprise is Surprise => !!surprise) ?? []
|
||||
return {
|
||||
...reward,
|
||||
id: surprise.id,
|
||||
coupons: "coupon" in surprise ? surprise.coupon || [] : [],
|
||||
}
|
||||
})
|
||||
.flatMap((surprises) => (surprises ? [surprises] : []))
|
||||
|
||||
return surprises
|
||||
}),
|
||||
update: contentStackBaseWithProtectedProcedure
|
||||
unwrap: protectedProcedure
|
||||
.input(rewardsUpdateInput)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const response = await Promise.resolve({ ok: true })
|
||||
// const response = await api.post(api.endpoints.v1.rewards, {
|
||||
// body: {
|
||||
// ids: [input.id],
|
||||
// },
|
||||
// })
|
||||
if (!response.ok) {
|
||||
return false
|
||||
getUnwrapSurpriseCounter.add(1)
|
||||
|
||||
const promises = input.map(({ rewardId, couponCode }) => {
|
||||
return api.post(api.endpoints.v1.Profile.Reward.unwrap, {
|
||||
body: {
|
||||
rewardId,
|
||||
couponCode,
|
||||
},
|
||||
headers: {
|
||||
Authorization: `Bearer ${ctx.session.token.access_token}`,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
const responses = await Promise.all(promises)
|
||||
|
||||
const errors = await Promise.all(
|
||||
responses.map(async (apiResponse) => {
|
||||
if (!apiResponse.ok) {
|
||||
const text = await apiResponse.text()
|
||||
|
||||
getUnwrapSurpriseFailCounter.add(1, {
|
||||
error_type: "http_error",
|
||||
error: JSON.stringify({
|
||||
status: apiResponse.status,
|
||||
statusText: apiResponse.statusText,
|
||||
text,
|
||||
}),
|
||||
})
|
||||
|
||||
console.error(
|
||||
"contentstack.unwrap API error",
|
||||
JSON.stringify({
|
||||
error: {
|
||||
status: apiResponse.status,
|
||||
statusText: apiResponse.statusText,
|
||||
text,
|
||||
},
|
||||
query: {},
|
||||
})
|
||||
)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
)
|
||||
|
||||
if (errors.filter((ok) => !ok).length > 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
getUnwrapSurpriseSuccessCounter.add(1)
|
||||
|
||||
return true
|
||||
}),
|
||||
})
|
||||
|
||||
@@ -44,6 +44,15 @@ export const getByLevelRewardFailCounter = meter.createCounter(
|
||||
export const getByLevelRewardSuccessCounter = meter.createCounter(
|
||||
"trpc.contentstack.reward.byLevel-success"
|
||||
)
|
||||
export const getUnwrapSurpriseCounter = meter.createCounter(
|
||||
"trpc.contentstack.reward.unwrap"
|
||||
)
|
||||
export const getUnwrapSurpriseFailCounter = meter.createCounter(
|
||||
"trpc.contentstack.reward.unwrap-fail"
|
||||
)
|
||||
export const getUnwrapSurpriseSuccessCounter = meter.createCounter(
|
||||
"trpc.contentstack.reward.unwrap-success"
|
||||
)
|
||||
|
||||
const ONE_HOUR = 60 * 60
|
||||
|
||||
|
||||
@@ -74,3 +74,6 @@ export const getRoomPackagesInputSchema = z.object({
|
||||
children: z.number().optional().default(0),
|
||||
packageCodes: z.array(z.string()).optional().default([]),
|
||||
})
|
||||
export const getCityCoordinatesInputSchema = z.object({
|
||||
city: z.string(),
|
||||
})
|
||||
|
||||
@@ -200,14 +200,14 @@ const rewardNightSchema = z.object({
|
||||
|
||||
export const pointOfInterestSchema = z
|
||||
.object({
|
||||
name: z.string(),
|
||||
distance: z.number(),
|
||||
name: z.string().optional(),
|
||||
distance: z.number().optional(),
|
||||
category: z.object({
|
||||
name: z.string(),
|
||||
group: z.string(),
|
||||
name: z.string().optional(),
|
||||
group: z.string().optional(),
|
||||
}),
|
||||
location: locationSchema,
|
||||
isHighlighted: z.boolean(),
|
||||
location: locationSchema.optional(),
|
||||
isHighlighted: z.boolean().optional(),
|
||||
})
|
||||
.transform((poi) => ({
|
||||
name: poi.name,
|
||||
@@ -215,8 +215,8 @@ export const pointOfInterestSchema = z
|
||||
categoryName: poi.category.name,
|
||||
group: getPoiGroupByCategoryName(poi.category.name),
|
||||
coordinates: {
|
||||
lat: poi.location.latitude,
|
||||
lng: poi.location.longitude,
|
||||
lat: poi.location?.latitude ?? 0,
|
||||
lng: poi.location?.longitude ?? 0,
|
||||
},
|
||||
}))
|
||||
|
||||
@@ -463,7 +463,9 @@ export const getHotelDataSchema = z.object({
|
||||
parking: z.array(parkingSchema),
|
||||
pointsOfInterest: z
|
||||
.array(pointOfInterestSchema)
|
||||
.transform((pois) => pois.sort((a, b) => a.distance - b.distance)),
|
||||
.transform((pois) =>
|
||||
pois.sort((a, b) => (a.distance ?? 0) - (b.distance ?? 0))
|
||||
),
|
||||
ratings: ratingsSchema,
|
||||
rewardNight: rewardNightSchema,
|
||||
restaurantImages: facilitySchema.optional(),
|
||||
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
import { getVerifiedUser, parsedUser } from "../user/query"
|
||||
import {
|
||||
getBreakfastPackageInputSchema,
|
||||
getCityCoordinatesInputSchema,
|
||||
getHotelDataInputSchema,
|
||||
getHotelsAvailabilityInputSchema,
|
||||
getRatesInputSchema,
|
||||
@@ -342,7 +343,7 @@ export const hotelQueryRouter = router({
|
||||
return {
|
||||
hotelId,
|
||||
hotelName: hotelAttributes.name,
|
||||
hotelDescription: hotelAttributes.hotelContent.texts.descriptions.short,
|
||||
hotelDescriptions: hotelAttributes.hotelContent.texts,
|
||||
hotelLocation: hotelAttributes.location,
|
||||
hotelAddress: hotelAttributes.address,
|
||||
hotelRatings: hotelAttributes.ratings,
|
||||
@@ -355,6 +356,9 @@ export const hotelQueryRouter = router({
|
||||
alerts: hotelAlerts,
|
||||
faq: contentstackData?.faq,
|
||||
healthFacilities: hotelAttributes.healthFacilities,
|
||||
contact: hotelAttributes.contactInformation,
|
||||
socials: hotelAttributes.socialMedia,
|
||||
ecoLabels: hotelAttributes.hotelFacts.ecoLabels,
|
||||
}
|
||||
}),
|
||||
availability: router({
|
||||
@@ -1078,4 +1082,19 @@ export const hotelQueryRouter = router({
|
||||
)
|
||||
}),
|
||||
}),
|
||||
map: router({
|
||||
city: serviceProcedure
|
||||
.input(getCityCoordinatesInputSchema)
|
||||
.query(async function ({ input }) {
|
||||
const apiKey = process.env.GOOGLE_STATIC_MAP_KEY
|
||||
const { city } = input
|
||||
const url = `https://maps.googleapis.com/maps/api/geocode/json?address=${encodeURIComponent(city)}&key=${apiKey}`
|
||||
|
||||
const response = await fetch(url)
|
||||
const data = await response.json()
|
||||
const { lat, lng } = data.results[0].geometry.location
|
||||
|
||||
return { lat, lng }
|
||||
}),
|
||||
}),
|
||||
})
|
||||
|
||||
@@ -12,13 +12,14 @@ import {
|
||||
type Countries,
|
||||
} from "./output"
|
||||
|
||||
import type { Lang } from "@/constants/languages"
|
||||
import type { Endpoint } from "@/lib/api/endpoints"
|
||||
import type { RequestOptionsWithOutBody } from "@/types/fetch"
|
||||
import { PointOfInterestGroupEnum } from "@/types/hotel"
|
||||
import { HotelLocation } from "@/types/trpc/routers/hotel/locations"
|
||||
import type { Lang } from "@/constants/languages"
|
||||
import type { Endpoint } from "@/lib/api/endpoints"
|
||||
|
||||
export function getPoiGroupByCategoryName(category: string) {
|
||||
export function getPoiGroupByCategoryName(category: string | undefined) {
|
||||
if (!category) return PointOfInterestGroupEnum.LOCATION
|
||||
switch (category) {
|
||||
case "Airport":
|
||||
case "Bus terminal":
|
||||
|
||||
@@ -101,13 +101,6 @@ export function createDetailsStore(
|
||||
})
|
||||
)
|
||||
},
|
||||
updateValidity(property, isValid) {
|
||||
return set(
|
||||
produce((state: DetailsState) => {
|
||||
state.isValid[property] = isValid
|
||||
})
|
||||
)
|
||||
},
|
||||
},
|
||||
|
||||
data: merge(
|
||||
|
||||
@@ -1,14 +1,35 @@
|
||||
import {
|
||||
Reward,
|
||||
SurpriseReward,
|
||||
} from "@/server/routers/contentstack/reward/output"
|
||||
import { Reward } from "@/server/routers/contentstack/reward/output"
|
||||
|
||||
export interface Surprise extends Reward {
|
||||
endsAt: SurpriseReward["endsAt"]
|
||||
id: SurpriseReward["id"]
|
||||
coupons: { couponCode?: string; expiresAt?: string }[]
|
||||
id?: string
|
||||
}
|
||||
|
||||
export interface SurprisesProps {
|
||||
surprises: Surprise[]
|
||||
membershipNumber?: string
|
||||
}
|
||||
|
||||
export interface NavigationProps {
|
||||
selectedSurprise: number
|
||||
totalSurprises: number
|
||||
showSurprise: (direction: number) => void
|
||||
}
|
||||
|
||||
export interface CardProps extends React.PropsWithChildren {
|
||||
title?: string
|
||||
}
|
||||
|
||||
export interface InitialProps {
|
||||
totalSurprises: number
|
||||
onOpen: VoidFunction
|
||||
}
|
||||
|
||||
export interface SlideProps {
|
||||
surprise: Surprise
|
||||
membershipNumber?: string
|
||||
}
|
||||
|
||||
export interface HeaderProps extends React.PropsWithChildren {
|
||||
onClose: VoidFunction
|
||||
}
|
||||
|
||||
10
types/components/hotelPage/sidepeek/aboutTheHotel.ts
Normal file
10
types/components/hotelPage/sidepeek/aboutTheHotel.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import type { Hotel, HotelAddress, HotelLocation } from "@/types/hotel"
|
||||
|
||||
export type AboutTheHotelSidePeekProps = {
|
||||
hotelAddress: HotelAddress
|
||||
coordinates: HotelLocation
|
||||
contact: Hotel["contactInformation"]
|
||||
socials: Hotel["socialMedia"]
|
||||
ecoLabels: Hotel["hotelFacts"]["ecoLabels"]
|
||||
descriptions: Hotel["hotelContent"]["texts"]
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import type { AboutTheHotelSidePeekProps } from "./aboutTheHotel"
|
||||
|
||||
export type ContactInformationProps = Omit<
|
||||
AboutTheHotelSidePeekProps,
|
||||
"descriptions"
|
||||
>
|
||||
@@ -22,6 +22,7 @@ export interface SelectHotelMapProps {
|
||||
mapId: string
|
||||
hotels: HotelData[]
|
||||
filterList: CategorizedFilters
|
||||
cityCoordinates: Coordinates
|
||||
}
|
||||
|
||||
type ImageSizes = z.infer<typeof imageSizesSchema>
|
||||
|
||||
@@ -12,7 +12,6 @@ export interface DetailsState {
|
||||
updateBedType: (data: BedTypeSchema) => void
|
||||
updateBreakfast: (data: BreakfastPackage | false) => void
|
||||
updateDetails: (data: DetailsSchema) => void
|
||||
updateValidity: (property: StepEnum, isValid: boolean) => void
|
||||
}
|
||||
data: DetailsSchema & {
|
||||
bedType: BedTypeSchema | undefined
|
||||
|
||||
Reference in New Issue
Block a user