Merge pull request #29 from pennyteenycat/develop

Develop
This commit is contained in:
pennyteenycat 2025-07-27 22:15:41 +03:00 committed by GitHub
commit 0fe7f4b454
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 114 additions and 29 deletions

View File

@ -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;
}

View File

@ -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;
}
}

View File

@ -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} />

View File

@ -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}>

View File

@ -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

View File

@ -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;
}
}
}

View File

@ -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>

View File

@ -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 {

View File

@ -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>
)}

View File

@ -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

View File

@ -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>
);
}

View File

@ -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;
}
}
}
}

View File

@ -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>
)}

View File

@ -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) {

View File

@ -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>;

View File

@ -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