Merged in monorepo-step-1 (pull request #1080)
Migrate to a monorepo setup - step 1 * Move web to subfolder /apps/scandic-web * Yarn + transitive deps - Move to yarn - design-system package removed for now since yarn doesn't support the parameter for token (ie project currently broken) - Add missing transitive dependencies as Yarn otherwise prevents these imports - VS Code doesn't pick up TS path aliases unless you open /apps/scandic-web instead of root (will be fixed with monorepo) * Pin framer-motion to temporarily fix typing issue https://github.com/adobe/react-spectrum/issues/7494 * Pin zod to avoid typ error There seems to have been a breaking change in the types returned by zod where error is now returned as undefined instead of missing in the type. We should just handle this but to avoid merge conflicts just pin the dependency for now. * Pin react-intl version Pin version of react-intl to avoid tiny type issue where formatMessage does not accept a generic any more. This will be fixed in a future commit, but to avoid merge conflicts just pin for now. * Pin typescript version Temporarily pin version as newer versions as stricter and results in a type error. Will be fixed in future commit after merge. * Setup workspaces * Add design-system as a monorepo package * Remove unused env var DESIGN_SYSTEM_ACCESS_TOKEN * Fix husky for monorepo setup * Update netlify.toml * Add lint script to root package.json * Add stub readme * Fix react-intl formatMessage types * Test netlify.toml in root * Remove root toml * Update netlify.toml publish path * Remove package-lock.json * Update build for branch/preview builds Approved-by: Linus Flood
This commit is contained in:
committed by
Linus Flood
parent
667cab6fb6
commit
80100e7631
9
apps/scandic-web/components/Current/AdobeScript.tsx
Normal file
9
apps/scandic-web/components/Current/AdobeScript.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import Script from "next/script"
|
||||
|
||||
import { env } from "@/env/server"
|
||||
|
||||
export default function AdobeScript() {
|
||||
return env.ADOBE_SCRIPT_SRC ? (
|
||||
<Script data-cookieconsent="statistics" src={env.ADOBE_SCRIPT_SRC} />
|
||||
) : null
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
import { Lang } from "@/constants/languages"
|
||||
|
||||
import styles from "./contact.module.css"
|
||||
|
||||
import { type ContactNode, Section } from "@/types/requests/asides/contact"
|
||||
|
||||
export default function Contact({ sections, system: { locale } }: ContactNode) {
|
||||
if (!sections.length) {
|
||||
return null
|
||||
}
|
||||
|
||||
const visitingAddressMessage = getVisitingAddressMessage(locale)
|
||||
return (
|
||||
<section>
|
||||
{sections.map((section, idx) => {
|
||||
switch (section.__typename) {
|
||||
case Section.ContactBlockSectionsExtraInfo:
|
||||
return <p>{section.extra_info.text}</p>
|
||||
case Section.ContactBlockSectionsMailingAddress:
|
||||
return (
|
||||
<p key={`section-mail-${idx}`}>
|
||||
{section.mailing_address.name}
|
||||
<br />
|
||||
{section.mailing_address.street}
|
||||
<br />
|
||||
{section.mailing_address.zip} {section.mailing_address.city}
|
||||
<br />
|
||||
{section.mailing_address.country}
|
||||
</p>
|
||||
)
|
||||
case Section.ContactBlockSectionsPhone:
|
||||
return (
|
||||
<div
|
||||
className={styles.highlightBlock}
|
||||
key={`section-phone-${idx}`}
|
||||
>
|
||||
<h3>{section.phone.title}</h3>
|
||||
<div className={styles.phoneContainer}>
|
||||
<svg
|
||||
focusable="false"
|
||||
className={styles.phoneIcon}
|
||||
viewBox="0 0 32 32"
|
||||
>
|
||||
<use
|
||||
xmlnsXlink="http://www.w3.org/1999/xlink"
|
||||
xlinkHref="/_static/img/icons/sprites.svg#icon-phone"
|
||||
></use>
|
||||
</svg>
|
||||
|
||||
<a
|
||||
href={`tel:+${section.phone.number}`}
|
||||
itemProp="telephone"
|
||||
className={styles.phoneNumberLink}
|
||||
>
|
||||
+{section.phone.number}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
case Section.ContactBlockSectionsTitle:
|
||||
return (
|
||||
<h2 className={styles.heading} key={`section-heading-${idx}`}>
|
||||
{section.title.text}
|
||||
</h2>
|
||||
)
|
||||
case Section.ContactBlockSectionsVisitingAddress:
|
||||
return (
|
||||
<p key={`section-visiting-address-${idx}`}>
|
||||
{visitingAddressMessage}: {section.visiting_address.street}{" "}
|
||||
</p>
|
||||
)
|
||||
default:
|
||||
return null
|
||||
}
|
||||
})}
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
function getVisitingAddressMessage(lang: Lang) {
|
||||
switch (lang) {
|
||||
case Lang.sv:
|
||||
return "Besöksadress"
|
||||
case Lang.en:
|
||||
return "Visiting address"
|
||||
case Lang.da:
|
||||
return "Besøgsadresse"
|
||||
case Lang.de:
|
||||
return "Besuchsadresse"
|
||||
case Lang.fi:
|
||||
return "Vierailuosoite"
|
||||
case Lang.no:
|
||||
return "Besøksadresse"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
.highlightBlock {
|
||||
padding: 10px 10px 15px;
|
||||
background: #fff;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.heading {
|
||||
font-family: BrandonText-Bold, Arial, Helvetica, sans-serif;
|
||||
font-size: 1.375rem;
|
||||
line-height: 1.1em;
|
||||
text-transform: uppercase;
|
||||
font-weight: 400;
|
||||
color: #483729;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.phoneContainer {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.phoneNumberLink {
|
||||
line-height: 1.1em;
|
||||
font-family: Helvetica, Arial, sans-serif;
|
||||
font-weight: 400;
|
||||
text-transform: none;
|
||||
font-size: 1.125rem;
|
||||
color: #00838e;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.phoneIcon {
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
fill: #00838e;
|
||||
}
|
||||
|
||||
.p {
|
||||
color: #333;
|
||||
line-height: 1.3;
|
||||
margin-bottom: 1em;
|
||||
padding-top: 7px;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.phoneNumberLink:active,
|
||||
.phoneNumberLink:hover {
|
||||
outline: 0;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1367px) {
|
||||
.heading {
|
||||
font-size: 1.625rem;
|
||||
}
|
||||
|
||||
.phoneNumberLink {
|
||||
line-height: 1.1em;
|
||||
font-size: 1.375rem;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import Contact from "./Contact"
|
||||
|
||||
import type { ContactsProps } from "@/types/components/current/asides/contact"
|
||||
|
||||
export default function Contacts({ contacts }: ContactsProps) {
|
||||
return contacts.map((contact) => (
|
||||
<Contact key={contact.node.system.uid} {...contact.node} />
|
||||
))
|
||||
}
|
||||
84
apps/scandic-web/components/Current/Aside/Puff/index.tsx
Normal file
84
apps/scandic-web/components/Current/Aside/Puff/index.tsx
Normal file
@@ -0,0 +1,84 @@
|
||||
"use client"
|
||||
import { useRouter } from "next/navigation"
|
||||
|
||||
import { Button } from "@scandic-hotels/design-system/current"
|
||||
|
||||
import { renderOptions as currentRenderOptions } from "@/components/Current/currentRenderOptions"
|
||||
import DeprecatedJsonToHtml from "@/components/DeprecatedJsonToHtml"
|
||||
import Image from "@/components/Image"
|
||||
|
||||
import { renderOptions } from "./renderOptions"
|
||||
|
||||
import styles from "./puff.module.css"
|
||||
|
||||
import type { PuffProps } from "@/types/components/current/asides/puff"
|
||||
import { PuffStyleEnum } from "@/types/requests/puff"
|
||||
|
||||
export default function Puff({
|
||||
imageConnection,
|
||||
link,
|
||||
text,
|
||||
puff_style,
|
||||
title,
|
||||
}: PuffProps) {
|
||||
const router = useRouter()
|
||||
|
||||
switch (puff_style) {
|
||||
case PuffStyleEnum.button:
|
||||
function onClick() {
|
||||
router.push(link.href)
|
||||
}
|
||||
|
||||
return (
|
||||
<article>
|
||||
{imageConnection.edges.map((image) => (
|
||||
<Image
|
||||
alt={image.node.title}
|
||||
className={styles.image}
|
||||
height={image.node.dimension.height}
|
||||
key={image.node.system.uid}
|
||||
src={image.node.url}
|
||||
width={image.node.dimension.width}
|
||||
/>
|
||||
))}
|
||||
<section className={styles.content}>
|
||||
<DeprecatedJsonToHtml
|
||||
embeds={[]}
|
||||
nodes={text.json.children}
|
||||
renderOptions={{ ...currentRenderOptions, ...renderOptions }}
|
||||
/>
|
||||
<div>
|
||||
<Button onPress={onClick}>{link.title || title}</Button>
|
||||
</div>
|
||||
</section>
|
||||
</article>
|
||||
)
|
||||
case PuffStyleEnum.default:
|
||||
return (
|
||||
<a className={styles.link} href={link.href}>
|
||||
<article>
|
||||
{imageConnection.edges.map((image) => (
|
||||
<Image
|
||||
alt={image.node.title}
|
||||
className={styles.image}
|
||||
height={image.node.dimension.height}
|
||||
key={image.node.system.uid}
|
||||
src={image.node.url}
|
||||
width={image.node.dimension.width}
|
||||
/>
|
||||
))}
|
||||
<section className={styles.content}>
|
||||
<header>
|
||||
<h3 className={styles.heading}>{title}</h3>
|
||||
</header>
|
||||
<DeprecatedJsonToHtml
|
||||
embeds={[]}
|
||||
nodes={text.json.children}
|
||||
renderOptions={{ ...currentRenderOptions, ...renderOptions }}
|
||||
/>
|
||||
</section>
|
||||
</article>
|
||||
</a>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
.link {
|
||||
display: inline-block;
|
||||
transition: 200ms ease;
|
||||
}
|
||||
|
||||
.link:hover {
|
||||
text-decoration: none;
|
||||
transform: scale(1.01);
|
||||
}
|
||||
|
||||
.image {
|
||||
height: auto;
|
||||
object-fit: contain;
|
||||
object-position: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.content {
|
||||
display: grid;
|
||||
padding: 10px;
|
||||
background-color: #fff;
|
||||
gap: 27px;
|
||||
}
|
||||
|
||||
.heading {
|
||||
color: #00838e;
|
||||
font-family: Helvetica, Arial, sans-serif;
|
||||
font-weight: 400;
|
||||
line-height: normal;
|
||||
margin-bottom: 0;
|
||||
text-decoration: none;
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
.link:hover .heading {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.p {
|
||||
color: #333;
|
||||
line-height: 1.3;
|
||||
margin-bottom: 0;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
.content {
|
||||
padding: 20px 0px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1367px) {
|
||||
.heading {
|
||||
font-size: 1.375rem;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import styles from "./puff.module.css"
|
||||
|
||||
import type { EmbedByUid } from "@/types/components/deprecatedjsontohtml"
|
||||
import { RTETypeEnum } from "@/types/rte/enums"
|
||||
import type { RTEDefaultNode, RTENext } from "@/types/rte/node"
|
||||
import type { RenderOptions } from "@/types/rte/option"
|
||||
|
||||
export const renderOptions: RenderOptions = {
|
||||
[RTETypeEnum.p]: (
|
||||
node: RTEDefaultNode,
|
||||
embeds: EmbedByUid,
|
||||
next: RTENext,
|
||||
fullRenderOptions: RenderOptions
|
||||
) => {
|
||||
return (
|
||||
<p key={node.uid} className={styles.p}>
|
||||
{next(node.children, embeds, fullRenderOptions)}
|
||||
</p>
|
||||
)
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
.wrapper {
|
||||
padding: 20px 10px 5px;
|
||||
}
|
||||
34
apps/scandic-web/components/Current/Aside/index.tsx
Normal file
34
apps/scandic-web/components/Current/Aside/index.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import Contacts from "./Contacts"
|
||||
import Puff from "./Puff"
|
||||
|
||||
import styles from "./aside.module.css"
|
||||
|
||||
import type { AsideProps } from "@/types/components/current/aside"
|
||||
import { AsideTypenameEnum } from "@/types/requests/utils/typename"
|
||||
|
||||
export default function Aside({ blocks }: AsideProps) {
|
||||
if (!blocks?.length) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<aside className={styles.wrapper}>
|
||||
{blocks.map((block, idx) => {
|
||||
const type = block.__typename
|
||||
switch (type) {
|
||||
case AsideTypenameEnum.CurrentBlocksPageAsideContact:
|
||||
return (
|
||||
<Contacts
|
||||
contacts={block.contact.contactConnection.edges}
|
||||
key={`block-${idx}`}
|
||||
/>
|
||||
)
|
||||
case AsideTypenameEnum.CurrentBlocksPageAsidePuff:
|
||||
return <Puff key={`block-${idx}`} {...block.puff} />
|
||||
default:
|
||||
return null
|
||||
}
|
||||
})}
|
||||
</aside>
|
||||
)
|
||||
}
|
||||
68
apps/scandic-web/components/Current/Blocks/List/ListItem.tsx
Normal file
68
apps/scandic-web/components/Current/Blocks/List/ListItem.tsx
Normal file
@@ -0,0 +1,68 @@
|
||||
import { cva } from "class-variance-authority"
|
||||
|
||||
import styles from "./list.module.css"
|
||||
|
||||
import { BlockListItemsEnum, type ListItem } from "@/types/requests/blocks/list"
|
||||
|
||||
const config = {
|
||||
variants: {
|
||||
type: {
|
||||
default: styles.disc,
|
||||
checkmark: styles.checkmark,
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
type: "default",
|
||||
},
|
||||
} as const
|
||||
|
||||
const listItemStyle = cva(styles.listItem, config)
|
||||
|
||||
export default function ListItem({ listItem }: { listItem: ListItem }) {
|
||||
const typeName = listItem.__typename
|
||||
|
||||
switch (typeName) {
|
||||
case BlockListItemsEnum.CurrentBlocksPageBlocksListBlockListItemsListItem:
|
||||
return (
|
||||
<li
|
||||
className={listItemStyle({
|
||||
type: listItem.list_item.list_item_style,
|
||||
})}
|
||||
>
|
||||
{listItem.list_item.subtitle ? (
|
||||
<>
|
||||
<strong>{listItem.list_item.title}</strong>
|
||||
<br />
|
||||
<span>{listItem.list_item.subtitle}</span>
|
||||
</>
|
||||
) : (
|
||||
listItem.list_item.title
|
||||
)}
|
||||
</li>
|
||||
)
|
||||
case BlockListItemsEnum.CurrentBlocksPageBlocksListBlockListItemsListItemExternalLink:
|
||||
return (
|
||||
<li
|
||||
className={listItemStyle({
|
||||
type: listItem.list_item_external_link.list_item_style,
|
||||
})}
|
||||
>
|
||||
<a
|
||||
className={styles.link}
|
||||
href={listItem.list_item_external_link.link.href}
|
||||
>
|
||||
<strong>{listItem.list_item_external_link.link.title}</strong>
|
||||
</a>
|
||||
{listItem.list_item_external_link.subtitle && (
|
||||
<span>
|
||||
<br />
|
||||
{listItem.list_item_external_link.subtitle}
|
||||
</span>
|
||||
)}
|
||||
</li>
|
||||
)
|
||||
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}
|
||||
18
apps/scandic-web/components/Current/Blocks/List/index.tsx
Normal file
18
apps/scandic-web/components/Current/Blocks/List/index.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import ListItem from "./ListItem"
|
||||
|
||||
import styles from "./list.module.css"
|
||||
|
||||
import type { ListProps } from "@/types/requests/blocks/list"
|
||||
|
||||
export default function List({ list }: ListProps) {
|
||||
return (
|
||||
<>
|
||||
{list.title ? <h2 className={styles.title}>{list.title}</h2> : null}
|
||||
<ul className={styles.ul}>
|
||||
{list.list_items.map((item, i) => (
|
||||
<ListItem listItem={item} key={`list-item-${i}`} />
|
||||
))}
|
||||
</ul>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
.title {
|
||||
color: #483729;
|
||||
font-family: BrandonText-Bold, Arial, Helvetica, sans-serif;
|
||||
font-size: 1.375rem;
|
||||
font-weight: 400;
|
||||
line-height: 1.1em;
|
||||
margin-bottom: 1rem;
|
||||
margin-top: 2rem;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.title:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.ul {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.listItem {
|
||||
margin-bottom: 8px;
|
||||
padding-left: 1.3em;
|
||||
}
|
||||
|
||||
.checkmark {
|
||||
padding-left: 1.6em;
|
||||
}
|
||||
|
||||
.checkmark::before,
|
||||
.disc::before {
|
||||
position: relative;
|
||||
top: 4px;
|
||||
display: inline-block;
|
||||
height: 0px;
|
||||
width: 0px;
|
||||
}
|
||||
|
||||
.checkmark::before {
|
||||
content: url("/_static/img/bullet-list-tick-birch-v2.svg");
|
||||
transform: scale(0.9);
|
||||
left: -1.2em;
|
||||
}
|
||||
|
||||
.disc::before {
|
||||
content: "•";
|
||||
color: rgb(157, 160, 161);
|
||||
font-size: 26px;
|
||||
left: -0.7em;
|
||||
}
|
||||
|
||||
.link {
|
||||
border-bottom: 1px dotted #00838e;
|
||||
color: #00838e;
|
||||
text-decoration: none;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.link:active,
|
||||
.link:hover {
|
||||
text-decoration: underline;
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
.link:hover {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1367px) {
|
||||
.title {
|
||||
font-size: 1.625rem;
|
||||
}
|
||||
}
|
||||
3
apps/scandic-web/components/Current/Blocks/Puffs.tsx
Normal file
3
apps/scandic-web/components/Current/Blocks/Puffs.tsx
Normal file
@@ -0,0 +1,3 @@
|
||||
export default function Puffs() {
|
||||
return <></>
|
||||
}
|
||||
15
apps/scandic-web/components/Current/Blocks/Text.tsx
Normal file
15
apps/scandic-web/components/Current/Blocks/Text.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import DeprecatedJsonToHtml from "@/components/DeprecatedJsonToHtml"
|
||||
|
||||
import { renderOptions } from "./../currentRenderOptions"
|
||||
|
||||
import type { TextProps } from "@/types/components/current/blocks/text"
|
||||
|
||||
export default function Text({ text }: TextProps) {
|
||||
return (
|
||||
<DeprecatedJsonToHtml
|
||||
embeds={text.content.embedded_itemsConnection.edges}
|
||||
nodes={text.content.json.children}
|
||||
renderOptions={renderOptions}
|
||||
/>
|
||||
)
|
||||
}
|
||||
10
apps/scandic-web/components/Current/Blocks/blocks.module.css
Normal file
10
apps/scandic-web/components/Current/Blocks/blocks.module.css
Normal file
@@ -0,0 +1,10 @@
|
||||
.wrapper {
|
||||
background-color: #fff;
|
||||
padding: 20px 10px 5px;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1367px) {
|
||||
.wrapper {
|
||||
padding: 20px 0 0;
|
||||
}
|
||||
}
|
||||
34
apps/scandic-web/components/Current/Blocks/index.tsx
Normal file
34
apps/scandic-web/components/Current/Blocks/index.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import List from "./List"
|
||||
import Puffs from "./Puffs"
|
||||
import Text from "./Text"
|
||||
|
||||
import styles from "./blocks.module.css"
|
||||
|
||||
import type { BlocksProps } from "@/types/components/current/blocks"
|
||||
import { BlocksTypenameEnum } from "@/types/requests/utils/typename"
|
||||
|
||||
export default function Blocks({ blocks }: BlocksProps) {
|
||||
if (!blocks?.length) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<section className={styles.wrapper}>
|
||||
{blocks.map((block, idx) => {
|
||||
const type = block.__typename
|
||||
switch (type) {
|
||||
case BlocksTypenameEnum.CurrentBlocksPageBlocksList:
|
||||
return <List key={`${block.__typename}-${idx}`} {...block} />
|
||||
case BlocksTypenameEnum.CurrentBlocksPageBlocksPuffs:
|
||||
return <Puffs key={`${block.__typename}-${idx}`} {...block} />
|
||||
case BlocksTypenameEnum.CurrentBlocksPageBlocksText:
|
||||
return <Text key={`${block.__typename}-${idx}`} {...block} />
|
||||
default:
|
||||
console.log(`Unknown type: (${type})`)
|
||||
return null
|
||||
}
|
||||
})}
|
||||
<div id="mainarea_personalized"></div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
.wrapper {
|
||||
position: relative;
|
||||
background: #f3f2f1;
|
||||
display: block;
|
||||
padding-bottom: 50px;
|
||||
width: 100%;
|
||||
}
|
||||
47
apps/scandic-web/components/Current/ContentPage/index.tsx
Normal file
47
apps/scandic-web/components/Current/ContentPage/index.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
import Aside from "@/components/Current/Aside"
|
||||
import Blocks from "@/components/Current/Blocks"
|
||||
import Hero from "@/components/Current/Hero"
|
||||
import Preamble from "@/components/Current/Preamble"
|
||||
import Section from "@/components/Current/Section"
|
||||
import SubnavMobile from "@/components/Current/SubnavMobile"
|
||||
|
||||
import styles from "./contentPage.module.css"
|
||||
|
||||
import type { ContentPageProps } from "@/types/components/current/contentPage"
|
||||
|
||||
export default function ContentPage({ data }: ContentPageProps) {
|
||||
const page = data.all_current_blocks_page.items[0]
|
||||
const images = page.hero?.imagesConnection
|
||||
const breadcrumbs = page.breadcrumbs.parents
|
||||
const parent = breadcrumbs.at(-1)
|
||||
|
||||
return (
|
||||
<>
|
||||
{images?.totalCount ? <Hero images={images.edges} /> : null}
|
||||
<main className={styles.wrapper} id="maincontent" role="main">
|
||||
<input
|
||||
id="lbl-personalized-areas"
|
||||
name="lbl-personalized-areas"
|
||||
type="hidden"
|
||||
value=""
|
||||
/>
|
||||
<SubnavMobile
|
||||
breadcrumbs={breadcrumbs}
|
||||
parent={parent}
|
||||
title={page.breadcrumbs.title}
|
||||
/>
|
||||
<Preamble
|
||||
breadcrumbs={breadcrumbs}
|
||||
breadcrumbParent={parent}
|
||||
breadcrumbTitle={page.breadcrumbs.title}
|
||||
preamble={page.preamble}
|
||||
title={page.title}
|
||||
/>
|
||||
<Section>
|
||||
<Blocks blocks={page.blocks} />
|
||||
<Aside blocks={page.aside} />
|
||||
</Section>
|
||||
</main>
|
||||
</>
|
||||
)
|
||||
}
|
||||
26
apps/scandic-web/components/Current/Footer/Navigation.tsx
Normal file
26
apps/scandic-web/components/Current/Footer/Navigation.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import styles from "./navigation.module.css"
|
||||
|
||||
import type { FooterNavigationProps } from "@/types/components/current/footer"
|
||||
|
||||
export default function Navigation({ linkGroups }: FooterNavigationProps) {
|
||||
return (
|
||||
<ul className={styles.container}>
|
||||
{linkGroups.map((group) => (
|
||||
<li className={styles.section} key={group.title}>
|
||||
<div className={styles.linkList}>
|
||||
<h3 className={styles.linkListTitle}>{group.title}</h3>
|
||||
<ul className={styles.listPages}>
|
||||
{group.links.map((link) => (
|
||||
<li className={styles.li} key={link.href}>
|
||||
<a className={styles.listLink} href={link.href}>
|
||||
{link.title}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)
|
||||
}
|
||||
166
apps/scandic-web/components/Current/Footer/footer.module.css
Normal file
166
apps/scandic-web/components/Current/Footer/footer.module.css
Normal file
@@ -0,0 +1,166 @@
|
||||
.container {
|
||||
position: relative;
|
||||
background: #000;
|
||||
color: #fff;
|
||||
z-index: 9;
|
||||
overflow: hidden;
|
||||
border-top: 1px solid #868686;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
text-rendering: optimizeLegibility;
|
||||
}
|
||||
|
||||
.container::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: -10px;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 10px;
|
||||
z-index: 3;
|
||||
-webkit-box-shadow: rgba(0, 0, 0, 0.85) 0 0 10px;
|
||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.85);
|
||||
}
|
||||
|
||||
.content {
|
||||
position: relative;
|
||||
box-sizing: content-box;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 30px 10px 0 10px;
|
||||
}
|
||||
|
||||
.contentHeading {
|
||||
text-align: center;
|
||||
color: #fff;
|
||||
font-family: BrandonText-Bold, Arial, Helvetica, sans-serif;
|
||||
font-size: 22px;
|
||||
line-height: 17.6px;
|
||||
text-transform: uppercase;
|
||||
font-weight: 400;
|
||||
color: #483729;
|
||||
margin-bottom: 16px;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.hiddenAccessible {
|
||||
display: block;
|
||||
position: absolute;
|
||||
left: -100000px;
|
||||
top: auto;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.footerSections {
|
||||
display: block;
|
||||
padding: 0;
|
||||
margin: -10px;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.footerLink {
|
||||
clear: both;
|
||||
padding: 0 0 3px;
|
||||
}
|
||||
|
||||
.footerLinkHeader {
|
||||
display: none;
|
||||
padding: 0 20px;
|
||||
border-bottom: 1px solid #e3e0db;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.contentBottom {
|
||||
max-width: 700px;
|
||||
margin: 0 auto;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.contentBottomTitle {
|
||||
margin: 16px 0;
|
||||
font-weight: 700;
|
||||
font-size: 16px;
|
||||
line-height: 22.4px;
|
||||
font-family: Helvetica, Arial, sans-serif;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.footerAboutText {
|
||||
margin-bottom: 16px;
|
||||
line-height: 22.4px;
|
||||
font-family: Helvetica, Arial, sans-serif;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.appDownloadTable {
|
||||
height: 62px;
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.tableRow {
|
||||
height: 62px;
|
||||
}
|
||||
|
||||
.tableData {
|
||||
width: 50px;
|
||||
height: 62px;
|
||||
padding: 1px;
|
||||
}
|
||||
|
||||
.sectionContainer {
|
||||
text-align: center;
|
||||
margin-top: 32px;
|
||||
margin-bottom: 16px;
|
||||
padding-bottom: 3px;
|
||||
}
|
||||
|
||||
.sectionTitle {
|
||||
font-family: Helvetica, Arial, sans-serif;
|
||||
font-weight: 400;
|
||||
line-height: normal;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
margin: 16px 0;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.socialMediaIconsContainer {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding-right: 4px;
|
||||
padding-bottom: 4px;
|
||||
}
|
||||
|
||||
.socialMediaIconLink {
|
||||
border-bottom: 3px solid transparent;
|
||||
}
|
||||
|
||||
.socialMediaIcon {
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
overflow: hidden;
|
||||
display: block;
|
||||
text-indent: -9999px;
|
||||
fill: #fff;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 950px) {
|
||||
.container {
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
.contentHeading {
|
||||
margin: 0;
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
.socialMediaIconsContainer {
|
||||
gap: 9px;
|
||||
}
|
||||
}
|
||||
166
apps/scandic-web/components/Current/Footer/index.tsx
Normal file
166
apps/scandic-web/components/Current/Footer/index.tsx
Normal file
@@ -0,0 +1,166 @@
|
||||
import { getCurrentFooter } from "@/lib/trpc/memoizedRequests"
|
||||
|
||||
import Image from "@/components/Image"
|
||||
import { getLang } from "@/i18n/serverContext"
|
||||
|
||||
import Navigation from "./Navigation"
|
||||
|
||||
import styles from "./footer.module.css"
|
||||
|
||||
export default async function Footer() {
|
||||
const footerData = await getCurrentFooter(getLang())
|
||||
if (!footerData) {
|
||||
return null
|
||||
}
|
||||
return (
|
||||
<footer className={styles.container}>
|
||||
<div className={styles.content}>
|
||||
<h2 className={styles.contentHeading}>
|
||||
<Image
|
||||
alt={footerData.logoConnection.edges[0].node.title}
|
||||
data-nosvgsrc="/_static/img/scandic-logotype-white.png" // what here?
|
||||
height={23}
|
||||
src={footerData.logoConnection.edges[0].node.url}
|
||||
width={102.17}
|
||||
/>
|
||||
<span className={styles.hiddenAccessible}>Scandic</span>
|
||||
</h2>
|
||||
<ul className={styles.footerSections}>
|
||||
{footerData.navigation.map((group) => (
|
||||
<li className={styles.footerLink} key={group.title}>
|
||||
<div className={styles.footerLinkHeader}>{group.title}</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
<div>
|
||||
<Navigation linkGroups={footerData.navigation} />
|
||||
|
||||
<div className={styles.contentBottom}>
|
||||
<p className={styles.contentBottomTitle}>
|
||||
{footerData.about.title}
|
||||
</p>
|
||||
<div>
|
||||
<p className={styles.footerAboutText}>{footerData.about.text}</p>
|
||||
<p className={styles.contentBottomTitle}>
|
||||
{footerData.app_downloads.title}
|
||||
</p>
|
||||
<table className={styles.appDownloadTable}>
|
||||
<tbody>
|
||||
<tr className={styles.tableRow}>
|
||||
<td
|
||||
className={styles.tableData}
|
||||
style={{ textAlign: "right" }}
|
||||
>
|
||||
<a
|
||||
title="Appstore"
|
||||
href={footerData.app_downloads.app_store.href}
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>
|
||||
<Image
|
||||
alt={
|
||||
footerData.app_downloads.app_store.imageConnection
|
||||
.edges[0].node.title
|
||||
}
|
||||
height={45}
|
||||
src={
|
||||
footerData.app_downloads.app_store.imageConnection
|
||||
.edges[0].node.url
|
||||
}
|
||||
width={135}
|
||||
/>
|
||||
</a>
|
||||
</td>
|
||||
<td
|
||||
className={styles.tableData}
|
||||
style={{ textAlign: "left" }}
|
||||
>
|
||||
<a
|
||||
title="Google Play"
|
||||
href={footerData.app_downloads.google_play.href}
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>
|
||||
<Image
|
||||
alt={
|
||||
footerData.app_downloads.google_play.imageConnection
|
||||
.edges[0].node.title
|
||||
}
|
||||
height={45}
|
||||
src={
|
||||
footerData.app_downloads.google_play.imageConnection
|
||||
.edges[0].node.url
|
||||
}
|
||||
width={135}
|
||||
/>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.sectionContainer}>
|
||||
<h3 className={styles.sectionTitle}>
|
||||
{footerData.social_media.title}
|
||||
</h3>
|
||||
<section className={styles.socialMediaIconsContainer}>
|
||||
<a
|
||||
href={footerData.social_media.facebook.href}
|
||||
rel="noopener"
|
||||
target="_blank"
|
||||
className={styles.socialMediaIconLink}
|
||||
title="Scandic on Facebook"
|
||||
>
|
||||
<svg
|
||||
focusable="false"
|
||||
className={styles.socialMediaIcon}
|
||||
viewBox="0 0 150 150"
|
||||
role="img"
|
||||
aria-labelledby="social-icons facebook-icon"
|
||||
>
|
||||
<title id="facebook-icon">
|
||||
{footerData.social_media.facebook.title}
|
||||
</title>
|
||||
<use xlinkHref="/_static/img/icons/sprites.svg#icon-facebook"></use>
|
||||
</svg>
|
||||
</a>
|
||||
<a
|
||||
href={footerData.social_media.instagram.href}
|
||||
rel="noopener"
|
||||
target="_blank"
|
||||
className={styles.socialMediaIconLink}
|
||||
title="Scandic on Instagram"
|
||||
>
|
||||
<svg
|
||||
focusable="false"
|
||||
className={styles.socialMediaIcon}
|
||||
viewBox="0 0 150 150"
|
||||
role="img"
|
||||
aria-labelledby="social-icons instagram-icon"
|
||||
>
|
||||
<title id="instagram-icon">
|
||||
{footerData.social_media.instagram.title}
|
||||
</title>
|
||||
<use xlinkHref="/_static/img/icons/sprites.svg#icon-instagram"></use>
|
||||
</svg>
|
||||
</a>
|
||||
</section>
|
||||
</div>
|
||||
<div className={styles.sectionContainer}>
|
||||
<h3 className={styles.sectionTitle}>
|
||||
{footerData.trip_advisor.title}
|
||||
</h3>
|
||||
<Image
|
||||
alt="Trip Advisor logotype"
|
||||
height={24}
|
||||
src={footerData.trip_advisor.logoConnection.edges[0].node.url}
|
||||
width={160}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
.container {
|
||||
display: block;
|
||||
padding: 0;
|
||||
margin: -10px;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.section {
|
||||
padding: 0 0 3px;
|
||||
display: block;
|
||||
height: auto;
|
||||
float: left;
|
||||
width: 100%;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.linkList {
|
||||
padding: 10px 20px 20px;
|
||||
}
|
||||
|
||||
.linkListTitle {
|
||||
color: #fff;
|
||||
font-family:
|
||||
Helvetica Neue,
|
||||
Roboto,
|
||||
Helvetica,
|
||||
Arial,
|
||||
sans-serif;
|
||||
font-size: 16px;
|
||||
line-height: 22px;
|
||||
border-bottom: 1px solid #e3e0db;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
padding-bottom: 3px;
|
||||
margin-bottom: 15px;
|
||||
margin-top: 0;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
text-rendering: optimizeLegibility;
|
||||
}
|
||||
|
||||
.listPages {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.li {
|
||||
padding-left: 38px;
|
||||
margin-bottom: 8px;
|
||||
background-image: url(/_static/img/bullet-list-arrow-circle-white.svg);
|
||||
background-repeat: no-repeat;
|
||||
background-position: 0 7px;
|
||||
background-size: 19px;
|
||||
}
|
||||
|
||||
.listLink {
|
||||
text-decoration: none;
|
||||
color: #fff;
|
||||
font-family: Helvetica, Arial, sans-serif;
|
||||
font-size: 16px;
|
||||
line-height: 32px;
|
||||
}
|
||||
|
||||
.listLink:hover,
|
||||
.listLink:active,
|
||||
.listLink:focus {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 950px) {
|
||||
.container {
|
||||
display: flex;
|
||||
}
|
||||
.section {
|
||||
margin-bottom: 4px;
|
||||
padding: 0 0 20px;
|
||||
}
|
||||
|
||||
.linkList {
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
.linkListTitle {
|
||||
padding: 0 20px;
|
||||
}
|
||||
.listPages {
|
||||
padding: 10px 20px;
|
||||
}
|
||||
|
||||
.linkListTitle {
|
||||
padding-bottom: 2px;
|
||||
line-height: 24px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
.button {
|
||||
background-color: #02838e;
|
||||
color: #fff;
|
||||
padding: 5px 15px;
|
||||
display: inline-block;
|
||||
line-height: 20px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 50px;
|
||||
height: 38px;
|
||||
line-height: 20px;
|
||||
font-size: 14px;
|
||||
font-family: Helvetica, Arial, sans-serif;
|
||||
font-weight: 400;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
text-rendering: optimizeLegibility;
|
||||
align-content: center;
|
||||
}
|
||||
|
||||
.button:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.button:focus,
|
||||
.button:active {
|
||||
box-shadow: 0 0 1px 2px #b4defa;
|
||||
outline: none;
|
||||
border: 1px solid hsl(0, 0%, 80%);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1367px) {
|
||||
.button {
|
||||
font-weight: 600;
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
height: auto;
|
||||
padding: 12px 32px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
"use client"
|
||||
|
||||
import "@scandic-hotels/design-system/current/style.css"
|
||||
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import styles from "./bookingButton.module.css"
|
||||
|
||||
export default function BookingButton({ href }: { href: string }) {
|
||||
const intl = useIntl()
|
||||
return (
|
||||
<a className={styles.button} href={href}>
|
||||
{intl.formatMessage({ id: "Book" })}
|
||||
</a>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { MainMenuSkeleton } from "../MainMenu"
|
||||
import { TopMenuSkeleton } from "../TopMenu"
|
||||
|
||||
import styles from "../header.module.css"
|
||||
|
||||
export default async function HeaderFallback() {
|
||||
return (
|
||||
<header className={styles.header} role="banner">
|
||||
<TopMenuSkeleton />
|
||||
<MainMenuSkeleton />
|
||||
</header>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
.desktop {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.container {
|
||||
position: relative;
|
||||
|
||||
font-family:
|
||||
Helvetica Neue,
|
||||
Helvetica,
|
||||
Arial,
|
||||
sans-serif;
|
||||
}
|
||||
|
||||
.toggle {
|
||||
display: flex;
|
||||
color: #fff;
|
||||
padding: 3px 15px;
|
||||
font-size: 14px;
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
margin: 0 auto;
|
||||
cursor: pointer;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.hiddenAccessible {
|
||||
display: block;
|
||||
position: absolute;
|
||||
left: -100000em;
|
||||
top: auto;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.caret {
|
||||
display: inline-block;
|
||||
width: 0;
|
||||
height: 0;
|
||||
margin-left: 2px;
|
||||
vertical-align: middle;
|
||||
border-top: 5px dashed;
|
||||
border-right: 5px solid transparent;
|
||||
border-left: 5px solid transparent;
|
||||
}
|
||||
|
||||
.icon {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
vertical-align: -4px;
|
||||
margin-right: 3px;
|
||||
fill: #fff;
|
||||
}
|
||||
|
||||
.dropdown {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
z-index: 100;
|
||||
display: none;
|
||||
float: left;
|
||||
min-width: 160px;
|
||||
padding: 5px 0;
|
||||
margin: 2px 0 0;
|
||||
list-style: none;
|
||||
font-size: 16px;
|
||||
text-align: left;
|
||||
background-color: #fff;
|
||||
border: 1px solid rgba(0, 0, 0, 0.15);
|
||||
border-radius: 4px;
|
||||
-webkit-box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);
|
||||
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);
|
||||
background-clip: padding-box;
|
||||
}
|
||||
|
||||
.li {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.dropdown.isOpen {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.link {
|
||||
clear: both;
|
||||
color: grey;
|
||||
display: block;
|
||||
font-weight: 400;
|
||||
padding: 3px 20px;
|
||||
white-space: nowrap;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.link:hover {
|
||||
background-color: #f5f5f5;
|
||||
color: #737373;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.active > .link {
|
||||
background-color: #00838e;
|
||||
color: #fff;
|
||||
outline: 0;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
@media (min-width: 1366px) {
|
||||
.desktop {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
"use client"
|
||||
import { useCallback, useEffect, useRef, useState } from "react"
|
||||
|
||||
import { type Lang, languages } from "@/constants/languages"
|
||||
|
||||
import Link from "@/components/TempDesignSystem/Link"
|
||||
import useLang from "@/hooks/useLang"
|
||||
|
||||
import styles from "./desktop.module.css"
|
||||
|
||||
import type { LanguageSwitcherProps } from "@/types/components/current/languageSwitcher"
|
||||
|
||||
export default function Desktop({ urls }: LanguageSwitcherProps) {
|
||||
const currentLanguage = useLang()
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const divRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
function toggleOpen() {
|
||||
setIsOpen((prevIsOpen) => !prevIsOpen)
|
||||
}
|
||||
|
||||
const close = useCallback(() => {
|
||||
setIsOpen(false)
|
||||
}, [setIsOpen])
|
||||
|
||||
useEffect(() => {
|
||||
function handleClickOutside(evt: Event) {
|
||||
const target = evt.target as HTMLElement
|
||||
if (divRef.current && target && !divRef.current.contains(target)) {
|
||||
close()
|
||||
}
|
||||
}
|
||||
|
||||
if (divRef.current) {
|
||||
document.addEventListener("click", handleClickOutside, false)
|
||||
}
|
||||
return () => {
|
||||
document.removeEventListener("click", handleClickOutside, false)
|
||||
}
|
||||
}, [close])
|
||||
|
||||
const urlKeys = Object.keys(urls)
|
||||
|
||||
if (urlKeys.length === 1 && urlKeys[0] === currentLanguage) {
|
||||
return (
|
||||
<div className={styles.container} ref={divRef}>
|
||||
<section className={styles.languageSwitcher}>
|
||||
<svg focusable="false" className={styles.icon} viewBox="0 0 32 32">
|
||||
<use xlinkHref="/_static/img/icons/sprites.svg#icon-globe"></use>
|
||||
</svg>
|
||||
{languages[currentLanguage]}
|
||||
</section>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<section className={styles.desktop}>
|
||||
<div className={styles.container} ref={divRef}>
|
||||
<button
|
||||
aria-pressed="false"
|
||||
className={styles.toggle}
|
||||
onClick={toggleOpen}
|
||||
>
|
||||
<svg focusable="false" className={styles.icon} viewBox="0 0 32 32">
|
||||
<use xlinkHref="/_static/img/icons/sprites.svg#icon-globe"></use>
|
||||
</svg>
|
||||
{languages[currentLanguage]}
|
||||
<span className={styles.hiddenAccessible}>Choose language</span>
|
||||
<span className={styles.caret}></span>
|
||||
</button>
|
||||
<ul className={`${styles.dropdown} ${isOpen ? styles.isOpen : ""}`}>
|
||||
{urlKeys.map((key) => {
|
||||
const url = urls[key as Lang]?.url
|
||||
if (url) {
|
||||
return (
|
||||
<li
|
||||
key={key}
|
||||
className={`${styles.li} ${currentLanguage === key ? styles.active : ""}`}
|
||||
>
|
||||
{urls[key as Lang]?.isExternal ? (
|
||||
<Link className={styles.link} href={url}>
|
||||
{languages[key as Lang]}
|
||||
</Link>
|
||||
) : (
|
||||
<a className={styles.link} href={url}>
|
||||
{languages[key as Lang]}
|
||||
</a>
|
||||
)}
|
||||
</li>
|
||||
)
|
||||
}
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
"use client"
|
||||
import { useState } from "react"
|
||||
|
||||
import { type Lang, languages } from "@/constants/languages"
|
||||
|
||||
import useLang from "@/hooks/useLang"
|
||||
|
||||
import styles from "./mobile.module.css"
|
||||
|
||||
import type { LanguageSwitcherProps } from "@/types/components/current/languageSwitcher"
|
||||
|
||||
export default function Mobile({ urls }: LanguageSwitcherProps) {
|
||||
const currentLanguage = useLang()
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
|
||||
function toggleOpen() {
|
||||
setIsOpen((prevIsOpen) => !prevIsOpen)
|
||||
}
|
||||
const urlKeys = Object.keys(urls)
|
||||
|
||||
if (urlKeys.length === 1 && urlKeys[0] === currentLanguage) {
|
||||
return <div className={styles.toggle}>{languages[currentLanguage]}</div>
|
||||
}
|
||||
|
||||
return (
|
||||
<section className={styles.mobile}>
|
||||
<div>
|
||||
<button
|
||||
aria-pressed="false"
|
||||
className={styles.toggle}
|
||||
onClick={toggleOpen}
|
||||
>
|
||||
{languages[currentLanguage]}{" "}
|
||||
<span
|
||||
className={`${styles.arrow} ${isOpen ? styles.open : ""}`}
|
||||
></span>
|
||||
<span className={styles.hiddenAccessible}>Choose language</span>
|
||||
</button>
|
||||
<ul className={`${styles.dropdown} ${isOpen ? styles.isOpen : ""}`}>
|
||||
{urlKeys.map((key) => {
|
||||
const url = urls[key as Lang]?.url
|
||||
if (url) {
|
||||
return (
|
||||
<li key={key} className={styles.li}>
|
||||
<a href={url} className={styles.link}>
|
||||
{languages[key as Lang]}
|
||||
</a>
|
||||
</li>
|
||||
)
|
||||
}
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
.mobile {
|
||||
display: block;
|
||||
font-family: Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
.toggle {
|
||||
font-size: 14px;
|
||||
padding: 5px 0;
|
||||
display: block;
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
margin: 0 auto;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.hiddenAccessible {
|
||||
display: block;
|
||||
position: absolute;
|
||||
left: -100000em;
|
||||
top: auto;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.li {
|
||||
list-style: none;
|
||||
font-size: 14px;
|
||||
font-family: Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
.link {
|
||||
color: #333;
|
||||
text-decoration: none;
|
||||
line-height: 22.4px;
|
||||
}
|
||||
|
||||
.dropdown {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.dropdown.isOpen {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.arrow {
|
||||
background-image: url("/_static/img/icons/arrows/arrow-down-grey.png");
|
||||
background-position: 50%;
|
||||
background-repeat: no-repeat;
|
||||
display: inline-block;
|
||||
margin-left: 5px;
|
||||
padding: 5px 10px;
|
||||
}
|
||||
|
||||
.arrow.open {
|
||||
background-image: url("/_static/img/icons/arrows/arrow-up-grey.png");
|
||||
}
|
||||
|
||||
.link {
|
||||
color: grey;
|
||||
display: block;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.link:hover {
|
||||
color: #7f7369;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
@media (min-width: 1366px) {
|
||||
.mobile {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
"use client"
|
||||
|
||||
import { usePathname } from "next/navigation"
|
||||
|
||||
import { trpc } from "@/lib/trpc/client"
|
||||
|
||||
import SkeletonShimmer from "@/components/SkeletonShimmer"
|
||||
import useLang from "@/hooks/useLang"
|
||||
|
||||
import Desktop from "./Desktop"
|
||||
import Mobile from "./Mobile"
|
||||
|
||||
export default function LanguageSwitcher() {
|
||||
const currentLanguage = useLang()
|
||||
const pathName = usePathname()
|
||||
|
||||
const { data: languagesResponse, isLoading } =
|
||||
trpc.contentstack.languageSwitcher.get.useQuery({
|
||||
pathName,
|
||||
lang: currentLanguage,
|
||||
})
|
||||
|
||||
if (isLoading) {
|
||||
return <SkeletonShimmer width="12ch" />
|
||||
}
|
||||
|
||||
if (!languagesResponse?.urls) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Desktop urls={languagesResponse.urls} />
|
||||
<Mobile urls={languagesResponse.urls} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
276
apps/scandic-web/components/Current/Header/MainMenu/index.tsx
Normal file
276
apps/scandic-web/components/Current/Header/MainMenu/index.tsx
Normal file
@@ -0,0 +1,276 @@
|
||||
"use client"
|
||||
import { usePathname } from "next/navigation"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { logout } from "@/constants/routes/handleAuth"
|
||||
import { myPages } from "@/constants/routes/myPages"
|
||||
import useDropdownStore from "@/stores/main-menu"
|
||||
|
||||
import Image from "@/components/Image"
|
||||
import LoginButton from "@/components/LoginButton"
|
||||
import Avatar from "@/components/MyPages/Avatar"
|
||||
import SkeletonShimmer from "@/components/SkeletonShimmer"
|
||||
import Link from "@/components/TempDesignSystem/Link"
|
||||
import useLang from "@/hooks/useLang"
|
||||
import { trackClick } from "@/utils/tracking"
|
||||
|
||||
import BookingButton from "../BookingButton"
|
||||
|
||||
import styles from "./mainMenu.module.css"
|
||||
|
||||
import type { MainMenuProps } from "@/types/components/current/header/mainMenu"
|
||||
import { DropdownTypeEnum } from "@/types/components/dropdown/dropdown"
|
||||
|
||||
export function MainMenu({
|
||||
frontpageLinkText,
|
||||
homeHref,
|
||||
links,
|
||||
logo,
|
||||
topMenuMobileLinks,
|
||||
languageSwitcher,
|
||||
myPagesMobileDropdown,
|
||||
bookingHref,
|
||||
user,
|
||||
}: MainMenuProps) {
|
||||
const intl = useIntl()
|
||||
const lang = useLang()
|
||||
const pathname = usePathname()
|
||||
|
||||
const isThreeStaticPagesPathnames = [
|
||||
"/de/sponsoring",
|
||||
"/en/sponsoring",
|
||||
"/da/sponsorering",
|
||||
"/fi/sponsorointi",
|
||||
"/no/vi-sponser",
|
||||
"/sv/vi-sponsrar",
|
||||
"/de/scandic-entdecken/wlan",
|
||||
"/en/explore-scandic/wifi",
|
||||
"/da/oplev-scandic/wifi",
|
||||
"/fi/koe-scandic/maksuton-internetyhteys",
|
||||
"/no/utforsk-scandic/wifi",
|
||||
"/sv/utforska-scandic/wi-fi",
|
||||
"/de/kundenbetreuung/haufig-gestellte-fragen/nutzung-der-internetseite",
|
||||
"/en/customer-service/frequently-asked-questions/using-the-website",
|
||||
"/da/kundeservice/sporgsmal-og-svar/om-scandics-website",
|
||||
"/fi/asiakaspalvelu/usein-kysytyt-kysymykset/tietoja-internetsivuista",
|
||||
"/no/kundeservice/sporsmal-og-svar/bruk-av-nettsiden",
|
||||
"/sv/kundservice/fragor-och-svar/om-scandics-webbplats",
|
||||
"/de/current-content-page",
|
||||
"/en/current-content-page",
|
||||
"/da/current-content-page",
|
||||
"/fi/current-content-page",
|
||||
"/no/current-content-page",
|
||||
"/sv/current-content-page",
|
||||
].includes(pathname)
|
||||
|
||||
const { toggleDropdown, isMyPagesMobileMenuOpen, isHamburgerMenuOpen } =
|
||||
useDropdownStore()
|
||||
|
||||
function handleMyPagesMobileMenuClick() {
|
||||
// Only track click when opening it
|
||||
if (!isMyPagesMobileMenuOpen) {
|
||||
trackClick("profile picture icon")
|
||||
}
|
||||
toggleDropdown(DropdownTypeEnum.MyPagesMobileMenu)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.mainMenu}>
|
||||
<div
|
||||
className={styles.container}
|
||||
itemScope
|
||||
itemType="http://schema.org/Organization"
|
||||
>
|
||||
<meta itemProp="name" content="Scandic" />
|
||||
<nav className={styles.navBar}>
|
||||
<button
|
||||
aria-pressed="false"
|
||||
className={`${styles.expanderBtn} ${isHamburgerMenuOpen ? styles.expanded : ""}`}
|
||||
onClick={() => toggleDropdown(DropdownTypeEnum.HamburgerMenu)}
|
||||
type="button"
|
||||
>
|
||||
<span className={styles.iconBars}></span>
|
||||
<span className={styles.hiddenAccessible}>Menu</span>
|
||||
</button>
|
||||
|
||||
<a className={styles.logoLink} href={homeHref}>
|
||||
<span className={styles.hiddenAccessible}>{frontpageLinkText}</span>
|
||||
<Image
|
||||
alt="Scandic Hotels logo"
|
||||
className={styles.logo}
|
||||
data-js="scandiclogoimg"
|
||||
data-nosvgsrc="/_static/img/scandic-logotype.png"
|
||||
itemProp="logo"
|
||||
height={22}
|
||||
src={logo.url}
|
||||
width={logo.dimension.width}
|
||||
/>
|
||||
</a>
|
||||
|
||||
<ul
|
||||
className={`${styles.listWrapper} ${isHamburgerMenuOpen ? styles.isOpen : ""}`}
|
||||
>
|
||||
<ul className={styles.linkRow}>
|
||||
{user ? (
|
||||
<li className={styles.mobileLinkRow}>
|
||||
<Link
|
||||
className={styles.mobileLinkButton}
|
||||
href={myPages[lang]}
|
||||
>
|
||||
{intl.formatMessage({ id: "My pages" })}
|
||||
</Link>
|
||||
</li>
|
||||
) : (
|
||||
<>
|
||||
<li>
|
||||
<Image
|
||||
src="/_static/img/icon-scandic-friends.svg"
|
||||
alt="Scanidc Friends Logo"
|
||||
height={35}
|
||||
width={35}
|
||||
className={styles.scandicFriendsLogo}
|
||||
/>
|
||||
</li>
|
||||
<li className={styles.mobileLinkRow}>
|
||||
<LoginButton
|
||||
position="hamburger menu"
|
||||
trackingId="loginStartHamburgerMenu"
|
||||
className={styles.mobileLinkButton}
|
||||
>
|
||||
{intl.formatMessage({ id: "Log in" })}
|
||||
</LoginButton>
|
||||
</li>
|
||||
</>
|
||||
)}
|
||||
|
||||
<li className={styles.mobileLinkRow}>
|
||||
<span className={styles.mobileSeparator} />
|
||||
</li>
|
||||
<li className={styles.mobileLinkRow}>
|
||||
<a className={styles.mobileLinkButton} href="">
|
||||
{intl.formatMessage({ id: "Find booking" })}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<ul className={styles.mainLinks}>
|
||||
{links.map((link, i) => (
|
||||
<li className={styles.li} key={link.href + i}>
|
||||
<Link
|
||||
className={styles.link}
|
||||
href={link.href}
|
||||
trackingId={
|
||||
isHamburgerMenuOpen
|
||||
? `hamburger - ${link.title}`
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{link.title}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
<ul className={styles.mobileList}>
|
||||
{topMenuMobileLinks.map(({ link }, i) => (
|
||||
<li className={styles.mobileLi} key={link.href + i}>
|
||||
<Link
|
||||
className={styles.mobileLink}
|
||||
href={link.href}
|
||||
trackingId={
|
||||
isHamburgerMenuOpen
|
||||
? `hamburger - ${link.title}`
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{link.title}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
{languageSwitcher ? (
|
||||
<li className={styles.mobileLi}>{languageSwitcher}</li>
|
||||
) : null}
|
||||
{!!user ? (
|
||||
<li className={`${styles.mobileLi} ${styles.logout}`}>
|
||||
<Link
|
||||
href={logout[lang]}
|
||||
className={styles.mobileLink}
|
||||
prefetch={false}
|
||||
>
|
||||
{intl.formatMessage({ id: "Log out" })}
|
||||
</Link>
|
||||
</li>
|
||||
) : null}
|
||||
</ul>
|
||||
<div className={styles.buttonContainer}>
|
||||
<div className={styles.myPagesDesktopLink}>
|
||||
{!isThreeStaticPagesPathnames && user ? (
|
||||
<Link className={styles.link} href={myPages[lang]}>
|
||||
{intl.formatMessage({ id: "My pages" })}
|
||||
</Link>
|
||||
) : null}
|
||||
</div>
|
||||
<BookingButton href={bookingHref} />
|
||||
{!isThreeStaticPagesPathnames && myPagesMobileDropdown && user ? (
|
||||
<div
|
||||
role="button"
|
||||
onClick={handleMyPagesMobileMenuClick}
|
||||
className={styles.avatarButton}
|
||||
>
|
||||
<Avatar firstName={user.firstName} lastName={user.lastName} />
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
{isMyPagesMobileMenuOpen ? myPagesMobileDropdown : null}
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function MainMenuSkeleton() {
|
||||
return (
|
||||
<div className={styles.mainMenu}>
|
||||
<div
|
||||
className={styles.container}
|
||||
itemScope
|
||||
itemType="http://schema.org/Organization"
|
||||
>
|
||||
<meta itemProp="name" content="Scandic" />
|
||||
<nav className={styles.navBar}>
|
||||
<button
|
||||
aria-pressed="false"
|
||||
className={styles.expanderBtn}
|
||||
type="button"
|
||||
>
|
||||
<span className={styles.iconBars}></span>
|
||||
<span className={styles.hiddenAccessible}>Menu</span>
|
||||
</button>
|
||||
|
||||
<a className={styles.logoLink} href={""}>
|
||||
<Image
|
||||
alt="Scandic Hotels logo"
|
||||
className={styles.logo}
|
||||
data-js="scandiclogoimg"
|
||||
itemProp="logo"
|
||||
height={20}
|
||||
src={"/_static/img/scandic-logotype.png"}
|
||||
width={200}
|
||||
/>
|
||||
</a>
|
||||
|
||||
<ul className={styles.listWrapper}>
|
||||
{Array.from({ length: 5 }, () => "").map((_, i) => (
|
||||
<li className={`${styles.li} ${styles.skeletonWrapper}`} key={i}>
|
||||
<SkeletonShimmer height="22px" width="130px" />
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<div className={styles.buttonContainer}>
|
||||
<BookingButton href={""} />
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,348 @@
|
||||
.mainMenu {
|
||||
background-color: var(--Main-Grey-White);
|
||||
background-image: none;
|
||||
box-shadow: 0px 1.001px 1.001px 0px rgba(0, 0, 0, 0.05);
|
||||
max-height: 100%;
|
||||
overflow: visible;
|
||||
width: 100%;
|
||||
height: var(--current-mobile-site-header-height);
|
||||
max-width: var(--max-width-navigation);
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.container {
|
||||
box-sizing: content-box;
|
||||
height: 100%;
|
||||
margin: 0 auto;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.mainLinks {
|
||||
background-color: #f3f2f1;
|
||||
}
|
||||
|
||||
.navBar {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 80px 1fr;
|
||||
grid-template-areas: "expanderBtn logoLink . buttonContainer";
|
||||
grid-template-rows: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.expanderBtn {
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
justify-self: flex-start;
|
||||
padding: 11px 8px 16px;
|
||||
transition: 0.3s;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.iconBars,
|
||||
.iconBars::after,
|
||||
.iconBars::before {
|
||||
background: #757575;
|
||||
border-radius: 2.3px;
|
||||
display: inline-block;
|
||||
height: 5px;
|
||||
position: relative;
|
||||
transition: 0.3s;
|
||||
width: 32px;
|
||||
}
|
||||
|
||||
.iconBars::after,
|
||||
.iconBars::before {
|
||||
content: "";
|
||||
left: 0;
|
||||
position: absolute;
|
||||
transform-origin: 2.286px center;
|
||||
}
|
||||
|
||||
.iconBars::after {
|
||||
top: -8px;
|
||||
}
|
||||
|
||||
.iconBars::before {
|
||||
top: 8px;
|
||||
}
|
||||
|
||||
.expanded .iconBars {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.expanded .iconBars::after,
|
||||
.expanded .iconBars::before {
|
||||
top: 0;
|
||||
transform-origin: 50% 50%;
|
||||
width: 32px;
|
||||
}
|
||||
|
||||
.expanded .iconBars::after {
|
||||
transform: rotate(-45deg);
|
||||
}
|
||||
|
||||
.expanded .iconBars::before {
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
.hiddenAccessible {
|
||||
display: block;
|
||||
height: 1px;
|
||||
left: -100000em;
|
||||
overflow: hidden;
|
||||
position: absolute;
|
||||
top: auto;
|
||||
width: 1px;
|
||||
}
|
||||
|
||||
.logoLink {
|
||||
/*padding: 16px 0 8px;*/
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
width: 80px;
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: 80px;
|
||||
object-fit: fill;
|
||||
}
|
||||
|
||||
.listWrapper {
|
||||
background-color: #fff;
|
||||
border-top: 1px solid #e3e0db;
|
||||
display: none;
|
||||
list-style: none;
|
||||
overflow-y: visible;
|
||||
padding-bottom: 20px;
|
||||
margin: 0;
|
||||
padding-inline-start: 0;
|
||||
}
|
||||
|
||||
.listWrapper.isOpen {
|
||||
display: block;
|
||||
left: 0;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 100%;
|
||||
}
|
||||
|
||||
.li {
|
||||
border-bottom: none;
|
||||
display: block;
|
||||
line-height: 17px;
|
||||
position: relative;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.link {
|
||||
color: #000;
|
||||
text-decoration: none;
|
||||
display: block;
|
||||
font-family:
|
||||
Helvetica Neue,
|
||||
Helvetica,
|
||||
Arial,
|
||||
sans-serif;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
padding-bottom: 20px;
|
||||
padding-top: 20px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.link:hover {
|
||||
color: #7f7369;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.linkRow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-bottom: 1px solid #e3e0db;
|
||||
background-color: #f3f2f1 !important;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.scandicFriendsLogo {
|
||||
margin-right: 4px;
|
||||
margin-left: -4px;
|
||||
}
|
||||
|
||||
.mobileLinkRow {
|
||||
margin: 6px 0;
|
||||
padding: 15px 15px 15px 5px;
|
||||
}
|
||||
|
||||
.mobileLinkButton {
|
||||
font-size: 14px;
|
||||
font-family:
|
||||
Helvetica Neue,
|
||||
Helvetica,
|
||||
Arial,
|
||||
sans-serif !important;
|
||||
font-weight: 700;
|
||||
background-color: transparent !important;
|
||||
text-decoration: none;
|
||||
color: #000;
|
||||
outline-color: transparent;
|
||||
}
|
||||
|
||||
.mobileSeparator {
|
||||
border-left: 1px solid #e3e0db;
|
||||
height: 35px;
|
||||
margin-bottom: -12px;
|
||||
margin-left: -1px;
|
||||
margin-top: -12px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.mobileList {
|
||||
padding-top: 6px;
|
||||
}
|
||||
|
||||
.mobileLi {
|
||||
display: block;
|
||||
position: relative;
|
||||
text-align: center;
|
||||
line-height: 22.4px;
|
||||
padding: 5px 0;
|
||||
}
|
||||
|
||||
.mobileLi.logout {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.mobileLink {
|
||||
color: #000;
|
||||
display: block;
|
||||
font-family: Helvetica !important;
|
||||
font-size: 14px;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.buttonContainer {
|
||||
display: inline-flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
margin-right: 8px;
|
||||
gap: var(--Spacing-x3);
|
||||
}
|
||||
|
||||
.myPagesDesktopLink {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.skeletonWrapper {
|
||||
padding: 4px 10px;
|
||||
height: 100%;
|
||||
align-content: center;
|
||||
}
|
||||
|
||||
@media (min-width: 1367px) {
|
||||
.navBar {
|
||||
grid-template-columns: 140px auto 1fr;
|
||||
height: 82.4px;
|
||||
align-content: center;
|
||||
padding: 0px 0px var(--Spacing-x-quarter) 0px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.logoLink {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
padding: 27px 30px 26px 0;
|
||||
text-align: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.mainMenu {
|
||||
box-shadow: none;
|
||||
background-color: hsla(0, 0%, 100%, 0.95);
|
||||
position: relative;
|
||||
z-index: unset;
|
||||
height: 82.4px;
|
||||
}
|
||||
|
||||
.container {
|
||||
padding: 0 var(--Spacing-x5) 0 120px;
|
||||
}
|
||||
|
||||
.mainLinks {
|
||||
padding-top: 2.5px;
|
||||
background-color: transparent;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.expanderBtn {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: 102.17px;
|
||||
height: 100%;
|
||||
padding-bottom: 4px;
|
||||
}
|
||||
|
||||
.listWrapper {
|
||||
border-top: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-top: 0;
|
||||
position: static;
|
||||
width: 100%;
|
||||
padding-bottom: 0px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.listWrapper.isOpen {
|
||||
position: static;
|
||||
}
|
||||
|
||||
.li {
|
||||
display: inline-grid;
|
||||
float: none;
|
||||
vertical-align: middle;
|
||||
line-height: 1.15;
|
||||
}
|
||||
|
||||
.link {
|
||||
background-image: none;
|
||||
font-family: var(--typography-Body-Regular-fontFamily);
|
||||
font-size: var(--typography-Body-Regular-fontSize);
|
||||
font-feature-settings:
|
||||
"clig" off,
|
||||
"liga" off;
|
||||
font-weight: 600;
|
||||
line-height: 125%;
|
||||
padding: 30px 15px;
|
||||
text-transform: uppercase;
|
||||
color: var(--text-black); /* Design system should return #404040 */
|
||||
}
|
||||
|
||||
.linkRow {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.mobileList {
|
||||
display: none;
|
||||
padding-top: 0px;
|
||||
}
|
||||
|
||||
.mobileLi {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.buttonContainer {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.avatarButton {
|
||||
display: none;
|
||||
}
|
||||
.myPagesDesktopLink {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
"use client"
|
||||
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { logout } from "@/constants/routes/handleAuth"
|
||||
import { trpc } from "@/lib/trpc/client"
|
||||
import useDropdownStore from "@/stores/main-menu"
|
||||
|
||||
import SkeletonShimmer from "@/components/SkeletonShimmer"
|
||||
import Divider from "@/components/TempDesignSystem/Divider"
|
||||
import Link from "@/components/TempDesignSystem/Link"
|
||||
import Title from "@/components/TempDesignSystem/Text/Title"
|
||||
import useLang from "@/hooks/useLang"
|
||||
|
||||
import styles from "./my-pages-mobile-dropdown.module.css"
|
||||
|
||||
import type { ReactNode } from "react"
|
||||
|
||||
import { DropdownTypeEnum } from "@/types/components/dropdown/dropdown"
|
||||
|
||||
export default function MyPagesMobileDropdown() {
|
||||
const intl = useIntl()
|
||||
const { toggleDropdown, isMyPagesMobileMenuOpen } = useDropdownStore()
|
||||
|
||||
const handleOnClick = () => toggleDropdown(DropdownTypeEnum.MyPagesMobileMenu)
|
||||
|
||||
return (
|
||||
<nav
|
||||
className={`${styles.navigationMenu} ${isMyPagesMobileMenuOpen ? styles.navigationMenuIsOpen : ""}`}
|
||||
>
|
||||
<Title textTransform="capitalize" level="h5">
|
||||
<div className={styles.heading}>
|
||||
{intl.formatMessage({ id: "My pages" })}
|
||||
</div>
|
||||
</Title>
|
||||
|
||||
<List>
|
||||
<PrimaryLinks handleOnClick={handleOnClick} />
|
||||
</List>
|
||||
<List>
|
||||
<SecondaryLinks handleOnClick={handleOnClick} />
|
||||
</List>
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
|
||||
function List({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<>
|
||||
<div className={styles.dividerWrapper}>
|
||||
<Divider color="subtle" />
|
||||
</div>
|
||||
<ul className={styles.dropdownWrapper}>
|
||||
<ul className={styles.dropdownLinks}>{children}</ul>
|
||||
</ul>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function PrimaryLinks({ handleOnClick }: { handleOnClick: () => void }) {
|
||||
const {
|
||||
data: myPagesNavigation,
|
||||
isLoading,
|
||||
isSuccess,
|
||||
} = useMyPagesNavigation()
|
||||
|
||||
const primaryLinks = myPagesNavigation?.primaryLinks ?? []
|
||||
return (
|
||||
<>
|
||||
{isLoading && <Skeletons count={4} />}
|
||||
{isSuccess &&
|
||||
primaryLinks.map((link, i) => (
|
||||
<li key={link.href + i}>
|
||||
<Link
|
||||
href={link.href}
|
||||
partialMatch
|
||||
size={"regular"}
|
||||
variant="myPageMobileDropdown"
|
||||
color="burgundy"
|
||||
onClick={handleOnClick}
|
||||
>
|
||||
{link.text}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</>
|
||||
)
|
||||
}
|
||||
function SecondaryLinks({ handleOnClick }: { handleOnClick: () => void }) {
|
||||
const {
|
||||
data: myPagesNavigation,
|
||||
isLoading,
|
||||
isSuccess,
|
||||
} = useMyPagesNavigation()
|
||||
|
||||
const secondaryLinks = myPagesNavigation?.secondaryLinks ?? []
|
||||
const intl = useIntl()
|
||||
const lang = useLang()
|
||||
|
||||
return (
|
||||
<>
|
||||
{isLoading && <Skeletons count={3} />}
|
||||
{isSuccess &&
|
||||
secondaryLinks.map((link, i) => (
|
||||
<li key={link.href + i}>
|
||||
<Link
|
||||
href={link.href}
|
||||
partialMatch
|
||||
size={"small"}
|
||||
variant="myPageMobileDropdown"
|
||||
color="burgundy"
|
||||
onClick={handleOnClick}
|
||||
>
|
||||
{link.text}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
<li>
|
||||
<Link
|
||||
href={logout[lang]}
|
||||
prefetch={false}
|
||||
size="small"
|
||||
color="burgundy"
|
||||
variant="myPageMobileDropdown"
|
||||
>
|
||||
{intl.formatMessage({ id: "Log out" })}
|
||||
</Link>
|
||||
</li>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function Skeletons({ count }: { count: number }) {
|
||||
return (
|
||||
<>
|
||||
{Array.from({ length: count }).map((_, i) => (
|
||||
<li key={i} className={styles.skeletonItem}>
|
||||
<SkeletonShimmer width="100px" height="20px" />
|
||||
</li>
|
||||
))}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function useMyPagesNavigation() {
|
||||
const lang = useLang()
|
||||
return trpc.navigation.myPages.useQuery({ lang })
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
.navigationMenu {
|
||||
background-color: #fff;
|
||||
border-top: 1px solid #e3e0db;
|
||||
display: none;
|
||||
list-style: none;
|
||||
overflow-y: visible;
|
||||
margin: 0;
|
||||
padding-inline-start: 0;
|
||||
}
|
||||
|
||||
.navigationMenu.navigationMenuIsOpen {
|
||||
display: block;
|
||||
left: 0;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 100%;
|
||||
}
|
||||
|
||||
.dropdownWrapper {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
padding: 20px var(--Spacing-x2);
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: flex-start;
|
||||
background-color: var(--Main-Grey-White);
|
||||
box-shadow:
|
||||
0px 276px 77px 0px rgba(0, 0, 0, 0),
|
||||
0px 177px 71px 0px rgba(0, 0, 0, 0.01),
|
||||
0px 99px 60px 0px rgba(0, 0, 0, 0.05),
|
||||
0px 44px 44px 0px rgba(0, 0, 0, 0.09),
|
||||
0px 11px 24px 0px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.dividerWrapper {
|
||||
background-color: var(--Main-Grey-White);
|
||||
padding: 0 var(--Spacing-x2);
|
||||
margin: auto;
|
||||
place-content: center;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.heading {
|
||||
padding: 20px var(--Spacing-x2);
|
||||
}
|
||||
|
||||
.dropdownLinks {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Spacing-x-half);
|
||||
width: 100%;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1367px) {
|
||||
.navigationMenu {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.navigationMenu.navigationMenuIsOpen {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.skeletonItem {
|
||||
padding: var(--Spacing-x1);
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
.banner {
|
||||
align-items: center;
|
||||
background: #606060;
|
||||
color: #fff;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 10px;
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.reloadBtn {
|
||||
color: #fff;
|
||||
background-color: #00838e;
|
||||
border: 0;
|
||||
border-radius: 18px;
|
||||
outline: 0 none;
|
||||
padding: 5px 15px;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import styles from "./banner.module.css"
|
||||
|
||||
export default function OfflineBanner() {
|
||||
return (
|
||||
<div className={`${styles.banner} ${styles.hidden}`}>
|
||||
You are offline, some content may be out of date.
|
||||
<button className={styles.reloadBtn} type="button">Reload</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
112
apps/scandic-web/components/Current/Header/TopMenu/index.tsx
Normal file
112
apps/scandic-web/components/Current/Header/TopMenu/index.tsx
Normal file
@@ -0,0 +1,112 @@
|
||||
import { logout } from "@/constants/routes/handleAuth"
|
||||
import { overview } from "@/constants/routes/myPages"
|
||||
import { getName } from "@/lib/trpc/memoizedRequests"
|
||||
|
||||
import LoginButton from "@/components/LoginButton"
|
||||
import SkeletonShimmer from "@/components/SkeletonShimmer"
|
||||
import Link from "@/components/TempDesignSystem/Link"
|
||||
import { getIntl } from "@/i18n"
|
||||
import { getLang } from "@/i18n/serverContext"
|
||||
|
||||
import styles from "./topMenu.module.css"
|
||||
|
||||
import type { TopMenuProps } from "@/types/components/current/header/topMenu"
|
||||
|
||||
function capitalize(str: string) {
|
||||
return str.charAt(0).toUpperCase().toUpperCase() + str.slice(1).toLowerCase()
|
||||
}
|
||||
|
||||
export default async function TopMenu({
|
||||
frontpageLinkText,
|
||||
homeHref,
|
||||
links,
|
||||
languageSwitcher,
|
||||
}: TopMenuProps) {
|
||||
const intl = await getIntl()
|
||||
const user = await getName()
|
||||
return (
|
||||
<div className={styles.topMenu}>
|
||||
<div className={styles.container}>
|
||||
<a className={styles.homeLink} href={homeHref}>
|
||||
{frontpageLinkText}
|
||||
</a>
|
||||
|
||||
<ul className={styles.list}>
|
||||
{languageSwitcher ? (
|
||||
<li className={styles.langSwitcher}>{languageSwitcher}</li>
|
||||
) : null}
|
||||
|
||||
{links.map(({ link }, i) => (
|
||||
<li key={link.href + i}>
|
||||
<a className={styles.link} href={link.href}>
|
||||
{link.title}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
<li className={styles.sessionContainer}>
|
||||
{user ? (
|
||||
<>
|
||||
{user ? (
|
||||
<Link
|
||||
href={overview[getLang()]}
|
||||
className={styles.sessionLink}
|
||||
prefetch={false}
|
||||
>
|
||||
<span data-hj-suppress>{capitalize(user.firstName)}</span>
|
||||
</Link>
|
||||
) : null}
|
||||
<div className={styles.loginSeparator} />
|
||||
<Link
|
||||
href={logout[getLang()]}
|
||||
className={styles.sessionLink}
|
||||
prefetch={false}
|
||||
>
|
||||
{intl.formatMessage({ id: "Log out" })}
|
||||
</Link>
|
||||
</>
|
||||
) : (
|
||||
<LoginButton
|
||||
position="hamburger menu"
|
||||
trackingId="loginStartTopMenu"
|
||||
className={`${styles.sessionLink} ${styles.loginLink}`}
|
||||
variant="default"
|
||||
size="small"
|
||||
>
|
||||
{intl.formatMessage({ id: "Log in" })}
|
||||
</LoginButton>
|
||||
)}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export async function TopMenuSkeleton() {
|
||||
const intl = await getIntl()
|
||||
const links = new Array(5).fill("")
|
||||
return (
|
||||
<div className={styles.topMenu}>
|
||||
<div className={styles.container}>
|
||||
<ul className={styles.list}>
|
||||
{links.map((_link, i) => (
|
||||
<li key={i} className={styles.skeletonWrapper}>
|
||||
<SkeletonShimmer width="100px" height="16px" />
|
||||
</li>
|
||||
))}
|
||||
<li className={styles.sessionContainer}>
|
||||
<LoginButton
|
||||
position="hamburger menu"
|
||||
trackingId="loginStartTopMenu"
|
||||
className={`${styles.sessionLink} ${styles.loginLink}`}
|
||||
variant="default"
|
||||
size="small"
|
||||
>
|
||||
{intl.formatMessage({ id: "Log in" })}
|
||||
</LoginButton>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
.topMenu {
|
||||
background-color: #8d3a7c;
|
||||
color: #fff;
|
||||
display: none;
|
||||
font-size: 14px;
|
||||
padding: 0;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.container {
|
||||
box-sizing: content-box;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin: 0 auto;
|
||||
max-width: 1200px;
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
.homeLink {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.list {
|
||||
display: flex;
|
||||
list-style: none;
|
||||
line-height: 22.4px;
|
||||
}
|
||||
|
||||
.link {
|
||||
color: #fff;
|
||||
display: inline-block;
|
||||
padding: 3px 10px;
|
||||
text-decoration: none;
|
||||
font-family:
|
||||
Helvetica Neue,
|
||||
Helvetica,
|
||||
Arial,
|
||||
sans-serif;
|
||||
font-size: 13px;
|
||||
font-weight: 400;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
text-rendering: optimizeLegibility;
|
||||
}
|
||||
.langSwitcher {
|
||||
text-align: center;
|
||||
position: relative;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.skeletonWrapper {
|
||||
padding: 4px 10px;
|
||||
height: 30px;
|
||||
align-content: center;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
.container {
|
||||
padding: 0 30px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 950px) {
|
||||
.topMenu {
|
||||
background-color: #3d3835;
|
||||
display: block;
|
||||
}
|
||||
.list {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.link {
|
||||
padding-top: 4px;
|
||||
padding-bottom: 4px;
|
||||
}
|
||||
|
||||
.loginContainer {
|
||||
margin-left: 10px;
|
||||
background-color: #f3f2f1;
|
||||
}
|
||||
|
||||
.loginLink {
|
||||
padding-left: 30px;
|
||||
padding-right: 30px;
|
||||
color: #000;
|
||||
font-family:
|
||||
Helvetica Neue,
|
||||
Helvetica,
|
||||
Arial,
|
||||
sans-serif !important;
|
||||
}
|
||||
|
||||
.sessionContainer {
|
||||
margin-left: 10px;
|
||||
background-color: #f3f2f1;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.loginSeparator {
|
||||
height: 15px;
|
||||
border-right: 1px solid #000;
|
||||
}
|
||||
|
||||
.sessionLink {
|
||||
padding: 4px 15px;
|
||||
color: #000;
|
||||
font-family:
|
||||
Helvetica Neue,
|
||||
Helvetica,
|
||||
Arial,
|
||||
sans-serif !important;
|
||||
font-size: 13px;
|
||||
font-weight: 400;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
text-rendering: optimizeLegibility;
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.sessionLink.loginLink {
|
||||
padding-left: 30px;
|
||||
padding-right: 30px;
|
||||
}
|
||||
}
|
||||
14
apps/scandic-web/components/Current/Header/header.module.css
Normal file
14
apps/scandic-web/components/Current/Header/header.module.css
Normal file
@@ -0,0 +1,14 @@
|
||||
.header {
|
||||
display: grid;
|
||||
background-color: var(--Main-Grey-White);
|
||||
position: relative;
|
||||
z-index: var(--header-z-index);
|
||||
}
|
||||
|
||||
@media screen and (max-width: 950px) {
|
||||
.header {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: var(--header-z-index);
|
||||
}
|
||||
}
|
||||
52
apps/scandic-web/components/Current/Header/index.tsx
Normal file
52
apps/scandic-web/components/Current/Header/index.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
import { homeHrefs } from "@/constants/homeHrefs"
|
||||
import { env } from "@/env/server"
|
||||
import { getCurrentHeader, getName } from "@/lib/trpc/memoizedRequests"
|
||||
|
||||
import { getLang } from "@/i18n/serverContext"
|
||||
|
||||
import LanguageSwitcher from "./LanguageSwitcher"
|
||||
import { MainMenu } from "./MainMenu"
|
||||
import MyPagesMobileDropdown from "./MyPagesMobileDropdown"
|
||||
import OfflineBanner from "./OfflineBanner"
|
||||
import TopMenu from "./TopMenu"
|
||||
|
||||
import styles from "./header.module.css"
|
||||
|
||||
export default async function Header() {
|
||||
const lang = getLang()
|
||||
const [data, user] = await Promise.all([getCurrentHeader(lang), getName()])
|
||||
|
||||
if (!data?.header) {
|
||||
return null
|
||||
}
|
||||
|
||||
const homeHref = homeHrefs[env.NODE_ENV][getLang()]
|
||||
const { frontpageLinkText, logo, menu, topMenu } = data.header
|
||||
|
||||
const topMenuMobileLinks = topMenu.links
|
||||
.filter((link) => link.show_on_mobile)
|
||||
.sort((a, b) => (a.sort_order_mobile < b.sort_order_mobile ? 1 : -1))
|
||||
|
||||
return (
|
||||
<header className={styles.header} role="banner">
|
||||
<OfflineBanner />
|
||||
<TopMenu
|
||||
frontpageLinkText={frontpageLinkText}
|
||||
homeHref={homeHref}
|
||||
links={topMenu.links}
|
||||
languageSwitcher={<LanguageSwitcher />}
|
||||
/>
|
||||
<MainMenu
|
||||
frontpageLinkText={frontpageLinkText}
|
||||
homeHref={homeHref}
|
||||
links={menu.links}
|
||||
logo={logo}
|
||||
topMenuMobileLinks={topMenuMobileLinks}
|
||||
languageSwitcher={<LanguageSwitcher />}
|
||||
myPagesMobileDropdown={<MyPagesMobileDropdown />}
|
||||
bookingHref={homeHref}
|
||||
user={user}
|
||||
/>
|
||||
</header>
|
||||
)
|
||||
}
|
||||
34
apps/scandic-web/components/Current/Hero/hero.module.css
Normal file
34
apps/scandic-web/components/Current/Hero/hero.module.css
Normal file
@@ -0,0 +1,34 @@
|
||||
.wrapper {
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.picture {
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
transition: opacity 400ms ease-in-out 0s;
|
||||
transform: translate(-50%, -50%) !important;
|
||||
}
|
||||
|
||||
.heroImage {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
max-height: 600px;
|
||||
min-height: 100px;
|
||||
object-fit: cover;
|
||||
object-position: center;
|
||||
aspect-ratio: 1/1;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
.heroImage {
|
||||
aspect-ratio: auto;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1367px) {
|
||||
.wrapper {
|
||||
overflow: hidden;
|
||||
position: sticky;
|
||||
top: 0px;
|
||||
}
|
||||
}
|
||||
23
apps/scandic-web/components/Current/Hero/index.tsx
Normal file
23
apps/scandic-web/components/Current/Hero/index.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import Image from "@/components/Image"
|
||||
|
||||
import styles from "./hero.module.css"
|
||||
|
||||
import type { HeroProps } from "@/types/components/current/hero"
|
||||
|
||||
export default function Hero({ images }: HeroProps) {
|
||||
return (
|
||||
<div className={styles.wrapper} aria-label="Hero" tabIndex={0}>
|
||||
{images.map(({ node: image }) => (
|
||||
<picture className={styles.picture} key={image.title}>
|
||||
<Image
|
||||
alt={image.title}
|
||||
className={styles.heroImage}
|
||||
height={image.dimension.height}
|
||||
src={image.url}
|
||||
width={image.dimension.width}
|
||||
/>
|
||||
</picture>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
94
apps/scandic-web/components/Current/LangPopup/index.tsx
Normal file
94
apps/scandic-web/components/Current/LangPopup/index.tsx
Normal file
@@ -0,0 +1,94 @@
|
||||
import { headers } from "next/headers"
|
||||
|
||||
import { Lang, localeToLang } from "@/constants/languages"
|
||||
|
||||
import { getLang } from "@/i18n/serverContext"
|
||||
|
||||
export default function LangPopup() {
|
||||
const headersList = headers()
|
||||
const preferedLang = headersList.get("Accept-Language") ?? ""
|
||||
|
||||
const possibleLangs = Object.keys(localeToLang)
|
||||
|
||||
if (!possibleLangs.includes(preferedLang)) {
|
||||
return null
|
||||
}
|
||||
|
||||
const langOfChoice: Lang = localeToLang[preferedLang as Lang]
|
||||
|
||||
if (langOfChoice === getLang()) {
|
||||
return null
|
||||
}
|
||||
|
||||
let language = ""
|
||||
let viewIn = ""
|
||||
|
||||
switch (langOfChoice) {
|
||||
case Lang.de:
|
||||
language = "Deutsch"
|
||||
viewIn = "Ansicht in"
|
||||
break
|
||||
case Lang.da:
|
||||
language = "Dansk"
|
||||
viewIn = "Se in"
|
||||
break
|
||||
case Lang.fi:
|
||||
language = "Suomi"
|
||||
viewIn = "Katso in"
|
||||
break
|
||||
case Lang.no:
|
||||
language = "Norsk"
|
||||
viewIn = "Se in"
|
||||
break
|
||||
case Lang.sv:
|
||||
language = "Svenska"
|
||||
viewIn = "Visa in"
|
||||
break
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="lang-popup hidden" id="lang-popup">
|
||||
<a
|
||||
href="#"
|
||||
className="lang-popup__close"
|
||||
title="Close language change popup"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlnsXlink="http://www.w3.org/1999/xlink"
|
||||
width="16px"
|
||||
height="16px"
|
||||
viewBox="0 0 16 16"
|
||||
version="1.1"
|
||||
style={{
|
||||
stroke: "#757575",
|
||||
width: "14px",
|
||||
height: "14px",
|
||||
display: "block",
|
||||
strokeWidth: "2px",
|
||||
}}
|
||||
>
|
||||
<title>Close</title>
|
||||
<g id="web-close" fillRule="evenodd">
|
||||
<line x1="0" y1="0" x2="100%" y2="100%" />
|
||||
<line x1="0" y1="100%" x2="100%" y2="0" />
|
||||
</g>
|
||||
</svg>
|
||||
</a>
|
||||
<div className="lang-popup__body">
|
||||
<p className="lang-popup__msg">
|
||||
You are viewing our website in English, would you like to change to{" "}
|
||||
{language}?
|
||||
</p>
|
||||
</div>
|
||||
<div className="lang-popup__footer">
|
||||
<a href="" className="lang-popup__cta btn btn--primary">
|
||||
{viewIn} {language}
|
||||
</a>
|
||||
<a href="#" className="lang-popup__cancel btn btn--link">
|
||||
No thanks
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
22
apps/scandic-web/components/Current/LoadingSpinner/index.tsx
Normal file
22
apps/scandic-web/components/Current/LoadingSpinner/index.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import styles from "./loading.module.css"
|
||||
|
||||
export default function LoadingSpinner() {
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.spinner}>
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
.container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 200px;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
}
|
||||
|
||||
.spinner div {
|
||||
transform-origin: 40px 40px;
|
||||
animation: spinnerAnimation 1.2s linear infinite;
|
||||
}
|
||||
|
||||
.spinner div::after {
|
||||
content: " ";
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 3px;
|
||||
left: 37px;
|
||||
width: 6px;
|
||||
height: 18px;
|
||||
border-radius: 20%;
|
||||
background: red;
|
||||
}
|
||||
|
||||
.spinner div:nth-child(1) {
|
||||
transform: rotate(0deg);
|
||||
animation-delay: -1.1s;
|
||||
}
|
||||
|
||||
.spinner div:nth-child(2) {
|
||||
transform: rotate(30deg);
|
||||
animation-delay: -1s;
|
||||
}
|
||||
.spinner div:nth-child(3) {
|
||||
transform: rotate(60deg);
|
||||
animation-delay: -0.9s;
|
||||
}
|
||||
.spinner div:nth-child(4) {
|
||||
transform: rotate(90deg);
|
||||
animation-delay: -0.8s;
|
||||
}
|
||||
.spinner div:nth-child(5) {
|
||||
transform: rotate(120deg);
|
||||
animation-delay: -0.7s;
|
||||
}
|
||||
.spinner div:nth-child(6) {
|
||||
transform: rotate(150deg);
|
||||
animation-delay: -0.6s;
|
||||
}
|
||||
.spinner div:nth-child(7) {
|
||||
transform: rotate(180deg);
|
||||
animation-delay: -0.5s;
|
||||
}
|
||||
.spinner div:nth-child(8) {
|
||||
transform: rotate(210deg);
|
||||
animation-delay: -0.4s;
|
||||
}
|
||||
.spinner div:nth-child(9) {
|
||||
transform: rotate(240deg);
|
||||
animation-delay: -0.3s;
|
||||
}
|
||||
.spinner div:nth-child(10) {
|
||||
transform: rotate(270deg);
|
||||
animation-delay: -0.2s;
|
||||
}
|
||||
.spinner div:nth-child(11) {
|
||||
transform: rotate(300deg);
|
||||
animation-delay: -0.1s;
|
||||
}
|
||||
.spinner div:nth-child(12) {
|
||||
transform: rotate(330deg);
|
||||
animation-delay: 0s;
|
||||
}
|
||||
|
||||
@keyframes spinnerAnimation {
|
||||
0% {
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
131
apps/scandic-web/components/Current/NotFound/Texts.ts
Normal file
131
apps/scandic-web/components/Current/NotFound/Texts.ts
Normal file
@@ -0,0 +1,131 @@
|
||||
import type { Lang } from "@/constants/languages"
|
||||
|
||||
type Texts = {
|
||||
title: string
|
||||
goToStartPage: {
|
||||
question: string
|
||||
link: string
|
||||
linkText: string
|
||||
}
|
||||
goToDestinations: {
|
||||
question: string
|
||||
link: string
|
||||
linkText: string
|
||||
}
|
||||
goToOffers: {
|
||||
question: string
|
||||
link: string
|
||||
linkText: string
|
||||
}
|
||||
}
|
||||
|
||||
export const texts: Record<Lang, Texts> = {
|
||||
en: {
|
||||
title: "Sorry, page not found.",
|
||||
goToStartPage: {
|
||||
question: "Would you like to go back to the ",
|
||||
link: "https://www.scandichotels.com/",
|
||||
linkText: "startpage",
|
||||
},
|
||||
goToDestinations: {
|
||||
question: "Or take a trip to our ",
|
||||
link: "https://www.scandichotels.com/hotels",
|
||||
linkText: "destinations",
|
||||
},
|
||||
goToOffers: {
|
||||
question: " or latest ",
|
||||
link: "https://www.scandichotels.com/weekend-packages-and-offers",
|
||||
linkText: "offers",
|
||||
},
|
||||
},
|
||||
sv: {
|
||||
title: "Oj då, vi kunde inte hitta sidan du söker.",
|
||||
goToStartPage: {
|
||||
question: "Vill du gå tillbaka till ",
|
||||
link: "https://www.scandichotels.se/",
|
||||
linkText: "startsidan",
|
||||
},
|
||||
goToDestinations: {
|
||||
question: "Eller resa till våra ",
|
||||
link: "https://www.scandichotels.se/hotell",
|
||||
linkText: "destinationer",
|
||||
},
|
||||
goToOffers: {
|
||||
question: " eller se våra senaste ",
|
||||
link: "https://www.scandichotels.se/erbjudanden-och-weekendpaket",
|
||||
linkText: "erbjudanden",
|
||||
},
|
||||
},
|
||||
de: {
|
||||
title: "Tut uns leid, Seite nicht gefunden.",
|
||||
goToStartPage: {
|
||||
question: "Möchten Sie zurück zur ",
|
||||
link: "https://www.scandichotels.de/",
|
||||
linkText: "Startseite",
|
||||
},
|
||||
goToDestinations: {
|
||||
question: "Oder machen Sie einen Ausflug zu unseren ",
|
||||
link: "https://www.scandichotels.de/hotelsuche",
|
||||
linkText: "Reisezielen",
|
||||
},
|
||||
goToOffers: {
|
||||
question: " und aktuellen ",
|
||||
link: "https://www.scandichotels.de/angebote-arrangements",
|
||||
linkText: "Angeboten",
|
||||
},
|
||||
},
|
||||
fi: {
|
||||
title: "Valitettavasti sivua ei löydy.",
|
||||
goToStartPage: {
|
||||
question: "Haluaisitko mennä takaisin ",
|
||||
link: "https://www.scandichotels.fi/",
|
||||
linkText: "etusivulle",
|
||||
},
|
||||
goToDestinations: {
|
||||
question: "Voit myös tutustu ",
|
||||
link: "https://www.scandichotels.fi/hotellit",
|
||||
linkText: "kohteisiimme",
|
||||
},
|
||||
goToOffers: {
|
||||
question: " tai ajankohtaisiin ",
|
||||
link: "https://www.scandichotels.fi/tarjoukset",
|
||||
linkText: "tarjouksiimme",
|
||||
},
|
||||
},
|
||||
no: {
|
||||
title: "Oi da, vi fant ikke siden du lette etter...",
|
||||
goToStartPage: {
|
||||
question: "Vil du gå tilbake til ",
|
||||
link: "https://www.scandichotels.no/",
|
||||
linkText: "startsiden",
|
||||
},
|
||||
goToDestinations: {
|
||||
question: "Eller ta en tur til våre ",
|
||||
link: "https://www.scandichotels.no/hotell",
|
||||
linkText: "destinasjoner",
|
||||
},
|
||||
goToOffers: {
|
||||
question: " eller siste ",
|
||||
link: "https://www.scandichotels.no/hotelltilbud",
|
||||
linkText: "tilbud",
|
||||
},
|
||||
},
|
||||
da: {
|
||||
title: "Hov - siden kan ikke findes!",
|
||||
goToStartPage: {
|
||||
question: "Vil du gå tilbage til ",
|
||||
link: "https://www.scandichotels.dk/",
|
||||
linkText: "startsiden",
|
||||
},
|
||||
goToDestinations: {
|
||||
question: "Eller tag en tur til vores ",
|
||||
link: "https://www.scandichotels.dk/hoteller",
|
||||
linkText: "destinationer",
|
||||
},
|
||||
goToOffers: {
|
||||
question: " eller seneste ",
|
||||
link: "https://www.scandichotels.dk/tilbud-og-hotelpakker",
|
||||
linkText: "tilbud",
|
||||
},
|
||||
},
|
||||
}
|
||||
50
apps/scandic-web/components/Current/NotFound/index.tsx
Normal file
50
apps/scandic-web/components/Current/NotFound/index.tsx
Normal file
@@ -0,0 +1,50 @@
|
||||
import { getLang } from "@/i18n/serverContext"
|
||||
|
||||
import { texts } from "./Texts"
|
||||
|
||||
import styles from "./notFound.module.css"
|
||||
|
||||
export default function NotFound() {
|
||||
const lang = getLang()
|
||||
const infoTexts = texts[lang]
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.content}>
|
||||
<h1 className={styles.header}>{infoTexts.title}</h1>
|
||||
<div className={styles.pitch}>
|
||||
<p className={styles.text}>
|
||||
{infoTexts.goToStartPage.question}
|
||||
<a
|
||||
className={styles.link}
|
||||
title={infoTexts.goToStartPage.linkText}
|
||||
href={infoTexts.goToStartPage.link}
|
||||
>
|
||||
{infoTexts.goToStartPage.linkText}
|
||||
</a>
|
||||
?
|
||||
</p>
|
||||
<p className={styles.text}>
|
||||
{infoTexts.goToDestinations.question}
|
||||
<a
|
||||
className={styles.link}
|
||||
title={infoTexts.goToDestinations.linkText}
|
||||
href={infoTexts.goToDestinations.link}
|
||||
>
|
||||
{infoTexts.goToDestinations.linkText}
|
||||
</a>
|
||||
{infoTexts.goToOffers.question}
|
||||
<a
|
||||
className={styles.link}
|
||||
title={infoTexts.goToOffers.linkText}
|
||||
href={infoTexts.goToOffers.link}
|
||||
>
|
||||
{infoTexts.goToOffers.linkText}
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
.container {
|
||||
margin-top: 0;
|
||||
background: var(--Main-Grey-White);
|
||||
position: relative;
|
||||
border-top: 50px solid var(--Main-Grey-White);
|
||||
background-clip: content-box;
|
||||
}
|
||||
|
||||
.content {
|
||||
position: relative;
|
||||
text-align: center;
|
||||
box-sizing: content-box;
|
||||
max-width: var(--max-width-page);
|
||||
margin: 0 auto;
|
||||
padding: 70px 30px;
|
||||
}
|
||||
|
||||
.header {
|
||||
font-family:
|
||||
brandon text,
|
||||
Arial,
|
||||
Helvetica,
|
||||
sans-serif;
|
||||
font-size: 32px;
|
||||
line-height: 1;
|
||||
margin: 0;
|
||||
text-transform: uppercase;
|
||||
font-weight: 400;
|
||||
color: #483729;
|
||||
}
|
||||
|
||||
.pitch {
|
||||
margin-top: 32px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.text {
|
||||
font-family:
|
||||
Helvetica Neue,
|
||||
Roboto,
|
||||
Helvetica,
|
||||
Arial,
|
||||
sans-serif;
|
||||
font-weight: 300;
|
||||
line-height: normal;
|
||||
text-transform: none;
|
||||
font-size: 20px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.link {
|
||||
color: #00838e;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 740px) {
|
||||
.content {
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 950px) {
|
||||
.header {
|
||||
font-size: 46px;
|
||||
}
|
||||
|
||||
.text {
|
||||
font-size: 24px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
.nav {
|
||||
display: none;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
.parent {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.list {
|
||||
align-items: center;
|
||||
display: grid;
|
||||
gap: 7px;
|
||||
grid-auto-flow: column;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.link {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.currentPage {
|
||||
color: #7f7369;
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
.currentPage,
|
||||
.li {
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.4em;
|
||||
}
|
||||
|
||||
.li::before,
|
||||
.currentPage::before {
|
||||
content: "›";
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
@media (min-width: 740px) {
|
||||
.nav {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import styles from "./breadcrumbs.module.css"
|
||||
|
||||
import type { BreadcrumbsProps } from "@/types/components/current/breadcrumbs"
|
||||
|
||||
export default function Breadcrumbs({
|
||||
breadcrumbs,
|
||||
parent,
|
||||
title,
|
||||
}: BreadcrumbsProps) {
|
||||
return (
|
||||
<nav className={styles.nav}>
|
||||
<ul className={styles.list}>
|
||||
{parent ? (
|
||||
<li className={styles.parent}>
|
||||
<a href={parent.href}>
|
||||
{parent.title}
|
||||
</a>
|
||||
</li>
|
||||
) : null}
|
||||
{breadcrumbs.map((breadcrumb) => (
|
||||
<li
|
||||
className={styles.li}
|
||||
itemProp="breadcrumb"
|
||||
key={breadcrumb.href}
|
||||
>
|
||||
<a className={styles.link} href={breadcrumb.href}>
|
||||
{breadcrumb.title}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
<li className={styles.currentPage}>
|
||||
<span>{title}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
37
apps/scandic-web/components/Current/Preamble/index.tsx
Normal file
37
apps/scandic-web/components/Current/Preamble/index.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
import DeprecatedJsonToHtml from "@/components/DeprecatedJsonToHtml"
|
||||
|
||||
import { renderOptions as currentRenderOptions } from "./../currentRenderOptions"
|
||||
import Breadcrumbs from "./Breadcrumbs"
|
||||
import { renderOptions } from "./renderOptions"
|
||||
|
||||
import styles from "./preamble.module.css"
|
||||
|
||||
import type { PreambleProps } from "@/types/components/current/preamble"
|
||||
|
||||
export default function Preamble({
|
||||
breadcrumbs,
|
||||
breadcrumbParent,
|
||||
breadcrumbTitle,
|
||||
preamble,
|
||||
title,
|
||||
}: PreambleProps) {
|
||||
return (
|
||||
<section className={styles.container}>
|
||||
<section>
|
||||
<Breadcrumbs
|
||||
breadcrumbs={breadcrumbs}
|
||||
parent={breadcrumbParent}
|
||||
title={breadcrumbTitle}
|
||||
/>
|
||||
<h1>{title}</h1>
|
||||
{preamble?.text ? (
|
||||
<DeprecatedJsonToHtml
|
||||
embeds={preamble.text.embedded_itemsConnection.edges}
|
||||
nodes={preamble.text.json.children}
|
||||
renderOptions={{ ...currentRenderOptions, ...renderOptions }}
|
||||
/>
|
||||
) : null}
|
||||
</section>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
.container {
|
||||
display: grid;
|
||||
gap: 60px;
|
||||
margin: 0 auto;
|
||||
padding: 20px 10px 25px;
|
||||
background: #fff;
|
||||
box-sizing: content-box;
|
||||
}
|
||||
|
||||
.preamble {
|
||||
color: #333;
|
||||
font-family:
|
||||
Helvetica Neue,
|
||||
Roboto,
|
||||
Helvetica,
|
||||
Arial,
|
||||
sans-serif;
|
||||
font-size: 1.25rem;
|
||||
font-weight: 300;
|
||||
line-height: normal;
|
||||
text-transform: none;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
.container {
|
||||
background: transparent;
|
||||
padding: 30px 30px 15px;
|
||||
grid-template-columns: 2fr 1fr;
|
||||
max-width: 1200px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1367px) {
|
||||
.container {
|
||||
padding: 50px 30px 35px;
|
||||
}
|
||||
|
||||
.preamble {
|
||||
font-size: 1.5rem;
|
||||
line-height: 2.25rem;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import styles from "./preamble.module.css"
|
||||
|
||||
import type { EmbedByUid } from "@/types/components/deprecatedjsontohtml"
|
||||
import { RTETypeEnum } from "@/types/rte/enums"
|
||||
import type { RTEDefaultNode, RTENext } from "@/types/rte/node"
|
||||
import type { RenderOptions } from "@/types/rte/option"
|
||||
|
||||
export const renderOptions: RenderOptions = {
|
||||
[RTETypeEnum.p]: (
|
||||
node: RTEDefaultNode,
|
||||
embeds: EmbedByUid,
|
||||
next: RTENext,
|
||||
fullRenderOptions: RenderOptions
|
||||
) => {
|
||||
return (
|
||||
<p key={node.uid} className={styles.preamble}>
|
||||
{next(node.children, embeds, fullRenderOptions)}
|
||||
</p>
|
||||
)
|
||||
},
|
||||
}
|
||||
9
apps/scandic-web/components/Current/Section/index.tsx
Normal file
9
apps/scandic-web/components/Current/Section/index.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import styles from "./section.module.css"
|
||||
|
||||
export default function Section({ children }: React.PropsWithChildren) {
|
||||
return (
|
||||
<div className={styles.wrapper}>
|
||||
<section className={styles.section}>{children}</section>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
.section {
|
||||
box-sizing: content-box;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
.section {
|
||||
padding: 30px 30px 15px;
|
||||
grid-template-columns: 2fr 1fr;
|
||||
display: grid;
|
||||
gap: 70px;
|
||||
max-width: 1200px;
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
background-color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1367px) {
|
||||
.section {
|
||||
padding: 50px 30px 35px;
|
||||
}
|
||||
}
|
||||
33
apps/scandic-web/components/Current/SubnavMobile.tsx
Normal file
33
apps/scandic-web/components/Current/SubnavMobile.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import type { SubnavMobileProps } from "@/types/components/current/subnavMobile"
|
||||
|
||||
export default async function SubnavMobile({
|
||||
breadcrumbs,
|
||||
parent,
|
||||
title,
|
||||
}: SubnavMobileProps) {
|
||||
return (
|
||||
<div className="subnav-mobile hidden-small hidden-medium hidden-large">
|
||||
<nav className="u-flex">
|
||||
<ul className="breadcrumb-list hidden-small hidden-medium hidden-large">
|
||||
{parent ? (
|
||||
<li className="breadcrumb-list__parent hidden-medium hidden-large">
|
||||
<a href={parent.href}>
|
||||
{parent.title}
|
||||
</a>
|
||||
</li>
|
||||
) : null}
|
||||
{breadcrumbs.map((breadcrumb) => (
|
||||
<li className="breadcrumb-list__body" key={breadcrumb.href}>
|
||||
<a href={breadcrumb.href}>
|
||||
{breadcrumb.title}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
<li className="breadcrumb-list__body">
|
||||
<span>{title}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
120
apps/scandic-web/components/Current/Tracking.tsx
Normal file
120
apps/scandic-web/components/Current/Tracking.tsx
Normal file
@@ -0,0 +1,120 @@
|
||||
"use client"
|
||||
|
||||
import { usePathname, useSearchParams } from "next/navigation"
|
||||
import { useEffect } from "react"
|
||||
|
||||
import type {
|
||||
SiteSectionObject,
|
||||
TrackingData,
|
||||
TrackingProps,
|
||||
} from "@/types/components/tracking"
|
||||
|
||||
function createPageObject(trackingData: TrackingData) {
|
||||
const englishSegments = trackingData.englishUrl
|
||||
? trackingData.englishUrl.split("/").filter((seg?: string) => seg)
|
||||
: null
|
||||
|
||||
const [lang, ...segments] = trackingData.pathName
|
||||
.split("/")
|
||||
.filter((seg: string) => seg)
|
||||
|
||||
function getSiteSections(segments: string[]): SiteSectionObject {
|
||||
/*
|
||||
Adobe expects the properties sitesection1 - sitessection6, hence the for-loop below
|
||||
The segments ['explore-scandic', 'wifi'] should result in:
|
||||
{
|
||||
sitesection1: "explore-scandic",
|
||||
sitesection2: "explore-scandic|wifi",
|
||||
sitesection3: "explore-scandic|wifi|",
|
||||
sitesection4: "explore-scandic|wifi||",
|
||||
sitesection5: "explore-scandic|wifi|||",
|
||||
sitesection6: "explore-scandic|wifi||||",
|
||||
}
|
||||
*/
|
||||
const sitesections = {} as SiteSectionObject
|
||||
for (let i = 0; i < 6; i++) {
|
||||
const key = ("sitesection" + (i + 1)) as keyof SiteSectionObject
|
||||
|
||||
sitesections[key] = segments.slice(0, i + 1).join("|")
|
||||
if (i > 0 && !segments[i]) {
|
||||
sitesections[key] = sitesections[key].concat(
|
||||
"|".repeat(i + 1 - segments.length)
|
||||
)
|
||||
}
|
||||
}
|
||||
return sitesections
|
||||
}
|
||||
const sitesections = englishSegments
|
||||
? getSiteSections(englishSegments)
|
||||
: getSiteSections(segments)
|
||||
const { host: domain, href: fullurl, origin } = window.location
|
||||
const page_obj = {
|
||||
pagename: englishSegments ? englishSegments.join("|") : segments.join("|"),
|
||||
pagetype: "contentpage",
|
||||
pageurl: origin + trackingData.pathName,
|
||||
fullurl,
|
||||
createDate: trackingData.createdDate,
|
||||
publishDate: trackingData.publishedDate,
|
||||
domain,
|
||||
errorcode: null, // handle
|
||||
querystring: trackingData.queryString || "",
|
||||
pageid: trackingData.pageId,
|
||||
// sessionid: "<unique identifier of session>", // base on what?
|
||||
domainlanguage: trackingData.lang ? trackingData.lang : lang,
|
||||
hotelbrand: "scandic",
|
||||
siteversion: "new-web",
|
||||
...sitesections,
|
||||
}
|
||||
return page_obj
|
||||
}
|
||||
|
||||
export default function Tracking({ pageData }: TrackingProps) {
|
||||
const pathName = usePathname()
|
||||
const queryString = useSearchParams().toString()
|
||||
|
||||
function CookiebotCallbackOnAccept() {
|
||||
const cookie = window._satellite.cookie.get("CookieConsent")
|
||||
|
||||
if (window.Cookiebot?.changed && window.adobe) {
|
||||
if (cookie?.includes("statistics:true")) {
|
||||
window.adobe.optIn.approve(window.adobe.OptInCategories.ANALYTICS, true)
|
||||
} else {
|
||||
window.adobe.optIn.deny(window.adobe.OptInCategories.ANALYTICS, true)
|
||||
}
|
||||
window.adobe.optIn.complete()
|
||||
console.warn("window.load event explicitly dispatched.")
|
||||
window.dispatchEvent(new Event("load"))
|
||||
}
|
||||
}
|
||||
|
||||
function CookebotCallbackOnDecline() {
|
||||
if (window.Cookiebot?.changed && window.adobe) {
|
||||
window.adobe.optIn.deny(window.adobe.OptInCategories.ANALYTICS, true)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (window.datalayer) {
|
||||
const trackingData = { ...pageData, pathName, queryString }
|
||||
const pageObject = createPageObject(trackingData)
|
||||
|
||||
window.datalayer.page = pageObject
|
||||
}
|
||||
}, [pathName, queryString, pageData])
|
||||
|
||||
useEffect(() => {
|
||||
// handle consent
|
||||
window.addEventListener("CookiebotOnAccept", CookiebotCallbackOnAccept)
|
||||
window.addEventListener("CookiebotOnDecline", CookebotCallbackOnDecline)
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("CookiebotOnAccept", CookiebotCallbackOnAccept)
|
||||
window.removeEventListener(
|
||||
"CookiebotOnDecline",
|
||||
CookebotCallbackOnDecline
|
||||
)
|
||||
}
|
||||
}, [])
|
||||
|
||||
return null
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
.image {
|
||||
height: auto;
|
||||
margin-bottom: var(--Spacing-x2);
|
||||
max-width: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
474
apps/scandic-web/components/Current/currentRenderOptions.tsx
Normal file
474
apps/scandic-web/components/Current/currentRenderOptions.tsx
Normal file
@@ -0,0 +1,474 @@
|
||||
import Image from "@/components/Image"
|
||||
import Link from "@/components/TempDesignSystem/Link"
|
||||
|
||||
import styles from "./currentRenderOptions.module.css"
|
||||
|
||||
import type { EmbedByUid } from "@/types/components/deprecatedjsontohtml"
|
||||
import { EmbedEnum } from "@/types/requests/utils/embeds"
|
||||
import type { Attributes } from "@/types/rte/attrs"
|
||||
import { RTEItemTypeEnum, RTETypeEnum } from "@/types/rte/enums"
|
||||
import {
|
||||
type RTEDefaultNode,
|
||||
RTEMarkType,
|
||||
type RTENext,
|
||||
type RTENode,
|
||||
type RTERegularNode,
|
||||
} from "@/types/rte/node"
|
||||
import type { RenderOptions } from "@/types/rte/option"
|
||||
|
||||
function extractPossibleAttributes(attrs: Attributes | undefined) {
|
||||
if (!attrs) return {}
|
||||
const props: Record<string, any> = {}
|
||||
if (attrs.id) {
|
||||
props.id = attrs.id
|
||||
}
|
||||
|
||||
if (attrs.class) {
|
||||
props.className = attrs.class
|
||||
} else if (attrs["class-name"]) {
|
||||
props.className = attrs["class-name"]
|
||||
} else if (attrs.classname) {
|
||||
props.className = attrs.classname
|
||||
} else if (attrs?.style?.["text-align"]) {
|
||||
props.style = {
|
||||
textAlign: attrs?.style?.["text-align"],
|
||||
}
|
||||
}
|
||||
|
||||
return props
|
||||
}
|
||||
|
||||
export const renderOptions: RenderOptions = {
|
||||
[RTETypeEnum.a]: (
|
||||
node: RTERegularNode,
|
||||
embeds: EmbedByUid,
|
||||
next: RTENext,
|
||||
fullRenderOptions: RenderOptions
|
||||
) => {
|
||||
if (node.attrs.url) {
|
||||
const props = extractPossibleAttributes(node.attrs)
|
||||
return (
|
||||
<a
|
||||
{...props}
|
||||
href={node.attrs.url}
|
||||
target={node.attrs.target ?? "_blank"}
|
||||
key={node.uid}
|
||||
>
|
||||
{next(node.children, embeds, fullRenderOptions)}
|
||||
</a>
|
||||
)
|
||||
}
|
||||
return null
|
||||
},
|
||||
|
||||
[RTETypeEnum.blockquote]: (
|
||||
node: RTEDefaultNode,
|
||||
embeds: EmbedByUid,
|
||||
next: RTENext,
|
||||
fullRenderOptions: RenderOptions
|
||||
) => {
|
||||
const props = extractPossibleAttributes(node.attrs)
|
||||
return (
|
||||
<blockquote key={node.uid} {...props}>
|
||||
{next(node.children, embeds, fullRenderOptions)}
|
||||
</blockquote>
|
||||
)
|
||||
},
|
||||
|
||||
[RTETypeEnum.code]: (
|
||||
node: RTEDefaultNode,
|
||||
embeds: EmbedByUid,
|
||||
next: RTENext,
|
||||
fullRenderOptions: RenderOptions
|
||||
) => {
|
||||
const props = extractPossibleAttributes(node.attrs)
|
||||
return (
|
||||
<code key={node.uid} {...props}>
|
||||
{next(node.children, embeds, fullRenderOptions)}
|
||||
</code>
|
||||
)
|
||||
},
|
||||
|
||||
[RTETypeEnum.embed]: (
|
||||
node: RTEDefaultNode,
|
||||
embeds: EmbedByUid,
|
||||
next: RTENext,
|
||||
fullRenderOptions: RenderOptions
|
||||
) => {
|
||||
const props = extractPossibleAttributes(node.attrs)
|
||||
if (node.attrs.src) {
|
||||
props.src = node.attrs.src
|
||||
}
|
||||
if (node.attrs.url) {
|
||||
props.src = node.attrs.url
|
||||
}
|
||||
if (!props.src) {
|
||||
return null
|
||||
}
|
||||
return (
|
||||
<iframe key={node.uid} {...props}>
|
||||
{next(node.children, embeds, fullRenderOptions)}
|
||||
</iframe>
|
||||
)
|
||||
},
|
||||
|
||||
[RTETypeEnum.h1]: (
|
||||
node: RTEDefaultNode,
|
||||
embeds: EmbedByUid,
|
||||
next: RTENext,
|
||||
fullRenderOptions: RenderOptions
|
||||
) => {
|
||||
const props = extractPossibleAttributes(node.attrs)
|
||||
return (
|
||||
<h1 key={node.uid} {...props}>
|
||||
{next(node.children, embeds, fullRenderOptions)}
|
||||
</h1>
|
||||
)
|
||||
},
|
||||
|
||||
[RTETypeEnum.h2]: (
|
||||
node: RTEDefaultNode,
|
||||
embeds: EmbedByUid,
|
||||
next: RTENext,
|
||||
fullRenderOptions: RenderOptions
|
||||
) => {
|
||||
const props = extractPossibleAttributes(node.attrs)
|
||||
return (
|
||||
<h2 key={node.uid} {...props}>
|
||||
{next(node.children, embeds, fullRenderOptions)}
|
||||
</h2>
|
||||
)
|
||||
},
|
||||
|
||||
[RTETypeEnum.h3]: (
|
||||
node: RTEDefaultNode,
|
||||
embeds: EmbedByUid,
|
||||
next: RTENext,
|
||||
fullRenderOptions: RenderOptions
|
||||
) => {
|
||||
const props = extractPossibleAttributes(node.attrs)
|
||||
return (
|
||||
<h3 key={node.uid} {...props}>
|
||||
{next(node.children, embeds, fullRenderOptions)}
|
||||
</h3>
|
||||
)
|
||||
},
|
||||
|
||||
[RTETypeEnum.h4]: (
|
||||
node: RTEDefaultNode,
|
||||
embeds: EmbedByUid,
|
||||
next: RTENext,
|
||||
fullRenderOptions: RenderOptions
|
||||
) => {
|
||||
const props = extractPossibleAttributes(node.attrs)
|
||||
return (
|
||||
<h4 key={node.uid} {...props}>
|
||||
{next(node.children, embeds, fullRenderOptions)}
|
||||
</h4>
|
||||
)
|
||||
},
|
||||
|
||||
[RTETypeEnum.h5]: (
|
||||
node: RTEDefaultNode,
|
||||
embeds: EmbedByUid,
|
||||
next: RTENext,
|
||||
fullRenderOptions: RenderOptions
|
||||
) => {
|
||||
const props = extractPossibleAttributes(node.attrs)
|
||||
return (
|
||||
<h5 key={node.uid} {...props}>
|
||||
{next(node.children, embeds, fullRenderOptions)}
|
||||
</h5>
|
||||
)
|
||||
},
|
||||
|
||||
[RTETypeEnum.h6]: (
|
||||
node: RTEDefaultNode,
|
||||
embeds: EmbedByUid,
|
||||
next: RTENext,
|
||||
fullRenderOptions: RenderOptions
|
||||
) => {
|
||||
const props = extractPossibleAttributes(node.attrs)
|
||||
return (
|
||||
<h6 key={node.uid} {...props}>
|
||||
{next(node.children, embeds, fullRenderOptions)}
|
||||
</h6>
|
||||
)
|
||||
},
|
||||
|
||||
[RTETypeEnum.hr]: () => {
|
||||
return <hr />
|
||||
},
|
||||
|
||||
[RTETypeEnum.li]: (
|
||||
node: RTEDefaultNode,
|
||||
embeds: EmbedByUid,
|
||||
next: RTENext,
|
||||
fullRenderOptions: RenderOptions
|
||||
) => {
|
||||
const props = extractPossibleAttributes(node.attrs)
|
||||
return (
|
||||
<li key={node.uid} {...props}>
|
||||
{next(node.children, embeds, fullRenderOptions)}
|
||||
</li>
|
||||
)
|
||||
},
|
||||
|
||||
[RTETypeEnum.ol]: (
|
||||
node: RTEDefaultNode,
|
||||
embeds: EmbedByUid,
|
||||
next: RTENext,
|
||||
fullRenderOptions: RenderOptions
|
||||
) => {
|
||||
const props = extractPossibleAttributes(node.attrs)
|
||||
return (
|
||||
<ol key={node.uid} {...props}>
|
||||
{next(node.children, embeds, fullRenderOptions)}
|
||||
</ol>
|
||||
)
|
||||
},
|
||||
|
||||
[RTETypeEnum.p]: (
|
||||
node: RTEDefaultNode,
|
||||
embeds: EmbedByUid,
|
||||
next: RTENext,
|
||||
fullRenderOptions: RenderOptions
|
||||
) => {
|
||||
const props = extractPossibleAttributes(node.attrs)
|
||||
return (
|
||||
<p {...props} key={node.uid}>
|
||||
{next(node.children, embeds, fullRenderOptions)}
|
||||
</p>
|
||||
)
|
||||
},
|
||||
|
||||
[RTETypeEnum.reference]: (
|
||||
node: RTENode,
|
||||
embeds: EmbedByUid,
|
||||
next: RTENext,
|
||||
fullRenderOptions: RenderOptions
|
||||
) => {
|
||||
if ("attrs" in node) {
|
||||
const type = node.attrs.type
|
||||
if (type === RTEItemTypeEnum.asset) {
|
||||
const image = embeds?.[node?.attrs?.["asset-uid"]]
|
||||
if (image?.node.__typename === EmbedEnum.SysAsset) {
|
||||
const alt = image?.node?.title ?? node.attrs.alt
|
||||
const alignment = node.attrs?.style?.["text-align"]
|
||||
? {
|
||||
alignSelf: node.attrs?.style?.["text-align"],
|
||||
}
|
||||
: {}
|
||||
return (
|
||||
<Image
|
||||
key={node.uid}
|
||||
alt={alt}
|
||||
className={styles.image}
|
||||
height={image.node.dimension.height}
|
||||
src={image?.node?.url}
|
||||
width={image.node.dimension.width}
|
||||
style={alignment}
|
||||
/>
|
||||
)
|
||||
}
|
||||
} else {
|
||||
const props = extractPossibleAttributes(node.attrs)
|
||||
const href = node.attrs?.locale
|
||||
? `/${node.attrs.locale}${node.attrs.href}`
|
||||
: node.attrs.href
|
||||
return (
|
||||
<Link {...props} href={href} key={node.uid}>
|
||||
{next(node.children, embeds, fullRenderOptions)}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
},
|
||||
|
||||
[RTETypeEnum.table]: (
|
||||
node: RTEDefaultNode,
|
||||
embeds: EmbedByUid,
|
||||
next: RTENext,
|
||||
fullRenderOptions: RenderOptions
|
||||
) => {
|
||||
const props = extractPossibleAttributes(node.attrs)
|
||||
return (
|
||||
<table key={node.uid} {...props}>
|
||||
{next(node.children, embeds, fullRenderOptions)}
|
||||
</table>
|
||||
)
|
||||
},
|
||||
|
||||
[RTETypeEnum.thead]: (
|
||||
node: RTEDefaultNode,
|
||||
embeds: EmbedByUid,
|
||||
next: RTENext,
|
||||
fullRenderOptions: RenderOptions
|
||||
) => {
|
||||
const props = extractPossibleAttributes(node.attrs)
|
||||
return (
|
||||
<thead key={node.uid} {...props}>
|
||||
{next(node.children, embeds, fullRenderOptions)}
|
||||
</thead>
|
||||
)
|
||||
},
|
||||
|
||||
[RTETypeEnum.tbody]: (
|
||||
node: RTEDefaultNode,
|
||||
embeds: EmbedByUid,
|
||||
next: RTENext,
|
||||
fullRenderOptions: RenderOptions
|
||||
) => {
|
||||
const props = extractPossibleAttributes(node.attrs)
|
||||
return (
|
||||
<tbody key={node.uid} {...props}>
|
||||
{next(node.children, embeds, fullRenderOptions)}
|
||||
</tbody>
|
||||
)
|
||||
},
|
||||
|
||||
[RTETypeEnum.tfoot]: (
|
||||
node: RTEDefaultNode,
|
||||
embeds: EmbedByUid,
|
||||
next: RTENext,
|
||||
fullRenderOptions: RenderOptions
|
||||
) => {
|
||||
const props = extractPossibleAttributes(node.attrs)
|
||||
return (
|
||||
<tfoot key={node.uid} {...props}>
|
||||
{next(node.children, embeds, fullRenderOptions)}
|
||||
</tfoot>
|
||||
)
|
||||
},
|
||||
|
||||
[RTETypeEnum.tr]: (
|
||||
node: RTEDefaultNode,
|
||||
embeds: EmbedByUid,
|
||||
next: RTENext,
|
||||
fullRenderOptions: RenderOptions
|
||||
) => {
|
||||
const props = extractPossibleAttributes(node.attrs)
|
||||
return (
|
||||
<tr key={node.uid} {...props}>
|
||||
{next(node.children, embeds, fullRenderOptions)}
|
||||
</tr>
|
||||
)
|
||||
},
|
||||
|
||||
[RTETypeEnum.th]: (
|
||||
node: RTEDefaultNode,
|
||||
embeds: EmbedByUid,
|
||||
next: RTENext,
|
||||
fullRenderOptions: RenderOptions
|
||||
) => {
|
||||
const props = extractPossibleAttributes(node.attrs)
|
||||
return (
|
||||
<th key={node.uid} {...props}>
|
||||
{next(node.children, embeds, fullRenderOptions)}
|
||||
</th>
|
||||
)
|
||||
},
|
||||
|
||||
[RTETypeEnum.td]: (
|
||||
node: RTEDefaultNode,
|
||||
embeds: EmbedByUid,
|
||||
next: RTENext,
|
||||
fullRenderOptions: RenderOptions
|
||||
) => {
|
||||
const props = extractPossibleAttributes(node.attrs)
|
||||
return (
|
||||
<td key={node.uid} {...props}>
|
||||
{next(node.children, embeds, fullRenderOptions)}
|
||||
</td>
|
||||
)
|
||||
},
|
||||
|
||||
[RTETypeEnum.ul]: (
|
||||
node: RTEDefaultNode,
|
||||
embeds: EmbedByUid,
|
||||
next: RTENext,
|
||||
fullRenderOptions: RenderOptions
|
||||
) => {
|
||||
const props = extractPossibleAttributes(node.attrs)
|
||||
return (
|
||||
<ul key={node.uid} {...props}>
|
||||
{next(node.children, embeds, fullRenderOptions)}
|
||||
</ul>
|
||||
)
|
||||
},
|
||||
|
||||
/** TextNode wrappers */
|
||||
[RTEMarkType.bold]: (children: React.ReactNode) => {
|
||||
return <strong>{children}</strong>
|
||||
},
|
||||
|
||||
[RTEMarkType.italic]: (children: React.ReactNode) => {
|
||||
return <em>{children}</em>
|
||||
},
|
||||
|
||||
[RTEMarkType.underline]: (children: React.ReactNode) => {
|
||||
return <u>{children}</u>
|
||||
},
|
||||
|
||||
[RTEMarkType.strikethrough]: (children: React.ReactNode) => {
|
||||
return <s>{children}</s>
|
||||
},
|
||||
|
||||
[RTEMarkType.inlineCode]: (children: React.ReactNode) => {
|
||||
return <span>{children}</span>
|
||||
},
|
||||
|
||||
[RTEMarkType.subscript]: (children: React.ReactNode) => {
|
||||
return <sub>{children}</sub>
|
||||
},
|
||||
|
||||
[RTEMarkType.superscript]: (children: React.ReactNode) => {
|
||||
return <sup>{children}</sup>
|
||||
},
|
||||
|
||||
[RTEMarkType.break]: (children: React.ReactNode) => {
|
||||
return (
|
||||
<>
|
||||
<br />
|
||||
{children}
|
||||
</>
|
||||
)
|
||||
},
|
||||
|
||||
[RTEMarkType.classnameOrId]: (
|
||||
children: React.ReactNode,
|
||||
className?: string,
|
||||
id?: string
|
||||
) => {
|
||||
let props = {
|
||||
className,
|
||||
id,
|
||||
}
|
||||
if (!className) {
|
||||
delete props.className
|
||||
}
|
||||
if (!id) {
|
||||
delete props.id
|
||||
}
|
||||
return (
|
||||
<span key={id} {...props}>
|
||||
{children}
|
||||
</span>
|
||||
)
|
||||
},
|
||||
|
||||
/**
|
||||
* Contentstack can return something called `default` as seen here in their
|
||||
* own SDK (https://github.com/contentstack/contentstack-utils-javascript/blob/master/src/options/default-node-options.ts#L89)
|
||||
*/
|
||||
default: (
|
||||
node: RTEDefaultNode,
|
||||
embeds: EmbedByUid,
|
||||
next: RTENext,
|
||||
fullRenderOptions: RenderOptions
|
||||
) => {
|
||||
return next(node.children, embeds, fullRenderOptions)
|
||||
},
|
||||
}
|
||||
Reference in New Issue
Block a user