@@ -12,7 +12,11 @@ } } .main { +<<<<<<< HEAD width: 97%; +======= + width: calc(100% - 30px); +>>>>>>> 8e0fa2c5838323857a12f54deaf4fe29372117c4 // margin: 0 auto; @media (max-width: 1000px) { @@ -0,0 +1,5 @@ +import { api } from '#/shared/api' + +export const checkBackendHealth = async () => { + return await api.get('/healthz') +} \ No newline at end of file @@ -0,0 +1 @@ +export * from './error-modal.routes' \ No newline at end of file @@ -0,0 +1 @@ +export * from './use-check-presense-backend' \ No newline at end of file @@ -0,0 +1,42 @@ +import { useState } from 'react' +import { checkBackendHealth } from '../api' +import { useShowDataStore } from '#/shared/lib/hooks' +import { useRouter } from 'next/router' +import { ERROR_MESSAGE, getModalById } from '#/features/modals' +import { withPending } from '#/shared' + +export const useCheckBackendHealth = () => { + const [buttonText, setButtonText] = useState('Проверить доступность') + const [isBackendAvailable, setIsBackendAvailable] = useState(false) + const modal = getModalById(ERROR_MESSAGE) + + const { showMessage } = useShowDataStore() + const { push } = useRouter() + + const goHome = () => { + push('/') + modal.setState(false) + } + + const [handleButtonClick, isLoading] = withPending(async () => { + const { status } = await checkBackendHealth() + + if (status === 200) { + setIsBackendAvailable(true) + setButtonText('Перейти на главную страничку') + } + if ([502, 503, 504].includes(status)) { + showMessage('Сервер ещё недоступен, ждём вас чуть позже', 'error') + } + }) + + return { + handleButtonClick, + buttonText, + setButtonText, + isBackendAvailable, + setIsBackendAvailable, + isLoading, + goHome + } +} @@ -0,0 +1,140 @@ +.wrapper { + width: 100vw; + height: 100vh; + display: flex; + align-items: center; + justify-content: center; +} +.loader { + width: 50px; +} +.container { + position: relative; + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + height: 80dvh; + gap: 47px; + + &__action { + height: 50px; + display: flex; + justify-content: center; + align-items: center; + width: fit-content; + // width: 100%; + } + + &__loader { + width: 50px; + height: 50px; + } + + &__content { + display: flex; + flex-direction: column; + + gap: 50px; + h1 { + color: #ffffff; + font-weight: 700; + font-size: 96px; + letter-spacing: -0.02em; + line-height: 1; + margin: 0; + } + + h2 { + color: #ffffff; + font-weight: 600; + font-size: 32px; + letter-spacing: -0.02em; + line-height: 1.2; + margin: 0 0 20px 0; + } + + p { + color: #a4aab5; + font-weight: 500; + font-size: 18px; + letter-spacing: -0.01em; + line-height: 1.4; + margin: 0; + } + } + + &__image { + width: 403px; + height: 382px; + background-size: cover; + } + + &__button { + width: fit-content; + padding: 14.5px 51px; + height: 50px; + + font-size: 15px; + font-weight: 500; + line-height: 140%; + letter-spacing: -1%; + transition: opacity 0.3s ease, transform 0.3s ease; + } + + &__fadeIn { + animation: fadeIn 0.4s ease-in forwards; + } + + &__fadeOut { + animation: fadeOut 0.4s ease-out forwards; + } +} + +@media screen and (max-width: 950px) { + .container { + flex-direction: column-reverse; + text-align: center; + + &__content { + align-items: center; + } + } + .footer { + display: none; + } +} + +@media screen and (max-width: 450px) { + .container__image { + width: 207px; + height: 196px; + } +} +@media screen and (max-width: 400px) { + .container__content h1 { + font-size: 48px; + } +} + +@keyframes fadeIn { + from { + opacity: 0; + transform: translateY(10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes fadeOut { + from { + opacity: 1; + transform: translateY(0); + } + to { + opacity: 0; + transform: translateY(-10px); + } +} \ No newline at end of file @@ -0,0 +1,86 @@ +import { ERROR_MESSAGE, getModalById, PlateTemplate } from '#/features/modals' +import { InfoPageTemplate } from '#/shared/ui/info-page-template' +import { useRouter } from 'next/router' +import styles from './error-message-fullscreen.module.scss' +import { CommonButton } from '#/shared/ui/button' +import Image from 'next/image' +import Link from 'next/link' +import { useEffect, useRef, useState } from 'react' +import { useCheckBackendHealth } from '../model' +import { checkBackendHealth } from '../api' +import { c, Loader } from '#/shared' +import { CSSTransition, SwitchTransition } from 'react-transition-group' + +export const ErrorMessageFullscreen = () => { + const { + buttonText, + isLoading, + handleButtonClick, + isBackendAvailable, + goHome, + setIsBackendAvailable, + setButtonText, + } = useCheckBackendHealth() + + const buttonRef = useRef(null) + + const modal = getModalById(ERROR_MESSAGE) + + useEffect(() => { + setIsBackendAvailable(false) + setButtonText('Проверить доступность') + }, [modal.state]) + + return ( + +
+
+
+

Упс!

+
+

Сервис временно недоступен

+

+ В настоящее время наш сайт не отвечает из-за технических работ или + перегрузки серверов. +

+
+ +
+ + + + {buttonText} + + + +
+
+ + {'network-error'} +
+
+
+ ) +} @@ -7,6 +7,7 @@ import { useImageIcons } from '../model' import BlankLinkSvg from '#/assets/svg/blank-link.svg?react' import DownloadSvg from '#/assets/svg/download.svg?react' import { CSSTransition } from 'react-transition-group' +import { ERROR_MESSAGE, getModalById } from '#/features/modals' interface ImageIconsPopupProps extends Message { opened: boolean @@ -16,6 +17,8 @@ interface ImageIconsPopupProps extends Message { export const ImageIconsPopup = ({ uid, file, content, opened, setOpened }: ImageIconsPopupProps) => { const { downloadFile } = useImageIcons() + const modal = getModalById(ERROR_MESSAGE) + return (
@@ -13,3 +13,4 @@ export const INVITE_USER = 'invite-user' export const INVITE_SECURITY_MODAL = 'invite-security' export const LIMIT_MODAL = 'limit-modal' export const LIMIT_SECURITY_MODAL = 'limit-security-modal' +export const ERROR_MESSAGE = 'error-message' @@ -28,6 +28,8 @@ export interface PlatesTemplateProps { containerClassName?: string paddingVariant?: PaddingVariant blurToogle?: BlurToogle + hasCLoseButton?: boolean + hasCloseOnCLickOutside?: boolean } export default function PlatesTemplate({ @@ -45,6 +47,8 @@ export default function PlatesTemplate({ variant = 'primary', paddingVariant = 'default', blurToogle = 'off', + hasCLoseButton = true, + hasCloseOnCLickOutside = true, }: PlatesTemplateProps) { const { setModal, getModal } = usePlatesStore() @@ -64,7 +68,7 @@ export default function PlatesTemplate({ containerClassName )} onClick={() => { - if (hasBg) modal.setState(false) + if (hasBg && hasCloseOnCLickOutside) modal.setState(false) }} > {hasTemplate ? ( @@ -77,20 +81,22 @@ export default function PlatesTemplate({ )} onClick={(e) => e.stopPropagation()} > - + {hasCLoseButton && ( + + )}
= 400 && resp.status < 500) { + throw new Error( + ERROR_MAPPING[(data as { detail: string }).detail] ?? + JSON.stringify(data as Object) + .replace(/\s+/, '_') + .replace(/[А-Яа-я]/, '') + .toLowerCase() + ) + } + return (await data) as User + }, + }), + CredentialsProvider({ + id: 'email_token', + type: 'credentials', + credentials: {}, + async authorize(credentials, req) { + const { token } = credentials as { token: string } + + const resp = await AuthProxy.loginByEmailToken(token) + + const data = await resp.json() + + if (resp.status >= 400 && resp.status < 500) { + throw new Error( + ERROR_MAPPING[(data as { detail: string }).detail] ?? + JSON.stringify(data as Object) + .replace(/\s+/, '_') + .replace(/[А-Яа-я]/, '') + .toLowerCase() + ) + } + return (await data) as User + }, + }), + ], + callbacks: { + async jwt({ token, user, account, trigger }) { + if (account?.provider === 'yandex') { + const result = await AuthProxy.exchangeTokenYandex(account.access_token || '') + + if (result.status >= 400 && result.status < 500) { + throw new Error('Непредвиденная ошибка') + } + + const info = await getAll( + (result.data as { access_token: string }).access_token + ) + + return { ...info.token, tokenExpiry: AuthConstants.getExpiresDate() } + } + + if (user) { + try { + token.access = user.token.access + token.refresh = user.token.refresh + token.tokenExpiry = AuthConstants.getExpiresDate() + return token + } catch (error) { + return { ...token, ...user } + } + } + + const shouldRefreshTime = Math.round(Date.now() - token.tokenExpiry) >= 0 + + if (shouldRefreshTime) { + return await AuthProxy.refreshToken(token) + } + + return token + }, + signIn: async ({ user, account, ...props }) => { + if (account?.provider === 'yandex') { + const { data: result, status } = await AuthProxy.exchangeTokenYandex( + account.access_token! + ) + + if (status >= 400 && status < 500) { + return Promise.reject( + new Error( + ERROR_YANDEX_MAPPING[ + (result as { error_description: string }).error_description + ] + ) + ) + } + + return Promise.resolve(true) + } + + return Promise.resolve(true) + }, + async session({ session, token }) { + session.access = token.access + session.refresh = token.refresh + return session + }, + }, + pages: { + signIn: '/login', + error: '/login', + }, +} + +export default NextAuth(authOptions) @@ -18,6 +18,7 @@ import { Error } from '#/shared' import { pingFangFont } from '#/shared/lib/constants/font/font' import { useBlockTelegram } from '#/shared/lib/hooks/use-block-telegram' import { Providers } from '#/widgets/providers' +import { ERROR_MESSAGE, getModalById } from '#/features/modals' const inter = Raleway({ subsets: ['latin'] }) @@ -31,8 +32,17 @@ export interface AppPropsWithLayout extends AppProps { } function App({ Component, pageProps: { session, ...pageProps } }: AppPropsWithLayout) { + const modal = getModalById(ERROR_MESSAGE) + /* axios.interceptors.response.use( + (response) => response, + (error) => { + if (!error.response) { + modal.setState(true) + } + return error + } + ) */ useBlockTelegram() - const getLayout = Component.getLayout ?? ((page) => page) return ( @@ -2,7 +2,7 @@ import axios, { AxiosResponse } from 'axios' import { User } from 'next-auth' import { API_URL } from '#/shared/lib/constants' -import { IOffer } from '#/widgets/payment/model/payment' +import { IOffer } from '#/widgets/payment/ui/payment' interface IUserBalance { current_token_balance: number @@ -3,6 +3,7 @@ import { useRouter } from 'next/navigation' import { api } from './instance' import axios, { InternalAxiosRequestConfig } from 'axios' import { useCallback } from 'react' +import { ERROR_MESSAGE, getModalById } from '#/features/modals' export function makePrivateRequest( callback: (...params: Params) => Promise @@ -11,6 +12,8 @@ export function makePrivateRequest( const { push } = useRouter() + const modal = getModalById(ERROR_MESSAGE) + const onRequest = async (config: any) => { if (config.headers.Authorization) return config @@ -26,6 +29,8 @@ export function makePrivateRequest( api.interceptors.response.use( async ({ status, ...r }) => { + if ([402, 403, 404].includes(status)) modal.setState(true) + if (![401].includes(status)) return { status, ...r } const session = await update('refresh') @@ -39,7 +44,7 @@ export function makePrivateRequest( return { status, ...r } }, (error) => { - if (!error.respose) return (window.location.href = '/network-error') + if (!error.respose) return modal.setState(true) } ) @@ -1,4 +1,5 @@ import { c } from '#/shared/lib/helpers' +import { forwardRef } from 'react' import styles from './common-button.module.scss' export type ButtonVariant = 'outline' | 'primary' | 'primary-outline' | 'gray' @@ -8,25 +9,22 @@ export interface CommonButtonProps extends React.ButtonHTMLAttributes { - return ( - - ) -} +export const CommonButton = forwardRef( + ({ children, variant = 'outline', className, disabled, ...props }, ref) => { + return ( + + ) + } +) @@ -31,11 +31,21 @@ export const InfoPageTemplate = ({

{secondaryTitle}

{description}

- + {buttonText}
- {imageAlt} + {imageAlt}