feat(SW-1446): add Jump to functionality to Destination Overview Page
This commit is contained in:
@@ -15,6 +15,7 @@ import CookieBotConsent from "@/components/CookieBot"
|
|||||||
import Footer from "@/components/Footer"
|
import Footer from "@/components/Footer"
|
||||||
import Header from "@/components/Header"
|
import Header from "@/components/Header"
|
||||||
import StorageCleaner from "@/components/HotelReservation/EnterDetails/StorageCleaner"
|
import StorageCleaner from "@/components/HotelReservation/EnterDetails/StorageCleaner"
|
||||||
|
import { RACRouterProvider } from "@/components/RACRouterProvider"
|
||||||
import SitewideAlert from "@/components/SitewideAlert"
|
import SitewideAlert from "@/components/SitewideAlert"
|
||||||
import { ToastHandler } from "@/components/TempDesignSystem/Toasts"
|
import { ToastHandler } from "@/components/TempDesignSystem/Toasts"
|
||||||
import AdobeSDKScript from "@/components/TrackingSDK/AdobeSDKScript"
|
import AdobeSDKScript from "@/components/TrackingSDK/AdobeSDKScript"
|
||||||
@@ -56,27 +57,31 @@ export default async function RootLayout({
|
|||||||
`}</Script>
|
`}</Script>
|
||||||
</head>
|
</head>
|
||||||
<body className="scandic">
|
<body className="scandic">
|
||||||
<SessionProvider basePath="/api/web/auth">
|
<div className="root">
|
||||||
<ClientIntlProvider
|
<SessionProvider basePath="/api/web/auth">
|
||||||
defaultLocale={Lang.en}
|
<ClientIntlProvider
|
||||||
locale={params.lang}
|
defaultLocale={Lang.en}
|
||||||
messages={messages}
|
locale={params.lang}
|
||||||
>
|
messages={messages}
|
||||||
<TrpcProvider>
|
>
|
||||||
<RouterTracking />
|
<TrpcProvider>
|
||||||
<SitewideAlert />
|
<RACRouterProvider>
|
||||||
<Header />
|
<RouterTracking />
|
||||||
{bookingwidget}
|
<SitewideAlert />
|
||||||
{children}
|
<Header />
|
||||||
<Footer />
|
{bookingwidget}
|
||||||
<ToastHandler />
|
{children}
|
||||||
<SessionRefresher />
|
<Footer />
|
||||||
<StorageCleaner />
|
<ToastHandler />
|
||||||
<CookieBotConsent />
|
<SessionRefresher />
|
||||||
<ReactQueryDevtools initialIsOpen={false} />
|
<StorageCleaner />
|
||||||
</TrpcProvider>
|
<CookieBotConsent />
|
||||||
</ClientIntlProvider>
|
<ReactQueryDevtools initialIsOpen={false} />
|
||||||
</SessionProvider>
|
</RACRouterProvider>
|
||||||
|
</TrpcProvider>
|
||||||
|
</ClientIntlProvider>
|
||||||
|
</SessionProvider>
|
||||||
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -63,6 +63,23 @@ body.overflow-hidden {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.root {
|
||||||
|
isolation: isolate;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* From Tailwind */
|
||||||
|
.sr-only {
|
||||||
|
position: absolute;
|
||||||
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
|
padding: 0;
|
||||||
|
margin: -1px;
|
||||||
|
overflow: hidden;
|
||||||
|
clip: rect(0, 0, 0, 0);
|
||||||
|
white-space: nowrap;
|
||||||
|
border-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
ul {
|
ul {
|
||||||
padding-inline-start: 0;
|
padding-inline-start: 0;
|
||||||
margin-block-start: 0;
|
margin-block-start: 0;
|
||||||
|
|||||||
@@ -0,0 +1,114 @@
|
|||||||
|
.label {
|
||||||
|
color: var(--Base-Text-Accent);
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
background: var(--Surface-Primary-Default);
|
||||||
|
padding: var(--Space-x15) var(--Space-x15) var(--Space-x15) var(--Space-x3);
|
||||||
|
border-radius: var(--Corner-radius-Rounded);
|
||||||
|
border: solid 1px var(--Border-Default);
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form:focus-within {
|
||||||
|
border-color: var(--UI-Input-Controls-Border-Focus);
|
||||||
|
|
||||||
|
& label {
|
||||||
|
color: var(--UI-Text-Active);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.autocomplete {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.searchField:focus-within + .results {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fields {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clearButton {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
right: 0;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
background: none;
|
||||||
|
border: 0;
|
||||||
|
color: var(--Text-Heading);
|
||||||
|
padding: var(--Space-x15); /* search field vertical padding */
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input {
|
||||||
|
width: 100%;
|
||||||
|
border: 0;
|
||||||
|
background: transparent;
|
||||||
|
|
||||||
|
&::-webkit-search-cancel-button,
|
||||||
|
&::-webkit-search-decoration {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::placeholder {
|
||||||
|
color: var(--UI-Text-Placeholder);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.results {
|
||||||
|
position: relative;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.searchButton {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--Space-x05);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menuContainer {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--Space-x2);
|
||||||
|
background: var(--Base-Surface-Primary-light-Normal);
|
||||||
|
border-radius: var(--Corner-radius-Large);
|
||||||
|
padding: var(--Space-x2);
|
||||||
|
width: 360px;
|
||||||
|
max-height: 430px;
|
||||||
|
box-sizing: content-box;
|
||||||
|
box-shadow: var(--BoxShadow-Level-4);
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: var(--Space-x2);
|
||||||
|
z-index: 50;
|
||||||
|
overflow-y: auto;
|
||||||
|
|
||||||
|
& > div {
|
||||||
|
transition: opacity 0.2s 0.2s linear;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.pending > div {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (min-width: 1367px) {
|
||||||
|
.autocomplete {
|
||||||
|
max-width: 680px;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,161 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { cx } from "class-variance-authority"
|
||||||
|
import { useRouter } from "next/navigation"
|
||||||
|
import { memo, useMemo, useTransition } from "react"
|
||||||
|
import {
|
||||||
|
Autocomplete,
|
||||||
|
Button as ButtonRAC,
|
||||||
|
Input,
|
||||||
|
Label,
|
||||||
|
SearchField,
|
||||||
|
} from "react-aria-components"
|
||||||
|
import { useIntl } from "react-intl"
|
||||||
|
import { useIsMounted } from "usehooks-ts"
|
||||||
|
|
||||||
|
import { Button } from "@scandic-hotels/design-system/Button"
|
||||||
|
import { MaterialIcon } from "@scandic-hotels/design-system/Icons"
|
||||||
|
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||||
|
|
||||||
|
import { ResultHistory } from "../Results/ResultHistory"
|
||||||
|
import { ResultMatches } from "../Results/ResultMatches"
|
||||||
|
|
||||||
|
import styles from "./clientInline.module.css"
|
||||||
|
|
||||||
|
import type { ClientProps } from "@/types/components/destinationOverviewPage/jumpTo/client"
|
||||||
|
|
||||||
|
const ResultMatchesMemo = memo(ResultMatches)
|
||||||
|
const ResultHistoryMemo = memo(ResultHistory)
|
||||||
|
|
||||||
|
export function ClientInline({
|
||||||
|
results,
|
||||||
|
latest,
|
||||||
|
setFilterString,
|
||||||
|
onAction,
|
||||||
|
onClearHistory,
|
||||||
|
}: ClientProps) {
|
||||||
|
const router = useRouter()
|
||||||
|
const intl = useIntl()
|
||||||
|
const [isPending, startTransition] = useTransition()
|
||||||
|
const isMounted = useIsMounted()
|
||||||
|
|
||||||
|
const showResults = !!results
|
||||||
|
const showHistory = isMounted() && (!results || results.length === 0)
|
||||||
|
|
||||||
|
const latestResults = useMemo(() => {
|
||||||
|
return latest.concat({
|
||||||
|
id: "actions", // The string "Actions" converts into a divider below
|
||||||
|
name: "Actions",
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
id: "clearHistory",
|
||||||
|
type: "clearHistory",
|
||||||
|
displayName: intl.formatMessage({ id: "Clear searches" }),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
}, [intl, latest])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Autocomplete>
|
||||||
|
<div className={styles.autocomplete}>
|
||||||
|
<SearchField
|
||||||
|
className={styles.searchField}
|
||||||
|
onClear={() => {
|
||||||
|
startTransition(() => {
|
||||||
|
setFilterString(null)
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{({ state }) => (
|
||||||
|
<form
|
||||||
|
className={styles.form}
|
||||||
|
onSubmit={(evt) => {
|
||||||
|
evt.preventDefault()
|
||||||
|
evt.stopPropagation()
|
||||||
|
|
||||||
|
startTransition(() => {
|
||||||
|
if (results) {
|
||||||
|
const firstItem = results[0].children[0]
|
||||||
|
onAction(firstItem.id)
|
||||||
|
if (firstItem.url) {
|
||||||
|
router.push(firstItem.url)
|
||||||
|
}
|
||||||
|
} else if (latest) {
|
||||||
|
const firstItem = latest[0].children[0]
|
||||||
|
onAction(firstItem.id)
|
||||||
|
if (firstItem.url) {
|
||||||
|
router.push(firstItem.url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className={styles.fields}>
|
||||||
|
<Typography variant="Body/Supporting text (caption)/smBold">
|
||||||
|
<Label className={styles.label}>
|
||||||
|
{intl.formatMessage({ id: "Where to?" })}
|
||||||
|
</Label>
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="Body/Paragraph/mdRegular">
|
||||||
|
<Input
|
||||||
|
className={styles.input}
|
||||||
|
placeholder={intl.formatMessage({
|
||||||
|
id: "Hotels & Destinations",
|
||||||
|
})}
|
||||||
|
onChange={(evt) => {
|
||||||
|
startTransition(() => {
|
||||||
|
if (evt.currentTarget.value) {
|
||||||
|
setFilterString(evt.currentTarget.value)
|
||||||
|
} else {
|
||||||
|
setFilterString(null)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
{state.value !== "" && (
|
||||||
|
<Typography variant="Body/Supporting text (caption)/smBold">
|
||||||
|
<ButtonRAC className={styles.clearButton}>Clear</ButtonRAC>
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Typography variant="Body/Supporting text (caption)/smBold">
|
||||||
|
<Button
|
||||||
|
className={styles.searchButton}
|
||||||
|
variant="Primary"
|
||||||
|
size="Small"
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
|
<MaterialIcon icon="search" color="CurrentColor" />
|
||||||
|
{intl.formatMessage({ id: "Search" })}
|
||||||
|
</Button>
|
||||||
|
</Typography>
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
|
</SearchField>
|
||||||
|
<div className={styles.results}>
|
||||||
|
<div
|
||||||
|
className={cx({
|
||||||
|
[styles.menuContainer]: true,
|
||||||
|
[styles.pending]: isPending,
|
||||||
|
})}
|
||||||
|
aria-live="polite"
|
||||||
|
>
|
||||||
|
{showResults ? (
|
||||||
|
<ResultMatchesMemo results={results} onAction={onAction} />
|
||||||
|
) : null}
|
||||||
|
{showHistory ? (
|
||||||
|
<ResultHistoryMemo
|
||||||
|
results={latestResults}
|
||||||
|
onClearHistory={onClearHistory}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Autocomplete>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,187 @@
|
|||||||
|
.label {
|
||||||
|
color: var(--Base-Text-Accent);
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.placeholder {
|
||||||
|
color: var(--UI-Text-Placeholder);
|
||||||
|
}
|
||||||
|
|
||||||
|
.searchField {
|
||||||
|
background: var(--Base-Background-Primary-Normal);
|
||||||
|
padding: var(--Space-x1) var(--Space-x15);
|
||||||
|
border-radius: var(--Corner-radius-Medium);
|
||||||
|
border: solid 1px transparent;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.searchField:focus-within {
|
||||||
|
border-color: var(--UI-Input-Controls-Border-Focus);
|
||||||
|
|
||||||
|
& label {
|
||||||
|
color: var(--UI-Text-Active);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.autocomplete {
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: auto 1fr auto;
|
||||||
|
gap: var(--Space-x4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.clearButton {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
right: 0;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
background: none;
|
||||||
|
border: 0;
|
||||||
|
color: var(--Text-Heading);
|
||||||
|
padding: var(--Space-x15); /* search field vertical padding */
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input {
|
||||||
|
width: 100%;
|
||||||
|
border: 0;
|
||||||
|
background: transparent;
|
||||||
|
|
||||||
|
&::-webkit-search-cancel-button,
|
||||||
|
&::-webkit-search-decoration {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::placeholder {
|
||||||
|
color: var(--UI-Text-Placeholder);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.results {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menuContainer {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
overflow-y: auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--Space-x2);
|
||||||
|
|
||||||
|
& > div {
|
||||||
|
transition: opacity 0.2s 0.2s linear;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.pending > div {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.trigger {
|
||||||
|
background: var(--Base-Surface-Primary-light-Normal);
|
||||||
|
padding: var(--Space-x15) var(--Space-x15) var(--Space-x15) var(--Space-x3);
|
||||||
|
border: solid 1px var(--Border-Intense);
|
||||||
|
border-radius: var(--Corner-radius-Rounded);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
text-align: left;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
& .icon {
|
||||||
|
background: var(--Base-Button-Primary-Fill-Normal);
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: var(--Corner-radius-Rounded);
|
||||||
|
color: var(--Base-Text-Inverted);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.modalOverlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100vw;
|
||||||
|
height: var(--visual-viewport-height);
|
||||||
|
background: rgba(0, 0, 0, 0.4);
|
||||||
|
|
||||||
|
&[data-entering] {
|
||||||
|
animation: overlay-fade 200ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-exiting] {
|
||||||
|
animation: overlay-fade 150ms reverse ease-in;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal {
|
||||||
|
--padding-x: 16px; /* Not a design token */
|
||||||
|
--height: 660px; /* Not a design token */
|
||||||
|
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: var(--height);
|
||||||
|
max-height: 95vh;
|
||||||
|
padding: var(--Space-x3) var(--padding-x);
|
||||||
|
background: var(--UI-Input-Controls-Surface-Normal);
|
||||||
|
z-index: 100;
|
||||||
|
border-top-left-radius: var(--Corner-radius-Large);
|
||||||
|
border-top-right-radius: var(--Corner-radius-Large);
|
||||||
|
|
||||||
|
&[data-entering] {
|
||||||
|
animation: modal-anim 200ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-exiting] {
|
||||||
|
animation: modal-anim 150ms reverse ease-in;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog {
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: auto 1fr;
|
||||||
|
height: 100%;
|
||||||
|
gap: var(--Space-x3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.closeButton {
|
||||||
|
background: transparent;
|
||||||
|
color: var(--UI-Text-High-contrast);
|
||||||
|
border: 0;
|
||||||
|
justify-self: end;
|
||||||
|
padding: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes overlay-fade {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes modal-anim {
|
||||||
|
from {
|
||||||
|
transform: translateY(100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,189 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { cx } from "class-variance-authority"
|
||||||
|
import { useRouter } from "next/navigation"
|
||||||
|
import { memo, useMemo, useTransition } from "react"
|
||||||
|
import {
|
||||||
|
Autocomplete,
|
||||||
|
Button as ButtonRAC,
|
||||||
|
Dialog,
|
||||||
|
DialogTrigger,
|
||||||
|
Heading,
|
||||||
|
Input,
|
||||||
|
Label,
|
||||||
|
Modal,
|
||||||
|
ModalOverlay,
|
||||||
|
SearchField,
|
||||||
|
} from "react-aria-components"
|
||||||
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
|
import { MaterialIcon } from "@scandic-hotels/design-system/Icons"
|
||||||
|
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||||
|
|
||||||
|
import { ResultHistory } from "../Results/ResultHistory"
|
||||||
|
import { ResultMatches } from "../Results/ResultMatches"
|
||||||
|
|
||||||
|
import styles from "./clientModal.module.css"
|
||||||
|
|
||||||
|
import type { ClientProps } from "@/types/components/destinationOverviewPage/jumpTo/client"
|
||||||
|
|
||||||
|
const ResultMatchesMemo = memo(ResultMatches)
|
||||||
|
const ResultHistoryMemo = memo(ResultHistory)
|
||||||
|
|
||||||
|
export function ClientModal({
|
||||||
|
results,
|
||||||
|
latest,
|
||||||
|
setFilterString,
|
||||||
|
onAction,
|
||||||
|
onClearHistory,
|
||||||
|
}: ClientProps) {
|
||||||
|
const router = useRouter()
|
||||||
|
const intl = useIntl()
|
||||||
|
const [isPending, startTransition] = useTransition()
|
||||||
|
|
||||||
|
const showResults = !!results
|
||||||
|
const showHistory = !results || results.length === 0
|
||||||
|
|
||||||
|
const latestResults = useMemo(() => {
|
||||||
|
return latest.concat({
|
||||||
|
id: "actions", // The string "Actions" converts into a divider below
|
||||||
|
name: "Actions",
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
id: "clearHistory",
|
||||||
|
type: "clearHistory",
|
||||||
|
displayName: intl.formatMessage({ id: "Clear searches" }),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
}, [intl, latest])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DialogTrigger>
|
||||||
|
<ButtonRAC className={styles.trigger}>
|
||||||
|
<div>
|
||||||
|
<Typography variant="Body/Supporting text (caption)/smBold">
|
||||||
|
<div className={styles.label}>
|
||||||
|
{intl.formatMessage({ id: "Where to?" })}
|
||||||
|
</div>
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="Body/Paragraph/mdRegular">
|
||||||
|
<div className={styles.placeholder}>
|
||||||
|
{intl.formatMessage({ id: "Hotels & Destinations" })}
|
||||||
|
</div>
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
<div className={styles.icon}>
|
||||||
|
<MaterialIcon icon="search" color="CurrentColor" />
|
||||||
|
</div>
|
||||||
|
</ButtonRAC>
|
||||||
|
<ModalOverlay className={styles.modalOverlay} isDismissable={true}>
|
||||||
|
<Modal className={styles.modal}>
|
||||||
|
<Dialog className={styles.dialog}>
|
||||||
|
<Heading level={2} className="sr-only">
|
||||||
|
{intl.formatMessage({ id: "Find a location" })}
|
||||||
|
</Heading>
|
||||||
|
<ButtonRAC
|
||||||
|
className={styles.closeButton}
|
||||||
|
slot="close"
|
||||||
|
aria-label={intl.formatMessage({ id: "Close" })}
|
||||||
|
>
|
||||||
|
<MaterialIcon icon="close" color="CurrentColor" />
|
||||||
|
</ButtonRAC>
|
||||||
|
<Autocomplete>
|
||||||
|
<div className={styles.autocomplete}>
|
||||||
|
<SearchField
|
||||||
|
autoFocus
|
||||||
|
className={styles.searchField}
|
||||||
|
onClear={() => {
|
||||||
|
startTransition(() => {
|
||||||
|
setFilterString(null)
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{({ state }) => (
|
||||||
|
<form
|
||||||
|
onSubmit={(evt) => {
|
||||||
|
evt.preventDefault()
|
||||||
|
evt.stopPropagation()
|
||||||
|
|
||||||
|
startTransition(() => {
|
||||||
|
if (results) {
|
||||||
|
const firstItem = results[0].children[0]
|
||||||
|
onAction(firstItem.id)
|
||||||
|
if (firstItem.url) {
|
||||||
|
router.push(firstItem.url)
|
||||||
|
}
|
||||||
|
} else if (latest) {
|
||||||
|
const firstItem = latest[0].children[0]
|
||||||
|
onAction(firstItem.id)
|
||||||
|
if (firstItem.url) {
|
||||||
|
router.push(firstItem.url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography variant="Body/Supporting text (caption)/smBold">
|
||||||
|
<Label className={styles.label}>
|
||||||
|
{intl.formatMessage({ id: "Where to?" })}
|
||||||
|
</Label>
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="Body/Paragraph/mdRegular">
|
||||||
|
<Input
|
||||||
|
className={styles.input}
|
||||||
|
placeholder={intl.formatMessage({
|
||||||
|
id: "Hotels & Destinations",
|
||||||
|
})}
|
||||||
|
onChange={(evt) => {
|
||||||
|
startTransition(() => {
|
||||||
|
if (evt.currentTarget.value) {
|
||||||
|
setFilterString(evt.currentTarget.value)
|
||||||
|
} else {
|
||||||
|
setFilterString(null)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
{state.value !== "" && (
|
||||||
|
<Typography variant="Body/Supporting text (caption)/smBold">
|
||||||
|
<ButtonRAC className={styles.clearButton}>
|
||||||
|
Clear
|
||||||
|
</ButtonRAC>
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
|
</SearchField>
|
||||||
|
<div className={styles.results}>
|
||||||
|
<div
|
||||||
|
className={cx({
|
||||||
|
[styles.menuContainer]: true,
|
||||||
|
[styles.pending]: isPending,
|
||||||
|
})}
|
||||||
|
aria-live="polite"
|
||||||
|
>
|
||||||
|
{showResults ? (
|
||||||
|
<ResultMatchesMemo
|
||||||
|
results={results}
|
||||||
|
onAction={onAction}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
{showHistory ? (
|
||||||
|
<ResultHistoryMemo
|
||||||
|
results={latestResults}
|
||||||
|
onClearHistory={onClearHistory}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Autocomplete>
|
||||||
|
</Dialog>
|
||||||
|
</Modal>
|
||||||
|
</ModalOverlay>
|
||||||
|
</DialogTrigger>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import {
|
||||||
|
Collection,
|
||||||
|
Header,
|
||||||
|
Menu,
|
||||||
|
MenuItem,
|
||||||
|
MenuSection,
|
||||||
|
Text,
|
||||||
|
} from "react-aria-components"
|
||||||
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
|
import { Button } from "@scandic-hotels/design-system/Button"
|
||||||
|
import { MaterialIcon } from "@scandic-hotels/design-system/Icons"
|
||||||
|
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||||
|
|
||||||
|
import styles from "./results.module.css"
|
||||||
|
|
||||||
|
import type { ResultHistoryProps } from "@/types/components/destinationOverviewPage/jumpTo/results"
|
||||||
|
|
||||||
|
export function ResultHistory({ results, onClearHistory }: ResultHistoryProps) {
|
||||||
|
const intl = useIntl()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Menu
|
||||||
|
aria-label={intl.formatMessage({
|
||||||
|
id: "Latest searches",
|
||||||
|
})}
|
||||||
|
className={styles.menu}
|
||||||
|
items={results}
|
||||||
|
renderEmptyState={() => {
|
||||||
|
return null
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{(section) => {
|
||||||
|
if (section.id === "actions") {
|
||||||
|
return (
|
||||||
|
<MenuSection key={section.id} className={styles.actionsSection}>
|
||||||
|
<Header className="sr-only">{section.name}</Header>
|
||||||
|
<Collection items={section.children}>
|
||||||
|
{(item) => (
|
||||||
|
<MenuItem
|
||||||
|
key={item.id}
|
||||||
|
href={item.url}
|
||||||
|
className={styles.item}
|
||||||
|
textValue={item.displayName}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
typography="Body/Supporting text (caption)/smBold"
|
||||||
|
variant="Text"
|
||||||
|
onPress={onClearHistory}
|
||||||
|
className={styles.clearHistoryButton}
|
||||||
|
>
|
||||||
|
<MaterialIcon icon="delete" color="CurrentColor" />
|
||||||
|
<Text slot="label">
|
||||||
|
{intl.formatMessage({
|
||||||
|
id: "Clear searches",
|
||||||
|
})}
|
||||||
|
</Text>
|
||||||
|
</Button>
|
||||||
|
</MenuItem>
|
||||||
|
)}
|
||||||
|
</Collection>
|
||||||
|
</MenuSection>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MenuSection key={section.id}>
|
||||||
|
<Typography variant="Title/Overline/sm">
|
||||||
|
<Header className={styles.sectionHeader}>{section.name}</Header>
|
||||||
|
</Typography>
|
||||||
|
<Collection items={section.children}>
|
||||||
|
{(item) => (
|
||||||
|
<MenuItem
|
||||||
|
key={item.id}
|
||||||
|
href={item.url}
|
||||||
|
className={styles.item}
|
||||||
|
textValue={item.displayName}
|
||||||
|
>
|
||||||
|
<Typography variant="Body/Paragraph/mdBold">
|
||||||
|
<Text slot="label" className={styles.itemLabel}>
|
||||||
|
{item.displayName}
|
||||||
|
</Text>
|
||||||
|
</Typography>
|
||||||
|
{item.description ? (
|
||||||
|
<Typography variant="Body/Paragraph/mdRegular">
|
||||||
|
<Text
|
||||||
|
slot="description"
|
||||||
|
className={styles.itemDescription}
|
||||||
|
>
|
||||||
|
{item.description}
|
||||||
|
</Text>
|
||||||
|
</Typography>
|
||||||
|
) : null}
|
||||||
|
</MenuItem>
|
||||||
|
)}
|
||||||
|
</Collection>
|
||||||
|
</MenuSection>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</Menu>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import {
|
||||||
|
Collection,
|
||||||
|
Header,
|
||||||
|
Menu,
|
||||||
|
MenuItem,
|
||||||
|
MenuSection,
|
||||||
|
Text,
|
||||||
|
} from "react-aria-components"
|
||||||
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
|
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||||
|
|
||||||
|
import styles from "./results.module.css"
|
||||||
|
|
||||||
|
import type { ResultMatchesProps } from "@/types/components/destinationOverviewPage/jumpTo/results"
|
||||||
|
|
||||||
|
export function ResultMatches({ results, onAction }: ResultMatchesProps) {
|
||||||
|
const intl = useIntl()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Menu
|
||||||
|
aria-label={intl.formatMessage({ id: "Results" })}
|
||||||
|
className={styles.menu}
|
||||||
|
items={results}
|
||||||
|
onAction={(key) => onAction(key.toString())}
|
||||||
|
renderEmptyState={() => {
|
||||||
|
return (
|
||||||
|
<div className={styles.noResults}>
|
||||||
|
<Typography variant="Body/Paragraph/mdBold">
|
||||||
|
<Header className={styles.noResultsLabel}>
|
||||||
|
{intl.formatMessage({ id: "No results" })}
|
||||||
|
</Header>
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="Body/Paragraph/mdRegular">
|
||||||
|
<span className={styles.noResultsDescription}>
|
||||||
|
{intl.formatMessage({
|
||||||
|
id: "We couldn't find a matching location for your search.",
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{(section) => (
|
||||||
|
<MenuSection key={section.id}>
|
||||||
|
<Typography variant="Title/Overline/sm">
|
||||||
|
<Header className={styles.sectionHeader}>{section.name}</Header>
|
||||||
|
</Typography>
|
||||||
|
<Collection items={section.children}>
|
||||||
|
{(item) => (
|
||||||
|
<MenuItem
|
||||||
|
className={styles.item}
|
||||||
|
href={item.url}
|
||||||
|
key={item.id}
|
||||||
|
textValue={item.displayName}
|
||||||
|
>
|
||||||
|
<Typography variant="Body/Paragraph/mdBold">
|
||||||
|
<Text slot="label" className={styles.itemLabel}>
|
||||||
|
{item.displayName}
|
||||||
|
</Text>
|
||||||
|
</Typography>
|
||||||
|
{item.description ? (
|
||||||
|
<Typography variant="Body/Paragraph/mdRegular">
|
||||||
|
<Text slot="description" className={styles.itemDescription}>
|
||||||
|
{item.description}
|
||||||
|
</Text>
|
||||||
|
</Typography>
|
||||||
|
) : null}
|
||||||
|
</MenuItem>
|
||||||
|
)}
|
||||||
|
</Collection>
|
||||||
|
</MenuSection>
|
||||||
|
)}
|
||||||
|
</Menu>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
|
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||||
|
|
||||||
|
import SkeletonShimmer from "@/components/SkeletonShimmer"
|
||||||
|
|
||||||
|
import styles from "./results.module.css"
|
||||||
|
|
||||||
|
export function ResultSkeleton() {
|
||||||
|
const intl = useIntl()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className={styles.menu}>
|
||||||
|
<Typography variant="Title/Overline/sm">
|
||||||
|
<header className={styles.sectionHeader}>
|
||||||
|
{intl.formatMessage({
|
||||||
|
id: "Loading results",
|
||||||
|
})}
|
||||||
|
</header>
|
||||||
|
</Typography>
|
||||||
|
<div>
|
||||||
|
<div className={styles.item}>
|
||||||
|
<Typography variant="Body/Paragraph/mdBold">
|
||||||
|
<SkeletonShimmer width="50%" />
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="Body/Paragraph/mdRegular">
|
||||||
|
<div className={styles.itemDescription}>
|
||||||
|
<SkeletonShimmer width="38%" />
|
||||||
|
</div>
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
<div className={styles.item}>
|
||||||
|
<Typography variant="Body/Paragraph/mdBold">
|
||||||
|
<SkeletonShimmer width="40%" />
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="Body/Paragraph/mdRegular">
|
||||||
|
<div className={styles.itemDescription}>
|
||||||
|
<SkeletonShimmer width="23%" />
|
||||||
|
</div>
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
<div className={styles.item}>
|
||||||
|
<Typography variant="Body/Paragraph/mdBold">
|
||||||
|
<SkeletonShimmer width="55%" />
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="Body/Paragraph/mdRegular">
|
||||||
|
<div className={styles.itemDescription}>
|
||||||
|
<SkeletonShimmer width="40%" />
|
||||||
|
</div>
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
<div className={styles.item}>
|
||||||
|
<Typography variant="Body/Paragraph/mdBold">
|
||||||
|
<SkeletonShimmer width="27%" />
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="Body/Paragraph/mdRegular">
|
||||||
|
<div className={styles.itemDescription}>
|
||||||
|
<SkeletonShimmer width="33%" />
|
||||||
|
</div>
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
<div className={styles.item}>
|
||||||
|
<Typography variant="Body/Paragraph/mdBold">
|
||||||
|
<SkeletonShimmer width="45%" />
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="Body/Paragraph/mdRegular">
|
||||||
|
<div className={styles.itemDescription}>
|
||||||
|
<SkeletonShimmer width="37%" />
|
||||||
|
</div>
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
.menu {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--Space-x2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sectionHeader {
|
||||||
|
color: var(--UI-Text-Placeholder);
|
||||||
|
padding-left: var(--Space-x1);
|
||||||
|
padding-bottom: var(--Space-x05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.item {
|
||||||
|
display: block;
|
||||||
|
padding: var(--Space-x1);
|
||||||
|
border-radius: var(--Corner-radius-Medium);
|
||||||
|
text-decoration: none;
|
||||||
|
|
||||||
|
&[data-focused],
|
||||||
|
&[data-focus-visible],
|
||||||
|
&[data-selected],
|
||||||
|
&[data-hovered] {
|
||||||
|
background: var(--Base-Surface-Primary-light-Hover-alt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.itemLabel {
|
||||||
|
display: block;
|
||||||
|
color: var(--UI-Text-High-contrast);
|
||||||
|
}
|
||||||
|
|
||||||
|
.itemDescription {
|
||||||
|
color: var(--UI-Text-Placeholder);
|
||||||
|
}
|
||||||
|
|
||||||
|
.noResults {
|
||||||
|
padding-left: var(--Space-x1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.noResultsLabel {
|
||||||
|
color: var(--Text-Default);
|
||||||
|
}
|
||||||
|
|
||||||
|
.noResultsDescription {
|
||||||
|
color: var(--Text-Tertiary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.actionsSection {
|
||||||
|
border-top: solid 1px var(--Border-Divider-Subtle);
|
||||||
|
padding-top: var(--Space-x2); /* match gap of .menu */
|
||||||
|
}
|
||||||
|
|
||||||
|
.actionsSection .item {
|
||||||
|
padding-top: 0;
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clearHistoryButton {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: var(--Space-x05);
|
||||||
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
.inline {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (min-width: 768px) {
|
||||||
|
.inline {
|
||||||
|
display: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,181 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { useCallback, useMemo, useState } from "react"
|
||||||
|
import { useIsMounted, useMediaQuery } from "usehooks-ts"
|
||||||
|
|
||||||
|
import { isDefined } from "@/server/utils"
|
||||||
|
|
||||||
|
import { ClientInline } from "./ClientInline"
|
||||||
|
import { ClientModal } from "./ClientModal"
|
||||||
|
|
||||||
|
import styles from "./client.module.css"
|
||||||
|
|
||||||
|
import type {
|
||||||
|
JumpToData,
|
||||||
|
JumpToProps,
|
||||||
|
LocationMatchResults,
|
||||||
|
ScoringMatch,
|
||||||
|
} from "@/types/components/destinationOverviewPage/jumpTo"
|
||||||
|
import type { ClientProps } from "@/types/components/destinationOverviewPage/jumpTo/client"
|
||||||
|
|
||||||
|
export function JumpToClient<T extends JumpToData>({
|
||||||
|
data,
|
||||||
|
history,
|
||||||
|
onAction,
|
||||||
|
onClearHistory,
|
||||||
|
}: JumpToProps<T>) {
|
||||||
|
const isMounted = useIsMounted()
|
||||||
|
const displayInModal = useMediaQuery("(max-width: 767px)")
|
||||||
|
|
||||||
|
const [filterString, setFilterString] = useState<string | null>(null)
|
||||||
|
|
||||||
|
const filter = useCallback(
|
||||||
|
(needle: string): LocationMatchResults => {
|
||||||
|
needle = needle.toLowerCase().trim().replace("scandic ", "")
|
||||||
|
|
||||||
|
// This algorithm ranks the location data set based on a ruleset. Each
|
||||||
|
// match is given a score to rank the results. Different rules give
|
||||||
|
// different scores. The lower the string matching index the higer the
|
||||||
|
// score. All matchings are done with lower case comparison.
|
||||||
|
//
|
||||||
|
// Ruleset, from higest to lower in ranking score:
|
||||||
|
//
|
||||||
|
// 1. Match on name. If no match on name, check cityIdentifier. This
|
||||||
|
// allows for cities that suffer from different spellings to have a change
|
||||||
|
// to get matched and ranked better.
|
||||||
|
//
|
||||||
|
// 2. Match on keywords. Only the highest ranking keyword is considered.
|
||||||
|
// This prevents keyword overloading and evens out the matches.
|
||||||
|
const matchesWithScore = data
|
||||||
|
.map<ScoringMatch | null>((item) => {
|
||||||
|
// Rank all the names and filter out those that don't rank at all
|
||||||
|
const nameScores = item.rankingNames
|
||||||
|
.map((v) => {
|
||||||
|
const index = v.indexOf(needle)
|
||||||
|
const score = index !== -1 ? 1000 - index : 0
|
||||||
|
return score
|
||||||
|
})
|
||||||
|
.filter((score) => score > 0)
|
||||||
|
|
||||||
|
// Calculate the highest ranking name
|
||||||
|
const bestNameScore = nameScores.length ? Math.max(...nameScores) : 0
|
||||||
|
|
||||||
|
// Rank all the keywords and filter out those that don't rank at all
|
||||||
|
const keywordScores = item.rankingKeywords
|
||||||
|
.map((v) => {
|
||||||
|
const index = v.indexOf(needle)
|
||||||
|
const score = index !== -1 ? 500 - index : 0
|
||||||
|
return score
|
||||||
|
})
|
||||||
|
.filter((score) => score > 0)
|
||||||
|
|
||||||
|
// Calculate the highest ranking keyword
|
||||||
|
const bestKeywordScore = keywordScores.length
|
||||||
|
? Math.max(...keywordScores)
|
||||||
|
: 0
|
||||||
|
|
||||||
|
const totalScore = bestNameScore + bestKeywordScore
|
||||||
|
|
||||||
|
return totalScore > 0
|
||||||
|
? {
|
||||||
|
id: item.id,
|
||||||
|
displayName: item.displayName,
|
||||||
|
type: item.type,
|
||||||
|
description: item.description,
|
||||||
|
url: item.url,
|
||||||
|
score: totalScore,
|
||||||
|
}
|
||||||
|
: null
|
||||||
|
})
|
||||||
|
.filter(isDefined)
|
||||||
|
.sort((a, b) => {
|
||||||
|
return b.score - a.score
|
||||||
|
})
|
||||||
|
|
||||||
|
if (matchesWithScore.length > 0) {
|
||||||
|
// Map matchesWithScore to build the final results of matches and
|
||||||
|
// remove the score from the output as it is not needed anymore
|
||||||
|
const matches = matchesWithScore
|
||||||
|
.map(({ score, ...item }) => item) // No need for score anymore
|
||||||
|
.reduce<LocationMatchResults>(
|
||||||
|
(acc, item) => {
|
||||||
|
// Do this verbosely because its helps TS understand the data flow better.
|
||||||
|
if (item.type === "cities") {
|
||||||
|
acc[0].children.push(item)
|
||||||
|
} else if (item.type === "hotels") {
|
||||||
|
acc[1].children.push(item)
|
||||||
|
}
|
||||||
|
return acc
|
||||||
|
},
|
||||||
|
[
|
||||||
|
{
|
||||||
|
id: "cities",
|
||||||
|
name: "Cities",
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "hotels",
|
||||||
|
name: "Hotels",
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
// Hide section that does not have any matches
|
||||||
|
return matches.filter((section) => section.children.length)
|
||||||
|
}
|
||||||
|
|
||||||
|
return []
|
||||||
|
},
|
||||||
|
[data]
|
||||||
|
)
|
||||||
|
|
||||||
|
const latest: LocationMatchResults = useMemo(() => {
|
||||||
|
if (data && history && history.length) {
|
||||||
|
const children = history
|
||||||
|
.map((v) => {
|
||||||
|
return data.find((d) => d.id === v.id && d.type === v.type)
|
||||||
|
})
|
||||||
|
.filter(isDefined)
|
||||||
|
.slice(0, 5) // Only show five items
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: "latestSearches",
|
||||||
|
name: "Latest searches",
|
||||||
|
children,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
return []
|
||||||
|
}, [data, history])
|
||||||
|
|
||||||
|
const results = useMemo(() => {
|
||||||
|
if (filterString) {
|
||||||
|
return filter(filterString)
|
||||||
|
} else {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}, [filterString, filter])
|
||||||
|
|
||||||
|
const props: ClientProps = useMemo(() => {
|
||||||
|
return {
|
||||||
|
results,
|
||||||
|
latest,
|
||||||
|
setFilterString,
|
||||||
|
onAction,
|
||||||
|
onClearHistory,
|
||||||
|
}
|
||||||
|
}, [results, latest, setFilterString, onAction, onClearHistory])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className={styles.modal} hidden={isMounted() && !displayInModal}>
|
||||||
|
<ClientModal {...props} />
|
||||||
|
</div>
|
||||||
|
<div className={styles.inline} hidden={isMounted() && displayInModal}>
|
||||||
|
<ClientInline {...props} />
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { use } from "react"
|
||||||
|
import { useLocalStorage } from "usehooks-ts"
|
||||||
|
|
||||||
|
import { localStorageKey } from "@/components/Forms/BookingWidget/FormContent/Search/reducer"
|
||||||
|
|
||||||
|
import { JumpToClient } from "../Client"
|
||||||
|
|
||||||
|
import type { JumpToHistory } from "@/types/components/destinationOverviewPage/jumpTo"
|
||||||
|
import type { JumpToResolverProps } from "@/types/components/destinationOverviewPage/jumpTo/resolver"
|
||||||
|
|
||||||
|
export function JumpToResolver({ dataPromise }: JumpToResolverProps) {
|
||||||
|
const data = use(dataPromise)
|
||||||
|
|
||||||
|
const [history, setHistory, clearHistory] = useLocalStorage<JumpToHistory>(
|
||||||
|
localStorageKey,
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!data) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<JumpToClient
|
||||||
|
data={data}
|
||||||
|
history={history}
|
||||||
|
onAction={(key) => {
|
||||||
|
const existsInHistory = history.find((h) => h.id === key)
|
||||||
|
if (!existsInHistory) {
|
||||||
|
const item = data.find((d) => d.id === key)
|
||||||
|
if (item) {
|
||||||
|
const { id, type } = item
|
||||||
|
// latest added should be shown first
|
||||||
|
const newHistory = [{ id, type }, ...history]
|
||||||
|
setHistory(newHistory)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onClearHistory={() => {
|
||||||
|
debugger
|
||||||
|
clearHistory()
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { getJumpToData } from "@/lib/trpc/memoizedRequests"
|
||||||
|
|
||||||
|
import { JumpToResolver } from "@/components/ContentType/DestinationPage/DestinationOverviewPage/JumpTo/Resolver"
|
||||||
|
|
||||||
|
export async function JumpTo() {
|
||||||
|
const dataPromise = getJumpToData()
|
||||||
|
|
||||||
|
return <JumpToResolver dataPromise={dataPromise} />
|
||||||
|
}
|
||||||
@@ -37,3 +37,15 @@
|
|||||||
max-width: var(--max-width-content);
|
max-width: var(--max-width-content);
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.jumpToContainer {
|
||||||
|
display: grid;
|
||||||
|
gap: var(--Space-x4);
|
||||||
|
padding: var(--Space-x4) var(--Space-x2);
|
||||||
|
background: var(--Surface-Secondary-Default);
|
||||||
|
}
|
||||||
|
|
||||||
|
.heading {
|
||||||
|
color: var(--Text-Interactive-Default);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { Suspense } from "react"
|
import { Suspense } from "react"
|
||||||
|
|
||||||
|
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||||
|
|
||||||
import { getDestinationOverviewPage } from "@/lib/trpc/memoizedRequests"
|
import { getDestinationOverviewPage } from "@/lib/trpc/memoizedRequests"
|
||||||
|
|
||||||
import Blocks from "@/components/Blocks"
|
import Blocks from "@/components/Blocks"
|
||||||
@@ -7,6 +9,7 @@ import SkeletonShimmer from "@/components/SkeletonShimmer"
|
|||||||
import TrackingSDK from "@/components/TrackingSDK"
|
import TrackingSDK from "@/components/TrackingSDK"
|
||||||
|
|
||||||
import HotelsSection from "./HotelsSection"
|
import HotelsSection from "./HotelsSection"
|
||||||
|
import { JumpTo } from "./JumpTo"
|
||||||
import OverviewMapContainer from "./OverviewMapContainer"
|
import OverviewMapContainer from "./OverviewMapContainer"
|
||||||
|
|
||||||
import styles from "./destinationOverviewPage.module.css"
|
import styles from "./destinationOverviewPage.module.css"
|
||||||
@@ -22,6 +25,12 @@ export default async function DestinationOverviewPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
<div className={styles.jumpToContainer}>
|
||||||
|
<Typography variant="Title/lg">
|
||||||
|
<h1 className={styles.heading}>{destinationOverviewPage.heading}</h1>
|
||||||
|
</Typography>
|
||||||
|
<JumpTo />
|
||||||
|
</div>
|
||||||
<div className={styles.mapContainer}>
|
<div className={styles.mapContainer}>
|
||||||
<Suspense fallback={<SkeletonShimmer width={"100%"} height={"100%"} />}>
|
<Suspense fallback={<SkeletonShimmer width={"100%"} height={"100%"} />}>
|
||||||
<OverviewMapContainer />
|
<OverviewMapContainer />
|
||||||
|
|||||||
22
apps/scandic-web/components/RACRouterProvider/index.tsx
Normal file
22
apps/scandic-web/components/RACRouterProvider/index.tsx
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
// https://react-spectrum.adobe.com/react-aria/routing.html#app-router
|
||||||
|
|
||||||
|
"use client"
|
||||||
|
|
||||||
|
import { useRouter } from "next/navigation"
|
||||||
|
import { RouterProvider } from "react-aria-components"
|
||||||
|
|
||||||
|
import type { PropsWithChildren } from "react"
|
||||||
|
|
||||||
|
declare module "react-aria-components" {
|
||||||
|
interface RouterConfig {
|
||||||
|
routerOptions: NonNullable<
|
||||||
|
Parameters<ReturnType<typeof useRouter>["push"]>[1]
|
||||||
|
>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function RACRouterProvider({ children }: PropsWithChildren) {
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
return <RouterProvider navigate={router.push}>{children}</RouterProvider>
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { cva } from "class-variance-authority"
|
import { cva, cx } from "class-variance-authority"
|
||||||
|
|
||||||
import styles from "./skeleton.module.css"
|
import styles from "./skeleton.module.css"
|
||||||
|
|
||||||
@@ -15,11 +15,13 @@ const variants = cva(styles.shimmer, {
|
|||||||
})
|
})
|
||||||
|
|
||||||
export default function SkeletonShimmer({
|
export default function SkeletonShimmer({
|
||||||
|
className,
|
||||||
height,
|
height,
|
||||||
width,
|
width,
|
||||||
contrast = "light",
|
contrast = "light",
|
||||||
display = "initial",
|
display = "initial",
|
||||||
}: {
|
}: {
|
||||||
|
className?: string
|
||||||
height?: string
|
height?: string
|
||||||
width?: string
|
width?: string
|
||||||
contrast?: "light" | "dark"
|
contrast?: "light" | "dark"
|
||||||
@@ -27,13 +29,16 @@ export default function SkeletonShimmer({
|
|||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
className={variants({ contrast })}
|
className={cx(className, variants({ contrast }))}
|
||||||
style={{
|
style={{
|
||||||
height: height,
|
height: height,
|
||||||
width: width,
|
width: width,
|
||||||
maxWidth: "100%",
|
maxWidth: "100%",
|
||||||
display: display,
|
display: display,
|
||||||
}}
|
}}
|
||||||
/>
|
>
|
||||||
|
{/* zero width space, allows for font styles to affect height */}
|
||||||
|
<span aria-hidden="true">​</span>
|
||||||
|
</span>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,6 +37,9 @@
|
|||||||
animation: shimmer 3s infinite;
|
animation: shimmer 3s infinite;
|
||||||
content: "";
|
content: "";
|
||||||
}
|
}
|
||||||
|
.shimmer span {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
@keyframes shimmer {
|
@keyframes shimmer {
|
||||||
100% {
|
100% {
|
||||||
transform: translateX(100%);
|
transform: translateX(100%);
|
||||||
|
|||||||
@@ -298,6 +298,7 @@
|
|||||||
"Filter by": "Filtrer efter",
|
"Filter by": "Filtrer efter",
|
||||||
"Final VAT breakdown will be provided at check-out.": "Endelig momsopgørelse gives ved check-out.",
|
"Final VAT breakdown will be provided at check-out.": "Endelig momsopgørelse gives ved check-out.",
|
||||||
"Find": "Finde",
|
"Find": "Finde",
|
||||||
|
"Find a location": "Find a location",
|
||||||
"Find booking": "Find booking",
|
"Find booking": "Find booking",
|
||||||
"Find hotels": "Find hotel",
|
"Find hotels": "Find hotel",
|
||||||
"Find hotels and destinations": "Find hoteller og destinationer",
|
"Find hotels and destinations": "Find hoteller og destinationer",
|
||||||
@@ -438,6 +439,7 @@
|
|||||||
"Link my accounts": "Link my accounts",
|
"Link my accounts": "Link my accounts",
|
||||||
"Link your accounts": "Link your accounts",
|
"Link your accounts": "Link your accounts",
|
||||||
"Linked account": "Linked account",
|
"Linked account": "Linked account",
|
||||||
|
"Loading results": "Loading results",
|
||||||
"Location": "Beliggenhed",
|
"Location": "Beliggenhed",
|
||||||
"Location (shown in local language)": "Location (shown in local language)",
|
"Location (shown in local language)": "Location (shown in local language)",
|
||||||
"Location in hotel": "Plassering på hotellet",
|
"Location in hotel": "Plassering på hotellet",
|
||||||
@@ -682,6 +684,7 @@
|
|||||||
"Restaurant & Bar": "Restaurant & Bar",
|
"Restaurant & Bar": "Restaurant & Bar",
|
||||||
"Restaurants": "Restauranter",
|
"Restaurants": "Restauranter",
|
||||||
"Restaurants & Bars": "Restaurants & Bars",
|
"Restaurants & Bars": "Restaurants & Bars",
|
||||||
|
"Results": "Results",
|
||||||
"Retype new password": "Gentag den nye adgangskode",
|
"Retype new password": "Gentag den nye adgangskode",
|
||||||
"Reward night": "Bonusnat",
|
"Reward night": "Bonusnat",
|
||||||
"Room": "Værelse",
|
"Room": "Værelse",
|
||||||
|
|||||||
@@ -299,6 +299,7 @@
|
|||||||
"Filter by": "Filtern nach",
|
"Filter by": "Filtern nach",
|
||||||
"Final VAT breakdown will be provided at check-out.": "Die endgültige Mehrwertsteueraufstellung wird beim Check-out bereitgestellt.",
|
"Final VAT breakdown will be provided at check-out.": "Die endgültige Mehrwertsteueraufstellung wird beim Check-out bereitgestellt.",
|
||||||
"Find": "Finden",
|
"Find": "Finden",
|
||||||
|
"Find a location": "Find a location",
|
||||||
"Find booking": "Buchung finden",
|
"Find booking": "Buchung finden",
|
||||||
"Find hotels": "Hotels finden",
|
"Find hotels": "Hotels finden",
|
||||||
"Find hotels and destinations": "Finden Sie Hotels und Reiseziele",
|
"Find hotels and destinations": "Finden Sie Hotels und Reiseziele",
|
||||||
@@ -439,6 +440,7 @@
|
|||||||
"Link my accounts": "Link my accounts",
|
"Link my accounts": "Link my accounts",
|
||||||
"Link your accounts": "Link your accounts",
|
"Link your accounts": "Link your accounts",
|
||||||
"Linked account": "Linked account",
|
"Linked account": "Linked account",
|
||||||
|
"Loading results": "Loading results",
|
||||||
"Location": "Ort",
|
"Location": "Ort",
|
||||||
"Location (shown in local language)": "Location (shown in local language)",
|
"Location (shown in local language)": "Location (shown in local language)",
|
||||||
"Location in hotel": "Lage im Hotel",
|
"Location in hotel": "Lage im Hotel",
|
||||||
@@ -681,6 +683,7 @@
|
|||||||
"Restaurant & Bar": "Restaurant & Bar",
|
"Restaurant & Bar": "Restaurant & Bar",
|
||||||
"Restaurants": "Restaurants",
|
"Restaurants": "Restaurants",
|
||||||
"Restaurants & Bars": "Restaurants & Bars",
|
"Restaurants & Bars": "Restaurants & Bars",
|
||||||
|
"Results": "Results",
|
||||||
"Retype new password": "Neues Passwort erneut eingeben",
|
"Retype new password": "Neues Passwort erneut eingeben",
|
||||||
"Reward night": "Bonusnacht",
|
"Reward night": "Bonusnacht",
|
||||||
"Room": "Zimmer",
|
"Room": "Zimmer",
|
||||||
|
|||||||
@@ -299,6 +299,7 @@
|
|||||||
"Filter by": "Filter by",
|
"Filter by": "Filter by",
|
||||||
"Final VAT breakdown will be provided at check-out.": "Final VAT breakdown will be provided at check-out.",
|
"Final VAT breakdown will be provided at check-out.": "Final VAT breakdown will be provided at check-out.",
|
||||||
"Find": "Find",
|
"Find": "Find",
|
||||||
|
"Find a location": "Find a location",
|
||||||
"Find booking": "Find booking",
|
"Find booking": "Find booking",
|
||||||
"Find hotels": "Find hotels",
|
"Find hotels": "Find hotels",
|
||||||
"Find hotels and destinations": "Find hotels and destinations",
|
"Find hotels and destinations": "Find hotels and destinations",
|
||||||
@@ -440,6 +441,7 @@
|
|||||||
"Link my accounts": "Link my accounts",
|
"Link my accounts": "Link my accounts",
|
||||||
"Link your accounts": "Link your accounts",
|
"Link your accounts": "Link your accounts",
|
||||||
"Linked account": "Linked account",
|
"Linked account": "Linked account",
|
||||||
|
"Loading results": "Loading results",
|
||||||
"Location": "Location",
|
"Location": "Location",
|
||||||
"Location (shown in local language)": "Location (shown in local language)",
|
"Location (shown in local language)": "Location (shown in local language)",
|
||||||
"Location in hotel": "Location in hotel",
|
"Location in hotel": "Location in hotel",
|
||||||
@@ -683,6 +685,7 @@
|
|||||||
"Restaurant & Bar": "Restaurant & Bar",
|
"Restaurant & Bar": "Restaurant & Bar",
|
||||||
"Restaurants": "Restaurants",
|
"Restaurants": "Restaurants",
|
||||||
"Restaurants & Bars": "Restaurants & Bars",
|
"Restaurants & Bars": "Restaurants & Bars",
|
||||||
|
"Results": "Results",
|
||||||
"Retype new password": "Retype new password",
|
"Retype new password": "Retype new password",
|
||||||
"Reward night": "Reward night",
|
"Reward night": "Reward night",
|
||||||
"Room": "Room",
|
"Room": "Room",
|
||||||
|
|||||||
@@ -298,6 +298,7 @@
|
|||||||
"Filter by": "Suodatusperuste",
|
"Filter by": "Suodatusperuste",
|
||||||
"Final VAT breakdown will be provided at check-out.": "Lopullinen ALV-erittely annetaan uloskirjautumisen yhteydessä.",
|
"Final VAT breakdown will be provided at check-out.": "Lopullinen ALV-erittely annetaan uloskirjautumisen yhteydessä.",
|
||||||
"Find": "Löytää",
|
"Find": "Löytää",
|
||||||
|
"Find a location": "Find a location",
|
||||||
"Find booking": "Etsi varaus",
|
"Find booking": "Etsi varaus",
|
||||||
"Find hotels": "Etsi hotelleja",
|
"Find hotels": "Etsi hotelleja",
|
||||||
"Find hotels and destinations": "Etsi hotelleja ja kohteita",
|
"Find hotels and destinations": "Etsi hotelleja ja kohteita",
|
||||||
@@ -438,6 +439,7 @@
|
|||||||
"Link my accounts": "Link my accounts",
|
"Link my accounts": "Link my accounts",
|
||||||
"Link your accounts": "Link your accounts",
|
"Link your accounts": "Link your accounts",
|
||||||
"Linked account": "Linked account",
|
"Linked account": "Linked account",
|
||||||
|
"Loading results": "Loading results",
|
||||||
"Location": "Sijainti",
|
"Location": "Sijainti",
|
||||||
"Location (shown in local language)": "Location (shown in local language)",
|
"Location (shown in local language)": "Location (shown in local language)",
|
||||||
"Location in hotel": "Sijainti hotellissa",
|
"Location in hotel": "Sijainti hotellissa",
|
||||||
@@ -680,6 +682,7 @@
|
|||||||
"Restaurant & Bar": "Ravintola & Baari",
|
"Restaurant & Bar": "Ravintola & Baari",
|
||||||
"Restaurants": "Ravintolat",
|
"Restaurants": "Ravintolat",
|
||||||
"Restaurants & Bars": "Restaurants & Bars",
|
"Restaurants & Bars": "Restaurants & Bars",
|
||||||
|
"Results": "Results",
|
||||||
"Retype new password": "Kirjoita uusi salasana uudelleen",
|
"Retype new password": "Kirjoita uusi salasana uudelleen",
|
||||||
"Reward night": "Palkintoyö",
|
"Reward night": "Palkintoyö",
|
||||||
"Room": "Huone",
|
"Room": "Huone",
|
||||||
|
|||||||
@@ -297,6 +297,7 @@
|
|||||||
"Filter by": "Filtrer etter",
|
"Filter by": "Filtrer etter",
|
||||||
"Final VAT breakdown will be provided at check-out.": "Endelig MVA-oversikt gis ved utsjekking.",
|
"Final VAT breakdown will be provided at check-out.": "Endelig MVA-oversikt gis ved utsjekking.",
|
||||||
"Find": "Finne",
|
"Find": "Finne",
|
||||||
|
"Find a location": "Find a location",
|
||||||
"Find booking": "Finn booking",
|
"Find booking": "Finn booking",
|
||||||
"Find hotels": "Finn hotell",
|
"Find hotels": "Finn hotell",
|
||||||
"Find hotels and destinations": "Finn hoteller og destinasjoner",
|
"Find hotels and destinations": "Finn hoteller og destinasjoner",
|
||||||
@@ -437,6 +438,7 @@
|
|||||||
"Link my accounts": "Link my accounts",
|
"Link my accounts": "Link my accounts",
|
||||||
"Link your accounts": "Link your accounts",
|
"Link your accounts": "Link your accounts",
|
||||||
"Linked account": "Linked account",
|
"Linked account": "Linked account",
|
||||||
|
"Loading results": "Loading results",
|
||||||
"Location": "Beliggenhet",
|
"Location": "Beliggenhet",
|
||||||
"Location (shown in local language)": "Location (shown in local language)",
|
"Location (shown in local language)": "Location (shown in local language)",
|
||||||
"Location in hotel": "Plassering på hotellet",
|
"Location in hotel": "Plassering på hotellet",
|
||||||
@@ -679,6 +681,7 @@
|
|||||||
"Restaurant & Bar": "Restaurant & Bar",
|
"Restaurant & Bar": "Restaurant & Bar",
|
||||||
"Restaurants": "Restauranter",
|
"Restaurants": "Restauranter",
|
||||||
"Restaurants & Bars": "Restaurants & Bars",
|
"Restaurants & Bars": "Restaurants & Bars",
|
||||||
|
"Results": "Results",
|
||||||
"Retype new password": "Skriv inn nytt passord på nytt",
|
"Retype new password": "Skriv inn nytt passord på nytt",
|
||||||
"Reward night": "Bonusnatt",
|
"Reward night": "Bonusnatt",
|
||||||
"Room": "Rom",
|
"Room": "Rom",
|
||||||
|
|||||||
@@ -297,6 +297,7 @@
|
|||||||
"Filter by": "Filtrera på",
|
"Filter by": "Filtrera på",
|
||||||
"Final VAT breakdown will be provided at check-out.": "Slutlig momsuppdelning tillhandahålls vid utcheckning.",
|
"Final VAT breakdown will be provided at check-out.": "Slutlig momsuppdelning tillhandahålls vid utcheckning.",
|
||||||
"Find": "Hitta",
|
"Find": "Hitta",
|
||||||
|
"Find a location": "Find a location",
|
||||||
"Find booking": "Hitta bokning",
|
"Find booking": "Hitta bokning",
|
||||||
"Find hotels": "Hitta hotell",
|
"Find hotels": "Hitta hotell",
|
||||||
"Find hotels and destinations": "Hitta hotell och destinationer",
|
"Find hotels and destinations": "Hitta hotell och destinationer",
|
||||||
@@ -437,6 +438,7 @@
|
|||||||
"Link my accounts": "Link my accounts",
|
"Link my accounts": "Link my accounts",
|
||||||
"Link your accounts": "Link your accounts",
|
"Link your accounts": "Link your accounts",
|
||||||
"Linked account": "Linked account",
|
"Linked account": "Linked account",
|
||||||
|
"Loading results": "Loading results",
|
||||||
"Location": "Plats",
|
"Location": "Plats",
|
||||||
"Location (shown in local language)": "Location (shown in local language)",
|
"Location (shown in local language)": "Location (shown in local language)",
|
||||||
"Location in hotel": "Plats på hotellet",
|
"Location in hotel": "Plats på hotellet",
|
||||||
@@ -679,6 +681,7 @@
|
|||||||
"Restaurant & Bar": "Restaurang & Bar",
|
"Restaurant & Bar": "Restaurang & Bar",
|
||||||
"Restaurants": "Restauranger",
|
"Restaurants": "Restauranger",
|
||||||
"Restaurants & Bars": "Restaurants & Bars",
|
"Restaurants & Bars": "Restaurants & Bars",
|
||||||
|
"Results": "Results",
|
||||||
"Retype new password": "Upprepa nytt lösenord",
|
"Retype new password": "Upprepa nytt lösenord",
|
||||||
"Reward night": "Bonusnatt",
|
"Reward night": "Bonusnatt",
|
||||||
"Room": "Rum",
|
"Room": "Rum",
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
query GetLocationsUrls($locale: String!) {
|
||||||
|
hotels: all_hotel_page(locale: $locale) {
|
||||||
|
items {
|
||||||
|
url
|
||||||
|
id: hotel_page_id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cities: all_destination_city_page {
|
||||||
|
items {
|
||||||
|
url
|
||||||
|
id: destination_settings {
|
||||||
|
sv: city_sweden
|
||||||
|
pl: city_poland
|
||||||
|
no: city_norway
|
||||||
|
de: city_germany
|
||||||
|
fi: city_finland
|
||||||
|
da: city_denmark
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { isDefined } from "@/server/utils"
|
||||||
|
|
||||||
import { getLang } from "@/i18n/serverContext"
|
import { getLang } from "@/i18n/serverContext"
|
||||||
import { cache } from "@/utils/cache"
|
import { cache } from "@/utils/cache"
|
||||||
|
|
||||||
@@ -268,3 +270,79 @@ export const getPageSettingsBookingCode = cache(
|
|||||||
export const getJobylonFeed = cache(async function getMemoizedJobylonFeed() {
|
export const getJobylonFeed = cache(async function getMemoizedJobylonFeed() {
|
||||||
return serverClient().partner.jobylon.feed.get()
|
return serverClient().partner.jobylon.feed.get()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export const getJumpToData = cache(async function getMemoizedJumpToData() {
|
||||||
|
const lang = getLang()
|
||||||
|
const [locationsResults, urlsResults] = await Promise.allSettled([
|
||||||
|
getLocations(),
|
||||||
|
serverClient().hotel.locations.urls({ lang }),
|
||||||
|
])
|
||||||
|
|
||||||
|
if (
|
||||||
|
locationsResults.status === "fulfilled" &&
|
||||||
|
urlsResults.status === "fulfilled"
|
||||||
|
) {
|
||||||
|
const locations = locationsResults.value
|
||||||
|
const urls = urlsResults.value
|
||||||
|
|
||||||
|
if (!locations || !urls) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return locations
|
||||||
|
.map((location) => {
|
||||||
|
const { id, name, type } = location
|
||||||
|
|
||||||
|
const isCity = type === "cities"
|
||||||
|
const isHotel = type === "hotels"
|
||||||
|
|
||||||
|
let url: string | undefined
|
||||||
|
|
||||||
|
if (isCity) {
|
||||||
|
url = urls.cities.find(
|
||||||
|
(c) =>
|
||||||
|
c.id &&
|
||||||
|
location.cityIdentifier &&
|
||||||
|
c.id === location.cityIdentifier
|
||||||
|
)?.url
|
||||||
|
} else if (isHotel) {
|
||||||
|
url = urls.hotels.find(
|
||||||
|
(h) => h.id && location.id && h.id === location.id
|
||||||
|
)?.url
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!url) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
let description = ""
|
||||||
|
if (isCity) {
|
||||||
|
description = location.country
|
||||||
|
} else if (isHotel) {
|
||||||
|
description = location.relationships.city.name
|
||||||
|
}
|
||||||
|
|
||||||
|
const rankingNames: string[] = [location.name]
|
||||||
|
if (isCity) {
|
||||||
|
if (location.cityIdentifier) {
|
||||||
|
rankingNames.push(location.cityIdentifier)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const rankingKeywords = location.keyWords || []
|
||||||
|
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
displayName: name,
|
||||||
|
type,
|
||||||
|
description,
|
||||||
|
url,
|
||||||
|
rankingNames: rankingNames.map((v) => v.toLowerCase()),
|
||||||
|
rankingKeywords: rankingKeywords.map((v) => v.toLowerCase()),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.filter(isDefined)
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
|||||||
@@ -83,7 +83,7 @@
|
|||||||
"next": "^14.2.25",
|
"next": "^14.2.25",
|
||||||
"next-auth": "5.0.0-beta.19",
|
"next-auth": "5.0.0-beta.19",
|
||||||
"react": "^18",
|
"react": "^18",
|
||||||
"react-aria-components": "^1.6.0",
|
"react-aria-components": "^1.7.1",
|
||||||
"react-day-picker": "^9.0.8",
|
"react-day-picker": "^9.0.8",
|
||||||
"react-dom": "^18",
|
"react-dom": "^18",
|
||||||
"react-feather": "^2.0.10",
|
"react-feather": "^2.0.10",
|
||||||
|
|||||||
@@ -164,6 +164,10 @@ export const getLocationsInput = z.object({
|
|||||||
lang: z.nativeEnum(Lang),
|
lang: z.nativeEnum(Lang),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export const getLocationsUrlsInput = z.object({
|
||||||
|
lang: z.nativeEnum(Lang),
|
||||||
|
})
|
||||||
|
|
||||||
export const roomFeaturesInputSchema = z.object({
|
export const roomFeaturesInputSchema = z.object({
|
||||||
hotelId: z.string(),
|
hotelId: z.string(),
|
||||||
startDate: z.string(),
|
startDate: z.string(),
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import { Lang } from "@/constants/languages"
|
|||||||
import { env } from "@/env/server"
|
import { env } from "@/env/server"
|
||||||
import * as api from "@/lib/api"
|
import * as api from "@/lib/api"
|
||||||
import { dt } from "@/lib/dt"
|
import { dt } from "@/lib/dt"
|
||||||
|
import { GetLocationsUrls } from "@/lib/graphql/Query/Locations/Locations.graphql"
|
||||||
|
import { request } from "@/lib/graphql/request"
|
||||||
import { badRequestError, unauthorizedError } from "@/server/errors/trpc"
|
import { badRequestError, unauthorizedError } from "@/server/errors/trpc"
|
||||||
import {
|
import {
|
||||||
contentStackBaseWithServiceProcedure,
|
contentStackBaseWithServiceProcedure,
|
||||||
@@ -16,10 +18,15 @@ import { toApiLang } from "@/server/utils"
|
|||||||
import { generateChildrenString } from "@/components/HotelReservation/utils"
|
import { generateChildrenString } from "@/components/HotelReservation/utils"
|
||||||
import { getCacheClient } from "@/services/dataCache"
|
import { getCacheClient } from "@/services/dataCache"
|
||||||
import { cache } from "@/utils/cache"
|
import { cache } from "@/utils/cache"
|
||||||
|
import { removeMultipleSlashes } from "@/utils/url"
|
||||||
|
|
||||||
import { getHotelPageUrls } from "../contentstack/hotelPage/utils"
|
import { getHotelPageUrls } from "../contentstack/hotelPage/utils"
|
||||||
import { getVerifiedUser } from "../user/query"
|
import { getVerifiedUser } from "../user/query"
|
||||||
import { additionalDataSchema } from "./schemas/hotel/include/additionalData"
|
import { additionalDataSchema } from "./schemas/hotel/include/additionalData"
|
||||||
|
import {
|
||||||
|
type GetLocationsUrlsData,
|
||||||
|
locationsUrlsSchema,
|
||||||
|
} from "./schemas/location/urls"
|
||||||
import { meetingRoomsSchema } from "./schemas/meetingRoom"
|
import { meetingRoomsSchema } from "./schemas/meetingRoom"
|
||||||
import {
|
import {
|
||||||
ancillaryPackageInputSchema,
|
ancillaryPackageInputSchema,
|
||||||
@@ -32,6 +39,7 @@ import {
|
|||||||
getHotelsByCSFilterInput,
|
getHotelsByCSFilterInput,
|
||||||
getHotelsByHotelIdsAvailabilityInputSchema,
|
getHotelsByHotelIdsAvailabilityInputSchema,
|
||||||
getLocationsInput,
|
getLocationsInput,
|
||||||
|
getLocationsUrlsInput,
|
||||||
getMeetingRoomsInputSchema,
|
getMeetingRoomsInputSchema,
|
||||||
hotelInputSchema,
|
hotelInputSchema,
|
||||||
hotelsAvailabilityInputSchema,
|
hotelsAvailabilityInputSchema,
|
||||||
@@ -55,6 +63,11 @@ import {
|
|||||||
roomFeaturesSchema,
|
roomFeaturesSchema,
|
||||||
roomsAvailabilitySchema,
|
roomsAvailabilitySchema,
|
||||||
} from "./output"
|
} from "./output"
|
||||||
|
import {
|
||||||
|
locationsUrlsCounter,
|
||||||
|
locationsUrlsFailCounter,
|
||||||
|
locationsUrlsSuccessCounter,
|
||||||
|
} from "./telemetry"
|
||||||
import tempRatesData from "./tempRatesData.json"
|
import tempRatesData from "./tempRatesData.json"
|
||||||
import {
|
import {
|
||||||
getCitiesByCountry,
|
getCitiesByCountry,
|
||||||
@@ -1505,6 +1518,79 @@ export const hotelQueryRouter = router({
|
|||||||
"max"
|
"max"
|
||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
|
urls: publicProcedure
|
||||||
|
.input(getLocationsUrlsInput)
|
||||||
|
.query(async ({ input }) => {
|
||||||
|
const procedureName = "hotels.locations.urls"
|
||||||
|
|
||||||
|
const { lang } = input
|
||||||
|
|
||||||
|
locationsUrlsCounter.add(1, { lang })
|
||||||
|
|
||||||
|
console.info(
|
||||||
|
`${procedureName}: start`,
|
||||||
|
JSON.stringify({ query: { lang } })
|
||||||
|
)
|
||||||
|
|
||||||
|
const response = await request<GetLocationsUrlsData>(
|
||||||
|
GetLocationsUrls,
|
||||||
|
{ locale: lang },
|
||||||
|
{
|
||||||
|
key: `${lang}:${procedureName}`,
|
||||||
|
ttl: "max",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
if (!response.data) {
|
||||||
|
locationsUrlsFailCounter.add(1, {
|
||||||
|
lang,
|
||||||
|
error_type: "no data",
|
||||||
|
response: JSON.stringify(response),
|
||||||
|
})
|
||||||
|
|
||||||
|
console.error(`${procedureName}: no data`, {
|
||||||
|
variables: { lang },
|
||||||
|
error_type: "no data",
|
||||||
|
response,
|
||||||
|
})
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const locationsUrls = locationsUrlsSchema.safeParse(response.data)
|
||||||
|
|
||||||
|
if (!locationsUrls.success) {
|
||||||
|
locationsUrlsFailCounter.add(1, {
|
||||||
|
lang,
|
||||||
|
error_type: "validation_error",
|
||||||
|
error: JSON.stringify(locationsUrls.error),
|
||||||
|
})
|
||||||
|
|
||||||
|
console.error(`${procedureName}: validation error`, {
|
||||||
|
variables: { lang },
|
||||||
|
error_type: "validation_error",
|
||||||
|
error: locationsUrls.error,
|
||||||
|
})
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
locationsUrlsSuccessCounter.add(1, { lang })
|
||||||
|
|
||||||
|
console.info(`${procedureName}: success`, {
|
||||||
|
variables: { lang },
|
||||||
|
})
|
||||||
|
|
||||||
|
const { data } = locationsUrls
|
||||||
|
data.hotels = data.hotels.map((hotel) => {
|
||||||
|
hotel.url = removeMultipleSlashes(`/${lang}/${hotel.url}`)
|
||||||
|
return hotel
|
||||||
|
})
|
||||||
|
data.cities = data.cities.map((city) => {
|
||||||
|
city.url = removeMultipleSlashes(`/${lang}/${city.url}`)
|
||||||
|
return city
|
||||||
|
})
|
||||||
|
return locationsUrls.data
|
||||||
|
}),
|
||||||
}),
|
}),
|
||||||
map: router({
|
map: router({
|
||||||
city: serviceProcedure
|
city: serviceProcedure
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
import { z } from "zod"
|
||||||
|
|
||||||
|
import { isDefined } from "@/server/utils"
|
||||||
|
|
||||||
|
export const locationsUrlsSchema = z.object({
|
||||||
|
hotels: z
|
||||||
|
.object({
|
||||||
|
items: z.array(
|
||||||
|
z.object({
|
||||||
|
id: z.string(),
|
||||||
|
url: z.string(),
|
||||||
|
})
|
||||||
|
),
|
||||||
|
})
|
||||||
|
.transform((data) => {
|
||||||
|
return data.items
|
||||||
|
}),
|
||||||
|
cities: z
|
||||||
|
.object({
|
||||||
|
items: z
|
||||||
|
.array(
|
||||||
|
z
|
||||||
|
.object({
|
||||||
|
id: z
|
||||||
|
.object({
|
||||||
|
da: z.string().nullish(),
|
||||||
|
de: z.string().nullish(),
|
||||||
|
en: z.string().nullish(),
|
||||||
|
fi: z.string().nullish(),
|
||||||
|
no: z.string().nullish(),
|
||||||
|
pl: z.string().nullish(),
|
||||||
|
sv: z.string().nullish(),
|
||||||
|
})
|
||||||
|
.transform(
|
||||||
|
(data) =>
|
||||||
|
data.da ||
|
||||||
|
data.de ||
|
||||||
|
data.en ||
|
||||||
|
data.fi ||
|
||||||
|
data.no ||
|
||||||
|
data.pl ||
|
||||||
|
data.sv
|
||||||
|
),
|
||||||
|
url: z.string().nullish(),
|
||||||
|
})
|
||||||
|
.partial()
|
||||||
|
.transform((data) => {
|
||||||
|
if (!data.id || !data.url) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: data.id,
|
||||||
|
url: data.url,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.transform((data) => {
|
||||||
|
return data.filter(isDefined)
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
.transform((data) => {
|
||||||
|
return data.items
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
export type GetLocationsUrlsData = z.input<typeof locationsUrlsSchema>
|
||||||
|
export type LocationsUrls = z.output<typeof locationsUrlsSchema>
|
||||||
@@ -102,3 +102,13 @@ export const additionalDataSuccessCounter = meter.createCounter(
|
|||||||
export const additionalDataFailCounter = meter.createCounter(
|
export const additionalDataFailCounter = meter.createCounter(
|
||||||
"trpc.hotels.additionalData-fail"
|
"trpc.hotels.additionalData-fail"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
export const locationsUrlsCounter = meter.createCounter(
|
||||||
|
"trpc.hotels.locations.urls"
|
||||||
|
)
|
||||||
|
export const locationsUrlsSuccessCounter = meter.createCounter(
|
||||||
|
"trpc.hotels.locations.urls-success"
|
||||||
|
)
|
||||||
|
export const locationsUrlsFailCounter = meter.createCounter(
|
||||||
|
"trpc.hotels.locations.urls-fail"
|
||||||
|
)
|
||||||
|
|||||||
@@ -4,6 +4,10 @@ import { z } from "zod"
|
|||||||
import { Lang } from "@/constants/languages"
|
import { Lang } from "@/constants/languages"
|
||||||
import { env } from "@/env/server"
|
import { env } from "@/env/server"
|
||||||
|
|
||||||
|
export function isDefined<T>(argument: T | undefined | null): argument is T {
|
||||||
|
return argument !== undefined && argument !== null
|
||||||
|
}
|
||||||
|
|
||||||
export const langInput = z.object({
|
export const langInput = z.object({
|
||||||
lang: z.nativeEnum(Lang),
|
lang: z.nativeEnum(Lang),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -18,5 +18,5 @@
|
|||||||
".next/types/**/*.ts",
|
".next/types/**/*.ts",
|
||||||
"netlify/functions/**/*.mts"
|
"netlify/functions/**/*.mts"
|
||||||
],
|
],
|
||||||
"exclude": ["node_modules"]
|
"exclude": ["**/node_modules/**"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,10 +17,6 @@ export type DestinationsData = DestinationCountry[]
|
|||||||
|
|
||||||
export type Cities = DestinationCountry["cities"]
|
export type Cities = DestinationCountry["cities"]
|
||||||
|
|
||||||
export type HotelsSectionProps = {
|
|
||||||
destinations: DestinationsData
|
|
||||||
}
|
|
||||||
|
|
||||||
export type DestinationsListProps = {
|
export type DestinationsListProps = {
|
||||||
destinations: DestinationsData
|
destinations: DestinationsData
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import type { JumpToData, JumpToProps, LocationMatchResultsState } from "./"
|
||||||
|
|
||||||
|
export type ClientProps = {
|
||||||
|
results: LocationMatchResultsState
|
||||||
|
latest: NonNullable<LocationMatchResultsState>
|
||||||
|
setFilterString: (filter: string | null) => void
|
||||||
|
onAction: JumpToProps<JumpToData>["onAction"]
|
||||||
|
onClearHistory: () => void
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
export type JumpToDataItem = {
|
||||||
|
id: string
|
||||||
|
displayName: string
|
||||||
|
type: "hotels" | "cities"
|
||||||
|
description: string
|
||||||
|
url: string
|
||||||
|
rankingNames: string[]
|
||||||
|
rankingKeywords: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type JumpToData = JumpToDataItem[]
|
||||||
|
|
||||||
|
export type JumpToHistory = {
|
||||||
|
id: JumpToDataItem["id"]
|
||||||
|
type: JumpToDataItem["type"]
|
||||||
|
}[]
|
||||||
|
|
||||||
|
export type JumpToProps<T extends { id: string }[]> = {
|
||||||
|
data: T
|
||||||
|
history: JumpToHistory
|
||||||
|
onAction: (id: T[number]["id"]) => void
|
||||||
|
onClearHistory: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export type LocationMatch = {
|
||||||
|
id: string
|
||||||
|
displayName: string
|
||||||
|
type: string
|
||||||
|
description?: string
|
||||||
|
url?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ScoringMatch = LocationMatch & {
|
||||||
|
score: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export type LocationMatchResult = {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
children: LocationMatch[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type LocationMatchResults = LocationMatchResult[]
|
||||||
|
export type LocationMatchResultsState = LocationMatchResults | null
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import type { getJumpToData } from "@/lib/trpc/memoizedRequests"
|
||||||
|
|
||||||
|
export type JumpToResolverProps = {
|
||||||
|
dataPromise: ReturnType<typeof getJumpToData>
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import type { ClientProps } from "./client"
|
||||||
|
|
||||||
|
export type ResultHistoryProps = Pick<ClientProps, "onClearHistory"> & {
|
||||||
|
results: ClientProps["latest"]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ResultMatchesProps = Pick<ClientProps, "onAction"> & {
|
||||||
|
results: NonNullable<ClientProps["results"]>
|
||||||
|
}
|
||||||
@@ -39,6 +39,13 @@ export const PrimaryDefault: Story = {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const PrimaryDisabled: Story = {
|
||||||
|
args: {
|
||||||
|
...PrimaryDefault.args,
|
||||||
|
isDisabled: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
export const PrimaryLarge: Story = {
|
export const PrimaryLarge: Story = {
|
||||||
args: {
|
args: {
|
||||||
...PrimaryDefault.args,
|
...PrimaryDefault.args,
|
||||||
@@ -69,6 +76,13 @@ export const SecondaryDefault: Story = {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const SecondaryDisabled: Story = {
|
||||||
|
args: {
|
||||||
|
...SecondaryDefault.args,
|
||||||
|
isDisabled: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
export const SecondaryLarge: Story = {
|
export const SecondaryLarge: Story = {
|
||||||
args: {
|
args: {
|
||||||
...SecondaryDefault.args,
|
...SecondaryDefault.args,
|
||||||
@@ -99,6 +113,13 @@ export const TertiaryDefault: Story = {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const TertiaryDisabled: Story = {
|
||||||
|
args: {
|
||||||
|
...TertiaryDefault.args,
|
||||||
|
isDisabled: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
export const TertiaryLarge: Story = {
|
export const TertiaryLarge: Story = {
|
||||||
args: {
|
args: {
|
||||||
...TertiaryDefault.args,
|
...TertiaryDefault.args,
|
||||||
@@ -120,6 +141,43 @@ export const TertiarySmall: Story = {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const InvertedDefault: Story = {
|
||||||
|
args: {
|
||||||
|
onPress: fn(),
|
||||||
|
children: 'Inverted button',
|
||||||
|
typography: 'Body/Paragraph/mdBold',
|
||||||
|
variant: 'Inverted',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const InvertedDisabled: Story = {
|
||||||
|
args: {
|
||||||
|
...InvertedDefault.args,
|
||||||
|
isDisabled: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const InvertedLarge: Story = {
|
||||||
|
args: {
|
||||||
|
...InvertedDefault.args,
|
||||||
|
size: 'Large',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const InvertedMedium: Story = {
|
||||||
|
args: {
|
||||||
|
...InvertedDefault.args,
|
||||||
|
size: 'Medium',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const InvertedSmall: Story = {
|
||||||
|
args: {
|
||||||
|
...InvertedDefault.args,
|
||||||
|
size: 'Small',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
export const TextDefault: Story = {
|
export const TextDefault: Story = {
|
||||||
args: {
|
args: {
|
||||||
onPress: fn(),
|
onPress: fn(),
|
||||||
|
|||||||
@@ -74,6 +74,24 @@
|
|||||||
color: var(--Component-Button-Brand-Tertiary-On-fill-Disabled);
|
color: var(--Component-Button-Brand-Tertiary-On-fill-Disabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.variant-inverted {
|
||||||
|
background-color: var(--Component-Button-Inverted-Default);
|
||||||
|
border-color: transparent;
|
||||||
|
color: var(--Component-Button-Inverted-On-fill-Default);
|
||||||
|
}
|
||||||
|
|
||||||
|
.variant-inverted:hover {
|
||||||
|
background-color: var(--Component-Button-Inverted-Hover);
|
||||||
|
border-color: transparent;
|
||||||
|
color: var(--Component-Button-Inverted-On-fill-Hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.variant-inverted:disabled {
|
||||||
|
background-color: var(--Component-Button-Inverted-Disabled);
|
||||||
|
border-color: transparent;
|
||||||
|
color: var(--Component-Button-Inverted-On-fill-Disabled);
|
||||||
|
}
|
||||||
|
|
||||||
.variant-text {
|
.variant-text {
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
border-color: transparent;
|
border-color: transparent;
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ export const config = {
|
|||||||
Primary: styles['variant-primary'],
|
Primary: styles['variant-primary'],
|
||||||
Secondary: styles['variant-secondary'],
|
Secondary: styles['variant-secondary'],
|
||||||
Tertiary: styles['variant-tertiary'],
|
Tertiary: styles['variant-tertiary'],
|
||||||
|
Inverted: styles['variant-inverted'],
|
||||||
Text: styles['variant-text'],
|
Text: styles['variant-text'],
|
||||||
Icon: styles['variant-icon'],
|
Icon: styles['variant-icon'],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -126,7 +126,7 @@
|
|||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-aria-components": "^1.6.0",
|
"react-aria-components": "^1.7.1",
|
||||||
"react-dom": "^18.2.0"
|
"react-dom": "^18.2.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -161,7 +161,7 @@
|
|||||||
"material-symbols": "^0.29.0",
|
"material-symbols": "^0.29.0",
|
||||||
"prettier": "^3.4.2",
|
"prettier": "^3.4.2",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-aria-components": "^1.6.0",
|
"react-aria-components": "^1.7.1",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-material-symbols": "^4.4.0",
|
"react-material-symbols": "^4.4.0",
|
||||||
"rollup": "^4.34.8",
|
"rollup": "^4.34.8",
|
||||||
|
|||||||
Reference in New Issue
Block a user