@@ -1,3 +1,5 @@ - - + + \ No newline at end of file @@ -1,3 +1,4 @@ - - + + \ No newline at end of file @@ -0,0 +1,11 @@ + + + \ No newline at end of file @@ -0,0 +1,12 @@ + + + + + + + + + + \ No newline at end of file @@ -0,0 +1,5 @@ + + + \ No newline at end of file @@ -0,0 +1,5 @@ + + + \ No newline at end of file @@ -0,0 +1,125 @@ +import { useAppSelector } from '@/src/main/store/store' +import { TooltipCustom } from '@/src/shared' +import { API_URL } from '@/src/shared/lib/constants' +import { InputStyleSmallLight, InputStyleSmallDark } from '@/src/shared/ui/input' +import { TableRow, TableCell, TextField, Stack } from '@mui/material' +import { useMask } from '@react-input/mask' +import axios from 'axios' +import { useSession } from 'next-auth/react' +import Image from 'next/image' +import { useState, useMemo, useEffect } from 'react' + +export interface KeyRowProps extends ApiKeyDTO { + deleteKey: (name: string) => void + keyValue: string +} + +export const KeyRow = ({ + key, + name, + user, + keyValue, + deleteKey, + token_limit, + created_at, + expires_at, + ...props +}: KeyRowProps) => { + const [isCopy, setIsCopy] = useState(false) + const [limit, setLimit] = useState(token_limit) + const theme = useAppSelector((state) => state.theme.theme) + const { data: session } = useSession() + + const inputRef = useMask({ + mask: '_'.repeat(10), + replacement: { + _: /\d+/, + }, + }) + + const copy = (text: string) => { + navigator.clipboard.writeText(text) + setIsCopy(true) + setTimeout(() => setIsCopy(false), 3000) + } + + const computedLimit = useMemo(() => { + return limit === '' || limit === 'Бесконечно' ? null : Number(limit) + }, [limit]) + + useEffect(() => { + let timeout = window.setTimeout(() => { + console.log(limit) + if (limit !== token_limit) { + axios.patch( + API_URL + '/public/api-key', + { token_limit: computedLimit, name }, + { headers: { Authorization: `Bearer ${session?.access}` } } + ) + } + }, 1000) + return () => window.clearTimeout(timeout) + }, [limit]) + + return ( + + + {name} + + + {keyValue} + + + setLimit(e.target.value)} + sx={ + theme === 'light' + ? { ...InputStyleSmallLight } + : { ...InputStyleSmallDark } + } + /> + + + {created_at.split('T')[0]} + + + {expires_at !== null ? expires_at : 'Бессрочно'} + + + + {isCopy ? ( + {'copy'} + ) : ( + + copy(keyValue)} + src={'/svg/copy.svg'} + width={20} + height={20} + style={{ cursor: 'pointer' }} + alt={'copy'} + /> + + )} + deleteKey(name)} + style={{ cursor: 'pointer' }} + src='/svg/main_menu/trash.svg' + width={20} + height={20} + alt='Удалить' + /> + + + + ) +} @@ -0,0 +1 @@ +export * from './message.routes' \ No newline at end of file @@ -0,0 +1,25 @@ +import { API_URL } from "@/src/shared/lib/constants" +import { IMessageRequest } from "@/src/shared/lib/types/types-gpt" +import axios, { AxiosResponse } from "axios" +import { Message, MessageSend } from "../types" + +export async function sendImage( + model: string | null, + dataForSend: MessageSend | FormData, + token?: string +) { + const HeaderDataType = + dataForSend instanceof FormData ? 'multipart/form-data' : 'application/json' + + return await axios.post( + API_URL + `/media/image/${model}`, + dataForSend, + { + withCredentials: true, + headers: { + Authorization: `Bearer ${token}`, + 'Content-Type': HeaderDataType, + }, + } + ) +} \ No newline at end of file @@ -0,0 +1 @@ +export * from './message' \ No newline at end of file @@ -0,0 +1,18 @@ +export interface MessageSend { + content: string + file?: File | null + info: T +} + +export interface Message { + content: string + created_at: string + elapsed_time: string + file: File | null | string + from_model: boolean + info: null + is_favourite: boolean + is_sent: boolean + uid: string + model: string +} @@ -0,0 +1,2 @@ +export * from './types' +export * from './api' \ No newline at end of file @@ -0,0 +1,11 @@ +import { API_URL } from '@/src/shared/lib/constants' +import axios from 'axios' +import { IModel } from '../types' + +export async function getBotParams(slug: string, token?: string) { + return await axios.get(API_URL + `/ml_models/${slug}`, { + headers: { + Authorization: `Bearer ${token}`, + }, + }) +} @@ -0,0 +1,11 @@ +import { API_URL } from '@/src/shared/lib/constants' +import axios from 'axios' +import { IShortModel } from '../types' + +export async function getModelsImages(token?: string) { + return await axios.get(API_URL + '/ml_models/?category=images', { + headers: { + Authorization: `Bearer ${token}`, + }, + }) +} @@ -0,0 +1,2 @@ +export * from './bot.route' +export * from './images-bots.route' @@ -0,0 +1 @@ +export * from './use-images-bots' \ No newline at end of file @@ -0,0 +1,133 @@ +import { useState } from 'react' +import { IModel } from '../types' +import { useSession } from 'next-auth/react' +import { getBotParams } from '../api' +import { useAppDispatch } from '@/src/main/store/store' +import { setParams } from '@/src/main/store/model-parametres-store' + +export function useImageBot(slug: string) { + const [botParams, setBotParams] = useState(null) + const [version, setVersion] = useState('') + const [modelType, setModelType] = useState('') + + const dispatch = useAppDispatch() + + const { data } = useSession() + + function setDefault(bot: IModel) { + setVersion('') + + dispatch( + setParams(bot.parameters.reduce((a, v) => ({ ...a, [v.key]: v.values.default }), {})) + ) + } + + // это пиз**ц + // нужен рефакторинг (я то в этом не разбираюсь) + // а стажеры и подавно)))) + function setStoreParams(bot: IModel) { + dispatch( + setParams( + bot.parameters.reduce( + (a, v) => + v.versions.includes(bot.versions[0].slug) + ? { ...a, [v.key]: v.values.default } + : { ...a }, + {} + ) + ) + ) + } + + function setAllParams(bot: IModel) { + setBotParams(bot) + setModelType(bot.slug) + + // store + if (bot.versions.length === 0) return setDefault(bot) + + setVersion(bot.versions[0].slug) + setStoreParams(bot) + } + + // это пиз**ц + const resetParams = () => { + if (botParams) { + dispatch(setParams({})) + if (botParams.versions.length !== 0) { + setVersion(botParams.versions[0].slug) + dispatch( + setParams( + botParams.parameters.reduce( + (a, v) => + v.versions.includes(botParams.versions[0].slug) + ? { ...a, [v.key]: v.values.default } + : { ...a }, + {} + ) + ) + ) + } else { + setVersion(botParams.slug) + dispatch( + setParams( + botParams.parameters.reduce( + (a, v) => ({ ...a, [v.key]: v.values.default }), + {} + ) + ) + ) + } + } + } + + // это пиз**ц + const setDefaultParams = () => { + if (!botParams) return + + dispatch(setParams({})) + + if (version === '') { + return dispatch( + setParams( + botParams.parameters.reduce( + (a, v) => ({ ...a, [v.key]: v.values.default }), + {} + ) + ) + ) + } + + dispatch( + setParams( + botParams.parameters.reduce( + (a, v) => + v.versions.includes(version) + ? { ...a, [v.key]: v.values.default } + : { ...a }, + {} + ) + ) + ) + } + + // api functions + + async function fetchBotParams() { + if (!data) return + + const { data: bot, ...response } = await getBotParams(slug, data.access) + + if (response.status < 400) setAllParams(bot) + } + + return { + botParams, + version, + modelType, + fetchBotParams, + setVersion, + resetParams, + setDefaultParams + } +} @@ -0,0 +1,24 @@ +import { useState } from 'react' +import { getModelsImages } from '../api' +import { useSession } from 'next-auth/react' +import { IShortModel } from '../types' + +export function useImagesBots() { + const [bots, setBots] = useState([]) + + const { data } = useSession() + + async function fetchBots() { + if (!data) return + + const response = await getModelsImages(data.access) + + if (response.status < 400) setBots(response.data) + } + + return { + bots, + fetchBots, + setBots + } +} @@ -0,0 +1 @@ +export * from './model.types' \ No newline at end of file @@ -0,0 +1,54 @@ +type ModelForChats = 'chatgpt' | 'llama2' | 'vicuna' | 'deepl' | 'mistral' + +export interface IShortModel { + uid: string + title: string + description: string + slug: string + image: string + actual_stat: { + generation_time: string + tokens_cost: string + } +} + +export interface IModel { + uid: string + title: string + description: string + slug: string + image: string + settings: { is_active: boolean } + parameters: IModelParams[] + versions: IModelVersions[] + inputs: IModelInputs[] +} +export interface IModelParams { + name: string + description: string + key: string + type: string + required: boolean + values: { + availables: string[] + default: any + end: number + start: number + step: number + } + versions: string[] +} +export interface IModelVersions { + name: string + description: string + default: boolean + slug: string +} +export interface IModelInputs { + // type: 'image' | 'zip' | 'text' | 'audio' | 'pdf' | 'txt' + type: string + required: boolean + versions: string[] +} + +export default ModelForChats @@ -0,0 +1,3 @@ +export * from './api' +export * from './model' +export * from './types' \ No newline at end of file @@ -0,0 +1,168 @@ +import axios, { AxiosResponse } from 'axios' +import { User } from 'next-auth' + +import { API_URL } from '@/src/shared/lib/constants' +import { IOffer } from '@/src/widgets/payment/model/payment' +import { AccountType, DataForLogin, PayProductRequest, PayProductResponse } from '../model/types' + +export async function getPaymentsPlans(token: string): Promise { + try { + const { data } = await axios.get(API_URL + '/payments/plans', { + headers: { + Authorization: `Bearer ${token}`, + }, + }) + + return data + } catch (err) { + return null + } +} + +export async function payProduct(token: string | null, plan: string): Promise { + if (token === null) { + return null + } + + try { + const { data } = await axios.post>( + API_URL + '/payments/plans', + { + uid: plan, + is_test: 1, + }, + { + headers: { + Authorization: `Bearer ${token}`, + }, + } + ) + return data.payment_url + } catch (err) { + return null + } +} + +export async function changePassword( + token: string | null, + password_1: string, + password_2: string, + current_password: string +): Promise { + try { + const { status } = await axios.put( + API_URL + '/auth/reset-pass', + { + password_1, + password_2, + current_password, + }, + { + headers: { + Authorization: `Bearer ${token}`, + }, + } + ) + + return status + } catch (err) { + return 400 + } +} + +export async function getApiKeys(token: string | null): Promise { + try { + const { data } = await axios.get(API_URL + '/public/api-key', { + headers: { + Authorization: `Bearer ${token}`, + }, + }) + + return data + } catch (err) { + return null + } +} + +export async function createApiKeys(data: any, token: string | null): Promise { + try { + const { data: result } = await axios.post(API_URL + '/public/api-key', data, { + headers: { + Authorization: `Bearer ${token}`, + }, + }) + + return result + } catch (err) { + return null + } +} + +export async function deleteApiKey(name: any, token: string | null): Promise { + try { + const { data: result, status } = await axios.delete(API_URL + '/public/api-key', { + data: { + name, + }, + headers: { + Authorization: `Bearer ${token}`, + }, + }) + + return status + } catch (err) { + return null + } +} + +export async function loginByEmail(email: string, password: string): Promise { + try { + const { data } = await axios.post>( + API_URL + '/auth/login', + { + email, + password, + } + ) + + return data + } catch (err) { + return null + } +} + +export async function removeSub(token?: string) { + try { + const { status } = await axios.delete(API_URL + '/payments/plans', { + headers: { + Authorization: `Bearer ${token}`, + }, + }) + + return status + } catch (err) { + return null + } +} + + +export const getAccountType = async (token: string | null | undefined): Promise => { + if (!token) { + return 'regular' + } + + try { + const { data } = await axios.get>( + API_URL + '/auth/account-type', + { + headers: { + Authorization: `Bearer ${token}`, + }, + } + ) + + return data.status + } catch (err) { + return 'regular' + } +} @@ -1,22 +0,0 @@ -import axios, { AxiosResponse } from 'axios' - -import { API_URL } from '@/src/shared/lib/constants' - -import { AccountType } from '../model/types' -export const getAccountType = async (token: string | null | undefined): Promise => { - if (!token) { - return 'regular' - } - - try { - const { data } = await axios.get>(API_URL + '/auth/account-type', { - headers: { - Authorization: `Bearer ${token}`, - }, - }) - - return data.status - } catch (err) { - return 'regular' - } -} @@ -0,0 +1 @@ +export * from './account-endpoints' @@ -14,3 +14,21 @@ export interface IUserSetting { type: SettingType value: SettingValueType } + +export interface UserBalance { + current_token_balance: number +} + +export interface PayProductRequest { + uid: string + is_test: number +} + +export interface PayProductResponse { + payment_url: string +} + +export interface DataForLogin { + email: string + password: string +} @@ -1 +1,2 @@ export { getAllInfo, userSlice } from './model/user-type-slice' +export * from './api' @@ -7,12 +7,12 @@ import { useSession } from 'next-auth/react' import { useAppSelector } from '@/src/main/store/store' import { ButtonGray, ButtonUI, Error, InputStyleDark, InputStyleLight, Loader, Modal } from '@/src/shared' -import { accountApi } from '@/src/shared/api/account-endpoints' import { API_URL } from '@/src/shared/lib/constants' import { useShowData } from '@/src/shared/lib/hooks' import { DateInput } from '@/src/shared/ui/date-input/date-input' import styles from '../invite-person-in-business/ui/invite-modal.module.scss' +import { createApiKeys, getApiKeys } from '@/src/entities/user-account' export const ApiKeyModal = ({ open, @@ -45,10 +45,10 @@ export const ApiKeyModal = ({ setIsLoading(true) const { result } = endDate && endDate !== '' - ? await accountApi.createApiKeys({ name: title !== '' ? title : keyName, expires_at: endDate }, data?.access) - : await accountApi.createApiKeys({ name: title !== '' ? title : keyName }, data?.access) + ? await createApiKeys({ name: title !== '' ? title : keyName, expires_at: endDate }, data?.access) + : await createApiKeys({ name: title !== '' ? title : keyName }, data?.access) if (result !== null) { - accountApi.getApiKeys(data?.access).then((res) => { + getApiKeys(data?.access).then((res) => { setKeys(res) setIsLoading(false) }) @@ -1,7 +1,7 @@ -import { accountApi } from '@/src/shared/api/account-endpoints' +import { loginByEmail } from "@/src/entities/user-account" export const authTelegram = async (email: any, password: any) => { - const user = await accountApi.loginByEmail(email, password) + const user = await loginByEmail(email, password) if (user !== null) { (window as any).Telegram.WebApp.sendData(user.token.access) @@ -10,7 +10,7 @@ export const authTelegram = async (email: any, password: any) => { export const authTelegramYandex = async (email: any, password: any) => { - const user = await accountApi.loginByEmail(email, password) + const user = await loginByEmail(email, password) if (user !== null) { ;(window as any).Telegram.WebApp.sendData(user.token.access) @@ -0,0 +1 @@ +export * from './use-image-bot-create' \ No newline at end of file @@ -0,0 +1,54 @@ +import { getUserBalance } from '@/src/entities/balance' +import { Message, MessageSend, sendImage } from '@/src/entities/message' +import { useAppDispatch } from '@/src/main/store/store' +import { Device } from '@/src/shared/lib/types/entities' +import { formDataHelper } from '@/src/widgets/messages' +import { useSession } from 'next-auth/react' +import { Dispatch, SetStateAction, useState } from 'react' + +export function useImageBotCreateImage( + showError: (message: string) => void, + type: string, + device: Device, + setLoading: (value: boolean) => void, + setMessages: Dispatch> +) { + const [isComplete, setIsComplete] = useState(false) + + const { data } = useSession() + + const dispatch = useAppDispatch() + + const createImage = async (dataForSend: MessageSend) => { + const { content, file } = dataForSend + + setIsComplete(false) + setLoading(true) + + const dataSending = file ? formDataHelper(file, dataForSend) : dataForSend + + const response = await sendImage(type, dataSending, data?.access) + + if (response.status >= 400) showError('Ошибка отправки сообщения') + + setLoading(false) + + dispatch(getUserBalance(data?.access)) + + setMessages((prev: Message[]) => { + if (!prev || !prev.length) return response.data + + if (device === 'desktop') { + return [...response.data, ...prev] + } + return [...prev, ...response.data] + }) + + setIsComplete(true) + } + + return { + createImage, + isComplete + } +} @@ -0,0 +1 @@ +export * from './model' \ No newline at end of file @@ -0,0 +1 @@ +export * from './use-images-bot-filters' \ No newline at end of file @@ -0,0 +1,16 @@ +import { useAppSelector } from '@/src/main/store/store' +import { useState } from 'react' + +export function useImagesBotFilters() { + const [openFiltersMobile, setOpenFiltersMobile] = useState(false) + const [params, setParams] = useState(false) + const includeParams = useAppSelector((state) => state.params.params) + + return { + openFiltersMobile, + setOpenFiltersMobile, + params, + setParams, + includeParams, + } +} @@ -0,0 +1 @@ +export * from './model' \ No newline at end of file @@ -0,0 +1 @@ +export * from './use-images-uniq-input' \ No newline at end of file @@ -0,0 +1,65 @@ +import { useShowData } from '@/src/shared' +import { MessageSend } from '@/src/shared/lib/types/model' +import { useState, ChangeEvent } from 'react' + +export function useImagesUniqInput( + version: string, + includeParams: object, + createImage: (dataForSend: MessageSend) => any +) { + const [image, setImage] = useState(null) + + const { error, showError } = useShowData() + + function onLoadImage(event: ChangeEvent) { + if (event.target.files) { + setImage(event.target.files[0]) + } + } + + function onCreateImage(input: string, required: (string | null)[]) { + + // про switch не слышали люди)) + if (required.includes('text') && (input === '' || input === null)) { + showError('Введите сообщение!') + return false + } + if (required.includes('image') && image === null) { + showError('Прикрепите изображение!') + return false + } + if (required.includes('zip') && image === null) { + showError('Прикрепите архив!') + return false + } + + // Снова какой то пиз**ц + let data = {} + if (version === '') { + data = { + ...includeParams, + } + } else { + data = { + version: version, + ...includeParams, + } + } + + createImage({ + content: input, + file: image, + info: { + ...data, + }, + }) + return true + } + + return { + image, + setImage, + onLoadImage, + onCreateImage, + } +} @@ -0,0 +1 @@ +export * from './model' \ No newline at end of file @@ -0,0 +1 @@ +export * from './use-images-bot-pagination' \ No newline at end of file @@ -0,0 +1,115 @@ +import { Message } from '@/src/entities/message' +import { useAppSelector } from '@/src/main/store/store' +import { useShowData } from '@/src/shared' +import { Device } from '@/src/shared/lib/types/entities' +import { getImagesGalery } from '@/src/widgets/messages' +import { useMediaQuery } from '@mui/material' +import { useSession } from 'next-auth/react' +import { useRef, useState } from 'react' +import { Limit, LimitSize } from '../types' + +export function useImageBotPagination(deviceType: Device) { + const refScrollMobile = useRef(null) + const refScrollDesktop = useRef(null) + const mobileScrollContainer = useRef(null) + const offset = useRef(0) + + const [messages, setMessages] = useState([]) + + const [loading, setLoading] = useState(false) + + const { showError } = useShowData() + + const { data } = useSession() + + const limits: Record = { + small: { + active: useMediaQuery('(max-height: 600px)'), + limit: 20, + firstLimit: 30, + }, + medium: { + active: useMediaQuery('(min-height: 600px) and (max-height: 900px)'), + limit: 30, + firstLimit: 50, + }, + large: { + active: useMediaQuery('(min-height: 900px)'), + limit: 45, + firstLimit: 70, + }, + } + + const fetchMessages = async (count?: number) => { + if (!data) return + + setLoading(true) + const { data: answer, ...response } = await getImagesGalery( + data.access, + offset.current, + count || 10 + ) + setLoading(false) + + if (response.status >= 400 || !Array.isArray(answer)) + return showError('Ошибка загрузки чата') + + if (deviceType === 'desktop') { + setMessages((prev) => [...prev, ...answer]) + offset.current = offset.current + answer.length + return + } + + setMessages((prev) => [...answer.reverse(), ...prev]) + offset.current = offset.current + answer.length + } + + const callback = async function (entries: IntersectionObserverEntry[]) { + if (!entries[0].isIntersecting) return + + if (deviceType === 'desktop') { + const active = Object.values(limits).find((item) => item.active) + + if (offset.current > 0) return fetchMessages(active?.limit) + + fetchMessages(active?.firstLimit) + } + + if (!mobileScrollContainer.current) return + + const scrollBottom = + mobileScrollContainer.current.scrollHeight - mobileScrollContainer.current.scrollTop + + await fetchMessages() + + setTimeout(() => { + mobileScrollContainer.current!.scroll({ + top: mobileScrollContainer.current!.scrollHeight - scrollBottom, + behavior: 'smooth', + }) + }, 500) + } + + function onObserverMounted() { + const currentObserver = + deviceType === 'desktop' ? refScrollDesktop.current : refScrollMobile.current + + if (!currentObserver) return + + const observer = new IntersectionObserver(callback, { rootMargin: '400px' }) + + observer.observe(currentObserver!) + } + + return { + refScrollMobile, + refScrollDesktop, + onObserverMounted, + messages, + loading, + setLoading, + setMessages, + fetchMessages, + mobileScrollContainer, + } +} @@ -0,0 +1 @@ +export * from './limits' \ No newline at end of file @@ -0,0 +1,7 @@ +export type LimitSize = 'small' | 'medium' | 'large' + +export interface Limit { + active: boolean + limit: number + firstLimit: number +} \ No newline at end of file @@ -0,0 +1,2 @@ +export * from './model' +export * from './types' \ No newline at end of file @@ -0,0 +1,3 @@ + + +export * from './use-images-library' \ No newline at end of file @@ -0,0 +1,4 @@ +export interface ImageWithState { + image: string + state: boolean +} \ No newline at end of file @@ -0,0 +1,53 @@ +import { useEffect, useMemo, useState } from 'react' +import { ImageWithState } from './types' + +export const useImagesLibrary = (images: string[], current: string | null, reverse: boolean) => { + const [imagesWithState, setImagesWithState] = useState(getImagesWithState(images)) + const [initialCount, setInitialCount] = useState(0) + + function getImagesWithState(images: string[]) { + const imagesWithState = images.map((image) => ({ + image, + state: false, + })) + + return imagesWithState + } + + function updateImageState(image: string, state: boolean) { + setImagesWithState((prev) => prev.map((item) => (item.image === image ? { ...item, state } : item))) + } + + const currentImageIndex = useMemo( + () => (!current ? undefined : imagesWithState.findIndex((image) => image.image === current)), + [imagesWithState, current] + ) + + useEffect(() => { + if (initialCount === images.length) { + return + } + + setInitialCount(images.length) + + const filteredMessages = images.filter((item) => + !imagesWithState.find((i) => i.image === item) ? true : false + ) + + if (filteredMessages.length === images.length) { + return setImagesWithState(getImagesWithState(filteredMessages)) + } + + if (reverse) setImagesWithState([...imagesWithState, ...getImagesWithState(filteredMessages)]) + else setImagesWithState([...getImagesWithState(filteredMessages), ...imagesWithState]) + }, [images]) + + return { + imagesWithState, + currentImageIndex, + initialCount, + setInitialCount, + setImagesWithState, + updateImageState, + } +} @@ -0,0 +1,53 @@ +import { debounce } from 'lodash' +import { useCallback, useEffect, useState } from 'react' +import { Swiper as SwiperCore } from 'swiper' +import { ImageWithState } from './types' + +export const useLibrarySwiper = (onSlideFalse: ((...args: any) => any) | undefined, reverse: boolean) => { + const [swiper, setSwiper] = useState(null) + + const keydown = (e: KeyboardEvent) => { + if (e.key === 'ArrowRight') { + e.preventDefault() + slideNext() + } + if (e.key === 'ArrowLeft') { + e.preventDefault() + slidePrev() + } + } + + const slidePrev = function () { + const result = swiper?.slidePrev() + + if (!result && onSlideFalse && !reverse) { + onSlideFalse() + } + } + + const slideNext = function () { + const result = swiper?.slideNext() + + if (!result && onSlideFalse && reverse) { + onSlideFalse() + } + } + + useEffect(() => { + document.addEventListener('keydown', keydown, true) + return () => { + document.removeEventListener('keydown', keydown, true) + } + }, [swiper]) + + // useEffect(() => { + // swiper?.slideTo(currentImageIndex || 0) + // }, [currentImageIndex]) + + return { + swiper, + setSwiper, + slidePrev, + slideNext, + } +} @@ -1,27 +1,62 @@ -import React, { Dispatch, SetStateAction } from 'react' +import React, { Dispatch, SetStateAction, useEffect, useState } from 'react' import Image from 'next/image' import styles from './modal-styles.module.scss' +import { ArrowDropDown } from '@mui/icons-material' +import { c, Loader } from '@/src/shared' +import { useImagesLibrary } from '../model' + +import { Swiper, SwiperSlide } from 'swiper/react' +import 'swiper/css' +import { Swiper as SwiperCore } from 'swiper' +import { useLibrarySwiper } from '../model/use-swiper' interface IProps { modal: boolean setModal: Dispatch> - image: string + current: string | null + setCurrent?: (value: string | null) => void + onSlideFalse?: (...args: any) => any + reverse?: boolean + images: string[] } -export default function FullScreenModal({ modal, setModal, image }: IProps) { - const isSvg = image.includes('.svg') + +export default function FullScreenModal({ + modal, + setModal, + current, + images, + onSlideFalse, + reverse = false, +}: IProps) { + const { imagesWithState, updateImageState, currentImageIndex, initialCount } = + useImagesLibrary(images, current, reverse) + + const { swiper, setSwiper, slideNext, slidePrev } = useLibrarySwiper(onSlideFalse, reverse) + + useEffect(() => { + if (!(initialCount < images.length && swiper)) return + + if (reverse) { + setTimeout(() => swiper.slideNext(), 1000) + return + } + + setTimeout(() => swiper.slideTo(images.length - initialCount - 1, 1000), 1000) + }, [images]) return (
setModal(false)} >
setModal(false)}> - +
- {!isSvg && image ? ( - К сожалению, изображение не загрузилось - ) : ( - К сожалению, изображение не загрузилось + + {modal && ( + setSwiper(swiper)} + > + {imagesWithState.map(({ image, state }, index) => ( + + {!state && } + {!image.includes('.svg') ? ( + e.stopPropagation()} + onLoadingComplete={() => { + updateImageState(image, true) + }} + loading='lazy' + src={image} + width={'1500'} + height={'1500'} + alt='К сожалению, изображение не загрузилось' + className={styles.image_style} + /> + ) : ( + e.stopPropagation()} + onLoad={() => updateImageState(image, true)} + alt='К сожалению, изображение не загрузилось' + className={styles.image_style} + /> + )} + + ))} + )} +
) @@ -0,0 +1 @@ +export { default as ImageModal } from './full-screen-modal' @@ -0,0 +1,92 @@ +.close_block { + position: absolute; + z-index: 105; + cursor: pointer; + right: 25px; + top: 25px; +} + +.arrow { + position: absolute; + z-index: 1205; + cursor: pointer; + top: 50%; + + width: 50px; + height: 50px; + + background: transparent; + border: none; + outline: none; + + svg { + fill: white; + width: 50px; + height: 50px; + } + + &_left { + transform: translateY(-50%) rotate(90deg); + left: -40px; + @media screen and (max-width: 768px) { + left: -3px; + } + } + + &_right { + transform: translateY(-50%) rotate(-90deg); + right: -50px; + + @media screen and (max-width: 768px) { + right: -10px; + } + } +} + +.image_block { + position: absolute; + padding: 0 20px; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: 105; + margin: auto; + width: fit-content; + height: fit-content; +} + +.loader { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); +} + +.image_style { + width: auto; + max-width: 70vw; + height: 100%; + object-fit: contain; + + @media screen and (max-width: 1024px) { + width: auto; + max-width: 70vw; + height: auto; + } +} + +.slide { + width: 80vw !important; + display: flex !important; + align-items: center; + justify-content: center; +} + +.swiper { + position: relative; + width: 100%; + max-width: 80vw; + max-height: 1000px; + height: 90vh; +} @@ -0,0 +1 @@ +export * from './ui' @@ -1,30 +0,0 @@ -.close_block{ - position: absolute; - z-index: 105; - cursor: pointer; - right: 25px; - top: 25px; -} - -.image_block{ - position: absolute; - padding: 0 20px; - top: 0; - right: 0; - bottom: 0; - left: 0; - z-index: 105; - margin: auto; - width: fit-content; - height: fit-content; -} - -.image_style{ - position: relative; - width: 100%; - height: 100%; - max-width: 960px; - max-height: 700px; - border-radius: 15px; - object-fit: contain; -} \ No newline at end of file @@ -4,7 +4,14 @@ import Link from 'next/link' import styles from '@/src/shared/styles/chats-bot-pages.module.scss' -export default function Title(props: any) { +export interface TitleProps { + type: string + title: string + linkBack?: string + rightSlot?: React.ReactNode +} + +export default function Title(props: TitleProps) { return ( @@ -12,11 +19,19 @@ export default function Title(props: any) { {' '} {props.type} •{' '} -  {props.title} + +  {props.title} + - - {props.title} - +
+ + {props.title} + +
{props.rightSlot}
+
) } @@ -5,7 +5,7 @@ import axios from 'axios' import { UniqInput } from '@/src/main/components/uniq_input' import { useAppSelector } from '@/src/main/store/store' import { ChatProps } from '@/src/shared/lib/types/model' -import { ChatMessagesList } from '@/src/widgets/messages/chat-messages-list' +import { ChatMessagesList } from '@/src/widgets/messages/ui/chat-messages-list' import 'intro.js/introjs.css' @@ -24,5 +24,5 @@ height: 25px; border-radius: 7px; border: var(--new-ui-ctrl-f-button-border); - background: var(--new-ui-ctrl-f-button-bg); + background: var(--new-ui-ctrl-f-button-bg); } @@ -29,7 +29,14 @@ interface Props { isLoader?: boolean } -export const Layout: React.FC = ({ children, device, isAuthPage = false, titlePage, title = titlePage, isLoader }) => { +export const Layout: React.FC = ({ + children, + device, + isAuthPage = false, + titlePage, + title = titlePage, + isLoader, +}) => { const { data: sessionData } = useSession() const appState = useAppSelector((state) => state) const dispatch = useAppDispatch() @@ -81,7 +88,10 @@ export const Layout: React.FC = ({ children, device, isAuthPage = false, targetDevice: device, targetType: 'sidemenu', }) - if (setting) setting?.value?.sidemenu_state === 'opened' ? setSidemenuDefaultOpen(true) : setSidemenuDefaultOpen(false) + if (setting) + setting?.value?.sidemenu_state === 'opened' + ? setSidemenuDefaultOpen(true) + : setSidemenuDefaultOpen(false) } }, [appState.settings.state]) @@ -17,10 +17,10 @@ export const store = configureStore({ balance: balanceSlice.reducer, stepper: stepperSlice.reducer, user: userSlice.reducer, + copy: copySlice.reducer, notification: notificationSlice.reducer, params: paramsStore.reducer, settings: settingsSlice.reducer, - copy: copySlice.reducer, }, }) @@ -15,14 +15,21 @@ --border-color2: #e7e7e7; --bg-audio: #f2f2fe; - --new-ui-bg-app-color: #eff0f2; - --new-ui-main-color: white; - --new-ui-gray-color: #a4aab5; - --new-ui-text-color: #2b2b42; - --new-ui-border: 2px solid #eff0f2; - --new-ui-btn-danger-bg: #ff23721a; - --new-ui-ctrl-f-button-bg: #f9f9fc; - --new-ui-ctrl-f-button-border: 1px solid #c4cbd8; + --new-ui-bg-app-color: #eff0f2; + --new-ui-main-color: white; + --new-ui-gray-color: #A4AAB5; + --new-ui-text-color: #2B2B42; + --new-ui-border: 2px solid #EFF0F2; + --new-ui-btn-danger-bg: #FF23721A; + --new-ui-ctrl-f-button-bg:#F9F9FC; + --new-ui-ctrl-f-button-border:1px solid #C4CBD8; + + --copy-border: #EFF0F2; + --copy-color: #343437; + + --cards-hover:#F9F9FF; + --choosen-tab: #FFFFFF; + --choosen-tab-color:#373737 ; } :root[data-theme='dark'] { @@ -42,14 +49,21 @@ --border-color2: #2c2c2c; --bg-audio: #303030; - --new-ui-bg-app-color: #303035; - --new-ui-border: 1px solid #40404e; - --new-ui-main-color: #151518; - --new-ui-gray-color: #a4aab5; - --new-ui-text-color: white; - --new-ui-btn-danger-bg: #ff23721a; - --new-ui-ctrl-f-button-bg: #242428; - --new-ui-ctrl-f-button-border: 1px solid #303035; + --new-ui-bg-app-color: #303035; + --new-ui-border: 1px solid #40404E; + --new-ui-main-color: #151518; + --new-ui-gray-color: #A4AAB5; + --new-ui-text-color: white; + --new-ui-btn-danger-bg: #FF23721A; + --new-ui-ctrl-f-button-bg:#242428; + --new-ui-ctrl-f-button-border:1px solid #303035; + + --copy-border: #343437; + --copy-color:#EFF0F2; + + --cards-hover:#303047; + --choosen-tab: #151518; + --choosen-tab-color: #FFFFFF; } * { @@ -323,19 +337,24 @@ textarea { } .rdw-editor-toolbar { - background-color: transparent !important; - padding-bottom: 15px !important; - border: none !important; - border-bottom: 1px solid #eff0f2 !important; + background-color: transparent !important; + padding: 20px 0 !important; + border: none !important; + border-bottom: 1px solid var(--copy-border) !important; + border-top: 1px solid var(--copy-border) !important; } .rdw-dropdown-wrapper { - background-color: transparent !important; - border: 2px solid var(--new-ui-bg-app-color) !important; - border-radius: 10px !important; - padding: 10px !important; - height: 36px !important; - min-width: 40px !important; + background-color: transparent !important; + border: 2px solid var(--new-ui-bg-app-color) !important; + border-radius: 10px !important; + padding: 0 !important; + height: 36px !important; + min-width: 40px !important; +} + +.rdw-dropdown-selectedtext{ + padding: 0 16px 0 12px !important; } .rdw-dropdown-wrapper:hover { @@ -352,22 +371,61 @@ textarea { } .rdw-dropdown-optionwrapper { - width: 100% !important; - margin-top: 15px !important; - overflow: hidden; - color: inherit !important; - overflow-y: hidden !important; + border: none !important; + border-radius: 10px !important; + width: 100% !important; + margin-top: 12px !important; + overflow: hidden; + color: inherit !important; + background-color: var(--background-color-main) !important; + overflow-y: hidden !important; } .rdw-block-dropdown { width: 150px !important; } .rdw-dropdown-optionwrapper > li { - color: inherit !important; + color: var(--new-ui-text-color) !important; + padding: 0 16px 0 12px !important; +} + +.rdw-dropdown-optionwrapper > li:hover{ + background-color:var(--new-ui-gray-color) !important +} + +.rdw-dropdownoption-active{ + background: var(--new-ui-gray-color) !important } .rdw-dropdown-optionwrapper:hover { - box-shadow: none; - color: inherit !important; + border: none !important; + box-shadow: none !important; + color: inherit !important; +} + +.rdw-dropdown-carettoclose{ + border-radius: 5px !important; + border-bottom-color: var(--new-ui-gray-color) !important; +} + +.rdw-dropdown-carettoopen{ + border-radius: 5px !important; + border-top-color: var(--new-ui-gray-color) !important; +} + +.rdw-text-align-wrapper{ + margin: 0 !important; +} + +.rdw-list-wrapper{ + margin: 0 !important; +} + +.rdw-history-wrapper{ + margin: 0 !important; +} + +.rdw-block-wrapper{ + margin: 0 !important; } .border-bottom-1px-gray { @@ -394,8 +452,9 @@ textarea { } .smallScroll::-webkit-scrollbar-track { - background: initial; - margin: 21px 0; + background: initial; + /*margin: 21px 0;*/ + margin: 5px 0; } .smallScroll::-webkit-scrollbar-thumb { @@ -413,8 +472,87 @@ textarea { transition-duration: 250ms; } -.rotate-0 { - transform: rotate(0deg); - transition: all; - transition-duration: 250ms; +.rotate-0{ + transform: rotate(0deg); + transition: all; + transition-duration: 250ms; +} + + +/*COPY*/ +.toolbarClassName{ + align-items: center; + gap: 15px; +} +.wrapperClassName{ + +} + +.editorClassName{ + border: 1px solid transparent; + transition: border-color 0.3s; + cursor: text; +} + +.editorClassName div:focus{ + outline: none !important; + border-color: transparent !important; +} + +.editorClassName div .public-DraftStyleDefault-block{ + display: inline-block; + padding:0 1px; + margin: 0.5em 0 !important; +} + +.public-DraftEditor-content{ + overflow-y: scroll; + max-height: calc(75vh - 200px) ; + + @media (max-width: 768px) { + max-height: calc(70vh - 200px) ; + } + +} + + +.public-DraftEditor-content::-webkit-scrollbar{ + height: 5px; + width: 2px; +} + +.public-DraftEditor-content::-webkit-scrollbar-track { + background: initial; + margin: 21px 0; +} + +.public-DraftEditor-content::-webkit-scrollbar-thumb { + background-color: rgba(217, 217, 217, 0.49); + border-radius: 5px; +} + +.inline{ + gap:3px; + margin: 0 !important; +} + +.inline-btn{ + width: 12px; + height: 25px !important; + margin: 0 !important; + padding: 0 !important; +} + +.rdw-option-active{ + background: rgba(229, 229, 229, 0.18) !important; + -webkit-box-shadow: inset 0 0 5px #c1c1c1 !important; + -moz-box-shadow: inset 0 0 5px #c1c1c1 !important; + box-shadow: inset 0 0 5px #c1c1c1 !important; + outline: none !important; +} + +.copy-color{ + color:var(--copy-color); + border-color: var(--copy-border); + } @@ -0,0 +1,8 @@ +interface ApiKeyDTO { + created_at: string // ISO 8601 format + name: string + key: string + expires_at: string | null + user: User + token_limit: string +} @@ -0,0 +1,37 @@ +interface UserDTO { + uid: string + first_name: string + last_name: string + username: string + created_at: string // ISO 8601 format + email: string + is_active: boolean + is_superuser: boolean + is_staff: boolean + is_confirmed: boolean + is_subscribed_to_emails: boolean + show_balance: boolean + profile_picture_link: string + account_type: string + token: { + access: string + refresh: string + } + payment_plan: { + uid: string + plan: { + uid: string + title: string + price: string + tokens_per_plan: string + duration: string + accessed_models: string[] + } + last_payment_at: string // ISO 8601 date + next_payment_at: string // ISO 8601 date + current_token_balance: number + } + referral_code: string | null + is_social: boolean + social_auth: any[] // Assuming it can hold any type of objects +} @@ -62,6 +62,7 @@ const Page: React.FC = ({ deviceType, deviceOs }) => { isTryRename, setIsTryRename, } = useChats(modelType) + const { messages, sendMessage, loading, getMessagesPagination, deleteMessage } = useModel( currentChat, showError, @@ -71,14 +72,18 @@ const Page: React.FC = ({ deviceType, deviceOs }) => { const includeParams = useAppSelector((state) => state.params.params) const dispatch = useDispatch() + const { push } = useRouter() + const deleteMessageMemo = useCallback(deleteMessage, [currentChat, messages]) React.useEffect(() => { if (data?.access) { model_api.getBotParams(router.asPath.split('/')[2], data.access).then((res) => { + if (!res.title) return push('/404') + setBotParams(res) setModelType(res.slug) - if (res.versions.length !== 0) { + if (res.versions && res.versions.length !== 0) { setVersion(res.versions[0].slug) dispatch( setParametres( @@ -91,7 +96,7 @@ const Page: React.FC = ({ deviceType, deviceOs }) => { ) ) ) - } else { + } else if (res.parameters) { setVersion('') dispatch( setParametres( @@ -211,7 +216,7 @@ const Page: React.FC = ({ deviceType, deviceOs }) => { title={botParams?.title} > - + <Title title={botParams?.title ? botParams?.title : 'Загрузка...'} type={'Чат-боты'} linkBack={'/chat-bot'} /> <Box className={styles.main}> <Box className={styles.chatWindow}> <AllChatWindow @@ -246,28 +251,32 @@ const Page: React.FC<any> = ({ deviceType, deviceOs }) => { createNewChat={createNewChat} handleClickChatSetting={handleClickChatSetting} /> - {desktop && ( - <Stack className='pd-30 bg-color-block border-radius-main' spacing={2}> - {botParams?.versions && botParams.versions?.length !== 0 && ( - <> - <Typography - sx={{ - color: '#A4AAB5', - fontWeight: '600', - fontSize: '14px', - letterSpacing: '0.1px', - }} - > - ВЕРСИИ - </Typography> - <ChatSelect - setDefaultParams={setDefaultParams} - value={version} - list={botParams.versions} - setValue={setVersion} - /> - </> - )} + {desktop && botParams && ( + <Stack + className='pd-30 bg-color-block border-radius-main' + spacing={2} + > + {botParams?.versions && + botParams.versions?.length !== 0 && ( + <> + <Typography + sx={{ + color: '#A4AAB5', + fontWeight: '600', + fontSize: '14px', + letterSpacing: '0.1px', + }} + > + ВЕРСИИ + </Typography> + <ChatSelect + setDefaultParams={setDefaultParams} + value={version} + list={botParams.versions} + setValue={setVersion} + /> + </> + )} {botParams && botParams.parameters?.length > 0 && ( <Box display={'flex'} @@ -344,27 +353,28 @@ const Page: React.FC<any> = ({ deviceType, deviceOs }) => { onClose={hideMobileSettings} > <Stack spacing={1} padding={2.4}> - {botParams?.versions && botParams.versions?.length !== 0 && ( - <> - <Typography - sx={{ - color: '#A4AAB5', - fontWeight: '600', - fontSize: '14px', - letterSpacing: '0.1px', - margin: '20px 0px 0px !important', - }} - > - ВЕРСИИ - </Typography> - <ChatSelect - setDefaultParams={setDefaultParams} - value={version} - list={botParams.versions} - setValue={setVersion} - /> - </> - )} + {botParams?.versions && + botParams.versions?.length !== 0 && ( + <> + <Typography + sx={{ + color: '#A4AAB5', + fontWeight: '600', + fontSize: '14px', + letterSpacing: '0.1px', + margin: '20px 0px 0px !important', + }} + > + ВЕРСИИ + </Typography> + <ChatSelect + setDefaultParams={setDefaultParams} + value={version} + list={botParams.versions} + setValue={setVersion} + /> + </> + )} {botParams && botParams.parameters?.length > 0 ? ( <> <Typography @@ -1,7 +1,7 @@ import * as React from 'react' -import { useRef } from 'react' +import { useEffect, useMemo, useRef, useState } from 'react' import { useDispatch } from 'react-redux' -import { Collapse, Typography } from '@mui/material' +import { Collapse, Typography, useMediaQuery } from '@mui/material' import Box from '@mui/material/Box' import Stack from '@mui/material/Stack' import { useRouter } from 'next/router' @@ -20,12 +20,20 @@ import { useAppSelector } from '@/src/main/store/store' import { DrawerCustom, Error } from '@/src/shared' import model_api from '@/src/shared/api/models/api' import { useModelImages } from '@/src/shared/api/models/endpoints' -import { IModel } from '@/src/shared/api/models/models' +import { IModel, IShortModel } from '@/src/shared/api/models/models' import { getTypeDevice } from '@/src/shared/lib/helpers' import { getDeviceOs } from '@/src/shared/lib/helpers/get-type-device' -import { useShowData } from '@/src/shared/lib/hooks' +import { useShowData, useThemeAndDevice } from '@/src/shared/lib/hooks' import { ArrowDownScroll } from '@/src/shared/ui/icon-components/scroll-down-arrow' -import { ImageMessagesList } from '@/src/widgets/messages/image-messages-list' +import { ImageMessagesList } from '@/src/widgets/messages/ui/image-messages-list' +import { useImagesBots } from '@/src/entities/model-entity' +import { useImageBot } from '@/src/entities/model-entity/model/use-image-bot' +import { useImagesUniqInput } from '@/src/features/image-bot-input' +import { useImagesBotFilters } from '@/src/features/image-bot-filters' +import { useImageBotPagination } from '@/src/features/image-bot-pagination' +import { useImagesPagination } from '@/src/widgets/messages' +import { ImageModelsPopup } from '@/src/widgets/image-models-popup/ui' +import { useImageBotCreateImage } from '@/src/features/image-bot-create-image' export async function getServerSideProps(context: any): Promise<{ props: any }> { const deviceType = getTypeDevice(context) @@ -41,237 +49,74 @@ export async function getServerSideProps(context: any): Promise<{ props: any }> } const Images: React.FC<any> = ({ deviceType, deviceOs }) => { - const [image, setImage] = React.useState<File | null>(null) - const desktop = deviceType === 'desktop' - const ios = deviceOs === 'ios' - const { error, showError } = useShowData() - const [openFiltersMobile, setOpenFiltersMobile] = React.useState<boolean>(false) - const router = useRouter() - const { data } = useSession() - const [botParams, setBotParams] = React.useState<IModel | null>(null) - const [version, setVersion] = React.useState<string>('') - const [modelType, setModelType] = React.useState<string>('') - const [params, setParams] = React.useState<boolean>(false) - const includeParams = useAppSelector((state) => state.params.params) - const dispatch = useDispatch() - - const { messages, loading, createImage, isComplete, getMessagesPagination } = useModelImages<Setting>(showError, modelType, deviceType) - - const [chatScrollHeight, setChatScrollHeight] = React.useState(0) - const [scrollBottom, setScrollBottom] = React.useState(0) - const refScrollMobile = useRef<any>() - const [isPaginating, setIsPaginating] = React.useState(false) + const { query } = useRouter() - React.useEffect(() => { - model_api - .getBotParams(router.asPath.split('/')[2], data?.access) - .then((res) => { - setBotParams(res) - setModelType(res.slug) - if (res.versions.length !== 0) { - setVersion(res.versions[0].slug) - dispatch( - setParametres( - res.parameters.reduce( - (a, v) => - v.versions.includes(res.versions[0].slug) - ? { ...a, [v.key]: v.values.default } - : { ...a }, - {} - ) - ) - ) - } else { - setVersion('') - dispatch( - setParametres( - res.parameters.reduce( - (a, v) => ({ ...a, [v.key]: v.values.default }), - {} - ) - ) - ) - } - }) - .catch((err) => {}) - }, [data?.access, router.query]) - - const onLoadImage = (event: React.ChangeEvent<HTMLInputElement>) => { - if (event.target.files) { - setImage(event.target.files[0]) - // showError('Файл успешно загружен, можете отправлять его!') - } - } - - const viewMobileSettings = () => { - setOpenFiltersMobile(true) - } + const { + botParams, + version, + modelType, + fetchBotParams, + resetParams, + setDefaultParams, + setVersion, + } = useImageBot(query.slug as string) - const hideMobileSettings = () => { - setOpenFiltersMobile(false) - } + const { ios, desktop } = useThemeAndDevice(deviceType, deviceOs) - const resetParams = () => { - if (botParams) { - dispatch(setParametres({})) - if (botParams.versions.length !== 0) { - setVersion(botParams.versions[0].slug) - dispatch( - setParametres( - botParams.parameters.reduce( - (a, v) => - v.versions.includes(botParams.versions[0].slug) - ? { ...a, [v.key]: v.values.default } - : { ...a }, - {} - ) - ) - ) - } else { - setVersion(botParams.slug) - dispatch( - setParametres( - botParams.parameters.reduce( - (a, v) => ({ ...a, [v.key]: v.values.default }), - {} - ) - ) - ) - } - } - } + const { error, showError } = useShowData() - const setDefaultParams = () => { - if (botParams) { - dispatch(setParametres({})) - if (version !== '') { - dispatch( - setParametres( - botParams.parameters.reduce( - (a, v) => - v.versions.includes(version) - ? { ...a, [v.key]: v.values.default } - : { ...a }, - {} - ) - ) - ) - } else { - dispatch( - setParametres( - botParams.parameters.reduce( - (a, v) => ({ ...a, [v.key]: v.values.default }), - {} - ) - ) - ) - } - } - } + const router = useRouter() - const onCreateImage = (input: string, required: (string | null)[]) => { - if (required.includes('text') && (input === '' || input === null)) { - showError('Введите сообщение!') - return false - } - if (required.includes('image') && image === null) { - showError('Прикрепите изображение!') - return false - } - if (required.includes('zip') && image === null) { - showError('Прикрепите архив!') - return false - } - let data = {} - if (version === '') { - data = { - ...includeParams, - } - } else { - data = { - version: version, - ...includeParams, - } - } + const { data: session } = useSession() - createImage({ - content: input, - file: image, - info: { - ...data, - }, - }) - return true - } + const { openFiltersMobile, setOpenFiltersMobile, params, setParams, includeParams } = + useImagesBotFilters() - React.useEffect(() => { - if (isComplete) { - if (!desktop) { - const block = refScrollMobile.current - if (block) { - //@ts-ignore - block.scrollTop = block.scrollHeight - } - } else { - window.scroll(0, 0) - } - } - }, [isComplete]) + const { + refScrollMobile, + refScrollDesktop, + mobileScrollContainer, + onObserverMounted, + setLoading, + setMessages, + fetchMessages, + loading, + messages, + } = useImageBotPagination(deviceType) - const handleMobileScroll = () => { - setScrollBottom(refScrollMobile.current?.scrollHeight - refScrollMobile.current?.scrollTop - refScrollMobile.current?.clientHeight) + const { createImage, isComplete } = useImageBotCreateImage( + showError, + modelType, + deviceType, + setLoading, + setMessages + ) - if (refScrollMobile.current && messages?.length !== 0) { - const { scrollTop, scrollHeight, clientHeight } = refScrollMobile.current - if (scrollTop === 0) { - if (getMessagesPagination) { - setIsPaginating(true) - getMessagesPagination(deviceType) - } - } - } - } + const { onCreateImage, onLoadImage, image, setImage } = useImagesUniqInput( + version, + includeParams, + createImage + ) - const handleScroll = () => { - if (window.scrollY + window.innerHeight >= document.documentElement.scrollHeight) { - setIsPaginating(true) - getMessagesPagination(deviceType) - } + async function onFetch() { + await Promise.all([fetchBotParams()]) } - React.useEffect(() => { - window.addEventListener('scroll', handleScroll) - return () => { - window.removeEventListener('scroll', handleScroll) - } - }, []) - - React.useEffect(() => { - const block = deviceType === 'desktop' ? window : refScrollMobile.current - if (block) { - if (messages != undefined && !isPaginating) { - setChatScrollHeight(block.scrollHeight) - const time = setTimeout(() => { - //@ts-ignore - block.scrollTo({ - top: block.scrollHeight, - behavior: 'smooth', // добавляем плавную прокрутку - }) - }, 350) - return () => clearTimeout(time) - } else if (messages != undefined && isPaginating) { - //@ts-ignore - block.scrollTop = block.scrollHeight - chatScrollHeight - - setChatScrollHeight(block.scrollHeight) - } - } - setIsPaginating(false) - }, [messages]) + useEffect(() => { + onFetch() + onObserverMounted() + }, [session, router.query]) return ( <Layout titlePage={botParams?.title ? botParams?.title : ''} device={deviceType}> - <Title title={botParams?.title} type={'Изображения'} linkBack={'/images'} /> + <Box sx={{ display: 'flex', alignItems: 'center', position: 'relative' }}> + <Title + rightSlot={<ImageModelsPopup model={botParams} />} + title={'Модель'} + type={'Изображения'} + linkBack={'/images'} + /> + </Box> <Box display={'flex'} justifyContent='space-between' @@ -306,49 +151,36 @@ const Images: React.FC<any> = ({ deviceType, deviceOs }) => { imageLoad={onLoadImage} sendMessage={onCreateImage} unpinImage={() => setImage(null)} - viewMobileSettings={viewMobileSettings} + viewMobileSettings={() => + setOpenFiltersMobile(true) + } /> )} </Stack> <Box className={'bg-color-block border-radius-main'} - sx={{ padding: '30px' }} + sx={{ padding: '30px', position: 'relative' }} > <ImageMessagesList isComplete={isComplete} device={deviceType} images={messages} + getMessagesPagination={fetchMessages} /> - </Box> - </> - ) : ( - <Box className={'bg-color-block border-radius-main'}> - {scrollBottom > 500 && ( <Box sx={{ position: 'absolute', - left: 0, - right: 0, - width: 'fit-content', - cursor: 'pointer', - margin: '0 auto', - bottom: '100px', - zIndex: 10, + bottom: '0', + height: '800px', + width: '100%', }} - onClick={() => { - const block = refScrollMobile.current - - block.scrollTo({ - top: block.scrollHeight, - behavior: 'smooth', // добавляем плавную прокрутку - }) - }} - > - <ArrowDownScroll /> - </Box> - )} + ref={refScrollDesktop} + ></Box> + </Box> + </> + ) : ( + <Box className={'bg-color-block border-radius-main'}> <Box - ref={refScrollMobile} sx={{ padding: '30px', height: ios @@ -356,14 +188,20 @@ const Images: React.FC<any> = ({ deviceType, deviceOs }) => { : 'calc(200px + (400 - 200) * ((100vh - 400px) / (600 - 400)))', overflowY: 'scroll', overflowX: 'hidden', + position: 'relative', }} + ref={mobileScrollContainer} className={'smallScroll'} - onScroll={handleMobileScroll} > + <div + style={{ position: 'absolute', top: 300 }} + ref={refScrollMobile} + ></div> <ImageMessagesList isComplete={isComplete} device={deviceType} images={messages} + getMessagesPagination={fetchMessages} /> </Box> <Stack alignItems='center'> @@ -379,7 +217,9 @@ const Images: React.FC<any> = ({ deviceType, deviceOs }) => { imageLoad={onLoadImage} sendMessage={onCreateImage} unpinImage={() => setImage(null)} - viewMobileSettings={viewMobileSettings} + viewMobileSettings={() => + setOpenFiltersMobile(true) + } /> )} </Stack> @@ -465,7 +305,7 @@ const Images: React.FC<any> = ({ deviceType, deviceOs }) => { /> <ResetFilters desktop={desktop} - closeDrawer={hideMobileSettings} + closeDrawer={() => setOpenFiltersMobile(false)} reset={resetParams} /> </Collapse> @@ -482,7 +322,10 @@ const Images: React.FC<any> = ({ deviceType, deviceOs }) => { )} </Stack> )} - <DrawerCustom open={openFiltersMobile} onClose={hideMobileSettings}> + <DrawerCustom + open={openFiltersMobile} + onClose={() => setOpenFiltersMobile(false)} + > <Stack spacing={1} padding={2.4}> {botParams?.versions && botParams.versions.length !== 0 ? ( <> @@ -525,7 +368,7 @@ const Images: React.FC<any> = ({ deviceType, deviceOs }) => { params={botParams?.parameters} /> <ResetFilters - closeDrawer={hideMobileSettings} + closeDrawer={() => setOpenFiltersMobile(false)} desktop={desktop} reset={resetParams} /> @@ -19,6 +19,8 @@ axios.defaults.httpsAgent = new https.Agent({ rejectUnauthorized: false, }) +axios.defaults.validateStatus = (status) => status < 500 + const inter = Raleway({ subsets: ['latin'] }) function App({ Component, pageProps: { session, ...pageProps } }: AppProps) { @@ -31,6 +31,7 @@ export default function Document() { </div> </noscript> <Main /> + <div id='modal-container'></div> <NextScript /> </body> </Html> @@ -15,7 +15,6 @@ import { useAppDispatch, useAppSelector } from '@/src/main/store/store' import styles2 from '@/src/main/styles/accountTabs.module.css' import styles from '@/src/main/styles/business.module.scss' import { ButtonUI, Input, Loader, Modal, Success, SwitchCustom, useShowData } from '@/src/shared' -import { accountApi } from '@/src/shared/api/account-endpoints' import { API_URL } from '@/src/shared/lib/constants/constants' import { getAccessToken, getTypeDevice } from '@/src/shared/lib/helpers' import { IProps } from '@/src/shared/lib/types/entities' @@ -26,6 +25,7 @@ import { Subscription } from '@/src/widgets/payment/model/payment' import { Referral } from '@/src/widgets/referral' import { DownloadModal } from '../features/business-security-download/ui/download-modal' +import { changePassword, getPaymentsPlans } from '@/src/entities/user-account' const scopes = [ { title: 'Настройки', scope: 'setting' }, @@ -56,7 +56,7 @@ export async function getServerSideProps(context: any): Promise<{ props: IAccoun //@ts-ignore const session = await getServerSession(context.req, context.res) - const plans = token ? await accountApi.getPaymentsPlans(token) : null + const plans = token ? await getPaymentsPlans(token) : null //@ts-ignore if (!session) { @@ -173,12 +173,12 @@ const Account: React.FC<IAccountProps> = ({ device, token, plans }) => { if (Object.keys(query).length === 0) addQueryParams('setting') }, []) - const changePassword = async () => { + const changePasswordCallback = async () => { if (newPassword1 !== newPassword2) { showError('Укажите одинаковые новы пароли!', true) return } - const status = await accountApi.changePassword(token, newPassword1, newPassword2, currentPassword) + const status = await changePassword(token, newPassword1, newPassword2, currentPassword) if (status === 200) { showError('Пароль успешно изменён!') @@ -195,13 +195,23 @@ const Account: React.FC<IAccountProps> = ({ device, token, plans }) => { function body() { if (type === 'regular') { return ( - <Box width='80vw' height='60vh' display='flex' alignItems='center' justifyContent='center'> + <Box + width='80vw' + height='60vh' + display='flex' + alignItems='center' + justifyContent='center' + > <ScreenForInactive /> </Box> ) } if (type === 'business_account') { - return <Box className={styles.main}>Информация о корп.аккаунте доступна только владельцу и администраторам.</Box> + return ( + <Box className={styles.main}> + Информация о корп.аккаунте доступна только владельцу и администраторам. + </Box> + ) } if (type === 'business_host') { return <BusinessHost /> @@ -233,7 +243,10 @@ const Account: React.FC<IAccountProps> = ({ device, token, plans }) => { } } - const isUserDataChange = useMemo(() => name !== first_name || last_name !== lastName, [name, lastName]) + const isUserDataChange = useMemo( + () => name !== first_name || last_name !== lastName, + [name, lastName] + ) const promocodeActivate = async () => { if (!promocode.trim()) { @@ -333,7 +346,9 @@ const Account: React.FC<IAccountProps> = ({ device, token, plans }) => { height: '100%', }} > - <Typography sx={{ fontSize: 24, fontWeight: 'bold' }}>Ваш аккаунт</Typography> + <Typography sx={{ fontSize: 24, fontWeight: 'bold' }}> + Ваш аккаунт + </Typography> <Tabs value={scope} variant='scrollable' @@ -364,10 +379,18 @@ const Account: React.FC<IAccountProps> = ({ device, token, plans }) => { {scope === 'setting' ? ( <> <Box className={styles2.wrap_block}> - <Typography className='title-block'>Основные</Typography> + <Typography className='title-block'> + Основные + </Typography> <Box className={styles2.main_block}> <Box sx={{ marginRight: 2 }}> - <Box onClick={handleDivClick} sx={{ cursor: 'pointer', position: 'relative' }}> + <Box + onClick={handleDivClick} + sx={{ + cursor: 'pointer', + position: 'relative', + }} + > <input type='file' ref={fileInputRef} @@ -383,14 +406,24 @@ const Account: React.FC<IAccountProps> = ({ device, token, plans }) => { right: '37px', }} > - {!loading || !(userInfoLoaded === 'succeeded') ? ( - <Image src={'/Edit.svg'} height={30} width={30} alt={'Image change'} /> + {!loading || + !( + userInfoLoaded === 'succeeded' + ) ? ( + <Image + src={'/Edit.svg'} + height={30} + width={30} + alt={'Image change'} + /> ) : ( <Loader /> )} </Box> <Avatar - src={profile_picture_link as string} + src={ + profile_picture_link as string + } sx={{ width: 110, height: 110, @@ -403,32 +436,66 @@ const Account: React.FC<IAccountProps> = ({ device, token, plans }) => { </Box> <Box width='100%' sx={{ marginLeft: '30px' }}> <Box sx={{ marginTop: '0px' }}> - <Typography className={styles2.input_title}>Имя</Typography> + <Typography + className={styles2.input_title} + > + Имя + </Typography> <Input inputProps={{ maxLength: 15 }} value={name} - onChange={(e) => setName(e.target.value)} + onChange={(e) => + setName(e.target.value) + } fullWidth /> </Box> <Box sx={{ marginTop: '15px' }}> - <Typography className={styles2.input_title}>Фамилия</Typography> + <Typography + className={styles2.input_title} + > + Фамилия + </Typography> <Input inputProps={{ maxLength: 15 }} value={lastName} - onChange={(e) => setLastName(e.target.value)} + onChange={(e) => + setLastName(e.target.value) + } fullWidth /> </Box> <Box sx={{ marginTop: '15px' }}> - <Box display='flex' alignItems='center' justifyContent='space-between'> - <Typography className={styles2.input_title}>Email</Typography> - <Box display='flex' alignItems='center' sx={{ marginBottom: '5px' }}> + <Box + display='flex' + alignItems='center' + justifyContent='space-between' + > + <Typography + className={ + styles2.input_title + } + > + Email + </Typography> + <Box + display='flex' + alignItems='center' + sx={{ marginBottom: '5px' }} + > <SwitchCustom - checked={is_subscribed_to_emails} + checked={ + is_subscribed_to_emails + } onChange={async () => { - await dispatch(unfollowEmail(token)) - showError('Данные изменены!') + await dispatch( + unfollowEmail( + token + ) + ) + showError( + 'Данные изменены!' + ) }} /> <Typography @@ -445,10 +512,18 @@ const Account: React.FC<IAccountProps> = ({ device, token, plans }) => { <Input value={email} fullWidth /> </Box> <Box sx={{ marginTop: '15px' }}> - <Typography className={styles2.input_title}>Никнейм</Typography> + <Typography + className={styles2.input_title} + > + Никнейм + </Typography> <Input value={username} fullWidth /> </Box> - <ButtonUI onClick={changeUserData} sx={{ marginTop: '20px' }} text={'Сохранить'} /> + <ButtonUI + onClick={changeUserData} + sx={{ marginTop: '20px' }} + text={'Сохранить'} + /> {/*referral_code.code.trim() && ( <Box marginTop={1} display='flex'> <Typography className='text'>Ваша реферальная ссылка:</Typography> @@ -468,52 +543,97 @@ const Account: React.FC<IAccountProps> = ({ device, token, plans }) => { </Box> </Box> <Stack spacing={2} className={styles2.wrap_block}> - <Typography className='title-block'>Активация промокода</Typography> + <Typography className='title-block'> + Активация промокода + </Typography> <Input placeholder='Введите ваш промокод' value={promocode} onChange={(e) => setPromocode(e.target.value)} fullWidth /> - <Button onClick={promocodeActivate} className='btn-classic'> + <Button + onClick={promocodeActivate} + className='btn-classic' + > Активировать </Button> </Stack> {!is_social && ( <Box className={styles2.wrap_block}> - <Typography className='title-block'>Изменить пароль</Typography> - <Box className={styles2.main_block} flexDirection='column'> + <Typography className='title-block'> + Изменить пароль + </Typography> + <Box + className={styles2.main_block} + flexDirection='column' + > <Box sx={{ width: '100%' }}> - <Typography className={styles2.input_title}>Текущий пароль</Typography> + <Typography + className={styles2.input_title} + > + Текущий пароль + </Typography> <Input placeholder='Укажите текущий пароль' value={currentPassword} - onChange={(e) => setCurrentPassword(e.target.value)} + onChange={(e) => + setCurrentPassword( + e.target.value + ) + } fullWidth /> </Box> - <Box display='flex' justifyContent='space-between' sx={{ width: '100%', marginTop: '15px' }}> + <Box + display='flex' + justifyContent='space-between' + sx={{ + width: '100%', + marginTop: '15px', + }} + > <Box sx={{ width: '48%' }}> - <Typography className={styles2.input_title}>Новый пароль</Typography> + <Typography + className={ + styles2.input_title + } + > + Новый пароль + </Typography> <Input placeholder='Укажите новый пароль' value={newPassword1} - onChange={(e) => setNewPassword1(e.target.value)} + onChange={(e) => + setNewPassword1( + e.target.value + ) + } fullWidth /> </Box> <Box sx={{ width: '48%' }}> - <Typography className={styles2.input_title}>Подтвердить пароль</Typography> + <Typography + className={ + styles2.input_title + } + > + Подтвердить пароль + </Typography> <Input placeholder='Укажите новый пароль ещё раз' value={newPassword2} - onChange={(e) => setNewPassword2(e.target.value)} + onChange={(e) => + setNewPassword2( + e.target.value + ) + } fullWidth /> </Box> </Box> <ButtonUI - onClick={changePassword} + onClick={changePasswordCallback} sx={{ marginTop: '20px' }} text={'Изменить пароль'} style={{ width: '150px' }} @@ -521,9 +641,17 @@ const Account: React.FC<IAccountProps> = ({ device, token, plans }) => { </Box> </Box> )} - <Stack spacing={2} className={styles2.wrap_block} sx={{ marginBottom: 5 }}> - <Typography className='title-block'>Удаление аккаунта</Typography> - <Typography className='text'>Удаление аккаунта приведет к потере всех настроек</Typography> + <Stack + spacing={2} + className={styles2.wrap_block} + sx={{ marginBottom: 5 }} + > + <Typography className='title-block'> + Удаление аккаунта + </Typography> + <Typography className='text'> + Удаление аккаунта приведет к потере всех настроек + </Typography> <ButtonUI fullWidth onClick={() => setConfirmDeleteModal(true)} @@ -537,11 +665,23 @@ const Account: React.FC<IAccountProps> = ({ device, token, plans }) => { /> </Stack> - <Modal open={confirmDeleteModal} onClose={() => setConfirmDeleteModal(false)}> + <Modal + open={confirmDeleteModal} + onClose={() => setConfirmDeleteModal(false)} + > <Stack spacing={2}> - <Typography className='title-struct'>Удаление аккаунта</Typography> - <Typography className='text'>Вы действительно хотите удалить ваш аккаунт?</Typography> - <Button onClick={deleteAccount} fullWidth sx={{ marginTop: 3 }} className='btn-danger'> + <Typography className='title-struct'> + Удаление аккаунта + </Typography> + <Typography className='text'> + Вы действительно хотите удалить ваш аккаунт? + </Typography> + <Button + onClick={deleteAccount} + fullWidth + sx={{ marginTop: 3 }} + className='btn-danger' + > Удалить </Button> </Stack> @@ -1,5 +1,14 @@ -import React, { useEffect, useState } from 'react' -import { Box, Stack, Table, TableBody, TableHead, TableRow, TextField, Typography } from '@mui/material' +import React, { useEffect, useMemo, useState } from 'react' +import { + Box, + Stack, + Table, + TableBody, + TableHead, + TableRow, + TextField, + Typography, +} from '@mui/material' import Button from '@mui/material/Button' import TableCell from '@mui/material/TableCell' import axios from 'axios' @@ -12,10 +21,12 @@ import { ApiKeyModal } from '@/src/features/api-key-modal/api-key-modal' import { Layout } from '@/src/main/layout' import { useAppSelector } from '@/src/main/store/store' import { TooltipCustom } from '@/src/shared' -import { accountApi } from '@/src/shared/api/account-endpoints' import { API_URL } from '@/src/shared/lib/constants' import styles from '@/src/shared/styles/api-keys.module.scss' import { InputStyleSmallDark, InputStyleSmallLight } from '@/src/shared/ui/input' +import { useMask } from '@react-input/mask' +import { deleteApiKey, getApiKeys } from '@/src/entities/user-account' +import { KeyRow } from '@/src/entities/api-keys/ui/key-row' const ApiKeys: React.FC = () => { const [keys, setKeys] = useState<Array<any> | null>(null) @@ -32,7 +43,7 @@ const ApiKeys: React.FC = () => { useEffect(() => { if (status === 'authenticated') { setLoading(true) - accountApi.getApiKeys(data?.access).then((res) => { + getApiKeys(data?.access).then((res) => { setKeys(res) setLoading(false) }) @@ -58,9 +69,9 @@ const ApiKeys: React.FC = () => { } setLoading(true) - const result = await accountApi.deleteApiKey(name, data?.access) + const result = await deleteApiKey(name, data?.access) if (result !== null && result === 200) { - accountApi.getApiKeys(data?.access).then((res) => { + getApiKeys(data?.access).then((res) => { setKeys(res) setLoading(false) }) @@ -78,15 +89,28 @@ const ApiKeys: React.FC = () => { <Stack spacing={2} className={styles.wrap}> <Stack spacing={2} className='pd-30 bg-color-block border-radius-main'> <Typography className='text'> - API-ключ — это инструмент, который идентифицирует пользователя или программу, запрашивающих доступ к API платформы. - С помощью ключа можно отслеживать, кто и когда пользуется API, рассчитывать оплату. + API-ключ — это инструмент, который идентифицирует пользователя + или программу, запрашивающих доступ к API платформы. С помощью + ключа можно отслеживать, кто и когда пользуется API, рассчитывать + оплату. </Typography> <Box display='flex' alignItems='center'> - <Button onClick={() => setModal(true)} disabled={disabled} className='btn-classic' sx={{ width: '130px' }}> + <Button + onClick={() => setModal(true)} + disabled={disabled} + className='btn-classic' + sx={{ width: '130px' }} + > Создать ключ </Button> <Link href='https://air-docs.readthedocs.io/' target='_blank'> - <Typography className='text' sx={{ marginLeft: '15px', color: '#8280FF !important' }}> + <Typography + className='text' + sx={{ + marginLeft: '15px', + color: '#8280FF !important', + }} + > Документация </Typography> </Link> @@ -94,16 +118,30 @@ const ApiKeys: React.FC = () => { </Stack> {keys && keys.length !== 0 ? ( - <Stack spacing={1} className='pd-30 bg-color-block border-radius-main'> + <Stack + spacing={1} + className='pd-30 bg-color-block border-radius-main' + > <Typography className='title-block'>Мои ключи</Typography> - <Table sx={{ minWidth: 650, overflowX: 'scroll' }} aria-label='simple table'> + <Table + sx={{ minWidth: 650, overflowX: 'scroll' }} + aria-label='simple table' + > <TableHead> - <TableRow sx={{ '&:last-child td, &:last-child th ': { border: 0 } }}> + <TableRow + sx={{ + '&:last-child td, &:last-child th ': { + border: 0, + }, + }} + > <TableCell align='left'>Имя</TableCell> <TableCell align='left'>Ключ</TableCell> <TableCell align='left'>Лимит токенов</TableCell> <TableCell align='left'>Создан</TableCell> - <TableCell align='left'>Действителен до</TableCell> + <TableCell align='left'> + Действителен до + </TableCell> </TableRow> </TableHead> <TableBody> @@ -111,12 +149,9 @@ const ApiKeys: React.FC = () => { keys.map((el) => { return ( <KeyRow + {...el} key={el.key} keyValue={el.key} - name={el.name} - limit={el.token_limit} - created_at={el.created_at.split('T')[0]} - expires_at={el.expires_at} deleteKey={deleteKey} /> ) @@ -125,10 +160,20 @@ const ApiKeys: React.FC = () => { </Table> </Stack> ) : ( - <Box height={140} width={'100%'} display='flex' alignItems='center' justifyContent='center'> + <Box + height={140} + width={'100%'} + display='flex' + alignItems='center' + justifyContent='center' + > <Box textAlign='center'> - <Typography className='title-vspomogatel'>У вас пока нет ключей 😞 </Typography> - <Typography className='title-main-gray'>Создайте первый ключ</Typography> + <Typography className='title-vspomogatel'> + У вас пока нет ключей 😞{' '} + </Typography> + <Typography className='title-main-gray'> + Создайте первый ключ + </Typography> </Box> </Box> )} @@ -138,82 +183,4 @@ const ApiKeys: React.FC = () => { ) } -const KeyRow = (props: any) => { - const [isCopy, setIsCopy] = useState(false) - const [limit, setLimit] = useState(props.limit) - const theme = useAppSelector((state) => state.theme.theme) - const { data: session } = useSession() - - const copy = (text: string) => { - navigator.clipboard.writeText(text) - setIsCopy(true) - setTimeout(() => setIsCopy(false), 3000) - } - - useEffect(() => { - let timeout = window.setTimeout(() => { - if (limit !== props.limit) { - axios.patch( - API_URL + '/public/api-key', - { token_limit: Number(limit), name: props.name }, - { headers: { Authorization: `Bearer ${session?.access}` } } - ) - } - }, 1000) - return () => window.clearTimeout(timeout) - }, [limit]) - - return ( - <TableRow key={props.key} sx={{ '&:last-child td, &:last-child th ': { border: 0 } }}> - <TableCell component='th' align='left' scope='row'> - {props.name} - </TableCell> - <TableCell component='th' align='left' sx={{ color: '#8280FF !important' }}> - {props.keyValue} - </TableCell> - <TableCell component='th' align='center' sx={{ color: '#8280FF !important' }} width={160}> - <TextField - value={limit} - placeholder={limit === null || limit === '' ? 'Бесконечно' : ''} - type='text' - onChange={(e) => setLimit(e.target.value)} - sx={theme === 'light' ? { ...InputStyleSmallLight } : { ...InputStyleSmallDark }} - /> - </TableCell> - <TableCell component='th' align='left'> - {props.created_at.split('T')[0]} - </TableCell> - <TableCell component='th' align='left'> - {props.expires_at !== null ? props.expires_at : 'Бессрочно'} - </TableCell> - <TableCell component='th' align='left'> - <Stack direction='row' spacing={2}> - {isCopy ? ( - <Image src={'/svg/tic.svg'} width={20} height={22} alt={'copy'} /> - ) : ( - <TooltipCustom title={'Скопировать ключ'}> - <Image - onClick={() => copy(props.keyValue)} - src={'/svg/copy.svg'} - width={20} - height={20} - style={{ cursor: 'pointer' }} - alt={'copy'} - /> - </TooltipCustom> - )} - <Image - onClick={() => props.deleteKey(props.name)} - style={{ cursor: 'pointer' }} - src='/svg/main_menu/trash.svg' - width={20} - height={20} - alt='Удалить' - /> - </Stack> - </TableCell> - </TableRow> - ) -} - export default ApiKeys @@ -6,13 +6,12 @@ import { useRouter } from 'next/router' import { Layout } from '@/src/main/layout' import { API_URL } from '@/src/shared/lib/constants/constants' +import { getTypeDevice } from '../shared/lib/helpers' export async function getServerSideProps(context: any): Promise<{ props: any }> { - const UA = context.req.headers['user-agent'] - const isMobile = Boolean(UA.match(/Android|BlackBerry|iPhone|iPad|iPod|Opera Mini|IEMobile|WPDesktop/i)) return { props: { - device: isMobile ? 'mobile' : 'desktop', + device: getTypeDevice(context), }, } } @@ -15,14 +15,12 @@ import { RegisterEmailForm } from '@/src/features/register-by-email' import { Layout } from '@/src/main/layout' import { useAppSelector } from '@/src/main/store/store' import { TDeviceProp } from '@/src/shared/lib/types/entities' +import { getTypeDevice } from '../shared/lib/helpers' export async function getServerSideProps(context: any): Promise<{ props: TDeviceProp }> { - const UA = context.req.headers['user-agent'] - const isMobile = Boolean(UA.match(/Android|BlackBerry|iPhone|iPad|iPod|Opera Mini|IEMobile|WPDesktop/i)) - return { props: { - device: isMobile ? 'mobile' : 'desktop', + device: getTypeDevice(context), }, } } @@ -67,7 +65,15 @@ const Register: React.FC<TDeviceProp> = ({ device }) => { backgroundColor: theme === 'light' ? 'white' : '#303035', }} > - {!desktop && <Image style={{ marginTop: desktop ? 0 : '20px' }} src={'/logo.svg'} alt={''} width={50} height={50} />} + {!desktop && ( + <Image + style={{ marginTop: desktop ? 0 : '20px' }} + src={'/logo.svg'} + alt={''} + width={50} + height={50} + /> + )} <Box sx={{ width: desktop ? '50%' : '100%', @@ -93,15 +99,22 @@ const Register: React.FC<TDeviceProp> = ({ device }) => { <Typography className='text'> Вы успешно зарегистрированы. <br /> - Мы отправили письмо для верификации на ваш email. В случае отсутствия, проверьте папку Спам. + Мы отправили письмо для верификации на ваш email. В + случае отсутствия, проверьте папку Спам. <br /> <br /> - Если вы регистрировались с корпоративной почты и письмо-подтверждение вам не пришло, рекомендуем + Если вы регистрировались с корпоративной почты и + письмо-подтверждение вам не пришло, рекомендуем зарегистрироваться с личной почты </Typography> </Box> ) : ( - <Stack direction='column' justifyContent='center' alignItems='center' spacing={2}> + <Stack + direction='column' + justifyContent='center' + alignItems='center' + spacing={2} + > <Typography sx={{ color: theme === 'light' ? '#181C32' : '#E1E1E1', @@ -150,7 +163,11 @@ const Register: React.FC<TDeviceProp> = ({ device }) => { /> Войти с Яндекс ID </Button> - <Typography sx={{ fontSize: '15px', color: '#A4AAB5' }}>или email</Typography> + <Typography + sx={{ fontSize: '15px', color: '#A4AAB5' }} + > + или email + </Typography> </> )} <RegisterEmailForm successLogin={onSuccessRegister} /> @@ -159,8 +176,22 @@ const Register: React.FC<TDeviceProp> = ({ device }) => { </Box> </Box> {desktop && ( - <Box sx={{ width: '50%', height: '100vh', backgroundColor: '#8280FF', overflow: 'hidden', position: 'relative' }}> - <Image style={{ position: 'absolute', top: 35, left: 35 }} src={'/svg/logo.svg'} alt={''} width={42} height={42} /> + <Box + sx={{ + width: '50%', + height: '100vh', + backgroundColor: '#8280FF', + overflow: 'hidden', + position: 'relative', + }} + > + <Image + style={{ position: 'absolute', top: 35, left: 35 }} + src={'/svg/logo.svg'} + alt={''} + width={42} + height={42} + /> <Image src={imageLink} alt={''} @@ -236,7 +236,7 @@ export function useModelImages<T>(showError: (message: string) => void, type: st const { getData, sendData } = ModelsWithImagesEndpoints - const [messages, setMessages] = useState<Message[] | null>(null) + const [messages, setMessages] = useState<Message[]>([]) const [loading, setLoading] = useState<boolean>(false) @@ -1,157 +0,0 @@ -import axios, { AxiosResponse } from 'axios' -import { User } from 'next-auth' - -import { API_URL } from '@/src/shared/lib/constants' -import { IOffer } from '@/src/widgets/payment/model/payment' - -interface IUserBalance { - current_token_balance: number -} - -interface IPayProductRequest { - uid: string - is_test: number -} - -interface IPayProductResponse { - payment_url: string -} - -interface DataForLogin { - email: string - password: string -} - -export const accountApi = { - async getPaymentsPlans(token: string): Promise<IOffer[] | null> { - try { - const { data } = await axios.get<IOffer[]>(API_URL + '/payments/plans', { - headers: { - Authorization: `Bearer ${token}`, - }, - }) - - return data - } catch (err) { - return null - } - }, - - async payProduct(token: string | null, plan: string): Promise<string | null> { - if (token === null) { - return null - } - - try { - const { data } = await axios.post<IPayProductRequest, AxiosResponse<IPayProductResponse>>( - API_URL + '/payments/plans', - { - uid: plan, - is_test: 1, - }, - { - headers: { - Authorization: `Bearer ${token}`, - }, - } - ) - return data.payment_url - } catch (err) { - return null - } - }, - - async changePassword(token: string | null, password_1: string, password_2: string, current_password: string): Promise<number> { - try { - const { status } = await axios.put( - API_URL + '/auth/reset-pass', - { - password_1, - password_2, - current_password, - }, - { - headers: { - Authorization: `Bearer ${token}`, - }, - } - ) - - return status - } catch (err) { - return 400 - } - }, - - async getApiKeys(token: string | null): Promise<any> { - try { - const { data } = await axios.get(API_URL + '/public/api-key', { - headers: { - Authorization: `Bearer ${token}`, - }, - }) - - return data - } catch (err) { - return null - } - }, - - async createApiKeys(data: any, token: string | null): Promise<any> { - try { - const { data: result } = await axios.post(API_URL + '/public/api-key', data, { - headers: { - Authorization: `Bearer ${token}`, - }, - }) - - return result - } catch (err) { - return null - } - }, - - async deleteApiKey(name: any, token: string | null): Promise<any> { - try { - const { data: result, status } = await axios.delete(API_URL + '/public/api-key', { - data: { - name, - }, - headers: { - Authorization: `Bearer ${token}`, - }, - }) - - return status - } catch (err) { - return null - } - }, - - async loginByEmail(email: string, password: string): Promise<User | null> { - try { - const { data } = await axios.post<DataForLogin, AxiosResponse<User>>(API_URL + '/auth/login', { - email, - password, - }) - - return data - } catch (err) { - return null - } - }, - - async removeSub(token?: string) { - try { - const { status } = await axios.delete(API_URL + '/payments/plans', { - headers: { - Authorization: `Bearer ${token}`, - }, - }) - - return status - } catch (err) { - return null - } - }, -} @@ -0,0 +1,12 @@ +import dynamic from 'next/dynamic'; + +type ClientOnlyProps = { children: JSX.Element }; +const ClientOnly = (props: ClientOnlyProps) => { + const { children } = props; + + return children; +}; + +export default dynamic(() => Promise.resolve(ClientOnly), { + ssr: false, +}); @@ -0,0 +1 @@ +export { default as ClientOnly } from './client-only' @@ -0,0 +1 @@ +export * from './ui' @@ -21,3 +21,16 @@ export function formatAndSortDates(inputArray: { date: string; value: number }[] return el }) } + +export function formatDate( + date: string | number | Date, + options: Intl.DateTimeFormatOptions, + lang?: string +) { + if (typeof date !== "object") { + date = new Date(typeof date === "number" ? date : Date.parse(date)) + } + + const formatter = Intl.DateTimeFormat("ru", options) + return formatter.format(date) +} @@ -1,3 +1,5 @@ export { getAccessToken } from './get-token' export { getTypeDevice } from './get-type-device' export { useConcat } from './reactive-concat' +export { c } from './string' +export * from './date-helper' @@ -0,0 +1,3 @@ +export function c(...args: Array<string | null | undefined | boolean>) { + return args.filter(Boolean).join(" ") +} @@ -1,13 +1,11 @@ import { useAppSelector } from '@/src/main/store/store' +import { DeviceOs } from '../types/entities' -interface IThemeAndDevice { - theme: 'dark' | 'light' - desktop: boolean -} -export const useThemeAndDevice = (device?: 'desktop' | 'mobile'): IThemeAndDevice => { +export const useThemeAndDevice = (device?: 'desktop' | 'mobile', deviceOs?: DeviceOs) => { const theme = useAppSelector((state) => state.theme.theme) const desktop = device === 'desktop' + const ios = deviceOs === 'ios' - return { theme, desktop } + return { theme, desktop, ios } } @@ -1,44 +1,44 @@ .main { - width: 100%; - display: flex; - justify-content: flex-start; - gap: 20px; - margin-bottom: 24px; + width: 100%; + display: flex; + justify-content: flex-start; + gap: 20px; + margin-bottom: 24px; - .chatWindow { - width: 70%; - } + .chatWindow { + width: 70%; + } - .settings { - width: 25%; - } + .settings { + width: 25%; + } - @media (max-width:768px) { - width: 100%; - display: flex; - - flex-direction: column-reverse; - - .chatWindow { - width: 100%; - } + @media (max-width: 768px) { + width: 100%; + display: flex; - .settings { - width: 100%; - } - } + flex-direction: column-reverse; + + .chatWindow { + width: 100%; + } + + .settings { + width: 100%; + } + } } -.wrapModelTitle{ - margin-bottom: 20px; - margin-top: 15px; - @media (max-width:768px) { - margin: 0; - margin-top: 15px; - } - .bigTitle { - @media (max-width:768px) { - display: none; - overflow: hidden; - } - } +.wrapModelTitle { + margin-bottom: 20px; + margin-top: 15px; + @media (max-width: 768px) { + margin: 0; + margin-top: 15px; + } +} + +.bigTitle { + @media (max-width: 768px) { + font-size: 26px !important; + } } @@ -1,4 +1,5 @@ export { emailOptions, onlyNumbersOption } from './lib/constants/hook-form-options' +export { useConcat } from './lib/helpers' export { translateTypeModel } from './lib/helpers/model-helpers' export { useShowData } from './lib/hooks' export { useAutoLoad } from './lib/hooks/use-auto-load' @@ -8,14 +9,15 @@ export { ButtonUI } from './ui/button/button' export { ButtonGray } from './ui/button/button-gray' export { CheckBoxAgreeWithRules } from './ui/check-box-agree-with-rules' export { DalleFilterMenu } from './ui/dalle-filter-menu' +export * from './client-only' export * from './ui/drawer' export { Error } from './ui/error' +export * from './lib/helpers' export * from './ui/graphics-main-page' export { Input, InputStyleDark, InputStyleLight } from './ui/input' export { InputImagesModels } from './ui/input-images-models/input-images-models' export { Loader } from './ui/loader/loader' export type { ModalProps } from './ui/modal/modal' -export { useConcat } from './lib/helpers' export { Modal } from './ui/modal/modal' export { NotificationMenu } from './ui/notification-menu' export { RandomImage } from './ui/random-image' @@ -0,0 +1 @@ +export * from './use-image-models-popup' \ No newline at end of file @@ -0,0 +1,27 @@ +import { useImagesBots } from '@/src/entities/model-entity' +import { useMemo, useState } from 'react' + +export function useImageModelsPopup() { + const { bots, fetchBots, setBots } = useImagesBots() + + const [popupOpen, setPopupOpen] = useState(false) + + const averageTokenCost = useMemo(() => { + if (!bots.length) return 0 + + const prices = bots.filter((item) => item.actual_stat).map((item) => item.actual_stat) + + const sum = prices.reduce((acc, item) => acc + Number(item.tokens_cost), 0) + + return sum / prices.length + }, [bots]) + + return { + bots, + setBots, + fetchBots, + averageTokenCost, + popupOpen, + setPopupOpen + } +} @@ -0,0 +1 @@ +export type CostZones = 30 | 60 | 90 @@ -0,0 +1 @@ +export * from './cost' \ No newline at end of file @@ -0,0 +1,64 @@ +.container { + position: relative; + + @media (max-width: 768px) { + position: static; + } +} + +.select { + padding: 0px 18px; + background: rgba(#8280ff, 0.1); + border-radius: 10px; + display: flex; + align-items: center; + width: max-content; + gap: 10px; + cursor: pointer; + + &__arrow { + padding-top: 5px; + @media (max-width: 768px) { + width: 15px; + height: 15px; + } + } + + &__title { + color: var(--air-color); + font-size: 35px; + font-weight: 600; + + @media (max-width: 768px) { + font-size: 26px !important; + } + } +} + +.popup { + background: var(--new-ui-main-color); + border-radius: 15px; + width: max-content; + position: absolute; + z-index: 105; + transform: translateY(20px); + visibility: hidden; + opacity: 0; + transition: all 0.3s ease-in-out; + border: 1px solid #40404e4b; + max-height: 70vh; + overflow-y: scroll; + + + @media (max-width: 768px) { + left: 0; + right: 0; + width: fit-content; + } + + &_open { + visibility: visible; + opacity: 1; + transform: translateY(0); + } +} @@ -0,0 +1,56 @@ +import { Typography } from '@mui/material' +import React, { useEffect } from 'react' +import styles from './image-models-popup.module.scss' +import { c } from '@/src/shared/lib/helpers' +import { useImageModelsPopup } from '../model' +import { ImagePopupModel } from './image-popup-model' +import { IModel } from '@/src/entities/model-entity' +import { useRouter } from 'next/router' +import ArrowDownSvg from '@/src/assets/svg/arrow-down.svg?react' + +interface ImageModelsPopupProps { + model: IModel | null +} + +export const ImageModelsPopup = ({ model }: ImageModelsPopupProps) => { + const { bots, fetchBots, averageTokenCost, popupOpen, setPopupOpen } = useImageModelsPopup() + + const { push } = useRouter() + + useEffect(() => { + fetchBots() + }, []) + + return ( + <> + <div className={c(styles.container)}> + <div onClick={() => setPopupOpen((prev) => !prev)} className={c(styles.select)}> + <Typography className={styles.select__title}> + {model ? model.title : 'Загрузка...'} + </Typography> + <ArrowDownSvg + className={styles.select__arrow} + style={{ transform: popupOpen ? 'rotate(180deg)' : 'rotate(0deg)' }} + fill='#8280FF' + width={20} + height={20} + /> + </div> + <div className={c(styles.popup, popupOpen ? styles.popup_open : null)}> + {bots.map((item) => ( + <ImagePopupModel + key={item.uid} + averageTokenCost={averageTokenCost} + active={model ? item.slug === model.slug : false} + setActive={(slug) => { + setPopupOpen(false) + push(`/images/${slug}`) + }} + {...item} + /> + ))} + </div> + </div> + </> + ) +} @@ -0,0 +1,56 @@ +.model { + // font-family: Inter; + padding: 15px 20px; + cursor: pointer; + display: flex; + align-items: center; + gap: 22px; + justify-content: space-between; + max-width: 500px; + transition: background-color 0.3s ease; + + &:hover{ + background-color: rgba($color: #a4aab5, $alpha: 0.1); + } + + @media (max-width: 768px) { + // max-width: calc(100% - 100px) !important; + width: 100%; + // max-width: unset; + } + &__title { + font-weight: 600; + font-size: 18px; + margin-bottom: 4px; + } + + &__description { + font-size: 14px; + color: #a4aab5; + margin-bottom: 7px; + } + + &__badges { + display: flex; + gap: 10px; + } + + &__badge { + padding: 4px 6px; + font-size: 13px; + display: flex; + align-items: center; + gap: 2px; + background-color: rgba($color: #a4aab5, $alpha: 0.1); + color: white; + font-weight: 500; + border-radius: 5px; + } + + &__time { + gap: 6px; + color: var(--new-ui-gray-color); + } + &__cost { + } +} @@ -0,0 +1,82 @@ +import { IShortModel } from '@/src/entities/model-entity' +import React, { useMemo } from 'react' + +import styles from './image-popup-model.module.scss' +import { c, formatDate } from '@/src/shared/lib/helpers' +import SuccessRoundedSvg from '@/src/assets/svg/success-rounded.svg?react' +import ClockSvg from '@/src/assets/svg/clock.svg?react' +import LightningSvg from '@/src/assets/svg/lightning.svg?react' +import { CostZones } from '../types' + +interface ImagePopupModelProps extends IShortModel { + active: boolean + setActive: (slug: string) => void + averageTokenCost: number +} + +export interface CostZone { + remainder: number + color: string +} + +export const costZoneColors: CostZone[] = [ + { remainder: -20, color: '#1EB034' }, + { remainder: 20, color: '#B0891E' }, + { remainder: 1000, color: '#B01E1E' }, +] + +export const ImagePopupModel = ({ + uid, + title, + description, + slug, + image, + actual_stat, + active, + averageTokenCost, + setActive, +}: ImagePopupModelProps) => { + // жесткий костыль))) - временный + const formatedTime = useMemo(() => { + if (!actual_stat) return null + + const seconds = Number(actual_stat.generation_time.split(':')[2]) + + const range = Math.ceil(seconds - seconds / 3) + + return `${range} - ${seconds} сек.` + }, [actual_stat]) + + const costZone = useMemo(() => { + if (!actual_stat) return null + + const remainder = Number(actual_stat.tokens_cost) - averageTokenCost + + return costZoneColors.find((item) => item.remainder > remainder) + }, [actual_stat, averageTokenCost]) + + return ( + <div onClick={() => setActive(slug)} className={styles.model}> + <div className={styles.model__content}> + <p className={styles.model__title}>{title}</p> + <p className={styles.model__description}>{description}</p> + + {actual_stat && ( + <div className={styles.model__badges}> + <div className={c(styles.model__badge, styles.model__time)}> + <ClockSvg width={16} height={16} /> + <span>{formatedTime}</span> + </div> + + <div style={{ color: costZone?.color }} className={c(styles.model__badge, styles.model__cost)}> + <LightningSvg fill={costZone?.color} width={10} height={18} /> + {Number(actual_stat.tokens_cost)} + </div> + </div> + )} + </div> + + {active && <SuccessRoundedSvg width={24} height={24} />} + </div> + ) +} @@ -0,0 +1,2 @@ +export * from './image-models-popup' +export * from './image-popup-model' \ No newline at end of file @@ -0,0 +1 @@ +export * from './types' \ No newline at end of file @@ -0,0 +1,14 @@ +import { Message, MessageSend } from '@/src/entities/message' +import { API_URL } from '@/src/shared/lib/constants' +import { IMessageRequest } from '@/src/shared/lib/types/types-gpt' +import axios, { AxiosError, AxiosResponse } from 'axios' + +export async function getImagesGalery(token?: string, offset?: number, limit = 10) { + return await axios.get<Message>(API_URL + `/media/gallery/images?limit=${limit}&offset=${offset}`, { + headers: { + Authorization: `Bearer ${token}`, + }, + }) +} + + @@ -0,0 +1 @@ +export * from './image-messages.routes' \ No newline at end of file @@ -0,0 +1,10 @@ +import { MessageSend } from '@/src/entities/message' + +export function formDataHelper(file: File, dataForSend: MessageSend<any>): FormData { + const FD = new FormData() + FD.append('file', file) + FD.append('info', JSON.stringify(dataForSend.info)) + FD.append('content', JSON.stringify(dataForSend.content)) + + return FD +} @@ -0,0 +1,3 @@ +export * from './date-from-string' +export * from './day-months-string' +export * from './form-data' \ No newline at end of file @@ -0,0 +1 @@ +export * from './use-images-pagination' \ No newline at end of file @@ -0,0 +1,72 @@ +import { getUserBalance } from '@/src/entities/balance' +import { Message, MessageSend, sendImage } from '@/src/entities/message' +import { useAppDispatch } from '@/src/main/store/store' +import { useSession } from 'next-auth/react' +import { useState, useRef, useEffect, useCallback } from 'react' +import { getImagesGalery } from '../api' +import { Device } from '@/src/shared/lib/types/entities' +import { formDataHelper } from '../lib' + +export function useImagesPagination( + showError: (message: string) => void, + type: string, + device: Device +) { + const { data } = useSession() + + const [messages, setMessages] = useState<Message[]>([]) + + const [loading, setLoading] = useState<boolean>(false) + + const [offset, setOffset] = useState<number>(0) + + const dispatch = useAppDispatch() + + async function onFetch() { + if (!data) return + + setLoading(true) + console.log(offset) + const { data: answer, ...response } = await getImagesGalery(data.access, offset) + setLoading(false) + + if (response.status >= 400) return showError('Ошибка загрузки чата') + + if (!Array.isArray(answer)) return showError('Ошибка загрузки чата') + + if (device === 'desktop') { + setMessages(answer) + return setOffset(answer.length) + } + + setMessages(answer.reverse()) + setOffset(answer.length) + } + + useEffect(() => { + onFetch() + }, [data, type]) + + const getMessagesPagination = async (device: 'mobile' | 'desktop') => { + if (!data) return + + setLoading(true) + const { data: answer, ...response } = await getImagesGalery(data.access, offset) + setLoading(false) + + if (response.status >= 400) return showError('Ошибка загрузки чата') + + if (!Array.isArray(answer)) return showError('Ошибка загрузки сообщений') + + if (device === 'mobile') { + const newMessages = answer.reverse() + setMessages([...newMessages, ...messages]) + return setOffset((prev) => prev + answer.length) + } + + setMessages([...messages, ...answer]) + setOffset((prev) => prev + answer.length) + } + + return { messages, loading, getMessagesPagination } +} @@ -0,0 +1,26 @@ +import { Message } from '@/src/shared/lib/types/model' +import { useMemo, useState } from 'react' + +export const useMessages = (messages: Message[]) => { + const [chosenImage, setChosenImage] = useState<string | null>(null) + + const [loaded, setLoaded] = useState(false) + + // типизация ну супер кривая))) + const computedLibraryImages = useMemo<string[]>(() => { + return messages + .map((el) => { + if (el.file && (el.file as any).includes('.zip')) return null + return el.file as string + }) + .filter((el) => el !== null) as string[] + }, [messages]) + + return { + chosenImage, + setChosenImage, + loaded, + setLoaded, + computedLibraryImages, + } +} @@ -1,11 +1,14 @@ -import React, { memo, useEffect } from 'react' import { Box, CircularProgress } from '@mui/material' +import React, { memo, useEffect, useMemo, useRef, useState } from 'react' import { useSession } from 'next-auth/react' import { Message } from '@/src/shared/lib/types/model' import { ArrowDownScroll } from '@/src/shared/ui/icon-components/scroll-down-arrow' -import { IsNextDay } from '@/src/widgets/messages/is-next-day' -import { PreviewView } from '@/src/widgets/messages/message-components/preview-view' +import { ImageModal } from '@/src/features/image-modal' +import { ClientOnly } from '@/src/shared' +import { createPortal } from 'react-dom' +import { IsNextDay } from './is-next-day' +import { PreviewView } from './preview-view' interface IMessagesList { device: 'mobile' | 'desktop' @@ -22,7 +25,18 @@ interface IMessagesList { } export const ChatMessagesList: React.FC<IMessagesList> = memo( - ({ messageResponse, onLoadImage, setResendValue, modelTitle, device, modelType, mode, getMessagesPagination, deleteMessage, loading }) => { + ({ + messageResponse, + onLoadImage, + setResendValue, + modelTitle, + device, + modelType, + mode, + getMessagesPagination, + deleteMessage, + loading, + }) => { const paginationScroll = React.useRef<any>() const [isPaginating, setIsPaginating] = React.useState(false) const [chatScrollHeight, setChatScrollHeight] = React.useState(0) @@ -30,13 +44,27 @@ export const ChatMessagesList: React.FC<IMessagesList> = memo( const desktop = device === 'desktop' const { status } = useSession() - const [isNewMessage, setIsNewMessage] = React.useState(false) + const [modal, setModal] = useState<boolean>(false) + const [isNewMessage, setIsNewMessage] = useState(false) + + const [currentSrc, setCurrentSrc] = useState<string | null>(null) + + const onlyImageMessage = useMemo(() => { + if (!messageResponse) return [] + console.log(messageResponse) + return messageResponse + .map((item) => item.file) + .filter( + (item) => item !== null && !(item as string).includes('.zip') + ) as string[] + }, [messageResponse]) React.useEffect(() => { const block = paginationScroll.current if (messageResponse != undefined && !isPaginating) { setChatScrollHeight(paginationScroll.current.scrollHeight) + const time = setTimeout(() => { if (block) { //@ts-ignore @@ -62,8 +90,11 @@ export const ChatMessagesList: React.FC<IMessagesList> = memo( }, [messageResponse]) const handleScroll = () => { - console.log(paginationScroll.current?.scrollHeight - paginationScroll.current?.scrollTop - paginationScroll.current?.clientHeight) - setScrollBottom(paginationScroll.current?.scrollHeight - paginationScroll.current?.scrollTop - paginationScroll.current?.clientHeight) + setScrollBottom( + paginationScroll.current?.scrollHeight - + paginationScroll.current?.scrollTop - + paginationScroll.current?.clientHeight + ) if (paginationScroll.current && messageResponse?.length !== 0) { const { scrollTop, scrollHeight, clientHeight } = paginationScroll.current @@ -77,89 +108,112 @@ export const ChatMessagesList: React.FC<IMessagesList> = memo( } return ( - <Box - sx={{ - overflowY: 'scroll', - overflowX: 'hidden', - '::-webkit-scrollbar': { - display: 'none', - }, - paddingRight: desktop ? 2 : 0, - paddingLeft: desktop ? 1 : 0, - }} - ref={paginationScroll} - onScroll={handleScroll} - > - {scrollBottom > 500 && ( - <Box - sx={{ - position: 'absolute', - left: 0, - right: 0, - width: 'fit-content', - cursor: 'pointer', - margin: '0 auto', - bottom: '100px', - zIndex: 10, - }} - onClick={() => { - //@ts-ignore - const block = paginationScroll.current - - block.scrollTo({ - top: block.scrollHeight, - behavior: 'smooth', // добавляем плавную прокрутку - }) - }} - > - <ArrowDownScroll /> - </Box> - )} - - {loading && ( - <Box - sx={{ - position: 'absolute', - left: 0, - right: 0, - width: 'fit-content', - margin: '0 auto', - top: '10px', - zIndex: 10, - }} - > - <CircularProgress - size={18} - thickness={3} + <> + <ClientOnly> + {createPortal( + <ImageModal + modal={modal} + current={currentSrc} + setModal={setModal} + onSlideFalse={async () => { + getMessagesPagination && (await getMessagesPagination()) + }} + images={onlyImageMessage} + />, + document.getElementById('modal-container')! + )} + </ClientOnly> + + <Box + sx={{ + overflowY: 'scroll', + overflowX: 'hidden', + '::-webkit-scrollbar': { + display: 'none', + }, + paddingRight: desktop ? 2 : 0, + paddingLeft: desktop ? 1 : 0, + }} + ref={paginationScroll} + onScroll={handleScroll} + > + {scrollBottom > 500 && ( + <Box sx={{ - color: '#7F7DF3', + position: 'absolute', + left: 0, + right: 0, + width: 'fit-content', + cursor: 'pointer', + margin: '0 auto', + bottom: '100px', + zIndex: 10, }} - /> - </Box> - )} - - {messageResponse?.length === 0 && status === 'authenticated' ? ( - <>{desktop && modelType !== 'deepl' && <PreviewView setValue={setResendValue} />}</> - ) : ( - messageResponse?.map((message, idx) => { - return ( - <IsNextDay - onLoadImage={onLoadImage} - setResendValue={setResendValue} - key={idx} - message={message} - index={idx - 1} - messageResponse={messageResponse} - isNewMessage={isNewMessage} - modelType={modelType} - modelTitle={modelTitle} - deleteMessage={deleteMessage} - device={device} + onClick={() => { + //@ts-ignore + const block = paginationScroll.current + + block.scrollTo({ + top: block.scrollHeight, + behavior: 'smooth', // добавляем плавную прокрутку + }) + }} + > + <ArrowDownScroll /> + </Box> + )} + + {loading && ( + <Box + sx={{ + position: 'absolute', + left: 0, + right: 0, + width: 'fit-content', + margin: '0 auto', + top: '10px', + zIndex: 10, + }} + > + <CircularProgress + size={18} + thickness={3} + sx={{ + color: '#7F7DF3', + }} /> - ) - }) - )} - </Box> + </Box> + )} + + {messageResponse?.length === 0 && status === 'authenticated' ? ( + <> + {desktop && modelType !== 'deepl' && ( + <PreviewView setValue={setResendValue} /> + )} + </> + ) : ( + messageResponse?.map((message, idx) => { + return ( + <IsNextDay + onLoadImage={onLoadImage} + setResendValue={setResendValue} + key={idx} + message={message} + index={idx - 1} + setCurrentSrc={setCurrentSrc} + setModal={setModal} + messageResponse={messageResponse} + isNewMessage={isNewMessage} + modelType={modelType} + modelTitle={modelTitle} + deleteMessage={deleteMessage} + device={device} + /> + ) + }) + )} + </Box> + </> ) } ) @@ -0,0 +1,47 @@ +.model { + position: absolute; + top: 12px; + left: 12px; + z-index: 100; + background-color: rgba($color: #151518, $alpha: 0.5); + font-size: 13px; + padding: 2px 8px; + border-radius: 10px; + font-weight: 500; + color: #fff; +} +.wrap { + display: flex; + overflow: hidden; + width: 100%; + flex-wrap: wrap; + gap: 20px; + @media (max-width: 766px) { + height: 70vh; + width: 100%; + overflow-y: scroll; + text-align: center; + justify-content: center; + } + div { + margin-right: 10px; + @media (max-width: 766px) { + width: 100%; + } + + .image { + margin: 10px auto; + width: 250px; + height: 250px; + border-radius: 15px !important; + } + + @media (max-width: 1700px) { + } + + @media (max-width: 766px) { + text-align: center; + width: 80%; + } + } +} @@ -4,27 +4,30 @@ import Box from '@mui/material/Box' import Image from 'next/image' import Link from 'next/link' -import FullScreenModal from '@/src/features/image-modal/full-screen-modal' +import { ImageModal } from '@/src/features/image-modal' import { useAppSelector } from '@/src/main/store/store' -import { Success, TooltipCustom, useShowData } from '@/src/shared' +import { ClientOnly, Success, TooltipCustom, useShowData } from '@/src/shared' import { useAutoScroll } from '@/src/shared/lib/hooks' -import { Message } from '@/src/shared/lib/types/model' import styles from './image-messages-list.module.scss' +import { createPortal } from 'react-dom' +import { useMessages } from '../model/use-messages' +import { Message } from '@/src/entities/message' interface MessagesList { device: 'mobile' | 'desktop' - images: Message[] | null + images: Message[] + getMessagesPagination?: () => Promise<void> isComplete: boolean } -export const ImageMessagesList: React.FC<MessagesList> = memo(({ device, images }) => { +export const ImageMessagesList: React.FC<MessagesList> = memo(({ device, images, getMessagesPagination }) => { const { error, showError, isError } = useShowData() const [modal, setModal] = useState<boolean>(false) - const [chosenImage, setChosenImage] = useState<string>('') const theme = useAppSelector((state) => state.theme.theme) const [iconsMenu, setIconsMenu] = React.useState<string>('') - const [loaded, setLoaded] = useState(false) + + const { chosenImage, setChosenImage, loaded, setLoaded, computedLibraryImages } = useMessages(images) const downloadFile = (url: string | null, content: string | undefined) => { if (url) { @@ -56,11 +59,25 @@ export const ImageMessagesList: React.FC<MessagesList> = memo(({ device, images } } - const ImageIcons = ({ uid, url, content }: { uid: string; url: string | null; content: string | undefined }) => { + const ImageIcons = ({ + uid, + url, + content, + }: { + uid: string + url: string | null + content: string | undefined + }) => { return ( <> <svg - style={{ position: 'absolute', top: '10px', right: '10px', cursor: 'pointer', zIndex: '5' }} + style={{ + position: 'absolute', + top: '10px', + right: '10px', + cursor: 'pointer', + zIndex: '5', + }} onClick={() => toggleMenu(uid)} width='35' height='35' @@ -68,7 +85,12 @@ export const ImageMessagesList: React.FC<MessagesList> = memo(({ device, images fill='none' xmlns='http://www.w3.org/2000/svg' > - <circle cx='17.5' cy='17.5' r='17.5' fill={theme === 'dark' ? '#303035' : '#FFFFFF'} /> + <circle + cx='17.5' + cy='17.5' + r='17.5' + fill={theme === 'dark' ? '#303035' : '#FFFFFF'} + /> <path fillRule='evenodd' clipRule='evenodd' @@ -140,7 +162,19 @@ export const ImageMessagesList: React.FC<MessagesList> = memo(({ device, images return ( <> - <FullScreenModal modal={modal} setModal={setModal} image={chosenImage} /> + <ClientOnly> + {createPortal( + <ImageModal + images={computedLibraryImages} + modal={modal} + onSlideFalse={() => getMessagesPagination && getMessagesPagination()} + setModal={setModal} + reverse={device === 'desktop'} + current={chosenImage} + />, + document.getElementById('modal-container')! + )} + </ClientOnly> <Success message={error} open={Boolean(error)} isError={isError} /> <Box className={'mt-15'}> <Typography @@ -167,13 +201,13 @@ export const ImageMessagesList: React.FC<MessagesList> = memo(({ device, images > {images?.length !== 0 && Array.isArray(images) && - images.map((message) => { + images.map((message, index) => { //@ts-ignore const isZip = message.file && message.file.includes('.zip') //@ts-ignore const isSvg = message.file && message.file.includes('.svg') return ( - <Box key={message.uid} sx={{ position: 'relative' }}> + <Box key={index} sx={{ position: 'relative' }}> {isZip ? ( <Box display='flex' @@ -182,14 +216,29 @@ export const ImageMessagesList: React.FC<MessagesList> = memo(({ device, images sx={{ border: '1px solid #8280FF', borderRadius: 5, - width: device === 'desktop' ? '250px' : '291px', - height: device === 'desktop' ? '284px' : '291px', + width: + device === 'desktop' + ? '250px' + : '291px', + height: + device === 'desktop' + ? '284px' + : '291px', }} > <Box textAlign='center'> - <Typography>Эта генерация является архивом</Typography> - <Typography color='#8280FF' marginTop={2}> - <Link href={message.file!.toString()}>Скачать</Link> + <Typography> + Эта генерация является архивом + </Typography> + <Typography + color='#8280FF' + marginTop={2} + > + <Link + href={message.file!.toString()} + > + Скачать + </Link> </Typography> </Box> </Box> @@ -198,47 +247,74 @@ export const ImageMessagesList: React.FC<MessagesList> = memo(({ device, images <Box sx={{ cursor: 'pointer', - width: device === 'desktop' ? '250px' : '291px', - height: device === 'desktop' ? '250px' : '291px', + width: + device === 'desktop' + ? '250px' + : '291px', + height: + device === 'desktop' + ? '250px' + : '291px', position: 'relative', overflow: 'hidden', borderRadius: '15px', }} > + <span className={styles.model}> + {message.model} + </span> {!isSvg ? ( <> <Image className={styles.image} onClick={() => { - setChosenImage(message.file as string) + setChosenImage( + message.file as string + ) setModal(true) }} - onLoadingComplete={() => setLoaded(true)} + onLoadingComplete={() => + setLoaded(true) + } style={{ - position: 'relative', + position: + 'relative', zIndex: '2', borderRadius: 15, width: '100%', height: '100%', - opacity: loaded ? '100%' : '0%', + opacity: loaded + ? '100%' + : '0%', userSelect: 'none', - objectFit: 'contain', + objectFit: + 'contain', }} width={500} height={500} - src={(message.file as unknown as string) || ''} - alt={'К сожалению, изображение не загрузилось'} + src={ + (message.file as unknown as string) || + '' + } + alt={ + 'К сожалению, изображение не загрузилось' + } /> <Image className={styles.image} - onLoadingComplete={() => setLoaded(true)} + onLoadingComplete={() => + setLoaded(true) + } style={{ - position: 'absolute', + position: + 'absolute', zIndex: '1', borderRadius: 15, width: '100%', height: '100%', - opacity: loaded ? '100%' : '0%', + opacity: loaded + ? '100%' + : '0%', userSelect: 'none', objectFit: 'cover', right: 0, @@ -246,7 +322,10 @@ export const ImageMessagesList: React.FC<MessagesList> = memo(({ device, images }} width={10} height={10} - src={(message.file as unknown as string) || ''} + src={ + (message.file as unknown as string) || + '' + } alt='' /> </> @@ -255,19 +334,26 @@ export const ImageMessagesList: React.FC<MessagesList> = memo(({ device, images <img className={styles.image} onClick={() => { - setChosenImage(message.file as string) + setChosenImage( + message.file as string + ) setModal(true) }} style={{ - position: 'relative', + position: + 'relative', zIndex: '2', borderRadius: 15, width: '100%', height: '100%', userSelect: 'none', - objectFit: 'contain', + objectFit: + 'contain', }} - src={(message.file as unknown as string) || ''} + src={ + (message.file as unknown as string) || + '' + } alt='К сожалению, изображение не загрузилось' /> <img @@ -275,18 +361,24 @@ export const ImageMessagesList: React.FC<MessagesList> = memo(({ device, images width='10px' height='10px' style={{ - position: 'absolute', + position: + 'absolute', zIndex: '1', borderRadius: 15, width: '100%', height: '100%', - opacity: loaded ? '100%' : '0%', + opacity: loaded + ? '100%' + : '0%', userSelect: 'none', objectFit: 'cover', right: 0, filter: 'blur(10px) brightness(0.7)', }} - src={(message.file as unknown as string) || ''} + src={ + (message.file as unknown as string) || + '' + } alt='К сожалению, изображение не загрузилось' /> </> @@ -295,14 +387,19 @@ export const ImageMessagesList: React.FC<MessagesList> = memo(({ device, images {!loaded && ( <Skeleton sx={{ - background: theme === 'dark' ? '#2d2d2f' : '#EFF0F2', + background: + theme === 'dark' + ? '#2d2d2f' + : '#EFF0F2', position: 'absolute', top: '0', left: '0', zIndex: '3', borderRadius: 5, - width: device === 'desktop' ? '250px' : '100%', - height: device === 'desktop' ? '250px' : '100%', + width: + device === 'desktop' ? '250px' : '100%', + height: + device === 'desktop' ? '250px' : '100%', }} variant='rectangular' /> @@ -311,29 +408,47 @@ export const ImageMessagesList: React.FC<MessagesList> = memo(({ device, images key={message.uid} uid={message.uid} content={message.content} - url={message.file ? message.file.toString() : null} + url={ + message.file + ? message.file.toString() + : null + } /> </Box> <TooltipCustom key={message.uid} placement='right' - title={message.content.length > 30 ? message.content : ''} + title={ + message.content.length > 30 + ? message.content + : '' + } > <Typography sx={{ fontSize: '15px', - maxWidth: device === 'desktop' ? '250px' : '100%', + maxWidth: + device === 'desktop' + ? '250px' + : '100%', marginTop: '12px', - color: theme === 'dark' ? '#A4AAB5' : '#555556', + color: + theme === 'dark' + ? '#A4AAB5' + : '#555556', }} > {!loaded ? '' : message.content.length > 0 - ? message?.content.replaceAll('"', '').slice(0, 30) + ? message?.content + .replaceAll('"', '') + .slice(0, 30) : 'описание отсутствует'} - {message?.content.length > 30 && loaded && '...'} + {message?.content.length > 30 && + loaded && + '...'} </Typography> </TooltipCustom> </Box> @@ -0,0 +1,7 @@ +export * from './chat-messages-list' +export * from './image-messages-list' +export * from './answer-wrap' +export * from './bot-message' +export * from './preview-view' +export * from './user-message' +export * from './is-next-day' \ No newline at end of file @@ -1,10 +1,10 @@ -import React, { memo } from 'react' +import React, { memo, useMemo } from 'react' import { Box, Typography } from '@mui/material' import { Message } from '@/src/shared/lib/types/model' -import { getDateFromString } from '@/src/widgets/messages/lib/getDateFromString' -import { getDayMontsString } from '@/src/widgets/messages/lib/getDayMontsString' -import { UserMessage } from '@/src/widgets/messages/message-components/user-message' +import { getDateFromString } from '@/src/widgets/messages/lib/date-from-string' +import { getDayMontsString } from '@/src/widgets/messages/lib/day-months-string' +import { UserMessage } from '@/src/widgets/messages/ui/user-message' interface IProps { message: Message @@ -12,8 +12,9 @@ interface IProps { messageResponse: Message[] | null modelTitle: string | undefined deleteMessage?: (message_uid: string) => void + setCurrentSrc: (value: string) => void isNewMessage: boolean - // clickPreview: (text: string) => void + setModal: (value: boolean) => void modelType: string device: 'mobile' | 'desktop' setResendValue: (value: string) => void @@ -21,7 +22,20 @@ interface IProps { } export const IsNextDay = memo( - ({ message, index, onLoadImage, messageResponse, modelTitle, deleteMessage, isNewMessage, modelType, device, setResendValue }: IProps) => { + ({ + message, + index, + onLoadImage, + messageResponse, + modelTitle, + deleteMessage, + setCurrentSrc, + setModal, + isNewMessage, + modelType, + device, + setResendValue, + }: IProps) => { if (messageResponse && message.created_at) { const date = getDateFromString(message.created_at) const prevDate = getDateFromString(messageResponse[index]?.created_at) @@ -51,7 +65,9 @@ export const IsNextDay = memo( setResendValue={setResendValue} modelTitle={modelTitle} deleteMessage={deleteMessage} + setCurrentSrc={setCurrentSrc} key={message.uid} + setModal={setModal} message={message} isNewMessage={isNewMessage} modelType={modelType} @@ -81,9 +97,11 @@ export const IsNextDay = memo( </Typography> </Box> <UserMessage + setModal={setModal} onLoadImage={onLoadImage} setResendValue={setResendValue} modelTitle={modelTitle} + setCurrentSrc={setCurrentSrc} deleteMessage={deleteMessage} key={message.uid} message={message} @@ -101,7 +119,9 @@ export const IsNextDay = memo( setResendValue={setResendValue} modelTitle={modelTitle} deleteMessage={deleteMessage} + setCurrentSrc={setCurrentSrc} key={message.uid} + setModal={setModal} message={message} isNewMessage={isNewMessage} modelType={modelType} @@ -1,13 +1,13 @@ -import React, { useState } from 'react' +import React, { useMemo, useState } from 'react' import { Box, Menu, MenuItem, Skeleton, Slide, Typography } from '@mui/material' import Stack from '@mui/material/Stack' import Image from 'next/image' -import FullScreenModal from '@/src/features/image-modal/full-screen-modal' +import { ImageModal } from '@/src/features/image-modal' import { useAppSelector } from '@/src/main/store/store' import { TooltipCustom } from '@/src/shared' import { Message } from '@/src/shared/lib/types/model' -import { BotMessage } from '@/src/widgets/messages/message-components/bot-message' +import { BotMessage } from '@/src/widgets/messages/ui/bot-message' interface IMessagesList { // messageResponse: Message[] | null @@ -18,18 +18,19 @@ interface IMessagesList { device: 'mobile' | 'desktop' modelType: string isNewMessage: boolean + setCurrentSrc: (value: string) => void message: Message + setModal: (value: boolean) => void deleteMessage?: (message_uid: string) => void modelTitle: string | undefined setResendValue: (value: string) => void onLoadImage?: (event: React.ChangeEvent<HTMLInputElement> | null, file?: File) => void } -export const UserMessage = React.memo(function UserMessage(props: IMessagesList) { +export const UserMessage = React.memo(function UserMessage({ setModal, setCurrentSrc, ...props }: IMessagesList) { const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null) const open = Boolean(anchorEl) const desktop = props.device === 'desktop' - const [modal, setModal] = useState<boolean>(false) const [loaded, setLoaded] = useState(false) const copy = (text: string) => { @@ -64,266 +65,95 @@ export const UserMessage = React.memo(function UserMessage(props: IMessagesList) return ( <> - {props.message.file && <FullScreenModal modal={modal} setModal={setModal} image={props.message.file.toString()} />} <span className='tutorial-message'> {!props.message.from_model ? ( // <Slide className='tutorial-message-me' direction='left' in={props.isNewMessage} mountOnEnter unmountOnExit> - <Box> - <Box - className='smallScroll' - sx={{ - width: 'fit-content', - marginLeft: 'auto', - maxWidth: desktop ? '68%' : '100%', - marginTop: 2, - marginBottom: 2, - }} - > - <Stack direction='row' justifyContent='end' alignItems='center'> - <Typography - variant='body2' + <Box + className='smallScroll' + sx={{ + width: 'fit-content', + marginLeft: 'auto', + maxWidth: desktop ? '68%' : '100%', + marginTop: 2, + marginBottom: 2, + }} + > + <Stack direction='row' justifyContent='end' alignItems='center'> + <Typography + variant='body2' + sx={{ + color: theme === 'light' ? '#868686' : '#A6A5A5', + lineHeight: '19.6px', + fontSize: '13px', + fontWeight: '600', + marginLeft: 1, + }} + > + {props.message.created_at.slice(10, 16).replace('T', ' ')} + </Typography> + </Stack> + + <Box display='flex' alignItems='center' justifyContent='end'> + {props.message.file ? ( + <Box sx={{ - color: theme === 'light' ? '#868686' : '#A6A5A5', - lineHeight: '19.6px', - fontSize: '13px', - fontWeight: '600', - marginLeft: 1, + display: 'flex', + alignItems: 'end', + flexDirection: 'column', + height: '60%', + width: '60%', }} > - {props.message.created_at.slice(10, 16).replace('T', ' ')} - </Typography> - </Stack> - - <Box display='flex' alignItems='center' justifyContent='end'> - {props.message.file ? ( - <Box - sx={{ - display: 'flex', - alignItems: 'end', - flexDirection: 'column', - height: '60%', - width: '60%', - }} - > - <Box sx={{ position: 'relative' }}> - <Image - onClick={() => { - setModal(true) - }} - onLoadingComplete={() => setLoaded(true)} - style={{ - objectFit: 'contain', - height: '100%', + <Box sx={{ position: 'relative' }}> + <Image + onClick={() => { + if (!props.message.file) return + setCurrentSrc(props.message.file.toString()) + setModal(true) + }} + onLoadingComplete={() => setLoaded(true)} + style={{ + objectFit: 'contain', + height: '100%', + width: '100%', + borderRadius: '13px', + cursor: 'pointer', + opacity: loaded ? '100%' : '0%', + minWidth: '100px', + minHeight: '100px', + }} + width={500} + height={500} + src={props.message.file.toString()} + alt={'К сожалению, изображение не загрузилось'} + /> + {!loaded && ( + <Skeleton + sx={{ + background: theme === 'dark' ? '#2d2d2f' : '#EFF0F2', + position: 'absolute', + top: '0', + left: '0', + zIndex: '1', + borderRadius: 5, width: '100%', - borderRadius: '13px', - cursor: 'pointer', - opacity: loaded ? '100%' : '0%', - minWidth: '100px', - minHeight: '100px', + height: '100%', }} - width={500} - height={500} - src={props.message.file.toString()} - alt={'К сожалению, изображение не загрузилось'} + variant='rectangular' /> - {!loaded && ( - <Skeleton - sx={{ - background: theme === 'dark' ? '#2d2d2f' : '#EFF0F2', - position: 'absolute', - top: '0', - left: '0', - zIndex: '1', - borderRadius: 5, - width: '100%', - height: '100%', - }} - variant='rectangular' - /> - )} - </Box> - <Box display='flex' alignItems='center'> - <Box marginRight={1} sx={{ cursor: 'pointer' }}> - <Box - onClick={handleClick} - aria-controls={open ? 'message-menu' : undefined} - aria-haspopup='true' - aria-expanded={open ? 'true' : undefined} - > - <TooltipCustom title={'Открыть меню'}> - <svg - width='22' - height='22' - viewBox='0 0 19 19' - fill='none' - xmlns='http://www.w3.org/2000/svg' - > - <rect - x='1' - y='1' - width='7' - height='7' - rx='2' - stroke='#A4AAB5' - strokeWidth='2' - /> - <rect - x='1' - y='11' - width='7' - height='7' - rx='2' - stroke='#A4AAB5' - strokeWidth='2' - /> - <rect - x='11' - y='11' - width='7' - height='7' - rx='2' - stroke='#A4AAB5' - strokeWidth='2' - /> - <circle cx='14.5' cy='4.5' r='3.5' stroke='#A4AAB5' strokeWidth='2' /> - </svg> - </TooltipCustom> - </Box> - <Menu - autoFocus={false} - open={open} - anchorEl={anchorEl} - id='message-menu' - onClose={handleClose} - onClick={handleClose} - sx={{ - '& .MuiMenu-list': { - backgroundColor: theme === 'dark' ? '#303035' : '#EFF0F2', - color: '#8280FF', - borderRadius: '15px', - }, - '& .MuiPopover-paper': { - backgroundColor: theme === 'dark' ? '#303035' : '#EFF0F2', - borderRadius: '15px', - }, - }} - > - <MenuItem - autoFocus={false} - sx={{ - fontSize: '15px', - fontWeight: '500', - display: 'flex', - alignItems: 'center', - gap: '10px', - }} - onClick={() => { - copy(props.message.content) - }} - > - <svg - width='14' - height='14' - viewBox='0 0 19 19' - fill='none' - xmlns='http://www.w3.org/2000/svg' - > - <g id='Group 29094'> - <rect - id='Rectangle 3340' - x='0.85' - y='3.85' - width='14.3' - height='14.3' - rx='2.15' - fill='transparent' - stroke='#8280FF' - strokeWidth='1.7' - /> - <path - id='Vector 40' - d='M6.5 1H15C16.6569 1 18 2.34315 18 4V12.5' - stroke='#8280FF' - strokeWidth='1.7' - strokeLinecap='round' - strokeLinejoin='round' - /> - </g> - </svg> - Копировать - </MenuItem> - <MenuItem - autoFocus={false} - sx={{ - fontSize: '15px', - fontWeight: '500', - display: 'flex', - alignItems: 'center', - gap: '5px', - }} - onClick={() => { - if (props.deleteMessage) props.deleteMessage(props.message.uid) - }} - > - <svg - width='21' - height='14' - viewBox='0 0 60 42' - fill='none' - xmlns='http://www.w3.org/2000/svg' - > - <path - fillRule='evenodd' - clipRule='evenodd' - d='M17.4216 27.9757C18.1603 28.707 19.3566 28.707 20.0972 27.9757L24.4097 23.6633L28.6079 27.8633C29.3391 28.5946 30.5278 28.5946 31.2609 27.8633C31.9941 27.1321 31.9941 25.932 31.2609 25.2007L27.0629 21.0195L31.3154 16.7632C32.0542 16.032 32.0542 14.8321 31.3154 14.0821C30.5767 13.3509 29.3785 13.3509 28.6398 14.0821L24.3873 18.3382L20.1891 14.1382C19.456 13.4069 18.2673 13.4069 17.536 14.1382C16.801 14.8694 16.801 16.0695 17.536 16.8008L21.7341 20.982L17.4216 25.2946C16.681 26.0446 16.681 27.2257 17.4216 27.9757ZM3.75289 7.87575C3.75289 5.81325 5.43291 4.12575 7.50666 4.12576L40.3735 4.12576L55.6566 21.0383L40.4279 37.8758L7.50666 37.8758C5.43291 37.8758 3.75288 36.1883 3.75288 34.1258L3.75289 7.87575ZM7.50666 41.6258L41.1534 41.6258C41.6784 41.6633 42.2147 41.4945 42.616 41.1007L59.4648 22.4633C59.8604 22.0696 60.031 21.5445 60.0029 21.0383C60.031 20.5133 59.8604 19.9883 59.4648 19.5945L42.616 0.957118C42.2485 0.600868 41.7685 0.413295 41.2885 0.413295L41.2885 0.375758L7.50666 0.375755C3.36104 0.375755 -0.000889326 3.732 -0.000889688 7.87575L-0.000891983 34.1258C-0.000892345 38.2695 3.36104 41.6258 7.50666 41.6258Z' - fill='#8280FF' - /> - </svg> - Удалить - </MenuItem> - </Menu> - </Box> - <TooltipCustom title='При отправке данного сообщения произошла ошибка, нажмите чтобы отправить заново'> - {!props.message.is_sent && ( - <Image - onClick={resend} - style={{ cursor: 'pointer', marginRight: '5px' }} - height={20} - width={20} - alt='1' - src={'/svg/important.svg'} - /> - )} - </TooltipCustom> - <Typography - className='smallScroll' - sx={{ - overflowY: 'scroll', - padding: '10px 15px', - backgroundColor: '#7F7DF3', - borderRadius: '13px', - color: '#FFFFFF', - lineHeight: '21px', - fontSize: '15px', - fontWeight: '400px', - marginTop: 0.4, - textAlign: 'left', - whiteSpace: props.message.content.length > 30 ? 'pre-wrap' : 'pre', - }} - > - {/*<Markdown content={props.message.content} />*/} - {props.message.content} - </Typography> - </Box> + )} </Box> - ) : ( <Box display='flex' alignItems='center'> <Box marginRight={1} sx={{ cursor: 'pointer' }}> <Box onClick={handleClick} - aria-controls={open ? 'message-menu' : undefined} + aria-controls={ + open ? 'message-menu' : undefined + } aria-haspopup='true' - aria-expanded={open ? 'true' : undefined} + aria-expanded={ + open ? 'true' : undefined + } > <TooltipCustom title={'Открыть меню'}> <svg @@ -360,7 +190,13 @@ export const UserMessage = React.memo(function UserMessage(props: IMessagesList) stroke='#A4AAB5' strokeWidth='2' /> - <circle cx='14.5' cy='4.5' r='3.5' stroke='#A4AAB5' strokeWidth='2' /> + <circle + cx='14.5' + cy='4.5' + r='3.5' + stroke='#A4AAB5' + strokeWidth='2' + /> </svg> </TooltipCustom> </Box> @@ -373,12 +209,14 @@ export const UserMessage = React.memo(function UserMessage(props: IMessagesList) onClick={handleClose} sx={{ '& .MuiMenu-list': { - backgroundColor: theme === 'dark' ? '#303035' : '#EFF0F2', + backgroundColor: + theme === 'dark' ? '#303035' : '#EFF0F2', color: '#8280FF', borderRadius: '15px', }, '& .MuiPopover-paper': { - backgroundColor: theme === 'dark' ? '#303035' : '#EFF0F2', + backgroundColor: + theme === 'dark' ? '#303035' : '#EFF0F2', borderRadius: '15px', }, }} @@ -437,7 +275,8 @@ export const UserMessage = React.memo(function UserMessage(props: IMessagesList) gap: '5px', }} onClick={() => { - if (props.deleteMessage) props.deleteMessage(props.message.uid) + if (props.deleteMessage) + props.deleteMessage(props.message.uid) }} > <svg @@ -462,7 +301,10 @@ export const UserMessage = React.memo(function UserMessage(props: IMessagesList) {!props.message.is_sent && ( <Image onClick={resend} - style={{ cursor: 'pointer', marginRight: '5px' }} + style={{ + cursor: 'pointer', + marginRight: '5px', + }} height={20} width={20} alt='1' @@ -483,15 +325,200 @@ export const UserMessage = React.memo(function UserMessage(props: IMessagesList) fontWeight: '400px', marginTop: 0.4, textAlign: 'left', - whiteSpace: props.message.content.length > 30 ? 'pre-wrap' : 'pre', + whiteSpace: + props.message.content.length > 30 ? 'pre-wrap' : 'pre', }} > {/*<Markdown content={props.message.content} />*/} {props.message.content} </Typography> </Box> - )} - </Box> + </Box> + ) : ( + <Box display='flex' alignItems='center'> + <Box marginRight={1} sx={{ cursor: 'pointer' }}> + <Box + onClick={handleClick} + aria-controls={open ? 'message-menu' : undefined} + aria-haspopup='true' + aria-expanded={open ? 'true' : undefined} + > + <TooltipCustom title={'Открыть меню'}> + <svg + width='22' + height='22' + viewBox='0 0 19 19' + fill='none' + xmlns='http://www.w3.org/2000/svg' + > + <rect + x='1' + y='1' + width='7' + height='7' + rx='2' + stroke='#A4AAB5' + strokeWidth='2' + /> + <rect + x='1' + y='11' + width='7' + height='7' + rx='2' + stroke='#A4AAB5' + strokeWidth='2' + /> + <rect + x='11' + y='11' + width='7' + height='7' + rx='2' + stroke='#A4AAB5' + strokeWidth='2' + /> + <circle + cx='14.5' + cy='4.5' + r='3.5' + stroke='#A4AAB5' + strokeWidth='2' + /> + </svg> + </TooltipCustom> + </Box> + <Menu + autoFocus={false} + open={open} + anchorEl={anchorEl} + id='message-menu' + onClose={handleClose} + onClick={handleClose} + sx={{ + '& .MuiMenu-list': { + backgroundColor: + theme === 'dark' ? '#303035' : '#EFF0F2', + color: '#8280FF', + borderRadius: '15px', + }, + '& .MuiPopover-paper': { + backgroundColor: + theme === 'dark' ? '#303035' : '#EFF0F2', + borderRadius: '15px', + }, + }} + > + <MenuItem + autoFocus={false} + sx={{ + fontSize: '15px', + fontWeight: '500', + display: 'flex', + alignItems: 'center', + gap: '10px', + }} + onClick={() => { + copy(props.message.content) + }} + > + <svg + width='14' + height='14' + viewBox='0 0 19 19' + fill='none' + xmlns='http://www.w3.org/2000/svg' + > + <g id='Group 29094'> + <rect + id='Rectangle 3340' + x='0.85' + y='3.85' + width='14.3' + height='14.3' + rx='2.15' + fill='transparent' + stroke='#8280FF' + strokeWidth='1.7' + /> + <path + id='Vector 40' + d='M6.5 1H15C16.6569 1 18 2.34315 18 4V12.5' + stroke='#8280FF' + strokeWidth='1.7' + strokeLinecap='round' + strokeLinejoin='round' + /> + </g> + </svg> + Копировать + </MenuItem> + <MenuItem + autoFocus={false} + sx={{ + fontSize: '15px', + fontWeight: '500', + display: 'flex', + alignItems: 'center', + gap: '5px', + }} + onClick={() => { + if (props.deleteMessage) + props.deleteMessage(props.message.uid) + }} + > + <svg + width='21' + height='14' + viewBox='0 0 60 42' + fill='none' + xmlns='http://www.w3.org/2000/svg' + > + <path + fillRule='evenodd' + clipRule='evenodd' + d='M17.4216 27.9757C18.1603 28.707 19.3566 28.707 20.0972 27.9757L24.4097 23.6633L28.6079 27.8633C29.3391 28.5946 30.5278 28.5946 31.2609 27.8633C31.9941 27.1321 31.9941 25.932 31.2609 25.2007L27.0629 21.0195L31.3154 16.7632C32.0542 16.032 32.0542 14.8321 31.3154 14.0821C30.5767 13.3509 29.3785 13.3509 28.6398 14.0821L24.3873 18.3382L20.1891 14.1382C19.456 13.4069 18.2673 13.4069 17.536 14.1382C16.801 14.8694 16.801 16.0695 17.536 16.8008L21.7341 20.982L17.4216 25.2946C16.681 26.0446 16.681 27.2257 17.4216 27.9757ZM3.75289 7.87575C3.75289 5.81325 5.43291 4.12575 7.50666 4.12576L40.3735 4.12576L55.6566 21.0383L40.4279 37.8758L7.50666 37.8758C5.43291 37.8758 3.75288 36.1883 3.75288 34.1258L3.75289 7.87575ZM7.50666 41.6258L41.1534 41.6258C41.6784 41.6633 42.2147 41.4945 42.616 41.1007L59.4648 22.4633C59.8604 22.0696 60.031 21.5445 60.0029 21.0383C60.031 20.5133 59.8604 19.9883 59.4648 19.5945L42.616 0.957118C42.2485 0.600868 41.7685 0.413295 41.2885 0.413295L41.2885 0.375758L7.50666 0.375755C3.36104 0.375755 -0.000889326 3.732 -0.000889688 7.87575L-0.000891983 34.1258C-0.000892345 38.2695 3.36104 41.6258 7.50666 41.6258Z' + fill='#8280FF' + /> + </svg> + Удалить + </MenuItem> + </Menu> + </Box> + <TooltipCustom title='При отправке данного сообщения произошла ошибка, нажмите чтобы отправить заново'> + {!props.message.is_sent && ( + <Image + onClick={resend} + style={{ cursor: 'pointer', marginRight: '5px' }} + height={20} + width={20} + alt='1' + src={'/svg/important.svg'} + /> + )} + </TooltipCustom> + <Typography + className='smallScroll' + sx={{ + overflowY: 'scroll', + padding: '10px 15px', + backgroundColor: '#7F7DF3', + borderRadius: '13px', + color: '#FFFFFF', + lineHeight: '21px', + fontSize: '15px', + fontWeight: '400px', + marginTop: 0.4, + textAlign: 'left', + whiteSpace: + props.message.content.length > 30 ? 'pre-wrap' : 'pre', + }} + > + {/*<Markdown content={props.message.content} />*/} + {props.message.content} + </Typography> + </Box> + )} </Box> </Box> ) : ( @@ -1,36 +0,0 @@ -.wrap { - display: flex; - overflow: hidden; - width: 100%; - flex-wrap: wrap; - gap: 20px; - @media (max-width: 766px) { - height: 70vh; - width: 100%; - overflow-y: scroll; - text-align: center; - justify-content: center; - } - div { - margin-right: 10px; - @media (max-width: 766px) { - width: 100%; - } - - .image { - margin:10px auto; - width: 250px; - height: 250px; - border-radius: 15px !important; - } - - @media (max-width: 1700px) { - - } - - @media (max-width: 766px) { - text-align: center; - width: 80%; - } - } -} \ No newline at end of file @@ -0,0 +1,4 @@ +export * from './ui' +export * from './lib' +export * from './api' +export * from './model' \ No newline at end of file @@ -1,7 +1,9 @@ -import { NavigationSearchModelLink } from './types' -import { API_URL } from '@/src/shared/lib/constants' import axios from 'axios' +import { API_URL } from '@/src/shared/lib/constants' + +import { NavigationSearchModelLink } from './types' + export const getModelChatLinks = async (token: string) => { return await axios.get<NavigationSearchModelLink[]>(API_URL + '/api/chats/links', { headers: { Authorization: `Bearer ${token}` }, @@ -1,4 +1,4 @@ -import { NavigationSearchLink } from "../api/types"; +import { NavigationSearchLink } from '../api/types'; export const staticLinks: NavigationSearchLink[] = [ { @@ -1,2 +1,2 @@ -export * from './use-model' -export * from './use-chat-links' \ No newline at end of file +export * from './use-chat-links' +export * from './use-model' \ No newline at end of file @@ -1,9 +1,10 @@ import { useEffect, useId, useMemo, useState } from 'react' -import { NavigationSearchLink, NavigationSearchModelLink } from '../api/types' -import { getModelChatLinks, getModelMediaLinks } from '../api/api-get-model-links' import { uniqueId } from 'lodash' import { useSession } from 'next-auth/react' +import { getModelChatLinks, getModelMediaLinks } from '../api/api-get-model-links' +import { NavigationSearchLink, NavigationSearchModelLink } from '../api/types' + export const useNavigationSearchChatLinks = () => { const [chatLinks, setChatLinks] = useState<NavigationSearchModelLink[]>([]) @@ -1,9 +1,10 @@ import { useEffect, useMemo, useState } from 'react' -import { NavigationSearchLink, NavigationSearchModelLink } from '../api/types' -import { getModelMediaLinks } from '../api/api-get-model-links' import { uniqueId } from 'lodash' import { useSession } from 'next-auth/react' +import { getModelMediaLinks } from '../api/api-get-model-links' +import { NavigationSearchLink, NavigationSearchModelLink } from '../api/types' + export const useNavigationSearchMediaLinks = () => { const [mediaLinks, setMediaLinks] = useState<NavigationSearchModelLink[]>([]) @@ -1,28 +1,22 @@ -import { Box, Typography } from '@mui/material' import React from 'react' +import { Box, Typography } from '@mui/material' import styles from './key-for-search.module.scss' -interface KeyForSearchProps { - userAgent: string | null -} - -export const KeyForSearch = ({ userAgent }: KeyForSearchProps) => { - if (userAgent === null) { - return null - } - +export const KeyForSearch = () => { const checkType = () => { - if (userAgent.includes('Windows')) { - return 'Ctrl + F' + if (navigator.userAgent && !navigator.userAgent.includes('Windows')) { + return '⌘+F' } else { - return '⌥+F' + return 'Ctrl + F' } } return ( <Box className={styles.ctrl}> - <Typography sx={{ fontSize: '13px', color: '#A4AAB5', fontWeight: 500 }}>{checkType()}</Typography> + <Typography sx={{ fontSize: '13px', color: '#A4AAB5', fontWeight: 500 }}> + {checkType()} + </Typography> </Box> ) } @@ -1,15 +1,18 @@ import React, { useEffect, useMemo } from 'react' import { Autocomplete, Box, Stack, TextField, Typography } from '@mui/material' import Image from 'next/image' -import { KeyForSearch } from './key-for-search' -import { useNavigationSearchChatLinks, useNavigationSearchModel } from '../model' -import { staticLinks } from '../config' -import { InputStyleDark, InputStyleLight, useConcat } from '@/src/shared' -import { useAppSelector } from '@/src/main/store/store' import { useRouter } from 'next/router' + +import { useAppSelector } from '@/src/main/store/store' +import { InputStyleDark, InputStyleLight, useConcat } from '@/src/shared' + import { NavigationSearchLink } from '../api/types' +import { staticLinks } from '../config' +import { useNavigationSearchChatLinks, useNavigationSearchModel } from '../model' import { useNavigationSearchMediaLinks } from '../model/use-media-links' +import { KeyForSearch } from './key-for-search' + interface SearchProps { device: string } @@ -30,10 +33,6 @@ export const Search = ({ device }: SearchProps) => { visibleMediaLinks, staticLinks ) - - useEffect(() => { - console.log(links) - }, [links]) const { search, setSearch, searchOpen, setSearchOpen, searchRef, filteredLinks } = useNavigationSearchModel(links) @@ -137,13 +136,7 @@ export const Search = ({ device }: SearchProps) => { }} /> ), - endAdornment: ( - <KeyForSearch - userAgent={ - desktop ? window.navigator.userAgent : null - } - /> - ), + endAdornment: <KeyForSearch />, }} /> )} @@ -1,2 +1,2 @@ -export * from './ui' -export * from './config' \ No newline at end of file +export * from './config' +export * from './ui' \ No newline at end of file @@ -6,11 +6,11 @@ import { useSession } from 'next-auth/react' import { useAppSelector } from '@/src/main/store/store' import { CheckBoxAgreeWithRules, Loader, Select } from '@/src/shared' -import { accountApi } from '@/src/shared/api/account-endpoints' import { getPaymentsHistory, PaymentHistory } from '@/src/shared/api/endpoints' import Offer from '@/src/widgets/payment/ui/offer' import styles from '../ui/payment.module.scss' +import { getPaymentsPlans, payProduct } from '@/src/entities/user-account' export interface IOffer { uid: string @@ -56,11 +56,11 @@ export const Payment: React.FC<IPaymentsProps & any> = ({ device, changeClose }) }, [data?.access]) useEffect(() => { - if (data?.access) accountApi.getPaymentsPlans(data?.access).then((res) => setOffers(res)) + if (data?.access) getPaymentsPlans(data?.access).then((res) => setOffers(res)) }, [data?.access]) const pay = async (uid: string) => { - const urlForPay = await accountApi.payProduct(data!.access, uid) + const urlForPay = await payProduct(data!.access, uid) urlForPay ? await Router.push(urlForPay) : null } @@ -18,7 +18,10 @@ import { signOut, useSession } from 'next-auth/react' import { addUserSettings } from '@/src/entities/user-account/api/add-user-settings' import { updateUserSettings } from '@/src/entities/user-account/api/update-user-settings' import { isSettingExist } from '@/src/entities/user-account/lib/helpers/is-setting-exist' -import { addUserAccountSettings, updateUserAccountSettings } from '@/src/entities/user-account/model/settings' +import { + addUserAccountSettings, + updateUserAccountSettings, +} from '@/src/entities/user-account/model/settings' import { getAll, ResponseAllInfo } from '@/src/entities/user-account/model/user-type-slice' import { useAppDispatch, useAppSelector } from '@/src/main/store/store' import { TooltipCustom } from '@/src/shared' @@ -29,12 +32,27 @@ import styles from '../styles/styles.module.css' export const menuListTop = [ { title: 'Дашборд', link: '/', icon: '/svg/side-menu/market', activeList: [] }, - { title: 'Оплата', link: '/account?scope=subscribe', icon: '/svg/side-menu/star', activeList: ['subscribe'] }, + { + title: 'Оплата', + link: '/account?scope=subscribe', + icon: '/svg/side-menu/star', + activeList: ['subscribe'], + }, ] export const menuListMiddle = [ - { title: 'Чат-боты', link: '/chat-bot', icon: '/svg/side-menu/chat', activeList: ['chat-bot'] }, - { title: 'Изображения', link: '/images', icon: '/svg/side-menu/image', activeList: ['images'] }, + { + title: 'Чат-боты', + link: '/chat-bot', + icon: '/svg/side-menu/chat', + activeList: ['chat-bot'], + }, + { + title: 'Изображения', + link: '/images/flux', + icon: '/svg/side-menu/image', + activeList: ['images'], + }, // { title: 'Копирайтинг', link: '/copywriting/my', icon: '/svg/side-menu/copyrating', activeList: ['copywriting'] }, // { title: 'Видео', link: '/video', icon: '/svg/side-menu/video', activeList: [] }, // { title: 'Аудио', link: '/audio', icon: '/svg/side-menu/audio', activeList: [] }, @@ -73,22 +91,30 @@ const closedMixin = (theme: Theme): CSSObject => ({ }, }) -const Drawer = styled(MuiDrawer, { shouldForwardProp: (prop) => prop !== 'open' })(({ theme, open }) => ({ - width: drawerWidth, - flexShrink: 0, - whiteSpace: 'nowrap', - boxSizing: 'border-box', - ...(open && { - ...openedMixin(theme), - '& .MuiDrawer-paper': openedMixin(theme), - }), - ...(!open && { - ...closedMixin(theme), - '& .MuiDrawer-paper': closedMixin(theme), - }), -})) +const Drawer = styled(MuiDrawer, { shouldForwardProp: (prop) => prop !== 'open' })( + ({ theme, open }) => ({ + width: drawerWidth, + flexShrink: 0, + whiteSpace: 'nowrap', + boxSizing: 'border-box', + ...(open && { + ...openedMixin(theme), + '& .MuiDrawer-paper': openedMixin(theme), + }), + ...(!open && { + ...closedMixin(theme), + '& .MuiDrawer-paper': closedMixin(theme), + }), + }) +) -export const SideMenu = ({ device, sidemenuDefaultOpen }: { device: Device; sidemenuDefaultOpen: boolean }) => { +export const SideMenu = ({ + device, + sidemenuDefaultOpen, +}: { + device: Device + sidemenuDefaultOpen: boolean +}) => { const [open, setOpen] = React.useState(sidemenuDefaultOpen) const [isInitialValueSet, setIsInitialValueSet] = React.useState(false) @@ -125,7 +151,11 @@ export const SideMenu = ({ device, sidemenuDefaultOpen }: { device: Device; side useEffect(() => { if (settings.state !== null && isInitialValueSet) { - let response = isSettingExist({ settings: settings.state, targetDevice: device, targetType: 'sidemenu' }) + let response = isSettingExist({ + settings: settings.state, + targetDevice: device, + targetType: 'sidemenu', + }) if (response && data?.access) { dispatch( @@ -140,7 +170,11 @@ export const SideMenu = ({ device, sidemenuDefaultOpen }: { device: Device; side dispatch( addUserAccountSettings({ token: data.access, - setting: { device, type: 'sidemenu', value: { sidemenu_state: open ? 'opened' : 'closed' } }, + setting: { + device, + type: 'sidemenu', + value: { sidemenu_state: open ? 'opened' : 'closed' }, + }, }) ) } @@ -149,7 +183,11 @@ export const SideMenu = ({ device, sidemenuDefaultOpen }: { device: Device; side useEffect(() => { if (settings.state !== null && !isInitialValueSet) { - let setting = isSettingExist({ settings: settings.state, targetDevice: device, targetType: 'sidemenu' }) + let setting = isSettingExist({ + settings: settings.state, + targetDevice: device, + targetType: 'sidemenu', + }) if (setting) { setting?.value?.sidemenu_state === 'opened' ? setOpen(true) : setOpen(false) } @@ -160,7 +198,12 @@ export const SideMenu = ({ device, sidemenuDefaultOpen }: { device: Device; side return ( <Drawer className={styles.drawer} - PaperProps={{ sx: { backgroundColor: theme.theme === 'light' ? 'white' : '#151518', border: 'none' } }} + PaperProps={{ + sx: { + backgroundColor: theme.theme === 'light' ? 'white' : '#151518', + border: 'none', + }, + }} variant='permanent' open={open} > @@ -180,7 +223,14 @@ export const SideMenu = ({ device, sidemenuDefaultOpen }: { device: Device; side alignItems={'center'} justifyContent='space-between' > - <Image style={{ cursor: 'pointer', marginLeft: 23 }} priority src='/logo.svg' height={26} width={31} alt='Error' /> + <Image + style={{ cursor: 'pointer', marginLeft: 23 }} + priority + src='/logo.svg' + height={26} + width={31} + alt='Error' + /> <Image style={{ @@ -254,12 +304,25 @@ export const SideMenu = ({ device, sidemenuDefaultOpen }: { device: Device; side </Box> </> )} - {openErrorModal && <ErrorModalLazy device={'desktop'} open={openErrorModal} handleClose={handleCloseErrorModal} />} + {openErrorModal && ( + <ErrorModalLazy + device={'desktop'} + open={openErrorModal} + handleClose={handleCloseErrorModal} + /> + )} <Box sx={{ position: 'absolute', bottom: 5, width: '100%', left: open ? 25 : 0 }}> {open && ( <Box sx={{ width: 'fit-content' }}> <Link href={'https://air.fail/requisites'} target='_blank'> - <Typography className='title-main-gray' sx={{ fontSize: 14, marginBottom: '7px', textDecorationStyle: 'dashed' }}> + <Typography + className='title-main-gray' + sx={{ + fontSize: 14, + marginBottom: '7px', + textDecorationStyle: 'dashed', + }} + > Реквизиты </Typography> </Link> @@ -282,7 +345,12 @@ type MenuItemProps = { } export function MenuItem(props: MenuItemProps) { - const isActive = useMemo(() => props.link === props.pathname || props.activeList?.some((el) => props.pathname.includes(el)), [props.pathname]) + const isActive = useMemo( + () => + props.link === props.pathname || + props.activeList?.some((el) => props.pathname.includes(el)), + [props.pathname] + ) const theme = useAppSelector((state) => state.theme.theme) return ( @@ -302,7 +370,8 @@ export function MenuItem(props: MenuItemProps) { '& .MuiTooltip-arrow': { color: theme === 'light' ? '#E8E8FA' : '#4B4B4B', }, - boxShadow: '0px 0px 4px rgba(0, 0, 0, 0.04), 0px 4px 32px rgba(0, 0, 0, 0.16)', + boxShadow: + '0px 0px 4px rgba(0, 0, 0, 0.04), 0px 4px 32px rgba(0, 0, 0, 0.16)', }, }, }} @@ -330,7 +399,9 @@ export function MenuItem(props: MenuItemProps) { <ListItem component='div' sx={{ - backgroundColor: isActive ? 'rgba(130, 128, 255, 0.08)' : 'none', + backgroundColor: isActive + ? 'rgba(130, 128, 255, 0.08)' + : 'none', borderRadius: '10px', }} > @@ -341,7 +412,12 @@ export function MenuItem(props: MenuItemProps) { justifyContent: 'center', }} > - <Image src={props.icon + (isActive ? '' : '-off') + '.svg'} width={19} height={19} alt={''} /> + <Image + src={props.icon + (isActive ? '' : '-off') + '.svg'} + width={19} + height={19} + alt={''} + /> </ListItemIcon> <ListItemText primary={props.title} @@ -349,7 +425,11 @@ export function MenuItem(props: MenuItemProps) { opacity: props.open ? 1 : 0, '.MuiTypography-root': { fontSize: 16, - color: isActive ? '#8280FF' : props.theme === 'light' ? '#A4AAB5' : '#D4D4D4', + color: isActive + ? '#8280FF' + : props.theme === 'light' + ? '#A4AAB5' + : '#D4D4D4', }, }} /> @@ -0,0 +1,27 @@ +declare module "*.scss" { + const content: Record<string, string>; + export default content; +} + +declare module "*.css" { + const content: string; + export default content; +} + +declare function ym(...args: any[]): void; + +declare module "*.svg?react" { + const content: React.FC<React.SVGProps<SVGSVGElement>>; + export default content; +} + +declare module "*.svg?url" { + const content: { + blurHeight: number; + blurWidth: number; + height: number; + src: string; + width: number; + }; + export default content; +} @@ -1,17 +1,26 @@ services: - app: - image: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA - build: + frontend: + image: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA + build: context: . dockerfile: Dockerfile - container_name: frontend - restart: unless-stopped - ports: - - '3000:3000' - env_file: - - .env.production + container_name: frontend + restart: unless-stopped + networks: + - infrastructure + - ui + expose: + - "3000" + labels: + - "traefik.enable=true" + - "traefik.docker.network=infrastructure" + - "traefik.http.routers.frontend.rule=Host(`$UI_DOMAIN`)" + - "traefik.http.routers.frontend.entrypoints=web" + env_file: + - .env.production networks: - default: - name: 'air' + infrastructure: external: true + ui: + name: ui @@ -9,6 +9,24 @@ const nextConfig = { layers: true, } + const fileLoaderRule = config.module.rules.find((rule) => rule.test?.test?.('.svg')) + + config.module.rules.push( + { + test: /\.svg$/i, + issuer: fileLoaderRule.issuer, + resourceQuery: /react/, + use: ['@svgr/webpack'], + }, + { + ...fileLoaderRule, + test: /\.svg$/i, + resourceQuery: /url/, + } + ) + + fileLoaderRule.exclude = /\.svg$/i + return config }, reactStrictMode: false, @@ -1,111 +1,122 @@ { - "name": "frontend", - "private": true, - "version": "1.0.0", - "scripts": { - "start": "next start", - "build": "next build", - "predeploy": "npm run build", - "deploy": "vk-miniapps-deploy", - "dev": "next dev", - "lint": "next lint", - "precommit": "lint-staged", - "prepare": "husky install", - "tunnel": "vk-tunnel --insecure=1 --http-protocol=https --ws-protocol=wss --host=0.0.0.0 --port=3000", - "export": "next export" - }, - "engines": { - "node": ">=12.0.0" - }, - "keywords": [], - "license": "MIT", - "lint-staged": { - "*.{js,ts,jsx,tsx}": [ - "prettier --write", - "eslint --fix" - ] - }, - "dependencies": { - "@babel/eslint-parser": "7.23.3", - "@dqbd/tiktoken": "^1.0.7", - "@emotion/react": "^11.11.0", - "@emotion/styled": "^11.11.0", - "@fontsource/roboto": "^4.5.8", - "@mui/icons-material": "^5.11.11", - "@mui/material": "^5.11.12", - "@mui/styled-engine-sc": "^5.11.11", - "@mui/x-date-pickers": "^6.6.0", - "@next/bundle-analyzer": "^13.4.3", - "@reduxjs/toolkit": "^1.9.5", - "@sentry/nextjs": "^7.59.2", - "@types/cookie": "^0.5.1", - "@types/intro.js": "^5.1.1", - "@types/lodash": "^4.14.195", - "@types/node": "18.15.2", - "@types/react": "18.0.28", - "@types/react-dom": "18.0.11", - "@types/react-syntax-highlighter": "^15.5.7", - "axios": "^0.24.0", - "base64-encode-file": "^1.0.7", - "chart.js": "^4.4.0", - "cookie": "^0.5.0", - "cross-env": "^7.0.3", - "dayjs": "^1.11.8", - "draft-js": "^0.11.7", - "eslint": "^8.7.0", - "eslint-config-next": "13.2.4", - "eslint-plugin-prettier": "^4.2.1", - "eslint-plugin-simple-import-sort": "^10.0.0", - "husky": "^8.0.0", - "i18next": "^23.4.1", - "intro.js-react": "^1.0.0", - "lint-staged": "^13.2.2", - "lodash.debounce": "^4.0.8", - "next": "13.2.4", - "next-auth": "^4.20.1", - "next-i18next": "^14.0.0", - "prettier": "^2.8.8", - "react": "18.2.0", - "react-chartjs-2": "^5.2.0", - "react-cookie": "^4.1.1", - "react-dom": "18.2.0", - "react-draft-wysiwyg": "^1.15.0", - "react-hook-form": "^7.43.9", - "react-i18next": "^13.0.3", - "react-markdown": "^8.0.7", - "react-redux": "^8.0.5", - "react-syntax-highlighter": "^15.5.0", - "remark-gfm": "^3.0.1", - "sass": "^1.63.4", - "styled-components": "^5.3.9", - "typescript": "5.1.3" - }, - "devDependencies": { - "@types/intro.js": "^5.1.1", - "@types/lodash": "^4.14.195", - "@types/react-draft-wysiwyg": "^1.13.8", - "@types/react-syntax-highlighter": "^15.5.7", - "eslint-config-prettier": "^8.8.0", - "lint-staged": "^13.2.2", - "prettier": "^2.8.8", - "typescript": "5.1.3" - }, - "resolutions": { - "react-scripts/webpack-dev-server/yargs/yargs-parser": ">=18.1.2" - }, - "overrides": { - "es5-ext@^0.10.50": "0.10.53" - }, - "browserslist": { - "production": [ - ">0.2%", - "not dead", - "not op_mini all" - ], - "development": [ - "last 1 chrome version", - "last 1 firefox version", - "last 1 safari version" - ] - } + "name": "frontend", + "private": true, + "version": "1.0.0", + "scripts": { + "start": "next start", + "build": "next build", + "predeploy": "npm run build", + "deploy": "vk-miniapps-deploy", + "dev": "next dev", + "lint": "next lint", + "precommit": "lint-staged", + "prepare": "husky install", + "tunnel": "vk-tunnel --insecure=1 --http-protocol=https --ws-protocol=wss --host=0.0.0.0 --port=3000", + "export": "next export" + }, + "engines": { + "node": ">=12.0.0" + }, + "keywords": [], + "license": "MIT", + "lint-staged": { + "*.{js,ts,jsx,tsx}": [ + "prettier --write", + "eslint --fix" + ] + }, + "dependencies": { + "@babel/eslint-parser": "7.23.3", + "@dqbd/tiktoken": "^1.0.7", + "@emotion/react": "^11.11.0", + "@emotion/styled": "^11.11.0", + "@fontsource/roboto": "^4.5.8", + "@lottiefiles/dotlottie-react": "^0.12.0", + "@mui/icons-material": "^5.11.11", + "@mui/material": "^5.11.12", + "@mui/styled-engine-sc": "^5.11.11", + "@mui/x-date-pickers": "^6.6.0", + "@next/bundle-analyzer": "^13.4.3", + "@react-input/mask": "^2.0.4", + "@reduxjs/toolkit": "^1.9.5", + "@sentry/nextjs": "^8.42.0", + "@types/cookie": "^0.5.1", + "@types/intro.js": "^5.1.1", + "@types/lodash": "^4.14.195", + "@types/node": "18.15.2", + "@types/react": "18.0.28", + "@types/react-dom": "18.0.11", + "@types/react-syntax-highlighter": "^15.5.7", + "axios": "^0.24.0", + "base64-encode-file": "^1.0.7", + "buffer": "^6.0.3", + "chart.js": "^4.4.0", + "cookie": "^0.5.0", + "cross-env": "^7.0.3", + "dayjs": "^1.11.8", + "draft-js": "^0.11.7", + "draft-js-import-html": "^1.4.1", + "draftjs-to-html": "^0.9.1", + "eslint": "^8.7.0", + "eslint-config-next": "13.2.4", + "eslint-plugin-prettier": "^4.2.1", + "eslint-plugin-simple-import-sort": "^10.0.0", + "husky": "^8.0.0", + "i18next": "^23.4.1", + "intro.js-react": "^1.0.0", + "lint-staged": "^13.2.2", + "lodash.debounce": "^4.0.8", + "next": "13.2.4", + "next-auth": "^4.20.1", + "next-i18next": "^14.0.0", + "prettier": "^2.8.8", + "react": "18.2.0", + "react-chartjs-2": "^5.2.0", + "react-color": "^2.19.3", + "react-cookie": "^4.1.1", + "react-dom": "18.2.0", + "react-draft-wysiwyg": "^1.15.0", + "react-hook-form": "^7.43.9", + "react-i18next": "^13.0.3", + "react-markdown": "^8.0.7", + "react-redux": "^8.0.5", + "react-syntax-highlighter": "^15.5.0", + "remark-gfm": "^3.0.1", + "sass": "^1.63.4", + "styled-components": "^5.3.9", + "swiper": "^11.2.1", + "typescript": "5.1.3" + }, + "devDependencies": { + "@svgr/webpack": "^8.1.0", + "@types/draftjs-to-html": "^0.8.4", + "@types/intro.js": "^5.1.1", + "@types/lodash": "^4.14.195", + "@types/react-color": "^3.0.13", + "@types/react-draft-wysiwyg": "^1.13.8", + "@types/react-lottie": "^1.2.10", + "@types/react-syntax-highlighter": "^15.5.7", + "eslint-config-prettier": "^8.8.0", + "lint-staged": "^13.2.2", + "prettier": "^2.8.8", + "typescript": "5.1.3" + }, + "resolutions": { + "react-scripts/webpack-dev-server/yargs/yargs-parser": ">=18.1.2" + }, + "overrides": { + "es5-ext@^0.10.50": "0.10.53" + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + } }