@@ -1,3 +1,11 @@ +## Precondition + + + +## Steps to reproduce + + + ## Expected Result @@ -10,10 +18,9 @@ Прикрепляйте видео/скриншоты тут --> +## Notes -## Steps to reproduce - - + *** @@ -0,0 +1,40 @@ +#!/usr/bin/env sh +# +# If you create/switch to a branch without a prefix (no '/'), +# automatically rename it to "feature/". +# +# Allowed prefixes (when '/' is present): +# fix/, feature/, release/, chore/ +# + +set -eu + +IS_BRANCH_CHECKOUT="${3:-0}" +if [ "$IS_BRANCH_CHECKOUT" != "1" ]; then + exit 0 +fi + +BRANCH="$(git symbolic-ref --quiet --short HEAD 2>/dev/null || true)" +if [ -z "$BRANCH" ]; then + exit 0 +fi + +case "$BRANCH" in + fix/*|feature/*|release/*|chore/*) + exit 0 + ;; +esac + +# No prefix -> default to feature/ +if ! printf "%s" "$BRANCH" | grep -q '/'; then + NEW_BRANCH="feature/$BRANCH" + if git show-ref --verify --quiet "refs/heads/$NEW_BRANCH"; then + exit 0 + fi + git branch -m "$BRANCH" "$NEW_BRANCH" 2>/dev/null || true + exit 0 +fi + +# Has a prefix but it's not allowed: cannot reliably fix here (checkout already happened). +exit 0 + @@ -0,0 +1,35 @@ +#!/usr/bin/env sh +# +# Enforce allowed branch prefixes: +# fix/, feature/, release/, chore/ +# +# If branch has no prefix (no '/'), automatically rename to feature/. +# + +set -eu + +BRANCH="$(git symbolic-ref --quiet --short HEAD 2>/dev/null || true)" +if [ -z "$BRANCH" ]; then + exit 0 +fi + +case "$BRANCH" in + fix/*|feature/*|release/*|chore/*) + exit 0 + ;; +esac + +# No slash => default to feature/ +if ! printf "%s" "$BRANCH" | grep -q '/'; then + NEW_BRANCH="feature/$BRANCH" + if ! git show-ref --verify --quiet "refs/heads/$NEW_BRANCH"; then + git branch -m "$BRANCH" "$NEW_BRANCH" + fi + exit 0 +fi + +echo >&2 "Ошибка: недопустимый префикс ветки '$BRANCH'." +echo >&2 "Разрешены только: fix/, feature/, release/, chore/." +echo >&2 "Либо используйте ветку без префикса — она будет автоматически переименована в feature/<имя>." +exit 1 + @@ -0,0 +1,65 @@ +#!/usr/bin/env sh +# +# Auto-prefix commit messages based on branch name: +# fix/..., feature/..., release/..., chore/... +# -> "refs #: " +# + +set -eu + +COMMIT_MSG_FILE="${1:-}" +COMMIT_SOURCE="${2:-}" + +if [ -z "$COMMIT_MSG_FILE" ] || [ ! -f "$COMMIT_MSG_FILE" ]; then + exit 0 +fi + +# Skip for merges/squashes/amends/templates (avoid fighting Git-generated messages). +case "$COMMIT_SOURCE" in + merge|squash|commit|template) + exit 0 + ;; +esac + +BRANCH="$(git symbolic-ref --quiet --short HEAD 2>/dev/null || true)" +TASK_NUMBER="" +if printf "%s" "$BRANCH" | grep -Eq '^(fix|feature|release|chore)/[0-9]+'; then + TASK_NUMBER="$(printf "%s" "$BRANCH" | sed -E 's#^(fix|feature|release|chore)/([0-9]+).*#\2#')" +else + exit 0 +fi + +FIRST_LINE="$(LC_ALL=C sed -n '1p' "$COMMIT_MSG_FILE" || true)" + +# Do not touch special commit subjects. +case "$FIRST_LINE" in + "Merge "*|"fixup! "*|"squash! "*|"revert! "*) + exit 0 + ;; +esac + +PREFIX="refs #${TASK_NUMBER}: " + +# Already has the right prefix (or already references the number at start) -> no-op. +case "$FIRST_LINE" in + "$PREFIX"*|"refs #${TASK_NUMBER} "*|"refs #${TASK_NUMBER}:"*) + exit 0 + ;; +esac + +# If message already starts with some refs prefix, don't double-prefix. +case "$FIRST_LINE" in + "refs #"*|"refs: #"* ) + exit 0 + ;; +esac + +TMP_FILE="${COMMIT_MSG_FILE}.tmp.$$" +{ + printf "%s%s\n" "$PREFIX" "$FIRST_LINE" + LC_ALL=C sed -n '2,$p' "$COMMIT_MSG_FILE" +} > "$TMP_FILE" + +mv "$TMP_FILE" "$COMMIT_MSG_FILE" +exit 0 + Binary files /dev/null and b/public/fonts/Inter/static/Inter_18pt-Black.ttf differ Binary files /dev/null and b/public/fonts/Inter/static/Inter_18pt-BlackItalic.ttf differ Binary files /dev/null and b/public/fonts/Inter/static/Inter_18pt-Bold.ttf differ Binary files /dev/null and b/public/fonts/Inter/static/Inter_18pt-BoldItalic.ttf differ Binary files /dev/null and b/public/fonts/Inter/static/Inter_18pt-ExtraBold.ttf differ Binary files /dev/null and b/public/fonts/Inter/static/Inter_18pt-ExtraBoldItalic.ttf differ Binary files /dev/null and b/public/fonts/Inter/static/Inter_18pt-ExtraLight.ttf differ Binary files /dev/null and b/public/fonts/Inter/static/Inter_18pt-ExtraLightItalic.ttf differ Binary files /dev/null and b/public/fonts/Inter/static/Inter_18pt-Italic.ttf differ Binary files /dev/null and b/public/fonts/Inter/static/Inter_18pt-Light.ttf differ Binary files /dev/null and b/public/fonts/Inter/static/Inter_18pt-LightItalic.ttf differ Binary files /dev/null and b/public/fonts/Inter/static/Inter_18pt-Medium.ttf differ Binary files /dev/null and b/public/fonts/Inter/static/Inter_18pt-MediumItalic.ttf differ Binary files /dev/null and b/public/fonts/Inter/static/Inter_18pt-Regular.ttf differ Binary files /dev/null and b/public/fonts/Inter/static/Inter_18pt-SemiBold.ttf differ Binary files /dev/null and b/public/fonts/Inter/static/Inter_18pt-SemiBoldItalic.ttf differ Binary files /dev/null and b/public/fonts/Inter/static/Inter_18pt-Thin.ttf differ Binary files /dev/null and b/public/fonts/Inter/static/Inter_18pt-ThinItalic.ttf differ Binary files /dev/null and b/public/fonts/Inter/static/Inter_24pt-Black.ttf differ Binary files /dev/null and b/public/fonts/Inter/static/Inter_24pt-BlackItalic.ttf differ Binary files /dev/null and b/public/fonts/Inter/static/Inter_24pt-Bold.ttf differ Binary files /dev/null and b/public/fonts/Inter/static/Inter_24pt-BoldItalic.ttf differ Binary files /dev/null and b/public/fonts/Inter/static/Inter_24pt-ExtraBold.ttf differ Binary files /dev/null and b/public/fonts/Inter/static/Inter_24pt-ExtraBoldItalic.ttf differ Binary files /dev/null and b/public/fonts/Inter/static/Inter_24pt-ExtraLight.ttf differ Binary files /dev/null and b/public/fonts/Inter/static/Inter_24pt-ExtraLightItalic.ttf differ Binary files /dev/null and b/public/fonts/Inter/static/Inter_24pt-Italic.ttf differ Binary files /dev/null and b/public/fonts/Inter/static/Inter_24pt-Light.ttf differ Binary files /dev/null and b/public/fonts/Inter/static/Inter_24pt-LightItalic.ttf differ Binary files /dev/null and b/public/fonts/Inter/static/Inter_24pt-Medium.ttf differ Binary files /dev/null and b/public/fonts/Inter/static/Inter_24pt-MediumItalic.ttf differ Binary files /dev/null and b/public/fonts/Inter/static/Inter_24pt-Regular.ttf differ Binary files /dev/null and b/public/fonts/Inter/static/Inter_24pt-SemiBold.ttf differ Binary files /dev/null and b/public/fonts/Inter/static/Inter_24pt-SemiBoldItalic.ttf differ Binary files /dev/null and b/public/fonts/Inter/static/Inter_24pt-Thin.ttf differ Binary files /dev/null and b/public/fonts/Inter/static/Inter_24pt-ThinItalic.ttf differ Binary files /dev/null and b/public/fonts/Inter/static/Inter_28pt-Black.ttf differ Binary files /dev/null and b/public/fonts/Inter/static/Inter_28pt-BlackItalic.ttf differ Binary files /dev/null and b/public/fonts/Inter/static/Inter_28pt-Bold.ttf differ Binary files /dev/null and b/public/fonts/Inter/static/Inter_28pt-BoldItalic.ttf differ Binary files /dev/null and b/public/fonts/Inter/static/Inter_28pt-ExtraBold.ttf differ Binary files /dev/null and b/public/fonts/Inter/static/Inter_28pt-ExtraBoldItalic.ttf differ Binary files /dev/null and b/public/fonts/Inter/static/Inter_28pt-ExtraLight.ttf differ Binary files /dev/null and b/public/fonts/Inter/static/Inter_28pt-ExtraLightItalic.ttf differ Binary files /dev/null and b/public/fonts/Inter/static/Inter_28pt-Italic.ttf differ Binary files /dev/null and b/public/fonts/Inter/static/Inter_28pt-Light.ttf differ Binary files /dev/null and b/public/fonts/Inter/static/Inter_28pt-LightItalic.ttf differ Binary files /dev/null and b/public/fonts/Inter/static/Inter_28pt-Medium.ttf differ Binary files /dev/null and b/public/fonts/Inter/static/Inter_28pt-MediumItalic.ttf differ Binary files /dev/null and b/public/fonts/Inter/static/Inter_28pt-Regular.ttf differ Binary files /dev/null and b/public/fonts/Inter/static/Inter_28pt-SemiBold.ttf differ Binary files /dev/null and b/public/fonts/Inter/static/Inter_28pt-SemiBoldItalic.ttf differ Binary files /dev/null and b/public/fonts/Inter/static/Inter_28pt-Thin.ttf differ Binary files /dev/null and b/public/fonts/Inter/static/Inter_28pt-ThinItalic.ttf differ Binary files /dev/null and b/public/fonts/Inter/Inter-Italic-VariableFont_opsz,wght.ttf differ Binary files /dev/null and b/public/fonts/Inter/Inter-VariableFont_opsz,wght.ttf differ Binary files a/public/fonts/Raleway/static/Raleway-Black.ttf and /dev/null differ Binary files a/public/fonts/Raleway/static/Raleway-BlackItalic.ttf and /dev/null differ Binary files a/public/fonts/Raleway/static/Raleway-Bold.ttf and /dev/null differ Binary files a/public/fonts/Raleway/static/Raleway-BoldItalic.ttf and /dev/null differ Binary files a/public/fonts/Raleway/static/Raleway-ExtraBold.ttf and /dev/null differ Binary files a/public/fonts/Raleway/static/Raleway-ExtraBoldItalic.ttf and /dev/null differ Binary files a/public/fonts/Raleway/static/Raleway-ExtraLight.ttf and /dev/null differ Binary files a/public/fonts/Raleway/static/Raleway-ExtraLightItalic.ttf and /dev/null differ Binary files a/public/fonts/Raleway/static/Raleway-Italic.ttf and /dev/null differ Binary files a/public/fonts/Raleway/static/Raleway-Light.ttf and /dev/null differ Binary files a/public/fonts/Raleway/static/Raleway-LightItalic.ttf and /dev/null differ Binary files a/public/fonts/Raleway/static/Raleway-Medium.ttf and /dev/null differ Binary files a/public/fonts/Raleway/static/Raleway-MediumItalic.ttf and /dev/null differ Binary files a/public/fonts/Raleway/static/Raleway-Regular.ttf and /dev/null differ Binary files a/public/fonts/Raleway/static/Raleway-SemiBold.ttf and /dev/null differ Binary files a/public/fonts/Raleway/static/Raleway-SemiBoldItalic.ttf and /dev/null differ Binary files a/public/fonts/Raleway/static/Raleway-Thin.ttf and /dev/null differ Binary files a/public/fonts/Raleway/static/Raleway-ThinItalic.ttf and /dev/null differ Binary files a/public/fonts/Raleway/Raleway-Italic-VariableFont_wght.ttf and /dev/null differ Binary files a/public/fonts/Raleway/Raleway-VariableFont_wght.ttf and /dev/null differ Binary files a/public/voice-clone/add-voice.png and b/public/voice-clone/add-voice.png differ Binary files a/public/voice-clone/my-generations.png and b/public/voice-clone/my-generations.png differ Binary files a/public/voice-clone/step-1.png and b/public/voice-clone/step-1.png differ @@ -19,10 +19,8 @@ export const CheckboxFilter = ({ name, value, filters, item_key, description, se const [check, setCheck] = React.useState(value || false) React.useEffect(() => { - if (filters[item_key] !== undefined && filters[item_key] !== check) { - setCheck(filters[item_key]) - } - }, [filters]) + setCheck(filters[item_key] ?? value) + }, [filters, item_key, value]) return ( @@ -20,10 +20,8 @@ export const SelectFilter = ({ selects, filters, item_key, name, description, se const [value, setValue] = React.useState(selects.default) React.useEffect(() => { - if (filters[item_key] !== undefined && filters[item_key] !== value) { - setValue(filters[item_key]) - } - }, [filters]) + setValue(filters[item_key] ?? selects.default) + }, [filters, item_key, selects.default]) const onChange = (e: SelectChangeEvent) => { setNewParam({ [item_key]: e.target.value }) @@ -38,14 +36,13 @@ export const SelectFilter = ({ selects, filters, item_key, name, description, se IconComponent={KeyboardArrowDownIcon} value={value} onChange={onChange} - inputProps={{ - MenuProps: { - MenuListProps: { - sx: { - color: '#A6A5A5', - backgroundColor: '#151518', - }, - }, + MenuProps={{ + transitionDuration: 0, + PaperProps: { + sx: { backgroundColor: '#151518', borderRadius: '8px', marginTop: '8px' }, + }, + MenuListProps: { + sx: { backgroundColor: '#151518', color: '#A6A5A5' }, }, }} style={{ @@ -27,10 +27,8 @@ export const Slide = ({ title, values, filters, item_key, description, setNewPar } React.useEffect(() => { - if (filters[item_key] !== undefined && filters[item_key] !== range) { - setRange(filters[item_key]) - } - }, [filters]) + setRange(filters[item_key] ?? values.default) + }, [filters, item_key, values.default]) return ( <> @@ -28,14 +28,13 @@ export const ChatSelect: React.FC = ({ value, setValue, list, setDefaul IconComponent={KeyboardArrowDownIcon} value={value} onChange={onChange} - inputProps={{ - MenuProps: { - MenuListProps: { - sx: { - color: '#A6A5A5', - backgroundColor: '#151518', - }, - }, + MenuProps={{ + transitionDuration: 0, + PaperProps: { + sx: { backgroundColor: '#151518', borderRadius: '8px', marginTop: '8px' }, + }, + MenuListProps: { + sx: { backgroundColor: '#151518', color: '#A6A5A5' }, }, }} style={{ @@ -4,7 +4,6 @@ import { configureStore } from '@reduxjs/toolkit' import { paramsStore } from '#/app/store/model-parametres-store' import { balanceSlice } from '#/entities/balance' import { userSlice } from '#/entities/user-account' -import { settingsSlice } from '#/entities/user-account/model/settings' import { chatsReducer } from '#/features/chats/chats-slice' import { pendingSlice } from '#/features/pending' import { stepperSlice } from '#/features/register-business' @@ -19,7 +18,6 @@ export const store = configureStore({ user: userSlice.reducer, notification: notificationSlice.reducer, params: paramsStore.reducer, - settings: settingsSlice.reducer, copy: copySlice.reducer, loading: pendingSlice.reducer, chats: chatsReducer, @@ -27,19 +27,19 @@ } @font-face { - font-family: 'Raleway'; + font-family: 'Inter'; font-style: normal; font-weight: 100 900; font-display: swap; - src: url('/fonts/Raleway/Raleway-VariableFont_wght.ttf') format('truetype'); + src: url('/fonts/Inter/Inter-VariableFont_opsz,wght.ttf') format('truetype'); } @font-face { - font-family: 'Raleway'; + font-family: 'Inter'; font-style: italic; font-weight: 100 900; font-display: swap; - src: url('/fonts/Raleway/Raleway-Italic-VariableFont_wght.ttf') format('truetype'); + src: url('/fonts/Inter/Inter-Italic-VariableFont_opsz,wght.ttf') format('truetype'); } * { @@ -49,7 +49,7 @@ } html { - font-family: 'Raleway', sans-serif; + font-family: 'Inter', sans-serif; } body { @@ -449,17 +449,6 @@ button { font: inherit; color: inherit; background-color: transparent; - text-align: left; - cursor: pointer; -} - -button { - padding: 0; - border: none; - font: inherit; - color: inherit; - background-color: transparent; - text-align: left; cursor: pointer; } @@ -1,17 +0,0 @@ -import { api } from '#/shared/api' - -import { IUserSetting } from '../model/types' - -export const addUserSettings = async ( - token: string, - setting: Omit -): Promise => { - try { - const { data } = await api.post('/api/users/settings/', setting, { - headers: { Authorization: `Bearer ${token}` }, - }) - return data - } catch (err) { - return null - } -} @@ -2,7 +2,7 @@ import axios, { AxiosResponse } from 'axios' import { getApiUrl } from '#/shared/lib/constants' -import { AccountType } from '../model/types' +import { AccountType } from '../model/account.types' export const getAccountType = async (token: string | null | undefined): Promise => { if (!token) { return 'regular' @@ -1,14 +0,0 @@ -import { api } from '#/shared/api' - -import { IUserSetting } from '../model/types' - -export const getUserSettings = async (token: string): Promise => { - try { - const { data } = await api.get('/api/users/settings/', { - headers: { Authorization: `Bearer ${token}` }, - }) - return data - } catch (err) { - return [] - } -} @@ -1 +0,0 @@ -export * from './settings.routes' \ No newline at end of file @@ -1,15 +0,0 @@ -import { api } from '#/shared/api' -import { Agent } from 'https' -import { IUserSetting } from '../model/types' - -export function getUserSettings() { - return api.get('/api/users/settings/') -} - -export function postUserSettings(option: Omit) { - return api.post('/api/users/settings/', option) -} - -export function updateUserSettings(id: string, value: any) { - return api.put(`/api/users/settings/${id}`, { value }) -} @@ -1,20 +0,0 @@ -import { api } from '#/shared/api' - -import { getUpdatedSettingsLocal } from '../lib/helpers/update-setting-local' -import { IUserSetting, SettingValueType } from '../model/types' - -export const updateUserSettings = async ( - token: string, - id: string, - value: SettingValueType, - settings: IUserSetting[] -) => { - try { - await api.put(`/api/users/settings/${id}`, { value }, { - headers: { Authorization: `Bearer ${token}` }, - }) - return getUpdatedSettingsLocal({ settings, id, value }) - } catch (err) { - return null - } -} @@ -1,23 +0,0 @@ -import { Device } from '#/shared/lib/types/entities' - -import { IUserSetting, SettingType } from '../../model/types' - -interface IProps { - settings: Omit | null - targetType: SettingType - targetDevice: Device -} - -export function isSettingExist({ settings, targetDevice, targetType }: IProps): IUserSetting | null { - if (!settings || settings.length === 0) { - return null - } - - for (let i = 0; i < settings.length; i++) { - if (settings[i].type === targetType && settings[i].device === targetDevice) { - return settings[i] - } - } - - return null -} @@ -1,22 +0,0 @@ -import { IUserSetting, SettingValueType } from '../../model/types' - -interface IProps { - settings: IUserSetting[] - id: string - value: SettingValueType -} - -export function getUpdatedSettingsLocal({ settings, id, value }: IProps): IUserSetting[] { - if (settings.length === 0) { - return settings - } - - return settings.map((setting) => - setting.id === id - ? { - ...setting, - value, - } - : setting - ) -} @@ -0,0 +1,2 @@ +export type AccountType = 'regular' | 'business_host' | 'business_account' + @@ -1,2 +1 @@ -export * from './settings-context' -export * from './use-global-settings' \ No newline at end of file +export {} \ No newline at end of file @@ -1,9 +0,0 @@ -import { createUseContext } from '#/shared' -import { createContext } from 'react' -import { useGlobalSettings } from './use-global-settings' - -export const UserSettingsContext = createContext | null>(null) - -export const useUserSettingsContext = createUseContext(UserSettingsContext) - -export const UserSettingsContextProvider = UserSettingsContext.Provider @@ -1,93 +0,0 @@ -import { createAsyncThunk, createSlice } from '@reduxjs/toolkit' - -import { addUserSettings } from '../api/add-user-settings' -import { getUserSettings } from '../api/get-user-settings' -import { updateUserSettings } from '../api/update-user-settings' -import { getUpdatedSettingsLocal } from '../lib/helpers/update-setting-local' - -import { IUserSetting, SettingValueType } from './types' -import { useLocalStorageSave } from '#/shared/lib/helpers/local-storage-helper' - -interface IAddSettings { - token: string | undefined | null - setting: Omit -} - -export const getUserAccountSettings = createAsyncThunk( - 'users/getSettings', - async (token: string | undefined | null) => { - if (!token) { - return initialState.state - } - return await getUserSettings(token) - } -) - -export const addUserAccountSettings = createAsyncThunk( - 'users/addSettings', - async ({ token, setting }: IAddSettings) => { - if (!token) { - return - } - return await addUserSettings(token, setting) - } -) - -export const updateUserAccountSettings = createAsyncThunk( - 'users/updateSettings', - async ({ - token, - id, - value, - settings, - }: { - token: string | undefined | null - id: string - value: SettingValueType - settings: IUserSetting[] | null - }) => { - if (!token) { - return initialState.state - } - if (!settings) { - return initialState.state - } - return await updateUserSettings(token, id, value, settings) - } -) - -const initialState: { state: IUserSetting[] | null } = { - state: null, -} - -export const settingsSlice = createSlice({ - name: 'balance', - initialState, - reducers: { - setSettings: (state, action) => { - state.state = action.payload - }, - }, - extraReducers: (builder) => { - builder.addCase(getUserAccountSettings.fulfilled, (state, action) => { - state.state = action.payload - localStorage.setItem('global_settings', JSON.stringify(action.payload)) - }) - builder.addCase(addUserAccountSettings.fulfilled, (state, action) => { - if (state.state === null) state.state = [] - if (action.payload) { - state.state = [...state.state, action.payload] - localStorage.setItem('global_settings', JSON.stringify([...state.state, action.payload])) - } - }) - - builder.addCase(updateUserAccountSettings.fulfilled, (state, action) => { - if (action.payload) { - state.state = action.payload - localStorage.setItem('global_settings', JSON.stringify(action.payload)) - } - }) - }, -}) - -export const { setSettings } = settingsSlice.actions @@ -1,16 +0,0 @@ -import { Device } from '#/shared/lib/types/entities' - -export type AccountType = 'regular' | 'business_host' | 'business_account' - -export type SettingType = 'sidemenu' | string - -export type SettingValueType = { - sidemenu_state?: 'opened' | 'closed' -} - -export interface IUserSetting { - id: string - device: Device - type: SettingType - value: SettingValueType | any -} @@ -1,66 +0,0 @@ -import { useLocalStorage } from 'usehooks-ts' -import { IUserSetting } from './types' -import { makePrivateRequest } from '#/shared/api' -import { getUserSettings, postUserSettings, updateUserSettings } from '../api' - -import { getDeviceType } from '#/shared' -import { useCallback, useEffect, useRef, useState } from 'react' -import { useSession } from 'next-auth/react' -import { useShowDataStore } from '#/shared/lib/hooks/use-show-data' - -export function useGlobalSettings() { - const [settings, setSettings] = useLocalStorage('global_settings', []) - - const { showMessage } = useShowDataStore() - - const device = getDeviceType() - - const { data } = useSession() - - const addSettings = makePrivateRequest(async (type: string, value: any) => { - const { status, data } = await postUserSettings({ device, type, value }) - - if (status !== 200) return showMessage('Ошибка создания настроек') - - setSettings((s) => [...s, data]) - }) - - const updateSettings = useCallback( - makePrivateRequest(async (type: string, value: any) => { - const option = settings.find((x) => x.type === type) - - if (!option) return showMessage('Ошибка присвоения настроек') - - const { status } = await updateUserSettings(option.id, value) - - if (status !== 204) return showMessage('Ошибка создания настроек') - - setSettings((s) => [...s.filter((x) => x.type !== type), { ...option, value }]) - }), - [settings, data] - ) - - const fetchUserSettings = makePrivateRequest(async () => { - const { status, data } = await getUserSettings() - - const device = getDeviceType() - - if (status !== 200) return showMessage('Ошибка загрузки данных пользователя') - - setSettings(data.filter((s) => s.device === device)) - }) - - const getOptionValue = (type: string, initial: any) => { - const option = settings.find((o) => o.type === type) - return option ? option.value : initial - } - - return { - settings, - setSettings, - addSettings, - fetchUserSettings, - updateSettings, - getOptionValue, - } -} @@ -1,7 +1,7 @@ import React, { useState } from 'react' import { Box, TextField, Typography } from '@mui/material' import axios from 'axios' -import { Dayjs } from 'dayjs' +import dayjs, { Dayjs } from 'dayjs' import { useRouter } from 'next/navigation' import { useSession } from 'next-auth/react' @@ -38,6 +38,11 @@ export const ApiKeyModal = ({ const keyName = `Ключ ${Math.floor(Math.random() * 100000) + 1}` + if (endDate && dayjs(endDate).isBefore(dayjs(), 'day')) { + showMessage('Указана неверная дата') + return + } + setIsLoading(true) const response = endDate && endDate !== '' @@ -0,0 +1,55 @@ +import { IModelParams } from '#/shared/api/models/models' + +export type ModelParamValues = Record + +export function getAllowedParamKeys(params: IModelParams[], currentVersion: string): Set { + return new Set( + params + .filter((el) => { + if (el.versions.length === 0) { + return true + } + + if (!currentVersion) { + return true + } + + return el.versions.includes(currentVersion) + }) + .map((el) => el.key) + ) +} + +export function pruneModelParams( + includeParams: ModelParamValues, + params: IModelParams[], + currentVersion: string +): ModelParamValues | null { + const allowedKeys = getAllowedParamKeys(params, currentVersion) + + if (allowedKeys.size === 0) { + return Object.keys(includeParams).length !== 0 ? {} : null + } + + const filteredEntries = Object.entries(includeParams).filter(([key]) => allowedKeys.has(key)) + + if (filteredEntries.length === Object.keys(includeParams).length) { + return null + } + + return filteredEntries.length ? Object.fromEntries(filteredEntries) : {} +} + +export function buildDefaultParamsForVersion(parameters: IModelParams[], version: string): ModelParamValues { + if (!version) { + return parameters.reduce((acc, param) => ({ ...acc, [param.key]: param.values.default }), {}) + } + + return parameters.reduce((acc, param) => { + if (param.versions.length === 0 || param.versions.includes(version)) { + return { ...acc, [param.key]: param.values.default } + } + + return acc + }, {} as ModelParamValues) +} @@ -0,0 +1,27 @@ +import React from 'react' + +import { replaceParams } from '#/app/store/model-parametres-store' +import { useAppDispatch, useAppSelector } from '#/app/store/store' +import { IModelParams } from '#/shared/api/models/models' + +import { pruneModelParams } from '../lib/prune-model-params' + +export function usePruneModelParams(params: IModelParams[] | undefined, currentVersion: string) { + const includeParams = useAppSelector((state) => state.params.params) as Record< + string, + string | number | number[] | boolean + > + const dispatch = useAppDispatch() + + React.useEffect(() => { + if (!params?.length) { + return + } + + const nextPayload = pruneModelParams(includeParams, params, currentVersion) + + if (nextPayload !== null) { + dispatch(replaceParams(nextPayload)) + } + }, [currentVersion, dispatch, includeParams, params]) +} @@ -1,126 +1,94 @@ -import React from 'react' -import { Stack } from '@mui/material' - -import { CheckboxFilter } from '#/app/components/filters/checkbox_filter' -import { InputFilter } from '#/app/components/filters/input_filter' -import { SelectFilter } from '#/app/components/filters/select_filter' -import { Slide } from '#/app/components/filters/slide_filter' -import { replaceParams, setParams } from '#/app/store/model-parametres-store' -import { useAppDispatch, useAppSelector } from '#/app/store/store' -import { IModelParams } from '#/shared/api/models/models' - -interface IProps { - params: IModelParams[] - currentVersion: string -} -export default function BotParamsMap({ params, currentVersion }: IProps) { - const includeParams = useAppSelector((state) => state.params.params) - const dispatch = useAppDispatch() - - const setNewParam = React.useCallback( - (payload: { [key: string]: string | number | number[] | boolean }) => { - dispatch(setParams(payload)) - }, - [dispatch] - ) - - React.useEffect(() => { - if (!params) { - return - } - - const allowedKeys = new Set( - params - .filter((el) => { - if (el.versions.length === 0) { - return true - } - - // Согласовано с chat-bot filteredParams: без выбранной версии считаем все параметры допустимыми - if (!currentVersion) { - return true - } - - return el.versions.includes(currentVersion) - }) - .map((el) => el.key) - ) - - if (allowedKeys.size === 0) { - if (Object.keys(includeParams).length !== 0) { - dispatch(replaceParams({})) - } - return - } - - const filteredEntries = Object.entries(includeParams).filter(([key]) => allowedKeys.has(key)) - - if (filteredEntries.length !== Object.keys(includeParams).length) { - const nextPayload = filteredEntries.length ? Object.fromEntries(filteredEntries) : {} - dispatch(replaceParams(nextPayload)) - } - }, [currentVersion, dispatch, includeParams, params, setNewParam]) - - return ( - - {params.map((item, idx) => { - if (item.versions.length === 0 || item.versions.includes(currentVersion)) { - if (item.type == 'floatrange' || item.type == 'intrange') { - return ( - - ) - } - if (item.type == 'bool') { - return ( - - ) - } - - if (item.type == 'list') { - return ( - - ) - } - - if (item.type == 'int' || item.type == 'str') { - return ( - - ) - } - } - })} - - ) -} +import React from 'react' +import { Stack } from '@mui/material' + +import { CheckboxFilter } from '#/app/components/filters/checkbox_filter' +import { InputFilter } from '#/app/components/filters/input_filter' +import { SelectFilter } from '#/app/components/filters/select_filter' +import { Slide } from '#/app/components/filters/slide_filter' +import { setParams } from '#/app/store/model-parametres-store' +import { useAppDispatch, useAppSelector } from '#/app/store/store' +import { IModelParams } from '#/shared/api/models/models' + +import { usePruneModelParams } from './model/use-prune-model-params' + +interface IProps { + params: IModelParams[] + currentVersion: string +} +export default function BotParamsMap({ params, currentVersion }: IProps) { + const includeParams = useAppSelector((state) => state.params.params) + const dispatch = useAppDispatch() + + usePruneModelParams(params, currentVersion) + + const setNewParam = React.useCallback( + (payload: { [key: string]: string | number | number[] | boolean }) => { + dispatch(setParams(payload)) + }, + [dispatch] + ) + + return ( + + {params.map((item, idx) => { + if (item.versions.length === 0 || item.versions.includes(currentVersion)) { + if (item.type == 'floatrange' || item.type == 'intrange') { + return ( + + ) + } + if (item.type == 'bool') { + return ( + + ) + } + + if (item.type == 'list') { + return ( + + ) + } + + if (item.type == 'int' || item.type == 'str') { + return ( + + ) + } + } + })} + + ) +} + \ No newline at end of file @@ -0,0 +1,93 @@ +.banner { + position: fixed; + z-index: 9999; + background: #1d1d21; + border: 1px solid rgba(164, 170, 181, 0.1); + border-radius: 16px; + padding: 20px; + display: flex; + flex-direction: column; + gap: 24px; +} + +.desktop { + right: 24px; + bottom: 24px; + width: 450px; +} + +.mobile { + left: 50%; + bottom: 16px; + transform: translateX(-50%); + width: calc(100% - 32px); + max-width: 335px; +} + +.content { + display: flex; + flex-direction: column; + gap: 12px; +} + +.title { + font-size: 24px; + font-weight: 600; + color: #fff; + line-height: 1; +} + +.text { + font-size: 14px; + color: #a4aab5; + line-height: 1.4; +} + +.link { + color: #8280ff; + text-decoration: underline; +} + +.actions { + display: flex; + flex-direction: column; + gap: 10px; +} + +.row { + display: flex; + gap: 10px; +} + +.btnPrimary { + flex: 1; + height: 40px; + border-radius: 10px; + background: #fff; + color: #000; + font-size: 14px; + font-weight: 500; + cursor: pointer; +} + +.btnOutline { + width: 100%; + height: 40px; + border: 1px solid #a4aab5; + border-radius: 10px; + background: transparent; + color: #fff; + font-size: 14px; + font-weight: 500; + cursor: pointer; +} + +.mobile .row { + flex-direction: column; +} + +.mobile .btnPrimary, +.mobile .btnOutline { + width: 100%; + flex: none; +} @@ -0,0 +1,65 @@ +import React from 'react' +import Link from 'next/link' + +import { useAppSelector } from '#/app/store/store' +import { getDeviceType } from '#/shared/lib/helpers' +import { initYandexMetrika } from '#/shared/lib/yandex-metrika' + +import styles from './cookie-consent.module.scss' + +function getConsent(): string | null { + if (typeof window === 'undefined') return null + const value = localStorage.getItem('cookie-consent') + if (value === 'all' || value === 'necessary' || value === 'declined') return value + return null +} + +export function CookieConsent() { + const account_type = useAppSelector((state) => state.user.account_type) + const [consent, setConsent] = React.useState(undefined) + const desktop = getDeviceType() === 'desktop' + + React.useEffect(() => { + if (account_type !== 'regular') return + const stored = getConsent() + setConsent(stored) + if (stored === 'all') initYandexMetrika() + }, [account_type]) + + const choose = (value: string) => { + localStorage.setItem('cookie-consent', value) + setConsent(value) + if (value === 'all') initYandexMetrika() + } + + if (account_type !== 'regular' || consent !== null) return null + + return ( +
+
+

