feat: chat fixes, header cleanup, scrollbar, UI improvements
- chat: fix create_direct endpoint (was /chats/ → /chats/create_direct/ with user_id) - chat: fix backend NotSupportedError — remove select_for_update()+distinct() combo - chat: normalize chat objects (participant_id from other_participant.id) - chat: enrich new chats with contact data so contacts section updates correctly - chat: sendMessage — JSON when no file, FormData only with attachment - chat: show send errors in UI instead of silent catch - header: hide search, language, contacts, workspaces, account buttons - theme: thin custom scrollbar (6px, theme-aware light/dark) - settings: always high contrast, vertical nav only, purple default, Inter font - settings-button: replaced gear icon with dark/light toggle + fullscreen button Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
da3736e131
commit
1b06404d64
|
|
@ -37,14 +37,19 @@ class ChatService:
|
||||||
|
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
# Ищем существующий чат между пользователями
|
# Ищем существующий чат между пользователями
|
||||||
# Используем select_for_update для блокировки найденных записей
|
# select_for_update() несовместим с distinct() в PostgreSQL,
|
||||||
existing_chat = Chat.objects.select_for_update().filter(
|
# поэтому сначала ищем без блокировки, затем лочим найденный объект
|
||||||
|
existing_chat = Chat.objects.filter(
|
||||||
chat_type='direct',
|
chat_type='direct',
|
||||||
participants__user=users[0]
|
participants__user=users[0]
|
||||||
).filter(
|
).filter(
|
||||||
participants__user=users[1]
|
participants__user=users[1]
|
||||||
).distinct().first()
|
).distinct().first()
|
||||||
|
|
||||||
|
if existing_chat:
|
||||||
|
# Лочим конкретную запись для безопасного возврата
|
||||||
|
existing_chat = Chat.objects.select_for_update().get(pk=existing_chat.pk)
|
||||||
|
|
||||||
if existing_chat:
|
if existing_chat:
|
||||||
return existing_chat, False
|
return existing_chat, False
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,14 @@ import { GuestGuard } from 'src/auth/guard';
|
||||||
export default function Layout({ children }) {
|
export default function Layout({ children }) {
|
||||||
return (
|
return (
|
||||||
<GuestGuard>
|
<GuestGuard>
|
||||||
<AuthSplitLayout section={{ title: 'Hi, Welcome back' }}>{children}</AuthSplitLayout>
|
<AuthSplitLayout
|
||||||
|
section={{
|
||||||
|
title: 'Добро пожаловать',
|
||||||
|
subtitle: 'Платформа для онлайн-обучения и работы с репетиторами.',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</AuthSplitLayout>
|
||||||
</GuestGuard>
|
</GuestGuard>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
'use client';
|
|
||||||
|
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
'use client';
|
|
||||||
|
|
||||||
import Button from '@mui/material/Button';
|
import Button from '@mui/material/Button';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
'use client';
|
|
||||||
|
|
||||||
import Button from '@mui/material/Button';
|
import Button from '@mui/material/Button';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
'use client';
|
|
||||||
|
|
||||||
import { DashboardContent } from 'src/layouts/dashboard';
|
import { DashboardContent } from 'src/layouts/dashboard';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
'use client';
|
|
||||||
|
|
||||||
import Button from '@mui/material/Button';
|
import Button from '@mui/material/Button';
|
||||||
import Container from '@mui/material/Container';
|
import Container from '@mui/material/Container';
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
'use client';
|
|
||||||
|
|
||||||
import Button from '@mui/material/Button';
|
import Button from '@mui/material/Button';
|
||||||
import Container from '@mui/material/Container';
|
import Container from '@mui/material/Container';
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
'use client';
|
|
||||||
|
|
||||||
import Container from '@mui/material/Container';
|
import Container from '@mui/material/Container';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
'use client';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
signIn as _signIn,
|
signIn as _signIn,
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
'use client';
|
|
||||||
|
|
||||||
import { Amplify } from 'aws-amplify';
|
import { Amplify } from 'aws-amplify';
|
||||||
import { useMemo, useEffect, useCallback } from 'react';
|
import { useMemo, useEffect, useCallback } from 'react';
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
'use client';
|
|
||||||
|
|
||||||
import { createContext } from 'react';
|
import { createContext } from 'react';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
'use client';
|
|
||||||
|
|
||||||
import { useAuth0, Auth0Provider } from '@auth0/auth0-react';
|
import { useAuth0, Auth0Provider } from '@auth0/auth0-react';
|
||||||
import { useMemo, useState, useEffect, useCallback } from 'react';
|
import { useMemo, useState, useEffect, useCallback } from 'react';
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
'use client';
|
|
||||||
|
|
||||||
import { doc, setDoc, collection } from 'firebase/firestore';
|
import { doc, setDoc, collection } from 'firebase/firestore';
|
||||||
import {
|
import {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
'use client';
|
|
||||||
|
|
||||||
import { doc, getDoc } from 'firebase/firestore';
|
import { doc, getDoc } from 'firebase/firestore';
|
||||||
import { onAuthStateChanged } from 'firebase/auth';
|
import { onAuthStateChanged } from 'firebase/auth';
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
'use client';
|
|
||||||
|
|
||||||
import axios, { endpoints } from 'src/utils/axios';
|
import axios, { endpoints } from 'src/utils/axios';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
'use client';
|
|
||||||
|
|
||||||
import { useMemo, useEffect, useCallback } from 'react';
|
import { useMemo, useEffect, useCallback } from 'react';
|
||||||
import { useSetState } from 'src/hooks/use-set-state';
|
import { useSetState } from 'src/hooks/use-set-state';
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
'use client';
|
|
||||||
|
|
||||||
import { paths } from 'src/routes/paths';
|
import { paths } from 'src/routes/paths';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
'use client';
|
|
||||||
|
|
||||||
import { useMemo, useEffect, useCallback } from 'react';
|
import { useMemo, useEffect, useCallback } from 'react';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
'use client';
|
|
||||||
|
|
||||||
import { useState, useEffect, useCallback } from 'react';
|
import { useState, useEffect, useCallback } from 'react';
|
||||||
|
|
||||||
|
|
@ -10,6 +9,21 @@ import { CONFIG } from 'src/config-global';
|
||||||
import { SplashScreen } from 'src/components/loading-screen';
|
import { SplashScreen } from 'src/components/loading-screen';
|
||||||
|
|
||||||
import { useAuthContext } from '../hooks';
|
import { useAuthContext } from '../hooks';
|
||||||
|
import { STORAGE_KEY } from '../context/jwt/constant';
|
||||||
|
import { isValidToken } from '../context/jwt/utils';
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
// Synchronously check if there's a valid token in localStorage
|
||||||
|
// to avoid showing SplashScreen on every load when user is already authenticated
|
||||||
|
function hasValidStoredToken() {
|
||||||
|
try {
|
||||||
|
const token = localStorage.getItem(STORAGE_KEY);
|
||||||
|
return token ? isValidToken(token) : false;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ----------------------------------------------------------------------
|
// ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
@ -22,7 +36,8 @@ export function AuthGuard({ children }) {
|
||||||
|
|
||||||
const { authenticated, loading } = useAuthContext();
|
const { authenticated, loading } = useAuthContext();
|
||||||
|
|
||||||
const [isChecking, setIsChecking] = useState(true);
|
// Skip splash if we already have a valid token — avoids flash on every page load
|
||||||
|
const [isChecking, setIsChecking] = useState(() => !hasValidStoredToken());
|
||||||
|
|
||||||
const createQueryString = useCallback(
|
const createQueryString = useCallback(
|
||||||
(name, value) => {
|
(name, value) => {
|
||||||
|
|
@ -40,18 +55,8 @@ export function AuthGuard({ children }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!authenticated) {
|
if (!authenticated) {
|
||||||
const { method } = CONFIG.auth;
|
const signInPath = paths.auth.jwt.signIn;
|
||||||
|
|
||||||
const signInPath = {
|
|
||||||
jwt: paths.auth.jwt.signIn,
|
|
||||||
auth0: paths.auth.auth0.signIn,
|
|
||||||
amplify: paths.auth.amplify.signIn,
|
|
||||||
firebase: paths.auth.firebase.signIn,
|
|
||||||
supabase: paths.auth.supabase.signIn,
|
|
||||||
}[method];
|
|
||||||
|
|
||||||
const href = `${signInPath}?${createQueryString('returnTo', pathname)}`;
|
const href = `${signInPath}?${createQueryString('returnTo', pathname)}`;
|
||||||
|
|
||||||
router.replace(href);
|
router.replace(href);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
'use client';
|
|
||||||
|
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
'use client';
|
|
||||||
|
|
||||||
import { m } from 'framer-motion';
|
import { m } from 'framer-motion';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
'use client';
|
|
||||||
|
|
||||||
import { useContext } from 'react';
|
import { useContext } from 'react';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
'use client';
|
|
||||||
|
|
||||||
import { m } from 'framer-motion';
|
import { m } from 'framer-motion';
|
||||||
import { useRef, useState, useEffect } from 'react';
|
import { useRef, useState, useEffect } from 'react';
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
'use client';
|
|
||||||
|
|
||||||
import { LazyMotion } from 'framer-motion';
|
import { LazyMotion } from 'framer-motion';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
'use client';
|
|
||||||
|
|
||||||
import { useRef, useMemo } from 'react';
|
import { useRef, useMemo } from 'react';
|
||||||
import { useScroll } from 'framer-motion';
|
import { useScroll } from 'framer-motion';
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
'use client';
|
|
||||||
|
|
||||||
import { forwardRef } from 'react';
|
import { forwardRef } from 'react';
|
||||||
import { Icon, disableCache } from '@iconify/react';
|
import { Icon, disableCache } from '@iconify/react';
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
'use client';
|
|
||||||
|
|
||||||
import { forwardRef } from 'react';
|
import { forwardRef } from 'react';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
'use client';
|
|
||||||
|
|
||||||
import Box from '@mui/material/Box';
|
import Box from '@mui/material/Box';
|
||||||
import { styled } from '@mui/material/styles';
|
import { styled } from '@mui/material/styles';
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
'use client';
|
|
||||||
|
|
||||||
import Box from '@mui/material/Box';
|
import Box from '@mui/material/Box';
|
||||||
import Portal from '@mui/material/Portal';
|
import Portal from '@mui/material/Portal';
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
'use client';
|
|
||||||
|
|
||||||
import Box from '@mui/material/Box';
|
import Box from '@mui/material/Box';
|
||||||
import Portal from '@mui/material/Portal';
|
import Portal from '@mui/material/Portal';
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
'use client';
|
|
||||||
|
|
||||||
import { useId, forwardRef } from 'react';
|
import { useId, forwardRef } from 'react';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
'use client';
|
|
||||||
|
|
||||||
import './styles.css';
|
import './styles.css';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,10 +7,10 @@ export const STORAGE_KEY = 'app-settings';
|
||||||
export const defaultSettings = {
|
export const defaultSettings = {
|
||||||
colorScheme: 'light',
|
colorScheme: 'light',
|
||||||
direction: 'ltr',
|
direction: 'ltr',
|
||||||
contrast: 'default',
|
contrast: 'hight',
|
||||||
navLayout: 'vertical',
|
navLayout: 'vertical',
|
||||||
primaryColor: 'default',
|
primaryColor: 'purple',
|
||||||
navColor: 'integrate',
|
navColor: 'integrate',
|
||||||
compactLayout: true,
|
compactLayout: false,
|
||||||
fontFamily: defaultFont,
|
fontFamily: 'Inter',
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
export * from './settings-context';
|
||||||
|
|
||||||
export * from './settings-provider';
|
export * from './settings-provider';
|
||||||
|
|
||||||
export * from './use-settings-context';
|
export * from './use-settings-context';
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
import { createContext } from 'react';
|
||||||
|
|
||||||
|
export const SettingsContext = createContext(undefined);
|
||||||
|
|
||||||
|
export const SettingsConsumer = SettingsContext.Consumer;
|
||||||
|
|
@ -1,17 +1,10 @@
|
||||||
'use client';
|
import { useMemo, useState, useCallback } from 'react';
|
||||||
|
|
||||||
import { useMemo, useState, useCallback, createContext } from 'react';
|
|
||||||
|
|
||||||
import { useCookies } from 'src/hooks/use-cookies';
|
import { useCookies } from 'src/hooks/use-cookies';
|
||||||
import { useLocalStorage } from 'src/hooks/use-local-storage';
|
import { useLocalStorage } from 'src/hooks/use-local-storage';
|
||||||
|
|
||||||
import { STORAGE_KEY, defaultSettings } from '../config-settings';
|
import { STORAGE_KEY, defaultSettings } from '../config-settings';
|
||||||
|
import { SettingsContext } from './settings-context';
|
||||||
// ----------------------------------------------------------------------
|
|
||||||
|
|
||||||
export const SettingsContext = createContext(undefined);
|
|
||||||
|
|
||||||
export const SettingsConsumer = SettingsContext.Consumer;
|
|
||||||
|
|
||||||
// ----------------------------------------------------------------------
|
// ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,7 @@
|
||||||
'use client';
|
|
||||||
|
|
||||||
import { useContext } from 'react';
|
import { useContext } from 'react';
|
||||||
|
|
||||||
import { SettingsContext } from './settings-provider';
|
import { SettingsContext } from './settings-context';
|
||||||
|
|
||||||
// ----------------------------------------------------------------------
|
// ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ import { SvgColor } from '../../svg-color';
|
||||||
|
|
||||||
export function FontOptions({ value, options, onClickOption }) {
|
export function FontOptions({ value, options, onClickOption }) {
|
||||||
return (
|
return (
|
||||||
<Block title="Font">
|
<Block title="Шрифт">
|
||||||
<Box component="ul" gap={1.5} display="grid" gridTemplateColumns="repeat(2, 1fr)">
|
<Box component="ul" gap={1.5} display="grid" gridTemplateColumns="repeat(2, 1fr)">
|
||||||
{options.map((option) => {
|
{options.map((option) => {
|
||||||
const selected = value === option;
|
const selected = value === option;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
'use client';
|
|
||||||
|
|
||||||
import { useState, useCallback } from 'react';
|
import { useState, useCallback } from 'react';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,7 @@ export function NavOptions({ options, value, onClickOption, hideNavColor, hideNa
|
||||||
const renderLayout = (
|
const renderLayout = (
|
||||||
<div>
|
<div>
|
||||||
<Box component="span" sx={labelStyles}>
|
<Box component="span" sx={labelStyles}>
|
||||||
Layout
|
Расположение
|
||||||
</Box>
|
</Box>
|
||||||
<Box gap={1.5} display="flex" sx={{ mt: 1.5 }}>
|
<Box gap={1.5} display="flex" sx={{ mt: 1.5 }}>
|
||||||
{options.layouts.map((option) => (
|
{options.layouts.map((option) => (
|
||||||
|
|
@ -58,7 +58,7 @@ export function NavOptions({ options, value, onClickOption, hideNavColor, hideNa
|
||||||
const renderColor = (
|
const renderColor = (
|
||||||
<div>
|
<div>
|
||||||
<Box component="span" sx={labelStyles}>
|
<Box component="span" sx={labelStyles}>
|
||||||
Color
|
Цвет
|
||||||
</Box>
|
</Box>
|
||||||
<Box gap={1.5} display="flex" sx={{ mt: 1.5 }}>
|
<Box gap={1.5} display="flex" sx={{ mt: 1.5 }}>
|
||||||
{options.colors.map((option) => (
|
{options.colors.map((option) => (
|
||||||
|
|
@ -74,7 +74,7 @@ export function NavOptions({ options, value, onClickOption, hideNavColor, hideNa
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Block title="Nav" tooltip="Dashboard only" sx={{ ...cssVars, gap: 2.5 }}>
|
<Block title="Навигация" sx={{ ...cssVars, gap: 2.5 }}>
|
||||||
{!hideNavLayout && renderLayout}
|
{!hideNavLayout && renderLayout}
|
||||||
{!hideNavColor && renderColor}
|
{!hideNavColor && renderColor}
|
||||||
</Block>
|
</Block>
|
||||||
|
|
@ -247,12 +247,11 @@ export function ColorOption({ option, selected, sx, ...other }) {
|
||||||
component="span"
|
component="span"
|
||||||
sx={{
|
sx={{
|
||||||
lineHeight: '18px',
|
lineHeight: '18px',
|
||||||
textTransform: 'capitalize',
|
|
||||||
fontWeight: 'fontWeightSemiBold',
|
fontWeight: 'fontWeightSemiBold',
|
||||||
fontSize: (theme) => theme.typography.pxToRem(13),
|
fontSize: (theme) => theme.typography.pxToRem(13),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{option}
|
{{ integrate: 'Встроен', apparent: 'Явный' }[option] ?? option}
|
||||||
</Box>
|
</Box>
|
||||||
</ButtonBase>
|
</ButtonBase>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ import { SvgColor } from '../../svg-color';
|
||||||
|
|
||||||
export function PresetsOptions({ value, options, onClickOption }) {
|
export function PresetsOptions({ value, options, onClickOption }) {
|
||||||
return (
|
return (
|
||||||
<Block title="Presets">
|
<Block title="Цветовая схема">
|
||||||
<Box component="ul" gap={1.5} display="grid" gridTemplateColumns="repeat(3, 1fr)">
|
<Box component="ul" gap={1.5} display="grid" gridTemplateColumns="repeat(3, 1fr)">
|
||||||
{options.map((option) => {
|
{options.map((option) => {
|
||||||
const selected = value === option.name;
|
const selected = value === option.name;
|
||||||
|
|
|
||||||
|
|
@ -1,195 +1,13 @@
|
||||||
'use client';
|
|
||||||
|
|
||||||
import Box from '@mui/material/Box';
|
|
||||||
import Stack from '@mui/material/Stack';
|
|
||||||
import Badge from '@mui/material/Badge';
|
|
||||||
import Tooltip from '@mui/material/Tooltip';
|
|
||||||
import IconButton from '@mui/material/IconButton';
|
|
||||||
import Typography from '@mui/material/Typography';
|
|
||||||
import Drawer, { drawerClasses } from '@mui/material/Drawer';
|
import Drawer, { drawerClasses } from '@mui/material/Drawer';
|
||||||
import { useTheme, useColorScheme } from '@mui/material/styles';
|
import { useTheme, useColorScheme } from '@mui/material/styles';
|
||||||
|
|
||||||
import COLORS from 'src/theme/core/colors.json';
|
|
||||||
import { paper, varAlpha } from 'src/theme/styles';
|
import { paper, varAlpha } from 'src/theme/styles';
|
||||||
import { defaultFont } from 'src/theme/core/typography';
|
|
||||||
import PRIMARY_COLOR from 'src/theme/with-settings/primary-color.json';
|
|
||||||
|
|
||||||
import { Iconify } from '../../iconify';
|
|
||||||
import { BaseOption } from './base-option';
|
|
||||||
import { NavOptions } from './nav-options';
|
|
||||||
import { Scrollbar } from '../../scrollbar';
|
|
||||||
import { FontOptions } from './font-options';
|
|
||||||
import { useSettingsContext } from '../context';
|
import { useSettingsContext } from '../context';
|
||||||
import { PresetsOptions } from './presets-options';
|
|
||||||
import { defaultSettings } from '../config-settings';
|
|
||||||
import { FullScreenButton } from './fullscreen-button';
|
|
||||||
|
|
||||||
// ----------------------------------------------------------------------
|
// Drawer больше не нужен — все настройки зафиксированы.
|
||||||
|
// Оставляем пустой экспорт для совместимости.
|
||||||
export function SettingsDrawer({
|
export function SettingsDrawer() {
|
||||||
sx,
|
return null;
|
||||||
hideFont,
|
|
||||||
hideCompact,
|
|
||||||
hidePresets,
|
|
||||||
hideNavColor,
|
|
||||||
hideContrast,
|
|
||||||
hideNavLayout,
|
|
||||||
hideDirection,
|
|
||||||
hideColorScheme,
|
|
||||||
}) {
|
|
||||||
const theme = useTheme();
|
|
||||||
|
|
||||||
const settings = useSettingsContext();
|
|
||||||
|
|
||||||
const { mode, setMode } = useColorScheme();
|
|
||||||
|
|
||||||
const renderHead = (
|
|
||||||
<Box display="flex" alignItems="center" sx={{ py: 2, pr: 1, pl: 2.5 }}>
|
|
||||||
<Typography variant="h6" sx={{ flexGrow: 1 }}>
|
|
||||||
Settings
|
|
||||||
</Typography>
|
|
||||||
|
|
||||||
<FullScreenButton />
|
|
||||||
|
|
||||||
<Tooltip title="Reset">
|
|
||||||
<IconButton
|
|
||||||
onClick={() => {
|
|
||||||
settings.onReset();
|
|
||||||
setMode(defaultSettings.colorScheme);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Badge color="error" variant="dot" invisible={!settings.canReset}>
|
|
||||||
<Iconify icon="solar:restart-bold" />
|
|
||||||
</Badge>
|
|
||||||
</IconButton>
|
|
||||||
</Tooltip>
|
|
||||||
|
|
||||||
<Tooltip title="Close">
|
|
||||||
<IconButton onClick={settings.onCloseDrawer}>
|
|
||||||
<Iconify icon="mingcute:close-line" />
|
|
||||||
</IconButton>
|
|
||||||
</Tooltip>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
|
|
||||||
const renderMode = (
|
|
||||||
<BaseOption
|
|
||||||
label="Dark mode"
|
|
||||||
icon="moon"
|
|
||||||
selected={settings.colorScheme === 'dark'}
|
|
||||||
onClick={() => {
|
|
||||||
settings.onUpdateField('colorScheme', mode === 'light' ? 'dark' : 'light');
|
|
||||||
setMode(mode === 'light' ? 'dark' : 'light');
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
const renderContrast = (
|
|
||||||
<BaseOption
|
|
||||||
label="Contrast"
|
|
||||||
icon="contrast"
|
|
||||||
selected={settings.contrast === 'hight'}
|
|
||||||
onClick={() =>
|
|
||||||
settings.onUpdateField('contrast', settings.contrast === 'default' ? 'hight' : 'default')
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
const renderRTL = (
|
|
||||||
<BaseOption
|
|
||||||
label="Right to left"
|
|
||||||
icon="align-right"
|
|
||||||
selected={settings.direction === 'rtl'}
|
|
||||||
onClick={() =>
|
|
||||||
settings.onUpdateField('direction', settings.direction === 'ltr' ? 'rtl' : 'ltr')
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
const renderCompact = (
|
|
||||||
<BaseOption
|
|
||||||
tooltip="Dashboard only and available at large resolutions > 1600px (xl)"
|
|
||||||
label="Compact"
|
|
||||||
icon="autofit-width"
|
|
||||||
selected={settings.compactLayout}
|
|
||||||
onClick={() => settings.onUpdateField('compactLayout', !settings.compactLayout)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
const renderPresets = (
|
|
||||||
<PresetsOptions
|
|
||||||
value={settings.primaryColor}
|
|
||||||
onClickOption={(newValue) => settings.onUpdateField('primaryColor', newValue)}
|
|
||||||
options={[
|
|
||||||
{ name: 'default', value: COLORS.primary.main },
|
|
||||||
{ name: 'cyan', value: PRIMARY_COLOR.cyan.main },
|
|
||||||
{ name: 'purple', value: PRIMARY_COLOR.purple.main },
|
|
||||||
{ name: 'blue', value: PRIMARY_COLOR.blue.main },
|
|
||||||
{ name: 'orange', value: PRIMARY_COLOR.orange.main },
|
|
||||||
{ name: 'red', value: PRIMARY_COLOR.red.main },
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
const renderNav = (
|
|
||||||
<NavOptions
|
|
||||||
value={{
|
|
||||||
color: settings.navColor,
|
|
||||||
layout: settings.navLayout,
|
|
||||||
}}
|
|
||||||
onClickOption={{
|
|
||||||
color: (newValue) => settings.onUpdateField('navColor', newValue),
|
|
||||||
layout: (newValue) => settings.onUpdateField('navLayout', newValue),
|
|
||||||
}}
|
|
||||||
options={{
|
|
||||||
colors: ['integrate', 'apparent'],
|
|
||||||
layouts: ['vertical', 'horizontal', 'mini'],
|
|
||||||
}}
|
|
||||||
hideNavColor={hideNavColor}
|
|
||||||
hideNavLayout={hideNavLayout}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
const renderFont = (
|
|
||||||
<FontOptions
|
|
||||||
value={settings.fontFamily}
|
|
||||||
onClickOption={(newValue) => settings.onUpdateField('fontFamily', newValue)}
|
|
||||||
options={[defaultFont, 'Inter', 'DM Sans', 'Nunito Sans']}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Drawer
|
|
||||||
anchor="right"
|
|
||||||
open={settings.openDrawer}
|
|
||||||
onClose={settings.onCloseDrawer}
|
|
||||||
slotProps={{ backdrop: { invisible: true } }}
|
|
||||||
sx={{
|
|
||||||
[`& .${drawerClasses.paper}`]: {
|
|
||||||
...paper({
|
|
||||||
theme,
|
|
||||||
color: varAlpha(theme.vars.palette.background.defaultChannel, 0.9),
|
|
||||||
}),
|
|
||||||
width: 360,
|
|
||||||
...sx,
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{renderHead}
|
|
||||||
|
|
||||||
<Scrollbar>
|
|
||||||
<Stack spacing={6} sx={{ px: 2.5, pb: 5 }}>
|
|
||||||
<Box gap={2} display="grid" gridTemplateColumns="repeat(2, 1fr)">
|
|
||||||
{!hideColorScheme && renderMode}
|
|
||||||
{!hideContrast && renderContrast}
|
|
||||||
{!hideDirection && renderRTL}
|
|
||||||
{!hideCompact && renderCompact}
|
|
||||||
</Box>
|
|
||||||
{!(hideNavLayout && hideNavColor) && renderNav}
|
|
||||||
{!hidePresets && renderPresets}
|
|
||||||
{!hideFont && renderFont}
|
|
||||||
</Stack>
|
|
||||||
</Scrollbar>
|
|
||||||
</Drawer>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
'use client';
|
|
||||||
|
|
||||||
import Portal from '@mui/material/Portal';
|
import Portal from '@mui/material/Portal';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
'use client';
|
|
||||||
|
|
||||||
import Box from '@mui/material/Box';
|
import Box from '@mui/material/Box';
|
||||||
import Button from '@mui/material/Button';
|
import Button from '@mui/material/Button';
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
'use client';
|
|
||||||
|
|
||||||
import { useMemo, useState, useCallback } from 'react';
|
import { useMemo, useState, useCallback } from 'react';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,24 +6,17 @@ import { localStorageGetItem } from 'src/utils/storage-available';
|
||||||
// ----------------------------------------------------------------------
|
// ----------------------------------------------------------------------
|
||||||
|
|
||||||
export function useLocalStorage(key, initialState) {
|
export function useLocalStorage(key, initialState) {
|
||||||
const [state, set] = useState(initialState);
|
|
||||||
|
|
||||||
const multiValue = initialState && typeof initialState === 'object';
|
const multiValue = initialState && typeof initialState === 'object';
|
||||||
|
|
||||||
|
// Read localStorage synchronously on first render — no useEffect, no flash
|
||||||
|
const [state, set] = useState(() => {
|
||||||
|
const stored = getStorage(key);
|
||||||
|
if (!stored) return initialState;
|
||||||
|
return multiValue ? { ...initialState, ...stored } : stored;
|
||||||
|
});
|
||||||
|
|
||||||
const canReset = !isEqual(state, initialState);
|
const canReset = !isEqual(state, initialState);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const restoredValue = getStorage(key);
|
|
||||||
|
|
||||||
if (restoredValue) {
|
|
||||||
if (multiValue) {
|
|
||||||
set((prevValue) => ({ ...prevValue, ...restoredValue }));
|
|
||||||
} else {
|
|
||||||
set(restoredValue);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [key, multiValue]);
|
|
||||||
|
|
||||||
const setState = useCallback(
|
const setState = useCallback(
|
||||||
(updateState) => {
|
(updateState) => {
|
||||||
if (multiValue) {
|
if (multiValue) {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
'use client';
|
|
||||||
|
|
||||||
import { useScroll, useMotionValueEvent } from 'framer-motion';
|
import { useScroll, useMotionValueEvent } from 'framer-motion';
|
||||||
import { useRef, useMemo, useState, useCallback } from 'react';
|
import { useRef, useMemo, useState, useCallback } from 'react';
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
'use client';
|
|
||||||
|
|
||||||
import Alert from '@mui/material/Alert';
|
import Alert from '@mui/material/Alert';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
'use client';
|
|
||||||
|
|
||||||
import Alert from '@mui/material/Alert';
|
import Alert from '@mui/material/Alert';
|
||||||
|
|
||||||
|
|
@ -38,6 +37,7 @@ export function AuthSplitLayout({ sx, section, children }) {
|
||||||
searchbar: false,
|
searchbar: false,
|
||||||
workspaces: false,
|
workspaces: false,
|
||||||
menuButton: false,
|
menuButton: false,
|
||||||
|
helpLink: false,
|
||||||
localization: false,
|
localization: false,
|
||||||
notifications: false,
|
notifications: false,
|
||||||
}}
|
}}
|
||||||
|
|
@ -77,26 +77,6 @@ export function AuthSplitLayout({ sx, section, children }) {
|
||||||
path: paths.auth.jwt.signIn,
|
path: paths.auth.jwt.signIn,
|
||||||
icon: `${CONFIG.site.basePath}/assets/icons/platforms/ic-jwt.svg`,
|
icon: `${CONFIG.site.basePath}/assets/icons/platforms/ic-jwt.svg`,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
label: 'Firebase',
|
|
||||||
path: paths.auth.firebase.signIn,
|
|
||||||
icon: `${CONFIG.site.basePath}/assets/icons/platforms/ic-firebase.svg`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Amplify',
|
|
||||||
path: paths.auth.amplify.signIn,
|
|
||||||
icon: `${CONFIG.site.basePath}/assets/icons/platforms/ic-amplify.svg`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Auth0',
|
|
||||||
path: paths.auth.auth0.signIn,
|
|
||||||
icon: `${CONFIG.site.basePath}/assets/icons/platforms/ic-auth0.svg`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Supabase',
|
|
||||||
path: paths.auth.supabase.signIn,
|
|
||||||
icon: `${CONFIG.site.basePath}/assets/icons/platforms/ic-supabase.svg`,
|
|
||||||
},
|
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
<Content layoutQuery={layoutQuery}>{children}</Content>
|
<Content layoutQuery={layoutQuery}>{children}</Content>
|
||||||
|
|
|
||||||
|
|
@ -16,9 +16,9 @@ export function Section({
|
||||||
method,
|
method,
|
||||||
layoutQuery,
|
layoutQuery,
|
||||||
methods,
|
methods,
|
||||||
title = 'Manage the job',
|
title = 'Добро пожаловать',
|
||||||
imgUrl = `${CONFIG.site.basePath}/assets/illustrations/illustration-dashboard.webp`,
|
imgUrl = `${CONFIG.site.basePath}/assets/illustrations/illustration-dashboard.webp`,
|
||||||
subtitle = 'More effectively with optimized workflows.',
|
subtitle = 'Платформа для онлайн-обучения и работы с репетиторами.',
|
||||||
...other
|
...other
|
||||||
}) {
|
}) {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
'use client';
|
|
||||||
|
|
||||||
import { useState, useCallback } from 'react';
|
import { useState, useCallback } from 'react';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
'use client';
|
|
||||||
|
|
||||||
import { m } from 'framer-motion';
|
import { m } from 'framer-motion';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
'use client';
|
|
||||||
|
|
||||||
import { m } from 'framer-motion';
|
import { m } from 'framer-motion';
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
|
|
|
||||||
|
|
@ -36,9 +36,9 @@ export function NavUpgrade({ sx, ...other }) {
|
||||||
const [sub, setSub] = useState(undefined); // undefined = loading, null = no sub
|
const [sub, setSub] = useState(undefined); // undefined = loading, null = no sub
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!user) return;
|
if (!user?.id) return;
|
||||||
fetchActiveSubscription().then(setSub);
|
fetchActiveSubscription().then(setSub);
|
||||||
}, [user]);
|
}, [user?.id]);
|
||||||
|
|
||||||
const displayName = user
|
const displayName = user
|
||||||
? `${user.first_name || ''} ${user.last_name || ''}`.trim() || user.email
|
? `${user.first_name || ''} ${user.last_name || ''}`.trim() || user.email
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
'use client';
|
|
||||||
|
|
||||||
import { m } from 'framer-motion';
|
import { m } from 'framer-motion';
|
||||||
import { useState, useEffect, useCallback } from 'react';
|
import { useState, useEffect, useCallback } from 'react';
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
'use client';
|
|
||||||
|
|
||||||
import { useState, useCallback } from 'react';
|
import { useState, useCallback } from 'react';
|
||||||
import parse from 'autosuggest-highlight/parse';
|
import parse from 'autosuggest-highlight/parse';
|
||||||
|
|
|
||||||
|
|
@ -1,45 +1,87 @@
|
||||||
'use client';
|
|
||||||
|
|
||||||
import { m } from 'framer-motion';
|
import { useState, useCallback } from 'react';
|
||||||
|
|
||||||
import Badge from '@mui/material/Badge';
|
import Tooltip from '@mui/material/Tooltip';
|
||||||
import SvgIcon from '@mui/material/SvgIcon';
|
|
||||||
import IconButton from '@mui/material/IconButton';
|
import IconButton from '@mui/material/IconButton';
|
||||||
|
import { useColorScheme } from '@mui/material/styles';
|
||||||
|
|
||||||
|
import { CONFIG } from 'src/config-global';
|
||||||
|
import { Iconify } from 'src/components/iconify';
|
||||||
|
import { SvgColor, svgColorClasses } from 'src/components/svg-color';
|
||||||
|
|
||||||
import { useSettingsContext } from 'src/components/settings/context';
|
import { useSettingsContext } from 'src/components/settings/context';
|
||||||
|
|
||||||
// ----------------------------------------------------------------------
|
// ----------------------------------------------------------------------
|
||||||
|
|
||||||
export function SettingsButton({ sx, ...other }) {
|
function FullScreenButton() {
|
||||||
const settings = useSettingsContext();
|
const [fullscreen, setFullscreen] = useState(false);
|
||||||
|
|
||||||
|
const onToggle = useCallback(() => {
|
||||||
|
if (!document.fullscreenElement) {
|
||||||
|
document.documentElement.requestFullscreen();
|
||||||
|
setFullscreen(true);
|
||||||
|
} else if (document.exitFullscreen) {
|
||||||
|
document.exitFullscreen();
|
||||||
|
setFullscreen(false);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<IconButton
|
<Tooltip title={fullscreen ? 'Выйти из полноэкранного режима' : 'Полный экран'}>
|
||||||
aria-label="settings"
|
<IconButton
|
||||||
onClick={settings.onToggleDrawer}
|
onClick={onToggle}
|
||||||
sx={{ p: 0, width: 40, height: 40, ...sx }}
|
sx={{
|
||||||
{...other}
|
p: 0,
|
||||||
>
|
width: 40,
|
||||||
<Badge color="error" variant="dot" invisible={!settings.canReset}>
|
height: 40,
|
||||||
<SvgIcon
|
[`& .${svgColorClasses.root}`]: {
|
||||||
component={m.svg}
|
background: (theme) =>
|
||||||
animate={{ rotate: 360 }}
|
`linear-gradient(135deg, ${theme.vars.palette.grey[500]} 0%, ${theme.vars.palette.grey[600]} 100%)`,
|
||||||
transition={{ duration: 8, ease: 'linear', repeat: Infinity }}
|
...(fullscreen && {
|
||||||
>
|
background: (theme) =>
|
||||||
{/* https://icon-sets.iconify.design/solar/settings-bold-duotone/ */}
|
`linear-gradient(135deg, ${theme.vars.palette.primary.light} 0%, ${theme.vars.palette.primary.main} 100%)`,
|
||||||
<path
|
}),
|
||||||
fill="currentColor"
|
},
|
||||||
fillRule="evenodd"
|
}}
|
||||||
d="M14.279 2.152C13.909 2 13.439 2 12.5 2s-1.408 0-1.779.152a2.008 2.008 0 0 0-1.09 1.083c-.094.223-.13.484-.145.863a1.615 1.615 0 0 1-.796 1.353a1.64 1.64 0 0 1-1.579.008c-.338-.178-.583-.276-.825-.308a2.026 2.026 0 0 0-1.49.396c-.318.242-.553.646-1.022 1.453c-.47.807-.704 1.21-.757 1.605c-.07.526.074 1.058.4 1.479c.148.192.357.353.68.555c.477.297.783.803.783 1.361c0 .558-.306 1.064-.782 1.36c-.324.203-.533.364-.682.556a1.99 1.99 0 0 0-.399 1.479c.053.394.287.798.757 1.605c.47.807.704 1.21 1.022 1.453c.424.323.96.465 1.49.396c.242-.032.487-.13.825-.308a1.64 1.64 0 0 1 1.58.008c.486.28.774.795.795 1.353c.015.38.051.64.145.863c.204.49.596.88 1.09 1.083c.37.152.84.152 1.779.152s1.409 0 1.779-.152a2.008 2.008 0 0 0 1.09-1.083c.094-.223.13-.483.145-.863c.02-.558.309-1.074.796-1.353a1.64 1.64 0 0 1 1.579-.008c.338.178.583.276.825.308c.53.07 1.066-.073 1.49-.396c.318-.242.553-.646 1.022-1.453c.47-.807.704-1.21.757-1.605a1.99 1.99 0 0 0-.4-1.479c-.148-.192-.357-.353-.68-.555c-.477-.297-.783-.803-.783-1.361c0-.558.306-1.064.782-1.36c.324-.203.533-.364.682-.556a1.99 1.99 0 0 0 .399-1.479c-.053-.394-.287-.798-.757-1.605c-.47-.807-.704-1.21-1.022-1.453a2.026 2.026 0 0 0-1.49-.396c-.242.032-.487.13-.825.308a1.64 1.64 0 0 1-1.58-.008a1.615 1.615 0 0 1-.795-1.353c-.015-.38-.051-.64-.145-.863a2.007 2.007 0 0 0-1.09-1.083"
|
>
|
||||||
clipRule="evenodd"
|
<SvgColor
|
||||||
opacity="0.5"
|
src={`${CONFIG.site.basePath}/assets/icons/setting/${fullscreen ? 'ic-exit-full-screen' : 'ic-full-screen'}.svg`}
|
||||||
/>
|
sx={{ width: 18, height: 18 }}
|
||||||
<path
|
/>
|
||||||
fill="currentColor"
|
</IconButton>
|
||||||
d="M15.523 12c0 1.657-1.354 3-3.023 3c-1.67 0-3.023-1.343-3.023-3S10.83 9 12.5 9c1.67 0 3.023 1.343 3.023 3"
|
</Tooltip>
|
||||||
/>
|
);
|
||||||
</SvgIcon>
|
}
|
||||||
</Badge>
|
|
||||||
</IconButton>
|
// ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
export function SettingsButton({ sx, ...other }) {
|
||||||
|
const settings = useSettingsContext();
|
||||||
|
const { mode, setMode } = useColorScheme();
|
||||||
|
|
||||||
|
const isDark = mode === 'dark' || settings.colorScheme === 'dark';
|
||||||
|
|
||||||
|
const handleToggle = () => {
|
||||||
|
const next = isDark ? 'light' : 'dark';
|
||||||
|
settings.onUpdateField('colorScheme', next);
|
||||||
|
setMode(next);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<FullScreenButton />
|
||||||
|
<Tooltip title={isDark ? 'Светлая тема' : 'Тёмная тема'}>
|
||||||
|
<IconButton
|
||||||
|
onClick={handleToggle}
|
||||||
|
sx={{ p: 0, width: 40, height: 40, ...sx }}
|
||||||
|
{...other}
|
||||||
|
>
|
||||||
|
<Iconify
|
||||||
|
icon={isDark ? 'solar:sun-bold-duotone' : 'solar:moon-bold-duotone'}
|
||||||
|
width={22}
|
||||||
|
/>
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
'use client';
|
|
||||||
|
|
||||||
import { useState, useCallback } from 'react';
|
import { useState, useCallback } from 'react';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -131,7 +131,7 @@ export function HeaderBase({
|
||||||
color="inherit"
|
color="inherit"
|
||||||
sx={{ typography: 'subtitle2' }}
|
sx={{ typography: 'subtitle2' }}
|
||||||
>
|
>
|
||||||
Need help?
|
Нужна помощь?
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
'use client';
|
|
||||||
|
|
||||||
import Box from '@mui/material/Box';
|
import Box from '@mui/material/Box';
|
||||||
import GlobalStyles from '@mui/material/GlobalStyles';
|
import GlobalStyles from '@mui/material/GlobalStyles';
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
'use client';
|
|
||||||
|
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
|
|
@ -42,14 +41,39 @@ export function DashboardLayout({ sx, children, data }) {
|
||||||
|
|
||||||
const layoutQuery = 'lg';
|
const layoutQuery = 'lg';
|
||||||
|
|
||||||
const navData = data?.nav ?? getNavData(user?.role);
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
const navData = useMemo(() => data?.nav ?? getNavData(user?.role), [data?.nav, user?.role]);
|
||||||
|
|
||||||
const isNavMini = settings.navLayout === 'mini';
|
const isNavMini = settings.navLayout === 'mini';
|
||||||
|
const isNavHorizontal = false; // горизонтальная навигация отключена
|
||||||
const isNavHorizontal = settings.navLayout === 'horizontal';
|
|
||||||
|
|
||||||
const isNavVertical = isNavMini || settings.navLayout === 'vertical';
|
const isNavVertical = isNavMini || settings.navLayout === 'vertical';
|
||||||
|
|
||||||
|
const headerData = useMemo(
|
||||||
|
() => ({
|
||||||
|
nav: navData,
|
||||||
|
langs: allLangs,
|
||||||
|
account: _account,
|
||||||
|
contacts: _contacts,
|
||||||
|
workspaces: _workspaces,
|
||||||
|
notifications: _notifications,
|
||||||
|
}),
|
||||||
|
[navData]
|
||||||
|
);
|
||||||
|
|
||||||
|
const headerSlotsDisplay = useMemo(
|
||||||
|
() => ({
|
||||||
|
signIn: false,
|
||||||
|
purchase: false,
|
||||||
|
helpLink: false,
|
||||||
|
searchbar: false,
|
||||||
|
localization: false,
|
||||||
|
contacts: false,
|
||||||
|
workspaces: false,
|
||||||
|
account: false,
|
||||||
|
}),
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<NavMobile
|
<NavMobile
|
||||||
|
|
@ -68,19 +92,8 @@ export function DashboardLayout({ sx, children, data }) {
|
||||||
layoutQuery={layoutQuery}
|
layoutQuery={layoutQuery}
|
||||||
disableElevation={isNavVertical}
|
disableElevation={isNavVertical}
|
||||||
onOpenNav={mobileNavOpen.onTrue}
|
onOpenNav={mobileNavOpen.onTrue}
|
||||||
data={{
|
data={headerData}
|
||||||
nav: navData,
|
slotsDisplay={headerSlotsDisplay}
|
||||||
langs: allLangs,
|
|
||||||
account: _account,
|
|
||||||
contacts: _contacts,
|
|
||||||
workspaces: _workspaces,
|
|
||||||
notifications: _notifications,
|
|
||||||
}}
|
|
||||||
slotsDisplay={{
|
|
||||||
signIn: false,
|
|
||||||
purchase: false,
|
|
||||||
helpLink: false,
|
|
||||||
}}
|
|
||||||
slots={{
|
slots={{
|
||||||
topArea: (
|
topArea: (
|
||||||
<Alert severity="info" sx={{ display: 'none', borderRadius: 0 }}>
|
<Alert severity="info" sx={{ display: 'none', borderRadius: 0 }}>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
'use client';
|
|
||||||
|
|
||||||
import Box from '@mui/material/Box';
|
import Box from '@mui/material/Box';
|
||||||
import { useTheme } from '@mui/material/styles';
|
import { useTheme } from '@mui/material/styles';
|
||||||
|
|
@ -6,8 +5,6 @@ import Container from '@mui/material/Container';
|
||||||
|
|
||||||
import { layoutClasses } from 'src/layouts/classes';
|
import { layoutClasses } from 'src/layouts/classes';
|
||||||
|
|
||||||
import { useSettingsContext } from 'src/components/settings';
|
|
||||||
|
|
||||||
// ----------------------------------------------------------------------
|
// ----------------------------------------------------------------------
|
||||||
|
|
||||||
export function Main({ children, isNavHorizontal, sx, ...other }) {
|
export function Main({ children, isNavHorizontal, sx, ...other }) {
|
||||||
|
|
@ -36,14 +33,12 @@ export function Main({ children, isNavHorizontal, sx, ...other }) {
|
||||||
export function DashboardContent({ sx, children, disablePadding, maxWidth = 'lg', ...other }) {
|
export function DashboardContent({ sx, children, disablePadding, maxWidth = 'lg', ...other }) {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
|
||||||
const settings = useSettingsContext();
|
|
||||||
|
|
||||||
const layoutQuery = 'lg';
|
const layoutQuery = 'lg';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container
|
<Container
|
||||||
className={layoutClasses.content}
|
className={layoutClasses.content}
|
||||||
maxWidth={settings.compactLayout ? maxWidth : false}
|
maxWidth={false}
|
||||||
sx={{
|
sx={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flex: '1 1 auto',
|
flex: '1 1 auto',
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
'use client';
|
|
||||||
|
|
||||||
import Alert from '@mui/material/Alert';
|
import Alert from '@mui/material/Alert';
|
||||||
import { useTheme } from '@mui/material/styles';
|
import { useTheme } from '@mui/material/styles';
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
'use client';
|
|
||||||
|
|
||||||
import Alert from '@mui/material/Alert';
|
import Alert from '@mui/material/Alert';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
'use client';
|
|
||||||
|
|
||||||
// core (MUI)
|
// core (MUI)
|
||||||
import {
|
import {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
'use client';
|
|
||||||
|
|
||||||
import i18next from 'i18next';
|
import i18next from 'i18next';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
/* eslint-disable perfectionist/sort-imports */
|
/* eslint-disable perfectionist/sort-imports */
|
||||||
|
|
||||||
'use client';
|
|
||||||
|
|
||||||
import 'dayjs/locale/en';
|
import 'dayjs/locale/en';
|
||||||
import 'dayjs/locale/vi';
|
import 'dayjs/locale/vi';
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
'use client';
|
|
||||||
|
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import './global.css';
|
import './global.css';
|
||||||
|
|
||||||
import { StrictMode } from 'react';
|
|
||||||
import { createRoot } from 'react-dom/client';
|
import { createRoot } from 'react-dom/client';
|
||||||
|
|
||||||
import App from './app';
|
import App from './app';
|
||||||
|
|
@ -9,8 +8,4 @@ import App from './app';
|
||||||
|
|
||||||
const root = createRoot(document.getElementById('root'));
|
const root = createRoot(document.getElementById('root'));
|
||||||
|
|
||||||
root.render(
|
root.render(<App />);
|
||||||
<StrictMode>
|
|
||||||
<App />
|
|
||||||
</StrictMode>
|
|
||||||
);
|
|
||||||
|
|
|
||||||
|
|
@ -1 +1,7 @@
|
||||||
export { Link as RouterLink } from 'react-router-dom';
|
import { forwardRef } from 'react';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
|
// Bridge component: MUI passes `href` prop, react-router-dom Link uses `to`
|
||||||
|
export const RouterLink = forwardRef(({ href, to, ...other }, ref) => (
|
||||||
|
<Link ref={ref} to={to ?? href} {...other} />
|
||||||
|
));
|
||||||
|
|
|
||||||
|
|
@ -1 +1,7 @@
|
||||||
export { useSearchParams } from 'react-router-dom';
|
import { useSearchParams as useRRSearchParams } from 'react-router-dom';
|
||||||
|
|
||||||
|
// Next.js-compatible wrapper: returns searchParams object directly (not a tuple)
|
||||||
|
export function useSearchParams() {
|
||||||
|
const [searchParams] = useRRSearchParams();
|
||||||
|
return searchParams;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import { GuestGuard } from 'src/auth/guard/guest-guard';
|
||||||
import { AuthSplitLayout } from 'src/layouts/auth-split';
|
import { AuthSplitLayout } from 'src/layouts/auth-split';
|
||||||
import { DashboardLayout } from 'src/layouts/dashboard';
|
import { DashboardLayout } from 'src/layouts/dashboard';
|
||||||
|
|
||||||
import { SplashScreen } from 'src/components/loading-screen';
|
import { LoadingScreen } from 'src/components/loading-screen';
|
||||||
|
|
||||||
// ----------------------------------------------------------------------
|
// ----------------------------------------------------------------------
|
||||||
// Auth
|
// Auth
|
||||||
|
|
@ -102,12 +102,8 @@ const Page404 = lazy(() =>
|
||||||
|
|
||||||
// ----------------------------------------------------------------------
|
// ----------------------------------------------------------------------
|
||||||
|
|
||||||
function Loading() {
|
|
||||||
return <SplashScreen />;
|
|
||||||
}
|
|
||||||
|
|
||||||
function S({ children }) {
|
function S({ children }) {
|
||||||
return <Suspense fallback={<Loading />}>{children}</Suspense>;
|
return <Suspense fallback={<LoadingScreen />}>{children}</Suspense>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function DashboardLayoutWrapper() {
|
function DashboardLayoutWrapper() {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
'use client';
|
|
||||||
|
|
||||||
import Tab from '@mui/material/Tab';
|
import Tab from '@mui/material/Tab';
|
||||||
import Box from '@mui/material/Box';
|
import Box from '@mui/material/Box';
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
'use client';
|
|
||||||
|
|
||||||
import { paths } from 'src/routes/paths';
|
import { paths } from 'src/routes/paths';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
'use client';
|
|
||||||
|
|
||||||
import { paths } from 'src/routes/paths';
|
import { paths } from 'src/routes/paths';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
'use client';
|
|
||||||
|
|
||||||
import { paths } from 'src/routes/paths';
|
import { paths } from 'src/routes/paths';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
'use client';
|
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
'use client';
|
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
'use client';
|
|
||||||
|
|
||||||
import { paths } from 'src/routes/paths';
|
import { paths } from 'src/routes/paths';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
'use client';
|
|
||||||
|
|
||||||
import Box from '@mui/material/Box';
|
import Box from '@mui/material/Box';
|
||||||
import Stack from '@mui/material/Stack';
|
import Stack from '@mui/material/Stack';
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
'use client';
|
|
||||||
|
|
||||||
import Tooltip from '@mui/material/Tooltip';
|
import Tooltip from '@mui/material/Tooltip';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
'use client';
|
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
'use client';
|
|
||||||
|
|
||||||
import dynamic from 'next/dynamic';
|
import dynamic from 'next/dynamic';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
'use client';
|
|
||||||
|
|
||||||
import { paths } from 'src/routes/paths';
|
import { paths } from 'src/routes/paths';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
'use client';
|
|
||||||
|
|
||||||
import AppBar from '@mui/material/AppBar';
|
import AppBar from '@mui/material/AppBar';
|
||||||
import Toolbar from '@mui/material/Toolbar';
|
import Toolbar from '@mui/material/Toolbar';
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
'use client';
|
|
||||||
|
|
||||||
import Box from '@mui/material/Box';
|
import Box from '@mui/material/Box';
|
||||||
import Button from '@mui/material/Button';
|
import Button from '@mui/material/Button';
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
'use client';
|
|
||||||
|
|
||||||
import { paths } from 'src/routes/paths';
|
import { paths } from 'src/routes/paths';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
'use client';
|
|
||||||
|
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import { useState, useCallback } from 'react';
|
import { useState, useCallback } from 'react';
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
'use client';
|
|
||||||
|
|
||||||
import { paths } from 'src/routes/paths';
|
import { paths } from 'src/routes/paths';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
'use client';
|
|
||||||
|
|
||||||
import Stack from '@mui/material/Stack';
|
import Stack from '@mui/material/Stack';
|
||||||
import { useTheme } from '@mui/material/styles';
|
import { useTheme } from '@mui/material/styles';
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
'use client';
|
|
||||||
|
|
||||||
import Box from '@mui/material/Box';
|
import Box from '@mui/material/Box';
|
||||||
import Paper from '@mui/material/Paper';
|
import Paper from '@mui/material/Paper';
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
'use client';
|
|
||||||
|
|
||||||
import Box from '@mui/material/Box';
|
import Box from '@mui/material/Box';
|
||||||
import Paper from '@mui/material/Paper';
|
import Paper from '@mui/material/Paper';
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
'use client';
|
|
||||||
|
|
||||||
import Button from '@mui/material/Button';
|
import Button from '@mui/material/Button';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
'use client';
|
|
||||||
|
|
||||||
import { useState, useCallback } from 'react';
|
import { useState, useCallback } from 'react';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
'use client';
|
|
||||||
|
|
||||||
import { useState, useCallback } from 'react';
|
import { useState, useCallback } from 'react';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
'use client';
|
|
||||||
|
|
||||||
import { paths } from 'src/routes/paths';
|
import { paths } from 'src/routes/paths';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
'use client';
|
|
||||||
|
|
||||||
import Box from '@mui/material/Box';
|
import Box from '@mui/material/Box';
|
||||||
import Stack from '@mui/material/Stack';
|
import Stack from '@mui/material/Stack';
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
'use client';
|
|
||||||
|
|
||||||
import Box from '@mui/material/Box';
|
import Box from '@mui/material/Box';
|
||||||
import Stack from '@mui/material/Stack';
|
import Stack from '@mui/material/Stack';
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
'use client';
|
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue