diff --git a/apps/scandic-web/app/[lang]/(live)/(public)/[contentType]/[uid]/page.tsx b/apps/scandic-web/app/[lang]/(live)/(public)/[contentType]/[uid]/page.tsx index 18cf5ddb9..9a6ab5569 100644 --- a/apps/scandic-web/app/[lang]/(live)/(public)/[contentType]/[uid]/page.tsx +++ b/apps/scandic-web/app/[lang]/(live)/(public)/[contentType]/[uid]/page.tsx @@ -64,8 +64,7 @@ export default async function ContentTypePage({ case PageContentTypeEnum.destinationCountryPage: return case PageContentTypeEnum.destinationCityPage: - const filterFromUrl = searchParams.filterFromUrl - return + return case PageContentTypeEnum.hotelPage: if (env.HIDE_FOR_NEXT_RELEASE) { return notFound() diff --git a/apps/scandic-web/components/ContentType/DestinationPage/CityDataContainer/index.tsx b/apps/scandic-web/components/ContentType/DestinationPage/CityDataContainer/index.tsx new file mode 100644 index 000000000..df8c7ac82 --- /dev/null +++ b/apps/scandic-web/components/ContentType/DestinationPage/CityDataContainer/index.tsx @@ -0,0 +1,50 @@ +import { + getDestinationCityPagesByCountry, + getHotelsByCountry, +} from "@/lib/trpc/memoizedRequests" + +import { getIntl } from "@/i18n" +import DestinationDataProvider from "@/providers/DestinationDataProvider" + +import type { SortItem } from "@/types/components/destinationFilterAndSort" +import type { Country } from "@/types/enums/country" +import { SortOption } from "@/types/enums/destinationFilterAndSort" + +interface CityDataContainerProps extends React.PropsWithChildren { + country: Country +} + +export function preload(country: Country) { + void getHotelsByCountry(country) + void getDestinationCityPagesByCountry(country) +} + +export default async function CityDataContainer({ + country, + children, +}: CityDataContainerProps) { + const intl = await getIntl() + const [hotels, cities] = await Promise.all([ + getHotelsByCountry(country), + getDestinationCityPagesByCountry(country), + ]) + + const sortItems: SortItem[] = [ + { + label: intl.formatMessage({ id: "Recommended" }), + value: SortOption.Recommended, + isDefault: true, + }, + { label: intl.formatMessage({ id: "Name" }), value: SortOption.Name }, + ] + + return ( + + {children} + + ) +} diff --git a/apps/scandic-web/components/ContentType/DestinationPage/CityListing/CityListingItem/CityListingItemSkeleton.tsx b/apps/scandic-web/components/ContentType/DestinationPage/CityListing/CityListingItem/CityListingItemSkeleton.tsx index fdc4bee8a..8669fa86d 100644 --- a/apps/scandic-web/components/ContentType/DestinationPage/CityListing/CityListingItem/CityListingItemSkeleton.tsx +++ b/apps/scandic-web/components/ContentType/DestinationPage/CityListing/CityListingItem/CityListingItemSkeleton.tsx @@ -1,3 +1,5 @@ +"use client" + import SkeletonShimmer from "@/components/SkeletonShimmer" import Divider from "@/components/TempDesignSystem/Divider" @@ -5,7 +7,7 @@ import ExperienceListSkeleton from "../../ExperienceList/ExperienceListSkeleton" import styles from "./cityListingItem.module.css" -export default async function CityListingItemSkeleton() { +export default function CityListingItemSkeleton() { return (
diff --git a/apps/scandic-web/components/ContentType/DestinationPage/CityListing/CityListingItem/index.tsx b/apps/scandic-web/components/ContentType/DestinationPage/CityListing/CityListingItem/index.tsx index 346fc9cb1..aa6b3634b 100644 --- a/apps/scandic-web/components/ContentType/DestinationPage/CityListing/CityListingItem/index.tsx +++ b/apps/scandic-web/components/ContentType/DestinationPage/CityListing/CityListingItem/index.tsx @@ -1,11 +1,13 @@ +"use client" + import Link from "next/link" +import { useIntl } from "react-intl" import ImageGallery from "@/components/ImageGallery" import Button from "@/components/TempDesignSystem/Button" import Divider from "@/components/TempDesignSystem/Divider" import Body from "@/components/TempDesignSystem/Text/Body" import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" -import { getIntl } from "@/i18n" import { mapImageVaultImagesToGalleryImages } from "@/utils/imageGallery" import ExperienceList from "../../ExperienceList" @@ -18,8 +20,8 @@ interface CityListingItemProps { city: DestinationCityListItem } -export default async function CityListingItem({ city }: CityListingItemProps) { - const intl = await getIntl() +export default function CityListingItem({ city }: CityListingItemProps) { + const intl = useIntl() const galleryImages = mapImageVaultImagesToGalleryImages(city.images) return ( diff --git a/apps/scandic-web/components/ContentType/DestinationPage/CityListing/CityListingSkeleton.tsx b/apps/scandic-web/components/ContentType/DestinationPage/CityListing/CityListingSkeleton.tsx index c1aa68a69..dc451c658 100644 --- a/apps/scandic-web/components/ContentType/DestinationPage/CityListing/CityListingSkeleton.tsx +++ b/apps/scandic-web/components/ContentType/DestinationPage/CityListing/CityListingSkeleton.tsx @@ -1,14 +1,17 @@ +"use client" + import SkeletonShimmer from "@/components/SkeletonShimmer" import CityListingItemSkeleton from "./CityListingItem/CityListingItemSkeleton" import styles from "./cityListing.module.css" -export default async function CityListingSkeleton() { +export default function CityListingSkeleton() { return (
-
- +
+ +
    {Array.from({ length: 3 }).map((_, index) => ( diff --git a/apps/scandic-web/components/ContentType/DestinationPage/CityListing/cityListing.module.css b/apps/scandic-web/components/ContentType/DestinationPage/CityListing/cityListing.module.css index 4bde934e7..1f4318bb4 100644 --- a/apps/scandic-web/components/ContentType/DestinationPage/CityListing/cityListing.module.css +++ b/apps/scandic-web/components/ContentType/DestinationPage/CityListing/cityListing.module.css @@ -1,6 +1,15 @@ .container { + --scroll-margin-top: calc( + var(--booking-widget-mobile-height) + var(--Spacing-x2) + ); display: grid; gap: var(--Spacing-x2); + scroll-margin-top: var(--scroll-margin-top); +} + +.listHeader { + display: flex; + justify-content: space-between; } .cityList { @@ -8,3 +17,15 @@ display: grid; gap: var(--Spacing-x2); } + +.cityList:not(.allVisible) li:nth-child(n + 6) { + display: none; +} + +@media screen and (min-width: 768px) { + .container { + --scroll-margin-top: calc( + var(--booking-widget-desktop-height) + var(--Spacing-x2) + ); + } +} diff --git a/apps/scandic-web/components/ContentType/DestinationPage/CityListing/index.tsx b/apps/scandic-web/components/ContentType/DestinationPage/CityListing/index.tsx index 9ad557f8f..d36aca2d3 100644 --- a/apps/scandic-web/components/ContentType/DestinationPage/CityListing/index.tsx +++ b/apps/scandic-web/components/ContentType/DestinationPage/CityListing/index.tsx @@ -1,45 +1,83 @@ -import { getDestinationCityPagesByCountry } from "@/lib/trpc/memoizedRequests" +"use client" +import { useRef, useState } from "react" +import { useIntl } from "react-intl" + +import { useDestinationDataStore } from "@/stores/destination-data" + +import DestinationFilterAndSort from "@/components/DestinationFilterAndSort" +import { BackToTopButton } from "@/components/TempDesignSystem/BackToTopButton" +import ShowMoreButton from "@/components/TempDesignSystem/ShowMoreButton" import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" -import { getIntl } from "@/i18n" +import { useScrollToTop } from "@/hooks/useScrollToTop" import CityListingItem from "./CityListingItem" +import CityListingSkeleton from "./CityListingSkeleton" import styles from "./cityListing.module.css" -import type { Country } from "@/types/enums/country" +export default function CityListing() { + const intl = useIntl() + const scrollRef = useRef(null) + const { showBackToTop, scrollToTop } = useScrollToTop({ + threshold: 300, + elementRef: scrollRef, + }) + const { activeCities, filters, sortItems, isLoading } = + useDestinationDataStore((state) => ({ + activeCities: state.activeCities, + filters: state.allFilters, + sortItems: state.sortItems, + isLoading: state.isLoading, + })) + const [allCitiesVisible, setAllCitiesVisible] = useState( + activeCities.length <= 5 + ) -interface CityListingProps { - country: Country -} - -export default async function CityListing({ country }: CityListingProps) { - const intl = await getIntl() - const cities = await getDestinationCityPagesByCountry(country) - - if (!cities.length) { - return null + function handleShowMore() { + if (scrollRef.current && allCitiesVisible) { + scrollRef.current.scrollIntoView({ behavior: "smooth" }) + } + setAllCitiesVisible((state) => !state) } - return ( -
    -
    + return isLoading ? ( + + ) : ( +
    +
    {intl.formatMessage( { id: `{count, plural, one {{count} Location} other {{count} Locations}}`, }, - { count: cities.length } + { count: activeCities.length } )} +
    -
      - {cities.map((city) => ( +
        + {activeCities.map((city) => (
      • ))}
      + {activeCities.length > 5 ? ( + + ) : null} + {showBackToTop && ( + + )}
    ) } diff --git a/apps/scandic-web/components/ContentType/DestinationPage/DestinationCityPage/CityMap/HotelList/HotelListSkeleton.tsx b/apps/scandic-web/components/ContentType/DestinationPage/DestinationCityPage/CityMap/HotelList/HotelListSkeleton.tsx new file mode 100644 index 000000000..76e45b8e3 --- /dev/null +++ b/apps/scandic-web/components/ContentType/DestinationPage/DestinationCityPage/CityMap/HotelList/HotelListSkeleton.tsx @@ -0,0 +1,23 @@ +"use client" + +import SkeletonShimmer from "@/components/SkeletonShimmer" + +import HotelListItemSkeleton from "../HotelListItem/HotelListItemSkeleton" + +import styles from "./hotelList.module.css" + +export default function HotelListSkeleton() { + return ( +
    +
    + + +
    +
      + {Array.from({ length: 3 }).map((_, index) => ( + + ))} +
    +
    + ) +} diff --git a/apps/scandic-web/components/ContentType/DestinationPage/DestinationCityPage/CityMap/HotelList/index.tsx b/apps/scandic-web/components/ContentType/DestinationPage/DestinationCityPage/CityMap/HotelList/index.tsx index 61af72a69..b6d3e3c7e 100644 --- a/apps/scandic-web/components/ContentType/DestinationPage/DestinationCityPage/CityMap/HotelList/index.tsx +++ b/apps/scandic-web/components/ContentType/DestinationPage/DestinationCityPage/CityMap/HotelList/index.tsx @@ -4,13 +4,14 @@ import { useMap, useMapsLibrary } from "@vis.gl/react-google-maps" import { useEffect, useMemo, useState } from "react" import { useIntl } from "react-intl" -import { useHotelDataStore } from "@/stores/hotel-data" +import { useDestinationDataStore } from "@/stores/destination-data" -import HotelFilterAndSort from "@/components/HotelFilterAndSort" +import DestinationFilterAndSort from "@/components/DestinationFilterAndSort" import Body from "@/components/TempDesignSystem/Text/Body" import { debounce } from "@/utils/debounce" import HotelListItem from "../HotelListItem" +import HotelListSkeleton from "./HotelListSkeleton" import { getVisibleHotels } from "./utils" import styles from "./hotelList.module.css" @@ -22,11 +23,13 @@ export default function HotelList() { const map = useMap() const coreLib = useMapsLibrary("core") const [visibleHotels, setVisibleHotels] = useState([]) - const { filters, sortItems, activeHotels } = useHotelDataStore((state) => ({ - filters: state.allFilters, - sortItems: state.sortItems, - activeHotels: state.activeHotels, - })) + const { filters, sortItems, activeHotels, isLoading } = + useDestinationDataStore((state) => ({ + filters: state.allFilters, + sortItems: state.sortItems, + activeHotels: state.activeHotels, + isLoading: state.isLoading, + })) const debouncedUpdateVisibleHotels = useMemo( () => @@ -51,7 +54,9 @@ export default function HotelList() { } }, [map, coreLib, debouncedUpdateVisibleHotels]) - return ( + return isLoading ? ( + + ) : (
    @@ -60,7 +65,11 @@ export default function HotelList() { { count: visibleHotels.length } )} - +
      {visibleHotels.map(({ hotel, url }) => ( diff --git a/apps/scandic-web/components/ContentType/DestinationPage/DestinationCityPage/CityMap/HotelListItem/HotelListItemSkeleton.tsx b/apps/scandic-web/components/ContentType/DestinationPage/DestinationCityPage/CityMap/HotelListItem/HotelListItemSkeleton.tsx new file mode 100644 index 000000000..963eed7a0 --- /dev/null +++ b/apps/scandic-web/components/ContentType/DestinationPage/DestinationCityPage/CityMap/HotelListItem/HotelListItemSkeleton.tsx @@ -0,0 +1,37 @@ +"use client" + +import SkeletonShimmer from "@/components/SkeletonShimmer" + +import styles from "./hotelListItem.module.css" + +export default function HotelListItemSkeleton() { + return ( +
      +
      + +
      +
      +
      +
      + +
      + +
      + + +
      +
      +
        + {Array.from({ length: 5 }).map((_, index) => ( +
      • + +
      • + ))} +
      +
      + +
      +
      +
      + ) +} diff --git a/apps/scandic-web/components/ContentType/DestinationPage/DestinationCityPage/CityMap/HotelListItem/hotelListItem.module.css b/apps/scandic-web/components/ContentType/DestinationPage/DestinationCityPage/CityMap/HotelListItem/hotelListItem.module.css index 3611025f9..2ab13683b 100644 --- a/apps/scandic-web/components/ContentType/DestinationPage/DestinationCityPage/CityMap/HotelListItem/hotelListItem.module.css +++ b/apps/scandic-web/components/ContentType/DestinationPage/DestinationCityPage/CityMap/HotelListItem/hotelListItem.module.css @@ -11,8 +11,25 @@ flex-direction: column; gap: var(--Spacing-x2); padding: var(--Spacing-x2) var(--Spacing-x3); - align-content: start; - justify-items: start; + align-content: flex-start; + justify-items: flex-start; +} + +.imageWrapper { + position: relative; + height: 200px; +} + +.tripAdvisor { + position: absolute; + top: 16px; + left: 16px; + display: flex; + align-items: center; + gap: var(--Spacing-x-half); + background-color: var(--Base-Surface-Primary-light-Normal); + padding: var(--Spacing-x-quarter) var(--Spacing-x1); + border-radius: var(--Corner-radius-Small); } .intro { @@ -48,7 +65,11 @@ .hotelListItem { width: 360px; min-height: 150px; - grid-template-columns: 1fr 2fr; + grid-template-columns: 160px 1fr; + } + + .imageWrapper { + height: 100%; } .content { diff --git a/apps/scandic-web/components/ContentType/DestinationPage/DestinationCityPage/CityMap/HotelListItem/index.tsx b/apps/scandic-web/components/ContentType/DestinationPage/DestinationCityPage/CityMap/HotelListItem/index.tsx index 0c58488f5..7b1690a3e 100644 --- a/apps/scandic-web/components/ContentType/DestinationPage/DestinationCityPage/CityMap/HotelListItem/index.tsx +++ b/apps/scandic-web/components/ContentType/DestinationPage/DestinationCityPage/CityMap/HotelListItem/index.tsx @@ -4,6 +4,7 @@ import Link from "next/link" import { useIntl } from "react-intl" import { mapFacilityToIcon } from "@/components/ContentType/HotelPage/data" +import { TripAdvisorIcon } from "@/components/Icons" import HotelLogo from "@/components/Icons/Logos" import ImageGallery from "@/components/ImageGallery" import Button from "@/components/TempDesignSystem/Button" @@ -29,13 +30,25 @@ export default function HotelListItem({ hotel, url }: HotelListItemProps) { return (
      - + + {hotel.ratings?.tripAdvisor.rating && ( +
      + + + {hotel.ratings.tripAdvisor.rating} + +
      )} - /> +
    diff --git a/apps/scandic-web/components/ContentType/DestinationPage/DestinationCityPage/CityMap/index.tsx b/apps/scandic-web/components/ContentType/DestinationPage/DestinationCityPage/CityMap/index.tsx index 504d58d87..a2fe2ab52 100644 --- a/apps/scandic-web/components/ContentType/DestinationPage/DestinationCityPage/CityMap/index.tsx +++ b/apps/scandic-web/components/ContentType/DestinationPage/DestinationCityPage/CityMap/index.tsx @@ -1,12 +1,12 @@ "use client" import { useIntl } from "react-intl" -import { useHotelDataStore } from "@/stores/hotel-data" +import { useDestinationDataStore } from "@/stores/destination-data" import Title from "@/components/TempDesignSystem/Text/Title" import Map from "../../Map" -import { getCityHeadingText } from "../../utils" +import { getHeadingText } from "../../utils" import HotelList from "./HotelList" import styles from "./cityMap.module.css" @@ -21,7 +21,7 @@ interface CityMapProps { export default function CityMap({ mapId, apiKey, city }: CityMapProps) { const intl = useIntl() - const { activeHotels, allFilters, activeFilters } = useHotelDataStore( + const { activeHotels, allFilters, activeFilters } = useDestinationDataStore( (state) => ({ activeHotels: state.activeHotels, allFilters: state.allFilters, @@ -37,7 +37,7 @@ export default function CityMap({ mapId, apiKey, city }: CityMapProps) { textTransform="regular" className={styles.title} > - {getCityHeadingText(intl, city.name, allFilters, activeFilters[0])} + {getHeadingText(intl, city.name, allFilters, activeFilters[0])} diff --git a/apps/scandic-web/components/ContentType/DestinationPage/DestinationCityPage/index.tsx b/apps/scandic-web/components/ContentType/DestinationPage/DestinationCityPage/index.tsx index 4e3adc600..1e4f1a5c6 100644 --- a/apps/scandic-web/components/ContentType/DestinationPage/DestinationCityPage/index.tsx +++ b/apps/scandic-web/components/ContentType/DestinationPage/DestinationCityPage/index.tsx @@ -23,13 +23,7 @@ import styles from "./destinationCityPage.module.css" import { PageContentTypeEnum } from "@/types/requests/contentType" -interface DestinationCityPageProps { - filterFromUrl: string | undefined -} - -export default async function DestinationCityPage({ - filterFromUrl, -}: DestinationCityPageProps) { +export default async function DestinationCityPage() { const pageData = await getDestinationCityPage() if (!pageData) { @@ -53,10 +47,7 @@ export default async function DestinationCityPage({ return ( <> }> - +
    @@ -67,7 +58,7 @@ export default async function DestinationCityPage({ {blocks && }