feat(BOOK-53): Added component for SEO filters and support filter switching

Approved-by: Chuma Mcphoy (We Ahead)
This commit is contained in:
Erik Tiekstra
2025-09-19 08:26:41 +00:00
parent 0e30a2d218
commit 7c92a8fc9a
30 changed files with 339 additions and 111 deletions

View File

@@ -32,8 +32,6 @@ export default function AccordionSection({ accordion, title }: AccordionProps) {
<SectionHeader textTransform="uppercase" title={title} />
<Accordion
className={`${styles.accordion} ${allAccordionsVisible ? styles.allVisible : ""}`}
theme="light"
variant="card"
>
{accordion.map((acc) =>
acc ? (

View File

@@ -14,6 +14,7 @@ import {
} from "@/lib/trpc/memoizedRequests"
import Breadcrumbs from "@/components/Breadcrumbs"
import { SeoFilters } from "@/components/ContentType/DestinationPage/SeoFilters"
import BreadcrumbsSkeleton from "@/components/TempDesignSystem/Breadcrumbs/BreadcrumbsSkeleton"
import { getIntl } from "@/i18n"
import { getLang } from "@/i18n/serverContext"
@@ -59,6 +60,7 @@ export default async function DestinationCityPage({
sidepeek_button_text,
sidepeek_content,
destination_settings,
seo_filters,
} = destinationCityPage
const allHotels = await getHotelsByCityIdentifier(cityIdentifier)
@@ -114,6 +116,7 @@ export default async function DestinationCityPage({
<main className={styles.mainContent}>
<HotelListing />
{blocks && <Blocks blocks={blocks} />}
<SeoFilters seoFilters={seo_filters} location={city.name} />
</main>
<aside className={styles.sidebar}>
<SidebarContentWrapper preamble={preamble} location={city.name}>

View File

@@ -15,6 +15,7 @@ import {
} from "@/lib/trpc/memoizedRequests"
import Breadcrumbs from "@/components/Breadcrumbs"
import { SeoFilters } from "@/components/ContentType/DestinationPage/SeoFilters"
import BreadcrumbsSkeleton from "@/components/TempDesignSystem/Breadcrumbs/BreadcrumbsSkeleton"
import { getIntl } from "@/i18n"
import { getLang } from "@/i18n/serverContext"
@@ -59,6 +60,7 @@ export default async function DestinationCountryPage({
sidepeek_button_text,
sidepeek_content,
destination_settings,
seo_filters,
} = destinationCountryPage
const [allHotels, allCities] = await Promise.all([
@@ -116,6 +118,10 @@ export default async function DestinationCountryPage({
<main className={styles.mainContent}>
<CityListing />
{blocks && <Blocks blocks={blocks} />}
<SeoFilters
seoFilters={seo_filters}
location={translatedCountry}
/>
</main>
<aside className={styles.sidebar}>
<SidebarContentWrapper

View File

@@ -0,0 +1,91 @@
"use client"
import { useIntl } from "react-intl"
import Accordion from "@scandic-hotels/design-system/Accordion"
import AccordionItem from "@scandic-hotels/design-system/Accordion/AccordionItem"
import Link from "@scandic-hotels/design-system/Link"
import { useDestinationDataStore } from "@/stores/destination-data"
import styles from "./seoFilters.module.css"
import type { HotelFilter } from "@scandic-hotels/trpc/types/hotel"
interface SeoFiltersProps {
seoFilters: {
facilityFilters: HotelFilter[]
surroundingsFilters: HotelFilter[]
} | null
location: string
}
export function SeoFilters({ seoFilters, location }: SeoFiltersProps) {
const intl = useIntl()
const { basePath } = useDestinationDataStore((state) => ({
basePath: state.basePathnameWithoutFilters,
}))
if (!seoFilters) {
return null
}
const { facilityFilters, surroundingsFilters } = seoFilters
if (!facilityFilters.length && !surroundingsFilters.length) {
return null
}
return (
<nav className={styles.seoFilters}>
<Accordion className={styles.accordion}>
{facilityFilters.length > 0 ? (
<AccordionItem
title={intl.formatMessage(
{ defaultMessage: "{location} Hotel facilities" },
{ location }
)}
showAsSubtitle
>
<ul className={styles.filterList}>
{facilityFilters.map((filter) => (
<li key={filter.id}>
<Link
href={`${basePath}/${filter.slug}`}
color="Text/Interactive/Secondary"
textDecoration="underline"
>
{filter.name}
</Link>
</li>
))}
</ul>
</AccordionItem>
) : null}
{surroundingsFilters.length > 0 ? (
<AccordionItem
title={intl.formatMessage(
{ defaultMessage: "{location} Hotel surroundings" },
{ location }
)}
showAsSubtitle
>
<ul className={styles.filterList}>
{surroundingsFilters.map((filter) => (
<li key={filter.id}>
<Link
href={`${basePath}/${filter.slug}`}
color="Text/Interactive/Secondary"
textDecoration="underline"
>
{filter.name}
</Link>
</li>
))}
</ul>
</AccordionItem>
) : null}
</Accordion>
</nav>
)
}

View File

@@ -0,0 +1,11 @@
.filterList {
column-count: 2;
list-style-type: none;
gap: var(--Space-x15);
}
@media screen and (min-width: 1367px) {
.filterList {
column-count: 3;
}
}

View File

@@ -37,7 +37,7 @@ export default async function AmenitiesSidePeek({
defaultMessage: "Close",
})}
>
<Accordion>
<Accordion type="sidepeek">
<ParkingAccordionItem
parking={parking.parking}
elevatorPitch={parking.parkingElevatorPitch}

View File

@@ -30,7 +30,7 @@ export default function AccessibilityAccordionItem({
})}
iconName={IconName.Accessibility}
className={styles.accordionItem}
variant="sidepeek"
type="sidepeek"
onOpen={() => trackAccordionClick("amenities:accessibility")}
>
<div className={styles.accessibilityContent}>

View File

@@ -439,12 +439,12 @@ export default function BookedRoomSidePeek({
/>
</div>
{hotelRoom ? (
<Accordion>
<Accordion type="sidepeek">
<AccordionItem
title={intl.formatMessage({
defaultMessage: "Room details",
})}
variant="sidepeek"
type="sidepeek"
>
<RoomDetails
roomDescription={hotelRoom.descriptions.medium}

View File

@@ -37,7 +37,7 @@ export default function DestinationDataProviderContent({
filters.push(...filterParam.split("&"))
}
updateActiveFiltersAndSort(filters, sort)
updateActiveFiltersAndSort(filters, sort, filterFromUrl)
}, [params, updateActiveFiltersAndSort, basePath])
return <>{children}</>

View File

@@ -69,7 +69,7 @@ export function createDestinationDataStore({
return create<DestinationDataState>((set) => ({
actions: {
updateActiveFiltersAndSort(filterSlugs, sort) {
updateActiveFiltersAndSort(filterSlugs, sort, filterSlugFromUrl) {
return set(
produce((state: DestinationDataState) => {
const newSort =
@@ -79,6 +79,10 @@ export function createDestinationDataStore({
const filters = flattenedFilters.filter((filter) =>
filterSlugs.includes(filter.slug)
)
const filterFromUrl =
flattenedFilters.find(
(filter) => filter.slug === filterSlugFromUrl
) ?? null
const filteredHotels = getFilteredHotels(state.allHotels, filters)
const sortedHotels = getSortedHotels(filteredHotels, newSort)
const filteredCities = state.allHotels.length
@@ -121,6 +125,7 @@ export function createDestinationDataStore({
state.activeHotels = sortedHotels
state.activeCities = sortedCities
state.filterFromUrl = filterFromUrl
state.pendingFilters = filters
state.pendingSort = newSort
state.pendingHotelCount = filteredHotels.length

View File

@@ -11,7 +11,8 @@ import type { ReadonlyURLSearchParams } from "next/navigation"
interface Actions {
updateActiveFiltersAndSort: (
filterSlugs: string[],
sort: string | null
sort: string | null,
filterSlugFromUrl: string | null
) => void
setPendingSort: (sort: HotelSortOption) => void
togglePendingFilter: (filter: HotelFilter) => void