Cookies и аналитика

+

+ Мы используем технические cookies для работы сайта и аналитические cookies для улучшения + сервиса. Подробнее — в{' '} + + Политике обработки персональных данных + + . +

+
+
+
+ + +
+ +
+
+ ) +} @@ -4,8 +4,7 @@ interface SendReport { errorMessage: string email: string file?: File - company?:string -} +} export async function sendReport({errorMessage, email, file}: SendReport) { let formData = new FormData() @@ -16,15 +15,3 @@ export async function sendReport({errorMessage, email, file}: SendReport) { } return await api.post('/reports/', formData) } - - -export async function sendBusinessReport({errorMessage, email, company, file}: SendReport) { - let formData = new FormData() - - formData.append('report_text', `${email}: ${errorMessage}`) - formData.append('company_name', `${company}`) - if (file) { - formData.append('images', file) - } - return await api.post('/reports/business-support/', formData) -} @@ -5,11 +5,11 @@ import { useAppSelector } from '#/app/store/store' import { makePrivateRequest } from '#/shared/api' import { useShowDataStore } from '#/shared/lib/hooks/use-show-data' -import { sendBusinessReport, sendReport } from '../api' +import { sendReport } from '../api' const MAX_FILE_SIZE = 4.5 * 1024 * 1024 // 4.5 MB -export const useErrorReport = (reportType:'business' | 'default' = 'default') => { +export const useErrorReport = () => { const email = useAppSelector((state) => state.user.email) const [file, setFile] = React.useState() @@ -34,7 +34,7 @@ export const useErrorReport = (reportType:'business' | 'default' = 'default') => } } - const sendError = makePrivateRequest(async (company) => { + const sendError = makePrivateRequest(async () => { if (errorMessage.trim() === '') { showMessage('Поле не должно быть пустым.') setError(true) @@ -42,10 +42,7 @@ export const useErrorReport = (reportType:'business' | 'default' = 'default') => return } - const response = await (reportType === 'business' - ? sendBusinessReport({ errorMessage, email, company, file }) - : sendReport({ errorMessage, email, file }) - ) + const response = await sendReport({ errorMessage, email, file }) if (response.status === 201) { setErrorMessage('') @@ -1,120 +0,0 @@ -import * as React from 'react' -import { useForm } from 'react-hook-form' -import { Typography } from '@mui/material' - -import { useAppSelector } from '#/app/store/store' -import AttachSvg from '#/assets/svg/attach.svg?react' -import PlusSvg from '#/assets/svg/plus.svg?react' -import { BUSINESS_ERROR_REPORT, getModalById, PlateTemplate } from '#/features/modals' -import { CompanyAutocomplete } from '#/features/register-business/api/company-autocomplete' -import { companyNameOptions } from '#/features/register-business/lib/constatnts-step-legal-informative' -import { c } from '#/shared' -import { useShowDataStore } from '#/shared/lib/hooks/use-show-data' -import { CommonButton } from '#/shared/ui/button' -import { CommonTextArea } from '#/shared/ui/common-textarea' - -import { useErrorReport } from '../model/use-error-report' - -import styles from './error-report.module.scss' - -export const BusinessErrorReportPlate = () => { - const modal = getModalById(BUSINESS_ERROR_REPORT) - const { error, isSend, setErrorMessage, handleFileChange, file, setFile, sendError } = useErrorReport('business') - const { showMessage } = useShowDataStore() - - const { companyName } = useAppSelector((state) => state.stepper.dataForCreate) - - const methods = useForm({ - defaultValues: { - companyName - }, - }) - - const handleSubmit = () => { - const formValues = methods.getValues() - const company = formValues.companyName?.trim() - - if (!company) { - showMessage('Пожалуйста, укажите наименование организации') - return - } - - sendError(company) - } - - return ( - - {isSend ? ( -
-

