@@ -1,3 +1,5 @@ - - + + \ No newline at end of file @@ -0,0 +1,5 @@ + + + \ No newline at end of file @@ -0,0 +1 @@ +export * from './use-message-time' \ No newline at end of file @@ -0,0 +1,45 @@ +import { useEffect, useState } from 'react' + +export function useMessageTime(created_at: string | null | undefined) { + const formatVariant = { + 60: (value: number) => getOneOrMore(value / 60, 'минуту', 'минут'), + 3600: (value: number) => getOneOrMore(value / 3600, 'час', 'часов'), + 86400: (value: number) => getOneOrMore(value / 86400, 'день', 'дней'), + } + + const [formatedDate, setFormatedDate] = useState(getFormatedMessageTime(created_at)) + + useEffect(() => { + setInterval(() => { + setFormatedDate(getFormatedMessageTime(created_at)) + }, 1000 * 60) + }, []) + + function getOneOrMore(value: number, one: string, more: string) { + const formated = Math.floor(value) === 0 ? 1 : Math.floor(value) + + return formated !== 0 ? `${formated} ${more} назад` : `${formated} ${one} назад` + } + + function getFormatedMessageTime(created_at?: string | null) { + if (!created_at) created_at = '' + + const date = new Date() + const createdDate = new Date(created_at) + + // разница в секундах + const difference = (date.getTime() - createdDate.getTime()) / 1000 + + for (const [key, value] of Object.entries(formatVariant).reverse()) { + if (difference > Number(key)) { + return value(difference) + } + } + + return formatVariant['60'](difference) + } + return { + getFormatedMessageTime, + formatedDate, + } +} @@ -5,6 +5,7 @@ gap: 30px; border-radius: 15px; height: 100%; + text-align: left; @media screen and (max-width: 1000px) { flex-direction: column; @@ -8,6 +8,7 @@ import NeuralSvg from '#/assets/svg/neural.svg?react' import RepeatSvg from '#/assets/svg/repeat.svg?react' import DownloadSvg from '#/assets/svg/download.svg?react' import TrashSvg from '#/assets/svg/trash.svg?react' +import { useMessageTime } from '../model' interface ImageMessageProps extends Omit { file: string | null @@ -15,9 +16,7 @@ interface ImageMessageProps extends Omit { } export const ImageMessage = memo(({ file, uid, created_at, model, content }: ImageMessageProps) => { - const formatedDate = useMemo(() => { - return Math.round((new Date().getTime() - new Date(created_at).getTime()) / 1000 / 60) - }, [created_at]) + const { formatedDate } = useMessageTime(created_at) const [imageLoadStates, setImageLoadStates] = useState(new Array(4).fill(false)) @@ -25,7 +24,7 @@ export const ImageMessage = memo(({ file, uid, created_at, model, content }: Ima
-

{formatedDate} мин. назад

+

{formatedDate}

{model} @@ -1,3 +1,4 @@ export * from './types' export * from './api' export * from './ui' +export * from './model' @@ -0,0 +1 @@ +export * from './use-messages-filters' \ No newline at end of file @@ -0,0 +1,16 @@ +import { Message } from '#/entities/message' +import { useState } from 'react' + +export function useMessagesFilters(messages: Message[]) { + const [value, setValue] = useState('') + + const filteredMessages = messages.filter((message) => { + return message.content.toLowerCase().includes(value.toLowerCase()) + }) + + return { + filteredMessages, + value, + setValue + } +} @@ -0,0 +1 @@ +export * from './message-fast-choice-menu' @@ -0,0 +1,75 @@ +.menu { + display: flex; + flex-direction: column; + gap: 15px; + padding: 10px; + border-radius: 15px; + border: 1px solid rgba($color: #a4aab5, $alpha: 0.2); + background: var(--new-ui-element-bg); + width: fit-content; + transition: all 0.6s ease-in-out; + + &:hover { + width: 280px !important; + .menu__new { + span { + display: inline-block; + } + } + + .menu__input { + display: inline-block; + width: 150px !important; + } + + .menu__inputbox { + justify-content: flex-end; + } + } + + &__new { + display: flex; + align-items: center; + justify-content: center; + gap: 7px; + padding: 10px 15px; + span { + display: none; + color: var(--new-ui-gray-color); + } + } + + &__inputbox { + display: flex; + align-items: center; + justify-content: center !important; + gap: 5px; + padding: 5px 7px; + } + + &__input { + outline: none; + border: none; + background: transparent; + color: var(--text-color-main); + display: none; + &::placeholder { + color: var(--new-ui-gray-color); + } + } +} + +.list { + height: 350px; + overflow-y: scroll; + + &__empty { + color: var(--new-ui-gray-color); + text-align: center; + } +} + +.loader { + text-align: center; + // width: max-content; +} @@ -0,0 +1,64 @@ +import React, { MouseEventHandler, useState } from 'react' +import styles from './message-fast-choice-menu.module.scss' +import { c, Loader } from '#/shared' +import { Message } from '#/entities/message' +import PlusSvg from '#/assets/svg/plus.svg?react' +import Image from 'next/image' +import { MessageItem } from './message-item' +import SearchSvg from '#/assets/svg/search.svg?react' +import { useMessagesFilters } from '../model' + +export interface MessageFastChoiceMenuProps { + messages: Message[] + loading: boolean +} + +export const MessageFastChoiceMenu = ({ messages, loading }: MessageFastChoiceMenuProps) => { + const { filteredMessages, value, setValue } = useMessagesFilters(messages) + + const [hovered, setHovered] = useState(false) + + return ( +
setHovered(true)} + onMouseLeave={() => setHovered(false)} + className={styles.menu} + > + +
    + {loading && ( +
    + +
    + )} + {!filteredMessages.length && !loading && ( +
  • Ничего не найдено
  • + )} + {filteredMessages.map((message) => ( +
  • + {}} + {...message} + /> +
  • + ))} +
+ +
+ + setValue(e.target.value)} + className={styles.menu__input} + placeholder='Поиск генераций' + type='text' + /> +
+
+ ) +} @@ -0,0 +1,58 @@ +.item { + display: flex; + align-items: center; + justify-content: space-between; + padding: 7px; + border-radius: 10px; + transition: width 0.6s ease-in-out; + + &_hovered { + .item__desc { + display: block; + opacity: 1; + } + + .item__right { + opacity: 1; + display: block; + } + } + + &:hover { + background-color: var(--new-ui-bg-app-color); + } + + &__desc { + opacity: 0; + transition: opacity 0.6s ease-in-out; + display: none; + } + + &__left { + display: flex; + gap: 7px; + align-items: center; + } + + &__right { + padding-right: 10px; + opacity: 0; + transition: opacity 0.6s ease-in-out; + display: none; + } + + &__img { + border-radius: 5px; + object-fit: cover; + } + + &__content { + font-weight: 500; + font-size: 14px; + } + + &__time { + font-size: 11px; + color: var(--new-ui-gray-color); + } +} @@ -0,0 +1,48 @@ +import { Message, useMessageTime } from '#/entities/message' +import Image from 'next/image' +import React from 'react' + +import styles from './message-item.module.scss' +import { c } from '#/shared' +import TrashSvg from '#/assets/svg/trash.svg?react' + +interface MessageItemProps extends Message { + deleteMessage: () => void + className?: string + hovered?: boolean +} + +export const MessageItem = ({ + created_at, + className, + file, + hovered, + content, + deleteMessage, +}: MessageItemProps) => { + const { formatedDate } = useMessageTime(created_at) + + return ( +
+
+ {content +
+

{formatedDate}

+

{content}

+
+
+ +
+ +
+
+ ) +} @@ -0,0 +1,2 @@ +export * from './ui' +export * from './model' @@ -1,5 +1,18 @@ export function formatAndSortDates(inputArray: { date: string; value: number }[]) { - const monthNames = ['янв', 'фев', 'мар', 'апр', 'май', 'июн', 'июл', 'авг', 'сен', 'окт', 'ноя', 'дек'] + const monthNames = [ + 'янв', + 'фев', + 'мар', + 'апр', + 'май', + 'июн', + 'июл', + 'авг', + 'сен', + 'окт', + 'ноя', + 'дек', + ] const formattedData = inputArray.map((item: any) => { const date = new Date(item.date) @@ -23,14 +36,14 @@ export function formatAndSortDates(inputArray: { date: string; value: number }[] } export function formatDate( - date: string | number | Date, - options: Intl.DateTimeFormatOptions, - lang?: string + date: string | number | Date, + options: Intl.DateTimeFormatOptions, + lang?: string ) { - if (typeof date !== "object") { - date = new Date(typeof date === "number" ? date : Date.parse(date)) - } + if (typeof date !== 'object') { + date = new Date(typeof date === 'number' ? date : Date.parse(date)) + } - const formatter = Intl.DateTimeFormat("ru", options) - return formatter.format(date) + const formatter = Intl.DateTimeFormat('ru', options) + return formatter.format(date) } @@ -12,16 +12,23 @@ transform: translateX(-50%); z-index: 3; + @media screen and (max-width: 1200px) { + top: 30px; + } +} - @media screen and (max-width: 1200px) { - top: 30px; - } +.popup { + position: absolute; + top: 50%; + right: 20px; + transform: translateY(-50%); + z-index: 2; } .messages { z-index: 2; - height: calc(100dvh - 20px - 20px - 40px - 25px); - overflow-y: scroll; + height: calc(100dvh - 20px - 20px - 40px - 25px); + overflow-y: scroll; } .input { @@ -49,7 +56,7 @@ var(--new-ui-bg-app-color) 30%, rgba(255, 255, 255, 0) 100% ); - z-index: 1; + z-index: 1; &_top { top: 0px; } @@ -13,9 +13,10 @@ import { useImageBot } from '#/entities/model-entity/model/use-image-bot' import { ImageModelsPopup, ImageModelsSelect } from '#/widgets/image-models-popup' import { ImageInput } from '#/features/image-models-input' -import { useRef } from 'react' +import { useEffect, useRef } from 'react' import { useMessagesStore } from '#/widgets/messages' import { ImagesEmpty } from '#/widgets/image-messages' +import { MessageFastChoiceMenu } from '#/features/message-fast-choice-menu' const ImageModelPage: NextPageWithLayout = () => { const { query } = useRouter() @@ -36,7 +37,7 @@ const ImageModelPage: NextPageWithLayout = () => { await Promise.all([fetchBotParams()]) } - React.useEffect(() => { + useEffect(() => { onFetch() }, [session, query]) @@ -67,6 +68,10 @@ const ImageModelPage: NextPageWithLayout = () => {
+
+ +
+
@@ -61,7 +61,7 @@ export const ImageMessagesList = ({ className, container }: MessagesListProps) = setChosenImage(message.file as string) }} > - + ))}