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:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
22
components/ContentType/HotelSubpage/HtmlContent/utils.ts
Normal file
22
components/ContentType/HotelSubpage/HtmlContent/utils.ts
Normal 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",
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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}>
|
||||
|
||||
Reference in New Issue
Block a user