Binary files a/public/images/dashboard.png and b/public/images/dashboard.png differ @@ -1,7 +1,7 @@ import axios from 'axios' import { Template } from '#/domains/copywrite/proxy/types/template' -import { API_URL } from '#/shared/lib/constants' +import { getApiUrl } from '#/shared/lib/constants' import { Message } from '#/shared/lib/types/model' export class CopywriteProxy { @@ -12,21 +12,21 @@ export class CopywriteProxy { } static async getGeneration(token?: string): Promise { - const { data } = await axios.get(API_URL + '/copywrite/', { + const { data } = await axios.get(getApiUrl() + '/copywrite/', { headers: { Authorization: `Bearer ${token}` }, }) return data } static async getTemplates(token?: string): Promise { - const { data } = await axios.get(API_URL + '/copywrite/templates/', { + const { data } = await axios.get(getApiUrl() + '/copywrite/templates/', { headers: { Authorization: `Bearer ${token}` }, }) return data } static async createTemplates(token?: string): Promise { - const { data } = await axios.post(API_URL + '/copywrite/templates/', { + const { data } = await axios.post(getApiUrl() + '/copywrite/templates/', { headers: { Authorization: `Bearer ${token}` }, }) return data @@ -1,11 +1,11 @@ import axios from 'axios' import { IShortModel } from '#/entities/model-entity' -import { API_URL } from '#/shared/lib/constants' +import { getApiUrl } from '#/shared/lib/constants' export async function getAudio(token?: string): Promise { try { - const { data } = await axios.get(API_URL + '/ml_models/?category=audio', { + const { data } = await axios.get(getApiUrl() + '/ml_models/?category=audio', { headers: { Authorization: `Bearer ${token}`, }, @@ -1,10 +1,10 @@ import axios from 'axios' -import { API_URL } from '#/shared/lib/constants' +import { getApiUrl } from '#/shared/lib/constants' export const getBalance = async (token: string): Promise => { try { - const { data } = await axios.get<{ current_token_balance: number }>(API_URL + '/payments/user-balance', { + const { data } = await axios.get<{ current_token_balance: number }>(getApiUrl() + '/payments/user-balance', { headers: { Authorization: `Bearer ${token}`, }, @@ -2,14 +2,14 @@ import axios from 'axios' import { Message, MessageSend } from '../types' -import { API_URL } from '#/shared/lib/constants' +import { getApiUrl } from '#/shared/lib/constants' export async function sendMediaMessage(model: string | null, modelType: 'video' | 'image' | 'audio', dataForSend: MessageSend | FormData, token?: string) { const HeaderDataType = dataForSend instanceof FormData ? 'multipart/form-data' : 'application/json' - return await axios.post(API_URL + `/media/${modelType}/${model}`, dataForSend, { + return await axios.post(getApiUrl() + `/media/${modelType}/${model}`, dataForSend, { withCredentials: true, validateStatus: (status) => status < 500, headers: { @@ -20,7 +20,7 @@ export async function sendMediaMessage(model: string | null, modelType: 'vide } export async function getImagesBySlug(slug: string, token: string, type:'image' | 'video' | 'audio', offset?: number, limit = 10) { - return await axios.get(API_URL + `/media/${type}/${slug}?limit=${limit}&offset=${offset}`, { + return await axios.get(getApiUrl() + `/media/${type}/${slug}?limit=${limit}&offset=${offset}`, { validateStatus: (status) => status < 500, headers: { Authorization: `Bearer ${token}`, @@ -2,10 +2,10 @@ import axios from 'axios' import { IModel } from '../types' -import { API_URL } from '#/shared/lib/constants' +import { getApiUrl } from '#/shared/lib/constants' export async function getBotParams(slug: string, token?: string) { - return await axios.get(API_URL + `/ml_models/${slug}`, { + return await axios.get(getApiUrl() + `/ml_models/${slug}`, { headers: { Authorization: `Bearer ${token}`, }, @@ -2,10 +2,10 @@ import axios from 'axios' import { IShortModel } from '../types' -import { API_URL } from '#/shared/lib/constants' +import { getApiUrl } from '#/shared/lib/constants' export async function getModelsImages(token?: string) { - return await axios.get(API_URL + '/ml_models/?category=images', { + return await axios.get(getApiUrl() + '/ml_models/?category=images', { headers: { Authorization: `Bearer ${token}`, }, @@ -1,6 +1,6 @@ import axios from 'axios' -import { API_URL } from '#/shared/lib/constants' +import { getApiUrl } from '#/shared/lib/constants' import { IUserSetting } from '../model/types' @@ -9,7 +9,7 @@ export const addUserSettings = async ( setting: Omit ): Promise => { try { - const { data } = await axios.post(API_URL + '/api/users/settings/', setting, { + const { data } = await axios.post(getApiUrl() + '/api/users/settings/', setting, { headers: { Authorization: `Bearer ${token}`, }, @@ -1,6 +1,6 @@ import axios, { AxiosResponse } from 'axios' -import { API_URL } from '#/shared/lib/constants' +import { getApiUrl } from '#/shared/lib/constants' import { AccountType } from '../model/types' export const getAccountType = async (token: string | null | undefined): Promise => { @@ -10,7 +10,7 @@ export const getAccountType = async (token: string | null | undefined): Promise< try { const { data } = await axios.get>( - API_URL + '/auth/account-type', + getApiUrl() + '/auth/account-type', { headers: { Authorization: `Bearer ${token}`, @@ -1,12 +1,12 @@ import axios from 'axios' -import { API_URL } from '#/shared/lib/constants' +import { getApiUrl } from '#/shared/lib/constants' import { IUserSetting } from '../model/types' export const getUserSettings = async (token: string): Promise => { try { - const { data } = await axios.get(API_URL + '/api/users/settings/', { + const { data } = await axios.get(getApiUrl() + '/api/users/settings/', { headers: { Authorization: `Bearer ${token}`, }, @@ -1,5 +1,4 @@ import { api } from '#/shared/api' -import { API_URL } from '#/shared/lib/constants' import { Agent } from 'https' import { IUserSetting } from '../model/types' @@ -1,6 +1,6 @@ import axios from 'axios' -import { API_URL } from '#/shared/lib/constants' +import { getApiUrl } from '#/shared/lib/constants' import { getUpdatedSettingsLocal } from '../lib/helpers/update-setting-local' import { IUserSetting, SettingValueType } from '../model/types' @@ -13,7 +13,7 @@ export const updateUserSettings = async ( ) => { try { const { data } = await axios.put( - API_URL + `/api/users/settings/${id}`, + getApiUrl() + `/api/users/settings/${id}`, { value: value }, { headers: { @@ -29,7 +29,15 @@ export function useGlobalSettings() { makePrivateRequest(async (type: string, value: any) => { const option = settings.find((x) => x.type === type) - if (!option) return showMessage('Ошибка присвоения настроек') + if (!option) { + // Если настройки нет, создаём её + const { status, data: newSetting } = await postUserSettings({ device, type, value }) + + if (status !== 200) return showMessage('Ошибка создания настроек') + + setSettings((s) => [...s, newSetting]) + return + } const { status } = await updateUserSettings(option.id, value) @@ -37,7 +45,7 @@ export function useGlobalSettings() { setSettings((s) => [...s.filter((x) => x.type !== type), { ...option, value }]) }), - [settings, data] + [settings, data, device] ) const fetchUserSettings = makePrivateRequest(async () => { @@ -2,7 +2,7 @@ import { createAsyncThunk, createSlice } from '@reduxjs/toolkit' import axios, { AxiosResponse } from 'axios' import { loadingThunk } from '#/app/store/store' -import { API_URL } from '#/shared/lib/constants' +import { getApiUrl } from '#/shared/lib/constants' type UserState = { status: loadingThunk @@ -33,11 +33,13 @@ export type ResponseAllInfo = { } payment_plan: { uid: string + is_recurring: boolean plan: { uid: string price: string individual: boolean tokens_per_plan: string + is_corporate: boolean title: string duration: string accessed_models: string[] | null @@ -74,10 +76,12 @@ const initialState: UserState & ResponseAllInfo = { account_type: 'regular', payment_plan: { uid: '', + is_recurring: false, plan: { uid: '', price: '', tokens_per_plan: '', + is_corporate: false, individual: false, duration: '', title: '', @@ -90,7 +94,7 @@ const initialState: UserState & ResponseAllInfo = { } export const getAll = async (token: string | null | undefined): Promise => { - const { data } = await axios.get>(API_URL + '/auth/me', { + const { data } = await axios.get>(getApiUrl() + '/auth/me', { headers: { Authorization: `Bearer ${token}`, }, @@ -105,7 +109,7 @@ export const getAllInfo = createAsyncThunk('user/getAllInfo', async (token: stri }) export const unfollowEmail = createAsyncThunk('user/unfollowEmail', async (token: string | null | undefined) => { - await axios.patch(API_URL + '/auth/email-sub', {}, { headers: { Authorization: `Bearer ${token}` } }) + await axios.patch(getApiUrl() + '/auth/email-sub', {}, { headers: { Authorization: `Bearer ${token}` } }) }) export const userSlice = createSlice({ @@ -7,7 +7,6 @@ import { useSession } from 'next-auth/react' import { ButtonGray, ButtonUI, Error, InputStyleDark, Loader, Modal } from '#/shared' import { accountApi } from '#/shared/api/account-endpoints' -import { API_URL } from '#/shared/lib/constants' import { useShowDataStore } from '#/shared/lib/hooks/use-show-data' import { DateInput } from '#/shared/ui/date-input/date-input' @@ -1,6 +1,6 @@ import axios, { AxiosResponse } from 'axios' -import { API_URL } from '#/shared/lib/constants' +import { getApiUrl } from '#/shared/lib/constants' import { ResponseGetBusinessGroups, ResponseGetPersons } from '../model/types' @@ -15,7 +15,7 @@ export const addBusinessGroup = async ( { uid: string; title: string; token_limit: string }, AxiosResponse >( - API_URL + '/auth/business-groups/', + getApiUrl() + '/auth/business-groups/', { title: title, token_limit: token_limit, @@ -1,6 +1,6 @@ import axios, { AxiosResponse } from 'axios' -import { API_URL } from '#/shared/lib/constants' +import { getApiUrl } from '#/shared/lib/constants' import { ResponseGetBusinessGroups, ResponseGetPersons } from '../model/types' @@ -10,7 +10,7 @@ export const addBusinessGroupUser = async ( token?: string ): Promise => { try { - const { data } = await axios.post(API_URL + `/auth/business-groups/${group_id}/accounts/${email}/`, { + const { data } = await axios.post(getApiUrl() + `/auth/business-groups/${group_id}/accounts/${email}/`, { headers: { Authorization: `Bearer ${token}`, }, @@ -1,6 +1,6 @@ import axios, { AxiosResponse } from 'axios' -import { API_URL } from '#/shared/lib/constants' +import { getApiUrl } from '#/shared/lib/constants' import { ResponseGetBusinessGroups } from '../model/types' @@ -12,7 +12,7 @@ export const changeBusinessGroup = async ( ): Promise => { try { const { data } = await axios.put( - API_URL + `/auth/business-groups/${group_id}/`, + getApiUrl() + `/auth/business-groups/${group_id}/`, { title: title, token_limit: token_limit, @@ -1,6 +1,6 @@ import axios, { AxiosResponse } from 'axios' -import { API_URL } from '#/shared/lib/constants' +import { getApiUrl } from '#/shared/lib/constants' import { ResponseGetBusinessGroups, ResponseGetPersons } from '../model/types' @@ -10,7 +10,7 @@ export const deleteBusinessGroupUser = async ( token?: string ): Promise => { try { - const { data } = await axios.delete(API_URL + `/auth/business-groups/${group_id}/accounts/${email}/`, { + const { data } = await axios.delete(getApiUrl() + `/auth/business-groups/${group_id}/accounts/${email}/`, { headers: { Authorization: `Bearer ${token}`, }, @@ -1,13 +1,13 @@ import axios, { AxiosResponse } from 'axios' -import { API_URL } from '#/shared/lib/constants' +import { getApiUrl } from '#/shared/lib/constants' import { ResponseGetBusinessGroups } from '../model/types' export const getBusinessGroup = async (group_id: string, token?: string): Promise => { try { const { data } = await axios.get( - API_URL + `/auth/business-groups/${group_id}/`, + getApiUrl() + `/auth/business-groups/${group_id}/`, { headers: { Authorization: `Bearer ${token}`, @@ -14,7 +14,7 @@ import { ResponseGetBusinessGroups } from '#/features/business-group/model/types import { ResponseGetPersons } from '#/features/invite-person-in-business' import { IEmailForms } from '#/features/register-by-email/model/types' import { ButtonGray, ButtonUI, Error, InputStyleDark, Loader, Modal, ModalProps } from '#/shared' -import { API_URL } from '#/shared/lib/constants' +import { getApiUrl } from '#/shared/lib/constants' import styles from './add-business-group.module.scss' import { useShowDataStore } from '#/shared/lib/hooks/use-show-data' @@ -50,7 +50,7 @@ export const ChangeBusinessGroup: FC = ({ open, onClose, compa }, [current_group, session?.access]) useEffect(() => { - axios.get(API_URL + '/auth/business-host/accounts?have_group=false', { + axios.get(getApiUrl() + '/auth/business-host/accounts?have_group=false', { headers: { Authorization: `Bearer ${session?.access}` }, }).then((res) => { setAddUsers(res.data) @@ -1,11 +1,11 @@ import axios from 'axios' -import { API_URL } from '#/shared/lib/constants' +import { getApiUrl } from '#/shared/lib/constants' export const downloadFileReq = async (start_date: string, end_date: string, token?: string): Promise => { try { const { data } = await axios.get( - API_URL + `/auth/business-security/download-report?end_date=${end_date}&start_date=${end_date}`, + getApiUrl() + `/auth/business-security/download-report?end_date=${end_date}&start_date=${end_date}`, { headers: { Authorization: `Bearer ${token}`, @@ -6,7 +6,7 @@ import { useRouter } from 'next/navigation' import { useSession } from 'next-auth/react' import { ButtonGray, ButtonUI, Error, Loader, Modal } from '#/shared' -import { API_URL } from '#/shared/lib/constants' +import { getApiUrl } from '#/shared/lib/constants' import { DateInput } from '#/shared/ui/date-input/date-input' import styles from './invite-modal.module.scss' @@ -22,7 +22,7 @@ export const DownloadModal = ({ open, setOpen }: { open: boolean; setOpen: React const downloadFile = () => { setIsLoading(true) axios.get( - API_URL + + getApiUrl() + `/auth/business-security/download-report?${startDate ? `start_date=${startDate}` : ''}${endDate ? `&end_date=${endDate}` : ''}`, { responseType: 'arraybuffer', @@ -1,14 +1,14 @@ import axios, { AxiosResponse } from 'axios' import { ResponseGetPersons } from '#/features/invite-person-in-business' -import { API_URL } from '#/shared/lib/constants' +import { getApiUrl } from '#/shared/lib/constants' import { InviteRoles, inviteRoles } from '../../invite-person-in-business/lib/constants' export const updateLimit = async (email?: string, limit?: string, token?: string, role?: InviteRoles) => { try { const { data } = await axios.put<{ token_limit: string; role: string }, AxiosResponse>( - API_URL + `/auth/business-host/accounts/${email}`, + getApiUrl() + `/auth/business-host/accounts/${email}`, { token_limit: limit, account_privileges: Object.entries(inviteRoles).find(([key, value]) => value === role)![0], @@ -9,6 +9,7 @@ import styles2 from '#/widgets/business-persons/ui/persons-list/persons-list.mod import { InviteRoles, inviteRoles } from '../../invite-person-in-business/lib/constants' import { updateLimit } from '../api/set-limit' +import { useShowDataStore } from '#/shared/lib/hooks/use-show-data' interface LimitModalProps { person: ResponseGetPersons | null @@ -20,28 +21,35 @@ interface LimitModalProps { addList: (newUser: ResponseGetPersons, list: 'personal' | 'security') => void } -export const LimitModal: React.FC = ({ - person, - onClose, - updateLimitProp, - list, - setList, - addList, - listType, -}) => { +export const LimitModal: React.FC = ({ person, onClose, updateLimitProp, list, setList, addList, listType }) => { const [limit, setLimit] = useState(person?.token_limit ? Math.floor(+person.token_limit).toString() : '0') const [role, setRole] = useState('Сотрудник') + const [originalRole, setOriginalRole] = useState('Сотрудник') const { data } = useSession() + const { showMessage } = useShowDataStore() + + const getRoleFromAccountType = (accountType: string | undefined): InviteRoles => { + switch (accountType) { + case 'business_account': + return 'Сотрудник' + case 'business_admin': + return 'Администратор' + case 'business_security': + return 'Сотрудник безопасности' + default: + return 'Сотрудник' + } + } const handleChangeBalance = () => { + const originalRoleValue = getRoleFromAccountType(person?.account_type) + updateLimit(person?.email, limit, data?.access, role).then((res) => { if (res === null) { return } else if (list && res.account_type !== person?.account_type) { if ( - (res.account_type === 'business_host' || - res.account_type === 'business_account' || - res.account_type === 'business_admin') && + (res.account_type === 'business_host' || res.account_type === 'business_account' || res.account_type === 'business_admin') && listType === 'security' ) { setList(list.filter((el) => el.email !== res.email)) @@ -52,19 +60,19 @@ export const LimitModal: React.FC = ({ } } - updateLimitProp(res.token_limit, person?.email) + if (role !== originalRoleValue) { + showMessage(`Роль пользователя ${person?.email} успешно изменена на "${role}"!`, 'success') + } else { + updateLimitProp(res.token_limit, person?.email) + } }) } useEffect(() => { - if (person?.account_type === 'business_account') { - setRole('Сотрудник') - } - if (person?.account_type === 'business_admin') { - setRole('Администратор') - } - if (person?.account_type === 'business_security') { - setRole('Сотрудник безопасности') + if (person) { + const roleFromAccount = getRoleFromAccountType(person.account_type) + setRole(roleFromAccount) + setOriginalRole(roleFromAccount) } }, [person]) @@ -84,13 +92,7 @@ export const LimitModal: React.FC = ({ Изменение лимита токенов - setLimit(e.target.value)} - fullWidth - sx={{ ...InputStyleDark }} - /> + setLimit(e.target.value)} fullWidth sx={{ ...InputStyleDark }} /> @@ -1,6 +1,6 @@ import axios, { AxiosResponse } from 'axios' -import { API_URL } from '#/shared/lib/constants' +import { getApiUrl } from '#/shared/lib/constants' import { AllowedModels } from './types' export const changeAccess = async ( @@ -11,7 +11,7 @@ export const changeAccess = async ( if (!value) { try { const { data } = await axios.put<{ models: string }, AxiosResponse>( - API_URL + '/auth/business-host/allowed-models', + getApiUrl() + '/auth/business-host/allowed-models', { models: [title], }, @@ -30,7 +30,7 @@ export const changeAccess = async ( if (value) { try { const { data } = await axios.delete<{ models: string }, AxiosResponse>( - API_URL + '/auth/business-host/allowed-models', + getApiUrl() + '/auth/business-host/allowed-models', { headers: { Authorization: `Bearer ${token}`, @@ -3,7 +3,7 @@ import axios from 'axios' import { RootState } from '#/app/store/store' import { api, createChat as createChatApi, getAllChats } from '#/shared/api/endpoints' -import { API_URL } from '#/shared/lib/constants' +import { getApiUrl } from '#/shared/lib/constants' import { Chat } from './types' @@ -46,7 +46,7 @@ export const createChatForModel = createAsyncThunk( 'chats/deleteByUid', async ({ uid, token }) => { - await axios.delete(API_URL + `/chats/${uid}/`, { + await axios.delete(getApiUrl() + `/chats/${uid}/`, { headers: { Authorization: `Bearer ${token}` }, }) return uid @@ -3,7 +3,7 @@ import axios, { AxiosResponse } from 'axios' import { Dayjs } from 'dayjs' import { RequestStats, ResponseStats } from '#/features/get-admin-stats' -import { API_URL } from '#/shared/lib/constants' +import { getApiUrl } from '#/shared/lib/constants' async function getStats( token: string | null | undefined, @@ -32,7 +32,7 @@ async function getStats( try { const { data } = await axios.get>( - API_URL + '/admin/stats' + queryParams, + getApiUrl() + '/admin/stats' + queryParams, { headers: { Authorization: `Bearer ${token}`, @@ -1,6 +1,6 @@ import axios, { AxiosResponse } from 'axios' -import { API_URL } from '#/shared/lib/constants' +import { getApiUrl } from '#/shared/lib/constants' import { InviteRoles, inviteRoles } from '../lib/constants' import { ResponseGetPersons } from '../model/types' @@ -13,7 +13,7 @@ export const invitePerson = async ( ) => { return await axios.post<{ email: string; token_limit: string }, AxiosResponse>( - API_URL + '/auth/business-host', + getApiUrl() + '/auth/business-host', { group: group === '' ? null : group, email, @@ -0,0 +1,16 @@ +import { useEffect } from 'react' + +import { getModalById, LOW_BALANCE_OFFER } from '#/features/modals' + +import { LowBalanceOfferPlate } from './low-balance-offer-plate' + + +export const LowBalanceOfferOnStart = () => { + const modal = getModalById(LOW_BALANCE_OFFER) + + useEffect(() => { + modal.setState(true) + }, []) + + return +} @@ -0,0 +1,47 @@ +.container { + width: 100%; + max-width: 570px; + padding: 32px 0 0 0; + display: flex; + flex-direction: column; + background-color: #151518; + align-items: flex-start; +} + +.text { + color: #8B8B8B; + font-size: 17px; + line-height: 1.5; + margin: 16px 0 24px; +} + +.text_heading { + font-weight: 800; + font-size: 48px; + line-height: 42px; +} + +.offersWrapper { + display: flex; + flex-direction: row; + justify-content: center; + flex-wrap: wrap; + gap: 20px; + width: 100%; + margin-bottom: 24px; + border-radius: 30px; + box-shadow: + 0 0 20px rgba(100, 180, 255, 0.12), + 0 0 40px rgba(80, 150, 255, 0.08), +} + +.link { + font-weight: 600; + font-style: SemiBold; + font-size: 16px; + align-self: center; + + p { + color:#80808E; + } +} @@ -0,0 +1,72 @@ +import React, { useEffect, useState } from 'react' +import Link from 'next/link' +import { useSession } from 'next-auth/react' + +import { LOW_BALANCE_OFFER, getModalById, PlateTemplate } from '#/features/modals' +import { c } from '#/shared' +import { accountApi } from '#/shared/api/account-endpoints' +import { getDeviceType } from '#/shared/lib/helpers' +import { IOffer } from '#/views/subscription' +import Offer from '#/views/subscription/ui/offer' + +import styles from './low-balance-offer-plate.module.scss' + +export const LowBalanceOfferPlate = () => { + const modal = getModalById(LOW_BALANCE_OFFER) + const { data } = useSession() + const [offers, setOffers] = useState(null) + + useEffect(() => { + if (data?.access) { + accountApi.getPaymentsPlans(data.access).then((res) => { + if (res) setOffers(res) + }) + } + }, [data?.access]) + + const handleClose = () => { + modal.setState(false) + } + + const offerIndex = modal.getStoreProperty('offerIndex') ?? 1 + const offerUid = modal.getStoreProperty('offerUid'); + const notEnoughtTokens = modal.getStoreProperty('notEnoughtTokens') + + const offerToShow = offerUid + ? offers?.find((o) => o.uid === offerUid) + : offerIndex + ? offers?.[offerIndex] + : offers?.[1] + + return ( + +
e.stopPropagation()}> + {notEnoughtTokens ? ( +

У вас не осталось
токенов...

+ ) : ( +

Осталось
мало токенов...

+ )} +

+ Подобрали для Вас наиболее подходящий тариф,
+ взгляните на преимущества! +

+
+ {offers === null ? ( +

Загрузка тарифов...

+ ) : offerToShow ? ( + + ) : null} +
+ +

Нет, я хочу посмотреть все тарифы

+ +
+
+ ) +} @@ -0,0 +1,80 @@ +import { useEffect, useRef } from 'react' +import axios from 'axios' + +import { useAppSelector } from '#/app/store/store' +import { getModalById, LOW_BALANCE_OFFER } from '#/features/modals' +import { LowBalanceOfferPlate } from './low-balance-offer-plate' +import type { ResponseAllInfo } from '#/entities/user-account/model/user-type-slice' +import { useFeatureFlag } from '#/shared/lib/hooks/use-feature-flag' +import { balanceSelector } from '#/shared/lib/selectors' + + +export const LowBalanceOfferTrigger = () => { + const balance = useAppSelector(balanceSelector) + const me = useAppSelector((state) => state.user) as ResponseAllInfo & { status: string; referral: string } + const isFewTokensModalEnabled = useFeatureFlag('few-tokens-modal-enabled') + const prevBalanceRef = useRef(null) + const modal = getModalById(LOW_BALANCE_OFFER) + + useEffect(() => { + const interceptorId = axios.interceptors.response.use( + (response) => { + if (response.status === 402 && response?.data?.detail?.includes('Баланс')) { + invokeLowBalanceOffer(true) + } + return response + }, + ) + + return () => { + axios.interceptors.response.eject(interceptorId) + } + }, []) + + useEffect(() => { + + if (!isFewTokensModalEnabled) return + + // Пропускаем первый рендер (начальная загрузка баланса) + if (prevBalanceRef.current === null) { + prevBalanceRef.current = balance + return + } + + if (prevBalanceRef.current !== balance) { + prevBalanceRef.current = balance + + if (typeof window !== 'undefined' && sessionStorage.getItem('low-balance-offer-shown') === 'true') { + return + } + + invokeLowBalanceOffer(); + } + }, [balance, me, modal, isFewTokensModalEnabled]) + + const invokeLowBalanceOffer = (notEnoughtTokens: boolean = false) => { + const tokensPerPlan = Number(me.payment_plan?.plan?.tokens_per_plan) + const isCorporate = me.payment_plan?.plan?.is_corporate + const isFreePlan = tokensPerPlan <= 10 + const threshold = tokensPerPlan * 0.15 + const freePlanThreshold = tokensPerPlan * 0.3 + + if (isCorporate) { + return + } + + if (!isFreePlan && balance < threshold) { + modal.setState(true, { offerUid: me.payment_plan?.plan?.uid, me, notEnoughtTokens }) + sessionStorage.setItem('low-balance-offer-shown', 'true') + return + } + + if (isFreePlan && balance < freePlanThreshold) { + modal.setState(true, { offerIndex: 1, notEnoughtTokens }) + sessionStorage.setItem('low-balance-offer-shown', 'true') + return + } + } + + return +} @@ -0,0 +1,3 @@ +export { LowBalanceOfferPlate } from './ui/low-balance-offer-plate' +export { LowBalanceOfferOnStart } from './ui/low-balance-offer-on-start' +export { LowBalanceOfferTrigger } from './ui/low-balance-offer-trigger' @@ -2,3 +2,5 @@ export const PLATE_CHANGE_PASSWORD = 'plate-change-password' export const RESEND_INVATION_PASSWORD = 'resend-invation-password' export const ERROR_REPORT = 'error-report' export const BUSINESS_ERROR_REPORT = 'business-error-report' +export const LOW_BALANCE_OFFER = 'low-balance-offer' +export const SUBSCRIPTION_CHANGE_NOTIFICATION = 'subscription-change-notification' @@ -13,11 +13,12 @@ function makeModalInstance(set: Set, get: Get, key: string) { store: {}, setState(state, store) { const modals = get().modals; + const currentModal = modals[key]; - store = store ?? {}; + const newStore = store !== undefined ? store : (currentModal?.store ?? {}); set(() => ({ - modals: { ...modals, [this.id]: { ...modals[key], state, store } }, + modals: { ...modals, [this.id]: { ...modals[key], state, store: newStore } }, })); }, setStoreProperty(key, value) { @@ -3,7 +3,7 @@ width: 100%; max-width: calc(100dvw); height: 100dvh; - background-color: rgba(0, 0, 0, 0.4); + background-color: rgba(0, 0, 0, 0.7); top: 0; left: 0; transition: all 300ms ease-in-out; @@ -66,7 +66,7 @@ &__content { background-color: var(--new-ui-main-color); - border-radius: 20px; + border-radius: 42px; position: relative; overflow: hidden; height: fit-content; @@ -10,7 +10,7 @@ import { Frequency, FrequencySelect, } from '#/features/register-business/lib/constants-step-information' -import { API_URL } from '#/shared/lib/constants' +import { getApiUrl } from '#/shared/lib/constants' export const createAccount = async (information: DataForCreate, token: string | null) => { const indexOfS = Object.values(FrequencySelect).indexOf(information.frequency as unknown as FrequencySelect) @@ -32,7 +32,7 @@ export const createAccount = async (information: DataForCreate, token: string | job_title: information.job_title, } - const res = await axios.post(API_URL + '/auth/business-host/create', data, { + const res = await axios.post(getApiUrl() + '/auth/business-host/create', data, { headers: { Authorization: `Bearer ${token}`, }, @@ -1,13 +1,14 @@ import axios, { AxiosResponse } from 'axios' -import * as process from 'process' +import { getEnv } from '#/shared/lib/env-store' import { DaDataResponse } from './types' -const url = process.env.NEXT_PUBLIC_URL_DADATA as string - -const token = process.env.NEXT_PUBLIC_TOKEN_DADATA export const getCompanyData = async (prompt: any) => { - if (!token) { + const env = getEnv() + const url = env?.NEXT_PUBLIC_URL_DADATA + const token = env?.NEXT_PUBLIC_TOKEN_DADATA + + if (!token || !url) { return null } @@ -16,15 +16,16 @@ import { PasswordOptions } from '#/features/register-by-email/lib/constants' import { IEmailForms } from '#/features/register-by-email/model/types' import { emailOptions } from '#/shared' import { InputStyleDark } from '#/shared' -import { API_URL } from '#/shared/lib/constants/constants' +import { getApiUrl } from '#/shared/lib/constants' import { useShowDataStore } from '#/shared/lib/hooks/use-show-data' import { useDeviceType } from '#/shared/lib/hooks/use-device-type' interface IRegisterEmailFormProps { successLogin: () => void + inputClassName?: string } -export const RegisterEmailForm: React.FC = ({ successLogin }) => { +export const RegisterEmailForm: React.FC = ({ successLogin, inputClassName }) => { const { register, handleSubmit, reset, watch } = useForm() const { desktop } = useDeviceType() @@ -64,7 +65,7 @@ export const RegisterEmailForm: React.FC = ({ successLo req_data.referer = localStorage.getItem('referral') } - const { status, data: result } = await axios.post(API_URL + '/auth/register', req_data) + const { status, data: result } = await axios.post(getApiUrl() + '/auth/register', req_data) if (status !== 201) { setLoading(false) @@ -94,45 +95,19 @@ export const RegisterEmailForm: React.FC = ({ successLo return (
- - Email - - - Пароль - - @@ -153,30 +128,18 @@ export const RegisterEmailForm: React.FC = ({ successLo ), }} + className={inputClassName} sx={{ ...InputStyleDark }} {...register('password1', { ...PasswordOptions, })} /> - - Подтвердите пароль - - { - return await axios.delete(API_URL + `/auth/business-host`, { + return await axios.delete(getApiUrl() + `/auth/business-host`, { data: { uid: person_uid, }, @@ -1,9 +1,9 @@ -import { API_URL } from '#/shared/lib/constants' +import { getApiUrl } from '#/shared/lib/constants' import axios from 'axios' export const resendInvation = async (email: string, token?: string) => { return await axios.post( - API_URL + `/auth/business-host/re-invite/${email}`, + getApiUrl() + `/auth/business-host/re-invite/${email}`, {}, { headers: { @@ -0,0 +1,44 @@ +.container { + width: 100%; + max-width: 920px; + padding: 32px 0 12px 0px; + display: flex; + flex-direction: column; + align-items: flex-start; +} + +.text_heading { + font-weight: 800; + font-size: 56px; + margin-bottom: 16px; +} + +.text { + color: #ffffff; + font-size: 18px; + font-weight: 500; + margin: 0 0 24px; +} + +.highlight { + color: #7F7DF3; +} + +.buttons { + display: flex; + justify-content: flex-end; + flex-direction: row; + align-items: flex-end; + margin-top: 12px; + gap: 8px; +} + +.buttonSecondary { + background-color: #2A2B30; + color: #ffffff; + border: none; + border-radius: 12px; + padding: 15px 25px; + cursor: pointer; + text-transform: none; +} @@ -0,0 +1,75 @@ +import React from 'react' + +import { getModalById, PlateTemplate, SUBSCRIPTION_CHANGE_NOTIFICATION } from '#/features/modals' +import { c } from '#/shared' +import { CommonButton } from '#/shared/ui/button' + +import styles from './subscription-change-notification-plate.module.scss' + +const formatTokens = (num: number) => num.toLocaleString('ru-RU') + +export const SubscriptionChangeNotificationPlate = () => { + const modal = getModalById(SUBSCRIPTION_CHANGE_NOTIFICATION) + + const fromTokens = modal.getStoreProperty('fromTokens') ?? 0 + const toTokens = modal.getStoreProperty('toTokens') ?? 0 + const isUpgrade = modal.getStoreProperty('isUpgrade') ?? false + const tokensToAdd = modal.getStoreProperty('tokensToAdd') + + const handleClose = () => { + modal.setState(false) + } + + const handleConfirm = () => { + modal.getStoreProperty<() => void>('onConfirm')?.() + modal.setState(false) + } + + return ( + +
e.stopPropagation()}> +

Смена подписки

+ + {tokensToAdd != null ? ( +

+ {fromTokens === toTokens ? ( + <> + При балансе{' '} + {formatTokens(toTokens - tokensToAdd)} токенов + {' '}вы докупите ещё{' '} + {formatTokens(tokensToAdd)} токенов. Продолжить? + + ) : ( + <> + Вы докупите{' '} + {formatTokens(tokensToAdd)} токенов + {' '}(лимит плана {formatTokens(toTokens)} − баланс {formatTokens(toTokens - tokensToAdd)} = {formatTokens(tokensToAdd)}). Продолжить? + + )} +

+ ) : !isUpgrade ? ( +

+ Внимание! Вы собираетесь перейти с подписки{' '} + {formatTokens(fromTokens)} токенов на подписку{' '} + {formatTokens(toTokens)} токенов. +
+ Уведомляем Вас, что при смене подписки сгорают все неиспользованные в прежней подписке токены. Продолжить? +

+ ) : ( +

+ Уведомляем вас, что при смене подписки часть токенов сверх лимита текущей подписки сгорит и не перенесётся на новый план. Продолжить? +

+ )} + +
+ + {tokensToAdd != null && fromTokens === toTokens ? 'Подтвердить' : 'Подтвердить смену тарифа'} + + + Отмена + +
+
+
+ ) +} @@ -0,0 +1,5 @@ +import { SubscriptionChangeNotificationPlate } from './subscription-change-notification-plate' + +export const SubscriptionChangeNotificationTrigger = () => { + return +} @@ -0,0 +1,2 @@ +export { SubscriptionChangeNotificationPlate } from './ui/subscription-change-notification-plate' +export { SubscriptionChangeNotificationTrigger } from './ui/subscription-change-notification-trigger' @@ -3,7 +3,7 @@ import axios from 'axios' import { useSession } from 'next-auth/react' import { dataLine as data } from '#/shared' -import { API_URL } from '#/shared/lib/constants' +import { getApiUrl } from '#/shared/lib/constants' type ResponseStats = { source: string; amount: string }[] @@ -28,7 +28,7 @@ const statsColors = [ const getStatistic = async (type: string, interval: string, token?: string) => { return await axios.get( - API_URL + `/payments/expenses?source_strategy=${type}&interval_strategy=${interval}`, + getApiUrl() + `/payments/expenses?source_strategy=${type}&interval_strategy=${interval}`, { headers: { Authorization: `Bearer ${token}` }, validateStatus: () => true, @@ -13,7 +13,7 @@ export abstract class AuthConstants { public static readonly client_secret_yandex = process.env.NEXT_PUBLIC_CLIENT_SECRET_YANDEX - public static readonly sessionTime = Number(process.env.SESSION_TIME) + public static readonly sessionTime = Number(process.env.NEXT_PUBLIC_SESSION_TIME) * 1000 private static readonly expiresTime = this.sessionTime @@ -1,13 +1,13 @@ import axios from 'axios' import { JWT } from 'next-auth/jwt' -import { API_URL } from '#/shared/lib/constants' +import { getApiUrl } from '#/shared/lib/constants' import { AuthConstants } from './constants' export class AuthorizationProxy { public readonly loginByEmail = async (email: string, password: string) => { - return await fetch(API_URL + '/auth/login', { + return await fetch(getApiUrl() + '/auth/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email, password }), @@ -15,7 +15,7 @@ export class AuthorizationProxy { } public readonly loginByEmailToken = async (emailToken: string) => { - return await fetch(API_URL + `/auth/login-from-token/${emailToken}`, { + return await fetch(getApiUrl() + `/auth/login-from-token/${emailToken}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, }) @@ -23,7 +23,7 @@ export class AuthorizationProxy { public readonly exchangeTokenYandex = async (yandex_token: string) => { return await axios.post<{ access_token: string } | { error_description: string }>( - API_URL + '/auth/login-social/convert-token', + getApiUrl() + '/auth/login-social/convert-token', { grant_type: 'convert_token', client_id: AuthConstants.django_app_client_id_yandex, @@ -50,7 +50,7 @@ export class AuthorizationProxy { public readonly refreshToken = async (tokenObject: JWT): Promise => { try { const { data } = await axios.post<{ access?: string; refresh?: string }>( - API_URL + '/auth/refresh', + getApiUrl() + '/auth/refresh', { refresh: tokenObject.refresh } ) return { @@ -0,0 +1,18 @@ +import type { NextApiRequest, NextApiResponse } from 'next' + + +export default function handler(req: NextApiRequest, res: NextApiResponse) { + if (req.method !== 'GET') { + return res.status(405).json({ error: 'Method not allowed' }) + } + + const env = Object.entries(process.env) + .filter(([key]) => key.startsWith('NEXT')) + .sort(([a], [b]) => a.localeCompare(b)) + .reduce>((acc, [key, value]) => { + acc[key] = value ?? '' + return acc + }, {}) + + res.status(200).json(env) +} @@ -13,10 +13,13 @@ import { FlagProvider } from '@unleash/proxy-client-react' import { store } from '#/app/store/store' import { Error } from '#/shared' +import { LowBalanceOfferTrigger } from '#/features/low-balance-offer' +import { SubscriptionChangeNotificationTrigger } from '#/features/subscription-change-notification' import { TourManager, getTourSteps, TourCard, setTourCompleted } from '#/features/nextstep-tour' 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 { Providers } from '#/widgets/providers' import ErrorBoundary from './error-boundary' @@ -92,6 +95,8 @@ function AppContent({ Component, pageProps }: { Component: NextPageWithLayout; p {Component.getLayout ? Component.getLayout() : } + + @@ -106,19 +111,21 @@ function AppContent({ Component, pageProps }: { Component: NextPageWithLayout; p function App({ Component, pageProps: { session, ...pageProps } }: AppPropsWithLayout) { useBlockTelegram() + const { env } = useEnv() + + // Fallback на process.env для SSR/prerender — useEnv пустой до fetch + const unleashUrl = env?.NEXT_PUBLIC_UNLEASH_URL || process.env.NEXT_PUBLIC_UNLEASH_URL || '' + const unleashClientKey = env?.NEXT_PUBLIC_UNLEASH_CLIENT_KEY || process.env.NEXT_PUBLIC_UNLEASH_CLIENT_KEY || '' + const unleashAppName = env?.NEXT_PUBLIC_UNLEASH_APP_NAME || process.env.NEXT_PUBLIC_UNLEASH_APP_NAME || '' return ( <> - + @@ -3,12 +3,12 @@ import axios from 'axios' import { VideoLink, VideoModel } from './models' import { IModel,IShortModel } from '#/entities/model-entity' -import { API_URL } from '#/shared/lib/constants' +import { getApiUrl } from '#/shared/lib/constants' const model_api = { async getBots(token?: string): Promise { try { - const { data } = await axios.get(API_URL + '/ml_models/?category=chat-bots', { + const { data } = await axios.get(getApiUrl() + '/ml_models/?category=chat-bots', { headers: { Authorization: `Bearer ${token}`, }, @@ -21,7 +21,7 @@ const model_api = { async getBotParams(slug: string, token?: string): Promise { try { - const { data } = await axios.get(API_URL + `/ml_models/${slug}`, { + const { data } = await axios.get(getApiUrl() + `/ml_models/${slug}`, { headers: { Authorization: `Bearer ${token}`, }, @@ -34,7 +34,7 @@ const model_api = { async getImages(token?: string): Promise { try { - const { data } = await axios.get(API_URL + '/ml_models/?category=images', { + const { data } = await axios.get(getApiUrl() + '/ml_models/?category=images', { headers: { Authorization: `Bearer ${token}`, }, @@ -51,7 +51,7 @@ const videos = { async getVideoLinks(token?: string): Promise { // Получение ссылок на изображения try { - const { data } = await axios.get(API_URL + '/media/video/links/', { + const { data } = await axios.get(getApiUrl() + '/media/video/links/', { headers: { Authorization: `Bearer ${token}`, }, @@ -65,7 +65,7 @@ const videos = { async getVideoModels(token?: string): Promise { // Получение всех моделей видео try { - const { data } = await axios.get(API_URL + '/ml_models/?category=videos', { + const { data } = await axios.get(getApiUrl() + '/ml_models/?category=videos', { headers: { Authorization: `Bearer ${token}`, }, @@ -83,7 +83,7 @@ const videos = { ): Promise { // Отправка сообщения для генерации видео try { - const response = await fetch(API_URL + `/media/video/${slug}`, { + const response = await fetch(getApiUrl() + `/media/video/${slug}`, { method: 'POST', headers: { 'Authorization': `Bearer ${token}`, @@ -1,12 +1,11 @@ import React, { useCallback, useEffect, useRef, useState } from 'react' import axios, { AxiosError, AxiosResponse } from 'axios' import { useSession } from 'next-auth/react' -import process from 'process' import { useAppDispatch } from '#/app/store/store' import { getUserBalance } from '#/entities/balance' import { Message, MessageSend } from '#/entities/message' -import { API_URL } from '#/shared/lib/constants' +import { getApiUrl } from '#/shared/lib/constants' import { Variant } from '#/shared/lib/hooks/use-show-data' import { Device } from '#/shared/lib/types/entities' import { IMessageRequest } from '#/shared/lib/types/types-gpt' @@ -24,7 +23,7 @@ const formDataHelper = (file: File, dataForSend: MessageSend): FormData => export const ModelsWithChatsEndpoints = { getData: async (chatUid: string, offset: number, token?: string) => { try { - const { data } = await axios.get(API_URL + `/chats/${chatUid}/messages/?limit=10&offset=${offset}`, { + const { data } = await axios.get(getApiUrl() + `/chats/${chatUid}/messages/?limit=10&offset=${offset}`, { headers: { Authorization: `Bearer ${token}`, }, @@ -42,7 +41,7 @@ export const ModelsWithChatsEndpoints = { const HeaderDataType = dataForSend instanceof FormData ? 'multipart/form-data' : 'application/json' return await axios.post>( - API_URL + `/chats/${chatUid}/messages/`, + getApiUrl() + `/chats/${chatUid}/messages/`, dataForSend, { withCredentials: true, @@ -173,7 +172,7 @@ export function useModel( } const deleteMessage = (message_uid: string) => { - axios.delete(process.env.NEXT_PUBLIC_API_HOST + `/chats/${currentChat}/messages/${message_uid}`, { + axios.delete(getApiUrl() + `/chats/${currentChat}/messages/${message_uid}`, { headers: { Authorization: `Bearer ${data?.access}`, }, @@ -188,7 +187,7 @@ export function useModel( export const ModelsWithImagesEndpoints = { getData: async (type: string, offset: number, token?: string) => { try { - const { data } = await axios.get(API_URL + `/media/image/${type}?limit=10&offset=${offset}`, { + const { data } = await axios.get(getApiUrl() + `/media/image/${type}?limit=10&offset=${offset}`, { headers: { Authorization: `Bearer ${token}`, }, @@ -206,7 +205,7 @@ export const ModelsWithImagesEndpoints = { const HeaderDataType = dataForSend instanceof FormData ? 'multipart/form-data' : 'application/json' try { - const { data } = await axios.post>(API_URL + `/media/image/${type}`, dataForSend, { + const { data } = await axios.post>(getApiUrl() + `/media/image/${type}`, dataForSend, { withCredentials: true, headers: { Authorization: `Bearer ${token}`, @@ -347,7 +346,7 @@ export function useModelImages(showMessage: (message: string) => void, type: export const ModelsMediaApi = { getData: async (type: string, token?: string) => { try { - const { data } = await axios.get(API_URL + `/media/audio/${type}`, { + const { data } = await axios.get(getApiUrl() + `/media/audio/${type}`, { headers: { Authorization: `Bearer ${token}`, }, @@ -363,7 +362,7 @@ export const ModelsMediaApi = { }, sendData: async (type: string | null, dataForSend: MessageSend | FormData, token?: string) => { try { - const { data } = await axios.post>(API_URL + `/media/audio/${type}`, dataForSend, { + const { data } = await axios.post>(getApiUrl() + `/media/audio/${type}`, dataForSend, { withCredentials: true, headers: { Authorization: `Bearer ${token}`, @@ -1,6 +1,6 @@ import axios from 'axios' -import { API_URL } from '#/shared/lib/constants' +import { getApiUrl } from '#/shared/lib/constants' export interface PredictPriceRequest { model_slug: string @@ -18,7 +18,7 @@ export async function predictPrice( token?: string ): Promise { const { data: result } = await axios.post( - API_URL + '/ml_model/predict-price/', + getApiUrl() + '/ml_model/predict-price/', data, { headers: { @@ -1,7 +1,7 @@ import axios, { AxiosResponse } from 'axios' import { User } from 'next-auth' -import { API_URL } from '#/shared/lib/constants' +import { getApiUrl } from '#/shared/lib/constants' import { IOffer } from '#/views/subscription' interface IUserBalance { @@ -25,7 +25,7 @@ interface DataForLogin { export const accountApi = { async getPaymentsPlans(token: string): Promise { try { - const { data } = await axios.get(API_URL + '/payments/plans', { + const { data } = await axios.get(getApiUrl() + '/payments/plans', { headers: { Authorization: `Bearer ${token}`, }, @@ -42,23 +42,26 @@ export const accountApi = { return null } - try { - const { data } = await axios.post>( - API_URL + '/payments/plans', - { - uid: plan, - is_test: 1, + const response = await axios.post>( + getApiUrl() + '/payments/plans', + { + uid: plan, + is_test: 1, + }, + { + headers: { + Authorization: `Bearer ${token}`, }, - { - headers: { - Authorization: `Bearer ${token}`, - }, - } - ) - return data.payment_url - } catch (err) { - return null + } + ) + + if (response.status >= 400) { + const error = new Error('Payment failed') as Error & { response: { data: unknown } } + error.response = { data: response.data } + throw error } + + return response.data.payment_url }, async changePassword( @@ -66,10 +69,10 @@ export const accountApi = { password_1: string, password_2: string, current_password: string - ): Promise { - try { - const { status } = await axios.put( - API_URL + '/auth/reset-pass', + ): Promise<{status:number,data:{detail:string}}>{ + + const response= await axios.put( + getApiUrl() + '/auth/reset-pass', { password_1, password_2, @@ -82,14 +85,12 @@ export const accountApi = { } ) - return status - } catch (err) { - return 400 - } + return response + }, async getApiKeys(token: string | null): Promise { - return await axios.get(API_URL + '/public/api-key', { + return await axios.get(getApiUrl() + '/public/api-key', { headers: { Authorization: `Bearer ${token}`, }, @@ -97,7 +98,7 @@ export const accountApi = { }, async createApiKeys(data: any, token: string | null): Promise { - return await axios.post(API_URL + '/public/api-key', data, { + return await axios.post(getApiUrl() + '/public/api-key', data, { headers: { Authorization: `Bearer ${token}`, }, @@ -105,7 +106,7 @@ export const accountApi = { }, async deleteApiKey(name: any, token: string | null): Promise { - return await axios.delete(API_URL + '/public/api-key', { + return await axios.delete(getApiUrl() + '/public/api-key', { data: { name, }, @@ -117,7 +118,7 @@ export const accountApi = { async loginByEmail(email: string, password: string): Promise { try { - const { data } = await axios.post>(API_URL + '/auth/login', { + const { data } = await axios.post>(getApiUrl() + '/auth/login', { email, password, }) @@ -130,7 +131,7 @@ export const accountApi = { async removeSub(token?: string) { try { - const { status } = await axios.delete(API_URL + '/payments/plans', { + const { status } = await axios.delete(getApiUrl() + '/payments/plans', { headers: { Authorization: `Bearer ${token}`, }, @@ -1,6 +1,5 @@ import axios, { AxiosResponse } from 'axios' import { Session } from 'next-auth' -import * as process from 'process' import { Chat } from '#/shared/api/type-model-chats' import { Error } from '#/shared/lib/types/entities' @@ -9,7 +8,7 @@ import { IMessageRequest, ISendMessageResponse } from '#/shared/lib/types/types- import { Message } from '../lib/types/model' -const API_URL = process.env.NEXT_PUBLIC_API_HOST +import { getApiUrl } from '#/shared/lib/constants' type Result = { question: string @@ -61,7 +60,7 @@ export const api = { async getMessagesChatGPT(token: string | null, uid: string): Promise { try { - const { data } = await axios.get(API_URL + `/chats/${uid}/messages/`, { + const { data } = await axios.get(getApiUrl() + `/chats/${uid}/messages/`, { headers: { Authorization: `Bearer ${token}`, }, @@ -79,7 +78,7 @@ export const api = { async updRenameChat(uid: string, title: string, token: string | undefined) { try { const { data } = await axios.put<{ uid: string; title: string; created_at: string }>( - API_URL + `/chats/${uid}/`, + getApiUrl() + `/chats/${uid}/`, { title: title }, { withCredentials: true, @@ -97,7 +96,7 @@ export const api = { async sendMessageChatGPT(token: string, message: any, uid: string): Promise { try { const { data } = await axios.post>( - API_URL + `/chats/${uid}/messages/`, + getApiUrl() + `/chats/${uid}/messages/`, message, { withCredentials: true, @@ -115,7 +114,7 @@ export const api = { async getFavoritesModel(token: string | null, session: Session | null): Promise { try { - const { data } = await axios.get(API_URL + `/ml_models/${session?.user?.name}`, { + const { data } = await axios.get(getApiUrl() + `/ml_models/${session?.user?.name}`, { headers: { Authorization: `Bearer ${token}`, }, @@ -129,7 +128,7 @@ export const api = { async getAllModels(token: string | null): Promise { try { - const { data } = await axios.get(API_URL + '/ml_models/', { + const { data } = await axios.get(getApiUrl() + '/ml_models/', { headers: { Authorization: token ? `Bearer ${token}` : '', }, @@ -146,7 +145,7 @@ export const api = { export const createChat = async (model: string, token?: string) => { try { const { data } = await axios.post( - API_URL + `/chats/?model=${model}`, + getApiUrl() + `/chats/?model=${model}`, { title: 'Новый чат', model, @@ -162,7 +161,7 @@ export const createChat = async (model: string, token?: string) => { export const getAllChats = async (model: string, token?: string): Promise => { try { - const { data } = await axios.get(API_URL + `/chats/?model=${model}`, { + const { data } = await axios.get(getApiUrl() + `/chats/?model=${model}`, { headers: { Authorization: `Bearer ${token}` }, }) @@ -181,7 +180,7 @@ export const getAllChats = async (model: string, token?: string): Promise { try { const { data, status } = await axios.put( - API_URL + '/ml_models/favourites ', + getApiUrl() + '/ml_models/favourites ', { uid: model_id, }, @@ -199,7 +198,7 @@ export const addToFavorites = async (model_id: string, token?: string) => { export const deleteFromFavorites = async (model_id: string, token?: string) => { try { - const { status, data } = await axios.delete(API_URL + '/ml_models/favorites', { + const { status, data } = await axios.delete(getApiUrl() + '/ml_models/favorites', { headers: { Authorization: `Bearer ${token}`, }, @@ -230,7 +229,7 @@ export interface PaymentHistory { export const getPaymentsHistory = async (token?: string) => { try { - const { data } = await axios.get(API_URL + '/payments/history', { + const { data } = await axios.get(getApiUrl() + '/payments/history', { headers: { Authorization: `Bearer ${token}`, }, @@ -256,7 +255,7 @@ export interface IReferral { export const getReferral = async (token?: string) => { try { - const { data } = await axios.get(API_URL + '/payments/referral-account', { + const { data } = await axios.get(getApiUrl() + '/payments/referral-account', { headers: { Authorization: `Bearer ${token}`, }, @@ -1,14 +1,21 @@ import axios from 'axios' +import { getApiUrl } from '#/shared/lib/constants' + export const api = axios.create({ validateStatus: () => true, - baseURL: process.env.NEXT_PUBLIC_API_HOST, +}) + +api.interceptors.request.use((config) => { + const baseURL = getApiUrl() + if (baseURL) config.baseURL = baseURL + return config }) api.interceptors.response.use( (response) => response, (error) => { - if (!error.response) { + if (!error.response && typeof window !== 'undefined') { window.location.href = '/network-error' } return error @@ -1,6 +1,11 @@ import * as process from 'process' -export const API_HOST = process.env.NEXT_PUBLIC_MAIN_URL +import { getEnv } from '#/shared/lib/env-store' + +export const getApiHost = () => + typeof window !== 'undefined' ? (getEnv()?.NEXT_PUBLIC_MAIN_URL ?? '') : (process.env.NEXT_PUBLIC_MAIN_URL ?? '') +export const getApiUrl = () => + typeof window !== 'undefined' ? (getEnv()?.NEXT_PUBLIC_API_HOST ?? '') : (process.env.NEXT_PUBLIC_API_HOST ?? '') export enum ModelPagesList { '/chatgpt', @@ -10,8 +15,6 @@ export enum ModelPagesList { '/kandinsky', } -export const API_URL = process.env.NEXT_PUBLIC_API_HOST - export const surpriseMePrompts = [ "Vintage 90's anime style. stylish model posing in 7/11 convenience store., sci-fi.", @@ -1 +1,6 @@ -export { API_HOST, API_URL, ModelPagesList, surpriseMePrompts } from './constants' +export { + getApiHost, + getApiUrl, + ModelPagesList, + surpriseMePrompts, +} from './constants' @@ -1,3 +1,4 @@ export { useAutoScroll } from './use-auto-scroll' export { useDeviceType } from './use-device-type' +export { useEnv } from './use-env' export { useFeatureFlag } from './use-feature-flag' \ No newline at end of file @@ -0,0 +1,62 @@ +import { useEffect, useState } from 'react' + +import { setEnv } from '#/shared/lib/env-store' + +type EnvRecord = Record + +const CACHE_TTL_MS = 5 * 60 * 1000 // 5 минут + +let cache: EnvRecord | null = null +let cacheTimestamp = 0 +let fetchPromise: Promise | null = null + +function isCacheValid(): boolean { + return cache !== null && Date.now() - cacheTimestamp < CACHE_TTL_MS +} + +function fetchEnv(): Promise { + if (isCacheValid()) return Promise.resolve(cache!) + if (fetchPromise) return fetchPromise + + fetchPromise = fetch('/api/env') + .then((r) => r.json()) + .then((data) => { + cache = data + cacheTimestamp = Date.now() + setEnv(data) + return data + }) + .finally(() => { + fetchPromise = null + }) + + return fetchPromise +} + +export function useEnv(): { env: EnvRecord | null; loading: boolean; error: Error | null } { + const [env, setEnv] = useState(isCacheValid() ? cache : null) + const [loading, setLoading] = useState(!isCacheValid()) + const [error, setError] = useState(null) + + useEffect(() => { + const load = () => { + if (isCacheValid()) { + setEnv(cache) + setLoading(false) + return + } + + fetchEnv() + .then(setEnv) + .catch(setError) + .finally(() => setLoading(false)) + } + + load() + + const interval = setInterval(load, CACHE_TTL_MS) + return () => clearInterval(interval) + }, []) + + return { env, loading, error } +} @@ -0,0 +1,11 @@ +export type EnvRecord = Record + +let store: EnvRecord | null = null + +export function getEnv(): EnvRecord | null { + return store +} + +export function setEnv(env: EnvRecord): void { + store = env +} @@ -16,7 +16,7 @@ import { DownloadModal } from '#/features/business-security-download' import { NextPageWithLayout } from '#/pages/_app' import { ButtonUI, Input, Loader, Modal, SwitchCustom } from '#/shared' import { accountApi } from '#/shared/api/account-endpoints' -import { API_URL } from '#/shared/lib/constants/constants' +import { getApiUrl } from '#/shared/lib/constants' import { getDeviceType } from '#/shared/lib/helpers' import { useShowDataStore } from '#/shared/lib/hooks/use-show-data' import { ScreenForInactive } from '#/widgets/business' @@ -60,7 +60,7 @@ const Account: NextPageWithLayout = () => { try { setLoading(true) - await axios.put(API_URL + '/auth/reset-profile-pic', formData, { + await axios.put(getApiUrl() + '/auth/reset-profile-pic', formData, { headers: { Authorization: `Bearer ${data?.access}`, 'Content-Type': 'multipart/form-data', @@ -117,25 +117,47 @@ const Account: NextPageWithLayout = () => { if (Object.keys(query).length === 0) addQueryParams('setting') }, []) + // const changePassword = async () => { + // if (!data) return + // if (newPassword1 !== newPassword2) { + // showMessage('Укажите одинаковые новыe пароли!') + // return + // } + // const status = await accountApi.changePassword(data.access, newPassword1, newPassword2, currentPassword) + + // if (status === 200) { + // showMessage('Пароль успешно изменён!') + // setNewPassword1('') + // setNewPassword2('') + // setCurrentPassword('') + // setTimeout(() => setSuccess(''), 6000) + // return + // } + + // showMessage('К сожалению, произошла ошибка') + // } const changePassword = async () => { - if (!data) return - if (newPassword1 !== newPassword2) { - showMessage('Укажите одинаковые новы пароли!') - return - } - const status = await accountApi.changePassword(data.access, newPassword1, newPassword2, currentPassword) - - if (status === 200) { - showMessage('Пароль успешно изменён!') - setNewPassword1('') - setNewPassword2('') - setCurrentPassword('') - setTimeout(() => setSuccess(''), 6000) - return - } - - showMessage('К сожалению, произошла ошибка') - } + if (!data) return + if (newPassword1 !== newPassword2) { + showMessage('Укажите одинаковые новые пароли!', 'error') + return + } + + { + const response = await accountApi.changePassword(data.access, newPassword1, newPassword2, currentPassword) + + if (response.status === 200) { + showMessage('Пароль успешно изменён!', 'success') + setNewPassword1('') + setNewPassword2('') + setCurrentPassword('') + setTimeout(() => setSuccess(''), 6000) + return + } + + showMessage(response.data.detail) + } +} function body() { if (type === 'regular') { @@ -189,7 +211,7 @@ const Account: NextPageWithLayout = () => { let resStatus = 404 try { const { status } = await axios.post( - API_URL + '/payments/promocode', + getApiUrl() + '/payments/promocode', { code: promocode }, { headers: { Authorization: `Bearer ${data.access}` } } ) @@ -222,7 +244,7 @@ const Account: NextPageWithLayout = () => { try { await axios.put( - API_URL + '/auth/user-data', + getApiUrl() + '/auth/user-data', { email: email, first_name: name, @@ -231,7 +253,7 @@ const Account: NextPageWithLayout = () => { { headers: { Authorization: `Bearer ${data.access}` } } ) - showMessage('Данные успешно изменены!') + showMessage('Данные успешно изменены!', 'success') dispatch(getAllInfo(data.access)) } catch (e) {} } @@ -239,7 +261,7 @@ const Account: NextPageWithLayout = () => { const deleteAccount = async () => { if (!data) return try { - const { status } = await axios.delete(API_URL + '/auth/remove', { + const { status } = await axios.delete(getApiUrl() + '/auth/remove', { headers: { Authorization: `Bearer ${data.access}`, }, @@ -4,7 +4,7 @@ import Image from 'next/image' import { useSession } from 'next-auth/react' import { TooltipCustom } from '#/shared' -import { API_URL } from '#/shared/lib/constants' +import { getApiUrl } from '#/shared/lib/constants' import { InputStyleSmallDark } from '#/shared/ui/input' import axios from 'axios' @@ -58,7 +58,7 @@ export const KeyItem: React.FC = ({ children, limit: initialLimit, return } axios.patch( - API_URL + '/public/api-key', + getApiUrl() + '/public/api-key', { token_limit: limit === '' ? null : Number(limit), name, @@ -1,16 +1,16 @@ -import { API_URL } from "#/shared/lib/constants" -import axios from "axios" +import { getApiUrl } from '#/shared/lib/constants' +import axios from 'axios' -const client_secret = process.env.NEXT_PUBLIC_DJANGO_GOOGLE_APP_CLIENT_SECRET -const client_id = process.env.NEXT_PUBLIC_DJANGO_GOOGLE_APP_CLIENT_ID +import { getEnv } from '#/shared/lib/env-store' export async function getTokenByAPIGoogle(token: string) { - const { data } = await axios.post(API_URL + '/auth/login-social/convert-token', { + const env = getEnv() + const { data } = await axios.post(getApiUrl() + '/auth/login-social/convert-token', { grant_type: 'convert_token', - client_id: client_id, - client_secret: client_secret, + client_id: env?.NEXT_PUBLIC_DJANGO_GOOGLE_APP_CLIENT_ID, + client_secret: env?.NEXT_PUBLIC_DJANGO_GOOGLE_APP_CLIENT_SECRET, backend: 'google-oauth2', - token: token, + token, }) return data @@ -5,7 +5,7 @@ import axios from 'axios' import Image from 'next/image' import Link from 'next/link' import { useRouter } from 'next/router' -import { API_URL } from '#/shared/lib/constants/constants' +import { getApiUrl } from '#/shared/lib/constants' import { getDeviceType, getRandomImage } from '#/shared/lib/helpers' import { NextPageWithLayout } from '#/pages/_app' @@ -20,22 +20,57 @@ const ChangePassword: NextPageWithLayout = () => { const { query, push } = useRouter() + const [error, setError] = useState('') + + const [loading, setLoading] = useState(false) + const changePassword = async () => { - let formData: any = new FormData() - - formData.append('password_1', password1) - formData.append('password_2', password2) - - if (password1.trim() && password2.trim()) { - try { - await axios.post(API_URL + `/auth/change-pass?token=${query.token}`, formData, { - headers: { - 'content-type': 'multipart/form-data ', - }, - }) - setIsChange(true) + setError('') + + if (!password1.trim() || !password2.trim()) { + setError('Пожалуйста, заполните оба поля') + return + } + + if (password1.length < 6) { + setError('Пароль должен содержать не менее 6 символов') + return + } + + if (password1 !== password2) { + setError('Пароли не совпадают') + return + } + + setLoading(true) + + try { + let formData: any = new FormData() + formData.append('password_1', password1) + formData.append('password_2', password2) + + const response = await axios.post(getApiUrl() + `/auth/change-pass?token=${query.token}`, formData, { + headers: { + 'content-type': 'multipart/form-data', + }, + }) + if (response.status != 200) { + setError(response.data.detail) setTimeout(() => push('/login'), 5000) - } catch (err) {} + return + } + setIsChange(true) + setTimeout(() => push('/login'), 5000) + } catch (err: any) { + if (err.response?.data?.detail) { + setError(err.response.data.detail) + } else if (err.response?.data?.message) { + setError(err.response.data.message) + } else { + setError('Произошла ошибка при смене пароля') + } + } finally { + setLoading(false) } } @@ -52,7 +87,7 @@ const ChangePassword: NextPageWithLayout = () => { sx={{ padding: 0, margin: desktop ? 0 : '60px auto', - backgroundColor: '#373737', + backgroundColor: '#373737', }} > { {desktop && ( - + { > Восстановление пароля + {error && ( + + {error} + + )} { const tokenData = window.location.search.slice(1).split('=')[1] formData.append("token", tokenData) - axios.post(API_URL + '/auth/confirm', formData, { + axios.post(getApiUrl() + '/auth/confirm', formData, { headers: { 'Content-Type': 'multipart/form-data', }, validateStatus: (status) => status < 400 @@ -1,11 +1,216 @@ +.container { + overflow: hidden; + padding: 0; + background-color: #151518; + + @media screen and (max-width: 1000px) { + margin: 0 auto; + } +} + +.registerLinkText { + color: #a4aab5; + line-height: 19.6px; + font-size: 14px; +} + +.form { + width: 50%; + height: 100vh; + display: flex; + justify-content: center; + align-items: center; + + @media screen and (max-width: 1000px) { + width: 100%; + align-items: flex-start; + margin-top: 10px; + } +} + +.formInner { + width: 40vh; + height: 100%; + + @media screen and (max-width: 1000px) { + margin-top: 40%; + } +} + +.logoMobile { + margin-top: 20px; +} + +.title { + color: #e1e1e1; + line-height: 36.4px; + font-size: 26px; + font-weight: bold; + margin-bottom: 20px; +} + +.orEmail { + flex: 1; + font-size: 15px; + color: #a4aab5; + display: flex; + justify-content: center; + align-items: center; +} + + +.orEmailWrapper { + display: flex; + justify-content: center; + align-items: center; + gap: 12px; + width: 100%; +} + +.orEmailLine { + height: 1px; + flex: 1; + background-color: #44444A; +} + + +.inputField { + :global(.MuiInputBase-input) { + padding: 12px 12px 14px 16px; + &::placeholder { + color: #a4aab5; + opacity: 1; + } + } +} + +.labelEmail { + color: #a4aab5; + line-height: 19.6px; + font-size: 14px; + padding-top: 8px; + padding-right: 90%; +} + +.passwordRow { + padding-top: 4px; + width: 100%; + justify-content: space-between; + display: flex; +} + +.labelPassword { + color: #a4aab5; + line-height: 19.6px; + font-size: 14px; +} + +.forgotPassword { + color: #8153fb; + line-height: 19.6px; + font-size: 14px; + font-weight: 500; +} + +.forgotPasswordLink { + text-decoration: none; + color: #7f7df3; +} + +.eyeToggle { + cursor: pointer; + display: flex; + align-items: center; +} + +.buttonWrapper { + height: 50px; + width: 100%; +} + +.submitButton { + margin-top: 15px; + background-color: #7f7df3; + color: #ffffff; + line-height: 20px; + font-size: 16px; + font-weight: 600; + border-radius: 13px; + text-transform: none; + width: 100%; + height: 48px; + + &:hover { + background-color: #7f7df3; + opacity: 0.9; + } +} + +.loadingWrapper { + margin: 25px auto; + display: flex; + justify-content: center; + color: #7f7df3; +} + +.registerLink { + width: 100%; + text-align: center; + margin-top: 15px; +} + +.registerLinkAnchor { + text-decoration: none; + color: #7f7df3; +} + .imagebox { + width: 50%; + height: 100vh; + background-color: #8280ff; + overflow: hidden; + position: relative; + display: flex; + align-items: center; + justify-content: center; + @media screen and (max-width: 1000px) { display: none; } } -.form{ - @media screen and (max-width: 1000px) { - width: 100%; - } +.imageboxLogo { + position: absolute; + top: 35px; + left: 35px; +} + +.dashboardImage { + position: absolute; + transform: scale(1.15); + bottom: 60px; + transform-origin: center; +} + +.imageboxLogoContent { + z-index: 1; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +} + +.imageboxLogoText { + font-weight: 800; + font-size: 48px; + color: #ffffff; +} + +.imageboxLogoSubText { + font-weight: 400; + font-style: Regular; + line-height: 24px; + font-size: 20px; + text-align: center; + white-space: pre-line; } \ No newline at end of file @@ -21,11 +21,10 @@ import { ERROR_MAPPING, ERRROR_YANDEX_TRANSLATE_MAPPING } from '#/pages/api/auth import { emailOptions } from '#/shared' import { getDeviceType } from '#/shared/lib/helpers' import { useShowDataStore } from '#/shared/lib/hooks/use-show-data' -import { InputStyleDark, InputStyleLight } from '#/shared/ui/input' +import { InputStyleDark } from '#/shared/ui/input' const Login: NextPageWithLayout = () => { - const device = getDeviceType() - + const [desktop, setDesktop] = React.useState(true) const { query, push } = useRouter() const [, setCookie] = useCookies() @@ -34,14 +33,16 @@ const Login: NextPageWithLayout = () => { Object.entries(query).forEach(([key, value]) => setCookie(key, value)) }, []) + React.useEffect(() => { + setDesktop(getDeviceType() === 'desktop') + }, []) + const { showMessage } = useShowDataStore() const [loading, setLoading] = React.useState(false) const [passwordShowed, showPassword] = React.useState(false) - const desktop = device === 'desktop' - const { register, handleSubmit } = useForm() useEffect(() => { @@ -96,40 +97,20 @@ const Login: NextPageWithLayout = () => { direction={desktop ? 'row' : 'column'} alignItems='center' justifyContent={desktop ? 'space-between' : 'center'} - sx={{ - overflow: 'hidden', - padding: 0, - margin: desktop ? 0 : '0px auto', - backgroundColor: '#303035', - }} > {!desktop && ( {''} )} - + { if (e.key === 'Enter') { await handleSubmit(onSubmit, checkError) @@ -142,98 +123,32 @@ const Login: NextPageWithLayout = () => { alignItems='center' spacing={2} > - - Вход - + Вход - - или email - - - Email - + +
+ или email +
+
+ - - - - - Пароль - - - - - - Забыли пароль? - - - -
- showPassword((x) => !x) - } - style={{ - cursor: 'pointer', - display: 'flex', - alignItems: 'center', - }} + className={styles.eyeToggle} + onClick={() => showPassword((x) => !x)} > {passwordShowed ? ( { ), }} + + className={styles.inputField} sx={{ ...InputStyleDark }} {...register('password', { ...PasswordOptions, })} /> - + + + + Забыли пароль? + + + + {!loading ? ( ) : ( - - + + )} - - + + Нет аккаунта? + Зарегистрироваться @@ -321,38 +207,29 @@ const Login: NextPageWithLayout = () => { {desktop && ( - + {''} {''} + + + Творчество. Технологии. Ты. + + + {`Создавай уникальный контент и общайся с продвинутыми чат-ботами,\n используя современные нейросети.`} + + )} @@ -1,11 +1,139 @@ +.container { + overflow: hidden; + padding: 0; + background-color: #151518; + + @media screen and (max-width: 1000px) { + margin: 0 auto; + } +} + +.form { + width: 50%; + height: 100vh; + display: flex; + justify-content: center; + align-items: center; + + @media screen and (max-width: 1000px) { + width: 100%; + align-items: flex-start; + margin-top: 10px; + } +} + +.formInner { + width: 40vh; + height: 80%; + display: flex; + justify-content: center; + align-items: center; + + @media screen and (max-width: 1000px) { + margin-top: 16px; + } +} + +.logoMobile { + margin-top: 20px; +} + +.title { + color: #e1e1e1; + line-height: 36.4px; + font-size: 26px; + font-weight: bold; + margin-bottom: 20px; +} + +.orEmailWrapper { + display: flex; + justify-content: center; + align-items: center; + gap: 12px; + width: 100%; +} + +.orEmailLine { + height: 1px; + flex: 1; + background-color: #44444a; +} + +.orEmail { + flex: 1; + font-size: 15px; + color: #a4aab5; + display: flex; + justify-content: center; + align-items: center; +} + +.successText { + text-align: center; + color: #e1e1e1; +} + .imagebox { + width: 50%; + height: 100vh; + background-color: #8280ff; + overflow: hidden; + position: relative; + display: flex; + align-items: center; + justify-content: center; + @media screen and (max-width: 1000px) { display: none; } } -.form{ - @media screen and (max-width: 1000px) { - width: 100%; +.imageboxLogo { + position: absolute; + top: 35px; + left: 35px; +} + +.dashboardImage { + position: absolute; + transform: scale(1.15); + bottom: 60px; + transform-origin: center; +} + +.imageboxLogoContent { + z-index: 1; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +} + +.imageboxLogoText { + font-weight: 800; + font-size: 48px; + color: #ffffff; +} + +.imageboxLogoSubText { + font-weight: 400; + font-style: Regular; + line-height: 24px; + font-size: 20px; + text-align: center; + white-space: pre-line; +} + +.inputField { + margin-bottom: 16px; + + :global(.MuiInputBase-input) { + padding-block: 10px; + + &::placeholder { + color: #a4aab5; + opacity: 1; + } } } \ No newline at end of file @@ -17,11 +17,12 @@ import { getDeviceType } from '#/shared/lib/helpers' import styles from './register.module.scss' const Register: NextPageWithLayout = () => { - const device = getDeviceType() - - const desktop = device === 'desktop' - + const [desktop, setDesktop] = useState(true) const [success, setSuccess] = useState(false) + + React.useEffect(() => { + setDesktop(getDeviceType() === 'desktop') + }, []) const [isReferral, setIsReferral] = useState(true) const referral = useAppSelector((state) => state.user.referral) @@ -45,49 +46,25 @@ const Register: NextPageWithLayout = () => { return ( {!desktop && ( {''} )} - - + + {success ? ( - - + + Вы успешно зарегистрированы.
Мы отправили письмо для верификации на ваш email. В случае отсутствия, проверьте @@ -100,66 +77,49 @@ const Register: NextPageWithLayout = () => {
) : ( - - Регистрация - + Регистрация {referral === '' && isReferral && ( <> - - или email - + +
+ или email +
+ )} - + )} {desktop && ( - - {''} - {''} - - )} + + {''} + {''} + + + Творчество. Технологии. Ты. + + + {`Создавай уникальный контент и общайся с продвинутыми чат-ботами,\n используя современные нейросети.`} + + + + )} ) } @@ -6,7 +6,7 @@ import axios from 'axios' import Image from 'next/image' import Router from 'next/router' import { Error, Input } from '#/shared' -import { API_URL } from '#/shared/lib/constants/constants' +import { getApiUrl } from '#/shared/lib/constants' import { getDeviceType, getRandomImage } from '#/shared/lib/helpers' import { NextPageWithLayout } from '#/pages/_app' import { useShowDataStore } from '#/shared/lib/hooks/use-show-data' @@ -18,7 +18,7 @@ const Reset: NextPageWithLayout = () => { const [isSend, setIsSend] = useState(false) const {showMessage, } = useShowDataStore() const sendMail = () => { - axios.post(API_URL + '/auth/update-pass', { + axios.post(getApiUrl() + '/auth/update-pass', { email: input_email, }) .then(function (response) { @@ -3,7 +3,11 @@ import { Box } from '@mui/material' import Router from 'next/router' import { useSession } from 'next-auth/react' +import { useAppSelector } from '#/app/store/store' +import { balanceSelector } from '#/shared/lib/selectors' +import { useShowDataStore } from '#/shared/lib/hooks/use-show-data' import LightningSvg from '#/assets/svg/lightning.svg?react' +import { getModalById, SUBSCRIPTION_CHANGE_NOTIFICATION } from '#/features/modals' import { accountApi } from '#/shared/api/account-endpoints' import { commaSeparated } from '#/shared/lib/helpers' import { IFeatures, IOffer, formatFeatureValue } from '#/views/subscription' @@ -14,10 +18,14 @@ interface IOfferProps extends IOffer { device: 'desktop' | 'mobile' isCurrentSubscription?: boolean index?: number + currentPlanTokens?: number } -const Offer: React.FC = ({ uid, tokens_per_plan, price, grouped_features, isCurrentSubscription, index }) => { +const Offer: React.FC = ({ uid, tokens_per_plan, price, grouped_features, isCurrentSubscription, index, currentPlanTokens = 0 }) => { const { data } = useSession() + const isRecurring = useAppSelector((state) => state.user.payment_plan?.is_recurring) ?? false + const balance = useAppSelector(balanceSelector) + const { showMessage } = useShowDataStore() const unitMap: Record = { Изображения: 'изображений', @@ -51,9 +59,96 @@ const Offer: React.FC = ({ uid, tokens_per_plan, price, grouped_fea const formattedFeatures = formatFeatures(grouped_features) - const pay = async (uid: string) => { - const urlForPay = await accountApi.payProduct(data!.access, uid) - urlForPay ? await Router.push(urlForPay) : null + const modal = getModalById(SUBSCRIPTION_CHANGE_NOTIFICATION) + + const pay = async (planUid: string) => { + try { + const urlForPay = await accountApi.payProduct(data!.access, planUid) + urlForPay ? await Router.push(urlForPay) : null + } catch (err: any) { + const detail = err?.response?.data?.detail + const message = detail ?? 'Произошла ошибка при оплате'; + showMessage(message) + } + } + + const isFreePlan = currentPlanTokens <= 10 + + const toTokens = Number(tokens_per_plan) + const isUpgrade = currentPlanTokens < toTokens + const isDowngrade = currentPlanTokens > toTokens + const balanceExceedsLimit = balance > currentPlanTokens + const isSamePlan = currentPlanTokens === toTokens + + + const handlePayClick = () => { + + // Бесплатный план: не показываем модалку + if (isFreePlan) { + pay(uid) + return + } + + // Не ежемесячный план: показываем модалку с уведомлением о том, что неиспользованные токены сгорят + if (!isRecurring) { + modal.setState(true, { + fromTokens: currentPlanTokens, + toTokens, + onConfirm: () => pay(uid), + }) + return + } + + // Апгрейд: модалка только если баланс превышает лимит текущего плана (токены сгорят) + if (isUpgrade && balanceExceedsLimit) { + modal.setState(true, { + fromTokens: currentPlanTokens, + toTokens, + isUpgrade: true, + onConfirm: () => pay(uid), + }) + return + } + + // Апгрейд без токенов сверх лимита: показываем сколько токенов докупит юзер + if (isUpgrade && !balanceExceedsLimit) { + const tokensToAdd = toTokens - balance + modal.setState(true, { + fromTokens: currentPlanTokens, + toTokens, + tokensToAdd, + onConfirm: () => pay(uid), + }) + return + } + + // Рекуррентный юзер покупает тот же план: показываем сколько токенов докупит (если есть что докупать) + if (isSamePlan) { + const tokensToAdd = toTokens - balance + if (tokensToAdd > 0) { + modal.setState(true, { + fromTokens: currentPlanTokens, + toTokens, + tokensToAdd, + onConfirm: () => pay(uid), + }) + return + } + pay(uid) + return + } + + // Даунгрейд: модалка всегда (неиспользованные токены сгорят) + if (isDowngrade) { + modal.setState(true, { + fromTokens: currentPlanTokens, + toTokens, + onConfirm: () => pay(uid), + }) + return + } + + pay(uid) } return ( @@ -81,7 +176,7 @@ const Offer: React.FC = ({ uid, tokens_per_plan, price, grouped_fea className={`${styles.tarif_btn} ${isCurrentSubscription ? styles.tarif_btn_current : ''}`} onClick={(e) => { e.stopPropagation() - pay(uid) + handlePayClick() }} > Оплатить @@ -8,12 +8,14 @@ import styles from './subscription.module.scss' interface PlansSectionProps { offers: IOffer[] | null - currentOfferUID:string + currentOfferUID: string + currentPlanTokens: number } export const PlansSection = ({ offers, - currentOfferUID + currentOfferUID, + currentPlanTokens, }: PlansSectionProps) => { return ( @@ -27,6 +29,7 @@ export const PlansSection = ({ isCurrentSubscription={false} index={idx} key={offer.uid} + currentPlanTokens={currentPlanTokens} /> ) })} @@ -65,7 +65,7 @@ const SubscriptionPage: NextPageWithLayout = () => { - + { - axios.get(API_URL + `/auth/business-host/download-expenses?type=employees&from_date=${fromDate}&to_date=${toDate}`, { + axios.get(getApiUrl() + `/auth/business-host/download-expenses?type=employees&from_date=${fromDate}&to_date=${toDate}`, { responseType: 'arraybuffer', headers: { Authorization: `Bearer ${data?.access}` }, }).then((res) => { @@ -82,7 +82,7 @@ export default function BusinessHost() { } useEffect(() => { - axios.put(API_URL + '/auth/business-host', { token_cap_enabled: mailing }, { headers: { Authorization: `Bearer ${data?.access}` } }) + axios.put(getApiUrl() + '/auth/business-host', { token_cap_enabled: mailing }, { headers: { Authorization: `Bearer ${data?.access}` } }) }, [mailing]) return ( @@ -145,7 +145,7 @@ const MailingBlock = ({ info, mailing }: { info: InfoBusiness | null | undefined } const req = async (mailingArr: string[]) => { - await axios.put(API_URL + '/auth/business-host', { token_cap_emails: mailingArr }, { headers: { Authorization: `Bearer ${data?.access}` } }) + await axios.put(getApiUrl() + '/auth/business-host', { token_cap_emails: mailingArr }, { headers: { Authorization: `Bearer ${data?.access}` } }) } return ( @@ -1,6 +1,6 @@ import axios from 'axios' -import { API_URL } from '#/shared/lib/constants' +import { getApiUrl } from '#/shared/lib/constants' export type InfoBusiness = { company_name: string @@ -18,7 +18,7 @@ export type InfoBusiness = { } export const getInfo = async (token?: string): Promise => { try { - const { data } = await axios.get(API_URL + '/auth/business-host', { + const { data } = await axios.get(getApiUrl() + '/auth/business-host', { headers: { Authorization: `Bearer ${token}`, }, @@ -1,10 +1,10 @@ import axios from 'axios' import { AllowedModels } from '#/features/changeAccessToModel' -import { API_URL } from '#/shared/lib/constants' +import { getApiUrl } from '#/shared/lib/constants' export const getModels = async (token?: string): Promise => { try { - const { data } = await axios.get(API_URL + '/auth/business-host/allowed-models', { + const { data } = await axios.get(getApiUrl() + '/auth/business-host/allowed-models', { headers: { Authorization: `Bearer ${token}`, }, @@ -2,11 +2,11 @@ import axios from 'axios' import { ResponseGetPersons } from '#/features/invite-person-in-business' import { ResponseGetBusinessGroups, ResponseGetIpList } from '#/features/invite-person-in-business/model/types' -import { API_URL } from '#/shared/lib/constants' +import { getApiUrl } from '#/shared/lib/constants' export const getBusinessGroups = async (token?: string): Promise => { try { - const { data } = await axios.get(API_URL + '/auth/business-groups', { + const { data } = await axios.get(getApiUrl() + '/auth/business-groups', { headers: { Authorization: `Bearer ${token}`, }, @@ -2,11 +2,11 @@ import axios from 'axios' import { ResponseGetPersons } from '#/features/invite-person-in-business' import { ResponseGetIpList } from '#/features/invite-person-in-business/model/types' -import { API_URL } from '#/shared/lib/constants' +import { getApiUrl } from '#/shared/lib/constants' export const getIpList = async (token?: string): Promise => { try { - const { data } = await axios.get(API_URL + '/auth/business-host/ip-whitelist', { + const { data } = await axios.get(getApiUrl() + '/auth/business-host/ip-whitelist', { headers: { Authorization: `Bearer ${token}`, }, @@ -2,7 +2,7 @@ import axios from 'axios' import { Dayjs } from 'dayjs' import { ResponseGetLogsList } from '#/features/invite-person-in-business/model/types' -import { API_URL } from '#/shared/lib/constants' +import { getApiUrl } from '#/shared/lib/constants' export const getLogsList = async ( offset: number, @@ -14,7 +14,7 @@ export const getLogsList = async ( ): Promise => { try { const { data } = await axios.get( - API_URL + + getApiUrl() + `/auth/business-host/logs/?${from_date ? 'from-date=' + from_date + '&' : ''}${ to_date ? '&to-date=' + to_date + '&' : '' }&limit=${limit}&offset=${offset}&log-identity=${logType}`, @@ -1,12 +1,12 @@ import axios from 'axios' import { ResponseGetPersons } from '#/features/invite-person-in-business' -import { API_URL } from '#/shared/lib/constants' +import { getApiUrl } from '#/shared/lib/constants' export const getPersons = async (token?: string, security?: boolean): Promise => { try { const { data } = await axios.get( - API_URL + `/auth/business-host/accounts${security ? '?type=sec' : '?type=regular&type=admin'}`, + getApiUrl() + `/auth/business-host/accounts${security ? '?type=sec' : '?type=regular&type=admin'}`, { headers: { Authorization: `Bearer ${token}`, @@ -17,7 +17,7 @@ import { AddBusinessGroup } from '#/features/business-group' import { ChangeBusinessGroup } from '#/features/business-group/ui/change-business-group' import { ResponseGetBusinessGroups } from '#/features/invite-person-in-business/model/types' import { Search } from '#/shared' -import { API_URL } from '#/shared/lib/constants' +import { getApiUrl } from '#/shared/lib/constants' import { getBusinessGroups } from '#/widgets/business-persons/api/get-businessGroups' import ArrowUpOrDown from '#/widgets/top-bar-model/ui/arrow-up-or-down' @@ -57,7 +57,7 @@ export const BusinessGroups = ({ company_uid }: { company_uid?: string }) => { const deleteGroup = async (group_id: string) => { await axios - .delete(API_URL + `/auth/business-groups/${group_id}/`, { + .delete(getApiUrl() + `/auth/business-groups/${group_id}/`, { headers: { Authorization: `Bearer ${data?.access}`, }, @@ -21,7 +21,7 @@ import { getPersons } from '#/widgets/business-persons/api/get-persons' import ArrowUpOrDown from '#/widgets/top-bar-model/ui/arrow-up-or-down' import { translateEmailStatus, formatDateStatus, formatEmail } from '../../lib/lib' -import { getModalById, PLATE_CHANGE_PASSWORD, RESEND_INVATION_PASSWORD } from '#/features/modals' +import { getModalById, PLATE_CHANGE_PASSWORD } from '#/features/modals' import { ResponseGetBusinessGroups } from '#/features/invite-person-in-business/model/types' import { useShowDataStore } from '#/shared/lib/hooks/use-show-data' import { resendInvation } from '#/features/resend-invation-corp/api/resend-invation' @@ -43,7 +43,6 @@ export const PersonsList = ({ const { data: session } = useSession() const passChangeModal = getModalById(PLATE_CHANGE_PASSWORD) - const passResendModal = getModalById(RESEND_INVATION_PASSWORD) useEffect(() => { setPersons(personsList) @@ -79,24 +78,32 @@ export const PersonsList = ({ }) ) setCurrentPerson(null) - showMessage(`Лимит пользователя ${email} успешно изменён!`, 'success') + showMessage(`Данные сотрудника ${email} изменены!`, 'success') } const handleOpenResendModal = async (email: string) => { - const response = await resendInvation(email, session?.access) - - if (response.status == 200) { - showMessage('Приглашение переотправлено!', 'success') - setPersons((prev) => - prev?.map((el) => { - if (el.email === email) { - return { ...el, acceptance_status: 'pending' as const } - } - return el - }) ?? null - ) - } else { - showMessage(response.data) + try { + const response = await resendInvation(email, session?.access) + + if (response.status == 200) { + showMessage('Приглашение переотправлено!', 'success') + setPersons((prev) => + prev?.map((el) => { + if (el.email === email) { + return { ...el, acceptance_status: 'pending' as const } + } + return el + }) ?? null + ) + } else { + // Проверяем тип response.data и преобразуем в строку + const errorMessage = typeof response.data === 'string' + ? response.data + : response.data?.detail || 'Ошибка при отправке приглашения' + showMessage(errorMessage, 'error') + } + } catch (error) { + showMessage('Произошла ошибка при отправке приглашения', 'error') } } @@ -250,4 +257,4 @@ export const PersonsList = ({ )} ) -} +} \ No newline at end of file @@ -35,15 +35,15 @@ import { Select, } from '#/shared' import { Search } from '#/shared' -import { API_URL } from '#/shared/lib/constants' // import styles from '#/widgets/business-models/ui/models-list/models-list.module.scss' import { getBusinessGroups } from '#/widgets/business-persons/api/get-businessGroups' import ArrowUpOrDown from '#/widgets/top-bar-model/ui/arrow-up-or-down' -import { getModalById, PLATE_CHANGE_PASSWORD, RESEND_INVATION_PASSWORD } from '#/features/modals' +import { getModalById, PLATE_CHANGE_PASSWORD } from '#/features/modals' import { formatDateStatus, formatEmail, translateEmailStatus } from '../../lib/lib' import { XMark } from '#/features/remove-person' import { useShowDataStore } from '#/shared/lib/hooks/use-show-data' +import { resendInvation } from '#/features/resend-invation-corp/api/resend-invation' export const SecurityList = ({ securityList, @@ -60,7 +60,6 @@ export const SecurityList = ({ const [currentPerson, setCurrentPerson] = useState(null) const passChangeModal = getModalById(PLATE_CHANGE_PASSWORD) - const passResendModal = getModalById(RESEND_INVATION_PASSWORD) useEffect(() => { setPersons(securityList) @@ -92,12 +91,40 @@ export const SecurityList = ({ el.token_limit = limit return el } - return el }) ) setCurrentPerson(null) - showMessage(`Лимит пользователя ${email} успешно изменён!`, 'success') + showMessage(`Данные сотрудника ${email} изменены!`, 'success') + } + + const handleOpenResendModal = async (email: string) => { + try { + const response = await resendInvation(email, data?.access) + + if (response.status == 200) { + showMessage('Приглашение переотправлено!', 'success') + setPersons((prev) => + prev?.map((el) => { + if (el.email === email) { + return { ...el, acceptance_status: 'pending' as const } + } + return el + }) ?? null + ) + } else { + const errorMessage = typeof response.data === 'string' + ? response.data + : response.data?.detail || 'Ошибка при отправке приглашения' + showMessage(errorMessage, 'error') + } + } catch (error) { + showMessage('Произошла ошибка при отправке приглашения', 'error') + } + } + + const handleChangePassword = (email: string) => { + passChangeModal.setState(true, { email }) } useEffect(() => { @@ -180,13 +207,7 @@ export const SecurityList = ({ {statusPerson === 'Приглашен' ? ( @@ -353,4 +368,4 @@ export const InviteSecurityModal: FC = ({ open, onClose, showN ) -} +} \ No newline at end of file @@ -1,10 +1,10 @@ import { Message, MessageSend } from '#/entities/message' -import { API_URL } from '#/shared/lib/constants' +import { getApiUrl } from '#/shared/lib/constants' import { IMessageRequest } from '#/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(API_URL + `/media/gallery/images?limit=${limit}&offset=${offset}`, { + return await axios.get(getApiUrl() + `/media/gallery/images?limit=${limit}&offset=${offset}`, { headers: { Authorization: `Bearer ${token}`, }, @@ -1,15 +1,15 @@ import { NavigationSearchModelLink } from './types' -import { API_URL } from '#/shared/lib/constants' +import { getApiUrl } from '#/shared/lib/constants' import axios from 'axios' export const getModelChatLinks = async (token: string) => { - return await axios.get(API_URL + '/api/chats/links', { + return await axios.get(getApiUrl() + '/api/chats/links', { headers: { Authorization: `Bearer ${token}` }, }) } export const getModelMediaLinks = async (token: string) => { - return await axios.get(API_URL + '/api/media/images/links', { + return await axios.get(getApiUrl() + '/api/media/images/links', { headers: { Authorization: `Bearer ${token}` }, }) } @@ -5,7 +5,7 @@ import { useRouter } from 'next/router' import { useSession } from 'next-auth/react' import { getReferral, IReferral } from '#/shared/api/endpoints' -import { API_HOST } from '#/shared/lib/constants' +import { getApiHost } from '#/shared/lib/constants' import { useShowDataStore } from '#/shared/lib/hooks/use-show-data' import { useAppSelector } from '#/app/store/store' interface IProps { @@ -15,7 +15,7 @@ interface IProps { export const Referral = ({ device }: IProps) => { const email = useAppSelector((state) => state.user.email) const { push } = useRouter() - const url = API_HOST + '/r/' + email + const url = getApiHost() + '/r/' + email const { showMessage } = useShowDataStore() const { data } = useSession() @@ -123,11 +123,7 @@ export const SideMenu = ({}) => { const business_error_report = getModalById(BUSINESS_ERROR_REPORT) const match = useMediaQuery('(min-height:800px)') - const open = useMemo(() => getOptionValue('sidemenu', { sidemenu_state: 'opened' }).sidemenu_state, [settings]) - - useEffect(() => { - if (isNextStepVisible) updateSettings('sidemenu', { sidemenu_state: 'closed' }) - }, [isNextStepVisible, updateSettings]) + const open = useMemo(() => getOptionValue('sidemenu', { sidemenu_state: 'closed' }).sidemenu_state, [settings]) const filteredMenuListTop = useMemo(() => { return menuListTop.filter((item) => { @@ -1,12 +1,12 @@ import axios, { AxiosResponse } from 'axios' -import { API_URL } from '#/shared/lib/constants' +import { getApiUrl } from '#/shared/lib/constants' import { WhisperResponse } from '../ui/whisper/whisper' export const getAudioList = async (token: string) => { try { - const { data } = await axios.get(API_URL + '/ml_models/whisper', { + const { data } = await axios.get(getApiUrl() + '/ml_models/whisper', { headers: { Authorization: `Bearer ${token}`, }, @@ -8,7 +8,7 @@ import Image from 'next/image' import { useSession } from 'next-auth/react' import { useAppSelector } from '#/app/store/store' -import { API_URL } from '#/shared/lib/constants' +import { getApiUrl } from '#/shared/lib/constants' import { decodeError } from '#/shared/lib/helpers/decode-error' import { styleInputWithoutBorderFocus } from '#/shared/ui/input' import styles from '#/shared/ui/search/search.module.scss' @@ -47,7 +47,7 @@ export const WhisperWidget = () => { const { data: session } = useSession() const removeAudio = async (uid: string) => { try { - const { status } = await axios.delete(API_URL + '/ml_models/whisper', { + const { status } = await axios.delete(getApiUrl() + '/ml_models/whisper', { headers: { Authorization: `Bearer ${session?.access}`, }, @@ -75,7 +75,7 @@ export const WhisperWidget = () => { formData.append('audio', audio) try { - const { data: result } = await axios.post(API_URL + '/ml_models/whisper', formData, { + const { data: result } = await axios.post(getApiUrl() + '/ml_models/whisper', formData, { headers: { 'content-type': 'multipart/form-data ', Authorization: `Bearer ${data?.access}`, @@ -5,11 +5,7 @@ import * as Sentry from "@sentry/nextjs"; Sentry.init({ - dsn: process.env.NEXT_PUBLIC_SENTRY_PROJECT_URL, - release: process.env.NEXT_PUBLIC_RELEASE ?? 'not-stated', integrations: [Sentry.replayIntegration({ maskAllText: false, blockAllMedia: false })], - tracesSampleRate: 1, - enableLogs: true, replaysSessionSampleRate: 0.1, replaysOnErrorSampleRate: 1.0, sendDefaultPii: true, @@ -29,4 +29,3 @@ NEXT_PUBLIC_DJANGO_VK_APP_CLIENT_SECRET=YWv1YXdE69FKN5NZpf583qvU0mq7wbsdrLShBLxn NEXT_PUBLIC_WS_API_URL=wss://devapi.air.fail/rtc NEXT_PUBLIC_SESSION_TIME=1000000 -SESSION_TIME=1000000 @@ -1,5 +0,0 @@ -# DO NOT commit this file to your repository! -# The SENTRY_AUTH_TOKEN variable is picked up by the Sentry Build Plugin. -# It's used for authentication when uploading source maps. -# You can also set this env variable in your own `.env` files and remove this file. -SENTRY_AUTH_TOKEN=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 @@ -2,10 +2,8 @@ /node_modules /.pnp .pnp.js -.env.* -!.env.sentry-build-plugin +.env* !.env.dist -.env /coverage @@ -13,7 +13,7 @@ build_staging: script: - export RELEASE="$(date -Iseconds)" - echo -e "\nRELEASE=$RELEASE\nNEXT_PUBLIC_RELEASE=$RELEASE\nCI=true" >> $ENV - - cp $ENV .env.production + - cp $ENV .env - docker compose --env-file $ENV build - docker compose push environment: @@ -43,7 +43,7 @@ deploy_staging: before_script: - export RELEASE="$(date -Iseconds)" - echo -e "\nRELEASE=$RELEASE\nNEXT_PUBLIC_RELEASE=$RELEASE" >> $ENV - - cp $ENV .env.production + - cp $ENV .env - mkdir -p $DOCKER_CERT_PATH - echo "$STAGING_CLUSTER_CA" > $DOCKER_CERT_PATH/ca.pem - echo "$STAGING_CLUSTER_CERT" > $DOCKER_CERT_PATH/cert.pem @@ -58,7 +58,7 @@ build_production: script: - export RELEASE="$(date -Iseconds)" - echo -e "\nRELEASE=$RELEASE\nNEXT_PUBLIC_RELEASE=$RELEASE\nCI=true" >> $ENV - - cp $ENV .env.production + - cp $ENV .env - docker compose -f stack.yml --env-file $ENV build - docker compose -f stack.yml push environment: @@ -88,7 +88,7 @@ deploy_production: before_script: - export RELEASE="$(date -Iseconds)" - echo -e "\nRELEASE=$RELEASE\nNEXT_PUBLIC_RELEASE=$RELEASE" >> $ENV - - cp $ENV .env.production + - cp $ENV .env - mkdir -p $DOCKER_CERT_PATH - echo "$PRODUCTION_CLUSTER_CA" > $DOCKER_CERT_PATH/ca.pem - echo "$PRODUCTION_CLUSTER_CERT" > $DOCKER_CERT_PATH/cert.pem @@ -8,7 +8,7 @@ services: networks: - infrastructure env_file: - - .env.production + - .env labels: - traefik.enable=true - traefik.docker.network=infrastructure @@ -28,6 +28,7 @@ const nextConfig = { return config }, reactStrictMode: false, + productionBrowserSourceMaps: true, images: { domains: [ 'app.air.fail', @@ -42,27 +43,12 @@ const withBundleAnalyzer = require('@next/bundle-analyzer')({ enabled: false, }) -module.exports = withBundleAnalyzer(nextConfig) +const analyzerConfig = withBundleAnalyzer(nextConfig) const { withSentryConfig } = require("@sentry/nextjs"); -module.exports = withSentryConfig(module.exports, { - org: process.env.NEXT_PUBLIC_SENTRY_ORGANIZATION, - project: process.env.NEXT_PUBLIC_SENTRY_PROJECT_NAME, - sentryUrl: process.env.NEXT_PUBLIC_SENTRY_URL, - release: { - name: process.env.NEXT_PUBLIC_RELEASE ?? 'not-stated', - deploy: { - env: process.env.NODE_ENV - } - }, - telemetry: false, +module.exports = withSentryConfig(analyzerConfig, { silent: !process.env.CI, widenClientFileUpload: true, - tunnelRoute: "/monitoring", - webpack: { - treeshake: { - removeDebugLogging: true, - }, - }, + tunnelRoute: "/monitoring" }); @@ -22,6 +22,7 @@ "@reduxjs/toolkit": "^1.9.5", "@sentry/cli": "^2.58.4", "@sentry/nextjs": "^10.32.1", + "@sentry/webpack-plugin": "^5.1.1", "@testing-library/user-event": "^14.6.1", "@types/cookie": "^0.5.1", "@types/intro.js": "^5.1.1", @@ -7024,9 +7025,9 @@ } }, "node_modules/@sentry/babel-plugin-component-annotate": { - "version": "4.6.1", - "resolved": "https://registry.npmjs.org/@sentry/babel-plugin-component-annotate/-/babel-plugin-component-annotate-4.6.1.tgz", - "integrity": "sha512-aSIk0vgBqv7PhX6/Eov+vlI4puCE0bRXzUG5HdCsHBpAfeMkI8Hva6kSOusnzKqs8bf04hU7s3Sf0XxGTj/1AA==", + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@sentry/babel-plugin-component-annotate/-/babel-plugin-component-annotate-4.9.1.tgz", + "integrity": "sha512-0gEoi2Lb54MFYPOmdTfxlNKxI7kCOvNV7gP8lxMXJ7nCazF5OqOOZIVshfWjDLrc0QrSV6XdVvwPV9GDn4wBMg==", "license": "MIT", "engines": { "node": ">= 14" @@ -7049,13 +7050,13 @@ } }, "node_modules/@sentry/bundler-plugin-core": { - "version": "4.6.1", - "resolved": "https://registry.npmjs.org/@sentry/bundler-plugin-core/-/bundler-plugin-core-4.6.1.tgz", - "integrity": "sha512-WPeRbnMXm927m4Kr69NTArPfI+p5/34FHftdCRI3LFPMyhZDzz6J3wLy4hzaVUgmMf10eLzmq2HGEMvpQmdynA==", + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@sentry/bundler-plugin-core/-/bundler-plugin-core-4.9.1.tgz", + "integrity": "sha512-moii+w7N8k8WdvkX7qCDY9iRBlhgHlhTHTUQwF2FNMhBHuqlNpVcSJJqJMjFUQcjYMBDrZgxhfKV18bt5ixwlQ==", "license": "MIT", "dependencies": { "@babel/core": "^7.18.5", - "@sentry/babel-plugin-component-annotate": "4.6.1", + "@sentry/babel-plugin-component-annotate": "4.9.1", "@sentry/cli": "^2.57.0", "dotenv": "^16.3.1", "find-up": "^5.0.0", @@ -7080,9 +7081,9 @@ } }, "node_modules/@sentry/cli": { - "version": "2.58.4", - "resolved": "https://registry.npmjs.org/@sentry/cli/-/cli-2.58.4.tgz", - "integrity": "sha512-ArDrpuS8JtDYEvwGleVE+FgR+qHaOp77IgdGSacz6SZy6Lv90uX0Nu4UrHCQJz8/xwIcNxSqnN22lq0dH4IqTg==", + "version": "2.58.5", + "resolved": "https://registry.npmjs.org/@sentry/cli/-/cli-2.58.5.tgz", + "integrity": "sha512-tavJ7yGUZV+z3Ct2/ZB6mg339i08sAk6HDkgqmSRuQEu2iLS5sl9HIvuXfM6xjv8fwlgFOSy++WNABNAcGHUbg==", "hasInstallScript": true, "license": "FSL-1.1-MIT", "dependencies": { @@ -7099,20 +7100,20 @@ "node": ">= 10" }, "optionalDependencies": { - "@sentry/cli-darwin": "2.58.4", - "@sentry/cli-linux-arm": "2.58.4", - "@sentry/cli-linux-arm64": "2.58.4", - "@sentry/cli-linux-i686": "2.58.4", - "@sentry/cli-linux-x64": "2.58.4", - "@sentry/cli-win32-arm64": "2.58.4", - "@sentry/cli-win32-i686": "2.58.4", - "@sentry/cli-win32-x64": "2.58.4" + "@sentry/cli-darwin": "2.58.5", + "@sentry/cli-linux-arm": "2.58.5", + "@sentry/cli-linux-arm64": "2.58.5", + "@sentry/cli-linux-i686": "2.58.5", + "@sentry/cli-linux-x64": "2.58.5", + "@sentry/cli-win32-arm64": "2.58.5", + "@sentry/cli-win32-i686": "2.58.5", + "@sentry/cli-win32-x64": "2.58.5" } }, "node_modules/@sentry/cli-darwin": { - "version": "2.58.4", - "resolved": "https://registry.npmjs.org/@sentry/cli-darwin/-/cli-darwin-2.58.4.tgz", - "integrity": "sha512-kbTD+P4X8O+nsNwPxCywtj3q22ecyRHWff98rdcmtRrvwz8CKi/T4Jxn/fnn2i4VEchy08OWBuZAqaA5Kh2hRQ==", + "version": "2.58.5", + "resolved": "https://registry.npmjs.org/@sentry/cli-darwin/-/cli-darwin-2.58.5.tgz", + "integrity": "sha512-lYrNzenZFJftfwSya7gwrHGxtE+Kob/e1sr9lmHMFOd4utDlmq0XFDllmdZAMf21fxcPRI1GL28ejZ3bId01fQ==", "license": "FSL-1.1-MIT", "optional": true, "os": [ @@ -7123,9 +7124,9 @@ } }, "node_modules/@sentry/cli-linux-arm": { - "version": "2.58.4", - "resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm/-/cli-linux-arm-2.58.4.tgz", - "integrity": "sha512-rdQ8beTwnN48hv7iV7e7ZKucPec5NJkRdrrycMJMZlzGBPi56LqnclgsHySJ6Kfq506A2MNuQnKGaf/sBC9REA==", + "version": "2.58.5", + "resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm/-/cli-linux-arm-2.58.5.tgz", + "integrity": "sha512-KtHweSIomYL4WVDrBrYSYJricKAAzxUgX86kc6OnlikbyOhoK6Fy8Vs6vwd52P6dvWPjgrMpUYjW2M5pYXQDUw==", "cpu": [ "arm" ], @@ -7141,9 +7142,9 @@ } }, "node_modules/@sentry/cli-linux-arm64": { - "version": "2.58.4", - "resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm64/-/cli-linux-arm64-2.58.4.tgz", - "integrity": "sha512-0g0KwsOozkLtzN8/0+oMZoOuQ0o7W6O+hx+ydVU1bktaMGKEJLMAWxOQNjsh1TcBbNIXVOKM/I8l0ROhaAb8Ig==", + "version": "2.58.5", + "resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm64/-/cli-linux-arm64-2.58.5.tgz", + "integrity": "sha512-/4gywFeBqRB6tR/iGMRAJ3HRqY6Z7Yp4l8ZCbl0TDLAfHNxu7schEw4tSnm2/Hh9eNMiOVy4z58uzAWlZXAYBQ==", "cpu": [ "arm64" ], @@ -7159,9 +7160,9 @@ } }, "node_modules/@sentry/cli-linux-i686": { - "version": "2.58.4", - "resolved": "https://registry.npmjs.org/@sentry/cli-linux-i686/-/cli-linux-i686-2.58.4.tgz", - "integrity": "sha512-NseoIQAFtkziHyjZNPTu1Gm1opeQHt7Wm1LbLrGWVIRvUOzlslO9/8i6wETUZ6TjlQxBVRgd3Q0lRBG2A8rFYA==", + "version": "2.58.5", + "resolved": "https://registry.npmjs.org/@sentry/cli-linux-i686/-/cli-linux-i686-2.58.5.tgz", + "integrity": "sha512-G7261dkmyxqlMdyvyP06b+RTIVzp1gZNgglj5UksxSouSUqRd/46W/2pQeOMPhloDYo9yLtCN2YFb3Mw4aUsWw==", "cpu": [ "x86", "ia32" @@ -7178,9 +7179,9 @@ } }, "node_modules/@sentry/cli-linux-x64": { - "version": "2.58.4", - "resolved": "https://registry.npmjs.org/@sentry/cli-linux-x64/-/cli-linux-x64-2.58.4.tgz", - "integrity": "sha512-d3Arz+OO/wJYTqCYlSN3Ktm+W8rynQ/IMtSZLK8nu0ryh5mJOh+9XlXY6oDXw4YlsM8qCRrNquR8iEI1Y/IH+Q==", + "version": "2.58.5", + "resolved": "https://registry.npmjs.org/@sentry/cli-linux-x64/-/cli-linux-x64-2.58.5.tgz", + "integrity": "sha512-rP04494RSmt86xChkQ+ecBNRYSPbyXc4u0IA7R7N1pSLCyO74e5w5Al+LnAq35cMfVbZgz5Sm0iGLjyiUu4I1g==", "cpu": [ "x64" ], @@ -7196,9 +7197,9 @@ } }, "node_modules/@sentry/cli-win32-arm64": { - "version": "2.58.4", - "resolved": "https://registry.npmjs.org/@sentry/cli-win32-arm64/-/cli-win32-arm64-2.58.4.tgz", - "integrity": "sha512-bqYrF43+jXdDBh0f8HIJU3tbvlOFtGyRjHB8AoRuMQv9TEDUfENZyCelhdjA+KwDKYl48R1Yasb4EHNzsoO83w==", + "version": "2.58.5", + "resolved": "https://registry.npmjs.org/@sentry/cli-win32-arm64/-/cli-win32-arm64-2.58.5.tgz", + "integrity": "sha512-AOJ2nCXlQL1KBaCzv38m3i2VmSHNurUpm7xVKd6yAHX+ZoVBI8VT0EgvwmtJR2TY2N2hNCC7UrgRmdUsQ152bA==", "cpu": [ "arm64" ], @@ -7212,9 +7213,9 @@ } }, "node_modules/@sentry/cli-win32-i686": { - "version": "2.58.4", - "resolved": "https://registry.npmjs.org/@sentry/cli-win32-i686/-/cli-win32-i686-2.58.4.tgz", - "integrity": "sha512-3triFD6jyvhVcXOmGyttf+deKZcC1tURdhnmDUIBkiDPJKGT/N5xa4qAtHJlAB/h8L9jgYih9bvJnvvFVM7yug==", + "version": "2.58.5", + "resolved": "https://registry.npmjs.org/@sentry/cli-win32-i686/-/cli-win32-i686-2.58.5.tgz", + "integrity": "sha512-EsuboLSOnlrN7MMPJ1eFvfMDm+BnzOaSWl8eYhNo8W/BIrmNgpRUdBwnWn9Q2UOjJj5ZopukmsiMYtU/D7ml9g==", "cpu": [ "x86", "ia32" @@ -7229,9 +7230,9 @@ } }, "node_modules/@sentry/cli-win32-x64": { - "version": "2.58.4", - "resolved": "https://registry.npmjs.org/@sentry/cli-win32-x64/-/cli-win32-x64-2.58.4.tgz", - "integrity": "sha512-cSzN4PjM1RsCZ4pxMjI0VI7yNCkxiJ5jmWncyiwHXGiXrV1eXYdQ3n1LhUYLZ91CafyprR0OhDcE+RVZ26Qb5w==", + "version": "2.58.5", + "resolved": "https://registry.npmjs.org/@sentry/cli-win32-x64/-/cli-win32-x64-2.58.5.tgz", + "integrity": "sha512-IZf+XIMiQwj+5NzqbOQfywlOitmCV424Vtf9c+ep61AaVScUFD1TSrQbOcJJv5xGxhlxNOMNgMeZhdexdzrKZg==", "cpu": [ "x64" ], @@ -7322,6 +7323,36 @@ "@opentelemetry/semantic-conventions": "^1.37.0" } }, + "node_modules/@sentry/nextjs/node_modules/@sentry/webpack-plugin": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@sentry/webpack-plugin/-/webpack-plugin-4.9.1.tgz", + "integrity": "sha512-Ssx2lHiq8VWywUGd/hmW3U3VYBC0Up7D6UzUiDAWvy18PbTCVszaa54fKMFEQ1yIBg/ePRET53pIzfkcZgifmQ==", + "license": "MIT", + "dependencies": { + "@sentry/bundler-plugin-core": "4.9.1", + "unplugin": "1.0.1", + "uuid": "^9.0.0" + }, + "engines": { + "node": ">= 14" + }, + "peerDependencies": { + "webpack": ">=4.40.0" + } + }, + "node_modules/@sentry/nextjs/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/@sentry/node": { "version": "10.32.1", "resolved": "https://registry.npmjs.org/@sentry/node/-/node-10.32.1.tgz", @@ -7653,20 +7684,124 @@ } }, "node_modules/@sentry/webpack-plugin": { - "version": "4.6.1", - "resolved": "https://registry.npmjs.org/@sentry/webpack-plugin/-/webpack-plugin-4.6.1.tgz", - "integrity": "sha512-CJgT/t2pQWsPsMx9VJ86goU/orCQhL2HhDj5ZYBol6fPPoEGeTqKOPCnv/xsbCAfGSp1uHpyRLTA/Gx96u7VVA==", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@sentry/webpack-plugin/-/webpack-plugin-5.1.1.tgz", + "integrity": "sha512-XgQg+t2aVrlQDfIiAEizqR/bsy6GtBygwgR+Kw11P/cYczj4W9PZ2IYqQEStBzHqnRTh5DbpyMcUNW2CujdA9A==", "license": "MIT", "dependencies": { - "@sentry/bundler-plugin-core": "4.6.1", - "unplugin": "1.0.1", + "@sentry/bundler-plugin-core": "5.1.1", "uuid": "^9.0.0" }, "engines": { - "node": ">= 14" + "node": ">= 18" }, "peerDependencies": { - "webpack": ">=4.40.0" + "webpack": ">=5.0.0" + } + }, + "node_modules/@sentry/webpack-plugin/node_modules/@sentry/babel-plugin-component-annotate": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@sentry/babel-plugin-component-annotate/-/babel-plugin-component-annotate-5.1.1.tgz", + "integrity": "sha512-x2wEpBHwsTyTF2rWsLKJlzrRF1TTIGOfX+ngdE+Yd5DBkoS58HwQv824QOviPGQRla4/ypISqAXzjdDPL/zalg==", + "license": "MIT", + "engines": { + "node": ">= 18" + } + }, + "node_modules/@sentry/webpack-plugin/node_modules/@sentry/bundler-plugin-core": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@sentry/bundler-plugin-core/-/bundler-plugin-core-5.1.1.tgz", + "integrity": "sha512-F+itpwR9DyQR7gEkrXd2tigREPTvtF5lC8qu6e4anxXYRTui1+dVR0fXNwjpyAZMhIesLfXRN7WY7ggdj7hi0Q==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.18.5", + "@sentry/babel-plugin-component-annotate": "5.1.1", + "@sentry/cli": "^2.58.5", + "dotenv": "^16.3.1", + "find-up": "^5.0.0", + "glob": "^13.0.6", + "magic-string": "~0.30.8" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@sentry/webpack-plugin/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@sentry/webpack-plugin/node_modules/brace-expansion": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", + "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@sentry/webpack-plugin/node_modules/glob": { + "version": "13.0.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", + "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", + "license": "BlueOak-1.0.0", + "dependencies": { + "minimatch": "^10.2.2", + "minipass": "^7.1.3", + "path-scurry": "^2.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@sentry/webpack-plugin/node_modules/lru-cache": { + "version": "11.2.7", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.7.tgz", + "integrity": "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==", + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@sentry/webpack-plugin/node_modules/minimatch": { + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", + "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@sentry/webpack-plugin/node_modules/path-scurry": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/@sentry/webpack-plugin/node_modules/uuid": { @@ -10900,17 +11035,6 @@ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==" }, - "node_modules/encoding": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", - "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "iconv-lite": "^0.6.2" - } - }, "node_modules/enhanced-resolve": { "version": "5.18.1", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz", @@ -12583,7 +12707,7 @@ "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" @@ -15876,10 +16000,10 @@ } }, "node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "license": "ISC", + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "license": "BlueOak-1.0.0", "engines": { "node": ">=16 || 14 >=14.17" } @@ -17795,7 +17919,7 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/sass": { @@ -7,11 +7,10 @@ }, "scripts": { "start": "next start", - "build": "next build && npm run sentry:sourcemaps", + "build": "next build", "dev": "next dev", "lint": "next lint", - "prepare": "husky", - "sentry:sourcemaps": "sentry-cli sourcemaps inject --org air --project ui-web .next && sentry-cli --url https://sentry.kisulkens.ru/ sourcemaps upload --org air --project ui-web .next" + "prepare": "husky" }, "engines": { "node": ">=12.0.0" @@ -38,6 +37,7 @@ "@reduxjs/toolkit": "^1.9.5", "@sentry/cli": "^2.58.4", "@sentry/nextjs": "^10.32.1", + "@sentry/webpack-plugin": "^5.1.1", "@testing-library/user-event": "^14.6.1", "@types/cookie": "^0.5.1", "@types/intro.js": "^5.1.1", @@ -1,9 +1,3 @@ import * as Sentry from "@sentry/nextjs"; -Sentry.init({ - dsn: process.env.NEXT_PUBLIC_SENTRY_PROJECT_URL, - release: process.env.NEXT_PUBLIC_RELEASE ?? 'not-stated', - tracesSampleRate: 1, - enableLogs: true, - sendDefaultPii: true, -}); +Sentry.init({sendDefaultPii: true}); @@ -1,9 +1,3 @@ import * as Sentry from "@sentry/nextjs"; -Sentry.init({ - dsn: process.env.NEXT_PUBLIC_SENTRY_PROJECT_URL, - release: process.env.NEXT_PUBLIC_RELEASE ?? 'not-stated', - tracesSampleRate: 1, - enableLogs: true, - sendDefaultPii: true, -}); +Sentry.init({sendDefaultPii: true}); \ No newline at end of file @@ -40,7 +40,7 @@ services: - traefik.http.services.frontend.loadbalancer.server.port=3000 env_file: - - .env.production + - .env networks: infrastructure: