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():
|
||||
# Ищем существующий чат между пользователями
|
||||
# Используем 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
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
'use client';
|
||||
|
||||
import Button from '@mui/material/Button';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
'use client';
|
||||
|
||||
import Button from '@mui/material/Button';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
'use client';
|
||||
|
||||
import { DashboardContent } from 'src/layouts/dashboard';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
'use client';
|
||||
|
||||
import Button from '@mui/material/Button';
|
||||
import Container from '@mui/material/Container';
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
'use client';
|
||||
|
||||
import Button from '@mui/material/Button';
|
||||
import Container from '@mui/material/Container';
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
'use client';
|
||||
|
||||
import Container from '@mui/material/Container';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
'use client';
|
||||
|
||||
import {
|
||||
signIn as _signIn,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
'use client';
|
||||
|
||||
import { Amplify } from 'aws-amplify';
|
||||
import { useMemo, useEffect, useCallback } from 'react';
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
'use client';
|
||||
|
||||
import { createContext } from 'react';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
'use client';
|
||||
|
||||
import { useAuth0, Auth0Provider } from '@auth0/auth0-react';
|
||||
import { useMemo, useState, useEffect, useCallback } from 'react';
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
'use client';
|
||||
|
||||
import { doc, setDoc, collection } from 'firebase/firestore';
|
||||
import {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
'use client';
|
||||
|
||||
import { doc, getDoc } from 'firebase/firestore';
|
||||
import { onAuthStateChanged } from 'firebase/auth';
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
'use client';
|
||||
|
||||
import axios, { endpoints } from 'src/utils/axios';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
'use client';
|
||||
|
||||
import { useMemo, useEffect, useCallback } from 'react';
|
||||
import { useSetState } from 'src/hooks/use-set-state';
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
'use client';
|
||||
|
||||
import { paths } from 'src/routes/paths';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
'use client';
|
||||
|
||||
import { useMemo, useEffect, useCallback } from 'react';
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
'use client';
|
||||
|
||||
import { m } from 'framer-motion';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
'use client';
|
||||
|
||||
import { useContext } from 'react';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
'use client';
|
||||
|
||||
import { m } from 'framer-motion';
|
||||
import { useRef, useState, useEffect } from 'react';
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
'use client';
|
||||
|
||||
import { LazyMotion } from 'framer-motion';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
'use client';
|
||||
|
||||
import { useRef, useMemo } from 'react';
|
||||
import { useScroll } from 'framer-motion';
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
'use client';
|
||||
|
||||
import { forwardRef } from 'react';
|
||||
import { Icon, disableCache } from '@iconify/react';
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
'use client';
|
||||
|
||||
import { forwardRef } from 'react';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
'use client';
|
||||
|
||||
import Box from '@mui/material/Box';
|
||||
import { styled } from '@mui/material/styles';
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
'use client';
|
||||
|
||||
import Box from '@mui/material/Box';
|
||||
import Portal from '@mui/material/Portal';
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
'use client';
|
||||
|
||||
import Box from '@mui/material/Box';
|
||||
import Portal from '@mui/material/Portal';
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
'use client';
|
||||
|
||||
import { useId, forwardRef } from 'react';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
'use client';
|
||||
|
||||
import './styles.css';
|
||||
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
export * from './settings-context';
|
||||
|
||||
export * from './settings-provider';
|
||||
|
||||
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, 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';
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,7 @@
|
|||
'use client';
|
||||
|
||||
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 }) {
|
||||
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;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
'use client';
|
||||
|
||||
import { useState, useCallback } from 'react';
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
'use client';
|
||||
|
||||
import Portal from '@mui/material/Portal';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
'use client';
|
||||
|
||||
import Box from '@mui/material/Box';
|
||||
import Button from '@mui/material/Button';
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
'use client';
|
||||
|
||||
import { useMemo, useState, useCallback } from 'react';
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
'use client';
|
||||
|
||||
import { useScroll, useMotionValueEvent } from 'framer-motion';
|
||||
import { useRef, useMemo, useState, useCallback } from 'react';
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
'use client';
|
||||
|
||||
import Alert from '@mui/material/Alert';
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
'use client';
|
||||
|
||||
import { useState, useCallback } from 'react';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
'use client';
|
||||
|
||||
import { m } from 'framer-motion';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
'use client';
|
||||
|
||||
import { m } from 'framer-motion';
|
||||
import { useCallback } from 'react';
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
'use client';
|
||||
|
||||
import { m } from 'framer-motion';
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
'use client';
|
||||
|
||||
import { useState, useCallback } from 'react';
|
||||
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 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 (
|
||||
<Tooltip title={fullscreen ? 'Выйти из полноэкранного режима' : 'Полный экран'}>
|
||||
<IconButton
|
||||
aria-label="settings"
|
||||
onClick={settings.onToggleDrawer}
|
||||
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}
|
||||
>
|
||||
<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"
|
||||
<Iconify
|
||||
icon={isDark ? 'solar:sun-bold-duotone' : 'solar:moon-bold-duotone'}
|
||||
width={22}
|
||||
/>
|
||||
<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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
'use client';
|
||||
|
||||
import { useState, useCallback } from 'react';
|
||||
|
||||
|
|
|
|||
|
|
@ -131,7 +131,7 @@ export function HeaderBase({
|
|||
color="inherit"
|
||||
sx={{ typography: 'subtitle2' }}
|
||||
>
|
||||
Need help?
|
||||
Нужна помощь?
|
||||
</Link>
|
||||
)}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
'use client';
|
||||
|
||||
import Box from '@mui/material/Box';
|
||||
import GlobalStyles from '@mui/material/GlobalStyles';
|
||||
|
|
|
|||
|
|
@ -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 }}>
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
'use client';
|
||||
|
||||
import Alert from '@mui/material/Alert';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
'use client';
|
||||
|
||||
import Alert from '@mui/material/Alert';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
'use client';
|
||||
|
||||
// core (MUI)
|
||||
import {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
'use client';
|
||||
|
||||
import i18next from 'i18next';
|
||||
import { useMemo } from 'react';
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
/* eslint-disable perfectionist/sort-imports */
|
||||
|
||||
'use client';
|
||||
|
||||
import 'dayjs/locale/en';
|
||||
import 'dayjs/locale/vi';
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
'use client';
|
||||
|
||||
import dayjs from 'dayjs';
|
||||
import { useCallback } from 'react';
|
||||
|
|
|
|||
|
|
@ -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 />);
|
||||
|
|
|
|||
|
|
@ -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 { 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() {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
'use client';
|
||||
|
||||
import Tab from '@mui/material/Tab';
|
||||
import Box from '@mui/material/Box';
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
'use client';
|
||||
|
||||
import { paths } from 'src/routes/paths';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
'use client';
|
||||
|
||||
import { paths } from 'src/routes/paths';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
'use client';
|
||||
|
||||
import { paths } from 'src/routes/paths';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
'use client';
|
||||
|
||||
import { paths } from 'src/routes/paths';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
'use client';
|
||||
|
||||
import Box from '@mui/material/Box';
|
||||
import Stack from '@mui/material/Stack';
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
'use client';
|
||||
|
||||
import Tooltip from '@mui/material/Tooltip';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
'use client';
|
||||
|
||||
import dynamic from 'next/dynamic';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
'use client';
|
||||
|
||||
import { paths } from 'src/routes/paths';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
'use client';
|
||||
|
||||
import AppBar from '@mui/material/AppBar';
|
||||
import Toolbar from '@mui/material/Toolbar';
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
'use client';
|
||||
|
||||
import Box from '@mui/material/Box';
|
||||
import Button from '@mui/material/Button';
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
'use client';
|
||||
|
||||
import { paths } from 'src/routes/paths';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
'use client';
|
||||
|
||||
import dayjs from 'dayjs';
|
||||
import { useState, useCallback } from 'react';
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
'use client';
|
||||
|
||||
import { paths } from 'src/routes/paths';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
'use client';
|
||||
|
||||
import Stack from '@mui/material/Stack';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
'use client';
|
||||
|
||||
import Box from '@mui/material/Box';
|
||||
import Paper from '@mui/material/Paper';
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
'use client';
|
||||
|
||||
import Box from '@mui/material/Box';
|
||||
import Paper from '@mui/material/Paper';
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
'use client';
|
||||
|
||||
import Button from '@mui/material/Button';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
'use client';
|
||||
|
||||
import { useState, useCallback } from 'react';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
'use client';
|
||||
|
||||
import { useState, useCallback } from 'react';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
'use client';
|
||||
|
||||
import { paths } from 'src/routes/paths';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
'use client';
|
||||
|
||||
import Box from '@mui/material/Box';
|
||||
import Stack from '@mui/material/Stack';
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
'use client';
|
||||
|
||||
import Box from '@mui/material/Box';
|
||||
import Stack from '@mui/material/Stack';
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Reference in New Issue