Спасибо!

-

Ваше сообщение направлено нашим специалистам. Ответ придет на почту, указанную при регистрации

-
- ) : ( -
e.stopPropagation()}> -

Запрос для компаний и партнеров

- - Опишите ваш сценарий — подберем решение и ответим за день. Работаем для корпораций, агентств и интеграторов - - - - - setErrorMessage(evt.target.value)} - buttomSlot={ - <> - - - } - /> - {file && ( -
-
- -
- {file.name} - {(file.size / 1024).toFixed(1)} KB -
-
- - -
- )} - -
- modal.setState(false)}> - Отмена - - - Отправить - -
-
- )} -
- ) -} \ No newline at end of file @@ -1,5 +1,4 @@ -import { Dispatch, SetStateAction, useEffect, useMemo, useState } from 'react' -import { ImageWithState } from './types' +import { Dispatch, SetStateAction, useEffect, useMemo } from 'react' import { Message } from '#/entities/message' export const useImagesLibrary = ( @@ -7,8 +6,6 @@ export const useImagesLibrary = ( current: string | null, images: Message[] ) => { - const [initialCount, setInitialCount] = useState(0) - const esc = (e: KeyboardEvent) => { if (e.key === 'Escape') { e.preventDefault() @@ -18,7 +15,7 @@ export const useImagesLibrary = ( const currentIndex = useMemo( () => images.findIndex((item) => item.file === current), - [current] + [current, images] ) useEffect(() => { @@ -27,8 +24,6 @@ export const useImagesLibrary = ( }, []) return { - initialCount, - setInitialCount, - currentIndex, + currentIndex, } } @@ -1,7 +1,5 @@ -import { debounce } from 'lodash' -import { useCallback, useEffect, useState } from 'react' +import { 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) @@ -41,10 +39,6 @@ export const useLibrarySwiper = (onSlideFalse: ((...args: any) => any) | undefin } }, [swiper]) - // useEffect(() => { - // swiper?.slideTo(currentImageIndex || 0) - // }, [currentImageIndex]) - return { swiper, setSwiper, @@ -0,0 +1,21 @@ +.overlay { + position: fixed; + z-index: 1201; + inset: 0; + width: 100%; + height: 100%; + display: none; +} + +.overlayVisible { + display: block; +} + +.backdrop { + position: absolute; + z-index: 101; + width: 100%; + height: 100%; + background-color: #000000; + opacity: 0.9; +} @@ -1,16 +1,15 @@ -import React, { Dispatch, SetStateAction, useEffect, useMemo, useRef, useState } from 'react' -import Image from 'next/image' +import { Dispatch, SetStateAction, useEffect, useRef } from 'react' +import overlayStyles from './full-screen-modal-overlay.module.scss' import styles from './modal-styles.module.scss' import { ArrowDropDown } from '@mui/icons-material' -import { c, Loader, TooltipCustom } from '#/shared' +import { c } from '#/shared' import { useImagesLibrary } from '../model' import { Swiper, SwiperSlide } from 'swiper/react' import 'swiper/css' import { useLibrarySwiper } from '../model/use-swiper' -import { ImageIcons, useImageIcons } from '#/widgets/messages' -import { Typography } from '@mui/material' +import { useImageIcons } from '#/widgets/messages' import { ModalImage } from './modal-image' import { Message } from '#/entities/message' @@ -32,55 +31,63 @@ export default function FullScreenModal({ onSlideFalse, reverse = false, }: IProps) { - const { initialCount, setInitialCount, currentIndex } = useImagesLibrary( + const { currentIndex } = useImagesLibrary( setModal, current, images ) - const count = useRef(0) + const prevImagesLengthRef = useRef(0) const { swiper, setSwiper, slideNext, slidePrev } = useLibrarySwiper(onSlideFalse, reverse) const { downloadFile } = useImageIcons() + useEffect(() => { - // debugger - if (reverse) - setTimeout(() => { - swiper?.slideNext() - count.current = images.length - }, 400) - else - setTimeout(() => { - swiper?.slideTo(images.length - count.current - 1) - count.current = images.length - }, 400) - }, [images]) + if (!modal) { + prevImagesLengthRef.current = 0 + return + } + if (!swiper) return + + const len = images.length + const prevLen = prevImagesLengthRef.current + + if (prevLen === 0) { + prevImagesLengthRef.current = len + return + } + + if (len === prevLen) return + + if (len < prevLen) { + prevImagesLengthRef.current = len + return + } + + const delta = len - prevLen + + const id = window.setTimeout(() => { + if (reverse) { + swiper.update() + } else { + const nextIndex = swiper.activeIndex + delta + const clamped = Math.max(0, Math.min(nextIndex, len - 1)) + swiper.slideTo(clamped, 0) + } + prevImagesLengthRef.current = len + }, 0) + + return () => clearTimeout(id) + }, [modal, images.length, reverse, swiper]) return (
setModal(false)} > -
+
= ({ successLo utm_source: cookie.utm_source, utm_medium: cookie.utm_medium, utm_campaign: cookie.utm_campaign, - utm_term: cookie.utm_campaign, - utm_content: cookie.utm_campaign, + utm_term: cookie.utm_term, + utm_content: cookie.utm_content, } if (localStorage.getItem('referral')) { @@ -64,6 +64,23 @@ export const authOptions: NextAuthOptions = { return (await data) as User }, }), + CredentialsProvider({ + id: 'tokens', + type: 'credentials', + credentials: {}, + async authorize(credentials) { + const { access, refresh } = credentials as { access: string; refresh: string } + + if (!access || !refresh) { + throw new Error('invalid_credentials') + } + + return { + id: 'tokens', + token: { access, refresh }, + } as User + }, + }), ], callbacks: { async jwt({ token, user, account, trigger }) { @@ -15,6 +15,7 @@ import { pingFangFont } from '#/shared/lib/constants/font/font' import { getDeviceType } from '#/shared/lib/helpers' import { useBlockTelegram } from '#/shared/lib/hooks/use-block-telegram' import { useEnv } from '#/shared/lib/hooks/use-env' +import { CookieConsent } from '#/features/cookie-consent/ui/cookie-consent' import { Providers } from '#/widgets/providers' import { setupAxios } from '#/shared/api/axios-interceptors' @@ -70,6 +71,7 @@ function AppContent({ {Component.getLayout ? Component.getLayout() : } + @@ -3,37 +3,9 @@ import { Head, Html, Main, NextScript } from 'next/document' export default function Document() { return ( - -