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:
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
.container {
|
||||
display: grid;
|
||||
gap: var(--Spacing-x2);
|
||||
}
|
||||
|
||||
.cityList {
|
||||
list-style: none;
|
||||
display: grid;
|
||||
gap: var(--Spacing-x2);
|
||||
}
|
||||
36
components/ContentType/DestinationPage/CityListing/index.tsx
Normal file
36
components/ContentType/DestinationPage/CityListing/index.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -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}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
.experienceList {
|
||||
list-style: none;
|
||||
display: flex;
|
||||
gap: var(--Spacing-x1);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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":
|
||||
Reference in New Issue
Block a user