Merged in feat/SW-904-add-back-to-top-button (pull request #923)
Feat/SW-904 add back to top button on hotel list page * feat(SW-904): Added back to top button * fix: removed alert on hotel listing page * Remove console.log Approved-by: Niclas Edenvin
This commit is contained in:
committed by
Niclas Edenvin
parent
bd0720dc0f
commit
60f1d268a9
@@ -7,7 +7,6 @@ import { selectHotelMap, selectRate } from "@/constants/routes/hotelReservation"
|
|||||||
|
|
||||||
import { mapFacilityToIcon } from "@/components/ContentType/HotelPage/data"
|
import { mapFacilityToIcon } from "@/components/ContentType/HotelPage/data"
|
||||||
import ImageGallery from "@/components/ImageGallery"
|
import ImageGallery from "@/components/ImageGallery"
|
||||||
import Alert from "@/components/TempDesignSystem/Alert"
|
|
||||||
import Button from "@/components/TempDesignSystem/Button"
|
import Button from "@/components/TempDesignSystem/Button"
|
||||||
import Divider from "@/components/TempDesignSystem/Divider"
|
import Divider from "@/components/TempDesignSystem/Divider"
|
||||||
import Link from "@/components/TempDesignSystem/Link"
|
import Link from "@/components/TempDesignSystem/Link"
|
||||||
@@ -133,13 +132,6 @@ export default function HotelCard({
|
|||||||
hotel={hotelData}
|
hotel={hotelData}
|
||||||
showCTA={true}
|
showCTA={true}
|
||||||
/>
|
/>
|
||||||
{hotelData.specialAlerts.length > 0 && (
|
|
||||||
<div className={styles.specialAlerts}>
|
|
||||||
{hotelData.specialAlerts.map((alert) => (
|
|
||||||
<Alert key={alert.id} type={alert.type} text={alert.text} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</section>
|
</section>
|
||||||
<HotelPriceList price={price} hotelId={hotel.hotelData.operaId} />
|
<HotelPriceList price={price} hotelId={hotel.hotelData.operaId} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
"use client"
|
"use client"
|
||||||
import { useSearchParams } from "next/navigation"
|
import { useSearchParams } from "next/navigation"
|
||||||
import { useMemo } from "react"
|
import { useEffect, useMemo, useState } from "react"
|
||||||
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
import { useHotelFilterStore } from "@/stores/hotel-filters"
|
import { useHotelFilterStore } from "@/stores/hotel-filters"
|
||||||
|
|
||||||
|
import { BackToTopButton } from "@/components/TempDesignSystem/BackToTopButton"
|
||||||
|
|
||||||
import HotelCard from "../HotelCard"
|
import HotelCard from "../HotelCard"
|
||||||
import { DEFAULT_SORT } from "../SelectHotel/HotelSorter"
|
import { DEFAULT_SORT } from "../SelectHotel/HotelSorter"
|
||||||
|
|
||||||
@@ -22,9 +25,11 @@ export default function HotelCardListing({
|
|||||||
activeCard,
|
activeCard,
|
||||||
onHotelCardHover,
|
onHotelCardHover,
|
||||||
}: HotelCardListingProps) {
|
}: HotelCardListingProps) {
|
||||||
|
const intl = useIntl()
|
||||||
const searchParams = useSearchParams()
|
const searchParams = useSearchParams()
|
||||||
const activeFilters = useHotelFilterStore((state) => state.activeFilters)
|
const activeFilters = useHotelFilterStore((state) => state.activeFilters)
|
||||||
const setResultCount = useHotelFilterStore((state) => state.setResultCount)
|
const setResultCount = useHotelFilterStore((state) => state.setResultCount)
|
||||||
|
const [showBackToTop, setShowBackToTop] = useState<boolean>(false)
|
||||||
|
|
||||||
const sortBy = useMemo(
|
const sortBy = useMemo(
|
||||||
() => searchParams.get("sort") ?? DEFAULT_SORT,
|
() => searchParams.get("sort") ?? DEFAULT_SORT,
|
||||||
@@ -82,6 +87,20 @@ export default function HotelCardListing({
|
|||||||
return filteredHotels
|
return filteredHotels
|
||||||
}, [activeFilters, sortedHotels, setResultCount])
|
}, [activeFilters, sortedHotels, setResultCount])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleScroll = () => {
|
||||||
|
const hasScrolledPast = window.scrollY > 490
|
||||||
|
setShowBackToTop(hasScrolledPast)
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener("scroll", handleScroll, { passive: true })
|
||||||
|
return () => window.removeEventListener("scroll", handleScroll)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
function scrollToTop() {
|
||||||
|
window.scrollTo({ top: 0, behavior: "smooth" })
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className={styles.hotelCards}>
|
<section className={styles.hotelCards}>
|
||||||
{hotels?.length
|
{hotels?.length
|
||||||
@@ -95,6 +114,7 @@ export default function HotelCardListing({
|
|||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
: null}
|
: null}
|
||||||
|
{showBackToTop && <BackToTopButton onClick={scrollToTop} />}
|
||||||
</section>
|
</section>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,8 +7,9 @@ import { useMediaQuery } from "usehooks-ts"
|
|||||||
|
|
||||||
import { selectHotel } from "@/constants/routes/hotelReservation"
|
import { selectHotel } from "@/constants/routes/hotelReservation"
|
||||||
|
|
||||||
import { CloseIcon, CloseLargeIcon } from "@/components/Icons"
|
import { ArrowUpIcon, CloseIcon, CloseLargeIcon } from "@/components/Icons"
|
||||||
import InteractiveMap from "@/components/Maps/InteractiveMap"
|
import InteractiveMap from "@/components/Maps/InteractiveMap"
|
||||||
|
import { BackToTopButton } from "@/components/TempDesignSystem/BackToTopButton"
|
||||||
import Button from "@/components/TempDesignSystem/Button"
|
import Button from "@/components/TempDesignSystem/Button"
|
||||||
import useLang from "@/hooks/useLang"
|
import useLang from "@/hooks/useLang"
|
||||||
|
|
||||||
@@ -109,17 +110,7 @@ export default function SelectHotelMap({
|
|||||||
activeHotelPin={activeHotelPin}
|
activeHotelPin={activeHotelPin}
|
||||||
setActiveHotelPin={setActiveHotelPin}
|
setActiveHotelPin={setActiveHotelPin}
|
||||||
/>
|
/>
|
||||||
{showBackToTop && (
|
{showBackToTop && <BackToTopButton onClick={scrollToTop} />}
|
||||||
<Button
|
|
||||||
intent="inverted"
|
|
||||||
size="small"
|
|
||||||
theme="base"
|
|
||||||
className={styles.backToTopButton}
|
|
||||||
onClick={scrollToTop}
|
|
||||||
>
|
|
||||||
{intl.formatMessage({ id: "Back to top" })}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
<InteractiveMap
|
<InteractiveMap
|
||||||
closeButton={closeButton}
|
closeButton={closeButton}
|
||||||
|
|||||||
@@ -23,10 +23,6 @@
|
|||||||
height: 44px;
|
height: 44px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.backToTopButton {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 768px) {
|
@media (min-width: 768px) {
|
||||||
.container .closeButton {
|
.container .closeButton {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -34,12 +30,7 @@
|
|||||||
.container .listingContainer .filterContainer .filterContainerCloseButton {
|
.container .listingContainer .filterContainer .filterContainerCloseButton {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
.backToTopButton {
|
|
||||||
position: fixed;
|
|
||||||
bottom: 24px;
|
|
||||||
left: 32px;
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
.listingContainer {
|
.listingContainer {
|
||||||
background-color: var(--Base-Surface-Secondary-light-Normal);
|
background-color: var(--Base-Surface-Secondary-light-Normal);
|
||||||
padding: var(--Spacing-x3) var(--Spacing-x4);
|
padding: var(--Spacing-x3) var(--Spacing-x4);
|
||||||
|
|||||||
33
components/Icons/ArrowUp.tsx
Normal file
33
components/Icons/ArrowUp.tsx
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import { iconVariants } from "./variants"
|
||||||
|
|
||||||
|
import type { IconProps } from "@/types/components/icon"
|
||||||
|
|
||||||
|
export default function ArrowUpIcon({ className, color, ...props }: IconProps) {
|
||||||
|
const classNames = iconVariants({ className, color })
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
className={classNames}
|
||||||
|
fill="none"
|
||||||
|
height="20"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
width="20"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<mask
|
||||||
|
id="a"
|
||||||
|
width="20"
|
||||||
|
height="20"
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
maskUnits="userSpaceOnUse"
|
||||||
|
>
|
||||||
|
<path fill="#D9D9D9" d="M0 0h20v20H0z" />
|
||||||
|
</mask>
|
||||||
|
<path
|
||||||
|
fill="#4D001B"
|
||||||
|
d="m9.219 6.541-4.021 4.021a.74.74 0 0 1-.552.235.778.778 0 0 1-.552-.245.796.796 0 0 1-.235-.552.74.74 0 0 1 .235-.552l5.354-5.355a.77.77 0 0 1 .849-.171.77.77 0 0 1 .255.171l5.354 5.355a.782.782 0 0 1 0 1.104.764.764 0 0 1-1.114 0l-4.01-4.01v9.135c0 .215-.077.4-.23.552a.752.752 0 0 1-.552.229.752.752 0 0 1-.552-.23.752.752 0 0 1-.23-.551V6.54Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ export { default as AirIcon } from "./Air"
|
|||||||
export { default as AirplaneIcon } from "./Airplane"
|
export { default as AirplaneIcon } from "./Airplane"
|
||||||
export { default as AllergyIcon } from "./Allergy"
|
export { default as AllergyIcon } from "./Allergy"
|
||||||
export { default as ArrowRightIcon } from "./ArrowRight"
|
export { default as ArrowRightIcon } from "./ArrowRight"
|
||||||
|
export { default as ArrowUpIcon } from "./ArrowUp"
|
||||||
export { default as BarIcon } from "./Bar"
|
export { default as BarIcon } from "./Bar"
|
||||||
export { default as BathtubIcon } from "./Bathtub"
|
export { default as BathtubIcon } from "./Bathtub"
|
||||||
export { default as BedDoubleIcon } from "./BedDouble"
|
export { default as BedDoubleIcon } from "./BedDouble"
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
.backToTopButton {
|
||||||
|
border-radius: var(--Corner-radius-Rounded);
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
position: fixed;
|
||||||
|
bottom: 20px;
|
||||||
|
right: 20px;
|
||||||
|
z-index: 1000;
|
||||||
|
background-color: var(--Base-Surface-Primary-light-Normal);
|
||||||
|
color: var(--Base-Button-Secondary-On-Fill-Normal);
|
||||||
|
border: 2px solid var(--Base-Button-Secondary-On-Fill-Normal);
|
||||||
|
gap: var(--Spacing-x-half);
|
||||||
|
padding: var(--Spacing-x1);
|
||||||
|
text-align: center;
|
||||||
|
transition:
|
||||||
|
background-color 300ms ease,
|
||||||
|
color 300ms ease;
|
||||||
|
font-family: var(--typography-Body-Bold-fontFamily);
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: var(--typography-Caption-Bold-fontSize);
|
||||||
|
line-height: var(--typography-Caption-Bold-lineHeight);
|
||||||
|
letter-spacing: 0.6%;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.backToTopButtonText {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
.backToTopButtonText {
|
||||||
|
display: initial;
|
||||||
|
}
|
||||||
|
.backToTopButton:hover {
|
||||||
|
background-color: var(--Base-Button-Tertiary-Fill-Normal);
|
||||||
|
color: var(--Base-Button-Tertiary-On-Fill-Hover);
|
||||||
|
}
|
||||||
|
.backToTopButton:hover > svg * {
|
||||||
|
fill: var(--Base-Button-Tertiary-On-Fill-Hover);
|
||||||
|
}
|
||||||
|
.backToTopButton {
|
||||||
|
padding: calc(var(--Spacing-x1) + 2px) var(--Spacing-x2);
|
||||||
|
}
|
||||||
|
}
|
||||||
20
components/TempDesignSystem/BackToTopButton/index.tsx
Normal file
20
components/TempDesignSystem/BackToTopButton/index.tsx
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { Button as ButtonRAC } from "react-aria-components"
|
||||||
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
|
import { ArrowUpIcon } from "@/components/Icons"
|
||||||
|
|
||||||
|
import styles from "./backToTopButton.module.css"
|
||||||
|
|
||||||
|
export function BackToTopButton({ onClick }: { onClick: () => void }) {
|
||||||
|
const intl = useIntl()
|
||||||
|
return (
|
||||||
|
<ButtonRAC className={styles.backToTopButton} onPress={onClick}>
|
||||||
|
<ArrowUpIcon color="burgundy" />
|
||||||
|
<span className={styles.backToTopButtonText}>
|
||||||
|
{intl.formatMessage({ id: "Back to top" })}
|
||||||
|
</span>
|
||||||
|
</ButtonRAC>
|
||||||
|
)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user