Merge remote-tracking branch 'origin/develop' into feat/SW-415-select-room-card

This commit is contained in:
Pontus Dreij
2024-10-10 14:35:38 +02:00
12 changed files with 185 additions and 45 deletions

View File

@@ -107,7 +107,8 @@
--main-menu-mobile-height: 75px;
--main-menu-desktop-height: 118px;
--booking-widget-desktop-height: 95px;
--booking-widget-mobile-height: 75px;
--booking-widget-desktop-height: 77px;
--hotel-page-map-desktop-width: 23.75rem;
/* Z-INDEX */

View File

@@ -1,3 +1,7 @@
.cardContainer {
scroll-margin-top: var(--hotel-page-scroll-margin-top);
}
.spanOne {
grid-column: span 1;
}

View File

@@ -26,7 +26,7 @@ export default function FacilitiesCardGrid({
}
return (
<section id={imageCard.card.id}>
<section id={imageCard.card.id} className={styles.cardContainer}>
<Grids.Stackable className={styles.desktopGrid}>
{facilitiesCardGrid.map((card: FacilityCardType) => (
<Card {...card} key={card.id} className={getCardClassName(card)} />

View File

@@ -13,6 +13,7 @@ import { RoomCard } from "./RoomCard"
import styles from "./rooms.module.css"
import { HotelHashValues } from "@/types/components/hotelPage/tabNavigation"
import type { RoomsProps } from "./types"
export function Rooms({ rooms }: RoomsProps) {
@@ -50,8 +51,11 @@ export function Rooms({ rooms }: RoomsProps) {
}
return (
<SectionContainer id="rooms-section">
<div ref={scrollRef}></div>
<SectionContainer
id={HotelHashValues.rooms}
className={styles.roomsContainer}
>
<div ref={scrollRef} className={styles.scrollRef}></div>
<SectionHeader
textTransform="capitalize"
title={intl.formatMessage({ id: "Rooms" })}

View File

@@ -1,3 +1,13 @@
.roomsContainer {
position: relative;
scroll-margin-top: var(--hotel-page-scroll-margin-top);
}
.scrollRef {
position: absolute;
top: calc(-1 * var(--hotel-page-scroll-margin-top));
}
.ctaContainer {
display: flex;
justify-content: center;

View File

@@ -1,8 +1,12 @@
"use client"
import { useRouter } from "next/navigation"
import { useEffect } from "react"
import { useIntl } from "react-intl"
import Link from "@/components/TempDesignSystem/Link"
import useHash from "@/hooks/useHash"
import useScrollSpy from "@/hooks/useScrollSpy"
import styles from "./tabNavigation.module.css"
@@ -11,50 +15,78 @@ import {
type TabNavigationProps,
} from "@/types/components/hotelPage/tabNavigation"
export default function TabNavigation({ restaurantTitle }: TabNavigationProps) {
export default function TabNavigation({
restaurantTitle,
hasActivities,
hasFAQ,
}: TabNavigationProps) {
const hash = useHash()
const intl = useIntl()
const router = useRouter()
const hotelTabLinks: { href: HotelHashValues | string; text: string }[] = [
const tabLinks: { hash: HotelHashValues; text: string }[] = [
{
href: HotelHashValues.overview,
hash: HotelHashValues.overview,
text: intl.formatMessage({ id: "Overview" }),
},
{ href: HotelHashValues.rooms, text: intl.formatMessage({ id: "Rooms" }) },
{ hash: HotelHashValues.rooms, text: intl.formatMessage({ id: "Rooms" }) },
{
href: HotelHashValues.restaurant,
hash: HotelHashValues.restaurant,
text: intl.formatMessage({ id: restaurantTitle }, { count: 1 }),
},
{
href: HotelHashValues.meetings,
hash: HotelHashValues.meetings,
text: intl.formatMessage({ id: "Meetings & Conferences" }),
},
{
href: HotelHashValues.wellness,
hash: HotelHashValues.wellness,
text: intl.formatMessage({ id: "Wellness & Exercise" }),
},
{
href: HotelHashValues.activities,
text: intl.formatMessage({ id: "Activities" }),
},
{ href: HotelHashValues.faq, text: intl.formatMessage({ id: "FAQ" }) },
...(hasActivities
? [
{
hash: HotelHashValues.activities,
text: intl.formatMessage({ id: "Activities" }),
},
]
: []),
...(hasFAQ
? [
{
hash: HotelHashValues.faq,
text: intl.formatMessage({ id: "FAQ" }),
},
]
: []),
]
const { activeSectionId, pauseScrollSpy } = useScrollSpy(
tabLinks.map(({ hash }) => hash)
)
useEffect(() => {
if (activeSectionId) {
router.replace(`#${activeSectionId}`, { scroll: false })
}
}, [activeSectionId, router])
return (
<div className={styles.stickyWrapper}>
<nav className={styles.tabsContainer}>
{hotelTabLinks.map((link) => {
{tabLinks.map((link) => {
const isActive =
hash === link.href ||
(hash === "" && link.href === HotelHashValues.overview)
hash === link.hash ||
(!hash && link.hash === HotelHashValues.overview)
return (
<Link
key={link.href}
href={link.href}
key={link.hash}
href={`#${link.hash}`}
active={isActive}
variant="tab"
color="burgundy"
textDecoration="none"
scroll={true}
onClick={pauseScrollSpy}
>
{intl.formatMessage({ id: link.text })}
</Link>

View File

@@ -1,6 +1,6 @@
.stickyWrapper {
position: sticky;
top: 0;
top: var(--booking-widget-mobile-height);
z-index: 1;
background-color: var(--Base-Surface-Subtle-Normal);
border-bottom: 1px solid var(--Base-Border-Subtle);
@@ -16,6 +16,12 @@
width: 100%;
}
@media screen and (min-width: 768px) {
.stickyWrapper {
top: var(--booking-widget-desktop-height);
}
}
@media screen and (min-width: 1367px) {
.tabsContainer {
padding: 0 var(--Spacing-x5);

View File

@@ -1,4 +1,8 @@
.pageContainer {
--hotel-page-navigation-height: 59px;
--hotel-page-scroll-margin-top: calc(
var(--hotel-page-navigation-height) + var(--Spacing-x2)
);
display: grid;
grid-template-areas:
"hotelImages"
@@ -26,6 +30,7 @@
.introContainer {
display: grid;
gap: var(--Spacing-x4);
scroll-margin-top: var(--hotel-page-scroll-margin-top);
}
@media screen and (min-width: 1367px) {
@@ -52,7 +57,7 @@
.mapWithCard {
position: sticky;
top: 0;
top: var(--booking-widget-desktop-height);
min-height: 500px; /* Fixed min to not cover the marker with the card */
height: calc(
100vh - var(--main-menu-desktop-height) -

View File

@@ -21,6 +21,8 @@ import TabNavigation from "./TabNavigation"
import styles from "./hotelPage.module.css"
import { HotelHashValues } from "@/types/components/hotelPage/tabNavigation"
export default async function HotelPage() {
const intl = await getIntl()
const lang = getLang()
@@ -61,9 +63,11 @@ export default async function HotelPage() {
</div>
<TabNavigation
restaurantTitle={getRestaurantHeading(hotelDetailedFacilities)}
hasActivities={!!activitiesCard}
hasFAQ={false}
/>
<main className={styles.mainSection}>
<div className={styles.introContainer}>
<div id={HotelHashValues.overview} className={styles.introContainer}>
<IntroSection
hotelName={hotelName}
hotelDescription={hotelDescription}

View File

@@ -3,21 +3,26 @@
import { useParams } from "next/navigation"
import { useEffect, useState } from "react"
function getHash() {
return typeof window !== "undefined" ? window.location.hash : undefined
}
function useHash() {
const [isClient, setIsClient] = useState(false)
const [hash, setHash] = useState(getHash())
export default function useHash() {
const [hash, setHash] = useState<string | undefined>(undefined)
const params = useParams()
useEffect(() => {
setIsClient(true)
setHash(getHash())
}, [params, setHash, setIsClient])
const updateHash = () => {
const newHash = window.location.hash
? window.location.hash.slice(1)
: undefined
setHash(newHash)
}
return isClient ? hash : null
updateHash()
window.addEventListener("hashchange", updateHash)
return () => {
window.removeEventListener("hashchange", updateHash)
}
}, [params])
return hash
}
export default useHash

67
hooks/useScrollSpy.ts Normal file
View File

@@ -0,0 +1,67 @@
import { useCallback, useEffect, useMemo, useRef, useState } from "react"
export default function useScrollSpy(
sectionIds: string[],
options: IntersectionObserverInit = {}
): {
activeSectionId: string
pauseScrollSpy: () => void
} {
const [activeSectionId, setActiveSectionId] = useState("")
const observerIsInactive = useRef(false)
const mergedOptions = useMemo(
() => ({
root: null,
// Make sure only to activate the section when it reaches the top of the viewport.
// A negative value for rootMargin shrinks the root bounding box inward,
// meaning elements will only be considered intersecting when they are further inside the viewport.
rootMargin: "-8% 0% -90% 0%",
threshold: 0,
...options,
}),
[options]
)
const handleIntersection = useCallback(
(entries: IntersectionObserverEntry[]) => {
if (observerIsInactive.current) {
return
}
const intersectingEntries: IntersectionObserverEntry[] = []
entries.forEach((e) => {
if (e.isIntersecting) {
intersectingEntries.push(e)
}
})
if (intersectingEntries.length) {
setActiveSectionId(intersectingEntries[0].target.id)
}
},
[]
)
useEffect(() => {
const observer = new IntersectionObserver(handleIntersection, mergedOptions)
const elements = sectionIds
.map((id) => document.getElementById(id))
.filter((el): el is HTMLElement => !!el)
elements.forEach((element) => {
observer.observe(element)
})
return () => elements.forEach((el) => el && observer.unobserve(el))
}, [sectionIds, mergedOptions, handleIntersection])
const pauseScrollSpy = () => {
observerIsInactive.current = true
setTimeout(() => {
observerIsInactive.current = false
}, 500)
}
return { activeSectionId, pauseScrollSpy }
}

View File

@@ -1,13 +1,15 @@
export enum HotelHashValues { // Should these be translated?
overview = "#overview",
rooms = "#rooms-section",
restaurant = "#restaurant-and-bar",
meetings = "#meetings-and-conferences",
wellness = "#wellness-and-exercise",
activities = "#activities",
faq = "#faq",
export enum HotelHashValues {
overview = "overview",
rooms = "rooms-section",
restaurant = "restaurant-and-bar",
meetings = "meetings-and-conferences",
wellness = "wellness-and-exercise",
activities = "activities",
faq = "faq",
}
export type TabNavigationProps = {
restaurantTitle: string
hasActivities: boolean
hasFAQ: boolean
}