diff --git a/apps/scandic-web/components/ContentType/DestinationPage/DestinationOverviewPage/JumpTo/Client/ClientInline/clientInline.module.css b/apps/scandic-web/components/ContentType/DestinationPage/DestinationOverviewPage/JumpTo/Client/ClientInline/clientInline.module.css deleted file mode 100644 index 8fb0a1165..000000000 --- a/apps/scandic-web/components/ContentType/DestinationPage/DestinationOverviewPage/JumpTo/Client/ClientInline/clientInline.module.css +++ /dev/null @@ -1,114 +0,0 @@ -.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: 400px; - 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; - } -} diff --git a/apps/scandic-web/components/ContentType/DestinationPage/DestinationOverviewPage/JumpTo/Client/ClientInline/index.tsx b/apps/scandic-web/components/ContentType/DestinationPage/DestinationOverviewPage/JumpTo/Client/ClientInline/index.tsx deleted file mode 100644 index 5eb451c02..000000000 --- a/apps/scandic-web/components/ContentType/DestinationPage/DestinationOverviewPage/JumpTo/Client/ClientInline/index.tsx +++ /dev/null @@ -1,147 +0,0 @@ -"use client" - -import { cx } from "class-variance-authority" -import { memo, 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/MaterialIcon" -import { Typography } from "@scandic-hotels/design-system/Typography" - -import { Results } from "../Results" - -import styles from "./clientInline.module.css" - -import type { ClientProps } from "@/types/components/destinationOverviewPage/jumpTo/client" - -const ResultsMemo = memo(Results) - -export function ClientInline({ - results, - latest, - setFilterString, - onAction, -}: ClientProps) { - const intl = useIntl() - const [isPending, startTransition] = useTransition() - const isMounted = useIsMounted() - - const showResults = !!results - const showHistory = - latest.length > 0 && isMounted() && (!results || results.length === 0) - - return ( - -
- { - startTransition(() => { - setFilterString(null) - }) - }} - > - {({ state }) => ( -
{ - evt.preventDefault() - evt.stopPropagation() - }} - > -
- - - - - { - startTransition(() => { - if (evt.currentTarget.value) { - setFilterString(evt.currentTarget.value) - } else { - setFilterString(null) - } - }) - }} - /> - - - {state.value !== "" && ( - - - {intl.formatMessage({ - defaultMessage: "Clear", - })} - - - )} -
- - - - -
- )} -
- {showResults || showHistory ? ( -
-
- {showResults ? ( - - ) : null} - {showHistory ? ( - - ) : null} -
-
- ) : null} -
-
- ) -} diff --git a/apps/scandic-web/components/ContentType/DestinationPage/DestinationOverviewPage/JumpTo/Client/ClientModal/clientModal.module.css b/apps/scandic-web/components/ContentType/DestinationPage/DestinationOverviewPage/JumpTo/Client/ClientModal/clientModal.module.css deleted file mode 100644 index 6c0f5ebc2..000000000 --- a/apps/scandic-web/components/ContentType/DestinationPage/DestinationOverviewPage/JumpTo/Client/ClientModal/clientModal.module.css +++ /dev/null @@ -1,192 +0,0 @@ -.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%; - cursor: pointer; - - & span { - display: block; - } - - & .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); - } -} diff --git a/apps/scandic-web/components/ContentType/DestinationPage/DestinationOverviewPage/JumpTo/Client/ClientModal/index.tsx b/apps/scandic-web/components/ContentType/DestinationPage/DestinationOverviewPage/JumpTo/Client/ClientModal/index.tsx deleted file mode 100644 index fa02d6a13..000000000 --- a/apps/scandic-web/components/ContentType/DestinationPage/DestinationOverviewPage/JumpTo/Client/ClientModal/index.tsx +++ /dev/null @@ -1,173 +0,0 @@ -"use client" - -import { cx } from "class-variance-authority" -import { memo, 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/MaterialIcon" -import { Typography } from "@scandic-hotels/design-system/Typography" - -import { Results } from "../Results" - -import styles from "./clientModal.module.css" - -import type { ClientProps } from "@/types/components/destinationOverviewPage/jumpTo/client" - -const ResultsMemo = memo(Results) - -export function ClientModal({ - results, - latest, - setFilterString, - onAction, -}: ClientProps) { - const intl = useIntl() - const [isPending, startTransition] = useTransition() - - const showResults = !!results - const showHistory = latest.length > 0 && (!results || results.length === 0) - - return ( - - - - - - {intl.formatMessage({ - defaultMessage: "Where to?", - })} - - - - - {intl.formatMessage({ - defaultMessage: "Hotels & Destinations", - })} - - - - - - - - - - - - {intl.formatMessage({ - defaultMessage: "Find a location", - })} - - - - - -
- { - startTransition(() => { - setFilterString(null) - }) - }} - > - {({ state }) => ( -
{ - evt.preventDefault() - evt.stopPropagation() - }} - > - - - - - { - startTransition(() => { - if (evt.currentTarget.value) { - setFilterString(evt.currentTarget.value) - } else { - setFilterString(null) - } - }) - }} - /> - - - {state.value !== "" && ( - - - {intl.formatMessage({ - defaultMessage: "Clear", - })} - - - )} -
- )} -
-
-
- {showResults ? ( - - ) : null} - {showHistory ? ( - - ) : null} -
-
-
-
-
-
-
-
- ) -} diff --git a/apps/scandic-web/components/ContentType/DestinationPage/DestinationOverviewPage/JumpTo/Client/Results/ResultsSkeleton.tsx b/apps/scandic-web/components/ContentType/DestinationPage/DestinationOverviewPage/JumpTo/Client/Results/ResultsSkeleton.tsx deleted file mode 100644 index 49d82f085..000000000 --- a/apps/scandic-web/components/ContentType/DestinationPage/DestinationOverviewPage/JumpTo/Client/Results/ResultsSkeleton.tsx +++ /dev/null @@ -1,77 +0,0 @@ -"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 ResultsSkeleton() { - const intl = useIntl() - - return ( -
- -
- {intl.formatMessage({ - defaultMessage: "Loading results", - })} -
-
-
-
- - - - -
- -
-
-
-
- - - - -
- -
-
-
-
- - - - -
- -
-
-
-
- - - - -
- -
-
-
-
- - - - -
- -
-
-
-
-
- ) -} diff --git a/apps/scandic-web/components/ContentType/DestinationPage/DestinationOverviewPage/JumpTo/Client/Results/index.tsx b/apps/scandic-web/components/ContentType/DestinationPage/DestinationOverviewPage/JumpTo/Client/Results/index.tsx deleted file mode 100644 index de5e47794..000000000 --- a/apps/scandic-web/components/ContentType/DestinationPage/DestinationOverviewPage/JumpTo/Client/Results/index.tsx +++ /dev/null @@ -1,135 +0,0 @@ -"use client" - -import { - Collection, - Header, - ListLayout, - Menu, - MenuItem, - MenuSection, - Text, - Virtualizer, -} 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 { ResultsProps } from "@/types/components/destinationOverviewPage/jumpTo/results" - -export function Results({ - "aria-label": ariaLabel, - results, - onAction, - renderEmptyState = false, -}: ResultsProps) { - const intl = useIntl() - - return ( - - { - if (renderEmptyState) { - return ( - <> - -
- {intl.formatMessage({ - defaultMessage: "No results", - })} -
-
- - - {intl.formatMessage({ - defaultMessage: - "We couldn't find a matching location for your search.", - })} - - - - ) - } - - return null - }} - > - {(section) => { - if (section.id === "actions") { - return ( - -
- {section.name} -
- - {(item) => ( - - <> - {item.icon ? item.icon : null} - - {item.displayName} - - - - )} - -
- ) - } - - return ( - - -
{section.name}
-
- - {(item) => ( - - - - {item.displayName} - - - {item.description ? ( - - - {item.description} - - - ) : null} - - )} - -
- ) - }} -
-
- ) -} diff --git a/apps/scandic-web/components/ContentType/DestinationPage/DestinationOverviewPage/JumpTo/Client/Results/results.module.css b/apps/scandic-web/components/ContentType/DestinationPage/DestinationOverviewPage/JumpTo/Client/Results/results.module.css deleted file mode 100644 index 8b587d9b3..000000000 --- a/apps/scandic-web/components/ContentType/DestinationPage/DestinationOverviewPage/JumpTo/Client/Results/results.module.css +++ /dev/null @@ -1,80 +0,0 @@ -.menu { - height: 100%; - overflow-y: auto; - - &[data-empty] { - height: initial; - overflow-y: initial; - padding-left: var(--Space-x1); - } -} - -.menu ~ .menu { - padding-top: var(--Space-x2); - border-top: solid 1px var(--Border-Divider-Subtle); -} - -.sectionHeader { - color: var(--UI-Text-Placeholder); - padding-left: var(--Space-x1); - padding-bottom: var(--Space-x05); - - /* Due to Virtualizer we cannot use gap in .menu, - instead we use padding-top on each section header */ - padding-top: var(--Space-x2); - /* Except for the first section header */ - .menu > div > div:first-child & { - padding-top: 0; - } -} - -.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); -} - -.noResultsLabel { - color: var(--Text-Default); -} - -.noResultsDescription { - color: var(--Text-Tertiary); -} - -.menuDivider { - padding-top: var(--Space-x2); - padding-bottom: var(--Space-x2); -} - -.menuDivider:before { - display: block; - content: ""; - height: 1px; - background: var(--Border-Divider-Subtle); -} - -.actionsSection .item { - display: flex; - flex-direction: row; - gap: var(--Space-x05); - align-items: center; - cursor: pointer; -} diff --git a/apps/scandic-web/components/ContentType/DestinationPage/DestinationOverviewPage/JumpTo/Client/client.module.css b/apps/scandic-web/components/ContentType/DestinationPage/DestinationOverviewPage/JumpTo/Client/client.module.css deleted file mode 100644 index f6113d1bc..000000000 --- a/apps/scandic-web/components/ContentType/DestinationPage/DestinationOverviewPage/JumpTo/Client/client.module.css +++ /dev/null @@ -1,13 +0,0 @@ -.inline { - display: none; -} - -@media screen and (min-width: 768px) { - .inline { - display: unset; - } - - .modal { - display: none; - } -} diff --git a/apps/scandic-web/components/ContentType/DestinationPage/DestinationOverviewPage/JumpTo/Client/index.tsx b/apps/scandic-web/components/ContentType/DestinationPage/DestinationOverviewPage/JumpTo/Client/index.tsx deleted file mode 100644 index 97d802125..000000000 --- a/apps/scandic-web/components/ContentType/DestinationPage/DestinationOverviewPage/JumpTo/Client/index.tsx +++ /dev/null @@ -1,209 +0,0 @@ -"use client" - -import { useCallback, useMemo, useState } from "react" -import { useIntl } from "react-intl" -import { useIsMounted, useMediaQuery } from "usehooks-ts" - -import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon" - -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({ - data, - history, - onAction, - onClearHistory, -}: JumpToProps) { - const intl = useIntl() - const isMounted = useIsMounted() - const displayInModal = useMediaQuery("(max-width: 767px)") - - const [filterString, setFilterString] = useState(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((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( - (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 = 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 - - const results: LocationMatchResults = [ - { - id: "latestSearches", - name: "Latest searches", - children: children, - }, - { - id: "actions", // The string "Actions" converts into a divider - name: "Actions", - children: [ - { - id: "clearHistory", - type: "clearHistory", - closesModal: false, - icon: , - displayName: intl.formatMessage({ - defaultMessage: "Clear searches", - }), - }, - ], - }, - ] - - return results - } - return [] - }, [data, history, intl]) - - const results = useMemo(() => { - if (filterString) { - return filter(filterString) - } else { - return null - } - }, [filterString, filter]) - - const props: ClientProps = useMemo(() => { - return { - results, - latest, - setFilterString, - onAction: (key) => { - switch (key) { - case "clearHistory": - onClearHistory() - break - default: - onAction(key) - } - }, - } - }, [results, latest, setFilterString, onAction, onClearHistory]) - - return ( - <> - - - - ) -} diff --git a/apps/scandic-web/components/ContentType/DestinationPage/DestinationOverviewPage/JumpTo/Resolver/index.tsx b/apps/scandic-web/components/ContentType/DestinationPage/DestinationOverviewPage/JumpTo/Resolver/index.tsx deleted file mode 100644 index e948dec98..000000000 --- a/apps/scandic-web/components/ContentType/DestinationPage/DestinationOverviewPage/JumpTo/Resolver/index.tsx +++ /dev/null @@ -1,44 +0,0 @@ -"use client" - -import { use } from "react" - -import { useSearchHistory } from "@/components/Forms/BookingWidget/FormContent/Search/useSearchHistory" - -import { JumpToClient } from "../Client" - -import type { JumpToResolverProps } from "@/types/components/destinationOverviewPage/jumpTo/resolver" - -export function JumpToResolver({ dataPromise }: JumpToResolverProps) { - const data = use(dataPromise) - - const { searchHistory, insertSearchHistoryItem, clearHistory } = - useSearchHistory() - - if (!data) { - return null - } - - return ( - { - const item = data.find((d) => d.id === key) - if (!item) { - return - } - - insertSearchHistoryItem({ - id: item.id, - name: item.displayName, - type: item.type, - searchTokens: [], - destination: item.description, - }) - }} - onClearHistory={() => { - clearHistory() - }} - /> - ) -} diff --git a/apps/scandic-web/components/ContentType/DestinationPage/DestinationOverviewPage/JumpTo/index.tsx b/apps/scandic-web/components/ContentType/DestinationPage/DestinationOverviewPage/JumpTo/index.tsx index 4212d1141..969021017 100644 --- a/apps/scandic-web/components/ContentType/DestinationPage/DestinationOverviewPage/JumpTo/index.tsx +++ b/apps/scandic-web/components/ContentType/DestinationPage/DestinationOverviewPage/JumpTo/index.tsx @@ -1,9 +1,63 @@ -import { getJumpToData } from "@/lib/trpc/memoizedRequests" +"use client" -import { JumpToResolver } from "@/components/ContentType/DestinationPage/DestinationOverviewPage/JumpTo/Resolver" +import { zodResolver } from "@hookform/resolvers/zod" +import * as Sentry from "@sentry/nextjs" +import { useRouter } from "next/navigation" +import { FormProvider, useForm } from "react-hook-form" +import { useIntl } from "react-intl" +import { z } from "zod" -export async function JumpTo() { - const dataPromise = getJumpToData() +import { Search } from "@/components/Forms/BookingWidget/FormContent/Search" +import { toast } from "@/components/TempDesignSystem/Toasts" - return +const jumpToSchema = z.object({ + destinationSearch: z.string().min(1, "Please enter a search term"), +}) +type JumpToSchema = z.infer + +export function JumpTo() { + const router = useRouter() + const intl = useIntl() + const methods = useForm({ + defaultValues: { + destinationSearch: "", + }, + shouldFocusError: false, + mode: "onSubmit", + resolver: zodResolver(jumpToSchema), + reValidateMode: "onSubmit", + }) + + return ( + + { + void 0 + }} + inputName={"destinationSearch"} + onSelect={(item) => { + if (!item.url) { + Sentry.captureMessage( + "Unable to JumpTo selected location, no URL provided", + { + extra: { locationName: item.name }, + } + ) + toast.error( + intl.formatMessage( + { defaultMessage: "Unable to open page for {locationName}" }, + { locationName: item.name } + ) + ) + + return + } + + router.push(item.url) + }} + withSearchButton + /> + + ) } diff --git a/apps/scandic-web/components/ContentType/DestinationPage/DestinationOverviewPage/JumpTo/jumpTo.module.css b/apps/scandic-web/components/ContentType/DestinationPage/DestinationOverviewPage/JumpTo/jumpTo.module.css new file mode 100644 index 000000000..fbcb5ed95 --- /dev/null +++ b/apps/scandic-web/components/ContentType/DestinationPage/DestinationOverviewPage/JumpTo/jumpTo.module.css @@ -0,0 +1,15 @@ +.searchContainer { + display: flex; + justify-content: center; + align-items: center; + margin: 0 auto; + width: 100%; + border-radius: var(--Corner-radius-Rounded); + border: 1px solid var(--Border-Default); + background: var(--Surface-Primary-Default); + padding: var(--Space-x15) var(--Space-x15) var(--Space-x15) var(--Space-x3); + + & > * { + flex: 1; + } +} diff --git a/apps/scandic-web/components/ContentType/DestinationPage/DestinationOverviewPage/destinationOverviewPage.module.css b/apps/scandic-web/components/ContentType/DestinationPage/DestinationOverviewPage/destinationOverviewPage.module.css index 39028f098..5488900f3 100644 --- a/apps/scandic-web/components/ContentType/DestinationPage/DestinationOverviewPage/destinationOverviewPage.module.css +++ b/apps/scandic-web/components/ContentType/DestinationPage/DestinationOverviewPage/destinationOverviewPage.module.css @@ -4,15 +4,12 @@ width: 100%; height: 610px; margin: 0 auto; -} -@media screen and (min-width: 768px) { - .mapContainer { + @media screen and (min-width: 768px) { height: 580px; } -} -@media screen and (min-width: 1367px) { - .mapContainer { + + @media screen and (min-width: 1367px) { height: 560px; } } @@ -38,14 +35,25 @@ margin: 0 auto; } -.jumpToContainer { - display: grid; +.headerContainer { + display: flex; + flex-direction: column; 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; + align-items: center; + + .heading { + color: var(--Text-Interactive-Default); + text-align: center; + } + + .jumpToContainer { + width: 100%; + + @media screen and (min-width: 768px) { + max-width: 800px; + } + } } diff --git a/apps/scandic-web/components/ContentType/DestinationPage/DestinationOverviewPage/index.tsx b/apps/scandic-web/components/ContentType/DestinationPage/DestinationOverviewPage/index.tsx index 498107cc0..96b11b91b 100644 --- a/apps/scandic-web/components/ContentType/DestinationPage/DestinationOverviewPage/index.tsx +++ b/apps/scandic-web/components/ContentType/DestinationPage/DestinationOverviewPage/index.tsx @@ -25,11 +25,13 @@ export default async function DestinationOverviewPage() { return ( <> -
+

