Merged in feat/SW-1453-city-listing-on-country-page (pull request #1222)

feat(SW-1453): added city listing component

* feat(SW-1453): added city listing component


Approved-by: Christian Andolf
Approved-by: Fredrik Thorsson
This commit is contained in:
Erik Tiekstra
2025-01-29 10:09:51 +00:00
parent a7468cd958
commit ca42876eb8
25 changed files with 496 additions and 57 deletions

View File

@@ -0,0 +1,39 @@
.container {
background-color: var(--Base-Surface-Primary-light-Normal);
border: 1px solid var(--Base-Border-Subtle);
border-radius: var(--Corner-radius-Medium);
overflow: hidden;
}
.image {
width: 100%;
max-height: 200px;
object-fit: cover;
}
.content {
display: grid;
gap: var(--Spacing-x2);
padding: var(--Spacing-x2) var(--Spacing-x3);
}
@media screen and (min-width: 768px) {
.container {
display: grid;
grid-template-columns: minmax(250px, 350px) auto;
}
.image {
max-height: none;
height: 100%;
}
.ctaWrapper {
display: flex;
justify-content: flex-end;
}
.button {
width: min(100%, 200px);
}
}

View File

@@ -0,0 +1,61 @@
import Link from "next/link"
import Image from "@/components/Image"
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 ExperienceList from "../../ExperienceList"
import styles from "./cityListingItem.module.css"
import type { DestinationCityListItem } from "@/types/trpc/routers/contentstack/destinationCityPage"
interface CityListingItemProps {
city: DestinationCityListItem
}
export default async function CityListingItem({ city }: CityListingItemProps) {
const intl = await getIntl()
const firstImage = city.images[0]
return (
<article className={styles.container}>
<Image
src={firstImage.url}
alt={firstImage.meta.alt || firstImage.meta.caption || ""}
width={300}
height={200}
className={styles.image}
/>
<section className={styles.content}>
<Subtitle asChild>
<h3>{city.heading}</h3>
</Subtitle>
<ExperienceList experiences={city.experiences} />
<Body>{city.preamble}</Body>
<Divider variant="horizontal" color="primaryLightSubtle" />
<div className={styles.ctaWrapper}>
<Button
intent="tertiary"
theme="base"
size="small"
className={styles.button}
asChild
>
<Link href={city.url}>
{intl.formatMessage(
{ id: "Explore {city}" },
{ city: city.cityName }
)}
</Link>
</Button>
</div>
</section>
</article>
)
}

View File

@@ -0,0 +1,10 @@
.container {
display: grid;
gap: var(--Spacing-x2);
}
.cityList {
list-style: none;
display: grid;
gap: var(--Spacing-x2);
}

View File

@@ -0,0 +1,36 @@
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
import { getIntl } from "@/i18n"
import CityListingItem from "./CityListingItem"
import styles from "./cityListing.module.css"
import type { DestinationCityListItem } from "@/types/trpc/routers/contentstack/destinationCityPage"
interface CityListingProps {
cities: DestinationCityListItem[]
}
export default async function CityListing({ cities }: CityListingProps) {
const intl = await getIntl()
return (
<section className={styles.container}>
<div className={styles.listHeader}>
<Subtitle type="two">
{intl.formatMessage(
{ id: `{count} Locations` },
{ count: cities.length }
)}
</Subtitle>
</div>
<ul className={styles.cityList}>
{cities.map((city) => (
<li key={city.system.uid}>
<CityListingItem city={city} />
</li>
))}
</ul>
</section>
)
}

View File

@@ -4,17 +4,16 @@ import { getDestinationCityPage } from "@/lib/trpc/memoizedRequests"
import Breadcrumbs from "@/components/Breadcrumbs"
import BreadcrumbsSkeleton from "@/components/TempDesignSystem/Breadcrumbs/BreadcrumbsSkeleton"
import Chip from "@/components/TempDesignSystem/Chip"
import Body from "@/components/TempDesignSystem/Text/Body"
import Title from "@/components/TempDesignSystem/Text/Title"
import TrackingSDK from "@/components/TrackingSDK"
import { getIntl } from "@/i18n"
import ExperienceList from "../ExperienceList"
import SidebarContentWrapper from "../SidebarContentWrapper"
import DestinationPageSidePeek from "../Sidepeek"
import StaticMap from "../StaticMap"
import TopImages from "../TopImages"
import { mapExperiencesToListData } from "../utils"
import styles from "./destinationCityPage.module.css"
@@ -41,7 +40,6 @@ export default async function DestinationCityPage() {
sidepeek_content,
destination_settings,
} = destinationCityPage
const experiencesList = await mapExperiencesToListData(experiences)
return (
<>
@@ -60,16 +58,7 @@ export default async function DestinationCityPage() {
<SidebarContentWrapper>
<Title level="h2">{heading}</Title>
<Body color="uiTextMediumContrast">{preamble}</Body>
<ul className={styles.experienceList}>
{experiencesList.map(({ Icon, name }) => (
<li key={name}>
<Chip variant="tag">
<Icon width={20} height={20} />
{name}
</Chip>
</li>
))}
</ul>
<ExperienceList experiences={experiences} />
{has_sidepeek && (
<DestinationPageSidePeek
buttonText={sidepeek_button_text}

View File

@@ -16,7 +16,7 @@
.mainSection {
grid-area: mainSection;
padding-bottom: var(--Spacing-x7);
min-height: 500px; /* This is a temporary value because of no content atm */
max-width: var(--max-width-page);
}
.sidebar {

View File

@@ -4,17 +4,17 @@ import { getDestinationCountryPage } from "@/lib/trpc/memoizedRequests"
import Breadcrumbs from "@/components/Breadcrumbs"
import BreadcrumbsSkeleton from "@/components/TempDesignSystem/Breadcrumbs/BreadcrumbsSkeleton"
import Chip from "@/components/TempDesignSystem/Chip"
import Body from "@/components/TempDesignSystem/Text/Body"
import Title from "@/components/TempDesignSystem/Text/Title"
import TrackingSDK from "@/components/TrackingSDK"
import { getIntl } from "@/i18n"
import CityListing from "../CityListing"
import ExperienceList from "../ExperienceList"
import SidebarContentWrapper from "../SidebarContentWrapper"
import DestinationPageSidePeek from "../Sidepeek"
import StaticMap from "../StaticMap"
import TopImages from "../TopImages"
import { mapExperiencesToListData } from "../utils"
import styles from "./destinationCountryPage.module.css"
@@ -30,7 +30,7 @@ export default async function DestinationCountryPage() {
return null
}
const { tracking, destinationCountryPage } = pageData
const { tracking, destinationCountryPage, cities } = pageData
const {
images,
heading,
@@ -41,7 +41,6 @@ export default async function DestinationCountryPage() {
sidepeek_content,
destination_settings,
} = destinationCountryPage
const experiencesList = await mapExperiencesToListData(experiences)
return (
<>
@@ -53,23 +52,13 @@ export default async function DestinationCountryPage() {
<TopImages images={images} />
</header>
<main className={styles.mainSection}>
{/* TODO: Add city listing by cityIdentifier */}
{">>>> MAIN CONTENT <<<<"}
<CityListing cities={cities} />
</main>
<aside className={styles.sidebar}>
<SidebarContentWrapper>
<Title level="h2">{heading}</Title>
<Body color="uiTextMediumContrast">{preamble}</Body>
<ul className={styles.experienceList}>
{experiencesList.map(({ Icon, name }) => (
<li key={name}>
<Chip variant="tag">
<Icon width={20} height={20} />
{name}
</Chip>
</li>
))}
</ul>
<ExperienceList experiences={experiences} />
{has_sidepeek && (
<DestinationPageSidePeek
buttonText={sidepeek_button_text}

View File

@@ -0,0 +1,6 @@
.experienceList {
list-style: none;
display: flex;
gap: var(--Spacing-x1);
flex-wrap: wrap;
}

View File

@@ -0,0 +1,30 @@
import Chip from "@/components/TempDesignSystem/Chip"
import { getIntl } from "@/i18n"
import { mapExperiencesToListData } from "./utils"
import styles from "./experienceList.module.css"
interface ExperienceListProps {
experiences: string[]
}
export default async function ExperienceList({
experiences,
}: ExperienceListProps) {
const intl = await getIntl()
const experienceList = mapExperiencesToListData(experiences, intl)
return (
<ul className={styles.experienceList}>
{experienceList.map(({ Icon, name }) => (
<li key={name}>
<Chip variant="tag">
<Icon width={20} height={20} />
{name}
</Chip>
</li>
))}
</ul>
)
}

View File

@@ -7,17 +7,16 @@ import {
NightlifeIcon,
StarFilledIcon,
} from "@/components/Icons"
import { getIntl } from "@/i18n"
import type { FC } from "react"
import type { IntlShape } from "react-intl"
import type { IconProps } from "@/types/components/icon"
export async function mapExperiencesToListData(
experiences: string[]
): Promise<{ Icon: FC<IconProps>; name: string }[]> {
const intl = await getIntl()
export function mapExperiencesToListData(
experiences: string[],
intl: IntlShape
): { Icon: FC<IconProps>; name: string }[] {
return experiences.map((experience) => {
switch (experience) {
case "Hiking":