@hhgtech/hhg-components
Version:
Hello Health Group common components
1,123 lines (1,089 loc) • 73.1 kB
JavaScript
import { a as __awaiter } from './tslib.es6-00ab44b2.js';
import React__default, { useMemo, useEffect, useRef, useCallback, useState, useReducer } from 'react';
import { Global, css } from '@emotion/react';
import { S as StyledLocationResultsWrapper, a as StyledLocationResultsList, b as StyledLiSkeleton, c as StyledLocationItem, d as StyledPinIcon, M as MagnifierIcon, C as CloseIcon, e as ClearIcon, P as PinIcon, D as DefaultThumbnailAny, f as DefaultThumbnailDoctor, g as DefaultThumbnailService, h as getApiPath, u as useGeolocation } from './utils-5d9a60df.js';
import { u as useTranslations } from './index-09d9e570.js';
import debounce from 'lodash/debounce';
import { I as ID_OPTION_ALL_LOCATION, L as LOCALIZED_SLUGS, a as ID_OPTION_CURRENT_LOCATION } from './constants-ce67620e.js';
import styled from '@emotion/styled';
import { I as Input } from './index-5e2dff13.js';
import { T as Text } from './index-0b67696c.js';
import { theme } from './miscTheme.js';
import shuffle from 'lodash/shuffle';
import { a as useOutsideClick } from './useOutsideClick-5ac585f9.js';
import { M as MediaQueries } from './utils-538169b3.js';
import Skeleton from 'react-loading-skeleton';
import { P as PATHS } from './paths-4a2d3f94.js';
import './index-8c40504a.js';
import 'date-fns/locale';
import './index-fe4471f4.js';
import './Locale-dc1237b9.js';
import './constantsSite.js';
import './constantsDomainLocales.js';
import './constantsRiskScreener.js';
import './constantsIsProduction.js';
import './miscCookieHelper.js';
import 'string-format';
import './translationsContext-18f7b7e0.js';
import '@mantine/core';
import './index-17c85f76.js';
const StyledSearchBarWrapper = styled.div ``;
const StyledSearchBarMobileWrapper = styled.div `
--searchbar-z-index: 305;
&[data-is-modal='true'] {
position: fixed;
left: 0;
right: 0;
top: 0;
height: var(--mobile-care-searchbar-height, 100%);
overflow-y: auto;
-webkit-overflow-scrolling: touch;
background-color: white;
z-index: var(--searchbar-z-index);
}
`;
const StyledSearchBarMobileModalHeader = styled.section ``;
const StyledHiddenInputMobile = styled.input `
position: fixed;
left: -100%;
top: -100%;
`;
const StyledInput = styled(Input) `
#booking-search-bar & {
input {
font-size: 16px;
}
}
`;
const LocationResults = ({ cities, isLoading, onLocationClick, selectedLocation, }) => {
const { t, locale } = useTranslations();
const renderContent = () => {
if (isLoading) {
return React__default.createElement(StyledLiSkeleton, { count: 5 });
}
if (!cities || cities.length < 1) {
return (React__default.createElement(React__default.Fragment, null,
React__default.createElement(StyledLocationItem, { "data-is-denied": "true" },
React__default.createElement(Text, { size: "sm", color: "#ccc" }, t('booking.searchBar.noResults')))));
}
return (React__default.createElement(React__default.Fragment, null,
React__default.createElement(StyledLocationItem, { key: "all-location", role: "button", tabIndex: 0, onClick: () => {
var _a;
if (onLocationClick) {
onLocationClick({
name: t('booking.searchBar.allLocations'),
id: ID_OPTION_ALL_LOCATION,
slug: (_a = LOCALIZED_SLUGS[locale]) === null || _a === void 0 ? void 0 : _a.ALL,
});
}
} },
React__default.createElement(StyledPinIcon, { color: theme.colors.neutral400 }),
React__default.createElement(Text, { size: "md" }, t('booking.searchBar.allLocations'))),
cities.map((city) => (React__default.createElement(StyledLocationItem, { key: city.id, onClick: (evt) => {
evt.nativeEvent.stopImmediatePropagation();
if (onLocationClick) {
onLocationClick(city);
}
}, role: "button", tabIndex: 0, "aria-label": `Select ${city.name} as location`, isSelected: (selectedLocation === null || selectedLocation === void 0 ? void 0 : selectedLocation.id) === city.id },
React__default.createElement(StyledPinIcon, { color: theme.colors.neutral400 }),
React__default.createElement(Text, { size: "md" }, city.name))))));
};
return (React__default.createElement(StyledLocationResultsWrapper, { "data-testid": "location-search-results" },
React__default.createElement(StyledLocationResultsList, null, renderContent())));
};
const EVENT_NAMES = {
TYPE_CHARACTER: 'TYPE_CHARACTER',
REMOVE_CHARACTER: 'REMOVE_CHARACTER',
REMOVE_ALL: 'REMOVE_ALL',
REMOVE_LAST_VISIBLE_NODE: 'REMOVE_LAST_VISIBLE_NODE',
PAUSE_FOR: 'PAUSE_FOR',
CHANGE_DELETE_SPEED: 'CHANGE_DELETE_SPEED',
CHANGE_DELAY: 'CHANGE_DELAY',
PASTE_STRING: 'PASTE_STRING',
};
const getRandomInteger = (min, max) => {
return Math.floor(Math.random() * (max - min + 1)) + min;
};
const VISIBLE_NODE_TYPES = {
TEXT_NODE: 'TEXT_NODE',
};
class Typewriter {
constructor(options) {
this.state = {
lastFrameTime: null,
pauseUntil: null,
eventQueue: [],
eventLoop: null,
eventLoopPaused: false,
reverseCalledEvents: [],
calledEvents: [],
visibleNodes: [],
initialOptions: null,
};
this.options = {
strings: null,
delay: 'natural',
pauseFor: 1500,
deleteSpeed: 'natural',
loop: false,
autoStart: false,
stringSplitter: null,
onCreateTextNode: null,
onRemoveNode: null,
};
this.start = () => {
this.state.eventLoopPaused = false;
this.runEventLoop();
return this;
};
this.pause = () => {
this.state.eventLoopPaused = true;
return this;
};
this.stop = () => {
if (this.state.eventLoop) {
window.cancelAnimationFrame(this.state.eventLoop);
this.state.eventLoop = null;
}
return this;
};
this.pauseFor = (ms) => {
this.addEventToQueue(EVENT_NAMES.PAUSE_FOR, { ms });
return this;
};
this.typeOutAllStrings = () => {
if (typeof this.options.strings === 'string') {
this.typeString(this.options.strings).pauseFor(this.options.pauseFor);
return this;
}
this.options.strings.forEach((string) => {
this.typeString(string)
.pauseFor(this.options.pauseFor)
.deleteAll(this.options.deleteSpeed);
});
return this;
};
this.typeString = (string, node = null) => {
if (string) {
const { stringSplitter } = this.options || {};
const characters = typeof stringSplitter === 'function'
? stringSplitter(string)
: string.split('');
this.typeCharacters(characters, node);
}
return this;
};
this.pasteString = (string, node = null) => {
if (string) {
this.addEventToQueue(EVENT_NAMES.PASTE_STRING, {
character: string,
node,
});
}
return this;
};
this.deleteAll = (speed = 'natural') => {
this.addEventToQueue(EVENT_NAMES.REMOVE_ALL, { speed });
return this;
};
this.changeDeleteSpeed = (speed) => {
if (!speed) {
throw new Error('Must provide new delete speed');
}
this.addEventToQueue(EVENT_NAMES.CHANGE_DELETE_SPEED, { speed });
return this;
};
this.changeDelay = (delay) => {
if (!delay) {
throw new Error('Must provide new delay');
}
this.addEventToQueue(EVENT_NAMES.CHANGE_DELAY, { delay });
return this;
};
this.deleteChars = (amount) => {
if (!amount) {
throw new Error('Must provide amount of characters to delete');
}
for (let i = 0; i < amount; i++) {
this.addEventToQueue(EVENT_NAMES.REMOVE_CHARACTER);
}
return this;
};
this.typeCharacters = (characters, node = null) => {
if (!characters || !Array.isArray(characters)) {
throw new Error('Characters must be an array');
}
characters.forEach((character) => {
this.addEventToQueue(EVENT_NAMES.TYPE_CHARACTER, { character, node });
});
return this;
};
this.removeCharacters = (characters) => {
if (!characters || !Array.isArray(characters)) {
throw new Error('Characters must be an array');
}
characters.forEach(() => {
this.addEventToQueue(EVENT_NAMES.REMOVE_CHARACTER);
});
return this;
};
this.addEventToQueue = (eventName, eventArgs, prepend = false) => {
return this.addEventToStateProperty(eventName, eventArgs, prepend, 'eventQueue');
};
this.addReverseCalledEvent = (eventName, eventArgs, prepend = false) => {
const { loop } = this.options;
if (!loop) {
return this;
}
return this.addEventToStateProperty(eventName, eventArgs, prepend, 'reverseCalledEvents');
};
this.addEventToStateProperty = (eventName, eventArgs, prepend = false, property) => {
const eventItem = {
eventName,
eventArgs: eventArgs || {},
};
if (prepend) {
this.state[property] = [eventItem, ...this.state[property]];
}
else {
this.state[property] = [...this.state[property], eventItem];
}
return this;
};
this.runEventLoop = () => {
if (!this.state.lastFrameTime) {
this.state.lastFrameTime = Date.now();
}
// Setup variables to calculate if this frame should run
const nowTime = Date.now();
const delta = nowTime - this.state.lastFrameTime;
if (!this.state.eventQueue.length) {
if (!this.options.loop) {
return;
}
// Reset event queue if we are looping
this.state.eventQueue = [...this.state.calledEvents];
this.state.calledEvents = [];
this.options = Object.assign({}, this.state.initialOptions);
}
// Request next frame
this.state.eventLoop = window.requestAnimationFrame(this.runEventLoop);
// Check if event loop is paused
if (this.state.eventLoopPaused) {
return;
}
// Check if state has pause until time
if (this.state.pauseUntil) {
// Check if event loop should be paused
if (nowTime < this.state.pauseUntil) {
return;
}
// Reset pause time
this.state.pauseUntil = null;
}
// Make a clone of event queue
const eventQueue = [...this.state.eventQueue];
// Get first event from queue
const currentEvent = eventQueue.shift();
// Setup delay variable
let delay = 0;
// Check if frame should run or be
// skipped based on fps interval
if (currentEvent.eventName === EVENT_NAMES.REMOVE_LAST_VISIBLE_NODE ||
currentEvent.eventName === EVENT_NAMES.REMOVE_CHARACTER) {
delay =
this.options.deleteSpeed === 'natural'
? getRandomInteger(40, 80)
: this.options.deleteSpeed;
}
else {
delay =
this.options.delay === 'natural'
? getRandomInteger(120, 160)
: this.options.delay;
}
if (delta <= delay) {
return;
}
// Get current event args
const { eventName, eventArgs } = currentEvent;
// Run item from event loop
switch (eventName) {
case EVENT_NAMES.PASTE_STRING:
case EVENT_NAMES.TYPE_CHARACTER: {
const { character } = eventArgs;
const textNode = document.createTextNode(character);
let textNodeToUse = textNode;
if (this.options.onCreateTextNode &&
typeof this.options.onCreateTextNode === 'function') {
textNodeToUse = this.options.onCreateTextNode(character, textNode);
}
this.state.visibleNodes = [
...this.state.visibleNodes,
{
type: VISIBLE_NODE_TYPES.TEXT_NODE,
character,
node: textNodeToUse,
},
];
break;
}
case EVENT_NAMES.REMOVE_CHARACTER: {
eventQueue.unshift({
eventName: EVENT_NAMES.REMOVE_LAST_VISIBLE_NODE,
eventArgs: { removingCharacterNode: true },
});
break;
}
case EVENT_NAMES.PAUSE_FOR: {
const { ms } = currentEvent.eventArgs;
this.state.pauseUntil = Date.now() + parseInt(ms);
break;
}
case EVENT_NAMES.REMOVE_ALL: {
const { visibleNodes } = this.state;
const { speed } = eventArgs;
const removeAllEventItems = [];
if (speed) {
removeAllEventItems.push({
eventName: EVENT_NAMES.CHANGE_DELETE_SPEED,
eventArgs: { speed, temp: true },
});
}
for (let i = 0, length = visibleNodes.length; i < length; i++) {
removeAllEventItems.push({
eventName: EVENT_NAMES.REMOVE_LAST_VISIBLE_NODE,
eventArgs: { removingCharacterNode: false },
});
}
if (speed) {
removeAllEventItems.push({
eventName: EVENT_NAMES.CHANGE_DELETE_SPEED,
eventArgs: { speed: this.options.deleteSpeed, temp: true },
});
}
eventQueue.unshift(...removeAllEventItems);
break;
}
case EVENT_NAMES.REMOVE_LAST_VISIBLE_NODE: {
if (this.state.visibleNodes.length) {
const { node, character } = this.state.visibleNodes.pop();
if (this.options.onRemoveNode &&
typeof this.options.onRemoveNode === 'function') {
this.options.onRemoveNode({
node,
character,
});
}
if (node) {
node.parentNode.removeChild(node);
}
}
break;
}
case EVENT_NAMES.CHANGE_DELETE_SPEED: {
this.options.deleteSpeed = currentEvent.eventArgs.speed;
break;
}
case EVENT_NAMES.CHANGE_DELAY: {
this.options.delay = currentEvent.eventArgs.delay;
break;
}
}
// Add que item to called queue if we are looping
if (this.options.loop) {
if (currentEvent.eventName !== EVENT_NAMES.REMOVE_LAST_VISIBLE_NODE &&
!(currentEvent.eventArgs && currentEvent.eventArgs.temp)) {
this.state.calledEvents = [...this.state.calledEvents, currentEvent];
}
}
// Replace state event queue with cloned queue
this.state.eventQueue = eventQueue;
// Set last frame time so it can be used to calculate next frame
this.state.lastFrameTime = nowTime;
};
if (options) {
this.options = Object.assign(Object.assign({}, this.options), options);
}
this.state.initialOptions = Object.assign({}, this.options);
this.init();
}
init() {
this.addEventToQueue(EVENT_NAMES.REMOVE_ALL, null, true);
if (this.options.autoStart === true && this.options.strings) {
this.typeOutAllStrings().start();
}
}
}
function useAnimatedPlaceholder({ inputRef, wrapperRef, disabled, }) {
const { t, locale } = useTranslations();
const values = useMemo(() => {
if (disabled) {
return [];
}
const shuffleKeys = [
'booking.searchBar.placeholder.hospital',
'booking.searchBar.placeholder.doctor',
];
if (locale !== 'zh-TW') {
shuffleKeys.push('booking.searchBar.placeholder.specialty', 'booking.searchBar.placeholder.service');
}
return shuffle(shuffleKeys).map((s) => t(s));
}, [t, locale]);
useEffect(() => {
if (disabled) {
return;
}
const input = getInput();
if (input) {
input.placeholder = '';
new Typewriter({
strings: values,
autoStart: true,
loop: true,
delay: 32,
deleteSpeed: 32,
pauseFor: 1500,
onCreateTextNode: customNodeCreator,
onRemoveNode: onRemoveNode,
});
}
}, [inputRef, wrapperRef]);
function customNodeCreator(character) {
const input = getInput();
if (input) {
input.placeholder = input.placeholder + character;
}
return null;
}
function onRemoveNode() {
const input = getInput();
if (input) {
input.placeholder = input.placeholder.slice(0, -1);
}
}
function getInput() {
if (inputRef && inputRef.current) {
return inputRef.current;
}
else if (wrapperRef && wrapperRef.current) {
return wrapperRef.current.querySelector('input') || null;
}
return null;
}
return;
}
const StyledFakeInputSearchMobile = styled.div `
user-select: none;
input {
pointer-events: none;
background-color: white;
}
`;
function MobileFakeInput({ focused, value = '', onClick, trackingAttributes, inputSearchPlaceholder, }) {
const { t } = useTranslations();
const wrapperRef = useRef(null);
useAnimatedPlaceholder({ wrapperRef, disabled: !!inputSearchPlaceholder });
const gaAttributes = useMemo(() => {
if (trackingAttributes) {
return {
'data-event-category': trackingAttributes.dataEventCategory,
'data-event-action': trackingAttributes.dataEventAction,
'data-event-label': trackingAttributes.dataEventLabel,
};
}
else {
return {};
}
}, [trackingAttributes]);
return (React__default.createElement(StyledFakeInputSearchMobile, Object.assign({ onClick: () => onClick && onClick(), ref: wrapperRef }, gaAttributes),
React__default.createElement(StyledInput, { inputClassName: focused === 'location' ? 'focused' : undefined, name: "booking-search-placeholder", displayIcon: React__default.createElement(MagnifierIcon, { color: theme.colors.gray400 }), placeholder: t('booking.searchBar.searchInput.placeholder'), value: value, size: "md" })));
}
const StyledTopBar = styled.header `
display: flex;
height: 64px;
align-items: center;
justify-content: space-between;
margin: 0 auto;
position: sticky;
z-index: 99;
background-color: white;
top: 0;
left: 0;
`;
const StyledTopBarHeaderRight = styled.div `
display: inline-flex;
flex: 1;
justify-content: flex-end;
cursor: pointer;
text-decoration: underline;
`;
const StyledTopBarIcon = styled.div `
display: flex;
align-items: center;
margin-left: 16px;
cursor: pointer;
`;
const StyledTopbarHeading = styled.div `
width: 100%;
margin-left: 24px;
`;
const StyledTopbarHeadingText = styled(Text) `
color: ${theme.colors.secondaryDark};
text-align: start;
`;
const MobileTopBar = ({ className, onLeftClick, headingText, onRightClick, iconLeft, textRight, }) => {
return (React__default.createElement(StyledTopBar, { className: className },
React__default.createElement(StyledTopBarIcon, { onClick: onLeftClick }, iconLeft || React__default.createElement(CloseIcon, { color: theme.colors.gray800 })),
React__default.createElement(StyledTopbarHeading, null,
React__default.createElement(StyledTopbarHeadingText, { size: "md", weight: "bold" }, headingText)),
React__default.createElement(StyledTopBarHeaderRight, { onClick: onRightClick }, textRight && React__default.createElement("p", null, textRight))));
};
const SEARCH_KEYWORD_MIN_LENGTH = {
'en-PH': 3,
'tl-PH': 2,
'id-ID': 3,
'vi-VN': 2,
'zh-TW': 0,
'ms-MY': 2,
'th-TH': 2,
};
const initialState = {
isMobile: false,
isDebug: false,
navigateTo: null,
geoLocationPermission: 'unset',
geoLocationCoordinates: null,
focused: 'none',
searchLocationKeywords: '',
searchKeywords: '',
selectedLocation: null,
defaultCity: null,
initialCities: [],
cities: [],
searchResults: [],
initialSearchResults: [],
isLoadingCities: false,
isLoadingSearchResults: false,
isMobileModalOpened: false,
initialSelectedCity: null,
autoCompleteDisabled: false,
inputSearchPlaceholder: '',
locale: 'vi-VN',
};
var ActionTypes;
(function (ActionTypes) {
ActionTypes["NavigateTo"] = "navigate to url";
ActionTypes["ClickOutsideInputLocation"] = "click outside input location";
ActionTypes["ClickOutsideInputSearch"] = "click outside input search";
ActionTypes["SelectCurrentLocation"] = "select current location";
ActionTypes["FocusInputLocation"] = "focus input location";
ActionTypes["FocusInputSearch"] = "focus input search";
ActionTypes["ChangeInputLocation"] = "change input location";
ActionTypes["ChangeInputSearch"] = "change input search";
ActionTypes["ClearInputLocation"] = "clear input location";
ActionTypes["ClearInputSearch"] = "clear input search";
ActionTypes["SelectLocation"] = "select location";
ActionTypes["FetchCitiesStart"] = "fetch cities start";
ActionTypes["FetchCitiesFinish"] = "fetch cities finish";
ActionTypes["FetchSearchResultsStart"] = "fetch search results start";
ActionTypes["FetchSearchResultsComplete"] = "fetch search results complete";
ActionTypes["FetchSearchResultsFinish"] = "fetch search results finish";
ActionTypes["FetchCitiesByGeoIpComplete"] = "fetch cities by geo IP finish";
ActionTypes["FetchCitiesByKeywordComplete"] = "fetch cities by keyword finish";
ActionTypes["DisplayInitialCities"] = "display initial cities";
ActionTypes["DisplayInitialSearchResults"] = "display initial search results";
ActionTypes["GrantGeoLocationPermission"] = "grant geolocation permission";
ActionTypes["DenyGeoLocationPermission"] = "deny geolocation permission";
ActionTypes["OpenMobileModal"] = "open mobile modal";
ActionTypes["CloseMobileModal"] = "close mobile modal";
ActionTypes["UpdateInitialSearchValue"] = "update initial search value";
ActionTypes["SelectSearchResult"] = "select search result";
ActionTypes["SetInputSearchPlaceholder"] = "set input search placeholder";
})(ActionTypes || (ActionTypes = {}));
function resetSearchLocationKeywords(state) {
const { selectedLocation, searchLocationKeywords } = state;
if (selectedLocation && searchLocationKeywords !== selectedLocation.name) {
return Object.assign(Object.assign({}, state), { searchLocationKeywords: selectedLocation.name });
}
return state;
}
function reducer(state, action) {
var _a;
if (state.isDebug) {
console.log('bookingSearchBar', { action, state });
}
switch (action.type) {
case ActionTypes.NavigateTo:
return Object.assign(Object.assign({}, state), { navigateTo: action.payload, focused: 'none', isMobileModalOpened: false });
case ActionTypes.ClickOutsideInputLocation: {
if (state.isMobile || state.focused !== 'location') {
return state;
}
return resetSearchLocationKeywords(Object.assign(Object.assign({}, state), { focused: 'none' }));
}
case ActionTypes.ClickOutsideInputSearch: {
if (state.isMobile || state.focused !== 'search') {
return state;
}
return Object.assign(Object.assign({}, state), { focused: 'none' });
}
case ActionTypes.FocusInputLocation: {
const nextState = Object.assign(Object.assign({}, state), { focused: 'location' });
if (((_a = state.selectedLocation) === null || _a === void 0 ? void 0 : _a.id) === ID_OPTION_CURRENT_LOCATION) {
nextState.searchLocationKeywords = '';
}
return nextState;
}
case ActionTypes.FocusInputSearch:
const nextState = Object.assign(Object.assign({}, state), { focused: 'search' });
if (state.isMobile) {
return resetSearchLocationKeywords(nextState);
}
return nextState;
case ActionTypes.ChangeInputLocation:
return Object.assign(Object.assign({}, state), { searchLocationKeywords: action.payload });
case ActionTypes.ChangeInputSearch: {
const keyword = action.payload;
return Object.assign(Object.assign({}, state), { searchKeywords: keyword, isLoadingSearchResults: keyword.trim().length > SEARCH_KEYWORD_MIN_LENGTH[state.locale] });
}
case ActionTypes.SelectCurrentLocation:
return Object.assign(Object.assign({}, state), { focused: 'none', selectedLocation: action.payload, searchLocationKeywords: action.payload.name });
case ActionTypes.ClearInputLocation:
return Object.assign(Object.assign({}, state), { focused: 'location', cities: state.initialCities, searchLocationKeywords: '' });
case ActionTypes.ClearInputSearch:
return Object.assign(Object.assign({}, state), { focused: 'search', searchKeywords: '' });
case ActionTypes.SelectLocation:
return Object.assign(Object.assign({}, state), { focused: 'none', selectedLocation: action.payload, searchLocationKeywords: action.payload.name });
case ActionTypes.SelectSearchResult:
return Object.assign(Object.assign({}, state), { searchKeywords: action.payload.name, isMobileModalOpened: false, focused: 'none', navigateTo: action.payload.url });
case ActionTypes.FetchCitiesStart:
return Object.assign(Object.assign({}, state), { isLoadingCities: true });
case ActionTypes.FetchCitiesFinish:
return Object.assign(Object.assign({}, state), { isLoadingCities: false });
case ActionTypes.FetchSearchResultsStart:
return Object.assign(Object.assign({}, state), { isLoadingSearchResults: true });
case ActionTypes.FetchSearchResultsFinish:
return Object.assign(Object.assign({}, state), { isLoadingSearchResults: false });
case ActionTypes.FetchSearchResultsComplete: {
const nextState = Object.assign(Object.assign({}, state), { searchResults: action.payload });
if (state.initialSearchResults.length === 0) {
nextState.initialSearchResults = action.payload;
}
return nextState;
}
case ActionTypes.FetchCitiesByGeoIpComplete: {
let { cities } = action.payload;
const { default: defaultCity } = action.payload;
cities = cities.filter((c) => c.id !== defaultCity.id);
cities.unshift(defaultCity);
if (state.initialSelectedCity &&
!cities.find((c) => c.id === state.initialSelectedCity.id)) {
cities.unshift(state.initialSelectedCity);
}
const nextState = Object.assign(Object.assign({}, state), { defaultCity, initialCities: cities, cities });
if (state.geoLocationPermission === 'denied' &&
!state.initialSelectedCity) {
nextState.selectedLocation = defaultCity;
nextState.searchLocationKeywords = defaultCity.name;
}
return nextState;
}
case ActionTypes.FetchCitiesByKeywordComplete:
return Object.assign(Object.assign({}, state), { cities: action.payload });
case ActionTypes.DisplayInitialCities:
return Object.assign(Object.assign({}, state), { cities: state.initialCities });
case ActionTypes.DisplayInitialSearchResults:
return Object.assign(Object.assign({}, state), { searchResults: state.initialSearchResults, isLoadingSearchResults: false });
case ActionTypes.GrantGeoLocationPermission:
return Object.assign(Object.assign({}, state), { geoLocationPermission: 'granted', geoLocationCoordinates: action.payload });
case ActionTypes.DenyGeoLocationPermission:
return Object.assign(Object.assign({}, state), { geoLocationPermission: 'denied', geoLocationCoordinates: null });
case ActionTypes.OpenMobileModal:
return Object.assign(Object.assign({}, state), { isMobileModalOpened: true });
case ActionTypes.CloseMobileModal:
return Object.assign(Object.assign({}, state), { isMobileModalOpened: false });
case ActionTypes.UpdateInitialSearchValue:
return Object.assign(Object.assign({}, state), { searchKeywords: action.payload });
case ActionTypes.SetInputSearchPlaceholder:
return Object.assign(Object.assign({}, state), { inputSearchPlaceholder: action.payload });
default:
return state;
}
}
const StyledSearchBarInputsWrapper = styled.div `
display: flex;
gap: 16px;
${MediaQueries.mbDown} {
flex-direction: column;
padding: 14px;
background-color: ${theme.colors.primary50};
input {
filter: drop-shadow(0px 4px 8px rgba(0, 0, 0, 0.04))
drop-shadow(0px 0px 2px rgba(0, 0, 0, 0.06))
drop-shadow(0px 0px 1px rgba(0, 0, 0, 0.04));
}
}
input {
background-color: white;
}
`;
const StyledInputWrapper = styled.div `
position: relative;
`;
const StyledInputLocationWrapper = styled(StyledInputWrapper) `
flex: 1;
`;
const StyledInputSearchWrapper = styled(StyledInputWrapper) `
flex: 2;
`;
const StyledClearIcon = styled(ClearIcon) `
display: none;
&.shown {
display: block;
}
`;
const SearchIcon = () => (React__default.createElement("svg", { width: "18", height: "18", viewBox: "0 0 18 18" },
React__default.createElement("path", { fillRule: "evenodd", clipRule: "evenodd", d: "M2.97887 8.15493C2.97887 5.29627 5.29627 2.97887 8.15493 2.97887C11.0136 2.97887 13.331 5.29627 13.331 8.15493C13.331 11.0136 11.0136 13.331 8.15493 13.331C5.29627 13.331 2.97887 11.0136 2.97887 8.15493ZM8.15493 1.5C4.47951 1.5 1.5 4.47951 1.5 8.15493C1.5 11.8303 4.47951 14.8099 8.15493 14.8099C9.72624 14.8099 11.1704 14.2653 12.3089 13.3546L15.2377 16.2834C15.5265 16.5722 15.9947 16.5722 16.2834 16.2834C16.5722 15.9947 16.5722 15.5265 16.2834 15.2377L13.3546 12.3089C14.2653 11.1704 14.8099 9.72624 14.8099 8.15493C14.8099 4.47951 11.8303 1.5 8.15493 1.5Z", fill: "white" })));
const StyledSearchIcon = styled.button `
min-height: 100%;
border-radius: 6px;
background-color: ${theme.colors.primaryBase};
display: flex;
justify-content: center;
align-items: center;
gap: 11px;
cursor: pointer;
flex: 0.3;
${MediaQueries.mbDown} {
--margin: 16px;
min-height: 44px;
margin: var(--margin);
width: calc(100% - var(--margin) * 2);
position: absolute;
bottom: 0;
}
&:hover {
background-color: ${theme.colors.primaryHover};
}
&:active {
background-color: ${theme.colors.primaryActive};
}
&:focus-visible {
outline: 1px solid black;
}
`;
const StyledText$1 = styled(Text) `
font-weight: ${theme.sizes.fwSemiBold};
`;
const SearchButton = ({ onClick }) => {
const { t } = useTranslations();
const title = t('booking.searchBar.searchButton');
return (React__default.createElement(StyledSearchIcon, { onClick: onClick, title: title },
React__default.createElement(SearchIcon, null),
React__default.createElement(StyledText$1, { size: "s3", color: "white" }, title)));
};
function SearchInputs({ isMobile, state, dispatch, debouncedFetchCitiesByKeyword, debouncedFetchSearchResults, focusLocationInput, focusSearchInput, renderLocationResults, renderSearchResults, locationTrackingAttributes, trackingAttributes, onSearch, initalSearchValue, }) {
const inputLocationWrapperRef = useRef(null);
const inputSearchWrapperRef = useRef(null);
const { focused, searchLocationKeywords, geoLocationPermission, selectedLocation, searchKeywords, inputSearchPlaceholder, } = state;
const { t } = useTranslations();
useAnimatedPlaceholder({
wrapperRef: inputSearchWrapperRef,
disabled: !!inputSearchPlaceholder,
});
useOutsideClick(inputLocationWrapperRef, () => {
if (!isMobile) {
dispatch({ type: ActionTypes.ClickOutsideInputLocation });
}
});
useOutsideClick(inputSearchWrapperRef, () => {
if (!isMobile) {
dispatch({ type: ActionTypes.ClickOutsideInputSearch });
}
});
function handleLocationInputClear() {
dispatch({ type: ActionTypes.ClearInputLocation });
debouncedFetchCitiesByKeyword.cancel();
focusLocationInput();
}
function handleSearchInputClear() {
dispatch({ type: ActionTypes.ClearInputSearch });
debouncedFetchSearchResults.cancel();
debouncedFetchSearchResults('', state);
focusSearchInput();
}
const searchInputPinIconColor = useMemo(() => {
if (geoLocationPermission === 'granted' &&
(selectedLocation === null || selectedLocation === void 0 ? void 0 : selectedLocation.id) === ID_OPTION_CURRENT_LOCATION) {
return theme.colors.primaryBase;
}
else {
return theme.colors.gray400;
}
}, [geoLocationPermission, selectedLocation]);
const handleSearchInputChange = useCallback(function handleSearchInputChange(value) {
dispatch({ type: ActionTypes.ChangeInputSearch, payload: value });
if (typeof onSearch === 'function') {
return;
}
if (value.trim().length > SEARCH_KEYWORD_MIN_LENGTH[state.locale] ||
initalSearchValue) {
debouncedFetchSearchResults(value.trim(), state);
}
else {
debouncedFetchSearchResults.cancel();
dispatch({ type: ActionTypes.DisplayInitialSearchResults });
}
}, [dispatch, state]);
const handleSearchLocationInputChange = useCallback(function handleSearchLocationInputChange(value) {
dispatch({ type: ActionTypes.ChangeInputLocation, payload: value });
if (value.trim().length > SEARCH_KEYWORD_MIN_LENGTH[state.locale]) {
debouncedFetchCitiesByKeyword(value.trim());
}
else {
debouncedFetchCitiesByKeyword.cancel();
dispatch({ type: ActionTypes.DisplayInitialCities });
}
}, [debouncedFetchCitiesByKeyword, dispatch]);
const handleKeyPress = (e) => {
if (state.autoCompleteDisabled && e.key === 'Enter' && onSearch) {
onSearch({
cityId: state.selectedLocation.id,
keyword: searchKeywords,
});
}
};
return (React__default.createElement(StyledSearchBarInputsWrapper, null,
React__default.createElement(StyledInputLocationWrapper, { ref: inputLocationWrapperRef },
React__default.createElement(StyledInput, { inputClassName: focused === 'location' ? 'focused' : undefined, placeholder: t('booking.searchBar.locationPlaceholder'), autoComplete: "off", name: "search-location", displayIcon: React__default.createElement(PinIcon, { className: "location-pin-icon", color: searchInputPinIconColor }), actionIcon: React__default.createElement(StyledClearIcon, { color: theme.colors.gray300, className: searchLocationKeywords !== '' ? 'shown' : '' }), onActionClick: handleLocationInputClear, value: searchLocationKeywords, onChange: handleSearchLocationInputChange, onFocus: () => dispatch({ type: ActionTypes.FocusInputLocation }), size: "md", trackingAttributes: locationTrackingAttributes }),
!isMobile && renderLocationResults()),
React__default.createElement(StyledInputSearchWrapper, { ref: inputSearchWrapperRef },
React__default.createElement(StyledInput, { placeholder: inputSearchPlaceholder ||
t('booking.searchBar.searchInput.placeholder'), autoComplete: "off", name: "search-results", actionIcon: React__default.createElement(StyledClearIcon, { color: theme.colors.gray300, className: searchKeywords !== '' ? 'shown' : '' }), onActionClick: handleSearchInputClear, value: searchKeywords, onChange: handleSearchInputChange, onFocus: () => dispatch({ type: ActionTypes.FocusInputSearch }), inputClassName: focused === 'search' ? 'focused' : undefined, size: "md", trackingAttributes: trackingAttributes, onKeyPress: handleKeyPress, enterKeyHint: state.autoCompleteDisabled ? 'search' : undefined }),
!isMobile && renderSearchResults()),
state.autoCompleteDisabled && !state.isMobile && (React__default.createElement(SearchButton, { onClick: () => {
onSearch({
cityId: state.selectedLocation.id,
keyword: searchKeywords,
});
} }))));
}
const DoctorIcon = () => (React__default.createElement("svg", { width: "14", height: "18", viewBox: "0 0 14 18" },
React__default.createElement("path", { d: "M7 .667c-1.702 0-3.36.146-4.48.365a1.67 1.67 0 0 0-1.353 1.64v2.162c0 .459.374.833.833.833h10a.835.835 0 0 0 .833-.833V2.698c0-.804-.566-1.487-1.354-1.64C10.356.84 8.693.667 7 .667m-.833.833h1.666v.834h.834V4h-.834v.834H6.167V4h-.834V2.334h.834zm-4.74 5a8 8 0 0 0-.234 1.276c-.046.404-.137 1.478-.13 1.589a1.94 1.94 0 0 0-.73 1.536c0 .935.645 1.735 1.51 1.928.902 2.19 2.71 4.505 5.157 4.505s4.255-2.338 5.156-4.532a1.935 1.935 0 0 0 1.51-1.9 1.94 1.94 0 0 0-.729-1.537c.007-.11-.08-1.205-.13-1.615a8.4 8.4 0 0 0-.234-1.25H5.359c1.169 1.706 3.916.687 5.678 1.12.537 1.172.13 2.813.13 2.813l.65.208c.095.03.183.114.183.26 0 .157-.114.293-.312.287l-.678-.104-.234.65C9.966 14.05 8.413 15.668 7 15.668s-2.965-1.618-3.776-3.932l-.26-.651-.704.104c-.143 0-.26-.13-.26-.287a.26.26 0 0 1 .182-.26l.651-.208c-.1.12-.423-1.729 0-2.396.759-.524 1.394-.16 2.292-1.537zm3.75 2.552c-.605 0-1.094.502-1.094 1.12s.489 1.12 1.094 1.12 1.094-.501 1.094-1.12c0-.618-.488-1.12-1.094-1.12m3.646 0c-.606 0-1.094.502-1.094 1.12s.488 1.12 1.094 1.12c.605 0 1.094-.501 1.094-1.12 0-.618-.489-1.12-1.094-1.12", fill: "#9AA2AC" })));
const ServiceIcon = () => (React__default.createElement("svg", { width: "18", height: "18", viewBox: "0 0 18 18" },
React__default.createElement("path", { d: "M9 .667A8.33 8.33 0 0 0 2.333 4H4.6A6.7 6.7 0 0 1 9 2.334c1.637 0 3.2.612 4.401 1.666h2.266A8.33 8.33 0 0 0 9 .667M1.786 4.834c-.71 2.207-1.12 4.765-1.12 7.5 0 1.751.16 3.434.47 5h15.729c.309-1.566.468-3.249.468-5 0-2.735-.41-5.293-1.12-7.5zM9 7.334a4.166 4.166 0 1 1 0 8.332 4.166 4.166 0 0 1 0-8.332M8.167 9v1.667H6.5v1.667h1.667V14h1.666v-1.666H11.5v-1.667H9.833V9z", fill: "#9AA2AC" })));
const HospitalIcon = () => (React__default.createElement("svg", { width: "16", height: "17", viewBox: "0 0 16 17" },
React__default.createElement("path", { d: "M8 .667.5 5.635v9.199c0 .92.746 1.666 1.667 1.666h11.666c.921 0 1.667-.745 1.667-1.666V5.635zm3.333 10h-2.5v2.5H7.167v-2.5h-2.5V9h2.5V6.5h1.666V9h2.5z", fill: "#9AA2AC" })));
const SpecialtyIcon = () => (React__default.createElement("svg", { width: "16", height: "16", viewBox: "0 0 16 16" },
React__default.createElement("path", { d: "M3.833.5a3.333 3.333 0 1 0 0 6.667 3.333 3.333 0 0 0 0-6.667M10.5.5c-.92 0-1.667.746-1.667 1.667V5.5c0 .92.746 1.667 1.667 1.667h3.333c.921 0 1.667-.746 1.667-1.667V2.167C15.5 1.246 14.754.5 13.833.5zM3.833 8.833a3.333 3.333 0 1 0 0 6.667 3.333 3.333 0 0 0 0-6.667m8.334 0a3.333 3.333 0 1 0 0 6.667 3.333 3.333 0 0 0 0-6.667", fill: "#9AA2AC" })));
const StyledTypeHeader = styled.header `
display: flex;
justify-content: space-between;
padding: 12px 24px;
background-color: ${theme.colors.neutral50};
box-shadow: inset 0px 1px 0px #e4e8ec;
`;
const StyledInfoWrapper = styled.div `
display: grid;
flex: 2;
align-items: center;
gap: 16px;
grid-template-columns: 16px 1fr;
`;
const StyledTypeName = styled.p `
color: ${theme.colors.neutral800};
font-size: 14px;
font-weight: ${theme.sizes.fwSemiBold};
letter-spacing: -0.2px;
line-height: 22px;
`;
const StyledSeeMoreButton = styled.button `
flex: 1;
cursor: pointer;
text-align: end;
`;
const StyledResultsContainer = styled.section `
padding: 12px 16px;
--rowGap: 16px;
--graphicSize: 32px;
${MediaQueries.tdUp} {
--rowGap: 8px;
}
`;
const StyledItemsWrapper = styled.ul `
--gap: 16px;
${MediaQueries.tdUp} {
--gap: 8px;
}
display: grid;
overflow: hidden;
gap: var(--gap);
transition: max-height 0.8s ease-out;
${({ isMore }) => isMore &&
`
height: auto;
max-height: 0;
margin-top: var(--gap);
`}
${({ isShown }) => isShown &&
`
max-height: 600px;
`}
@media (prefers-reduced-motion) {
transition: none;
}
${MediaQueries.tdUp} {
grid-template-columns: 1fr 1fr;
}
`;
const StyledSeeMore = styled(Text) `
margin-top: 12px;
margin-bottom: 8px;
cursor: pointer;
text-align: start;
text-transform: capitalize;
`;
const StyledResult = styled.li `
display: flex;
align-items: center;
gap: var(--rowGap);
cursor: pointer;
&.not-nav {
cursor: default;
}
`;
const StyledRoundedIconWrapper = styled.div `
--wrapper-size: var(--graphicSize);
border-radius: calc(var(--wrapper-size) / 2);
background-color: var(--neutral50);
width: var(--wrapper-size);
height: var(--wrapper-size);
aspect-ratio: 1/1;
overflow: hidden;
${({ type }) => type &&
type === 'doctors' &&
`
border-radius: calc(var(--graphicSize) / 2);
`}
img {
width: var(--graphicSize);
height: var(--graphicSize);
${({ type }) => type && type === 'doctors'
? `
object-fit: cover;
`
: `
object-fit: contain;
`}
}
&:focus-visible {
outline: 1px solid ${theme.colors.gray600};
}
`;
const StyledText = styled(Text) `
line-height: 22px;
letter-spacing: -0.2px;
${MediaQueries.tdUp} {
&[data-size='p3'] {
font-size: 14px;
}
}
`;
const StyledPictureWrapper = styled.figure `
max-width: var(--graphicSize);
min-width: var(--graphicSize);
`;
const StyledLocationPin = styled.svg `
margin: 0 auto;
display: block;
`;
const StyledImageSkeletonWrapper = styled.div `
display: grid;
place-items: center;
`;
const defaultThumbnailByType = {
doctors: DefaultThumbnailDoctor,
services: DefaultThumbnailService,
};
const ResultSectionItem = ({ type, item, imageUrl, className, onClickItem, }) => {
const onKeyDown = (e) => {
if (e.key === 'Enter' || e.key === 'Space') {
if (onClickItem) {
onClickItem(item);
}
}
};
const defaultThumbnail = useMemo(() => {
const Cmp = defaultThumbnailByType[type] || DefaultThumbnailAny;
return React__default.createElement(Cmp, { width: 32, height: type === 'services' ? 32 : 24 });
}, [type]);
return (React__default.createElement(StyledResult, { onClick: () => onClickItem && onClickItem(item), onKeyDown: onKeyDown, tabIndex: 0, role: "button", className: className },
React__default.createElement(StyledPictureWrapper, null, type === 'hospitals' ? (React__default.createElement(StyledLocationPin, { width: "12", height: "16", viewBox: "0 0 12 16" },
React__default.createElement("path", { fillRule: "evenodd", clipRule: "evenodd", d: "M0.75 5.94873C0.75 3.04623 3.0975 0.69873 6 0.69873C8.9025 0.69873 11.25 3.04623 11.25 5.94873C11.25 9.07623 7.935 13.3887 6.5775 15.0312C6.2775 15.3912 5.73 15.3912 5.43 15.0312C4.065 13.3887 0.75 9.07623 0.75 5.94873ZM4.125 5.94873C4.125 6.98373 4.965 7.82373 6 7.82373C7.035 7.82373 7.875 6.98373 7.875 5.94873C7.875 4.91373 7.035 4.07373 6 4.07373C4.965 4.07373 4.125 4.91373 4.125 5.94873Z", fill: "#9AA2AC" }))) : (React__default.createElement(StyledRoundedIconWrapper, { type: type }, imageUrl ? (React__default.createElement("img", { src: imageUrl, width: 32, height: type === 'services' ? 32 : 24, loading: "lazy" })) : (defaultThumbnail)))),
React__default.createElement(StyledText, { color: theme.colors.gray800, size: "p3" }, item.name)));
};
const ResultSection = ({ type, data, state, dispatch, itemsToShow, itemsToShowDesktop, isMobile, isLoading, keyword, isNavResultItem, }) => {
const [seeMoreOpen, setSeeMoreOpen] = useState(false);
const { t, locale } = useTranslations();
const { selectedLocation, searchKeywords } = state;
const slugs = useMemo(() => LOCALIZED_SLUGS[locale], [locale]);
const isMalay = locale === 'ms-MY';
const typeInformation = useMemo(() => {
return {
doctors: {
name: t('booking.searchPopup.doctors'),
icon: React__default.createElement(DoctorIcon, null),
slug: slugs.DOCTOR,
},
services: {
name: t('booking.searchPopup.services'),
icon: React__default.createElement(ServiceIcon, null),
slug: slugs.SERVICE,
},
hospitals: {
name: t('booking.searchPopup.hospitals'),
icon: React__default.createElement(HospitalIcon, null),
slug: slugs.HOSPITAL,
},
specialties: {
name: t('booking.searchPopup.specialties'),
icon: React__default.createElement(SpecialtyIcon, null),
slug: slugs.SPECIALTIES,
},
};
}, [type, slugs]);
const typeSlug = useMemo(() => typeInformation[type].slug, [typeInformation]);
const showItemCount = isMobile ? itemsToShow : itemsToShowDesktop;
function onClickItem(item) {
let url = '';
switch (type) {
case 'specialties':
url = `/${selectedLocation.slug}/${typeInformation.doctors.slug}/${item.slug}`;
break;
case 'services':
url = `/${selectedLocation.slug}/${typeSlug}/${item.slug}`;
break;
case 'doctors':
case 'hospitals':
url = `/${typeSlug}/${item.slug}-${item.id}`;
break;
}
if (url) {
dispatch({
type: ActionTypes.SelectSearchResult,
payload: { name: item.name, url },
});
}
}
function renderItem(item) {
return (React__default.createElement(ResultSectionItem, { key: item.id, item: item, type: type, imageUrl: item.logo || item.avatar || item.thumbnail, className: !isNavResultItem ? 'not-nav' : '', onClickItem: isNavResultItem && onClickItem }));
}
function onSeeAllClick() {
let url = '';
switch (type) {
case 'specialties':
url = `/${typeSlug}`;
break;
case 'services':
ca