commit
9f21c76d1d
@ -5,7 +5,8 @@
|
|||||||
background-color: #000;
|
background-color: #000;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
overflow-y: auto;
|
||||||
left: 0;
|
left: 0;
|
||||||
top: 56px;
|
top: 56px;
|
||||||
padding: 16px 16px 64px;
|
padding: 16px 16px 220px;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -20,9 +20,12 @@
|
|||||||
gap: 4px;
|
gap: 4px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
& > .badge {
|
& > .badge.badge {
|
||||||
background-color: #fbbf24;
|
background-color: #fbbf24;
|
||||||
width: 24px;
|
min-width: 24px;
|
||||||
|
min-height: 24px;
|
||||||
|
max-width: 28px;
|
||||||
|
max-height: 28px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { use, useEffect, useState } from "react";
|
import { use, useEffect, useState } from "react";
|
||||||
import Image from "next/image";
|
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
@ -11,6 +10,7 @@ import {
|
|||||||
IconName,
|
IconName,
|
||||||
OnlineIndicator,
|
OnlineIndicator,
|
||||||
Typography,
|
Typography,
|
||||||
|
UserAvatar,
|
||||||
} from "@/components/ui";
|
} from "@/components/ui";
|
||||||
import { revalidateChatsPage } from "@/entities/chats/actions";
|
import { revalidateChatsPage } from "@/entities/chats/actions";
|
||||||
import { IGetChatsListResponse } from "@/entities/chats/types";
|
import { IGetChatsListResponse } from "@/entities/chats/types";
|
||||||
@ -60,19 +60,16 @@ export default function ChatHeader({ chatsPromise }: ChatHeaderProps) {
|
|||||||
{!!totalUnreadCount && (
|
{!!totalUnreadCount && (
|
||||||
<Badge className={styles.badge}>
|
<Badge className={styles.badge}>
|
||||||
<Typography weight="semiBold" size="xs" color="black">
|
<Typography weight="semiBold" size="xs" color="black">
|
||||||
{totalUnreadCount}
|
{totalUnreadCount > 99 ? "99+" : totalUnreadCount}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.chatInfo}>
|
<div className={styles.chatInfo}>
|
||||||
{!!currentChat?.assistantAvatar ? (
|
{!!currentChat?.assistantAvatar ? (
|
||||||
<Image
|
<UserAvatar
|
||||||
src={currentChat.assistantAvatar}
|
src={currentChat.assistantAvatar}
|
||||||
alt="Aaron (Taro) avatar"
|
alt={`${currentChat.assistantName} avatar`}
|
||||||
width={48}
|
|
||||||
height={48}
|
|
||||||
className={styles.avatar}
|
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className={styles.avatar} />
|
<div className={styles.avatar} />
|
||||||
|
|||||||
@ -19,6 +19,11 @@ const getChatByAssistantId = (assistantId: string, chats: IChat[]) => {
|
|||||||
return chats.find(chat => chat.assistantId === assistantId) || null;
|
return chats.find(chat => chat.assistantId === assistantId) || null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getOptimalColumns = (count: number) => {
|
||||||
|
if (count <= 6) return count;
|
||||||
|
return Math.ceil(count / 2);
|
||||||
|
};
|
||||||
|
|
||||||
export default function AdvisersSection({
|
export default function AdvisersSection({
|
||||||
promiseAssistants,
|
promiseAssistants,
|
||||||
promiseChats,
|
promiseChats,
|
||||||
@ -26,7 +31,7 @@ export default function AdvisersSection({
|
|||||||
}: AdvisersSectionProps) {
|
}: AdvisersSectionProps) {
|
||||||
const assistants = use(promiseAssistants);
|
const assistants = use(promiseAssistants);
|
||||||
const chats = use(promiseChats);
|
const chats = use(promiseChats);
|
||||||
const columns = Math.ceil(assistants?.length / 2);
|
const columns = getOptimalColumns(assistants?.length || 0);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Section title="Advisers" contentClassName={styles.sectionContent}>
|
<Section title="Advisers" contentClassName={styles.sectionContent}>
|
||||||
|
|||||||
@ -32,7 +32,10 @@ function Billing() {
|
|||||||
<div className={styles.credits}>
|
<div className={styles.credits}>
|
||||||
<Typography as="p" weight="bold" color="white" align="left">
|
<Typography as="p" weight="bold" color="white" align="left">
|
||||||
{t("credits.title", {
|
{t("credits.title", {
|
||||||
credits: isLoading || roundedBalance === null ? "..." : String(roundedBalance),
|
credits:
|
||||||
|
isLoading || roundedBalance === null
|
||||||
|
? "..."
|
||||||
|
: String(roundedBalance),
|
||||||
})}
|
})}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography
|
<Typography
|
||||||
|
|||||||
@ -27,3 +27,23 @@
|
|||||||
width: fit-content;
|
width: fit-content;
|
||||||
background: none;
|
background: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.notificationIcon {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
& > .badge.badge {
|
||||||
|
min-width: 16px;
|
||||||
|
min-height: 16px;
|
||||||
|
max-width: 18px;
|
||||||
|
max-height: 18px;
|
||||||
|
border: 1px solid #fff;
|
||||||
|
position: absolute;
|
||||||
|
top: -1px;
|
||||||
|
right: -1px;
|
||||||
|
|
||||||
|
& > .badgeContent {
|
||||||
|
display: block;
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import { use } from "react";
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
|
|
||||||
import { Button, Icon, IconName } from "@/components/ui";
|
import { Badge, Button, Icon, IconName, Typography } from "@/components/ui";
|
||||||
import { IGetChatsListResponse } from "@/entities/chats/types";
|
import { IGetChatsListResponse } from "@/entities/chats/types";
|
||||||
import { useChatsSocket } from "@/hooks/chats/useChatsSocket";
|
import { useChatsSocket } from "@/hooks/chats/useChatsSocket";
|
||||||
import { ROUTES } from "@/shared/constants/client-routes";
|
import { ROUTES } from "@/shared/constants/client-routes";
|
||||||
@ -34,7 +34,21 @@ export default function Header({ className, chatsPromise }: HeaderProps) {
|
|||||||
</Link>
|
</Link>
|
||||||
<div>
|
<div>
|
||||||
<Link href={ROUTES.chat()}>
|
<Link href={ROUTES.chat()}>
|
||||||
<Icon name={IconName.Notification} iconChildren={totalUnreadCount} />
|
<Icon
|
||||||
|
name={IconName.Notification}
|
||||||
|
className={styles.notificationIcon}
|
||||||
|
>
|
||||||
|
<Badge className={styles.badge}>
|
||||||
|
<Typography
|
||||||
|
weight="semiBold"
|
||||||
|
size="xs"
|
||||||
|
color="white"
|
||||||
|
className={styles.badgeContent}
|
||||||
|
>
|
||||||
|
{totalUnreadCount > 99 ? "99+" : totalUnreadCount}
|
||||||
|
</Typography>
|
||||||
|
</Badge>
|
||||||
|
</Icon>
|
||||||
</Link>
|
</Link>
|
||||||
<Icon name={IconName.Search} />
|
<Icon name={IconName.Search} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -27,6 +27,15 @@
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 7px;
|
bottom: 7px;
|
||||||
left: 17px;
|
left: 17px;
|
||||||
|
|
||||||
|
min-width: 20px;
|
||||||
|
min-height: 20px;
|
||||||
|
max-width: 26px;
|
||||||
|
max-height: 26px;
|
||||||
|
|
||||||
|
& > .badgeContent {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
& > .label {
|
& > .label {
|
||||||
|
|||||||
@ -50,8 +50,13 @@ export default function NavigationBar({ chatsPromise }: NavigationBarProps) {
|
|||||||
<Icon name={item.icon} color={isActive ? "#007AFF" : "#8A8D93"}>
|
<Icon name={item.icon} color={isActive ? "#007AFF" : "#8A8D93"}>
|
||||||
{!!badge && (
|
{!!badge && (
|
||||||
<Badge className={styles.badge}>
|
<Badge className={styles.badge}>
|
||||||
<Typography weight="medium" size="xs" color="white">
|
<Typography
|
||||||
{badge}
|
weight="medium"
|
||||||
|
size="xs"
|
||||||
|
color="white"
|
||||||
|
className={styles.badgeContent}
|
||||||
|
>
|
||||||
|
{badge > 99 ? "99+" : badge}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -18,7 +18,7 @@ export default function NotificationIcon(props: SVGProps<SVGSVGElement>) {
|
|||||||
/>
|
/>
|
||||||
</g>
|
</g>
|
||||||
</g>
|
</g>
|
||||||
<path
|
{/* <path
|
||||||
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"
|
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"
|
||||||
fill="#EF4444"
|
fill="#EF4444"
|
||||||
/>
|
/>
|
||||||
@ -28,7 +28,8 @@ export default function NotificationIcon(props: SVGProps<SVGSVGElement>) {
|
|||||||
/>
|
/>
|
||||||
<text x="12" y="12" fill="white" fontSize="10">
|
<text x="12" y="12" fill="white" fontSize="10">
|
||||||
{props.children}
|
{props.children}
|
||||||
</text>
|
</text> */}
|
||||||
|
{props.children}
|
||||||
<defs>
|
<defs>
|
||||||
<clipPath id="clip0_20_1490">
|
<clipPath id="clip0_20_1490">
|
||||||
<rect
|
<rect
|
||||||
|
|||||||
@ -8,7 +8,8 @@ export interface UserAvatarProps {
|
|||||||
src: string;
|
src: string;
|
||||||
alt: string;
|
alt: string;
|
||||||
size?: "sm" | "md" | "lg";
|
size?: "sm" | "md" | "lg";
|
||||||
isOnline: boolean;
|
isOnline?: boolean;
|
||||||
|
isOnlineIndicator?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const sizes = {
|
const sizes = {
|
||||||
@ -22,6 +23,7 @@ export default function UserAvatar({
|
|||||||
alt,
|
alt,
|
||||||
size = "md",
|
size = "md",
|
||||||
isOnline,
|
isOnline,
|
||||||
|
isOnlineIndicator = true,
|
||||||
}: UserAvatarProps) {
|
}: UserAvatarProps) {
|
||||||
return (
|
return (
|
||||||
<div className={styles.avatarContainer}>
|
<div className={styles.avatarContainer}>
|
||||||
@ -31,8 +33,17 @@ export default function UserAvatar({
|
|||||||
className={styles.avatar}
|
className={styles.avatar}
|
||||||
width={sizes[size]}
|
width={sizes[size]}
|
||||||
height={sizes[size]}
|
height={sizes[size]}
|
||||||
|
style={{
|
||||||
|
width: sizes[size],
|
||||||
|
height: sizes[size],
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<OnlineIndicator isOnline={isOnline} className={styles.onlineIndicator} />
|
{isOnlineIndicator && isOnline !== undefined && (
|
||||||
|
<OnlineIndicator
|
||||||
|
isOnline={isOnline}
|
||||||
|
className={styles.onlineIndicator}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -35,9 +35,17 @@
|
|||||||
line-height: 20px;
|
line-height: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
& > .badge {
|
& > .badge.badge {
|
||||||
width: 24px;
|
min-width: 24px;
|
||||||
|
min-height: 24px;
|
||||||
|
max-width: 28px;
|
||||||
|
max-height: 28px;
|
||||||
background-color: #fbbf24;
|
background-color: #fbbf24;
|
||||||
|
|
||||||
|
& > .badgeContent {
|
||||||
|
display: block;
|
||||||
|
min-width: 16px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -19,7 +19,7 @@ export interface ChatItemProps {
|
|||||||
name: string;
|
name: string;
|
||||||
messagePreiew: LastMessagePreviewProps | null;
|
messagePreiew: LastMessagePreviewProps | null;
|
||||||
time: string | null;
|
time: string | null;
|
||||||
badgeContent: React.ReactNode;
|
badgeContent: number;
|
||||||
className?: string;
|
className?: string;
|
||||||
style?: React.CSSProperties;
|
style?: React.CSSProperties;
|
||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
@ -53,8 +53,13 @@ export default function ChatItem({
|
|||||||
</Typography>
|
</Typography>
|
||||||
{!!badgeContent && (
|
{!!badgeContent && (
|
||||||
<Badge className={styles.badge}>
|
<Badge className={styles.badge}>
|
||||||
<Typography weight="medium" size="xs" color="white">
|
<Typography
|
||||||
{badgeContent}
|
weight="medium"
|
||||||
|
size="xs"
|
||||||
|
color="white"
|
||||||
|
className={styles.badgeContent}
|
||||||
|
>
|
||||||
|
{badgeContent > 99 ? "99+" : badgeContent}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -25,9 +25,9 @@ export const getUserBalance = async (): Promise<IUserBalanceResponse> => {
|
|||||||
const response = await fetch(url.toString(), {
|
const response = await fetch(url.toString(), {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
headers: {
|
headers: {
|
||||||
"Authorization": `Bearer ${accessToken}`,
|
Authorization: `Bearer ${accessToken}`,
|
||||||
"Content-Type": "application/json"
|
"Content-Type": "application/json",
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
|
|||||||
@ -17,6 +17,7 @@ export const AssistantSchema = z.object({
|
|||||||
clientSource: z.string(),
|
clientSource: z.string(),
|
||||||
createdAt: z.string(),
|
createdAt: z.string(),
|
||||||
updatedAt: z.string(),
|
updatedAt: z.string(),
|
||||||
|
personality: z.string().optional(),
|
||||||
});
|
});
|
||||||
export type Assistant = z.infer<typeof AssistantSchema>;
|
export type Assistant = z.infer<typeof AssistantSchema>;
|
||||||
|
|
||||||
|
|||||||
@ -16,7 +16,9 @@ export const useUserBalance = () => {
|
|||||||
const response = await getUserBalance();
|
const response = await getUserBalance();
|
||||||
setBalance(response.balance);
|
setBalance(response.balance);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err instanceof Error ? err : new Error("Failed to fetch balance"));
|
setError(
|
||||||
|
err instanceof Error ? err : new Error("Failed to fetch balance")
|
||||||
|
);
|
||||||
// Используем devLogger или другой механизм логирования в продакшене
|
// Используем devLogger или другой механизм логирования в продакшене
|
||||||
if (process.env.NODE_ENV !== "production") {
|
if (process.env.NODE_ENV !== "production") {
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user