feat(BOOK-53): Added component for SEO filters and support filter switching
Approved-by: Chuma Mcphoy (We Ahead)
This commit is contained in:
@@ -32,8 +32,6 @@ export default function AccordionSection({ accordion, title }: AccordionProps) {
|
|||||||
<SectionHeader textTransform="uppercase" title={title} />
|
<SectionHeader textTransform="uppercase" title={title} />
|
||||||
<Accordion
|
<Accordion
|
||||||
className={`${styles.accordion} ${allAccordionsVisible ? styles.allVisible : ""}`}
|
className={`${styles.accordion} ${allAccordionsVisible ? styles.allVisible : ""}`}
|
||||||
theme="light"
|
|
||||||
variant="card"
|
|
||||||
>
|
>
|
||||||
{accordion.map((acc) =>
|
{accordion.map((acc) =>
|
||||||
acc ? (
|
acc ? (
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
} from "@/lib/trpc/memoizedRequests"
|
} from "@/lib/trpc/memoizedRequests"
|
||||||
|
|
||||||
import Breadcrumbs from "@/components/Breadcrumbs"
|
import Breadcrumbs from "@/components/Breadcrumbs"
|
||||||
|
import { SeoFilters } from "@/components/ContentType/DestinationPage/SeoFilters"
|
||||||
import BreadcrumbsSkeleton from "@/components/TempDesignSystem/Breadcrumbs/BreadcrumbsSkeleton"
|
import BreadcrumbsSkeleton from "@/components/TempDesignSystem/Breadcrumbs/BreadcrumbsSkeleton"
|
||||||
import { getIntl } from "@/i18n"
|
import { getIntl } from "@/i18n"
|
||||||
import { getLang } from "@/i18n/serverContext"
|
import { getLang } from "@/i18n/serverContext"
|
||||||
@@ -59,6 +60,7 @@ export default async function DestinationCityPage({
|
|||||||
sidepeek_button_text,
|
sidepeek_button_text,
|
||||||
sidepeek_content,
|
sidepeek_content,
|
||||||
destination_settings,
|
destination_settings,
|
||||||
|
seo_filters,
|
||||||
} = destinationCityPage
|
} = destinationCityPage
|
||||||
|
|
||||||
const allHotels = await getHotelsByCityIdentifier(cityIdentifier)
|
const allHotels = await getHotelsByCityIdentifier(cityIdentifier)
|
||||||
@@ -114,6 +116,7 @@ export default async function DestinationCityPage({
|
|||||||
<main className={styles.mainContent}>
|
<main className={styles.mainContent}>
|
||||||
<HotelListing />
|
<HotelListing />
|
||||||
{blocks && <Blocks blocks={blocks} />}
|
{blocks && <Blocks blocks={blocks} />}
|
||||||
|
<SeoFilters seoFilters={seo_filters} location={city.name} />
|
||||||
</main>
|
</main>
|
||||||
<aside className={styles.sidebar}>
|
<aside className={styles.sidebar}>
|
||||||
<SidebarContentWrapper preamble={preamble} location={city.name}>
|
<SidebarContentWrapper preamble={preamble} location={city.name}>
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
} from "@/lib/trpc/memoizedRequests"
|
} from "@/lib/trpc/memoizedRequests"
|
||||||
|
|
||||||
import Breadcrumbs from "@/components/Breadcrumbs"
|
import Breadcrumbs from "@/components/Breadcrumbs"
|
||||||
|
import { SeoFilters } from "@/components/ContentType/DestinationPage/SeoFilters"
|
||||||
import BreadcrumbsSkeleton from "@/components/TempDesignSystem/Breadcrumbs/BreadcrumbsSkeleton"
|
import BreadcrumbsSkeleton from "@/components/TempDesignSystem/Breadcrumbs/BreadcrumbsSkeleton"
|
||||||
import { getIntl } from "@/i18n"
|
import { getIntl } from "@/i18n"
|
||||||
import { getLang } from "@/i18n/serverContext"
|
import { getLang } from "@/i18n/serverContext"
|
||||||
@@ -59,6 +60,7 @@ export default async function DestinationCountryPage({
|
|||||||
sidepeek_button_text,
|
sidepeek_button_text,
|
||||||
sidepeek_content,
|
sidepeek_content,
|
||||||
destination_settings,
|
destination_settings,
|
||||||
|
seo_filters,
|
||||||
} = destinationCountryPage
|
} = destinationCountryPage
|
||||||
|
|
||||||
const [allHotels, allCities] = await Promise.all([
|
const [allHotels, allCities] = await Promise.all([
|
||||||
@@ -116,6 +118,10 @@ export default async function DestinationCountryPage({
|
|||||||
<main className={styles.mainContent}>
|
<main className={styles.mainContent}>
|
||||||
<CityListing />
|
<CityListing />
|
||||||
{blocks && <Blocks blocks={blocks} />}
|
{blocks && <Blocks blocks={blocks} />}
|
||||||
|
<SeoFilters
|
||||||
|
seoFilters={seo_filters}
|
||||||
|
location={translatedCountry}
|
||||||
|
/>
|
||||||
</main>
|
</main>
|
||||||
<aside className={styles.sidebar}>
|
<aside className={styles.sidebar}>
|
||||||
<SidebarContentWrapper
|
<SidebarContentWrapper
|
||||||
|
|||||||
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -37,7 +37,7 @@ export default async function AmenitiesSidePeek({
|
|||||||
defaultMessage: "Close",
|
defaultMessage: "Close",
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<Accordion>
|
<Accordion type="sidepeek">
|
||||||
<ParkingAccordionItem
|
<ParkingAccordionItem
|
||||||
parking={parking.parking}
|
parking={parking.parking}
|
||||||
elevatorPitch={parking.parkingElevatorPitch}
|
elevatorPitch={parking.parkingElevatorPitch}
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ export default function AccessibilityAccordionItem({
|
|||||||
})}
|
})}
|
||||||
iconName={IconName.Accessibility}
|
iconName={IconName.Accessibility}
|
||||||
className={styles.accordionItem}
|
className={styles.accordionItem}
|
||||||
variant="sidepeek"
|
type="sidepeek"
|
||||||
onOpen={() => trackAccordionClick("amenities:accessibility")}
|
onOpen={() => trackAccordionClick("amenities:accessibility")}
|
||||||
>
|
>
|
||||||
<div className={styles.accessibilityContent}>
|
<div className={styles.accessibilityContent}>
|
||||||
|
|||||||
@@ -439,12 +439,12 @@ export default function BookedRoomSidePeek({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{hotelRoom ? (
|
{hotelRoom ? (
|
||||||
<Accordion>
|
<Accordion type="sidepeek">
|
||||||
<AccordionItem
|
<AccordionItem
|
||||||
title={intl.formatMessage({
|
title={intl.formatMessage({
|
||||||
defaultMessage: "Room details",
|
defaultMessage: "Room details",
|
||||||
})}
|
})}
|
||||||
variant="sidepeek"
|
type="sidepeek"
|
||||||
>
|
>
|
||||||
<RoomDetails
|
<RoomDetails
|
||||||
roomDescription={hotelRoom.descriptions.medium}
|
roomDescription={hotelRoom.descriptions.medium}
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ export default function DestinationDataProviderContent({
|
|||||||
filters.push(...filterParam.split("&"))
|
filters.push(...filterParam.split("&"))
|
||||||
}
|
}
|
||||||
|
|
||||||
updateActiveFiltersAndSort(filters, sort)
|
updateActiveFiltersAndSort(filters, sort, filterFromUrl)
|
||||||
}, [params, updateActiveFiltersAndSort, basePath])
|
}, [params, updateActiveFiltersAndSort, basePath])
|
||||||
|
|
||||||
return <>{children}</>
|
return <>{children}</>
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ export function createDestinationDataStore({
|
|||||||
|
|
||||||
return create<DestinationDataState>((set) => ({
|
return create<DestinationDataState>((set) => ({
|
||||||
actions: {
|
actions: {
|
||||||
updateActiveFiltersAndSort(filterSlugs, sort) {
|
updateActiveFiltersAndSort(filterSlugs, sort, filterSlugFromUrl) {
|
||||||
return set(
|
return set(
|
||||||
produce((state: DestinationDataState) => {
|
produce((state: DestinationDataState) => {
|
||||||
const newSort =
|
const newSort =
|
||||||
@@ -79,6 +79,10 @@ export function createDestinationDataStore({
|
|||||||
const filters = flattenedFilters.filter((filter) =>
|
const filters = flattenedFilters.filter((filter) =>
|
||||||
filterSlugs.includes(filter.slug)
|
filterSlugs.includes(filter.slug)
|
||||||
)
|
)
|
||||||
|
const filterFromUrl =
|
||||||
|
flattenedFilters.find(
|
||||||
|
(filter) => filter.slug === filterSlugFromUrl
|
||||||
|
) ?? null
|
||||||
const filteredHotels = getFilteredHotels(state.allHotels, filters)
|
const filteredHotels = getFilteredHotels(state.allHotels, filters)
|
||||||
const sortedHotels = getSortedHotels(filteredHotels, newSort)
|
const sortedHotels = getSortedHotels(filteredHotels, newSort)
|
||||||
const filteredCities = state.allHotels.length
|
const filteredCities = state.allHotels.length
|
||||||
@@ -121,6 +125,7 @@ export function createDestinationDataStore({
|
|||||||
state.activeHotels = sortedHotels
|
state.activeHotels = sortedHotels
|
||||||
state.activeCities = sortedCities
|
state.activeCities = sortedCities
|
||||||
|
|
||||||
|
state.filterFromUrl = filterFromUrl
|
||||||
state.pendingFilters = filters
|
state.pendingFilters = filters
|
||||||
state.pendingSort = newSort
|
state.pendingSort = newSort
|
||||||
state.pendingHotelCount = filteredHotels.length
|
state.pendingHotelCount = filteredHotels.length
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ import type { ReadonlyURLSearchParams } from "next/navigation"
|
|||||||
interface Actions {
|
interface Actions {
|
||||||
updateActiveFiltersAndSort: (
|
updateActiveFiltersAndSort: (
|
||||||
filterSlugs: string[],
|
filterSlugs: string[],
|
||||||
sort: string | null
|
sort: string | null,
|
||||||
|
filterSlugFromUrl: string | null
|
||||||
) => void
|
) => void
|
||||||
setPendingSort: (sort: HotelSortOption) => void
|
setPendingSort: (sort: HotelSortOption) => void
|
||||||
togglePendingFilter: (filter: HotelFilter) => void
|
togglePendingFilter: (filter: HotelFilter) => void
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ export function HotelSidePeekContent({
|
|||||||
</Typography>
|
</Typography>
|
||||||
<Contact hotel={hotel} />
|
<Contact hotel={hotel} />
|
||||||
|
|
||||||
<Accordion>
|
<Accordion type="sidepeek">
|
||||||
<ParkingAccordionItem
|
<ParkingAccordionItem
|
||||||
parking={hotel.parking}
|
parking={hotel.parking}
|
||||||
elevatorPitch={additionalHotelData?.hotelParking.elevatorPitch}
|
elevatorPitch={additionalHotelData?.hotelParking.elevatorPitch}
|
||||||
@@ -95,7 +95,7 @@ function AccessibilityAccordionItem({
|
|||||||
})}
|
})}
|
||||||
iconName={IconName.Accessibility}
|
iconName={IconName.Accessibility}
|
||||||
className={styles.accordionItem}
|
className={styles.accordionItem}
|
||||||
variant="sidepeek"
|
type="sidepeek"
|
||||||
onOpen={() => tracking.trackAccordionItemOpen("amenities:accessibility")}
|
onOpen={() => tracking.trackAccordionItemOpen("amenities:accessibility")}
|
||||||
>
|
>
|
||||||
<div className={styles.accessibilityContent}>
|
<div className={styles.accessibilityContent}>
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ export default function BreakfastAccordionItem({
|
|||||||
<AccordionItem
|
<AccordionItem
|
||||||
title={intl.formatMessage({ defaultMessage: "Breakfast" })}
|
title={intl.formatMessage({ defaultMessage: "Breakfast" })}
|
||||||
iconName={IconName.CoffeeAlt}
|
iconName={IconName.CoffeeAlt}
|
||||||
variant="sidepeek"
|
type="sidepeek"
|
||||||
className={styles.accordionItem}
|
className={styles.accordionItem}
|
||||||
onOpen={() => tracking.trackAccordionItemOpen("amenities:breakfast")}
|
onOpen={() => tracking.trackAccordionItemOpen("amenities:breakfast")}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ export default function CheckInCheckOutAccordionItem({
|
|||||||
<AccordionItem
|
<AccordionItem
|
||||||
title={intl.formatMessage({ defaultMessage: "Check-in/Check-out" })}
|
title={intl.formatMessage({ defaultMessage: "Check-in/Check-out" })}
|
||||||
iconName={IconName.Business}
|
iconName={IconName.Business}
|
||||||
variant="sidepeek"
|
type="sidepeek"
|
||||||
className={styles.accordionItem}
|
className={styles.accordionItem}
|
||||||
onOpen={() => tracking.trackAccordionItemOpen("amenities:check-in")}
|
onOpen={() => tracking.trackAccordionItemOpen("amenities:check-in")}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ export default function ParkingAccordionItem({
|
|||||||
defaultMessage: "Parking",
|
defaultMessage: "Parking",
|
||||||
})}
|
})}
|
||||||
iconName={IconName.Parking}
|
iconName={IconName.Parking}
|
||||||
variant="sidepeek"
|
type="sidepeek"
|
||||||
className={styles.accordionItem}
|
className={styles.accordionItem}
|
||||||
onOpen={() => tracking.trackAccordionItemOpen("amenities:parking")}
|
onOpen={() => tracking.trackAccordionItemOpen("amenities:parking")}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -263,4 +263,6 @@ export enum FacilityEnum {
|
|||||||
WideEntrance = 2085,
|
WideEntrance = 2085,
|
||||||
WideRestaurantEntrance = 2087,
|
WideRestaurantEntrance = 2087,
|
||||||
WiFiWirelessInternetAccessAllScandic = 5774,
|
WiFiWirelessInternetAccessAllScandic = 5774,
|
||||||
|
|
||||||
|
UNKNOWN = -1,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,45 +1,42 @@
|
|||||||
.accordionItem {
|
.accordionItem {
|
||||||
border-bottom: 1px solid var(--Base-Border-Subtle);
|
border-bottom: 1px solid var(--Border-Default);
|
||||||
}
|
|
||||||
|
|
||||||
.accordionItem.card {
|
&.card {
|
||||||
padding: var(--Spacing-x1);
|
padding: var(--Space-x1);
|
||||||
}
|
|
||||||
|
|
||||||
.accordionItem.sidepeek {
|
.summary {
|
||||||
padding: var(--Spacing-x1) 0;
|
padding: var(--Space-x15) var(--Space-x2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.sidepeek {
|
||||||
|
padding: var(--Space-x1) 0;
|
||||||
|
|
||||||
|
.summary {
|
||||||
|
padding: var(--Space-x15) var(--Space-x1);
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary:hover {
|
||||||
|
background-color: var(--Surface-Primary-Hover);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.summary {
|
.summary {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: var(--Spacing-x-one-and-half) var(--Spacing-x1);
|
gap: var(--Space-x15) var(--Space-x1);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
color: var(--Base-Text-High-contrast);
|
color: var(--Text-Interactive-Default);
|
||||||
font-family: var(--typography-Body-Bold-fontFamily);
|
|
||||||
font-size: var(--typography-Body-Bold-fontSize);
|
|
||||||
font-weight: var(--typography-Body-Bold-fontWeight);
|
|
||||||
transition: background-color 0.3s;
|
transition: background-color 0.3s;
|
||||||
}
|
|
||||||
.summary.card:hover {
|
|
||||||
background-color: var(--Base-Surface-Primary-light-Hover-alt);
|
|
||||||
}
|
|
||||||
.accordionItem.light .summary:hover {
|
|
||||||
background-color: var(--Base-Surface-Primary-light-Hover);
|
|
||||||
}
|
|
||||||
.accordionItem.subtle .summary:hover {
|
|
||||||
background-color: var(--Base-Surface-Primary-light-Normal);
|
|
||||||
}
|
|
||||||
|
|
||||||
.accordionItem.card .summary {
|
|
||||||
padding: var(--Spacing-x-one-and-half) var(--Spacing-x2);
|
|
||||||
border-radius: var(--Corner-radius-md);
|
border-radius: var(--Corner-radius-md);
|
||||||
}
|
list-style: none;
|
||||||
|
|
||||||
.accordionItem.sidepeek .summary {
|
&::-webkit-details-marker {
|
||||||
padding: var(--Spacing-x-one-and-half) var(--Spacing-x1);
|
display: none;
|
||||||
align-items: center;
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
@@ -47,7 +44,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
padding: var(--Spacing-x-one-and-half) var(--Spacing-x2) var(--Spacing-x1);
|
padding: var(--Space-x15) var(--Space-x2) var(--Space-x1);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
max-height: 0;
|
max-height: 0;
|
||||||
transition: max-height 0.3s;
|
transition: max-height 0.3s;
|
||||||
@@ -57,6 +54,7 @@
|
|||||||
transition: transform 0.3s;
|
transition: transform 0.3s;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
details[open] .chevron {
|
details[open] .chevron {
|
||||||
transform: rotate(180deg);
|
transform: rotate(180deg);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,17 +2,16 @@
|
|||||||
|
|
||||||
import { type ReactNode, useRef } from 'react'
|
import { type ReactNode, useRef } from 'react'
|
||||||
|
|
||||||
import Body from '../../Body'
|
|
||||||
import { IconByIconName } from '../../Icons/IconByIconName'
|
import { IconByIconName } from '../../Icons/IconByIconName'
|
||||||
import { MaterialIcon } from '../../Icons/MaterialIcon'
|
import { MaterialIcon } from '../../Icons/MaterialIcon'
|
||||||
import Subtitle from '../../Subtitle'
|
|
||||||
|
|
||||||
import { accordionItemVariants } from './variants'
|
import { accordionItemVariants } from './variants'
|
||||||
|
|
||||||
import styles from './accordionItem.module.css'
|
import styles from './accordionItem.module.css'
|
||||||
|
|
||||||
import type { IconName } from '../../Icons/iconName'
|
|
||||||
import type { VariantProps } from 'class-variance-authority'
|
import type { VariantProps } from 'class-variance-authority'
|
||||||
|
import type { IconName } from '../../Icons/iconName'
|
||||||
|
import { Typography } from '../../Typography'
|
||||||
|
|
||||||
export interface AccordionItemProps
|
export interface AccordionItemProps
|
||||||
extends React.HtmlHTMLAttributes<HTMLDetailsElement>,
|
extends React.HtmlHTMLAttributes<HTMLDetailsElement>,
|
||||||
@@ -21,6 +20,7 @@ export interface AccordionItemProps
|
|||||||
iconName?: IconName
|
iconName?: IconName
|
||||||
icon?: ReactNode
|
icon?: ReactNode
|
||||||
subtitle?: string
|
subtitle?: string
|
||||||
|
showAsSubtitle?: boolean
|
||||||
onOpen?: () => void
|
onOpen?: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -29,10 +29,10 @@ export default function AccordionItem({
|
|||||||
icon,
|
icon,
|
||||||
iconName,
|
iconName,
|
||||||
title,
|
title,
|
||||||
theme,
|
type,
|
||||||
variant,
|
|
||||||
className,
|
className,
|
||||||
subtitle,
|
subtitle,
|
||||||
|
showAsSubtitle = false,
|
||||||
onOpen,
|
onOpen,
|
||||||
}: AccordionItemProps) {
|
}: AccordionItemProps) {
|
||||||
const contentRef = useRef<HTMLDivElement>(null)
|
const contentRef = useRef<HTMLDivElement>(null)
|
||||||
@@ -71,36 +71,36 @@ export default function AccordionItem({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li className={accordionItemVariants({ className, variant, theme })}>
|
<li className={accordionItemVariants({ className, type })}>
|
||||||
<details ref={detailsRef} onToggle={toggleAccordion}>
|
<details ref={detailsRef} onToggle={toggleAccordion}>
|
||||||
<summary className={styles.summary}>
|
<summary className={styles.summary}>
|
||||||
{IconComp}
|
{IconComp}
|
||||||
{variant === 'sidepeek' ? (
|
{type === 'sidepeek' ? (
|
||||||
<Subtitle
|
<Typography variant="Title/Subtitle/md">
|
||||||
className={styles.title}
|
<p className={styles.title}>{title}</p>
|
||||||
type="two"
|
</Typography>
|
||||||
color="baseTextHighContrast"
|
|
||||||
>
|
|
||||||
{title}
|
|
||||||
</Subtitle>
|
|
||||||
) : (
|
) : (
|
||||||
<div className={styles.title}>
|
<div className={styles.title}>
|
||||||
{subtitle ? (
|
{subtitle || showAsSubtitle ? (
|
||||||
<Subtitle type="two" color="baseTextHighContrast">
|
<Typography variant="Title/Subtitle/md">
|
||||||
{title}
|
<p className={styles.title}>{title}</p>
|
||||||
</Subtitle>
|
</Typography>
|
||||||
) : (
|
) : (
|
||||||
<Body textTransform="bold" color="baseTextHighContrast">
|
<Typography variant="Body/Paragraph/mdBold">
|
||||||
{title}
|
<p className={styles.title}>{title}</p>
|
||||||
</Body>
|
</Typography>
|
||||||
|
)}
|
||||||
|
{subtitle && (
|
||||||
|
<Typography variant="Body/Paragraph/mdBold">
|
||||||
|
<p>{subtitle}</p>
|
||||||
|
</Typography>
|
||||||
)}
|
)}
|
||||||
{subtitle && <Body color="baseTextHighContrast">{subtitle}</Body>}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<MaterialIcon
|
<MaterialIcon
|
||||||
icon="keyboard_arrow_down"
|
icon="keyboard_arrow_down"
|
||||||
className={styles.chevron}
|
className={styles.chevron}
|
||||||
color="Icon/Interactive/Default"
|
color="CurrentColor"
|
||||||
size={20}
|
size={20}
|
||||||
/>
|
/>
|
||||||
</summary>
|
</summary>
|
||||||
|
|||||||
@@ -4,18 +4,12 @@ import styles from './accordionItem.module.css'
|
|||||||
|
|
||||||
export const accordionItemVariants = cva(styles.accordionItem, {
|
export const accordionItemVariants = cva(styles.accordionItem, {
|
||||||
variants: {
|
variants: {
|
||||||
variant: {
|
type: {
|
||||||
card: styles.card,
|
card: styles.card,
|
||||||
sidepeek: styles.sidepeek,
|
sidepeek: styles.sidepeek,
|
||||||
},
|
},
|
||||||
theme: {
|
|
||||||
default: styles.default,
|
|
||||||
light: styles.light,
|
|
||||||
subtle: styles.subtle,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
defaultVariants: {
|
defaultVariants: {
|
||||||
variant: 'card',
|
type: 'card',
|
||||||
theme: 'default',
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,25 +1,11 @@
|
|||||||
.accordion {
|
.accordion {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
}
|
|
||||||
|
|
||||||
.accordion.card {
|
&.card {
|
||||||
border-radius: var(--Corner-radius-md);
|
border-radius: var(--Corner-radius-md);
|
||||||
}
|
|
||||||
|
|
||||||
.accordion.light {
|
li:last-child {
|
||||||
background-color: var(--Base-Surface-Primary-light-Normal);
|
border-width: 0;
|
||||||
}
|
}
|
||||||
.accordion.subtle {
|
}
|
||||||
background-color: var(--Background-Primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.accordion li:last-child {
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.accordion details > summary {
|
|
||||||
list-style: none;
|
|
||||||
}
|
|
||||||
.accordion details > summary::-webkit-details-marker {
|
|
||||||
display: none;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import { Children, cloneElement, isValidElement } from 'react'
|
import { Children, cloneElement, isValidElement } from 'react'
|
||||||
|
|
||||||
import { accordionVariants } from './variants'
|
|
||||||
|
|
||||||
import type { VariantProps } from 'class-variance-authority'
|
import type { VariantProps } from 'class-variance-authority'
|
||||||
|
|
||||||
import type { AccordionItemProps } from './AccordionItem'
|
import type { AccordionItemProps } from './AccordionItem'
|
||||||
|
import { accordionVariants } from './variants'
|
||||||
|
|
||||||
interface AccordionProps
|
interface AccordionProps
|
||||||
extends React.HtmlHTMLAttributes<HTMLUListElement>,
|
extends React.HtmlHTMLAttributes<HTMLUListElement>,
|
||||||
@@ -13,14 +12,13 @@ interface AccordionProps
|
|||||||
export default function Accordion({
|
export default function Accordion({
|
||||||
children,
|
children,
|
||||||
className,
|
className,
|
||||||
theme,
|
type,
|
||||||
variant,
|
|
||||||
}: AccordionProps) {
|
}: AccordionProps) {
|
||||||
return (
|
return (
|
||||||
<ul className={accordionVariants({ className, variant, theme })}>
|
<ul className={accordionVariants({ className, type })}>
|
||||||
{Children.map(children, (child) => {
|
{Children.map(children, (child) => {
|
||||||
if (isValidElement<AccordionItemProps>(child)) {
|
if (isValidElement<AccordionItemProps>(child)) {
|
||||||
return cloneElement(child, { variant, theme })
|
return cloneElement(child, { type })
|
||||||
} else {
|
} else {
|
||||||
return child
|
return child
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,18 +4,12 @@ import styles from './accordion.module.css'
|
|||||||
|
|
||||||
export const accordionVariants = cva(styles.accordion, {
|
export const accordionVariants = cva(styles.accordion, {
|
||||||
variants: {
|
variants: {
|
||||||
variant: {
|
type: {
|
||||||
card: styles.card,
|
card: styles.card,
|
||||||
sidepeek: styles.sidepeek,
|
sidepeek: styles.sidepeek,
|
||||||
},
|
},
|
||||||
theme: {
|
|
||||||
default: styles.default,
|
|
||||||
light: styles.light,
|
|
||||||
subtle: styles.subtle,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
defaultVariants: {
|
defaultVariants: {
|
||||||
variant: 'card',
|
type: 'card',
|
||||||
theme: 'default',
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { FacilityEnum } from '@scandic-hotels/common/constants/facilities'
|
import { FacilityEnum } from '@scandic-hotels/common/constants/facilities'
|
||||||
|
|
||||||
import type { JSX } from 'react'
|
import type { JSX } from 'react'
|
||||||
import { IconName } from '../Icons/iconName'
|
|
||||||
import { MaterialIconSetIconProps } from '../Icons/MaterialIcon'
|
|
||||||
import { IconProps, NucleoIconProps } from '../Icons'
|
import { IconProps, NucleoIconProps } from '../Icons'
|
||||||
import { IconByIconName } from '../Icons/IconByIconName'
|
import { IconByIconName } from '../Icons/IconByIconName'
|
||||||
|
import { IconName } from '../Icons/iconName'
|
||||||
|
import { MaterialIconSetIconProps } from '../Icons/MaterialIcon'
|
||||||
|
|
||||||
interface mapFacilityToIconProps {
|
interface mapFacilityToIconProps {
|
||||||
id: FacilityEnum
|
id: FacilityEnum
|
||||||
@@ -299,4 +299,6 @@ const facilityToIconMap: Record<FacilityEnum, IconName> = {
|
|||||||
[FacilityEnum.WideEntrance]: IconName.StarFilled,
|
[FacilityEnum.WideEntrance]: IconName.StarFilled,
|
||||||
[FacilityEnum.WideRestaurantEntrance]: IconName.StarFilled,
|
[FacilityEnum.WideRestaurantEntrance]: IconName.StarFilled,
|
||||||
[FacilityEnum.WiFiWirelessInternetAccessAllScandic]: IconName.StarFilled,
|
[FacilityEnum.WiFiWirelessInternetAccessAllScandic]: IconName.StarFilled,
|
||||||
|
|
||||||
|
[FacilityEnum.UNKNOWN]: IconName.StarFilled,
|
||||||
}
|
}
|
||||||
|
|||||||
14
packages/trpc/lib/graphql/Fragments/HotelFilter.graphql
Normal file
14
packages/trpc/lib/graphql/Fragments/HotelFilter.graphql
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
#import "./System.graphql"
|
||||||
|
|
||||||
|
fragment HotelFilter on HotelFilter {
|
||||||
|
title
|
||||||
|
facility_id
|
||||||
|
category
|
||||||
|
slug
|
||||||
|
}
|
||||||
|
|
||||||
|
fragment HotelFilterRef on HotelFilter {
|
||||||
|
system {
|
||||||
|
...System
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
#import "../../Fragments/System.graphql"
|
#import "../../Fragments/System.graphql"
|
||||||
|
|
||||||
|
#import "../../Fragments/HotelFilter.graphql"
|
||||||
|
|
||||||
#import "../../Fragments/Blocks/Accordion.graphql"
|
#import "../../Fragments/Blocks/Accordion.graphql"
|
||||||
#import "../../Fragments/Blocks/Content.graphql"
|
#import "../../Fragments/Blocks/Content.graphql"
|
||||||
|
|
||||||
@@ -82,6 +84,15 @@ query GetDestinationCityPage($locale: String!, $uid: String!) {
|
|||||||
...Accordion_DestinationCityPage
|
...Accordion_DestinationCityPage
|
||||||
...Content_DestinationCityPage
|
...Content_DestinationCityPage
|
||||||
}
|
}
|
||||||
|
seo_filters {
|
||||||
|
filterConnection {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
...HotelFilter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
system {
|
system {
|
||||||
...System
|
...System
|
||||||
created_at
|
created_at
|
||||||
@@ -135,6 +146,15 @@ query GetDestinationCityPageRefs($locale: String!, $uid: String!) {
|
|||||||
...Accordion_DestinationCityPageRefs
|
...Accordion_DestinationCityPageRefs
|
||||||
...Content_DestinationCityPageRefs
|
...Content_DestinationCityPageRefs
|
||||||
}
|
}
|
||||||
|
seo_filters {
|
||||||
|
filterConnection {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
...HotelFilterRef
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
system {
|
system {
|
||||||
...System
|
...System
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
#import "../../Fragments/System.graphql"
|
#import "../../Fragments/System.graphql"
|
||||||
|
|
||||||
|
#import "../../Fragments/HotelFilter.graphql"
|
||||||
|
|
||||||
#import "../../Fragments/Blocks/Accordion.graphql"
|
#import "../../Fragments/Blocks/Accordion.graphql"
|
||||||
#import "../../Fragments/Blocks/Content.graphql"
|
#import "../../Fragments/Blocks/Content.graphql"
|
||||||
|
|
||||||
@@ -77,6 +79,15 @@ query GetDestinationCountryPage($locale: String!, $uid: String!) {
|
|||||||
...Accordion_DestinationCountryPage
|
...Accordion_DestinationCountryPage
|
||||||
...Content_DestinationCountryPage
|
...Content_DestinationCountryPage
|
||||||
}
|
}
|
||||||
|
seo_filters {
|
||||||
|
filterConnection {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
...HotelFilter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
system {
|
system {
|
||||||
...System
|
...System
|
||||||
created_at
|
created_at
|
||||||
@@ -117,6 +128,15 @@ query GetDestinationCountryPageRefs($locale: String!, $uid: String!) {
|
|||||||
...Accordion_DestinationCountryPageRefs
|
...Accordion_DestinationCountryPageRefs
|
||||||
...Content_DestinationCountryPageRefs
|
...Content_DestinationCountryPageRefs
|
||||||
}
|
}
|
||||||
|
seo_filters {
|
||||||
|
filterConnection {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
...HotelFilterRef
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
system {
|
system {
|
||||||
...System
|
...System
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,10 @@ import {
|
|||||||
accordionSchema,
|
accordionSchema,
|
||||||
} from "../schemas/blocks/accordion"
|
} from "../schemas/blocks/accordion"
|
||||||
import { contentRefsSchema, contentSchema } from "../schemas/blocks/content"
|
import { contentRefsSchema, contentSchema } from "../schemas/blocks/content"
|
||||||
|
import {
|
||||||
|
destinationFiltersRefsSchema,
|
||||||
|
destinationFiltersSchema,
|
||||||
|
} from "../schemas/destinationFilters"
|
||||||
import { mapLocationSchema } from "../schemas/mapLocation"
|
import { mapLocationSchema } from "../schemas/mapLocation"
|
||||||
import {
|
import {
|
||||||
linkRefsUnionSchema,
|
linkRefsUnionSchema,
|
||||||
@@ -159,6 +163,7 @@ export const destinationCityPageSchema = z.object({
|
|||||||
})
|
})
|
||||||
.nullish(),
|
.nullish(),
|
||||||
blocks: discriminatedUnionArray(blocksSchema.options).nullable(),
|
blocks: discriminatedUnionArray(blocksSchema.options).nullable(),
|
||||||
|
seo_filters: destinationFiltersSchema,
|
||||||
system: systemSchema.merge(
|
system: systemSchema.merge(
|
||||||
z.object({
|
z.object({
|
||||||
created_at: z.string(),
|
created_at: z.string(),
|
||||||
@@ -258,6 +263,7 @@ export const destinationCityPageRefsSchema = z.object({
|
|||||||
})
|
})
|
||||||
.nullish(),
|
.nullish(),
|
||||||
blocks: discriminatedUnionArray(blocksRefsSchema.options).nullable(),
|
blocks: discriminatedUnionArray(blocksRefsSchema.options).nullable(),
|
||||||
|
seo_filters: destinationFiltersRefsSchema,
|
||||||
system: systemSchema,
|
system: systemSchema,
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -11,6 +11,10 @@ import {
|
|||||||
accordionSchema,
|
accordionSchema,
|
||||||
} from "../schemas/blocks/accordion"
|
} from "../schemas/blocks/accordion"
|
||||||
import { contentRefsSchema, contentSchema } from "../schemas/blocks/content"
|
import { contentRefsSchema, contentSchema } from "../schemas/blocks/content"
|
||||||
|
import {
|
||||||
|
destinationFiltersRefsSchema,
|
||||||
|
destinationFiltersSchema,
|
||||||
|
} from "../schemas/destinationFilters"
|
||||||
import { mapLocationSchema } from "../schemas/mapLocation"
|
import { mapLocationSchema } from "../schemas/mapLocation"
|
||||||
import {
|
import {
|
||||||
linkRefsUnionSchema,
|
linkRefsUnionSchema,
|
||||||
@@ -88,6 +92,7 @@ export const destinationCountryPageSchema = z.object({
|
|||||||
})
|
})
|
||||||
.nullish(),
|
.nullish(),
|
||||||
blocks: discriminatedUnionArray(blocksSchema.options).nullable(),
|
blocks: discriminatedUnionArray(blocksSchema.options).nullable(),
|
||||||
|
seo_filters: destinationFiltersSchema,
|
||||||
system: systemSchema.merge(
|
system: systemSchema.merge(
|
||||||
z.object({
|
z.object({
|
||||||
created_at: z.string(),
|
created_at: z.string(),
|
||||||
@@ -160,6 +165,7 @@ export const destinationCountryPageRefsSchema = z.object({
|
|||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
blocks: discriminatedUnionArray(blocksRefsSchema.options).nullable(),
|
blocks: discriminatedUnionArray(blocksRefsSchema.options).nullable(),
|
||||||
|
seo_filters: destinationFiltersRefsSchema,
|
||||||
system: systemSchema,
|
system: systemSchema,
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import { z } from "zod"
|
||||||
|
|
||||||
|
import { isDefined } from "@scandic-hotels/common/utils/isDefined"
|
||||||
|
|
||||||
|
import { hotelFilterSchema } from "./hotelFilter"
|
||||||
|
import { systemSchema } from "./system"
|
||||||
|
|
||||||
|
export const destinationFiltersSchema = z
|
||||||
|
.array(
|
||||||
|
z.object({
|
||||||
|
filterConnection: z.object({
|
||||||
|
edges: z.array(
|
||||||
|
z.object({
|
||||||
|
node: hotelFilterSchema,
|
||||||
|
})
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.nullish()
|
||||||
|
.transform((data) => {
|
||||||
|
const filters = data
|
||||||
|
?.map(({ filterConnection }) => filterConnection.edges[0]?.node)
|
||||||
|
.filter(isDefined)
|
||||||
|
|
||||||
|
if (!data || !filters?.length) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const facilityFilters = filters.filter((f) => f.filterType === "facility")
|
||||||
|
const surroundingsFilters = filters.filter(
|
||||||
|
(f) => f.filterType === "surroundings"
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
facilityFilters,
|
||||||
|
surroundingsFilters,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export const destinationFiltersRefsSchema = z
|
||||||
|
.array(
|
||||||
|
z.object({
|
||||||
|
filterConnection: z.object({
|
||||||
|
edges: z.array(
|
||||||
|
z.object({
|
||||||
|
node: z.object({
|
||||||
|
system: systemSchema,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.nullish()
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { z } from "zod"
|
||||||
|
|
||||||
|
import { FacilityEnum } from "@scandic-hotels/common/constants/facilities"
|
||||||
|
|
||||||
|
export const hotelFilterSchema = z
|
||||||
|
.object({
|
||||||
|
title: z.string(),
|
||||||
|
facility_id: z.nativeEnum(FacilityEnum).catch(FacilityEnum.UNKNOWN),
|
||||||
|
category: z.string(),
|
||||||
|
slug: z.string(),
|
||||||
|
})
|
||||||
|
.transform((data) => ({
|
||||||
|
id: data.facility_id,
|
||||||
|
name: data.title,
|
||||||
|
filterType: data.category,
|
||||||
|
slug: data.slug,
|
||||||
|
sortOrder: 0,
|
||||||
|
}))
|
||||||
Reference in New Issue
Block a user