AW-496-connect-chats
edits
This commit is contained in:
parent
324e7a9eaa
commit
5b8fc5047d
@ -294,6 +294,7 @@
|
||||
},
|
||||
"new_messages": "Новые сообщения",
|
||||
"view_all": "View All ({count})",
|
||||
"hide_all": "Hide",
|
||||
"typing": "is typing...",
|
||||
"voice_message": "Voice message",
|
||||
"photo": "Photo",
|
||||
|
||||
@ -4,13 +4,14 @@ import {
|
||||
ChatModalsWrapper,
|
||||
MessageInputWrapper,
|
||||
} from "@/components/domains/chat";
|
||||
import { loadChatsList } from "@/entities/chats/loaders";
|
||||
|
||||
import styles from "./page.module.scss";
|
||||
|
||||
export default function Chat() {
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<ChatHeader />
|
||||
<ChatHeader chatsPromise={loadChatsList()} />
|
||||
<ChatMessagesWrapper />
|
||||
<MessageInputWrapper />
|
||||
<ChatModalsWrapper />
|
||||
|
||||
@ -10,32 +10,32 @@ import {
|
||||
NewMessagesWrapperSkeleton,
|
||||
} from "@/components/domains/chat";
|
||||
import { NavigationBar } from "@/components/layout";
|
||||
import {
|
||||
loadCategorizedChats,
|
||||
loadCorrespondenceStarted,
|
||||
loadUnreadChats,
|
||||
} from "@/entities/chats/loaders";
|
||||
import { loadChatsList } from "@/entities/chats/loaders";
|
||||
|
||||
import styles from "./page.module.scss";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
export const revalidate = 0;
|
||||
export const fetchCache = "force-no-store";
|
||||
|
||||
export default function Chats() {
|
||||
const chatsPromise = loadChatsList();
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<ChatListHeader />
|
||||
<section className={styles.categories}>
|
||||
<Suspense fallback={<NewMessagesWrapperSkeleton />}>
|
||||
<NewMessagesWrapper chatsPromise={loadUnreadChats()} />
|
||||
<NewMessagesWrapper chatsPromise={chatsPromise} />
|
||||
</Suspense>
|
||||
<Suspense fallback={<CorrespondenceStartedSkeleton />}>
|
||||
<CorrespondenceStartedWrapper
|
||||
chatsPromise={loadCorrespondenceStarted()}
|
||||
/>
|
||||
<CorrespondenceStartedWrapper chatsPromise={chatsPromise} />
|
||||
</Suspense>
|
||||
<Suspense fallback={<ChatCategoriesSkeleton />}>
|
||||
<ChatCategories chatsPromise={loadCategorizedChats()} />
|
||||
<ChatCategories chatsPromise={chatsPromise} />
|
||||
</Suspense>
|
||||
</section>
|
||||
<NavigationBar />
|
||||
<NavigationBar chatsPromise={chatsPromise} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -4,12 +4,17 @@ import {
|
||||
AdvisersSection,
|
||||
AdvisersSectionSkeleton,
|
||||
} from "@/components/domains/dashboard";
|
||||
import { loadChatsList } from "@/entities/chats/loaders";
|
||||
import { loadAssistants } from "@/entities/dashboard/loaders";
|
||||
|
||||
export default function Advisers() {
|
||||
return (
|
||||
<Suspense fallback={<AdvisersSectionSkeleton />}>
|
||||
<AdvisersSection promise={loadAssistants()} gridDisplayMode="vertical" />
|
||||
<AdvisersSection
|
||||
promiseAssistants={loadAssistants()}
|
||||
promiseChats={loadChatsList()}
|
||||
gridDisplayMode="vertical"
|
||||
/>
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,4 +1,6 @@
|
||||
import { DrawerProvider, Header, NavigationBar } from "@/components/layout";
|
||||
import { loadChatsList } from "@/entities/chats/loaders";
|
||||
import { ChatStoreProvider } from "@/providers/chat-store-provider";
|
||||
|
||||
import styles from "./layout.module.scss";
|
||||
|
||||
@ -7,11 +9,14 @@ export default function CoreLayout({
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
const chatsPromise = loadChatsList();
|
||||
return (
|
||||
<DrawerProvider>
|
||||
<Header className={styles.navBar} />
|
||||
<main className={styles.main}>{children}</main>
|
||||
<NavigationBar />
|
||||
<ChatStoreProvider>
|
||||
<Header className={styles.navBar} chatsPromise={chatsPromise} />
|
||||
<main className={styles.main}>{children}</main>
|
||||
<NavigationBar chatsPromise={chatsPromise} />
|
||||
</ChatStoreProvider>
|
||||
</DrawerProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@ -7,10 +7,13 @@ import {
|
||||
CompatibilitySectionSkeleton,
|
||||
MeditationSection,
|
||||
MeditationSectionSkeleton,
|
||||
NewMessagesSection,
|
||||
NewMessagesSectionSkeleton,
|
||||
PalmSection,
|
||||
PalmSectionSkeleton,
|
||||
} from "@/components/domains/dashboard";
|
||||
import { Horoscope } from "@/components/widgets";
|
||||
import { loadChatsList } from "@/entities/chats/loaders";
|
||||
import {
|
||||
loadAssistants,
|
||||
loadCompatibility,
|
||||
@ -21,12 +24,20 @@ import {
|
||||
import styles from "./page.module.scss";
|
||||
|
||||
export default function Home() {
|
||||
const chatsPromise = loadChatsList();
|
||||
return (
|
||||
<section className={styles.page}>
|
||||
<Suspense fallback={<NewMessagesSectionSkeleton />}>
|
||||
<NewMessagesSection chatsPromise={chatsPromise} />
|
||||
</Suspense>
|
||||
|
||||
<Horoscope />
|
||||
|
||||
<Suspense fallback={<AdvisersSectionSkeleton />}>
|
||||
<AdvisersSection promise={loadAssistants()} />
|
||||
<AdvisersSection
|
||||
promiseAssistants={loadAssistants()}
|
||||
promiseChats={chatsPromise}
|
||||
/>
|
||||
</Suspense>
|
||||
|
||||
<Suspense fallback={<CompatibilitySectionSkeleton />}>
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { DrawerProvider, Header } from "@/components/layout";
|
||||
import { loadChatsList } from "@/entities/chats/loaders";
|
||||
|
||||
import styles from "./layout.module.scss";
|
||||
|
||||
@ -9,7 +10,7 @@ export default function CoreLayout({
|
||||
}>) {
|
||||
return (
|
||||
<DrawerProvider>
|
||||
<Header className={styles.navBar} />
|
||||
<Header className={styles.navBar} chatsPromise={loadChatsList()} />
|
||||
<main className={styles.main}>{children}</main>
|
||||
</DrawerProvider>
|
||||
);
|
||||
|
||||
@ -74,6 +74,7 @@ export async function GET(req: NextRequest) {
|
||||
`${nextUrl || ROUTES.payment()}`,
|
||||
process.env.NEXT_PUBLIC_APP_URL || ""
|
||||
);
|
||||
|
||||
if (productId) redirectUrl.searchParams.set("productId", productId);
|
||||
if (placementId) redirectUrl.searchParams.set("placementId", placementId);
|
||||
if (paywallId) redirectUrl.searchParams.set("paywallId", paywallId);
|
||||
|
||||
@ -2,7 +2,7 @@ import "@/styles/reset.css";
|
||||
import "@/styles/globals.css";
|
||||
import "react-circular-progressbar/dist/styles.css";
|
||||
|
||||
import type { Metadata } from "next";
|
||||
import type { Metadata, Viewport } from "next";
|
||||
import { Inter } from "next/font/google";
|
||||
import { notFound } from "next/navigation";
|
||||
import { hasLocale, NextIntlClientProvider } from "next-intl";
|
||||
@ -11,6 +11,7 @@ import clsx from "clsx";
|
||||
|
||||
import { loadUser, loadUserId } from "@/entities/user/loaders";
|
||||
import { routing } from "@/i18n/routing";
|
||||
import { AppUiStoreProvider } from "@/providers/app-ui-store-provider";
|
||||
import { ChatsInitializationProvider } from "@/providers/chats-initialization-provider";
|
||||
import { RetainingStoreProvider } from "@/providers/retaining-store-provider";
|
||||
import SocketProvider from "@/providers/socket-provider";
|
||||
@ -29,6 +30,13 @@ const inter = Inter({
|
||||
variable: "--font-inter",
|
||||
});
|
||||
|
||||
export const viewport: Viewport = {
|
||||
width: "device-width",
|
||||
initialScale: 1,
|
||||
maximumScale: 1,
|
||||
userScalable: false,
|
||||
};
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "WIT",
|
||||
description:
|
||||
@ -60,7 +68,9 @@ export default async function RootLayout({
|
||||
<SocketProvider userId={userId}>
|
||||
<RetainingStoreProvider>
|
||||
<ChatsInitializationProvider>
|
||||
<ToastProvider maxVisible={3}>{children}</ToastProvider>
|
||||
<ToastProvider maxVisible={3}>
|
||||
<AppUiStoreProvider>{children}</AppUiStoreProvider>
|
||||
</ToastProvider>
|
||||
</ChatsInitializationProvider>
|
||||
</RetainingStoreProvider>
|
||||
</SocketProvider>
|
||||
|
||||
@ -31,7 +31,7 @@ export default function CategoryChats({
|
||||
userAvatar={{
|
||||
src: chat.assistantAvatar,
|
||||
alt: chat.assistantName,
|
||||
isOnline: true,
|
||||
isOnline: chat.status === "active",
|
||||
}}
|
||||
name={chat.assistantName}
|
||||
messagePreiew={
|
||||
|
||||
@ -4,31 +4,36 @@ import { use, useState } from "react";
|
||||
|
||||
import { Skeleton } from "@/components/ui";
|
||||
import { Chips } from "@/components/widgets";
|
||||
import { ICategorizedChats } from "@/entities/chats/types";
|
||||
import { IGetChatsListResponse } from "@/entities/chats/types";
|
||||
import { useChatsSocket } from "@/hooks/chats/useChatsSocket";
|
||||
|
||||
import { CategoryChats, ChatItemsList } from "..";
|
||||
|
||||
const MAX_HIDE_VISIBLE_COUNT = 3;
|
||||
|
||||
interface ChatCategoriesProps {
|
||||
chatsPromise: Promise<ICategorizedChats>;
|
||||
chatsPromise: Promise<IGetChatsListResponse>;
|
||||
}
|
||||
|
||||
export default function ChatCategories({ chatsPromise }: ChatCategoriesProps) {
|
||||
const chats = use(chatsPromise);
|
||||
const { categorizedChats } = useChatsSocket({ initialChats: chats });
|
||||
|
||||
const [activeChip, setActiveChip] = useState<string>("All");
|
||||
const [maxVisibleChats, setMaxVisibleChats] = useState<
|
||||
Partial<Record<string, number | null>>
|
||||
>({});
|
||||
|
||||
const chips = Object.keys(chats).map(key => ({
|
||||
const chips = Object.keys(categorizedChats).map(key => ({
|
||||
text: key,
|
||||
}));
|
||||
chips.unshift({
|
||||
text: "All",
|
||||
});
|
||||
|
||||
const filteredChats = Object.keys(chats).filter(key => {
|
||||
const filteredChats = Object.keys(categorizedChats).filter(key => {
|
||||
if (activeChip === "All") return true;
|
||||
return chats[key].some(chat => chat.category === activeChip);
|
||||
return categorizedChats[key].some(chat => chat.category === activeChip);
|
||||
});
|
||||
|
||||
return (
|
||||
@ -44,18 +49,22 @@ export default function ChatCategories({ chatsPromise }: ChatCategoriesProps) {
|
||||
title={key}
|
||||
key={key}
|
||||
viewAllProps={{
|
||||
count: chats[key].length,
|
||||
count: categorizedChats[key].length,
|
||||
isAll: !!maxVisibleChats[key],
|
||||
onClick: () => {
|
||||
setMaxVisibleChats(prev => ({
|
||||
...prev,
|
||||
[key]: !!prev[key] ? null : chats[key].length,
|
||||
[key]: !!prev[key] ? null : categorizedChats[key].length,
|
||||
}));
|
||||
},
|
||||
}}
|
||||
isVisibleViewAll={
|
||||
categorizedChats[key].length > MAX_HIDE_VISIBLE_COUNT
|
||||
}
|
||||
>
|
||||
<CategoryChats
|
||||
chats={chats[key]}
|
||||
maxVisibleChats={maxVisibleChats[key] ?? 3}
|
||||
chats={categorizedChats[key]}
|
||||
maxVisibleChats={maxVisibleChats[key] ?? MAX_HIDE_VISIBLE_COUNT}
|
||||
/>
|
||||
</ChatItemsList>
|
||||
))}
|
||||
|
||||
@ -1,11 +1,20 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { use, useEffect, useState } from "react";
|
||||
import Image from "next/image";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
import { Icon, IconName, OnlineIndicator, Typography } from "@/components/ui";
|
||||
import {
|
||||
Badge,
|
||||
Icon,
|
||||
IconName,
|
||||
OnlineIndicator,
|
||||
Typography,
|
||||
} from "@/components/ui";
|
||||
import { revalidateChatsPage } from "@/entities/chats/actions";
|
||||
import { IGetChatsListResponse } from "@/entities/chats/types";
|
||||
import { useChatsSocket } from "@/hooks/chats/useChatsSocket";
|
||||
import { useChat } from "@/providers/chat-provider";
|
||||
import { useChatStore } from "@/providers/chat-store-provider";
|
||||
import { formatSecondsToHHMMSS } from "@/shared/utils/date";
|
||||
@ -13,12 +22,17 @@ import { delay } from "@/shared/utils/delay";
|
||||
|
||||
import styles from "./ChatHeader.module.scss";
|
||||
|
||||
export default function ChatHeader() {
|
||||
interface ChatHeaderProps {
|
||||
chatsPromise: Promise<IGetChatsListResponse>;
|
||||
}
|
||||
|
||||
export default function ChatHeader({ chatsPromise }: ChatHeaderProps) {
|
||||
const t = useTranslations("Chat");
|
||||
const router = useRouter();
|
||||
const currentChat = useChatStore(state => state.currentChat);
|
||||
const { isLoadingAdvisorMessage, isAvailableChatting } = useChat();
|
||||
|
||||
const chats = use(chatsPromise);
|
||||
const { totalUnreadCount } = useChatsSocket({ initialChats: chats });
|
||||
const [timer, setTimer] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
@ -30,19 +44,26 @@ export default function ChatHeader() {
|
||||
})();
|
||||
}, [isAvailableChatting, timer]);
|
||||
|
||||
const handleBack = async () => {
|
||||
await revalidateChatsPage();
|
||||
router.back();
|
||||
};
|
||||
|
||||
return (
|
||||
<header className={styles.header}>
|
||||
<div className={styles.back} onClick={() => router.back()}>
|
||||
<div className={styles.back} onClick={handleBack}>
|
||||
<Icon
|
||||
name={IconName.ChevronLeft}
|
||||
size={{ height: 22, width: 22 }}
|
||||
color="#374151"
|
||||
/>
|
||||
{/* <Badge className={styles.badge}>
|
||||
<Typography weight="semiBold" size="xs" color="black">
|
||||
2
|
||||
</Typography>
|
||||
</Badge> */}
|
||||
{!!totalUnreadCount && (
|
||||
<Badge className={styles.badge}>
|
||||
<Typography weight="semiBold" size="xs" color="black">
|
||||
{totalUnreadCount}
|
||||
</Typography>
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.chatInfo}>
|
||||
{!!currentChat?.assistantAvatar ? (
|
||||
@ -60,7 +81,7 @@ export default function ChatHeader() {
|
||||
<Typography weight="semiBold" className={styles.name}>
|
||||
{currentChat?.assistantName}
|
||||
<OnlineIndicator
|
||||
isOnline={currentChat?.status === "inactive"}
|
||||
isOnline={currentChat?.status === "active"}
|
||||
className={styles.onlineIndicator}
|
||||
/>
|
||||
</Typography>
|
||||
|
||||
@ -11,6 +11,7 @@ interface ChatItemsListProps {
|
||||
children: React.ReactNode;
|
||||
title: string;
|
||||
viewAllProps: ViewAllProps;
|
||||
isVisibleViewAll?: boolean;
|
||||
}
|
||||
|
||||
export default function ChatItemsList({
|
||||
@ -18,10 +19,15 @@ export default function ChatItemsList({
|
||||
children,
|
||||
title,
|
||||
viewAllProps,
|
||||
isVisibleViewAll = true,
|
||||
}: ChatItemsListProps) {
|
||||
return (
|
||||
<div className={clsx(styles.chatItemsList, className)}>
|
||||
<ChatItemsListHeader title={title} viewAllProps={viewAllProps} />
|
||||
<ChatItemsListHeader
|
||||
title={title}
|
||||
viewAllProps={viewAllProps}
|
||||
isVisibleViewAll={isVisibleViewAll}
|
||||
/>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -7,18 +7,20 @@ import styles from "./ChatItemsListHeader.module.scss";
|
||||
interface ChatItemsListHeaderProps {
|
||||
title: string;
|
||||
viewAllProps: ViewAllProps;
|
||||
isVisibleViewAll?: boolean;
|
||||
}
|
||||
|
||||
export default function ChatItemsListHeader({
|
||||
title,
|
||||
viewAllProps,
|
||||
isVisibleViewAll = true,
|
||||
}: ChatItemsListHeaderProps) {
|
||||
return (
|
||||
<div className={styles.chatItemsListHeader}>
|
||||
<Typography className={styles.title} as="h3" size="lg" weight="bold">
|
||||
{title}
|
||||
</Typography>
|
||||
<ViewAll {...viewAllProps} />
|
||||
{isVisibleViewAll && <ViewAll {...viewAllProps} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -33,7 +33,12 @@ export default function ChatMessage({ message }: ChatMessageProps) {
|
||||
const { read } = useChat();
|
||||
|
||||
useEffect(() => {
|
||||
if (!!message.id && !message.isRead) {
|
||||
if (
|
||||
!!message.id &&
|
||||
!message.isRead &&
|
||||
message.id !== "typing" &&
|
||||
!message.id.startsWith("sending-message")
|
||||
) {
|
||||
read([message.id]);
|
||||
}
|
||||
}, [message.id, message.isRead, read]);
|
||||
|
||||
@ -5,6 +5,7 @@
|
||||
padding: 36px 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-end;
|
||||
justify-content: flex-start;
|
||||
flex-direction: column-reverse;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
@ -13,9 +13,6 @@ export default function ChatMessages({
|
||||
}: ChatMessagesProps) {
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
{messages.map(message => (
|
||||
<ChatMessage key={message.id} message={message} />
|
||||
))}
|
||||
{isLoadingAdvisorMessage && (
|
||||
<ChatMessage
|
||||
message={{
|
||||
@ -28,6 +25,9 @@ export default function ChatMessages({
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{messages.map(message => (
|
||||
<ChatMessage key={message.id} message={message} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -7,5 +7,5 @@
|
||||
.loaderTop {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 8px 0;
|
||||
padding-top: 16px;
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useMemo } from "react";
|
||||
import { useCallback, useEffect, useMemo, useRef } from "react";
|
||||
|
||||
import { Spinner } from "@/components/ui";
|
||||
import { useChat } from "@/providers/chat-provider";
|
||||
@ -17,15 +17,20 @@ export default function ChatMessagesWrapper() {
|
||||
hasMoreOlderMessages,
|
||||
isLoadingOlder,
|
||||
messagesWrapperRef,
|
||||
loadOlder,
|
||||
scrollToBottom,
|
||||
} = useChat();
|
||||
|
||||
// const handleScroll = useCallback(() => {
|
||||
// const el = messagesWrapperRef.current;
|
||||
// if (!el) return;
|
||||
const isInitialScrollDone = useRef(false);
|
||||
|
||||
// if (el.scrollTop < 120) loadOlder();
|
||||
// }, [loadOlder]);
|
||||
const handleScroll = useCallback(() => {
|
||||
const el = messagesWrapperRef.current;
|
||||
if (!el || !isInitialScrollDone.current) return;
|
||||
|
||||
if (el.scrollTop < 100) {
|
||||
loadOlder();
|
||||
}
|
||||
}, [loadOlder, messagesWrapperRef]);
|
||||
|
||||
const mappedMessages = useMemo(() => {
|
||||
const msgs = socketMessages.map(m => ({
|
||||
@ -40,14 +45,20 @@ export default function ChatMessagesWrapper() {
|
||||
}, [socketMessages]);
|
||||
|
||||
useEffect(() => {
|
||||
scrollToBottom();
|
||||
}, [scrollToBottom]);
|
||||
if (socketMessages.length > 0 && !isInitialScrollDone.current) {
|
||||
scrollToBottom();
|
||||
const timeout = setTimeout(() => {
|
||||
isInitialScrollDone.current = true;
|
||||
}, 1000);
|
||||
return () => clearTimeout(timeout);
|
||||
}
|
||||
}, [socketMessages.length, scrollToBottom]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={styles.messagesWrapper}
|
||||
ref={messagesWrapperRef}
|
||||
// onScroll={handleScroll}
|
||||
onScroll={handleScroll}
|
||||
>
|
||||
{isLoadingOlder && hasMoreOlderMessages && (
|
||||
<div className={styles.loaderTop}>
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
.container.container {
|
||||
.card.card {
|
||||
padding: 13px 0;
|
||||
}
|
||||
|
||||
@ -21,3 +21,12 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.container.container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: fit-content;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
@ -12,36 +12,47 @@ import { formatTime } from "@/shared/utils/date";
|
||||
|
||||
import styles from "./CorrespondenceStarted.module.scss";
|
||||
|
||||
const getTopPositionItem = (index: number) => {
|
||||
return Array.from({ length: index }, (_, i) => i).reduce((acc, current) => {
|
||||
return acc + 11 / 1.5 ** current;
|
||||
}, 0);
|
||||
};
|
||||
|
||||
interface CorrespondenceStartedProps {
|
||||
chats: IChat[];
|
||||
maxVisibleChats?: number;
|
||||
isVisibleAll?: boolean;
|
||||
maxHideVisibleCount?: number;
|
||||
}
|
||||
|
||||
export default function CorrespondenceStarted({
|
||||
chats,
|
||||
maxVisibleChats = 3,
|
||||
isVisibleAll = false,
|
||||
maxHideVisibleCount = 3,
|
||||
}: CorrespondenceStartedProps) {
|
||||
const router = useRouter();
|
||||
const t = useTranslations("Chat");
|
||||
const setCurrentChat = useChatStore(state => state.setCurrentChat);
|
||||
|
||||
return (
|
||||
<Card className={styles.container}>
|
||||
<div className={styles.header}>
|
||||
<Icon name={IconName.Pin} size={{ height: 17, width: 13.5 }} />
|
||||
<Typography size="sm" color="muted">
|
||||
{t("correspondence_started.pinned_chats")}
|
||||
</Typography>
|
||||
</div>
|
||||
<div className={styles.chats}>
|
||||
{chats.slice(0, maxVisibleChats).map(chat => (
|
||||
<Container isVisibleAll={isVisibleAll} chats={chats}>
|
||||
{chats
|
||||
.slice(0, isVisibleAll ? chats.length : maxHideVisibleCount)
|
||||
.map((chat, index) => (
|
||||
<ChatItem
|
||||
key={chat.id}
|
||||
className={styles.chat}
|
||||
style={
|
||||
!isVisibleAll
|
||||
? {
|
||||
top: `${getTopPositionItem(index)}px`,
|
||||
zIndex: 1111 - index,
|
||||
position: !!index ? "absolute" : "relative",
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
userAvatar={{
|
||||
src: chat.assistantAvatar,
|
||||
alt: chat.assistantName,
|
||||
isOnline: true,
|
||||
isOnline: chat.status === "active",
|
||||
}}
|
||||
name={chat.assistantName}
|
||||
messagePreiew={
|
||||
@ -62,7 +73,43 @@ export default function CorrespondenceStarted({
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
function Container({
|
||||
children,
|
||||
isVisibleAll,
|
||||
chats,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
isVisibleAll: boolean;
|
||||
chats: IChat[];
|
||||
}) {
|
||||
const t = useTranslations("Chat");
|
||||
|
||||
if (isVisibleAll) {
|
||||
return (
|
||||
<Card className={styles.card}>
|
||||
<div className={styles.header}>
|
||||
<Icon name={IconName.Pin} size={{ height: 17, width: 13.5 }} />
|
||||
<Typography size="sm" color="muted">
|
||||
{t("correspondence_started.pinned_chats")}
|
||||
</Typography>
|
||||
</div>
|
||||
|
||||
<div className={styles.chats}>{children}</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div
|
||||
className={styles.container}
|
||||
style={{
|
||||
paddingBottom: getTopPositionItem(chats.length - 1),
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,15 +1,17 @@
|
||||
"use client";
|
||||
|
||||
import { use, useState } from "react";
|
||||
import { use } from "react";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
import { Skeleton } from "@/components/ui";
|
||||
import { IChat } from "@/entities/chats/types";
|
||||
import { IGetChatsListResponse } from "@/entities/chats/types";
|
||||
import { useChatsSocket } from "@/hooks/chats/useChatsSocket";
|
||||
import { useAppUiStore } from "@/providers/app-ui-store-provider";
|
||||
|
||||
import { ChatItemsList, CorrespondenceStarted } from "..";
|
||||
|
||||
interface CorrespondenceStartedWrapperProps {
|
||||
chatsPromise: Promise<IChat[]>;
|
||||
chatsPromise: Promise<IGetChatsListResponse>;
|
||||
}
|
||||
|
||||
export default function CorrespondenceStartedWrapper({
|
||||
@ -17,24 +19,37 @@ export default function CorrespondenceStartedWrapper({
|
||||
}: CorrespondenceStartedWrapperProps) {
|
||||
const t = useTranslations("Chat");
|
||||
const chats = use(chatsPromise);
|
||||
const { startedChats } = useChatsSocket({ initialChats: chats });
|
||||
|
||||
const [maxVisibleChats, setMaxVisibleChats] = useState<number | null>(null);
|
||||
const { isVisibleAll } = useAppUiStore(
|
||||
state => state.chats.correspondenceStarted
|
||||
);
|
||||
const hasHydrated = useAppUiStore(state => state._hasHydrated);
|
||||
const setChatsCorrespondenceStarted = useAppUiStore(
|
||||
state => state.setChatsCorrespondenceStarted
|
||||
);
|
||||
|
||||
if (!hasHydrated) return <CorrespondenceStartedSkeleton />;
|
||||
|
||||
return (
|
||||
<>
|
||||
{!!chats.length && (
|
||||
{!!startedChats.length && (
|
||||
<ChatItemsList
|
||||
title={t("correspondence_started.title")}
|
||||
viewAllProps={{
|
||||
count: chats.length,
|
||||
count: startedChats.length,
|
||||
isAll: isVisibleAll,
|
||||
onClick: () => {
|
||||
setMaxVisibleChats(prev => (prev ? null : chats.length));
|
||||
setChatsCorrespondenceStarted({
|
||||
isVisibleAll: !isVisibleAll,
|
||||
});
|
||||
},
|
||||
}}
|
||||
isVisibleViewAll={startedChats.length > 1}
|
||||
>
|
||||
<CorrespondenceStarted
|
||||
chats={chats}
|
||||
maxVisibleChats={maxVisibleChats ?? 3}
|
||||
chats={startedChats}
|
||||
isVisibleAll={isVisibleAll}
|
||||
/>
|
||||
</ChatItemsList>
|
||||
)}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
.container {
|
||||
.container.container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: fit-content;
|
||||
@ -11,3 +11,18 @@
|
||||
left: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.card.card {
|
||||
padding: 13px 0;
|
||||
|
||||
& > .newMessage {
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
border-bottom: 1px solid #f3f4f6;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
import { Card } from "@/components/ui";
|
||||
import { ChatItem } from "@/components/widgets";
|
||||
import { IChat } from "@/entities/chats/types";
|
||||
import { useChatStore } from "@/providers/chat-store-provider";
|
||||
@ -19,14 +20,74 @@ const getTopPositionItem = (index: number) => {
|
||||
interface NewMessagesProps {
|
||||
chats: IChat[];
|
||||
isVisibleAll: boolean;
|
||||
maxHideVisibleCount?: number;
|
||||
}
|
||||
|
||||
export default function NewMessages({
|
||||
chats,
|
||||
isVisibleAll = false,
|
||||
maxHideVisibleCount = 3,
|
||||
}: NewMessagesProps) {
|
||||
const router = useRouter();
|
||||
const setCurrentChat = useChatStore(state => state.setCurrentChat);
|
||||
|
||||
return (
|
||||
<Container isVisibleAll={isVisibleAll} chats={chats}>
|
||||
{chats
|
||||
.slice(0, isVisibleAll ? chats.length : maxHideVisibleCount)
|
||||
.map((chat, index) => (
|
||||
<ChatItem
|
||||
key={chat.id}
|
||||
className={styles.newMessage}
|
||||
style={
|
||||
!isVisibleAll
|
||||
? {
|
||||
top: `${getTopPositionItem(index)}px`,
|
||||
zIndex: 1111 - index,
|
||||
position: !!index ? "absolute" : "relative",
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
userAvatar={{
|
||||
src: chat.assistantAvatar,
|
||||
alt: chat.assistantName,
|
||||
isOnline: chat.status === "active",
|
||||
}}
|
||||
name={chat.assistantName}
|
||||
messagePreiew={
|
||||
chat.lastMessage
|
||||
? {
|
||||
message: {
|
||||
type: chat.lastMessage.type,
|
||||
content: chat.lastMessage.text,
|
||||
},
|
||||
}
|
||||
: null
|
||||
}
|
||||
time={formatTime(chat.updatedAt)}
|
||||
badgeContent={chat.unreadCount}
|
||||
onClick={() => {
|
||||
setCurrentChat(chat);
|
||||
router.push(ROUTES.chat(chat.assistantId));
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
function Container({
|
||||
children,
|
||||
isVisibleAll,
|
||||
chats,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
isVisibleAll: boolean;
|
||||
chats: IChat[];
|
||||
}) {
|
||||
if (isVisibleAll) {
|
||||
return <Card className={styles.card}>{children}</Card>;
|
||||
}
|
||||
return (
|
||||
<div
|
||||
className={styles.container}
|
||||
@ -34,43 +95,7 @@ export default function NewMessages({
|
||||
paddingBottom: getTopPositionItem(chats.length - 1),
|
||||
}}
|
||||
>
|
||||
{chats.map((chat, index) => (
|
||||
<ChatItem
|
||||
key={chat.id}
|
||||
className={styles.newMessage}
|
||||
style={
|
||||
!isVisibleAll
|
||||
? {
|
||||
top: `${getTopPositionItem(index)}px`,
|
||||
zIndex: 1111 - index,
|
||||
position: !!index ? "absolute" : "relative",
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
userAvatar={{
|
||||
src: chat.assistantAvatar,
|
||||
alt: chat.assistantName,
|
||||
isOnline: true,
|
||||
}}
|
||||
name={chat.assistantName}
|
||||
messagePreiew={
|
||||
chat.lastMessage
|
||||
? {
|
||||
message: {
|
||||
type: chat.lastMessage.type,
|
||||
content: chat.lastMessage.text,
|
||||
},
|
||||
}
|
||||
: null
|
||||
}
|
||||
time={formatTime(chat.updatedAt)}
|
||||
badgeContent={chat.unreadCount}
|
||||
onClick={() => {
|
||||
setCurrentChat(chat);
|
||||
router.push(ROUTES.chat(chat.assistantId));
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,15 +1,17 @@
|
||||
"use client";
|
||||
|
||||
import { use, useState } from "react";
|
||||
import { use } from "react";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
import { Skeleton } from "@/components/ui";
|
||||
import { IChat } from "@/entities/chats/types";
|
||||
import { IGetChatsListResponse } from "@/entities/chats/types";
|
||||
import { useChatsSocket } from "@/hooks/chats/useChatsSocket";
|
||||
import { useAppUiStore } from "@/providers/app-ui-store-provider";
|
||||
|
||||
import { ChatItemsList, NewMessages } from "..";
|
||||
|
||||
interface NewMessagesWrapperProps {
|
||||
chatsPromise: Promise<IChat[]>;
|
||||
chatsPromise: Promise<IGetChatsListResponse>;
|
||||
}
|
||||
|
||||
export default function NewMessagesWrapper({
|
||||
@ -17,22 +19,29 @@ export default function NewMessagesWrapper({
|
||||
}: NewMessagesWrapperProps) {
|
||||
const t = useTranslations("Chat");
|
||||
const chats = use(chatsPromise);
|
||||
const { unreadChats } = useChatsSocket({ initialChats: chats });
|
||||
|
||||
const [isVisibleAll, setIsVisibleAll] = useState<boolean>(false);
|
||||
const { isVisibleAll } = useAppUiStore(state => state.chats.newMessages);
|
||||
const hasHydrated = useAppUiStore(state => state._hasHydrated);
|
||||
const setChatsNewMessages = useAppUiStore(state => state.setChatsNewMessages);
|
||||
|
||||
if (!hasHydrated) return <NewMessagesWrapperSkeleton />;
|
||||
|
||||
return (
|
||||
<>
|
||||
{!!chats.length && (
|
||||
{!!unreadChats.length && (
|
||||
<ChatItemsList
|
||||
title={t("new_messages")}
|
||||
viewAllProps={{
|
||||
count: chats.length,
|
||||
count: unreadChats.length,
|
||||
isAll: isVisibleAll,
|
||||
onClick: () => {
|
||||
setIsVisibleAll(prev => !prev);
|
||||
setChatsNewMessages({ isVisibleAll: !isVisibleAll });
|
||||
},
|
||||
}}
|
||||
isVisibleViewAll={unreadChats.length > 1}
|
||||
>
|
||||
<NewMessages chats={chats} isVisibleAll={isVisibleAll} />
|
||||
<NewMessages chats={unreadChats} isVisibleAll={isVisibleAll} />
|
||||
</ChatItemsList>
|
||||
)}
|
||||
</>
|
||||
|
||||
@ -7,19 +7,18 @@ import { Button, Typography } from "@/components/ui";
|
||||
import styles from "./ViewAll.module.scss";
|
||||
|
||||
export interface ViewAllProps {
|
||||
isAll: boolean;
|
||||
count: number;
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
export default function ViewAll({ count, onClick }: ViewAllProps) {
|
||||
export default function ViewAll({ count, isAll, onClick }: ViewAllProps) {
|
||||
const t = useTranslations("Chat");
|
||||
|
||||
return (
|
||||
<Button className={styles.viewAllButton} onClick={onClick}>
|
||||
<Typography size="sm" weight="medium" color="muted">
|
||||
{t("view_all", {
|
||||
count,
|
||||
})}
|
||||
{isAll ? t("hide_all") : t("view_all", { count })}
|
||||
</Typography>
|
||||
</Button>
|
||||
);
|
||||
|
||||
@ -1,21 +1,35 @@
|
||||
"use client";
|
||||
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
import { Button, Card, Stars, Typography } from "@/components/ui";
|
||||
import { IChat } from "@/entities/chats/types";
|
||||
import { Assistant } from "@/entities/dashboard/types";
|
||||
import { useChatStore } from "@/providers/chat-store-provider";
|
||||
import { ROUTES } from "@/shared/constants/client-routes";
|
||||
|
||||
import styles from "./AdviserCard.module.scss";
|
||||
|
||||
type AdviserCardProps = Assistant;
|
||||
type AdviserCardProps = {
|
||||
assistant: Assistant;
|
||||
chat: IChat | null;
|
||||
};
|
||||
|
||||
export default function AdviserCard({ assistant, chat }: AdviserCardProps) {
|
||||
const router = useRouter();
|
||||
const { _id, name, photoUrl, rating, reviewCount, description } = assistant;
|
||||
const setCurrentChat = useChatStore(state => state.setCurrentChat);
|
||||
|
||||
export default function AdviserCard({
|
||||
name,
|
||||
photoUrl,
|
||||
rating,
|
||||
reviewCount,
|
||||
description,
|
||||
}: AdviserCardProps) {
|
||||
return (
|
||||
<Card
|
||||
className={styles.card}
|
||||
style={{ backgroundImage: `url(${photoUrl})` }}
|
||||
onClick={() => {
|
||||
if (chat) {
|
||||
setCurrentChat(chat);
|
||||
}
|
||||
router.push(ROUTES.chat(_id));
|
||||
}}
|
||||
>
|
||||
<div className={styles.content}>
|
||||
<div className={styles.info}>
|
||||
|
||||
@ -2,6 +2,7 @@ import { use } from "react";
|
||||
import clsx from "clsx";
|
||||
|
||||
import { Grid, Section, Skeleton } from "@/components/ui";
|
||||
import { IChat, IGetChatsListResponse } from "@/entities/chats/types";
|
||||
import { Assistant } from "@/entities/dashboard/types";
|
||||
|
||||
import { AdviserCard } from "../../cards";
|
||||
@ -9,15 +10,22 @@ import { AdviserCard } from "../../cards";
|
||||
import styles from "./AdvisersSection.module.scss";
|
||||
|
||||
interface AdvisersSectionProps {
|
||||
promise: Promise<Assistant[]>;
|
||||
promiseAssistants: Promise<Assistant[]>;
|
||||
promiseChats: Promise<IGetChatsListResponse>;
|
||||
gridDisplayMode?: "vertical" | "horizontal";
|
||||
}
|
||||
|
||||
const getChatByAssistantId = (assistantId: string, chats: IChat[]) => {
|
||||
return chats.find(chat => chat.assistantId === assistantId) || null;
|
||||
};
|
||||
|
||||
export default function AdvisersSection({
|
||||
promise,
|
||||
promiseAssistants,
|
||||
promiseChats,
|
||||
gridDisplayMode = "horizontal",
|
||||
}: AdvisersSectionProps) {
|
||||
const assistants = use(promise);
|
||||
const assistants = use(promiseAssistants);
|
||||
const chats = use(promiseChats);
|
||||
const columns = Math.ceil(assistants?.length / 2);
|
||||
|
||||
return (
|
||||
@ -27,7 +35,14 @@ export default function AdvisersSection({
|
||||
className={clsx(styles.grid, styles[gridDisplayMode])}
|
||||
>
|
||||
{assistants.map(adviser => (
|
||||
<AdviserCard key={adviser._id} {...adviser} />
|
||||
<AdviserCard
|
||||
key={adviser._id}
|
||||
assistant={adviser}
|
||||
chat={getChatByAssistantId(
|
||||
adviser._id,
|
||||
chats.categorizedChats[adviser.category]
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
</Grid>
|
||||
</Section>
|
||||
|
||||
@ -0,0 +1,6 @@
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
gap: 16px;
|
||||
}
|
||||
@ -0,0 +1,51 @@
|
||||
"use client";
|
||||
|
||||
import { use } from "react";
|
||||
|
||||
import { NewMessages, ViewAll } from "@/components/domains/chat";
|
||||
import { Skeleton } from "@/components/ui";
|
||||
import { IGetChatsListResponse } from "@/entities/chats/types";
|
||||
import { useChatsSocket } from "@/hooks/chats/useChatsSocket";
|
||||
import { useAppUiStore } from "@/providers/app-ui-store-provider";
|
||||
|
||||
import styles from "./NewMessagesSection.module.scss";
|
||||
|
||||
interface NewMessagesSectionProps {
|
||||
chatsPromise: Promise<IGetChatsListResponse>;
|
||||
}
|
||||
|
||||
export default function NewMessagesSection({
|
||||
chatsPromise,
|
||||
}: NewMessagesSectionProps) {
|
||||
const chats = use(chatsPromise);
|
||||
const { unreadChats } = useChatsSocket({ initialChats: chats });
|
||||
|
||||
const { isVisibleAll } = useAppUiStore(state => state.home.newMessages);
|
||||
const hasHydrated = useAppUiStore(state => state._hasHydrated);
|
||||
const setHomeNewMessages = useAppUiStore(state => state.setHomeNewMessages);
|
||||
|
||||
if (!hasHydrated) return <NewMessagesSectionSkeleton />;
|
||||
|
||||
return (
|
||||
<>
|
||||
{!!unreadChats.length && (
|
||||
<div className={styles.container}>
|
||||
{unreadChats.length > 1 && (
|
||||
<ViewAll
|
||||
count={unreadChats.length}
|
||||
isAll={isVisibleAll}
|
||||
onClick={() => {
|
||||
setHomeNewMessages({ isVisibleAll: !isVisibleAll });
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<NewMessages chats={unreadChats} isVisibleAll={isVisibleAll} />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export const NewMessagesSectionSkeleton = () => {
|
||||
return <Skeleton style={{ height: 100, width: "100%" }} />;
|
||||
};
|
||||
@ -10,6 +10,10 @@ export {
|
||||
default as MeditationSection,
|
||||
MeditationSectionSkeleton,
|
||||
} from "./MeditationSection/MeditationSection";
|
||||
export {
|
||||
default as NewMessagesSection,
|
||||
NewMessagesSectionSkeleton,
|
||||
} from "./NewMessagesSection/NewMessagesSection";
|
||||
export {
|
||||
default as PalmSection,
|
||||
PalmSectionSkeleton,
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
.container {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: start;
|
||||
align-items: flex-start;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
|
||||
@ -1,9 +1,12 @@
|
||||
"use client";
|
||||
|
||||
import { use } from "react";
|
||||
import Link from "next/link";
|
||||
import clsx from "clsx";
|
||||
|
||||
import { Button, Icon, IconName } from "@/components/ui";
|
||||
import { IGetChatsListResponse } from "@/entities/chats/types";
|
||||
import { useChatsSocket } from "@/hooks/chats/useChatsSocket";
|
||||
import { ROUTES } from "@/shared/constants/client-routes";
|
||||
|
||||
import { useDrawer } from "..";
|
||||
@ -13,10 +16,13 @@ import styles from "./Header.module.scss";
|
||||
|
||||
interface HeaderProps {
|
||||
className?: string;
|
||||
chatsPromise: Promise<IGetChatsListResponse>;
|
||||
}
|
||||
|
||||
export default function Header({ className }: HeaderProps) {
|
||||
export default function Header({ className, chatsPromise }: HeaderProps) {
|
||||
const { open } = useDrawer();
|
||||
const chats = use(chatsPromise);
|
||||
const { totalUnreadCount } = useChatsSocket({ initialChats: chats });
|
||||
|
||||
return (
|
||||
<header className={clsx(styles.header, className)}>
|
||||
@ -27,7 +33,9 @@ export default function Header({ className }: HeaderProps) {
|
||||
<Logo />
|
||||
</Link>
|
||||
<div>
|
||||
<Icon name={IconName.Notification} />
|
||||
<Link href={ROUTES.chat()}>
|
||||
<Icon name={IconName.Notification} iconChildren={totalUnreadCount} />
|
||||
</Link>
|
||||
<Icon name={IconName.Search} />
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@ -1,21 +1,35 @@
|
||||
"use client";
|
||||
|
||||
import { use } from "react";
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { useLocale } from "next-intl";
|
||||
import clsx from "clsx";
|
||||
|
||||
import { Badge, Icon, Typography } from "@/components/ui";
|
||||
import { IGetChatsListResponse } from "@/entities/chats/types";
|
||||
import { useChatsSocket } from "@/hooks/chats/useChatsSocket";
|
||||
import { ROUTES } from "@/shared/constants/client-routes";
|
||||
import { navItems } from "@/shared/constants/navigation";
|
||||
import { NavItem, navItems } from "@/shared/constants/navigation";
|
||||
import { stripLocale } from "@/shared/utils/path";
|
||||
|
||||
import styles from "./NavigationBar.module.scss";
|
||||
|
||||
export default function NavigationBar() {
|
||||
const getBadge = (item: NavItem, totalUnreadCount: number) => {
|
||||
if (item.badgeId === "unreadCount") return totalUnreadCount;
|
||||
return null;
|
||||
};
|
||||
|
||||
interface NavigationBarProps {
|
||||
chatsPromise: Promise<IGetChatsListResponse>;
|
||||
}
|
||||
|
||||
export default function NavigationBar({ chatsPromise }: NavigationBarProps) {
|
||||
const pathname = usePathname();
|
||||
const locale = useLocale();
|
||||
const pathnameWithoutLocale = stripLocale(pathname, locale);
|
||||
const chats = use(chatsPromise);
|
||||
const { totalUnreadCount } = useChatsSocket({ initialChats: chats });
|
||||
|
||||
return (
|
||||
<nav className={styles.container}>
|
||||
@ -25,6 +39,8 @@ export default function NavigationBar() {
|
||||
? pathnameWithoutLocale === item.href
|
||||
: pathnameWithoutLocale.startsWith(item.href);
|
||||
|
||||
const badge = getBadge(item, totalUnreadCount);
|
||||
|
||||
return (
|
||||
<Link
|
||||
key={item.key}
|
||||
@ -32,10 +48,10 @@ export default function NavigationBar() {
|
||||
className={clsx(styles.item, { [styles.active]: isActive })}
|
||||
>
|
||||
<Icon name={item.icon} color={isActive ? "#007AFF" : "#8A8D93"}>
|
||||
{item.badge && (
|
||||
{!!badge && (
|
||||
<Badge className={styles.badge}>
|
||||
<Typography weight="medium" size="xs" color="white">
|
||||
{item.badge}
|
||||
{badge}
|
||||
</Typography>
|
||||
</Badge>
|
||||
)}
|
||||
|
||||
@ -94,6 +94,7 @@ export type IconProps = {
|
||||
cursor?: "pointer" | "auto";
|
||||
iconStyle?: CSSProperties;
|
||||
style?: CSSProperties;
|
||||
iconChildren?: ReactNode;
|
||||
};
|
||||
|
||||
export default function Icon({
|
||||
@ -107,6 +108,7 @@ export default function Icon({
|
||||
children,
|
||||
cursor = "pointer",
|
||||
style,
|
||||
iconChildren,
|
||||
...rest
|
||||
}: IconProps) {
|
||||
const Component = icons[name];
|
||||
@ -132,7 +134,9 @@ export default function Icon({
|
||||
display: "block",
|
||||
...rest.iconStyle,
|
||||
}}
|
||||
/>
|
||||
>
|
||||
{iconChildren}
|
||||
</Component>
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
|
||||
@ -26,10 +26,9 @@ export default function NotificationIcon(props: SVGProps<SVGSVGElement>) {
|
||||
d="M14.75 0C19.1683 0 22.75 3.58172 22.75 8C22.75 12.4183 19.1683 16 14.75 16C10.3317 16 6.75 12.4183 6.75 8C6.75 3.58172 10.3317 0 14.75 0Z"
|
||||
stroke="#E5E7EB"
|
||||
/>
|
||||
<path
|
||||
d="M14.7766 12.0994C14.3079 12.0994 13.89 12.0189 13.5231 11.858C13.1585 11.697 12.8685 11.4732 12.6531 11.1868C12.44 10.898 12.324 10.563 12.305 10.1818H13.1999C13.2189 10.4162 13.2994 10.6186 13.4414 10.7891C13.5835 10.9571 13.7693 11.0874 13.9989 11.1797C14.2286 11.272 14.4831 11.3182 14.7624 11.3182C15.0749 11.3182 15.3519 11.2637 15.5934 11.1548C15.8349 11.0459 16.0243 10.8944 16.1616 10.7003C16.2989 10.5062 16.3675 10.2812 16.3675 10.0256C16.3675 9.75805 16.3013 9.52249 16.1687 9.31889C16.0361 9.11293 15.842 8.95194 15.5863 8.83594C15.3306 8.71993 15.0181 8.66193 14.6488 8.66193H14.0664V7.88068H14.6488C14.9376 7.88068 15.1909 7.8286 15.4087 7.72443C15.6289 7.62026 15.8005 7.47348 15.9237 7.28409C16.0491 7.0947 16.1119 6.87216 16.1119 6.61648C16.1119 6.37026 16.0574 6.15601 15.9485 5.97372C15.8396 5.79143 15.6857 5.64938 15.4869 5.54759C15.2904 5.44579 15.0584 5.39489 14.7908 5.39489C14.5399 5.39489 14.3031 5.44105 14.0806 5.53338C13.8604 5.62334 13.6805 5.75473 13.5408 5.92756C13.4012 6.09801 13.3254 6.30398 13.3136 6.54545H12.4613C12.4755 6.1643 12.5903 5.83049 12.8058 5.54403C13.0212 5.25521 13.3029 5.0303 13.6509 4.86932C14.0013 4.70833 14.386 4.62784 14.805 4.62784C15.2549 4.62784 15.6407 4.71899 15.9627 4.90128C16.2847 5.0812 16.5321 5.31913 16.7049 5.61506C16.8777 5.91098 16.9641 6.23059 16.9641 6.57386C16.9641 6.98343 16.8564 7.33262 16.641 7.62145C16.4279 7.91027 16.1379 8.11032 15.771 8.22159V8.27841C16.2302 8.35417 16.5889 8.54948 16.8469 8.86435C17.105 9.17685 17.234 9.56392 17.234 10.0256C17.234 10.4209 17.1263 10.776 16.9109 11.0909C16.6978 11.4034 16.4066 11.6496 16.0373 11.8295C15.668 12.0095 15.2478 12.0994 14.7766 12.0994Z"
|
||||
fill="white"
|
||||
/>
|
||||
<text x="12" y="12" fill="white" fontSize="10">
|
||||
{props.children}
|
||||
</text>
|
||||
<defs>
|
||||
<clipPath id="clip0_20_1490">
|
||||
<rect
|
||||
|
||||
@ -1,9 +1,16 @@
|
||||
.onlineIndicator {
|
||||
aspect-ratio: 1/1;
|
||||
border-radius: 50%;
|
||||
background-color: #10b981;
|
||||
border: 2px solid #fff;
|
||||
|
||||
&.online {
|
||||
background-color: #10b981;
|
||||
}
|
||||
|
||||
&.offline {
|
||||
background-color: #9ca3af;
|
||||
}
|
||||
|
||||
&.sm {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
@ -15,10 +15,12 @@ export default function OnlineIndicator({
|
||||
}: OnlineIndicatorProps) {
|
||||
return (
|
||||
<div
|
||||
className={clsx(styles.onlineIndicator, styles[size], className)}
|
||||
style={{
|
||||
backgroundColor: isOnline ? "#10b981" : "#9CA3AF",
|
||||
}}
|
||||
className={clsx(
|
||||
styles.onlineIndicator,
|
||||
styles[size],
|
||||
isOnline ? styles.online : styles.offline,
|
||||
className
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@ -8,11 +8,12 @@
|
||||
background: #f3f4f6;
|
||||
font-size: 14px;
|
||||
overflow-y: auto;
|
||||
border: 1px solid transparent;
|
||||
|
||||
&:active,
|
||||
&:focus,
|
||||
&:focus-visible {
|
||||
outline: 1px solid #191f29;
|
||||
border: 1px solid #191f29;
|
||||
}
|
||||
|
||||
&::placeholder {
|
||||
|
||||
@ -4,6 +4,7 @@
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
min-height: 94px;
|
||||
width: 100%;
|
||||
|
||||
.content {
|
||||
display: grid;
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
"use server";
|
||||
|
||||
import { revalidateTag } from "next/cache";
|
||||
|
||||
import { http } from "@/shared/api/httpClient";
|
||||
import { API_ROUTES } from "@/shared/constants/api-routes";
|
||||
import { ActionResponse } from "@/types";
|
||||
@ -57,3 +59,7 @@ export async function fetchChatMessages(
|
||||
return { data: null, error: errorMessage };
|
||||
}
|
||||
}
|
||||
|
||||
export async function revalidateChatsPage() {
|
||||
revalidateTag("chats-list");
|
||||
}
|
||||
|
||||
@ -40,7 +40,7 @@ export const createChat = async (
|
||||
|
||||
export const getChatsList = async (): Promise<IGetChatsListResponse> => {
|
||||
return http.get<IGetChatsListResponse>(API_ROUTES.getChatsList(), {
|
||||
tags: ["chats", "list"],
|
||||
tags: ["chats-list"],
|
||||
schema: GetChatsListResponseSchema,
|
||||
revalidate: 0,
|
||||
});
|
||||
|
||||
@ -1,18 +1,12 @@
|
||||
import { cache } from "react";
|
||||
|
||||
import { createAllChats, getChatMessages, getChatsList } from "./api";
|
||||
|
||||
export const loadCreateAllChats = cache(createAllChats);
|
||||
export const loadCreateAllChats = createAllChats;
|
||||
|
||||
export const loadChatsList = cache(getChatsList);
|
||||
export const loadCategorizedChats = cache(() =>
|
||||
loadChatsList().then(d => d.categorizedChats)
|
||||
);
|
||||
export const loadUnreadChats = cache(() =>
|
||||
loadChatsList().then(d => d.unreadChats)
|
||||
);
|
||||
export const loadCorrespondenceStarted = cache(() =>
|
||||
loadChatsList().then(d => d.startedChats)
|
||||
);
|
||||
export const loadChatsList = getChatsList;
|
||||
export const loadCategorizedChats = () =>
|
||||
loadChatsList().then(d => d.categorizedChats);
|
||||
export const loadUnreadChats = () => loadChatsList().then(d => d.unreadChats);
|
||||
export const loadCorrespondenceStarted = () =>
|
||||
loadChatsList().then(d => d.startedChats);
|
||||
|
||||
export const loadChatMessages = cache(getChatMessages);
|
||||
export const loadChatMessages = getChatMessages;
|
||||
|
||||
@ -63,9 +63,7 @@ export const useChatSocket = (
|
||||
const [refillModals, setRefillModals] = useState<IRefillModals | null>(null);
|
||||
|
||||
const isLoadingAdvisorMessage = useMemo(() => {
|
||||
return (
|
||||
messages.length > 0 && messages[messages.length - 1].role !== "assistant"
|
||||
);
|
||||
return messages.length > 0 && messages[0].role !== "assistant";
|
||||
}, [messages]);
|
||||
|
||||
const joinChat = useCallback(
|
||||
@ -86,7 +84,7 @@ export const useChatSocket = (
|
||||
createdDate: new Date().toISOString(),
|
||||
isRead: false,
|
||||
};
|
||||
setMessages(prev => [...prev, sendingMessage]);
|
||||
setMessages(prev => [sendingMessage, ...prev]);
|
||||
if (options.onNewMessage) {
|
||||
options.onNewMessage(sendingMessage);
|
||||
}
|
||||
@ -145,8 +143,8 @@ export const useChatSocket = (
|
||||
setMessages(prev => {
|
||||
const ids = new Set(prev.map(m => m.id));
|
||||
return [
|
||||
...msgs.map(mapApiMessage).filter(m => !ids.has(m.id)),
|
||||
...prev,
|
||||
...msgs.map(mapApiMessage).filter(m => !ids.has(m.id)),
|
||||
];
|
||||
});
|
||||
setPage(nextPage);
|
||||
@ -182,7 +180,7 @@ export const useChatSocket = (
|
||||
);
|
||||
return Array.from(map.values()).sort(
|
||||
(a, b) =>
|
||||
new Date(a.createdDate).getTime() - new Date(b.createdDate).getTime()
|
||||
new Date(b.createdDate).getTime() - new Date(a.createdDate).getTime()
|
||||
);
|
||||
});
|
||||
|
||||
@ -196,7 +194,7 @@ export const useChatSocket = (
|
||||
});
|
||||
useSocketEvent("session_started", s => setSession(s.data));
|
||||
useSocketEvent("session_ended", () => setSession(null));
|
||||
useSocketEvent("show_refill_modals", r => setRefillModals(r));
|
||||
useSocketEvent("show_refill_modals", r => setRefillModals(r.data));
|
||||
|
||||
useEffect(() => {
|
||||
if (!session?.maxFinishedAt) return;
|
||||
|
||||
41
src/hooks/chats/useChatsSocket.ts
Normal file
41
src/hooks/chats/useChatsSocket.ts
Normal file
@ -0,0 +1,41 @@
|
||||
"use client";
|
||||
|
||||
import { useMemo, useState } from "react";
|
||||
|
||||
import { IGetChatsListResponse } from "@/entities/chats/types";
|
||||
|
||||
import { useSocketEvent } from "../socket/useSocketEvent";
|
||||
|
||||
interface UseChatsSocketOptions {
|
||||
initialChats?: IGetChatsListResponse;
|
||||
}
|
||||
|
||||
export const useChatsSocket = (options: UseChatsSocketOptions = {}) => {
|
||||
const initialChats = options.initialChats ?? {
|
||||
categorizedChats: {},
|
||||
startedChats: [],
|
||||
unreadChats: [],
|
||||
totalUnreadCount: 0,
|
||||
};
|
||||
|
||||
const [chats, setChats] = useState<IGetChatsListResponse>(initialChats);
|
||||
const [unreadCount, setUnreadCount] = useState<number>(
|
||||
initialChats.totalUnreadCount
|
||||
);
|
||||
|
||||
useSocketEvent("chats_updated", chats => setChats(chats));
|
||||
useSocketEvent("unread_messages_count", count =>
|
||||
setUnreadCount(count.unreadCount)
|
||||
);
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
chats,
|
||||
unreadChats: chats.unreadChats,
|
||||
startedChats: chats.startedChats,
|
||||
categorizedChats: chats.categorizedChats,
|
||||
totalUnreadCount: unreadCount,
|
||||
}),
|
||||
[chats, unreadCount]
|
||||
);
|
||||
};
|
||||
39
src/providers/app-ui-store-provider.tsx
Normal file
39
src/providers/app-ui-store-provider.tsx
Normal file
@ -0,0 +1,39 @@
|
||||
"use client";
|
||||
|
||||
import { createContext, type ReactNode, useContext, useRef } from "react";
|
||||
import { useStore } from "zustand";
|
||||
|
||||
import { AppUiStore, createAppUiStore } from "@/stores/app-ui-store";
|
||||
|
||||
export type AppUiStoreApi = ReturnType<typeof createAppUiStore>;
|
||||
|
||||
export const AppUiStoreContext = createContext<AppUiStoreApi | undefined>(
|
||||
undefined
|
||||
);
|
||||
|
||||
export interface AppUiStoreProviderProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export const AppUiStoreProvider = ({ children }: AppUiStoreProviderProps) => {
|
||||
const storeRef = useRef<AppUiStoreApi | null>(null);
|
||||
if (storeRef.current === null) {
|
||||
storeRef.current = createAppUiStore();
|
||||
}
|
||||
|
||||
return (
|
||||
<AppUiStoreContext.Provider value={storeRef.current}>
|
||||
{children}
|
||||
</AppUiStoreContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useAppUiStore = <T,>(selector: (store: AppUiStore) => T): T => {
|
||||
const appUiStoreContext = useContext(AppUiStoreContext);
|
||||
|
||||
if (!appUiStoreContext) {
|
||||
throw new Error(`useAppUiStore must be used within AppUiStoreProvider`);
|
||||
}
|
||||
|
||||
return useStore(appUiStoreContext, selector);
|
||||
};
|
||||
@ -13,7 +13,7 @@ import { useChatSocket } from "@/hooks/chats/useChatSocket";
|
||||
|
||||
interface ChatContextValue extends ReturnType<typeof useChatSocket> {
|
||||
messagesWrapperRef: React.RefObject<HTMLDivElement | null>;
|
||||
scrollToBottom: () => void;
|
||||
scrollToBottom: (behavior?: ScrollBehavior) => void;
|
||||
}
|
||||
|
||||
const ChatContext = createContext<ChatContextValue | null>(null);
|
||||
@ -47,11 +47,11 @@ export function ChatProvider({
|
||||
|
||||
const messagesWrapperRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const scrollToBottom = useCallback(() => {
|
||||
const scrollToBottom = useCallback((behavior: ScrollBehavior = "smooth") => {
|
||||
if (messagesWrapperRef.current) {
|
||||
messagesWrapperRef.current.scrollTo({
|
||||
top: messagesWrapperRef.current.scrollHeight,
|
||||
behavior: "smooth",
|
||||
behavior,
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { IGetChatsListResponse } from "@/entities/chats/types";
|
||||
import { Currency } from "@/types";
|
||||
|
||||
export interface IMessage {
|
||||
@ -55,6 +56,10 @@ export interface IRefillModals {
|
||||
products?: IRefillModalsProduct[];
|
||||
}
|
||||
|
||||
export interface IUnreadMessagesCount {
|
||||
unreadCount: number;
|
||||
}
|
||||
|
||||
export interface ClientToServerEvents {
|
||||
join_chat: (data: { chatId: string }) => void;
|
||||
leave_chat: (data: { chatId: string }) => void;
|
||||
@ -85,5 +90,9 @@ export interface ServerToClientEvents {
|
||||
data: ServerToClientEventsBaseData<ISessionStarted>
|
||||
) => void;
|
||||
session_ended: (data: ServerToClientEventsBaseData<boolean>) => void;
|
||||
show_refill_modals: (data: IRefillModals) => void;
|
||||
show_refill_modals: (
|
||||
data: ServerToClientEventsBaseData<IRefillModals>
|
||||
) => void;
|
||||
chats_updated: (data: IGetChatsListResponse) => void;
|
||||
unread_messages_count: (data: IUnreadMessagesCount) => void;
|
||||
}
|
||||
|
||||
@ -2,12 +2,12 @@ import { IconName } from "@/components/ui";
|
||||
|
||||
import { ROUTES } from "./client-routes";
|
||||
|
||||
interface NavItem {
|
||||
export interface NavItem {
|
||||
key: string;
|
||||
label: string;
|
||||
icon: IconName;
|
||||
href: string;
|
||||
badge?: number;
|
||||
badgeId?: string;
|
||||
}
|
||||
|
||||
export const navItems: NavItem[] = [
|
||||
@ -17,13 +17,13 @@ export const navItems: NavItem[] = [
|
||||
icon: IconName.Home,
|
||||
href: ROUTES.home(),
|
||||
},
|
||||
// {
|
||||
// key: "chat",
|
||||
// label: "Chat",
|
||||
// icon: IconName.Chat,
|
||||
// href: ROUTES.chat(),
|
||||
// badge: 12,
|
||||
// },
|
||||
{
|
||||
key: "chat",
|
||||
label: "Chat",
|
||||
icon: IconName.Chat,
|
||||
href: ROUTES.chat(),
|
||||
badgeId: "unreadCount",
|
||||
},
|
||||
{
|
||||
key: "advisers",
|
||||
label: "Advi...",
|
||||
|
||||
102
src/stores/app-ui-store.ts
Normal file
102
src/stores/app-ui-store.ts
Normal file
@ -0,0 +1,102 @@
|
||||
"use client";
|
||||
|
||||
import { createStore } from "zustand";
|
||||
import { persist } from "zustand/middleware";
|
||||
|
||||
interface AppUiState {
|
||||
chats: {
|
||||
correspondenceStarted: {
|
||||
isVisibleAll: boolean;
|
||||
};
|
||||
newMessages: {
|
||||
isVisibleAll: boolean;
|
||||
};
|
||||
};
|
||||
home: {
|
||||
newMessages: {
|
||||
isVisibleAll: boolean;
|
||||
};
|
||||
};
|
||||
_hasHydrated: boolean;
|
||||
}
|
||||
|
||||
export type AppUiActions = {
|
||||
setChats: (chats: AppUiState["chats"]) => void;
|
||||
setChatsCorrespondenceStarted: (
|
||||
correspondenceStarted: AppUiState["chats"]["correspondenceStarted"]
|
||||
) => void;
|
||||
setChatsNewMessages: (
|
||||
newMessages: AppUiState["chats"]["newMessages"]
|
||||
) => void;
|
||||
setHomeNewMessages: (newMessages: AppUiState["home"]["newMessages"]) => void;
|
||||
clearAppUiData: () => void;
|
||||
|
||||
setHasHydrated: (hasHydrated: boolean) => void;
|
||||
};
|
||||
|
||||
export type AppUiStore = AppUiState & AppUiActions;
|
||||
|
||||
const initialState: AppUiState = {
|
||||
chats: {
|
||||
correspondenceStarted: {
|
||||
isVisibleAll: true,
|
||||
},
|
||||
newMessages: {
|
||||
isVisibleAll: false,
|
||||
},
|
||||
},
|
||||
home: {
|
||||
newMessages: {
|
||||
isVisibleAll: false,
|
||||
},
|
||||
},
|
||||
_hasHydrated: false,
|
||||
};
|
||||
|
||||
export const createAppUiStore = (initState: AppUiState = initialState) => {
|
||||
return createStore<AppUiStore>()(
|
||||
persist(
|
||||
(set, get) => ({
|
||||
...initState,
|
||||
setChats: (chats: AppUiState["chats"]) => set({ chats }),
|
||||
|
||||
setChatsCorrespondenceStarted: (
|
||||
correspondenceStarted: AppUiState["chats"]["correspondenceStarted"]
|
||||
) =>
|
||||
set({
|
||||
chats: {
|
||||
...get().chats,
|
||||
correspondenceStarted,
|
||||
},
|
||||
}),
|
||||
|
||||
setChatsNewMessages: (
|
||||
newMessages: AppUiState["chats"]["newMessages"]
|
||||
) =>
|
||||
set({
|
||||
chats: {
|
||||
...get().chats,
|
||||
newMessages,
|
||||
},
|
||||
}),
|
||||
|
||||
setHomeNewMessages: (newMessages: AppUiState["home"]["newMessages"]) =>
|
||||
set({ home: { ...get().home, newMessages } }),
|
||||
|
||||
clearAppUiData: () => set(initialState),
|
||||
|
||||
setHasHydrated: (hasHydrated: boolean) =>
|
||||
set({ _hasHydrated: hasHydrated }),
|
||||
}),
|
||||
{
|
||||
name: "app-ui-storage",
|
||||
onRehydrateStorage: () => state => {
|
||||
// Вызывается после загрузки данных из localStorage
|
||||
if (state) {
|
||||
state.setHasHydrated(true);
|
||||
}
|
||||
},
|
||||
}
|
||||
)
|
||||
);
|
||||
};
|
||||
Loading…
Reference in New Issue
Block a user