feat(BOOK-67): Added functionality to show/hide the chatbot
Approved-by: Linus Flood
This commit is contained in:
@@ -31,6 +31,7 @@
|
|||||||
--back-to-top-button: 80;
|
--back-to-top-button: 80;
|
||||||
--language-switcher-z-index: 85;
|
--language-switcher-z-index: 85;
|
||||||
--sidepeek-z-index: 100;
|
--sidepeek-z-index: 100;
|
||||||
|
--chatbot-z-index: 149;
|
||||||
--lightbox-z-index: 150;
|
--lightbox-z-index: 150;
|
||||||
--default-modal-overlay-z-index: 100;
|
--default-modal-overlay-z-index: 100;
|
||||||
--default-modal-z-index: 101;
|
--default-modal-z-index: 101;
|
||||||
@@ -73,6 +74,15 @@ body.overflow-hidden {
|
|||||||
border-width: 0;
|
border-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#kindly-chat-api {
|
||||||
|
z-index: var(--chatbot-z-index);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hide chat widget when booking widget is open */
|
||||||
|
body:has([data-booking-widget-open="true"]) #kindly-chat-api {
|
||||||
|
z-index: -1 !important;
|
||||||
|
}
|
||||||
|
|
||||||
@media screen and (min-width: 768px) {
|
@media screen and (min-width: 768px) {
|
||||||
:root {
|
:root {
|
||||||
--max-width-single-spacing: var(--Layout-Tablet-Margin-Margin-min);
|
--max-width-single-spacing: var(--Layout-Tablet-Margin-Margin-min);
|
||||||
|
|||||||
54
apps/scandic-web/components/ChatbotScript/Client.tsx
Normal file
54
apps/scandic-web/components/ChatbotScript/Client.tsx
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { usePathname, useSearchParams } from "next/navigation"
|
||||||
|
import Script from "next/script"
|
||||||
|
import { useEffect } from "react"
|
||||||
|
|
||||||
|
import { shouldShowChatbot } from "@/components/ChatbotScript/utils"
|
||||||
|
import useLang from "@/hooks/useLang"
|
||||||
|
|
||||||
|
interface ChatbotClientProps {
|
||||||
|
liveLangs: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ChatbotClient({ liveLangs }: ChatbotClientProps) {
|
||||||
|
const lang = useLang()
|
||||||
|
const pathname = usePathname()
|
||||||
|
const searchParams = useSearchParams()
|
||||||
|
const currentLang = useLang()
|
||||||
|
const isLive = liveLangs.includes(currentLang)
|
||||||
|
const shouldShow =
|
||||||
|
isLive && shouldShowChatbot(pathname, searchParams, currentLang)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
window.kindlyOptions = {
|
||||||
|
position: {
|
||||||
|
bottom: "130px",
|
||||||
|
right: "20px",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (window.kindlyChat) {
|
||||||
|
if (!shouldShow) {
|
||||||
|
window.kindlyChat.closeChat()
|
||||||
|
window.kindlyChat.hideBubble()
|
||||||
|
} else {
|
||||||
|
window.kindlyChat.showBubble()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [shouldShow])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Script
|
||||||
|
id="kindly-chat"
|
||||||
|
src="https://chat.kindlycdn.com/kindly-chat.js"
|
||||||
|
data-bot-key="910bd27a-7472-43a1-bcfc-955b41adc3e7"
|
||||||
|
data-shadow-dom
|
||||||
|
data-bubble-hidden={!shouldShow}
|
||||||
|
data-language={lang}
|
||||||
|
defer
|
||||||
|
></Script>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import { usePathname } from "next/navigation"
|
|
||||||
import { useEffect } from "react"
|
|
||||||
|
|
||||||
import useLang from "@/hooks/useLang"
|
|
||||||
|
|
||||||
import { CHATBOT_HIDE_ROUTES } from "./constants"
|
|
||||||
|
|
||||||
interface ChatbotRouteChangeProps {
|
|
||||||
liveLangs: string[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ChatbotRouteChange({ liveLangs }: ChatbotRouteChangeProps) {
|
|
||||||
const pathName = usePathname()
|
|
||||||
const currentLang = useLang()
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const isLive = liveLangs.includes(currentLang)
|
|
||||||
const shouldHideChatbot = CHATBOT_HIDE_ROUTES.some((route) =>
|
|
||||||
pathName.includes(route)
|
|
||||||
)
|
|
||||||
if (window.kindlyChat) {
|
|
||||||
if (shouldHideChatbot || !isLive) {
|
|
||||||
window.kindlyChat.closeChat()
|
|
||||||
window.kindlyChat.hideBubble()
|
|
||||||
} else {
|
|
||||||
window.kindlyChat.showBubble()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [pathName, liveLangs, currentLang])
|
|
||||||
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
@@ -1 +1,18 @@
|
|||||||
export const CHATBOT_HIDE_ROUTES = ["/hotelreservation"]
|
import { Lang } from "@scandic-hotels/common/constants/language"
|
||||||
|
|
||||||
|
export const CHATBOT_SHOW_ROUTES = {
|
||||||
|
[Lang.en]: [
|
||||||
|
"/customer-service", // Customer service pages
|
||||||
|
"/scandic-friends", // My pages
|
||||||
|
"/hotels", // Hotel pages
|
||||||
|
],
|
||||||
|
[Lang.sv]: [],
|
||||||
|
[Lang.no]: [],
|
||||||
|
[Lang.fi]: [],
|
||||||
|
[Lang.da]: [],
|
||||||
|
[Lang.de]: [],
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export const CHATBOT_HIDE_CONDITIONS = {
|
||||||
|
searchParams: [{ key: "view", value: "map" }],
|
||||||
|
} as const
|
||||||
|
|||||||
@@ -1,33 +1,9 @@
|
|||||||
import Script from "next/script"
|
|
||||||
|
|
||||||
import { env } from "@/env/server"
|
import { env } from "@/env/server"
|
||||||
|
|
||||||
import { CHATBOT_HIDE_ROUTES } from "@/components/ChatbotScript/constants"
|
import { ChatbotClient } from "@/components/ChatbotScript/Client"
|
||||||
import { ChatbotRouteChange } from "@/components/ChatbotScript/RouteChange"
|
|
||||||
import { getLang } from "@/i18n/serverContext"
|
|
||||||
import { getPathname } from "@/utils/getPathname"
|
|
||||||
|
|
||||||
export default async function ChatbotScript() {
|
export default async function ChatbotScript() {
|
||||||
const lang = await getLang()
|
|
||||||
const liveLangs = env.CHATBOT_LIVE_LANGS
|
const liveLangs = env.CHATBOT_LIVE_LANGS
|
||||||
const pathName = await getPathname()
|
|
||||||
const isLive = liveLangs.includes(lang)
|
|
||||||
const shouldHideChatbot = CHATBOT_HIDE_ROUTES.some((route) =>
|
|
||||||
pathName.includes(route)
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
return <ChatbotClient liveLangs={liveLangs} />
|
||||||
<>
|
|
||||||
<Script
|
|
||||||
id="kindly-chat"
|
|
||||||
src="https://chat.kindlycdn.com/kindly-chat.js"
|
|
||||||
data-bot-key="910bd27a-7472-43a1-bcfc-955b41adc3e7"
|
|
||||||
data-shadow-dom
|
|
||||||
data-bubble-hidden={!isLive || shouldHideChatbot}
|
|
||||||
data-language={lang}
|
|
||||||
defer
|
|
||||||
></Script>
|
|
||||||
<ChatbotRouteChange liveLangs={liveLangs} />
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|||||||
42
apps/scandic-web/components/ChatbotScript/utils.ts
Normal file
42
apps/scandic-web/components/ChatbotScript/utils.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import {
|
||||||
|
removeMultipleSlashes,
|
||||||
|
removeTrailingSlash,
|
||||||
|
} from "@scandic-hotels/common/utils/url"
|
||||||
|
|
||||||
|
import { CHATBOT_HIDE_CONDITIONS, CHATBOT_SHOW_ROUTES } from "./constants"
|
||||||
|
|
||||||
|
import type { Lang } from "@scandic-hotels/common/constants/language"
|
||||||
|
|
||||||
|
export function shouldShowChatbot(
|
||||||
|
pathname: string,
|
||||||
|
searchParams: URLSearchParams | null,
|
||||||
|
lang: Lang
|
||||||
|
): boolean {
|
||||||
|
const cleanPathname = removeTrailingSlash(removeMultipleSlashes(pathname))
|
||||||
|
const isOnShowRoute = CHATBOT_SHOW_ROUTES[lang].some((route) => {
|
||||||
|
const fullRoute = removeTrailingSlash(
|
||||||
|
removeMultipleSlashes(`/${lang}${route}`)
|
||||||
|
)
|
||||||
|
return cleanPathname.startsWith(fullRoute)
|
||||||
|
})
|
||||||
|
|
||||||
|
const isOnStartPage = cleanPathname === `/${lang}`
|
||||||
|
|
||||||
|
if (!isOnShowRoute && !isOnStartPage) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (searchParams) {
|
||||||
|
const shouldHideOnSearchParams = CHATBOT_HIDE_CONDITIONS.searchParams.some(
|
||||||
|
({ key, value }) => {
|
||||||
|
const paramValue = searchParams.get(key)
|
||||||
|
return paramValue === value
|
||||||
|
}
|
||||||
|
)
|
||||||
|
if (shouldHideOnSearchParams) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
6
apps/scandic-web/types/window.d.ts
vendored
6
apps/scandic-web/types/window.d.ts
vendored
@@ -23,4 +23,10 @@ interface Window {
|
|||||||
showChat: () => void
|
showChat: () => void
|
||||||
hideBubble: () => void
|
hideBubble: () => void
|
||||||
}
|
}
|
||||||
|
kindlyOptions: {
|
||||||
|
position?: {
|
||||||
|
bottom?: string
|
||||||
|
right?: string
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
2
packages/booking-flow/global.d.ts
vendored
2
packages/booking-flow/global.d.ts
vendored
@@ -1,7 +1,7 @@
|
|||||||
import "@scandic-hotels/common/global.d.ts"
|
import "@scandic-hotels/common/global.d.ts"
|
||||||
import "@scandic-hotels/trpc/types.d.ts"
|
|
||||||
import "@scandic-hotels/trpc/auth.d.ts"
|
import "@scandic-hotels/trpc/auth.d.ts"
|
||||||
import "@scandic-hotels/trpc/jwt.d.ts"
|
import "@scandic-hotels/trpc/jwt.d.ts"
|
||||||
|
import "@scandic-hotels/trpc/types.d.ts"
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
|
|||||||
@@ -29,7 +29,7 @@
|
|||||||
.label {
|
.label {
|
||||||
color: var(--Text-Accent-Primary);
|
color: var(--Text-Accent-Primary);
|
||||||
}
|
}
|
||||||
.when:has([data-isopen="true"]) .label,
|
.when:has([data-datepicker-open="true"]) .label,
|
||||||
.rooms:has([data-pressed="true"]) .label {
|
.rooms:has([data-pressed="true"]) .label {
|
||||||
color: var(--Text-Interactive-Focus);
|
color: var(--Text-Interactive-Focus);
|
||||||
}
|
}
|
||||||
@@ -113,7 +113,7 @@
|
|||||||
.rooms:hover {
|
.rooms:hover {
|
||||||
background-color: var(--Surface-Primary-Hover);
|
background-color: var(--Surface-Primary-Hover);
|
||||||
}
|
}
|
||||||
.when:has([data-isopen="true"]),
|
.when:has([data-datepicker-open="true"]),
|
||||||
.rooms:has([data-focus-visible="true"], [data-pressed="true"]) {
|
.rooms:has([data-focus-visible="true"], [data-pressed="true"]) {
|
||||||
background-color: var(--Surface-Primary-Hover);
|
background-color: var(--Surface-Primary-Hover);
|
||||||
border: 1px solid var(--Border-Interactive-Focus);
|
border: 1px solid var(--Border-Interactive-Focus);
|
||||||
|
|||||||
@@ -241,7 +241,11 @@ export default function BookingWidgetClient({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<FormProvider {...methods}>
|
<FormProvider {...methods}>
|
||||||
<section ref={bookingWidgetRef} className={classNames} data-open={isOpen}>
|
<section
|
||||||
|
ref={bookingWidgetRef}
|
||||||
|
className={classNames}
|
||||||
|
data-booking-widget-open={isOpen}
|
||||||
|
>
|
||||||
<MobileToggleButton openMobileSearch={openMobileSearch} />
|
<MobileToggleButton openMobileSearch={openMobileSearch} />
|
||||||
<div className={formContainerClassNames}>
|
<div className={formContainerClassNames}>
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.container[data-isopen="true"] .hideWrapper {
|
.container[data-datepicker-open="true"] .hideWrapper {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,7 +44,7 @@
|
|||||||
z-index: 10001;
|
z-index: 10001;
|
||||||
}
|
}
|
||||||
|
|
||||||
.container[data-isopen="true"] .hideWrapper {
|
.container[data-datepicker-open="true"] .hideWrapper {
|
||||||
border-radius: var(--Corner-radius-lg) var(--Corner-radius-lg) 0 0;
|
border-radius: var(--Corner-radius-lg) var(--Corner-radius-lg) 0 0;
|
||||||
top: calc(max(var(--sitewide-alert-height), 20px));
|
top: calc(max(var(--sitewide-alert-height), 20px));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -140,7 +140,7 @@ export default function DatePickerForm({ name = "date" }: DatePickerFormProps) {
|
|||||||
onBlur={(e) => {
|
onBlur={(e) => {
|
||||||
closeOnBlur(e.nativeEvent)
|
closeOnBlur(e.nativeEvent)
|
||||||
}}
|
}}
|
||||||
data-isopen={isOpen}
|
data-datepicker-open={isOpen}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Make sure Date Picker is placed on top of other sticky/fixed components */
|
/* Make sure Date Picker is placed on top of other sticky/fixed components */
|
||||||
.wrapper:has([data-isopen="true"]) {
|
.wrapper:has([data-datepicker-open="true"]) {
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,11 +36,11 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.wrapper[data-open="true"] {
|
.wrapper[data-booking-widget-open="true"] {
|
||||||
z-index: var(--booking-widget-open-z-index);
|
z-index: var(--booking-widget-open-z-index);
|
||||||
}
|
}
|
||||||
|
|
||||||
.wrapper[data-open="true"] .formContainer {
|
.wrapper[data-booking-widget-open="true"] .formContainer {
|
||||||
left: 0;
|
left: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
}
|
}
|
||||||
@@ -53,7 +53,7 @@
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.wrapper[data-open="true"] + .backdrop {
|
.wrapper[data-booking-widget-open="true"] + .backdrop {
|
||||||
background-color: rgba(0, 0, 0, 0.4);
|
background-color: rgba(0, 0, 0, 0.4);
|
||||||
height: 100%;
|
height: 100%;
|
||||||
left: 0;
|
left: 0;
|
||||||
|
|||||||
Reference in New Issue
Block a user