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,6 +26,7 @@ export default async function RestaurantSidebar({
return (
<aside className={styles.sidebar}>
{openingDetails.length ? (
<div className={styles.content}>
<Title level="h3" as="h4">
{intl.formatMessage({ id: "Opening hours" })}
@@ -39,13 +40,15 @@ export default async function RestaurantSidebar({
/>
))}
</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>
)}
{restaurant.menus.length ? (
<div className={styles.content}>
<Title level="h3" as="h4">
{intl.formatMessage({ id: "Menus" })}
@@ -66,6 +69,7 @@ export default async function RestaurantSidebar({
))}
</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,12 +72,19 @@ 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} />
</div>
{meetingBookingWidget}
{pageData.heroImage && (
<Hero
src={pageData.heroImage.src}
alt={pageData.heroImage.alt}
/>
)}
</div>
) : null}
</div>
<div className={styles.contentContainer}>
<main className={styles.mainContent}>

101
package-lock.json generated
View File

@@ -50,6 +50,7 @@
"graphql": "^16.8.1",
"graphql-request": "^6.1.0",
"graphql-tag": "^2.12.6",
"html-react-parser": "^5.2.2",
"ics": "^3.8.1",
"immer": "10.1.1",
"input-otp": "^1.4.2",
@@ -11958,11 +11959,11 @@
}
},
"node_modules/domutils": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz",
"integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==",
"dev": true,
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz",
"integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==",
"license": "BSD-2-Clause",
"dev":true,
"dependencies": {
"dom-serializer": "^2.0.0",
"domelementtype": "^2.3.0",
@@ -14054,6 +14055,16 @@
"react-is": "^16.7.0"
}
},
"node_modules/html-dom-parser": {
"version": "5.0.13",
"resolved": "https://registry.npmjs.org/html-dom-parser/-/html-dom-parser-5.0.13.tgz",
"integrity": "sha512-B7JonBuAfG32I7fDouUQEogBrz3jK9gAuN1r1AaXpED6dIhtg/JwiSRhjGL7aOJwRz3HU4efowCjQBaoXiREqg==",
"license": "MIT",
"dependencies": {
"domhandler": "5.0.3",
"htmlparser2": "10.0.0"
}
},
"node_modules/html-encoding-sniffer": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz",
@@ -14072,6 +14083,58 @@
"integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==",
"dev": true
},
"node_modules/html-react-parser": {
"version": "5.2.2",
"resolved": "https://registry.npmjs.org/html-react-parser/-/html-react-parser-5.2.2.tgz",
"integrity": "sha512-yA5012CJGSFWYZsgYzfr6HXJgDap38/AEP4ra8Cw+WHIi2ZRDXRX/QVYdumRf1P8zKyScKd6YOrWYvVEiPfGKg==",
"license": "MIT",
"dependencies": {
"domhandler": "5.0.3",
"html-dom-parser": "5.0.13",
"react-property": "2.0.2",
"style-to-js": "1.1.16"
},
"peerDependencies": {
"@types/react": "0.14 || 15 || 16 || 17 || 18 || 19",
"react": "0.14 || 15 || 16 || 17 || 18 || 19"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/htmlparser2": {
"version": "10.0.0",
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.0.0.tgz",
"integrity": "sha512-TwAZM+zE5Tq3lrEHvOlvwgj1XLWQCtaaibSN11Q+gGBAS7Y1uZSWwXXRe4iF6OXnaq1riyQAPFOBtYc77Mxq0g==",
"funding": [
"https://github.com/fb55/htmlparser2?sponsor=1",
{
"type": "github",
"url": "https://github.com/sponsors/fb55"
}
],
"license": "MIT",
"dependencies": {
"domelementtype": "^2.3.0",
"domhandler": "^5.0.3",
"domutils": "^3.2.1",
"entities": "^6.0.0"
}
},
"node_modules/htmlparser2/node_modules/entities": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/entities/-/entities-6.0.0.tgz",
"integrity": "sha512-aKstq2TDOndCn4diEyp9Uq/Flu2i1GlLkc6XIDQSDMuaFE3OPW5OphLCyQ5SpSJZTb4reN+kTcYru5yIfXoRPw==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=0.12"
},
"funding": {
"url": "https://github.com/fb55/entities?sponsor=1"
}
},
"node_modules/http-assert": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/http-assert/-/http-assert-1.5.0.tgz",
@@ -14386,6 +14449,12 @@
"node": ">=10"
}
},
"node_modules/inline-style-parser": {
"version": "0.2.4",
"resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.4.tgz",
"integrity": "sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==",
"license": "MIT"
},
"node_modules/input-otp": {
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/input-otp/-/input-otp-1.4.2.tgz",
@@ -19891,6 +19960,12 @@
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
},
"node_modules/react-property": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/react-property/-/react-property-2.0.2.tgz",
"integrity": "sha512-+PbtI3VuDV0l6CleQMsx2gtK0JZbZKbpdu5ynr+lbsuvtmgbNcS3VM0tuY2QjFNOcWxvXeHjDpy42RO+4U2rug==",
"license": "MIT"
},
"node_modules/react-remove-scroll": {
"version": "2.5.7",
"resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.5.7.tgz",
@@ -21449,6 +21524,24 @@
"integrity": "sha512-H2N9c26eXjzL/S/K+i/RHHcFanE74dptvvjM8iwzwbVcWY/zjBbgRqF3K0DY4+OD+uTTASTBvDoxPDaPN02D7g==",
"dev": true
},
"node_modules/style-to-js": {
"version": "1.1.16",
"resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.16.tgz",
"integrity": "sha512-/Q6ld50hKYPH3d/r6nr117TZkHR0w0kGGIVfpG9N6D8NymRPM9RqCUv4pRpJ62E5DqOYx2AFpbZMyCPnjQCnOw==",
"license": "MIT",
"dependencies": {
"style-to-object": "1.0.8"
}
},
"node_modules/style-to-object": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.8.tgz",
"integrity": "sha512-xT47I/Eo0rwJmaXC4oilDGDWLohVhR6o/xAQcPQN8q6QBuZVL8qMYL85kLmST5cPjAorwvqIA4qXTRQoYHaL6g==",
"license": "MIT",
"dependencies": {
"inline-style-parser": "0.2.4"
}
},
"node_modules/styled-jsx": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.1.tgz",

View File

@@ -65,6 +65,7 @@
"graphql": "^16.8.1",
"graphql-request": "^6.1.0",
"graphql-tag": "^2.12.6",
"html-react-parser": "^5.2.2",
"ics": "^3.8.1",
"immer": "10.1.1",
"input-otp": "^1.4.2",