feat(BOOK-67): Added functionality to show/hide the chatbot

Approved-by: Linus Flood
This commit is contained in:
Erik Tiekstra
2025-10-16 10:59:47 +00:00
parent 69a1b5f213
commit 800df0ade9
13 changed files with 147 additions and 72 deletions

View File

@@ -31,6 +31,7 @@
--back-to-top-button: 80;
--language-switcher-z-index: 85;
--sidepeek-z-index: 100;
--chatbot-z-index: 149;
--lightbox-z-index: 150;
--default-modal-overlay-z-index: 100;
--default-modal-z-index: 101;
@@ -73,6 +74,15 @@ body.overflow-hidden {
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) {
:root {
--max-width-single-spacing: var(--Layout-Tablet-Margin-Margin-min);

View 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>
)
}

View File

@@ -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
}

View File

@@ -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

View File

@@ -1,33 +1,9 @@
import Script from "next/script"
import { env } from "@/env/server"
import { CHATBOT_HIDE_ROUTES } from "@/components/ChatbotScript/constants"
import { ChatbotRouteChange } from "@/components/ChatbotScript/RouteChange"
import { getLang } from "@/i18n/serverContext"
import { getPathname } from "@/utils/getPathname"
import { ChatbotClient } from "@/components/ChatbotScript/Client"
export default async function ChatbotScript() {
const lang = await getLang()
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 (
<>
<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} />
</>
)
return <ChatbotClient liveLangs={liveLangs} />
}

View 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
}

View File

@@ -23,4 +23,10 @@ interface Window {
showChat: () => void
hideBubble: () => void
}
kindlyOptions: {
position?: {
bottom?: string
right?: string
}
}
}

View File

@@ -1,7 +1,7 @@
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/jwt.d.ts"
import "@scandic-hotels/trpc/types.d.ts"
declare global {
interface Window {

View File

@@ -29,7 +29,7 @@
.label {
color: var(--Text-Accent-Primary);
}
.when:has([data-isopen="true"]) .label,
.when:has([data-datepicker-open="true"]) .label,
.rooms:has([data-pressed="true"]) .label {
color: var(--Text-Interactive-Focus);
}
@@ -113,7 +113,7 @@
.rooms: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"]) {
background-color: var(--Surface-Primary-Hover);
border: 1px solid var(--Border-Interactive-Focus);

View File

@@ -241,7 +241,11 @@ export default function BookingWidgetClient({
return (
<FormProvider {...methods}>
<section ref={bookingWidgetRef} className={classNames} data-open={isOpen}>
<section
ref={bookingWidgetRef}
className={classNames}
data-booking-widget-open={isOpen}
>
<MobileToggleButton openMobileSearch={openMobileSearch} />
<div className={formContainerClassNames}>
<button

View File

@@ -23,7 +23,7 @@
display: none;
}
.container[data-isopen="true"] .hideWrapper {
.container[data-datepicker-open="true"] .hideWrapper {
display: block;
}
@@ -44,7 +44,7 @@
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;
top: calc(max(var(--sitewide-alert-height), 20px));
}

View File

@@ -140,7 +140,7 @@ export default function DatePickerForm({ name = "date" }: DatePickerFormProps) {
onBlur={(e) => {
closeOnBlur(e.nativeEvent)
}}
data-isopen={isOpen}
data-datepicker-open={isOpen}
ref={ref}
>
<button

View File

@@ -5,7 +5,7 @@
}
/* 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;
}
@@ -36,11 +36,11 @@
}
}
.wrapper[data-open="true"] {
.wrapper[data-booking-widget-open="true"] {
z-index: var(--booking-widget-open-z-index);
}
.wrapper[data-open="true"] .formContainer {
.wrapper[data-booking-widget-open="true"] .formContainer {
left: 0;
bottom: 0;
}
@@ -53,7 +53,7 @@
padding: 0;
}
.wrapper[data-open="true"] + .backdrop {
.wrapper[data-booking-widget-open="true"] + .backdrop {
background-color: rgba(0, 0, 0, 0.4);
height: 100%;
left: 0;