{destinationOverviewPage.heading}

- +
+ +
}> diff --git a/apps/scandic-web/components/Forms/BookingWidget/FormContent/Search/SearchList/List/ListItem/index.tsx b/apps/scandic-web/components/Forms/BookingWidget/FormContent/Search/SearchList/List/ListItem/index.tsx index fea9c7633..ad79b69dc 100644 --- a/apps/scandic-web/components/Forms/BookingWidget/FormContent/Search/SearchList/List/ListItem/index.tsx +++ b/apps/scandic-web/components/Forms/BookingWidget/FormContent/Search/SearchList/List/ListItem/index.tsx @@ -47,21 +47,13 @@ export function ListItemSkeleton() { return (
  • - +
    - +
    - +
    - +
  • ) } diff --git a/apps/scandic-web/components/Forms/BookingWidget/FormContent/Search/SearchList/index.tsx b/apps/scandic-web/components/Forms/BookingWidget/FormContent/Search/SearchList/index.tsx index afe44049d..073ab2edf 100644 --- a/apps/scandic-web/components/Forms/BookingWidget/FormContent/Search/SearchList/index.tsx +++ b/apps/scandic-web/components/Forms/BookingWidget/FormContent/Search/SearchList/index.tsx @@ -1,5 +1,5 @@ "use client" -import { useEffect, useState } from "react" +import { useEffect } from "react" import { useFormContext } from "react-hook-form" import { useIntl } from "react-intl" import { useDebounceValue } from "usehooks-ts" @@ -23,6 +23,7 @@ import styles from "./searchList.module.css" import type { SearchListProps } from "@/types/components/search" export default function SearchList({ + searchInputName, getItemProps, getMenuProps, handleClearSearchHistory, @@ -33,14 +34,14 @@ export default function SearchList({ }: SearchListProps) { const lang = useLang() const intl = useIntl() - const [hasMounted, setHasMounted] = useState(false) + const { clearErrors, formState: { errors, isSubmitted }, } = useFormContext() - const searchError = errors["search"] + const searchError = errors[searchInputName] - const [debouncedSearch, setDebouncedSearch] = useDebounceValue(search, 250) + const [debouncedSearch, setDebouncedSearch] = useDebounceValue(search, 300) useEffect(() => { setDebouncedSearch(search) @@ -57,14 +58,14 @@ export default function SearchList({ ) useEffect(() => { - clearErrors("search") - }, [search, clearErrors]) + clearErrors(searchInputName) + }, [search, clearErrors, searchInputName]) useEffect(() => { let timeoutID: ReturnType | null = null if (searchError) { timeoutID = setTimeout(() => { - clearErrors("search") + clearErrors(searchInputName) // magic number originates from animation // 5000ms delay + 120ms exectuion }, 5120) @@ -75,15 +76,7 @@ export default function SearchList({ clearTimeout(timeoutID) } } - }, [clearErrors, searchError]) - - useEffect(() => { - setHasMounted(true) - }, [setHasMounted]) - - if (!hasMounted) { - return null - } + }, [clearErrors, searchError, searchInputName]) if (searchError && isSubmitted && typeof searchError.message === "string") { if (searchError.message === "Required") { @@ -166,7 +159,7 @@ export default function SearchList({ "We couldn't find a matching location for your search.", })} - {searchHistory && ( + {searchHistory && searchHistory.length > 0 && ( <> void + inputName: string + onSelect?: (selectedItem: AutoCompleteLocation) => void + variant?: "rounded" | "default" + withSearchButton?: boolean + selectOnBlur?: boolean +} -const SEARCH_TERM_NAME = "search" - -export default function Search({ handlePressEnter }: SearchProps) { - const { register, setValue } = useFormContext() +export function Search({ + handlePressEnter, + inputName: SEARCH_TERM_NAME, + onSelect, + variant, + withSearchButton = false, + selectOnBlur = false, +}: SearchProps) { + const { register, setValue, setFocus } = useFormContext() const intl = useIntl() - const searchTerm = useWatch({ name: SEARCH_TERM_NAME }) + const searchTerm = useWatch({ name: SEARCH_TERM_NAME }) as string const { searchHistory, insertSearchHistoryItem, clearHistory } = useSearchHistory() @@ -47,6 +64,7 @@ export default function Search({ handlePressEnter }: SearchProps) { case "cities": setValue("hotel", undefined) setValue("city", selectedItem.name) + break case "hotels": setValue("hotel", +selectedItem.id) @@ -56,12 +74,21 @@ export default function Search({ handlePressEnter }: SearchProps) { console.error("Unhandled type:", selectedItem.type) break } + + onSelect?.(selectedItem) } function handleClearSearchHistory() { clearHistory() } + const searchInputClassName = searchInputVariants({ + withSearchButton: withSearchButton, + }) + const clearButtonClassName = clearButtonVariants({ + visible: !!searchTerm?.trim(), + }) + return ( ( -
    -