Merged in feat/SW-1234-hotel-subpages (pull request #1387)

Feat(SW-1234): Prettify hotel subpages

Approved-by: Erik Tiekstra
Approved-by: Fredrik Thorsson
This commit is contained in:
Matilda Landström
2025-02-24 12:44:00 +00:00
parent 76cd1945c4
commit 59950678f3
11 changed files with 376 additions and 71 deletions

View File

@@ -1,2 +1,32 @@
.htmlContent {
.ul,
.ol {
padding: var(--Spacing-x2) var(--Spacing-x0);
display: grid;
gap: var(--Spacing-x1);
margin-left: var(--Spacing-x2);
}
.ol > li::marker {
color: var(--Primary-Light-On-Surface-Accent);
}
.li {
margin-left: var(--Spacing-x3);
}
.li > p {
display: inline;
}
@media screen and (min-width: 768px) {
.ol:has(li:nth-last-child(n + 5)),
.ul:has(li:nth-last-child(n + 5)) {
grid-template-columns: 1fr 1fr;
grid-auto-flow: column;
}
}
.tableContainer {
max-width: 100%;
overflow-x: auto;
}

View File

@@ -1,16 +1,156 @@
import { ElementType } from "domelementtype"
import parse, { type DOMNode, Element, type Text } from "html-react-parser"
import Link from "@/components/TempDesignSystem/Link"
import Table from "@/components/TempDesignSystem/Table"
import Body from "@/components/TempDesignSystem/Text/Body"
import Title from "@/components/TempDesignSystem/Text/Title"
import { NodeNames } from "./utils"
import styles from "./htmlContent.module.css"
type Node = Element | Text
interface HtmlContentProps {
html: string
}
export default function HtmlContent({ html }: HtmlContentProps) {
return (
<div
className={styles.htmlContent}
dangerouslySetInnerHTML={{
__html: html,
}}
/>
)
const cleanedHtml = html.replace("<p></p>\n", "")
const parsedContent = parse(cleanedHtml, {
replace: (domNode: DOMNode) => {
if (domNode instanceof Element) {
return renderNode(domNode)
} else {
if (domNode.data === "\n") {
return <br />
}
}
},
})
return <div>{parsedContent}</div>
}
function renderChildren(node: Element) {
return node.children?.map((child) => renderNode(child as Element))
}
function renderNode(domNode: Node) {
if (domNode.type === ElementType.Tag) {
switch (domNode.name) {
case NodeNames.h1:
case NodeNames.h2:
case NodeNames.h3:
case NodeNames.h4:
case NodeNames.h5:
return <Title level={domNode.name}>{renderChildren(domNode)}</Title>
case NodeNames.br:
return <br />
case NodeNames.a:
console.log(domNode.attribs.target)
return (
<Link
color="peach80"
textDecoration="underline"
weight="bold"
target="_blank" //Always open in new tab
href={domNode.attribs.href}
>
{renderChildren(domNode)}
</Link>
)
case NodeNames.ul:
let numberOfRows: number | undefined
if (domNode.children.length > 4) {
const half = domNode.children.length / 2
numberOfRows = Math.ceil(half)
}
return (
<ul
className={styles.ul}
style={
numberOfRows
? {
gridTemplateRows: `repeat(${numberOfRows}, auto)`,
}
: {}
}
>
{renderChildren(domNode)}
</ul>
)
case NodeNames.ol:
let numberOfOlRows: number | undefined
if (domNode.children.length > 4) {
const half = domNode.children.length / 2
numberOfOlRows = Math.ceil(half)
}
return (
<ol
className={styles.ol}
style={
numberOfOlRows
? {
gridTemplateRows: `repeat(${numberOfOlRows}, auto)`,
}
: {}
}
>
{renderChildren(domNode)}
</ol>
)
case NodeNames.li:
return <li>{renderChildren(domNode)}</li>
case NodeNames.td:
return <Table.TD>{renderChildren(domNode)}</Table.TD>
case NodeNames.th:
return (
<Table.TH>
<Body color={"burgundy"} textTransform={"bold"}>
{renderChildren(domNode)}
</Body>
</Table.TH>
)
case NodeNames.tr:
return <Table.TR>{renderChildren(domNode)}</Table.TR>
case NodeNames.tbody:
return <Table.TBody>{renderChildren(domNode)}</Table.TBody>
case NodeNames.table:
return (
<div className={styles.tableContainer}>
<Table>{renderChildren(domNode)}</Table>
</div>
)
case NodeNames.p:
return (
domNode.children.length !== 0 && (
<Body>{renderChildren(domNode)}</Body>
)
)
case NodeNames.em:
return <em>{renderChildren(domNode)}</em>
case NodeNames.strong:
return <strong>{renderChildren(domNode)}</strong>
case NodeNames.span:
return <span>{renderChildren(domNode)}</span>
}
} else if (domNode.type === ElementType.Text) {
return domNode.data
}
return null
}

View File

@@ -0,0 +1,22 @@
export enum NodeNames {
a = "a",
h1 = "h1",
h2 = "h2",
h3 = "h3",
h4 = "h4",
h5 = "h5",
li = "li",
ol = "ol",
p = "p",
table = "table",
tbody = "tbody",
td = "td",
text = "text",
th = "th",
tr = "tr",
ul = "ul",
em = "em",
strong = "strong",
br = "br",
span = "span",
}

View File

@@ -26,13 +26,7 @@ export default async function MeetingsSidebar({
{intl.formatMessage({ id: "Contact us" })}
</Title>
<div>
<Link
href={`tel:${phoneNumber}`}
color="peach80"
textDecoration="underline"
>
{phoneNumber}
</Link>
<Link href={`tel:${phoneNumber}`}>{phoneNumber}</Link>
{country === Country.Finland ? (
<Body>
{intl.formatMessage({
@@ -40,7 +34,11 @@ export default async function MeetingsSidebar({
})}
</Body>
) : null}
<Body>{email} </Body>
{email && (
<Link textDecoration="underline" href={`mailto:${email}`}>
{email}
</Link>
)}
</div>
</aside>
)

View File

@@ -40,11 +40,7 @@ export default async function ParkingSidebar({ hotel }: HotelSidebarProps) {
<Title level="h3" as="h4">
{intl.formatMessage({ id: "Contact us" })}
</Title>
<Link
href={`tel:${hotel.contactInformation.phoneNumber}`}
color="peach80"
textDecoration="underline"
>
<Link href={`tel:${hotel.contactInformation.phoneNumber}`}>
{hotel.contactInformation.phoneNumber}
</Link>

View File

@@ -26,46 +26,50 @@ export default async function RestaurantSidebar({
return (
<aside className={styles.sidebar}>
<div className={styles.content}>
<Title level="h3" as="h4">
{intl.formatMessage({ id: "Opening hours" })}
</Title>
{openingDetails.map((details) => (
<OpeningHours
key={details.openingHours.name}
openingHours={details.openingHours}
alternateOpeningHours={details.alternateOpeningHours}
heading={details.openingHours.name}
/>
))}
</div>
{openingDetails.length ? (
<div className={styles.content}>
<Title level="h3" as="h4">
{intl.formatMessage({ id: "Opening hours" })}
</Title>
{openingDetails.map((details) => (
<OpeningHours
key={details.openingHours.name}
openingHours={details.openingHours}
alternateOpeningHours={details.alternateOpeningHours}
heading={details.openingHours.name}
/>
))}
</div>
) : null}
{bookTableUrl && (
<Button intent="primary" theme="base" asChild>
<Link href={bookTableUrl}>
<a href={bookTableUrl}>
{intl.formatMessage({ id: "Book a table online" })}
</Link>
</a>
</Button>
)}
<div className={styles.content}>
<Title level="h3" as="h4">
{intl.formatMessage({ id: "Menus" })}
</Title>
<ul className={styles.menuList}>
{restaurant.menus.map(({ name, url }) => (
<li key={name}>
<Link
href={url}
color="baseTextMediumContrast"
textDecoration="underline"
variant="icon"
>
{name}
<OpenInNewSmallIcon />
</Link>
</li>
))}
</ul>
</div>
{restaurant.menus.length ? (
<div className={styles.content}>
<Title level="h3" as="h4">
{intl.formatMessage({ id: "Menus" })}
</Title>
<ul className={styles.menuList}>
{restaurant.menus.map(({ name, url }) => (
<li key={name}>
<Link
href={url}
color="baseTextMediumContrast"
textDecoration="underline"
variant="icon"
>
{name}
<OpenInNewSmallIcon />
</Link>
</li>
))}
</ul>
</div>
) : null}
<div className={styles.content}>
<Title level="h3" as="h4">
{intl.formatMessage({ id: "Address" })}
@@ -82,9 +86,13 @@ export default async function RestaurantSidebar({
</Title>
<div>
{phoneNumber && (
<Body color="uiTextHighContrast">{phoneNumber}</Body>
<Link href={`tel:${phoneNumber}`}>{phoneNumber}</Link>
)}
{email && (
<Link textDecoration="underline" href={`mailto:${email}`}>
{email}
</Link>
)}
{email && <Body color="uiTextHighContrast">{email}</Body>}
</div>
</div>
)}

View File

@@ -69,11 +69,7 @@ export default async function WellnessSidebar({ hotel }: WellnessSidebarProps) {
<Title level="h3" as="h4">
{intl.formatMessage({ id: "Contact us" })}
</Title>
<Link
href={`tel:${hotel.contactInformation.phoneNumber}`}
color="peach80"
textDecoration="underline"
>
<Link href={`tel:${hotel.contactInformation.phoneNumber}`}>
{hotel.contactInformation.phoneNumber}
</Link>
</aside>

View File

@@ -13,6 +13,8 @@
width: 100%;
max-width: var(--max-width-content);
margin: 0 auto;
display: grid;
gap: var(--Spacing-x4);
}
.contentContainer {
@@ -41,6 +43,12 @@
grid-column: 1;
}
.meetingBookingWidget {
padding: var(--Spacing-x4);
background-color: var(--Base-Surface-Primary-dark-Normal);
text-align: center;
}
@media (min-width: 1367px) {
.contentContainer {
grid-template-columns: var(--max-width-text-block) 1fr;

View File

@@ -56,6 +56,12 @@ export default async function HotelSubpage({
meetingRooms = await getMeetingRooms({ hotelId: hotelId, language: lang })
}
const meetingBookingWidget = meetingRooms ? (
<div className={styles.meetingBookingWidget}>
Booking Widget Placeholder
</div>
) : null
return (
<>
<section className={styles.hotelSubpage}>
@@ -66,11 +72,18 @@ export default async function HotelSubpage({
subpageTitle={pageData.heading}
/>
</Suspense>
{pageData.heroImage && (
{pageData.heroImage || meetingBookingWidget ? (
<div className={styles.heroWrapper}>
<Hero src={pageData.heroImage.src} alt={pageData.heroImage.alt} />
{meetingBookingWidget}
{pageData.heroImage && (
<Hero
src={pageData.heroImage.src}
alt={pageData.heroImage.alt}
/>
)}
</div>
)}
) : null}
</div>
<div className={styles.contentContainer}>