change theme chatbot and search

This commit is contained in:
hamid zarghami
2025-06-09 11:00:02 +03:30
parent f7b28556b0
commit 5153c1e1af
6 changed files with 309 additions and 60 deletions
+1 -1
View File
@@ -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
View File
@@ -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
+1 -1
View File
@@ -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} />
+9
View File
@@ -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,
});
};
+5
View File
@@ -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
View File
@@ -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}>