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:
Dev Server 2026-03-09 21:25:36 +03:00
parent da3736e131
commit 1b06404d64
220 changed files with 899 additions and 717 deletions

View File

@ -37,14 +37,19 @@ class ChatService:
with transaction.atomic():
# Ищем существующий чат между пользователями
# Используем select_for_update для блокировки найденных записей
existing_chat = Chat.objects.select_for_update().filter(
# select_for_update() несовместим с distinct() в PostgreSQL,
# поэтому сначала ищем без блокировки, затем лочим найденный объект
existing_chat = Chat.objects.filter(
chat_type='direct',
participants__user=users[0]
).filter(
participants__user=users[1]
).distinct().first()
if existing_chat:
# Лочим конкретную запись для безопасного возврата
existing_chat = Chat.objects.select_for_update().get(pk=existing_chat.pk)
if existing_chat:
return existing_chat, False

View File

@ -7,7 +7,14 @@ import { GuestGuard } from 'src/auth/guard';
export default function Layout({ children }) {
return (
<GuestGuard>
<AuthSplitLayout section={{ title: 'Hi, Welcome back' }}>{children}</AuthSplitLayout>
<AuthSplitLayout
section={{
title: 'Добро пожаловать',
subtitle: 'Платформа для онлайн-обучения и работы с репетиторами.',
}}
>
{children}
</AuthSplitLayout>
</GuestGuard>
);
}

View File

@ -1,4 +1,3 @@
'use client';
import { useState, useEffect } from 'react';

View File

@ -1,4 +1,3 @@
'use client';
import Button from '@mui/material/Button';

View File

@ -1,4 +1,3 @@
'use client';
import Button from '@mui/material/Button';

View File

@ -1,4 +1,3 @@
'use client';
import { DashboardContent } from 'src/layouts/dashboard';

View File

@ -1,4 +1,3 @@
'use client';
import Button from '@mui/material/Button';
import Container from '@mui/material/Container';

View File

@ -1,4 +1,3 @@
'use client';
import Button from '@mui/material/Button';
import Container from '@mui/material/Container';

View File

@ -1,4 +1,3 @@
'use client';
import Container from '@mui/material/Container';

View File

@ -1,4 +1,3 @@
'use client';
import {
signIn as _signIn,

View File

@ -1,4 +1,3 @@
'use client';
import { Amplify } from 'aws-amplify';
import { useMemo, useEffect, useCallback } from 'react';

View File

@ -1,4 +1,3 @@
'use client';
import { createContext } from 'react';

View File

@ -1,4 +1,3 @@
'use client';
import { useAuth0, Auth0Provider } from '@auth0/auth0-react';
import { useMemo, useState, useEffect, useCallback } from 'react';

View File

@ -1,4 +1,3 @@
'use client';
import { doc, setDoc, collection } from 'firebase/firestore';
import {

View File

@ -1,4 +1,3 @@
'use client';
import { doc, getDoc } from 'firebase/firestore';
import { onAuthStateChanged } from 'firebase/auth';

View File

@ -1,4 +1,3 @@
'use client';
import axios, { endpoints } from 'src/utils/axios';

View File

@ -1,4 +1,3 @@
'use client';
import { useMemo, useEffect, useCallback } from 'react';
import { useSetState } from 'src/hooks/use-set-state';

View File

@ -1,4 +1,3 @@
'use client';
import { paths } from 'src/routes/paths';

View File

@ -1,4 +1,3 @@
'use client';
import { useMemo, useEffect, useCallback } from 'react';

View File

@ -1,4 +1,3 @@
'use client';
import { useState, useEffect, useCallback } from 'react';
@ -10,6 +9,21 @@ import { CONFIG } from 'src/config-global';
import { SplashScreen } from 'src/components/loading-screen';
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 [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(
(name, value) => {
@ -40,18 +55,8 @@ export function AuthGuard({ children }) {
}
if (!authenticated) {
const { method } = CONFIG.auth;
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 signInPath = paths.auth.jwt.signIn;
const href = `${signInPath}?${createQueryString('returnTo', pathname)}`;
router.replace(href);
return;
}

View File

@ -1,4 +1,3 @@
'use client';
import { useState, useEffect } from 'react';

View File

@ -1,4 +1,3 @@
'use client';
import { m } from 'framer-motion';

View File

@ -1,4 +1,3 @@
'use client';
import { useContext } from 'react';

View File

@ -1,4 +1,3 @@
'use client';
import { m } from 'framer-motion';
import { useRef, useState, useEffect } from 'react';

View File

@ -1,4 +1,3 @@
'use client';
import { LazyMotion } from 'framer-motion';

View File

@ -1,4 +1,3 @@
'use client';
import { useRef, useMemo } from 'react';
import { useScroll } from 'framer-motion';

View File

@ -1,4 +1,3 @@
'use client';
import { forwardRef } from 'react';
import { Icon, disableCache } from '@iconify/react';

View File

@ -1,4 +1,3 @@
'use client';
import { forwardRef } from 'react';

View File

@ -1,4 +1,3 @@
'use client';
import Box from '@mui/material/Box';
import { styled } from '@mui/material/styles';

View File

@ -1,4 +1,3 @@
'use client';
import Box from '@mui/material/Box';
import Portal from '@mui/material/Portal';

View File

@ -1,4 +1,3 @@
'use client';
import Box from '@mui/material/Box';
import Portal from '@mui/material/Portal';

View File

@ -1,4 +1,3 @@
'use client';
import { useId, forwardRef } from 'react';

View File

@ -1,4 +1,3 @@
'use client';
import './styles.css';

View File

@ -7,10 +7,10 @@ export const STORAGE_KEY = 'app-settings';
export const defaultSettings = {
colorScheme: 'light',
direction: 'ltr',
contrast: 'default',
contrast: 'hight',
navLayout: 'vertical',
primaryColor: 'default',
primaryColor: 'purple',
navColor: 'integrate',
compactLayout: true,
fontFamily: defaultFont,
compactLayout: false,
fontFamily: 'Inter',
};

View File

@ -1,3 +1,5 @@
export * from './settings-context';
export * from './settings-provider';
export * from './use-settings-context';

View File

@ -0,0 +1,5 @@
import { createContext } from 'react';
export const SettingsContext = createContext(undefined);
export const SettingsConsumer = SettingsContext.Consumer;

View File

@ -1,17 +1,10 @@
'use client';
import { useMemo, useState, useCallback, createContext } from 'react';
import { useMemo, useState, useCallback } from 'react';
import { useCookies } from 'src/hooks/use-cookies';
import { useLocalStorage } from 'src/hooks/use-local-storage';
import { STORAGE_KEY, defaultSettings } from '../config-settings';
// ----------------------------------------------------------------------
export const SettingsContext = createContext(undefined);
export const SettingsConsumer = SettingsContext.Consumer;
import { SettingsContext } from './settings-context';
// ----------------------------------------------------------------------

View File

@ -1,8 +1,7 @@
'use client';
import { useContext } from 'react';
import { SettingsContext } from './settings-provider';
import { SettingsContext } from './settings-context';
// ----------------------------------------------------------------------

View File

@ -11,7 +11,7 @@ import { SvgColor } from '../../svg-color';
export function FontOptions({ value, options, onClickOption }) {
return (
<Block title="Font">
<Block title="Шрифт">
<Box component="ul" gap={1.5} display="grid" gridTemplateColumns="repeat(2, 1fr)">
{options.map((option) => {
const selected = value === option;

View File

@ -1,4 +1,3 @@
'use client';
import { useState, useCallback } from 'react';

View File

@ -40,7 +40,7 @@ export function NavOptions({ options, value, onClickOption, hideNavColor, hideNa
const renderLayout = (
<div>
<Box component="span" sx={labelStyles}>
Layout
Расположение
</Box>
<Box gap={1.5} display="flex" sx={{ mt: 1.5 }}>
{options.layouts.map((option) => (
@ -58,7 +58,7 @@ export function NavOptions({ options, value, onClickOption, hideNavColor, hideNa
const renderColor = (
<div>
<Box component="span" sx={labelStyles}>
Color
Цвет
</Box>
<Box gap={1.5} display="flex" sx={{ mt: 1.5 }}>
{options.colors.map((option) => (
@ -74,7 +74,7 @@ export function NavOptions({ options, value, onClickOption, hideNavColor, hideNa
);
return (
<Block title="Nav" tooltip="Dashboard only" sx={{ ...cssVars, gap: 2.5 }}>
<Block title="Навигация" sx={{ ...cssVars, gap: 2.5 }}>
{!hideNavLayout && renderLayout}
{!hideNavColor && renderColor}
</Block>
@ -247,12 +247,11 @@ export function ColorOption({ option, selected, sx, ...other }) {
component="span"
sx={{
lineHeight: '18px',
textTransform: 'capitalize',
fontWeight: 'fontWeightSemiBold',
fontSize: (theme) => theme.typography.pxToRem(13),
}}
>
{option}
{{ integrate: 'Встроен', apparent: 'Явный' }[option] ?? option}
</Box>
</ButtonBase>
);

View File

@ -11,7 +11,7 @@ import { SvgColor } from '../../svg-color';
export function PresetsOptions({ value, options, onClickOption }) {
return (
<Block title="Presets">
<Block title="Цветовая схема">
<Box component="ul" gap={1.5} display="grid" gridTemplateColumns="repeat(3, 1fr)">
{options.map((option) => {
const selected = value === option.name;

View File

@ -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 { useTheme, useColorScheme } from '@mui/material/styles';
import COLORS from 'src/theme/core/colors.json';
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 { PresetsOptions } from './presets-options';
import { defaultSettings } from '../config-settings';
import { FullScreenButton } from './fullscreen-button';
// ----------------------------------------------------------------------
export function SettingsDrawer({
sx,
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>
);
// Drawer больше не нужен все настройки зафиксированы.
// Оставляем пустой экспорт для совместимости.
export function SettingsDrawer() {
return null;
}

View File

@ -1,4 +1,3 @@
'use client';
import Portal from '@mui/material/Portal';

View File

@ -1,4 +1,3 @@
'use client';
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';

View File

@ -1,4 +1,3 @@
'use client';
import { useMemo, useState, useCallback } from 'react';

View File

@ -6,24 +6,17 @@ import { localStorageGetItem } from 'src/utils/storage-available';
// ----------------------------------------------------------------------
export function useLocalStorage(key, initialState) {
const [state, set] = useState(initialState);
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);
useEffect(() => {
const restoredValue = getStorage(key);
if (restoredValue) {
if (multiValue) {
set((prevValue) => ({ ...prevValue, ...restoredValue }));
} else {
set(restoredValue);
}
}
}, [key, multiValue]);
const setState = useCallback(
(updateState) => {
if (multiValue) {

View File

@ -1,4 +1,3 @@
'use client';
import { useScroll, useMotionValueEvent } from 'framer-motion';
import { useRef, useMemo, useState, useCallback } from 'react';

View File

@ -1,4 +1,3 @@
'use client';
import Alert from '@mui/material/Alert';

View File

@ -1,4 +1,3 @@
'use client';
import Alert from '@mui/material/Alert';
@ -38,6 +37,7 @@ export function AuthSplitLayout({ sx, section, children }) {
searchbar: false,
workspaces: false,
menuButton: false,
helpLink: false,
localization: false,
notifications: false,
}}
@ -77,26 +77,6 @@ export function AuthSplitLayout({ sx, section, children }) {
path: paths.auth.jwt.signIn,
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>

View File

@ -16,9 +16,9 @@ export function Section({
method,
layoutQuery,
methods,
title = 'Manage the job',
title = 'Добро пожаловать',
imgUrl = `${CONFIG.site.basePath}/assets/illustrations/illustration-dashboard.webp`,
subtitle = 'More effectively with optimized workflows.',
subtitle = 'Платформа для онлайн-обучения и работы с репетиторами.',
...other
}) {
const theme = useTheme();

View File

@ -1,4 +1,3 @@
'use client';
import { useState, useCallback } from 'react';

View File

@ -1,4 +1,3 @@
'use client';
import { m } from 'framer-motion';

View File

@ -1,4 +1,3 @@
'use client';
import { m } from 'framer-motion';
import { useCallback } from 'react';

View File

@ -36,9 +36,9 @@ export function NavUpgrade({ sx, ...other }) {
const [sub, setSub] = useState(undefined); // undefined = loading, null = no sub
useEffect(() => {
if (!user) return;
if (!user?.id) return;
fetchActiveSubscription().then(setSub);
}, [user]);
}, [user?.id]);
const displayName = user
? `${user.first_name || ''} ${user.last_name || ''}`.trim() || user.email

View File

@ -1,4 +1,3 @@
'use client';
import { m } from 'framer-motion';
import { useState, useEffect, useCallback } from 'react';

View File

@ -1,4 +1,3 @@
'use client';
import { useState, useCallback } from 'react';
import parse from 'autosuggest-highlight/parse';

View File

@ -1,45 +1,87 @@
'use client';
import { m } from 'framer-motion';
import { useState, useCallback } from 'react';
import Badge from '@mui/material/Badge';
import SvgIcon from '@mui/material/SvgIcon';
import Tooltip from '@mui/material/Tooltip';
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';
// ----------------------------------------------------------------------
export function SettingsButton({ sx, ...other }) {
const settings = useSettingsContext();
function FullScreenButton() {
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 (
<IconButton
aria-label="settings"
onClick={settings.onToggleDrawer}
sx={{ p: 0, width: 40, height: 40, ...sx }}
{...other}
>
<Badge color="error" variant="dot" invisible={!settings.canReset}>
<SvgIcon
component={m.svg}
animate={{ rotate: 360 }}
transition={{ duration: 8, ease: 'linear', repeat: Infinity }}
>
{/* https://icon-sets.iconify.design/solar/settings-bold-duotone/ */}
<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"
opacity="0.5"
/>
<path
fill="currentColor"
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"
/>
</SvgIcon>
</Badge>
</IconButton>
<Tooltip title={fullscreen ? 'Выйти из полноэкранного режима' : 'Полный экран'}>
<IconButton
onClick={onToggle}
sx={{
p: 0,
width: 40,
height: 40,
[`& .${svgColorClasses.root}`]: {
background: (theme) =>
`linear-gradient(135deg, ${theme.vars.palette.grey[500]} 0%, ${theme.vars.palette.grey[600]} 100%)`,
...(fullscreen && {
background: (theme) =>
`linear-gradient(135deg, ${theme.vars.palette.primary.light} 0%, ${theme.vars.palette.primary.main} 100%)`,
}),
},
}}
>
<SvgColor
src={`${CONFIG.site.basePath}/assets/icons/setting/${fullscreen ? 'ic-exit-full-screen' : 'ic-full-screen'}.svg`}
sx={{ width: 18, height: 18 }}
/>
</IconButton>
</Tooltip>
);
}
// ----------------------------------------------------------------------
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>
</>
);
}

View File

@ -1,4 +1,3 @@
'use client';
import { useState, useCallback } from 'react';

View File

@ -131,7 +131,7 @@ export function HeaderBase({
color="inherit"
sx={{ typography: 'subtitle2' }}
>
Need help?
Нужна помощь?
</Link>
)}

View File

@ -1,4 +1,3 @@
'use client';
import Box from '@mui/material/Box';
import GlobalStyles from '@mui/material/GlobalStyles';

View File

@ -1,4 +1,3 @@
'use client';
import { useMemo } from 'react';
@ -42,14 +41,39 @@ export function DashboardLayout({ sx, children, data }) {
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 isNavHorizontal = settings.navLayout === 'horizontal';
const isNavHorizontal = false; // горизонтальная навигация отключена
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 (
<>
<NavMobile
@ -68,19 +92,8 @@ export function DashboardLayout({ sx, children, data }) {
layoutQuery={layoutQuery}
disableElevation={isNavVertical}
onOpenNav={mobileNavOpen.onTrue}
data={{
nav: navData,
langs: allLangs,
account: _account,
contacts: _contacts,
workspaces: _workspaces,
notifications: _notifications,
}}
slotsDisplay={{
signIn: false,
purchase: false,
helpLink: false,
}}
data={headerData}
slotsDisplay={headerSlotsDisplay}
slots={{
topArea: (
<Alert severity="info" sx={{ display: 'none', borderRadius: 0 }}>

View File

@ -1,4 +1,3 @@
'use client';
import Box from '@mui/material/Box';
import { useTheme } from '@mui/material/styles';
@ -6,8 +5,6 @@ import Container from '@mui/material/Container';
import { layoutClasses } from 'src/layouts/classes';
import { useSettingsContext } from 'src/components/settings';
// ----------------------------------------------------------------------
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 }) {
const theme = useTheme();
const settings = useSettingsContext();
const layoutQuery = 'lg';
return (
<Container
className={layoutClasses.content}
maxWidth={settings.compactLayout ? maxWidth : false}
maxWidth={false}
sx={{
display: 'flex',
flex: '1 1 auto',

View File

@ -1,4 +1,3 @@
'use client';
import Alert from '@mui/material/Alert';
import { useTheme } from '@mui/material/styles';

View File

@ -1,4 +1,3 @@
'use client';
import Alert from '@mui/material/Alert';

View File

@ -1,4 +1,3 @@
'use client';
// core (MUI)
import {

View File

@ -1,4 +1,3 @@
'use client';
import i18next from 'i18next';
import { useMemo } from 'react';

View File

@ -1,6 +1,5 @@
/* eslint-disable perfectionist/sort-imports */
'use client';
import 'dayjs/locale/en';
import 'dayjs/locale/vi';

View File

@ -1,4 +1,3 @@
'use client';
import dayjs from 'dayjs';
import { useCallback } from 'react';

View File

@ -1,6 +1,5 @@
import './global.css';
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import App from './app';
@ -9,8 +8,4 @@ import App from './app';
const root = createRoot(document.getElementById('root'));
root.render(
<StrictMode>
<App />
</StrictMode>
);
root.render(<App />);

View File

@ -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} />
));

View File

@ -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;
}

View File

@ -7,7 +7,7 @@ import { GuestGuard } from 'src/auth/guard/guest-guard';
import { AuthSplitLayout } from 'src/layouts/auth-split';
import { DashboardLayout } from 'src/layouts/dashboard';
import { SplashScreen } from 'src/components/loading-screen';
import { LoadingScreen } from 'src/components/loading-screen';
// ----------------------------------------------------------------------
// Auth
@ -102,12 +102,8 @@ const Page404 = lazy(() =>
// ----------------------------------------------------------------------
function Loading() {
return <SplashScreen />;
}
function S({ children }) {
return <Suspense fallback={<Loading />}>{children}</Suspense>;
return <Suspense fallback={<LoadingScreen />}>{children}</Suspense>;
}
function DashboardLayoutWrapper() {

View File

@ -1,4 +1,3 @@
'use client';
import Tab from '@mui/material/Tab';
import Box from '@mui/material/Box';

View File

@ -1,4 +1,3 @@
'use client';
import { paths } from 'src/routes/paths';

View File

@ -1,4 +1,3 @@
'use client';
import { paths } from 'src/routes/paths';

View File

@ -1,4 +1,3 @@
'use client';
import { paths } from 'src/routes/paths';

View File

@ -1,4 +1,3 @@
'use client';
import { useState } from 'react';

View File

@ -1,4 +1,3 @@
'use client';
import { useState } from 'react';

View File

@ -1,4 +1,3 @@
'use client';
import { paths } from 'src/routes/paths';

View File

@ -1,4 +1,3 @@
'use client';
import Box from '@mui/material/Box';
import Stack from '@mui/material/Stack';

View File

@ -1,4 +1,3 @@
'use client';
import Tooltip from '@mui/material/Tooltip';

View File

@ -1,4 +1,3 @@
'use client';
import { useState } from 'react';

View File

@ -1,4 +1,3 @@
'use client';
import dynamic from 'next/dynamic';

View File

@ -1,4 +1,3 @@
'use client';
import { paths } from 'src/routes/paths';

View File

@ -1,4 +1,3 @@
'use client';
import AppBar from '@mui/material/AppBar';
import Toolbar from '@mui/material/Toolbar';

View File

@ -1,4 +1,3 @@
'use client';
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';

View File

@ -1,4 +1,3 @@
'use client';
import { paths } from 'src/routes/paths';

View File

@ -1,4 +1,3 @@
'use client';
import dayjs from 'dayjs';
import { useState, useCallback } from 'react';

View File

@ -1,4 +1,3 @@
'use client';
import { paths } from 'src/routes/paths';

View File

@ -1,4 +1,3 @@
'use client';
import Stack from '@mui/material/Stack';
import { useTheme } from '@mui/material/styles';

View File

@ -1,4 +1,3 @@
'use client';
import Box from '@mui/material/Box';
import Paper from '@mui/material/Paper';

View File

@ -1,4 +1,3 @@
'use client';
import Box from '@mui/material/Box';
import Paper from '@mui/material/Paper';

View File

@ -1,4 +1,3 @@
'use client';
import Button from '@mui/material/Button';

View File

@ -1,4 +1,3 @@
'use client';
import { useState, useCallback } from 'react';

View File

@ -1,4 +1,3 @@
'use client';
import { useState, useCallback } from 'react';

View File

@ -1,4 +1,3 @@
'use client';
import { paths } from 'src/routes/paths';

View File

@ -1,4 +1,3 @@
'use client';
import Box from '@mui/material/Box';
import Stack from '@mui/material/Stack';

View File

@ -1,4 +1,3 @@
'use client';
import Box from '@mui/material/Box';
import Stack from '@mui/material/Stack';

View File

@ -1,4 +1,3 @@
'use client';
import { useState } from 'react';

Some files were not shown because too many files have changed in this diff Show More