feat(BOOK-53): Added component for SEO filters and support filter switching
Approved-by: Chuma Mcphoy (We Ahead)
This commit is contained in:
@@ -43,7 +43,7 @@ export function HotelSidePeekContent({
|
||||
</Typography>
|
||||
<Contact hotel={hotel} />
|
||||
|
||||
<Accordion>
|
||||
<Accordion type="sidepeek">
|
||||
<ParkingAccordionItem
|
||||
parking={hotel.parking}
|
||||
elevatorPitch={additionalHotelData?.hotelParking.elevatorPitch}
|
||||
@@ -95,7 +95,7 @@ function AccessibilityAccordionItem({
|
||||
})}
|
||||
iconName={IconName.Accessibility}
|
||||
className={styles.accordionItem}
|
||||
variant="sidepeek"
|
||||
type="sidepeek"
|
||||
onOpen={() => tracking.trackAccordionItemOpen("amenities:accessibility")}
|
||||
>
|
||||
<div className={styles.accessibilityContent}>
|
||||
|
||||
@@ -44,7 +44,7 @@ export default function BreakfastAccordionItem({
|
||||
<AccordionItem
|
||||
title={intl.formatMessage({ defaultMessage: "Breakfast" })}
|
||||
iconName={IconName.CoffeeAlt}
|
||||
variant="sidepeek"
|
||||
type="sidepeek"
|
||||
className={styles.accordionItem}
|
||||
onOpen={() => tracking.trackAccordionItemOpen("amenities:breakfast")}
|
||||
>
|
||||
|
||||
@@ -29,7 +29,7 @@ export default function CheckInCheckOutAccordionItem({
|
||||
<AccordionItem
|
||||
title={intl.formatMessage({ defaultMessage: "Check-in/Check-out" })}
|
||||
iconName={IconName.Business}
|
||||
variant="sidepeek"
|
||||
type="sidepeek"
|
||||
className={styles.accordionItem}
|
||||
onOpen={() => tracking.trackAccordionItemOpen("amenities:check-in")}
|
||||
>
|
||||
|
||||
@@ -38,7 +38,7 @@ export default function ParkingAccordionItem({
|
||||
defaultMessage: "Parking",
|
||||
})}
|
||||
iconName={IconName.Parking}
|
||||
variant="sidepeek"
|
||||
type="sidepeek"
|
||||
className={styles.accordionItem}
|
||||
onOpen={() => tracking.trackAccordionItemOpen("amenities:parking")}
|
||||
>
|
||||
|
||||
@@ -263,4 +263,6 @@ export enum FacilityEnum {
|
||||
WideEntrance = 2085,
|
||||
WideRestaurantEntrance = 2087,
|
||||
WiFiWirelessInternetAccessAllScandic = 5774,
|
||||
|
||||
UNKNOWN = -1,
|
||||
}
|
||||
|
||||
@@ -1,45 +1,42 @@
|
||||
.accordionItem {
|
||||
border-bottom: 1px solid var(--Base-Border-Subtle);
|
||||
}
|
||||
border-bottom: 1px solid var(--Border-Default);
|
||||
|
||||
.accordionItem.card {
|
||||
padding: var(--Spacing-x1);
|
||||
}
|
||||
&.card {
|
||||
padding: var(--Space-x1);
|
||||
|
||||
.accordionItem.sidepeek {
|
||||
padding: var(--Spacing-x1) 0;
|
||||
.summary {
|
||||
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 {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--Spacing-x-one-and-half) var(--Spacing-x1);
|
||||
gap: var(--Space-x15) var(--Space-x1);
|
||||
cursor: pointer;
|
||||
color: var(--Base-Text-High-contrast);
|
||||
font-family: var(--typography-Body-Bold-fontFamily);
|
||||
font-size: var(--typography-Body-Bold-fontSize);
|
||||
font-weight: var(--typography-Body-Bold-fontWeight);
|
||||
color: var(--Text-Interactive-Default);
|
||||
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);
|
||||
}
|
||||
list-style: none;
|
||||
|
||||
.accordionItem.sidepeek .summary {
|
||||
padding: var(--Spacing-x-one-and-half) var(--Spacing-x1);
|
||||
align-items: center;
|
||||
&::-webkit-details-marker {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.title {
|
||||
@@ -47,7 +44,7 @@
|
||||
}
|
||||
|
||||
.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;
|
||||
max-height: 0;
|
||||
transition: max-height 0.3s;
|
||||
@@ -57,6 +54,7 @@
|
||||
transition: transform 0.3s;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
details[open] .chevron {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
@@ -2,17 +2,16 @@
|
||||
|
||||
import { type ReactNode, useRef } from 'react'
|
||||
|
||||
import Body from '../../Body'
|
||||
import { IconByIconName } from '../../Icons/IconByIconName'
|
||||
import { MaterialIcon } from '../../Icons/MaterialIcon'
|
||||
import Subtitle from '../../Subtitle'
|
||||
|
||||
import { accordionItemVariants } from './variants'
|
||||
|
||||
import styles from './accordionItem.module.css'
|
||||
|
||||
import type { IconName } from '../../Icons/iconName'
|
||||
import type { VariantProps } from 'class-variance-authority'
|
||||
import type { IconName } from '../../Icons/iconName'
|
||||
import { Typography } from '../../Typography'
|
||||
|
||||
export interface AccordionItemProps
|
||||
extends React.HtmlHTMLAttributes<HTMLDetailsElement>,
|
||||
@@ -21,6 +20,7 @@ export interface AccordionItemProps
|
||||
iconName?: IconName
|
||||
icon?: ReactNode
|
||||
subtitle?: string
|
||||
showAsSubtitle?: boolean
|
||||
onOpen?: () => void
|
||||
}
|
||||
|
||||
@@ -29,10 +29,10 @@ export default function AccordionItem({
|
||||
icon,
|
||||
iconName,
|
||||
title,
|
||||
theme,
|
||||
variant,
|
||||
type,
|
||||
className,
|
||||
subtitle,
|
||||
showAsSubtitle = false,
|
||||
onOpen,
|
||||
}: AccordionItemProps) {
|
||||
const contentRef = useRef<HTMLDivElement>(null)
|
||||
@@ -71,36 +71,36 @@ export default function AccordionItem({
|
||||
}
|
||||
|
||||
return (
|
||||
<li className={accordionItemVariants({ className, variant, theme })}>
|
||||
<li className={accordionItemVariants({ className, type })}>
|
||||
<details ref={detailsRef} onToggle={toggleAccordion}>
|
||||
<summary className={styles.summary}>
|
||||
{IconComp}
|
||||
{variant === 'sidepeek' ? (
|
||||
<Subtitle
|
||||
className={styles.title}
|
||||
type="two"
|
||||
color="baseTextHighContrast"
|
||||
>
|
||||
{title}
|
||||
</Subtitle>
|
||||
{type === 'sidepeek' ? (
|
||||
<Typography variant="Title/Subtitle/md">
|
||||
<p className={styles.title}>{title}</p>
|
||||
</Typography>
|
||||
) : (
|
||||
<div className={styles.title}>
|
||||
{subtitle ? (
|
||||
<Subtitle type="two" color="baseTextHighContrast">
|
||||
{title}
|
||||
</Subtitle>
|
||||
{subtitle || showAsSubtitle ? (
|
||||
<Typography variant="Title/Subtitle/md">
|
||||
<p className={styles.title}>{title}</p>
|
||||
</Typography>
|
||||
) : (
|
||||
<Body textTransform="bold" color="baseTextHighContrast">
|
||||
{title}
|
||||
</Body>
|
||||
<Typography variant="Body/Paragraph/mdBold">
|
||||
<p className={styles.title}>{title}</p>
|
||||
</Typography>
|
||||
)}
|
||||
{subtitle && (
|
||||
<Typography variant="Body/Paragraph/mdBold">
|
||||
<p>{subtitle}</p>
|
||||
</Typography>
|
||||
)}
|
||||
{subtitle && <Body color="baseTextHighContrast">{subtitle}</Body>}
|
||||
</div>
|
||||
)}
|
||||
<MaterialIcon
|
||||
icon="keyboard_arrow_down"
|
||||
className={styles.chevron}
|
||||
color="Icon/Interactive/Default"
|
||||
color="CurrentColor"
|
||||
size={20}
|
||||
/>
|
||||
</summary>
|
||||
|
||||
@@ -4,18 +4,12 @@ import styles from './accordionItem.module.css'
|
||||
|
||||
export const accordionItemVariants = cva(styles.accordionItem, {
|
||||
variants: {
|
||||
variant: {
|
||||
type: {
|
||||
card: styles.card,
|
||||
sidepeek: styles.sidepeek,
|
||||
},
|
||||
theme: {
|
||||
default: styles.default,
|
||||
light: styles.light,
|
||||
subtle: styles.subtle,
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'card',
|
||||
theme: 'default',
|
||||
type: 'card',
|
||||
},
|
||||
})
|
||||
|
||||
@@ -1,25 +1,11 @@
|
||||
.accordion {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.accordion.card {
|
||||
border-radius: var(--Corner-radius-md);
|
||||
}
|
||||
&.card {
|
||||
border-radius: var(--Corner-radius-md);
|
||||
|
||||
.accordion.light {
|
||||
background-color: var(--Base-Surface-Primary-light-Normal);
|
||||
}
|
||||
.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;
|
||||
li:last-child {
|
||||
border-width: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { Children, cloneElement, isValidElement } from 'react'
|
||||
|
||||
import { accordionVariants } from './variants'
|
||||
|
||||
import type { VariantProps } from 'class-variance-authority'
|
||||
|
||||
import type { AccordionItemProps } from './AccordionItem'
|
||||
import { accordionVariants } from './variants'
|
||||
|
||||
interface AccordionProps
|
||||
extends React.HtmlHTMLAttributes<HTMLUListElement>,
|
||||
@@ -13,14 +12,13 @@ interface AccordionProps
|
||||
export default function Accordion({
|
||||
children,
|
||||
className,
|
||||
theme,
|
||||
variant,
|
||||
type,
|
||||
}: AccordionProps) {
|
||||
return (
|
||||
<ul className={accordionVariants({ className, variant, theme })}>
|
||||
<ul className={accordionVariants({ className, type })}>
|
||||
{Children.map(children, (child) => {
|
||||
if (isValidElement<AccordionItemProps>(child)) {
|
||||
return cloneElement(child, { variant, theme })
|
||||
return cloneElement(child, { type })
|
||||
} else {
|
||||
return child
|
||||
}
|
||||
|
||||
@@ -4,18 +4,12 @@ import styles from './accordion.module.css'
|
||||
|
||||
export const accordionVariants = cva(styles.accordion, {
|
||||
variants: {
|
||||
variant: {
|
||||
type: {
|
||||
card: styles.card,
|
||||
sidepeek: styles.sidepeek,
|
||||
},
|
||||
theme: {
|
||||
default: styles.default,
|
||||
light: styles.light,
|
||||
subtle: styles.subtle,
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'card',
|
||||
theme: 'default',
|
||||
type: 'card',
|
||||
},
|
||||
})
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { FacilityEnum } from '@scandic-hotels/common/constants/facilities'
|
||||
|
||||
import type { JSX } from 'react'
|
||||
import { IconName } from '../Icons/iconName'
|
||||
import { MaterialIconSetIconProps } from '../Icons/MaterialIcon'
|
||||
import { IconProps, NucleoIconProps } from '../Icons'
|
||||
import { IconByIconName } from '../Icons/IconByIconName'
|
||||
import { IconName } from '../Icons/iconName'
|
||||
import { MaterialIconSetIconProps } from '../Icons/MaterialIcon'
|
||||
|
||||
interface mapFacilityToIconProps {
|
||||
id: FacilityEnum
|
||||
@@ -299,4 +299,6 @@ const facilityToIconMap: Record<FacilityEnum, IconName> = {
|
||||
[FacilityEnum.WideEntrance]: IconName.StarFilled,
|
||||
[FacilityEnum.WideRestaurantEntrance]: 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/HotelFilter.graphql"
|
||||
|
||||
#import "../../Fragments/Blocks/Accordion.graphql"
|
||||
#import "../../Fragments/Blocks/Content.graphql"
|
||||
|
||||
@@ -82,6 +84,15 @@ query GetDestinationCityPage($locale: String!, $uid: String!) {
|
||||
...Accordion_DestinationCityPage
|
||||
...Content_DestinationCityPage
|
||||
}
|
||||
seo_filters {
|
||||
filterConnection {
|
||||
edges {
|
||||
node {
|
||||
...HotelFilter
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
system {
|
||||
...System
|
||||
created_at
|
||||
@@ -135,6 +146,15 @@ query GetDestinationCityPageRefs($locale: String!, $uid: String!) {
|
||||
...Accordion_DestinationCityPageRefs
|
||||
...Content_DestinationCityPageRefs
|
||||
}
|
||||
seo_filters {
|
||||
filterConnection {
|
||||
edges {
|
||||
node {
|
||||
...HotelFilterRef
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
system {
|
||||
...System
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#import "../../Fragments/System.graphql"
|
||||
|
||||
#import "../../Fragments/HotelFilter.graphql"
|
||||
|
||||
#import "../../Fragments/Blocks/Accordion.graphql"
|
||||
#import "../../Fragments/Blocks/Content.graphql"
|
||||
|
||||
@@ -77,6 +79,15 @@ query GetDestinationCountryPage($locale: String!, $uid: String!) {
|
||||
...Accordion_DestinationCountryPage
|
||||
...Content_DestinationCountryPage
|
||||
}
|
||||
seo_filters {
|
||||
filterConnection {
|
||||
edges {
|
||||
node {
|
||||
...HotelFilter
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
system {
|
||||
...System
|
||||
created_at
|
||||
@@ -117,6 +128,15 @@ query GetDestinationCountryPageRefs($locale: String!, $uid: String!) {
|
||||
...Accordion_DestinationCountryPageRefs
|
||||
...Content_DestinationCountryPageRefs
|
||||
}
|
||||
seo_filters {
|
||||
filterConnection {
|
||||
edges {
|
||||
node {
|
||||
...HotelFilterRef
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
system {
|
||||
...System
|
||||
}
|
||||
|
||||
@@ -11,6 +11,10 @@ import {
|
||||
accordionSchema,
|
||||
} from "../schemas/blocks/accordion"
|
||||
import { contentRefsSchema, contentSchema } from "../schemas/blocks/content"
|
||||
import {
|
||||
destinationFiltersRefsSchema,
|
||||
destinationFiltersSchema,
|
||||
} from "../schemas/destinationFilters"
|
||||
import { mapLocationSchema } from "../schemas/mapLocation"
|
||||
import {
|
||||
linkRefsUnionSchema,
|
||||
@@ -159,6 +163,7 @@ export const destinationCityPageSchema = z.object({
|
||||
})
|
||||
.nullish(),
|
||||
blocks: discriminatedUnionArray(blocksSchema.options).nullable(),
|
||||
seo_filters: destinationFiltersSchema,
|
||||
system: systemSchema.merge(
|
||||
z.object({
|
||||
created_at: z.string(),
|
||||
@@ -258,6 +263,7 @@ export const destinationCityPageRefsSchema = z.object({
|
||||
})
|
||||
.nullish(),
|
||||
blocks: discriminatedUnionArray(blocksRefsSchema.options).nullable(),
|
||||
seo_filters: destinationFiltersRefsSchema,
|
||||
system: systemSchema,
|
||||
}),
|
||||
})
|
||||
|
||||
@@ -11,6 +11,10 @@ import {
|
||||
accordionSchema,
|
||||
} from "../schemas/blocks/accordion"
|
||||
import { contentRefsSchema, contentSchema } from "../schemas/blocks/content"
|
||||
import {
|
||||
destinationFiltersRefsSchema,
|
||||
destinationFiltersSchema,
|
||||
} from "../schemas/destinationFilters"
|
||||
import { mapLocationSchema } from "../schemas/mapLocation"
|
||||
import {
|
||||
linkRefsUnionSchema,
|
||||
@@ -88,6 +92,7 @@ export const destinationCountryPageSchema = z.object({
|
||||
})
|
||||
.nullish(),
|
||||
blocks: discriminatedUnionArray(blocksSchema.options).nullable(),
|
||||
seo_filters: destinationFiltersSchema,
|
||||
system: systemSchema.merge(
|
||||
z.object({
|
||||
created_at: z.string(),
|
||||
@@ -160,6 +165,7 @@ export const destinationCountryPageRefsSchema = z.object({
|
||||
}),
|
||||
}),
|
||||
blocks: discriminatedUnionArray(blocksRefsSchema.options).nullable(),
|
||||
seo_filters: destinationFiltersRefsSchema,
|
||||
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