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