change theme chatbot and search
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
VITE_TOKEN_NAME = 'dzone_token'
|
||||
VITE_REFRESH_TOKEN_NAME = 'dzone_refresh_token'
|
||||
VITE_BASE_URL = 'https://api-dzone.danakcorp.com'
|
||||
# VITE_BASE_URL = 'http://localhost:4000'
|
||||
# VITE_BASE_URL = 'https://nhrq9m1d-4000.eun1.devtunnels.ms'
|
||||
|
||||
VITE_SLUG_ID = 'slug_id'
|
||||
+43
-43
@@ -1,5 +1,5 @@
|
||||
import { FC, useState, useRef, useEffect } from "react";
|
||||
import { Send, CloseCircle, Wifi } from "iconsax-react";
|
||||
import { Send, CloseCircle, User } from "iconsax-react";
|
||||
import io, { Socket } from "socket.io-client";
|
||||
import { SOCKET_EVENTS } from "./constants";
|
||||
import { useSocketHandlers } from "./hooks/useSocketHandlers";
|
||||
@@ -423,38 +423,38 @@ const Chatbot: FC<ChatbotProps> = ({ isOpen, onClose }) => {
|
||||
};
|
||||
|
||||
// Get connection status icon and text
|
||||
const getConnectionStatusInfo = () => {
|
||||
switch (connectionStatus) {
|
||||
case "connected":
|
||||
return {
|
||||
icon: <Wifi size={16} className="text-green-400" />,
|
||||
text: "متصل",
|
||||
className: "bg-green-100 text-green-800",
|
||||
};
|
||||
case "connecting":
|
||||
return {
|
||||
icon: <Wifi size={16} className="text-yellow-400 animate-pulse" />,
|
||||
text: "در حال اتصال...",
|
||||
className: "bg-yellow-100 text-yellow-800",
|
||||
};
|
||||
case "error":
|
||||
return {
|
||||
icon: <Wifi size={16} className="text-red-500" />,
|
||||
text: "خطا در اتصال",
|
||||
className: "bg-red-100 text-red-800",
|
||||
};
|
||||
default:
|
||||
return {
|
||||
icon: <Wifi size={16} variant="Bold" className="text-gray-500" />,
|
||||
text: "قطع اتصال",
|
||||
className: "bg-gray-100 text-gray-800",
|
||||
};
|
||||
}
|
||||
};
|
||||
// const getConnectionStatusInfo = () => {
|
||||
// switch (connectionStatus) {
|
||||
// case "connected":
|
||||
// return {
|
||||
// icon: <Wifi size={16} className="text-green-400" />,
|
||||
// text: "متصل",
|
||||
// className: "bg-green-100 text-green-800",
|
||||
// };
|
||||
// case "connecting":
|
||||
// return {
|
||||
// icon: <Wifi size={16} className="text-yellow-400 animate-pulse" />,
|
||||
// text: "در حال اتصال...",
|
||||
// className: "bg-yellow-100 text-yellow-800",
|
||||
// };
|
||||
// case "error":
|
||||
// return {
|
||||
// icon: <Wifi size={16} className="text-red-500" />,
|
||||
// text: "خطا در اتصال",
|
||||
// className: "bg-red-100 text-red-800",
|
||||
// };
|
||||
// default:
|
||||
// return {
|
||||
// icon: <Wifi size={16} variant="Bold" className="text-gray-500" />,
|
||||
// text: "قطع اتصال",
|
||||
// className: "bg-gray-100 text-gray-800",
|
||||
// };
|
||||
// }
|
||||
// };
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
const connectionInfo = getConnectionStatusInfo();
|
||||
// const connectionInfo = getConnectionStatusInfo();
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -465,7 +465,7 @@ const Chatbot: FC<ChatbotProps> = ({ isOpen, onClose }) => {
|
||||
style={{ height: "500px", direction: "rtl" }}
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="bg-gradient-to-l from-blue-500 to-blue-600 text-white p-3.5 rounded-t-xl flex justify-between items-center shadow-sm">
|
||||
<div className="bg-black text-white p-3.5 rounded-t-xl flex justify-between items-center shadow-sm">
|
||||
<div className="flex items-center">
|
||||
<div
|
||||
className={`w-3 h-3 rounded-full ml-2 ${connectionStatus === "connected"
|
||||
@@ -477,7 +477,7 @@ const Chatbot: FC<ChatbotProps> = ({ isOpen, onClose }) => {
|
||||
></div>
|
||||
<h3 className="font-bold text-base">چت بات داناک</h3>
|
||||
{/* Always show a compact connection status */}
|
||||
<div
|
||||
{/* <div
|
||||
className={`flex items-center text-xs mr-2 ml-2 ${connectionStatus === "connected"
|
||||
? "text-green-200"
|
||||
: connectionStatus === "connecting"
|
||||
@@ -486,11 +486,11 @@ const Chatbot: FC<ChatbotProps> = ({ isOpen, onClose }) => {
|
||||
}`}
|
||||
>
|
||||
{connectionInfo.icon}
|
||||
</div>
|
||||
</div> */}
|
||||
</div>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="focus:outline-none hover:bg-blue-700 p-1 rounded-full transition-colors"
|
||||
className="focus:outline-none hover:bg-gray-700 p-1 rounded-full transition-colors"
|
||||
aria-label="بستن چت"
|
||||
>
|
||||
<CloseCircle size={20} />
|
||||
@@ -520,13 +520,13 @@ const Chatbot: FC<ChatbotProps> = ({ isOpen, onClose }) => {
|
||||
} animate-fade-in`}
|
||||
>
|
||||
{message.isUser && (
|
||||
<div className="w-7 h-7 rounded-full bg-blue-600 flex items-center justify-center ml-2 flex-shrink-0">
|
||||
<span className="text-white text-xs">👤</span>
|
||||
<div className="w-7 h-7 rounded-full bg-black flex items-center justify-center ml-2 flex-shrink-0">
|
||||
<span className="text-white text-xs"><User size={14} color="white" /></span>
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className={`relative px-3.5 py-2.5 pb-4 rounded-lg max-w-[85%] ${message.isUser
|
||||
? "bg-blue-600 text-white rounded-tr-none shadow-sm animate-slide-right"
|
||||
? "bg-black text-white rounded-tr-none shadow-sm animate-slide-right"
|
||||
: message.isSystem
|
||||
? message.metadata?.type === "error"
|
||||
? "bg-red-100 text-red-800 rounded-tl-none"
|
||||
@@ -541,7 +541,7 @@ const Chatbot: FC<ChatbotProps> = ({ isOpen, onClose }) => {
|
||||
: "bg-yellow-100 text-yellow-800 rounded-tl-none"
|
||||
: "bg-white text-gray-800 rounded-tl-none shadow-sm animate-slide-left"
|
||||
} ${message.streaming
|
||||
? "border-r-4 border-blue-400 streaming-message animate-pulse"
|
||||
? "border-r-4 border-gray-400 streaming-message animate-pulse"
|
||||
: ""
|
||||
}`}
|
||||
>
|
||||
@@ -572,8 +572,8 @@ const Chatbot: FC<ChatbotProps> = ({ isOpen, onClose }) => {
|
||||
)}
|
||||
</div>
|
||||
{!message.isUser && !message.isSystem && (
|
||||
<div className="w-7 h-7 rounded-full bg-blue-100 flex items-center justify-center mr-2 flex-shrink-0">
|
||||
<span className="text-blue-600 text-xs">🤖</span>
|
||||
<div className="w-7 h-7 rounded-full bg-gray-200 flex items-center justify-center mr-2 flex-shrink-0">
|
||||
<span className="text-gray-600 text-xs">🤖</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -589,8 +589,8 @@ const Chatbot: FC<ChatbotProps> = ({ isOpen, onClose }) => {
|
||||
<span></span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-7 h-7 rounded-full bg-blue-100 flex items-center justify-center mr-2 flex-shrink-0">
|
||||
<span className="text-blue-600 text-xs">🤖</span>
|
||||
<div className="w-7 h-7 rounded-full bg-gray-200 flex items-center justify-center mr-2 flex-shrink-0">
|
||||
<span className="text-gray-600 text-xs">🤖</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -621,7 +621,7 @@ const Chatbot: FC<ChatbotProps> = ({ isOpen, onClose }) => {
|
||||
isProcessing ||
|
||||
connectionStatus !== "connected"
|
||||
? "bg-gray-300"
|
||||
: "bg-blue-600 hover:bg-blue-700"
|
||||
: "bg-gray-800 hover:bg-gray-900"
|
||||
} text-white rounded-full w-9 h-9 flex items-center justify-center focus:outline-none transition-colors flex-shrink-0`}
|
||||
>
|
||||
<Send
|
||||
|
||||
@@ -13,7 +13,7 @@ const ChatbotButton: FC = () => {
|
||||
<>
|
||||
<button
|
||||
onClick={toggleChatbot}
|
||||
className="fixed bottom-5 right-5 bg-blue-600 text-white rounded-full w-14 h-14 flex items-center justify-center shadow-lg hover:bg-blue-700 transition-colors z-40"
|
||||
className="fixed bottom-5 right-5 bg-primary text-white rounded-full w-14 h-14 flex items-center justify-center shadow-lg hover:bg-gray-800 transition-colors z-40"
|
||||
aria-label="چت با پشتیبانی"
|
||||
>
|
||||
<MessageText1 size={24} />
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
getCompanyRequest,
|
||||
getMyCompany,
|
||||
getSliders,
|
||||
searchCompany,
|
||||
} from "../service/HomeService";
|
||||
|
||||
export const useGetSlugId = () => {
|
||||
@@ -57,3 +58,11 @@ export const useGetMyCompany = () => {
|
||||
queryFn: () => getMyCompany(),
|
||||
});
|
||||
};
|
||||
|
||||
export const useSearchCompany = (query: string) => {
|
||||
return useQuery({
|
||||
queryKey: ["search-company", query],
|
||||
queryFn: () => searchCompany(query),
|
||||
enabled: !!query,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -40,3 +40,8 @@ export const getSliders = async () => {
|
||||
const { data } = await axios.get(`/sliders/list/public`);
|
||||
return data;
|
||||
};
|
||||
|
||||
export const searchCompany = async (query: string) => {
|
||||
const { data } = await axios.get(`/search?q=${query}`);
|
||||
return data;
|
||||
};
|
||||
|
||||
+250
-15
@@ -1,6 +1,6 @@
|
||||
import { FC, useEffect, useState } from 'react'
|
||||
import Input from '../components/Input'
|
||||
import { ArrowDown2, CloseCircle, HambergerMenu, Logout } from 'iconsax-react'
|
||||
import { ArrowDown2, CloseCircle, HambergerMenu, Logout, SearchNormal } from 'iconsax-react'
|
||||
import AvatarImage from '../assets/images/avatar_image.png'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Link, useLocation, useNavigate, useParams } from 'react-router-dom'
|
||||
@@ -13,10 +13,16 @@ import SideBarItem from './SideBarItem'
|
||||
import Button from '../components/Button'
|
||||
import { buildPath } from '../helpers/utils'
|
||||
import { getToken } from '../config/func'
|
||||
|
||||
import { useSearchCompany } from '../pages/home/hooks/useHomeData'
|
||||
const Header: FC = () => {
|
||||
|
||||
const { isLogin, setIsLogin } = useSharedStore()
|
||||
const [search, setSearch] = useState<string>('')
|
||||
const { data: searchData, isLoading: searchLoading } = useSearchCompany(search)
|
||||
const [showSearchResults, setShowSearchResults] = useState(false)
|
||||
const [inputFocused, setInputFocused] = useState(false)
|
||||
const [showMobileSearch, setShowMobileSearch] = useState(false)
|
||||
|
||||
const checkLogin = async () => {
|
||||
const token = await getToken()
|
||||
setIsLogin(!!token)
|
||||
@@ -31,6 +37,60 @@ const Header: FC = () => {
|
||||
const { data } = useGetProfile(isLogin ? true : false)
|
||||
|
||||
|
||||
const handleSearchChange = (value: string) => {
|
||||
setSearch(value)
|
||||
// Show results if there's text
|
||||
if (value) {
|
||||
setShowSearchResults(true)
|
||||
} else {
|
||||
setShowSearchResults(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleInputFocus = () => {
|
||||
setInputFocused(true)
|
||||
// Show results if there's text when focused
|
||||
if (search) {
|
||||
setShowSearchResults(true)
|
||||
}
|
||||
}
|
||||
|
||||
const handleInputBlur = (e: React.FocusEvent) => {
|
||||
// Don't hide if clicking on the dropdown
|
||||
const searchContainer = document.getElementById('search-dropdown-container')
|
||||
const mobileSearchContainer = document.getElementById('mobile-search-dropdown-container')
|
||||
|
||||
if ((searchContainer && searchContainer.contains(e.relatedTarget as Node)) ||
|
||||
(mobileSearchContainer && mobileSearchContainer.contains(e.relatedTarget as Node))) {
|
||||
return
|
||||
}
|
||||
|
||||
// Otherwise hide and remove focus
|
||||
setInputFocused(false)
|
||||
setTimeout(() => {
|
||||
// Small delay to allow click events to process first
|
||||
setShowSearchResults(false)
|
||||
}, 200)
|
||||
}
|
||||
|
||||
const toggleMobileSearch = () => {
|
||||
setShowMobileSearch(prev => !prev)
|
||||
if (!showMobileSearch) {
|
||||
// When opening search, set focus
|
||||
setTimeout(() => {
|
||||
const mobileSearchInput = document.getElementById('mobile-search-input')
|
||||
if (mobileSearchInput) {
|
||||
(mobileSearchInput as HTMLInputElement).focus()
|
||||
setInputFocused(true)
|
||||
}
|
||||
}, 100)
|
||||
} else {
|
||||
// When closing search, clear results
|
||||
setSearch('')
|
||||
setShowSearchResults(false)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setPopoverKey((prevKey) => prevKey + 1);
|
||||
const check = async () => {
|
||||
@@ -39,23 +99,196 @@ const Header: FC = () => {
|
||||
check()
|
||||
}, [location.pathname]);
|
||||
|
||||
// Close search results when clicking outside
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (e: MouseEvent) => {
|
||||
const searchContainer = document.getElementById('search-dropdown-container')
|
||||
const mobileSearchContainer = document.getElementById('mobile-search-dropdown-container')
|
||||
const searchInput = document.getElementById('search-input-container')
|
||||
const mobileSearchInput = document.getElementById('mobile-search-input-container')
|
||||
|
||||
// Check for both desktop and mobile separately to avoid false negatives
|
||||
const isClickOutsideDesktopSearch =
|
||||
searchContainer && !searchContainer.contains(e.target as Node) &&
|
||||
searchInput && !searchInput.contains(e.target as Node);
|
||||
|
||||
const isClickOutsideMobileSearch =
|
||||
mobileSearchContainer && !mobileSearchContainer.contains(e.target as Node) &&
|
||||
mobileSearchInput && !mobileSearchInput.contains(e.target as Node);
|
||||
|
||||
// Only proceed if we have proper elements to check and click is outside both
|
||||
if ((searchContainer && searchInput && isClickOutsideDesktopSearch) ||
|
||||
(mobileSearchContainer && mobileSearchInput && isClickOutsideMobileSearch)) {
|
||||
setShowSearchResults(false)
|
||||
setInputFocused(false)
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('mousedown', handleClickOutside)
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside)
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className='fixed z-10 right-4 left-4 xl:right-[285px] top-4 xl:h-16 h-12 flex items-center px-6 bg-white justify-between rounded-[32px] xl:w-[calc(100%-305px)]'>
|
||||
|
||||
<div className='min-w-[270px] hidden xl:block'>
|
||||
<Input
|
||||
variant='search'
|
||||
placeholder={t('header.search')}
|
||||
/>
|
||||
</div>
|
||||
{
|
||||
isLogin &&
|
||||
<div onClick={() => setOpenSidebar(!openSidebar)} className='xl:hidden block'>
|
||||
<HambergerMenu size={24} color='black' />
|
||||
{/* Desktop Search */}
|
||||
<div className='min-w-[270px] hidden xl:block relative'>
|
||||
<div id="search-input-container">
|
||||
<Input
|
||||
variant='search'
|
||||
placeholder={t('header.search')}
|
||||
onChangeSearchFinal={handleSearchChange}
|
||||
onFocus={handleInputFocus}
|
||||
onBlur={handleInputBlur}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
|
||||
{showSearchResults && search && inputFocused && (
|
||||
<div
|
||||
id="search-dropdown-container"
|
||||
className='absolute z-20 mt-1 w-full bg-white rounded-xl shadow-lg max-h-80 overflow-y-auto'
|
||||
>
|
||||
{searchLoading && (
|
||||
<div className='p-4 text-center text-sm text-description'>
|
||||
در حال جستجو...
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!searchLoading && searchData?.data?.length === 0 && (
|
||||
<div className='p-4 text-center text-sm text-description'>
|
||||
نتیجهای یافت نشد
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!searchLoading && searchData?.data?.length > 0 && (
|
||||
<div className='py-2'>
|
||||
{searchData.data.map((company: any) => (
|
||||
<Link
|
||||
to={buildPath(`company/${company.id}`, slug)}
|
||||
key={company.id}
|
||||
className='flex items-center gap-3 px-4 py-2 hover:bg-gray-50 cursor-pointer'
|
||||
onClick={() => {
|
||||
setShowSearchResults(false)
|
||||
setSearch('')
|
||||
setInputFocused(false)
|
||||
}}
|
||||
>
|
||||
<div className='w-10 h-10 rounded-full overflow-hidden bg-gray-100'>
|
||||
{company.profileImageUrl && (
|
||||
<img
|
||||
src={company.profileImageUrl}
|
||||
alt={company.name}
|
||||
className='w-full h-full object-cover'
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className='flex flex-col'>
|
||||
<span className='text-sm font-medium'>{company.name}</span>
|
||||
<span className='text-xs text-description'>{company.chiefExecutiveOfficer}</span>
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Mobile Layout - Only hamburger menu now */}
|
||||
<div className={`${showMobileSearch ? 'w-full' : ''} items-center`}>
|
||||
{!showMobileSearch && isLogin && (
|
||||
<div onClick={() => setOpenSidebar(!openSidebar)} className='xl:hidden block'>
|
||||
<HambergerMenu size={24} color='black' />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Full width mobile search when active */}
|
||||
{showMobileSearch && (
|
||||
<div className='flex w-full items-center gap-2'>
|
||||
<div className='flex-1' id="mobile-search-input-container">
|
||||
<Input
|
||||
id="mobile-search-input"
|
||||
variant='search'
|
||||
placeholder={t('header.search')}
|
||||
onChangeSearchFinal={handleSearchChange}
|
||||
onFocus={handleInputFocus}
|
||||
onBlur={handleInputBlur}
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
onClick={toggleMobileSearch}
|
||||
className='p-2 flex-shrink-0 flex items-center justify-center'
|
||||
>
|
||||
<CloseCircle size={20} color='#8C90A3' />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Mobile Search Results */}
|
||||
{showMobileSearch && showSearchResults && search && (
|
||||
<div
|
||||
id="mobile-search-dropdown-container"
|
||||
className='fixed right-4 left-4 top-16 z-50 bg-white rounded-xl shadow-lg max-h-[calc(100vh-80px)] overflow-y-auto'
|
||||
>
|
||||
{searchLoading && (
|
||||
<div className='p-4 text-center text-sm text-description'>
|
||||
در حال جستجو...
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!searchLoading && searchData?.data?.length === 0 && (
|
||||
<div className='p-4 text-center text-sm text-description'>
|
||||
نتیجهای یافت نشد
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!searchLoading && searchData?.data?.length > 0 && (
|
||||
<div className='py-2'>
|
||||
{searchData.data.map((company: any) => (
|
||||
<Link
|
||||
to={buildPath(`company/${company.id}`, slug)}
|
||||
key={company.id}
|
||||
className='flex items-center gap-3 px-4 py-2 hover:bg-gray-50 cursor-pointer'
|
||||
onClick={() => {
|
||||
setShowSearchResults(false)
|
||||
setSearch('')
|
||||
setInputFocused(false)
|
||||
setShowMobileSearch(false)
|
||||
}}
|
||||
>
|
||||
<div className='w-10 h-10 rounded-full overflow-hidden bg-gray-100'>
|
||||
{company.profileImageUrl && (
|
||||
<img
|
||||
src={company.profileImageUrl}
|
||||
alt={company.name}
|
||||
className='w-full h-full object-cover'
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className='flex flex-col'>
|
||||
<span className='text-sm font-medium'>{company.name}</span>
|
||||
<span className='text-xs text-description'>{company.chiefExecutiveOfficer}</span>
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* <img src={LogoImage} className='h-6 xl:hidden block absolute right-0 left-0 mx-auto' /> */}
|
||||
<div className='flex xl:gap-6 gap-4 items-center'>
|
||||
<div className={`flex xl:gap-6 gap-4 items-center ${showMobileSearch ? 'hidden xl:flex' : ''}`}>
|
||||
{/* Mobile search icon - only visible on mobile when search is not active */}
|
||||
{!showMobileSearch && (
|
||||
<div onClick={toggleMobileSearch} className='xl:hidden block'>
|
||||
<SearchNormal size={20} color='black' />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Notifications */}
|
||||
{
|
||||
isLogin ? (
|
||||
<Notifications />
|
||||
@@ -67,6 +300,8 @@ const Header: FC = () => {
|
||||
onClick={() => navigate(buildPath('auth/login', slug))}
|
||||
/>
|
||||
}
|
||||
|
||||
{/* User profile menu */}
|
||||
{
|
||||
data && (
|
||||
<Popover className="relative" key={popoverKey}>
|
||||
|
||||
Reference in New Issue
Block a user