Compare commits
10 Commits
688337f778
...
9cbc5910ef
Author | SHA1 | Date | |
---|---|---|---|
|
9cbc5910ef | ||
|
092577f192 | ||
|
05f28b4bdf | ||
|
18b0bdbab7 | ||
|
cb60801a74 | ||
|
40214cef3f | ||
|
386d2b409d | ||
5debb1ebe9 | |||
4157cd574b | |||
8fe7850f05 |
@ -44,6 +44,11 @@ module.exports = {
|
||||
value: '',
|
||||
key: 'courses.statistics',
|
||||
},
|
||||
'courceNameSuggestion': {
|
||||
on: true,
|
||||
value: '',
|
||||
key: 'courceNameSuggestion',
|
||||
},
|
||||
},
|
||||
},
|
||||
config: {
|
||||
|
@ -157,6 +157,13 @@
|
||||
|
||||
"journal.pl.theme.switchDark": "Switch to dark theme",
|
||||
"journal.pl.theme.switchLight": "Switch to light theme",
|
||||
"journal.pl.theme.select": "Select theme",
|
||||
"journal.pl.theme.light": "Light",
|
||||
"journal.pl.theme.dark": "Dark",
|
||||
"journal.pl.theme.pink": "Pink",
|
||||
"journal.pl.theme.blue": "Blue",
|
||||
"journal.pl.theme.green": "Green",
|
||||
"journal.pl.theme.purple": "Purple",
|
||||
|
||||
"journal.pl.lang.switchToEn": "Switch to English",
|
||||
"journal.pl.lang.switchToRu": "Switch to Russian",
|
||||
|
@ -154,6 +154,13 @@
|
||||
|
||||
"journal.pl.theme.switchDark": "Переключить на темную тему",
|
||||
"journal.pl.theme.switchLight": "Переключить на светлую тему",
|
||||
"journal.pl.theme.select": "Выбрать тему",
|
||||
"journal.pl.theme.light": "Светлая",
|
||||
"journal.pl.theme.dark": "Тёмная",
|
||||
"journal.pl.theme.pink": "Розовая",
|
||||
"journal.pl.theme.blue": "Синяя",
|
||||
"journal.pl.theme.green": "Зелёная",
|
||||
"journal.pl.theme.purple": "Фиолетовая",
|
||||
|
||||
"journal.pl.lang.switchToEn": "Переключить на английский",
|
||||
"journal.pl.lang.switchToRu": "Переключить на русский",
|
||||
|
86
package-lock.json
generated
86
package-lock.json
generated
@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "journal.pl",
|
||||
"version": "3.17.2",
|
||||
"version": "3.18.2",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "journal.pl",
|
||||
"version": "3.17.2",
|
||||
"version": "3.18.2",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@brojs/cli": "^1.8.4",
|
||||
@ -2894,12 +2894,12 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "22.13.14",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.14.tgz",
|
||||
"integrity": "sha512-Zs/Ollc1SJ8nKUAgc7ivOEdIBM8JAKgrqqUYi2J997JuKO7/tpQC+WCetQ1sypiKCQWHdvdg9wBNpUPEWZae7w==",
|
||||
"version": "22.14.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.14.0.tgz",
|
||||
"integrity": "sha512-Kmpl+z84ILoG+3T/zQFyAJsU6EPTmOCj8/2+83fSN6djd6I4o7uOuGIH6vq3PrjY5BGitSbFuMN18j3iknubbA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~6.20.0"
|
||||
"undici-types": "~6.21.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/parse-json": {
|
||||
@ -2925,9 +2925,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react-dom": {
|
||||
"version": "18.3.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.5.tgz",
|
||||
"integrity": "sha512-P4t6saawp+b/dFrUr2cvkVsfvPguwsxtH6dNIYRllMsefqFzkZk5UIjzyDOv5g1dXIPdG4Sp1yCR4Z6RCUsG/Q==",
|
||||
"version": "18.3.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.6.tgz",
|
||||
"integrity": "sha512-nf22//wEbKXusP6E9pfOCDwFdHAX4u172eaJI4YkDRQEZiorm6KfYnSC2SWLDMVWUOWPERmJnN0ujeAfTBLvrw==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "^18.0.0"
|
||||
@ -4024,9 +4024,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/caniuse-lite": {
|
||||
"version": "1.0.30001707",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001707.tgz",
|
||||
"integrity": "sha512-3qtRjw/HQSMlDWf+X79N206fepf4SOOU6SQLMaq/0KkZLmSjPxAkBOQQ+FxbHKfHmYLZFfdWsO3KA90ceHPSnw==",
|
||||
"version": "1.0.30001709",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001709.tgz",
|
||||
"integrity": "sha512-NgL3vUTnDrPCZ3zTahp4fsugQ4dc7EKTSzwQDPEel6DMoMnfH2jhry9n2Zm8onbSR+f/QtKHFOA+iAQu4kbtWA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
@ -4943,9 +4943,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/electron-to-chromium": {
|
||||
"version": "1.5.128",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.128.tgz",
|
||||
"integrity": "sha512-bo1A4HH/NS522Ws0QNFIzyPcyUUNV/yyy70Ho1xqfGYzPUme2F/xr4tlEOuM6/A538U1vDA7a4XfCd1CKRegKQ==",
|
||||
"version": "1.5.130",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.130.tgz",
|
||||
"integrity": "sha512-Ou2u7L9j2XLZbhqzyX0jWDj6gA8D3jIfVzt4rikLf3cGBa0VdReuFimBKS9tQJA4+XpeCxj1NoWlfBXzbMa9IA==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/emoji-regex": {
|
||||
@ -5255,9 +5255,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-react": {
|
||||
"version": "7.37.4",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.4.tgz",
|
||||
"integrity": "sha512-BGP0jRmfYyvOyvMoRX/uoUeW+GqNj9y16bPQzqAHf3AYII/tDs+jMN0dBVkl88/OZwNGwrVFxE7riHsXVfy/LQ==",
|
||||
"version": "7.37.5",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz",
|
||||
"integrity": "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@ -5271,7 +5271,7 @@
|
||||
"hasown": "^2.0.2",
|
||||
"jsx-ast-utils": "^2.4.1 || ^3.0.0",
|
||||
"minimatch": "^3.1.2",
|
||||
"object.entries": "^1.1.8",
|
||||
"object.entries": "^1.1.9",
|
||||
"object.fromentries": "^2.0.8",
|
||||
"object.values": "^1.2.1",
|
||||
"prop-types": "^15.8.1",
|
||||
@ -6134,14 +6134,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/framer-motion": {
|
||||
"version": "12.6.2",
|
||||
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.6.2.tgz",
|
||||
"integrity": "sha512-7LgPRlPs5aG8UxeZiMCMZz8firC53+2+9TnWV22tuSi38D3IFRxHRUqOREKckAkt6ztX+Dn6weLcatQilJTMcg==",
|
||||
"version": "12.6.3",
|
||||
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.6.3.tgz",
|
||||
"integrity": "sha512-2hsqknz23aloK85bzMc9nSR2/JP+fValQ459ZTVElFQ0xgwR2YqNjYSuDZdFBPOwVCt4Q9jgyTt6hg6sVOALzw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"motion-dom": "^12.6.1",
|
||||
"motion-utils": "^12.5.0",
|
||||
"motion-dom": "^12.6.3",
|
||||
"motion-utils": "^12.6.3",
|
||||
"tslib": "^2.4.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
@ -6677,9 +6677,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/html-entities": {
|
||||
"version": "2.5.3",
|
||||
"resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.5.3.tgz",
|
||||
"integrity": "sha512-D3AfvN7SjhTgBSA8L1BN4FpPzuEd06uy4lHwSoRWr0lndi9BKaNzPLKGOWZ2ocSGguozr08TTb2jhCLHaemruw==",
|
||||
"version": "2.6.0",
|
||||
"resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.6.0.tgz",
|
||||
"integrity": "sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
@ -7856,19 +7856,19 @@
|
||||
}
|
||||
},
|
||||
"node_modules/motion-dom": {
|
||||
"version": "12.6.1",
|
||||
"resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.6.1.tgz",
|
||||
"integrity": "sha512-8XVsriTUEVOepoIDgE/LDGdg7qaKXWdt+wQA/8z0p8YzJDLYL8gbimZ3YkCLlj7bB2i/4UBD/g+VO7y9ZY0zHQ==",
|
||||
"version": "12.6.3",
|
||||
"resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.6.3.tgz",
|
||||
"integrity": "sha512-gRY08RjcnzgFYLemUZ1lo/e9RkBxR+6d4BRvoeZDSeArG4XQXERSPapKl3LNQRu22Sndjf1h+iavgY0O4NrYqA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"motion-utils": "^12.5.0"
|
||||
"motion-utils": "^12.6.3"
|
||||
}
|
||||
},
|
||||
"node_modules/motion-utils": {
|
||||
"version": "12.5.0",
|
||||
"resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.5.0.tgz",
|
||||
"integrity": "sha512-+hFFzvimn0sBMP9iPxBa9OtRX35ZQ3py0UHnb8U29VD+d8lQ8zH3dTygJWqK7av2v6yhg7scj9iZuvTS0f4+SA==",
|
||||
"version": "12.6.3",
|
||||
"resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.6.3.tgz",
|
||||
"integrity": "sha512-R/b3Ia2VxtTNZ4LTEO5pKYau1OUNHOuUfxuP0WFCTDYdHkeTBR9UtxR1cc8mDmKr8PEhmmfnTKGz3rSMjNRoRg==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
@ -8929,9 +8929,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/react-hook-form": {
|
||||
"version": "7.54.2",
|
||||
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.54.2.tgz",
|
||||
"integrity": "sha512-eHpAUgUjWbZocoQYUHposymRb4ZP6d0uwUnooL2uOybA9/3tPUvoAKqEWK1WaSiTxxOfTpffNZP7QwlnM3/gEg==",
|
||||
"version": "7.55.0",
|
||||
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.55.0.tgz",
|
||||
"integrity": "sha512-XRnjsH3GVMQz1moZTW53MxfoWN7aDpUg/GpVNc4A3eXRVNdGXfbzJ4vM4aLQ8g6XCUh1nIbx70aaNCl7kxnjog==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
@ -10534,9 +10534,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "6.20.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz",
|
||||
"integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==",
|
||||
"version": "6.21.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/unicode-canonical-property-names-ecmascript": {
|
||||
@ -10694,9 +10694,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/use-sync-external-store": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.4.0.tgz",
|
||||
"integrity": "sha512-9WXSPC5fMv61vaupRkCKCxsPxBocVnwakBEkMIHHpkTTg6icbJtg6jzgtLDm4bl3cSHAca52rYWih0k4K3PfHw==",
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz",
|
||||
"integrity": "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "journal.pl",
|
||||
"version": "3.17.2",
|
||||
"version": "3.18.2",
|
||||
"description": "bro-js platform journal ui repo",
|
||||
"main": "./src/index.tsx",
|
||||
"scripts": {
|
||||
|
@ -73,6 +73,14 @@ export const api = createApi({
|
||||
query: (courseId) => `/lesson/${courseId}/ai/generate-lessons`,
|
||||
}),
|
||||
|
||||
generateLessonName: builder.mutation<BaseResponse<{ name: string }[]>, { courseId: string, name: string }>({
|
||||
query: ({ courseId, name }) => ({
|
||||
url: `/lesson/${courseId}/ai/generate-lesson-name`,
|
||||
method: 'POST',
|
||||
body: { name },
|
||||
}),
|
||||
}),
|
||||
|
||||
createLesson: builder.mutation<
|
||||
BaseResponse<Lesson>,
|
||||
Partial<Lesson> & Pick<Lesson, 'name' | 'date'> & { courseId: string }
|
||||
|
69
src/__data__/slices/theme.ts
Normal file
69
src/__data__/slices/theme.ts
Normal file
@ -0,0 +1,69 @@
|
||||
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
||||
import { ThemeType } from '../../types/theme';
|
||||
import {
|
||||
LIGHT_THEME,
|
||||
DARK_THEME,
|
||||
PINK_THEME,
|
||||
BLUE_THEME,
|
||||
GREEN_THEME,
|
||||
PURPLE_THEME,
|
||||
THEMES,
|
||||
getNextTheme
|
||||
} from '../../utils/themes';
|
||||
|
||||
// Ключ для хранения текущей темы в localStorage
|
||||
const THEME_STORAGE_KEY = 'journal-pl-theme';
|
||||
|
||||
// Получаем сохраненную тему из localStorage или используем светлую тему по умолчанию
|
||||
const getSavedTheme = (): ThemeType => {
|
||||
if (typeof window !== 'undefined') {
|
||||
const savedTheme = localStorage.getItem(THEME_STORAGE_KEY) as ThemeType | null;
|
||||
if (savedTheme && THEMES.includes(savedTheme as ThemeType)) {
|
||||
return savedTheme as ThemeType;
|
||||
}
|
||||
}
|
||||
// По умолчанию используем светлую тему
|
||||
return LIGHT_THEME;
|
||||
};
|
||||
|
||||
interface ThemeState {
|
||||
currentTheme: ThemeType;
|
||||
}
|
||||
|
||||
const initialState: ThemeState = {
|
||||
currentTheme: getSavedTheme(),
|
||||
};
|
||||
|
||||
export const themeSlice = createSlice({
|
||||
name: 'theme',
|
||||
initialState,
|
||||
reducers: {
|
||||
setTheme: (state, action: PayloadAction<ThemeType>) => {
|
||||
state.currentTheme = action.payload;
|
||||
|
||||
// Сохраняем выбранную тему в localStorage
|
||||
if (typeof window !== 'undefined') {
|
||||
localStorage.setItem(THEME_STORAGE_KEY, action.payload);
|
||||
}
|
||||
},
|
||||
cycleNextTheme: (state) => {
|
||||
state.currentTheme = getNextTheme(state.currentTheme);
|
||||
|
||||
// Сохраняем выбранную тему в localStorage
|
||||
if (typeof window !== 'undefined') {
|
||||
localStorage.setItem(THEME_STORAGE_KEY, state.currentTheme);
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const { setTheme, cycleNextTheme } = themeSlice.actions;
|
||||
|
||||
// Селекторы для получения информации о текущей теме
|
||||
export const selectCurrentTheme = (state: { theme: ThemeState }) => state.theme.currentTheme;
|
||||
export const selectIsLightVariant = (state: { theme: ThemeState }) =>
|
||||
[LIGHT_THEME, PINK_THEME, BLUE_THEME, GREEN_THEME].includes(state.theme.currentTheme);
|
||||
export const selectIsDarkVariant = (state: { theme: ThemeState }) =>
|
||||
[DARK_THEME, PURPLE_THEME].includes(state.theme.currentTheme);
|
||||
|
||||
export default themeSlice.reducer;
|
@ -3,6 +3,7 @@ import { TypedUseSelectorHook, useSelector } from 'react-redux'
|
||||
|
||||
import { api } from './api/api'
|
||||
import { userSlice } from './slices/user'
|
||||
import { themeSlice } from './slices/theme'
|
||||
|
||||
export const createStore = (preloadedState = {}) =>
|
||||
configureStore({
|
||||
@ -10,6 +11,7 @@ export const createStore = (preloadedState = {}) =>
|
||||
reducer: {
|
||||
[api.reducerPath]: api.reducer,
|
||||
[userSlice.name]: userSlice.reducer,
|
||||
[themeSlice.name]: themeSlice.reducer,
|
||||
},
|
||||
middleware: (getDefaultMiddleware) =>
|
||||
getDefaultMiddleware({
|
||||
|
20
src/app.tsx
20
src/app.tsx
@ -4,18 +4,12 @@ import { Global } from '@emotion/react'
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
import dayjs from './utils/dayjs-config';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ChakraProvider, ColorModeScript, extendTheme } from '@chakra-ui/react'
|
||||
import { ChakraProvider, ColorModeScript } from '@chakra-ui/react'
|
||||
import { Provider } from 'react-redux'
|
||||
|
||||
import { Dashboard } from './dashboard';
|
||||
import { globalStyles } from './global.styles';
|
||||
|
||||
// Расширяем тему Chakra UI
|
||||
const theme = extendTheme({
|
||||
config: {
|
||||
initialColorMode: 'light',
|
||||
useSystemColorMode: false,
|
||||
},
|
||||
})
|
||||
import { chakraTheme } from './utils/theme';
|
||||
|
||||
interface AppProps {
|
||||
store: any; // Тип для store зависит от конкретной реализации хранилища
|
||||
@ -24,17 +18,19 @@ interface AppProps {
|
||||
const App: React.FC<AppProps> = ({ store }) => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<ChakraProvider theme={theme}>
|
||||
<ColorModeScript initialColorMode={theme.config.initialColorMode} />
|
||||
<Provider store={store}>
|
||||
<ChakraProvider theme={chakraTheme}>
|
||||
<ColorModeScript initialColorMode={chakraTheme.config.initialColorMode} />
|
||||
<BrowserRouter>
|
||||
<Helmet>
|
||||
<meta name="viewport" content="width=device-width, user-scalable=no" />
|
||||
<title>{t('journal.pl.title')}</title>
|
||||
</Helmet>
|
||||
<Global styles={globalStyles} />
|
||||
<Dashboard store={store} />
|
||||
<Dashboard />
|
||||
</BrowserRouter>
|
||||
</ChakraProvider>
|
||||
</Provider>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -21,6 +21,10 @@ import { MoonIcon, SunIcon, ChevronRightIcon, InfoIcon, ChevronDownIcon } from '
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Link, useLocation } from 'react-router-dom';
|
||||
import { getNavigationValue } from '@brojs/cli';
|
||||
import { ThemeSelector } from '../theme-selector';
|
||||
import { useThemeManager } from '../../hooks/useThemeManager';
|
||||
import { useAppSelector } from '../../__data__/store';
|
||||
import { selectCurrentTheme } from '../../__data__/slices/theme';
|
||||
|
||||
interface AppHeaderProps {
|
||||
serviceMenuContainerRef?: React.RefObject<HTMLDivElement>;
|
||||
@ -32,7 +36,9 @@ interface AppHeaderProps {
|
||||
}
|
||||
|
||||
export const AppHeader = ({ serviceMenuContainerRef, breadcrumbs }: AppHeaderProps) => {
|
||||
const { colorMode, toggleColorMode } = useColorMode();
|
||||
const { isLightVariant, isDarkVariant } = useThemeManager();
|
||||
// Используем напрямую селектор из Redux для получения темы и для передачи в key
|
||||
const currentTheme = useAppSelector(selectCurrentTheme);
|
||||
const { t, i18n } = useTranslation();
|
||||
const location = useLocation();
|
||||
|
||||
@ -55,11 +61,11 @@ export const AppHeader = ({ serviceMenuContainerRef, breadcrumbs }: AppHeaderPro
|
||||
};
|
||||
|
||||
// Определяем размеры для разных устройств
|
||||
const fontSize = useBreakpointValue({ base: 'xs', sm: 'xs', md: 'sm' });
|
||||
const fontSize = useBreakpointValue({ base: 'xs', sm: 'xs', md: 'sm' }, { ssr: false });
|
||||
|
||||
// Проверяем, на каком устройстве находимся
|
||||
const [isLargerThan768] = useMediaQuery("(min-width: 768px)");
|
||||
const [isLargerThan480] = useMediaQuery("(min-width: 480px)");
|
||||
const [isLargerThan768] = useMediaQuery("(min-width: 768px)", { ssr: false });
|
||||
const [isLargerThan480] = useMediaQuery("(min-width: 480px)", { ssr: false });
|
||||
|
||||
// Вертикальное отображение на мобильных устройствах
|
||||
const isMobile = !isLargerThan480;
|
||||
@ -68,23 +74,25 @@ export const AppHeader = ({ serviceMenuContainerRef, breadcrumbs }: AppHeaderPro
|
||||
const horizontalSeparator = useBreakpointValue({
|
||||
sm: <ChevronRightIcon color="gray.400" fontSize="xs" />,
|
||||
md: <ChevronRightIcon color="gray.400" />
|
||||
});
|
||||
}, { ssr: false });
|
||||
|
||||
const toggleLanguage = () => {
|
||||
const newLang = i18n.language === 'ru' ? 'en' : 'ru';
|
||||
i18n.changeLanguage(newLang);
|
||||
};
|
||||
|
||||
// Используем key={currentTheme} для принудительного перерендеринга при изменении темы
|
||||
return (
|
||||
<Box
|
||||
as="header"
|
||||
width="100%"
|
||||
py={{ base: 2, md: 3 }}
|
||||
bg={colorMode === 'light' ? 'white' : 'gray.800'}
|
||||
bg={isLightVariant ? 'white' : 'gray.800'}
|
||||
boxShadow="sm"
|
||||
position="sticky"
|
||||
top={0}
|
||||
zIndex={10}
|
||||
key={currentTheme} // Добавляем ключ для принудительного перерендеринга при изменении темы
|
||||
>
|
||||
{/* Рендеринг dots контейнера вне условной логики, всегда присутствует в DOM */}
|
||||
{serviceMenuContainerRef && (
|
||||
@ -126,18 +134,7 @@ export const AppHeader = ({ serviceMenuContainerRef, breadcrumbs }: AppHeaderPro
|
||||
{i18n.language === 'ru' ? 'EN' : 'RU'}
|
||||
</Button>
|
||||
|
||||
<IconButton
|
||||
aria-label={colorMode === 'light'
|
||||
? t('journal.pl.theme.switchDark')
|
||||
: t('journal.pl.theme.switchLight')
|
||||
}
|
||||
icon={colorMode === 'light' ? <MoonIcon boxSize={{ base: "14px" }} /> : <SunIcon boxSize={{ base: "14px" }} />}
|
||||
onClick={toggleColorMode}
|
||||
variant="ghost"
|
||||
size={{ base: "sm" }}
|
||||
minW={{ base: "30px" }}
|
||||
h={{ base: "30px" }}
|
||||
/>
|
||||
<ThemeSelector variant="icon" size="sm" />
|
||||
</HStack>
|
||||
</Flex>
|
||||
|
||||
@ -157,7 +154,7 @@ export const AppHeader = ({ serviceMenuContainerRef, breadcrumbs }: AppHeaderPro
|
||||
py={1}
|
||||
borderRadius="md"
|
||||
_hover={!crumb.isCurrentPage && crumb.path ? {
|
||||
bg: colorMode === 'light' ? 'gray.50' : 'gray.700',
|
||||
bg: isLightVariant ? 'gray.50' : 'gray.700',
|
||||
} : {}}
|
||||
>
|
||||
{index > 0 && (
|
||||
@ -184,7 +181,7 @@ export const AppHeader = ({ serviceMenuContainerRef, breadcrumbs }: AppHeaderPro
|
||||
) : (
|
||||
<Text
|
||||
fontWeight={crumb.isCurrentPage ? "bold" : "medium"}
|
||||
color={crumb.isCurrentPage ? (colorMode === 'light' ? 'cyan.600' : 'cyan.300') : undefined}
|
||||
color={crumb.isCurrentPage ? (isLightVariant ? 'cyan.600' : 'cyan.300') : undefined}
|
||||
fontSize={fontSize}
|
||||
noOfLines={1}
|
||||
title={crumb.title}
|
||||
@ -226,7 +223,7 @@ export const AppHeader = ({ serviceMenuContainerRef, breadcrumbs }: AppHeaderPro
|
||||
textOverflow="ellipsis"
|
||||
display="inline-block"
|
||||
fontWeight={crumb.isCurrentPage ? "bold" : "medium"}
|
||||
color={crumb.isCurrentPage ? (colorMode === 'light' ? 'cyan.600' : 'cyan.300') : undefined}
|
||||
color={crumb.isCurrentPage ? (isLightVariant ? 'cyan.600' : 'cyan.300') : undefined}
|
||||
title={crumb.title}
|
||||
>
|
||||
{crumb.title}
|
||||
@ -254,18 +251,7 @@ export const AppHeader = ({ serviceMenuContainerRef, breadcrumbs }: AppHeaderPro
|
||||
{i18n.language === 'ru' ? 'EN' : 'RU'}
|
||||
</Button>
|
||||
|
||||
<IconButton
|
||||
aria-label={colorMode === 'light'
|
||||
? t('journal.pl.theme.switchDark')
|
||||
: t('journal.pl.theme.switchLight')
|
||||
}
|
||||
icon={colorMode === 'light' ? <MoonIcon boxSize={{ sm: "14px", md: "16px" }} /> : <SunIcon boxSize={{ sm: "14px", md: "16px" }} />}
|
||||
onClick={toggleColorMode}
|
||||
variant="ghost"
|
||||
size={{ sm: "sm", md: "md" }}
|
||||
minW={{ sm: "34px" }}
|
||||
h={{ sm: "34px" }}
|
||||
/>
|
||||
<ThemeSelector variant="icon" size="sm" />
|
||||
</HStack>
|
||||
</Flex>
|
||||
)}
|
||||
|
@ -4,13 +4,13 @@ import { motion } from 'framer-motion'
|
||||
import { User, Reaction } from '../../__data__/model'
|
||||
import { UserCard } from '../user-card'
|
||||
import { StudentCardBack } from './StudentCardBack'
|
||||
import { useColorMode } from '@chakra-ui/react'
|
||||
import { useThemeManager } from '../../hooks/useThemeManager'
|
||||
|
||||
// Компонент маленькой батарейки для отображения в углу карточки
|
||||
const BatteryIndicator: React.FC<{
|
||||
student: User & { present?: boolean; recentlyPresent?: boolean }
|
||||
}> = ({ student }) => {
|
||||
const { colorMode } = useColorMode();
|
||||
const { isLightVariant, isDarkVariant } = useThemeManager();
|
||||
|
||||
// Та же логика из StudentCardBack для определения уровня батареи
|
||||
const getAttendanceLevel = () => {
|
||||
@ -28,17 +28,17 @@ const BatteryIndicator: React.FC<{
|
||||
// Цвета для разных уровней заряда батареи
|
||||
const colors = [
|
||||
// Empty (0)
|
||||
{ primary: colorMode === "light" ? "#E53E3E" : "#F56565" },
|
||||
{ primary: isLightVariant ? "#E53E3E" : "#F56565" },
|
||||
// Very Low (1)
|
||||
{ primary: colorMode === "light" ? "#DD6B20" : "#ED8936" },
|
||||
{ primary: isLightVariant ? "#DD6B20" : "#ED8936" },
|
||||
// Low (2)
|
||||
{ primary: colorMode === "light" ? "#D69E2E" : "#ECC94B" },
|
||||
{ primary: isLightVariant ? "#D69E2E" : "#ECC94B" },
|
||||
// Medium (3)
|
||||
{ primary: colorMode === "light" ? "#38B2AC" : "#4FD1C5" },
|
||||
{ primary: isLightVariant ? "#38B2AC" : "#4FD1C5" },
|
||||
// Good (4)
|
||||
{ primary: colorMode === "light" ? "#3182CE" : "#4299E1" },
|
||||
{ primary: isLightVariant ? "#3182CE" : "#4299E1" },
|
||||
// Excellent (5)
|
||||
{ primary: colorMode === "light" ? "#38A169" : "#48BB78" }
|
||||
{ primary: isLightVariant ? "#38A169" : "#48BB78" }
|
||||
];
|
||||
|
||||
const color = colors[batteryLevel].primary;
|
||||
@ -108,6 +108,8 @@ export const StudentCard: React.FC<StudentCardProps> = ({
|
||||
onAddUser,
|
||||
reaction
|
||||
}) => {
|
||||
const { isLightVariant } = useThemeManager();
|
||||
|
||||
return (
|
||||
<motion.li
|
||||
key={student.sub}
|
||||
@ -163,7 +165,7 @@ export const StudentCard: React.FC<StudentCardProps> = ({
|
||||
/>
|
||||
|
||||
{/* Battery indicator in corner */}
|
||||
<BatteryIndicator student={student} />
|
||||
{/* <BatteryIndicator student={student} /> */}
|
||||
</Box>
|
||||
|
||||
{/* Back side */}
|
||||
|
@ -1,9 +1,11 @@
|
||||
import React from 'react'
|
||||
import { Box, Flex, useColorMode } from '@chakra-ui/react'
|
||||
import { Box, Flex, useColorMode, Text } from '@chakra-ui/react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { User } from '../../__data__/model'
|
||||
import { AddMissedButton } from './sstyle'
|
||||
import { AddIcon } from '@chakra-ui/icons'
|
||||
import { UserCard } from '../user-card'
|
||||
import { useThemeManager } from '../../hooks/useThemeManager'
|
||||
|
||||
interface StudentCardBackProps {
|
||||
student: User & { present?: boolean; recentlyPresent?: boolean }
|
||||
@ -13,140 +15,106 @@ interface StudentCardBackProps {
|
||||
export const StudentCardBack: React.FC<StudentCardBackProps> = ({ student, onAddUser }) => {
|
||||
const { colorMode } = useColorMode()
|
||||
const { t } = useTranslation()
|
||||
const { isLightVariant, currentTheme } = useThemeManager()
|
||||
|
||||
// Determine attendance level based on student data
|
||||
// We simulate different battery levels based on student presence
|
||||
const getAttendanceLevel = () => {
|
||||
// Using a scale of 0-5 for battery levels (6 segments total)
|
||||
// In a real scenario, you would calculate this based on attendance history
|
||||
// Is the student marked as present?
|
||||
const isPresent = !!student.present;
|
||||
|
||||
// If student is present, show higher battery level
|
||||
if (student.present) {
|
||||
return student.recentlyPresent ? 4 : 5; // Full or almost full if present
|
||||
// Темы для карточки
|
||||
const themeStyles = {
|
||||
// Стандартная светлая тема
|
||||
light: {
|
||||
gradient: 'linear-gradient(135deg, rgba(226, 232, 240, 0.6), rgba(255, 255, 255, 0.9))',
|
||||
text: "#2E7D32",
|
||||
infoText: "gray.600",
|
||||
shadowColor: 'rgba(0, 0, 0, 0.1)',
|
||||
background: "white"
|
||||
},
|
||||
// Стандартная темная тема
|
||||
dark: {
|
||||
gradient: 'linear-gradient(135deg, rgba(30, 30, 30, 0.95), rgba(45, 55, 72, 0.7))',
|
||||
text: "#81C784",
|
||||
infoText: "gray.300",
|
||||
shadowColor: 'rgba(255, 255, 255, 0.2)',
|
||||
background: "gray.700"
|
||||
},
|
||||
// Синяя тема
|
||||
blue: {
|
||||
gradient: isLightVariant
|
||||
? 'linear-gradient(135deg, rgba(235, 244, 255, 0.8), rgba(202, 228, 255, 0.9))'
|
||||
: 'linear-gradient(135deg, rgba(10, 37, 64, 0.95), rgba(28, 69, 118, 0.8))',
|
||||
text: isLightVariant ? "#1A365D" : "#63B3ED",
|
||||
infoText: isLightVariant ? "blue.700" : "blue.200",
|
||||
shadowColor: isLightVariant ? 'rgba(40, 80, 150, 0.1)' : 'rgba(120, 180, 255, 0.2)',
|
||||
background: isLightVariant ? "white" : "gray.800"
|
||||
},
|
||||
// Зеленая тема
|
||||
green: {
|
||||
gradient: isLightVariant
|
||||
? 'linear-gradient(135deg, rgba(235, 255, 240, 0.8), rgba(210, 250, 215, 0.9))'
|
||||
: 'linear-gradient(135deg, rgba(10, 64, 37, 0.95), rgba(28, 118, 69, 0.8))',
|
||||
text: isLightVariant ? "#22543D" : "#68D391",
|
||||
infoText: isLightVariant ? "green.700" : "green.200",
|
||||
shadowColor: isLightVariant ? 'rgba(40, 150, 80, 0.1)' : 'rgba(120, 255, 180, 0.2)',
|
||||
background: isLightVariant ? "white" : "gray.800"
|
||||
},
|
||||
// Фиолетовая тема
|
||||
purple: {
|
||||
gradient: isLightVariant
|
||||
? 'linear-gradient(135deg, rgba(245, 240, 255, 0.8), rgba(230, 215, 250, 0.9))'
|
||||
: 'linear-gradient(135deg, rgba(44, 16, 74, 0.95), rgba(79, 32, 130, 0.8))',
|
||||
text: isLightVariant ? "#553C9A" : "#B794F4",
|
||||
infoText: isLightVariant ? "purple.700" : "purple.200",
|
||||
shadowColor: isLightVariant ? 'rgba(100, 40, 150, 0.1)' : 'rgba(180, 120, 255, 0.2)',
|
||||
background: isLightVariant ? "white" : "gray.800"
|
||||
},
|
||||
// Янтарная тема
|
||||
amber: {
|
||||
gradient: isLightVariant
|
||||
? 'linear-gradient(135deg, rgba(255, 250, 235, 0.8), rgba(255, 235, 200, 0.9))'
|
||||
: 'linear-gradient(135deg, rgba(74, 50, 16, 0.95), rgba(130, 90, 32, 0.8))',
|
||||
text: isLightVariant ? "#7B341E" : "#FBBF24",
|
||||
infoText: isLightVariant ? "orange.700" : "yellow.200",
|
||||
shadowColor: isLightVariant ? 'rgba(150, 100, 40, 0.1)' : 'rgba(255, 180, 120, 0.2)',
|
||||
background: isLightVariant ? "white" : "gray.800"
|
||||
},
|
||||
// Розовая тема
|
||||
pink: {
|
||||
gradient: isLightVariant
|
||||
? 'linear-gradient(135deg, rgba(255, 240, 245, 0.8), rgba(252, 217, 234, 0.9))'
|
||||
: 'linear-gradient(135deg, rgba(74, 16, 50, 0.95), rgba(139, 39, 116, 0.8))',
|
||||
text: isLightVariant ? "#97266D" : "#F687B3",
|
||||
infoText: isLightVariant ? "pink.700" : "pink.200",
|
||||
shadowColor: isLightVariant ? 'rgba(150, 40, 100, 0.1)' : 'rgba(255, 150, 210, 0.2)',
|
||||
background: isLightVariant ? "white" : "gray.800"
|
||||
}
|
||||
};
|
||||
|
||||
// Определяем текущую тему
|
||||
const getCurrentTheme = () => {
|
||||
// Базовая логика - используем темную/светлую тему
|
||||
let theme = isLightVariant ? 'light' : 'dark';
|
||||
|
||||
// Если доступна информация о текущей теме, используем соответствующую
|
||||
if (currentTheme) {
|
||||
if (currentTheme.includes('blue')) {
|
||||
theme = 'blue';
|
||||
} else if (currentTheme.includes('green')) {
|
||||
theme = 'green';
|
||||
} else if (currentTheme.includes('purple')) {
|
||||
theme = 'purple';
|
||||
} else if (currentTheme.includes('pink')) {
|
||||
theme = 'pink';
|
||||
} else if (currentTheme.includes('amber') || currentTheme.includes('orange') || currentTheme.includes('yellow')) {
|
||||
theme = 'amber';
|
||||
}
|
||||
}
|
||||
|
||||
// For absent students, randomly assign a lower battery level (0-3)
|
||||
// In a real implementation, this would come from attendance history
|
||||
const id = student.sub || '';
|
||||
// Use the student ID hash to deterministically assign a battery level
|
||||
// This creates a pseudo-random but consistent level for each student
|
||||
const idSum = id.split('').reduce((sum, char) => sum + char.charCodeAt(0), 0);
|
||||
return Math.min(3, Math.floor((idSum % 100) / 25));
|
||||
}
|
||||
return themeStyles[theme as keyof typeof themeStyles];
|
||||
};
|
||||
|
||||
const batteryLevel = getAttendanceLevel();
|
||||
|
||||
// Check if student recently joined the class
|
||||
const isRecentlyJoined = !!student.recentlyPresent;
|
||||
|
||||
// Get color scheme based on battery level
|
||||
const getBatteryColors = () => {
|
||||
// Colors for different battery levels
|
||||
const colorSchemes = [
|
||||
// Empty (0)
|
||||
{
|
||||
light: {
|
||||
primary: "#E53E3E", // red.500
|
||||
secondary: "#FED7D7", // red.100
|
||||
accent: "#C53030", // red.700
|
||||
text: "journal.pl.lesson.veryPoorAttendance"
|
||||
},
|
||||
dark: {
|
||||
primary: "#F56565", // red.400
|
||||
secondary: "#9B2C2C", // red.800
|
||||
accent: "#FEB2B2", // red.200
|
||||
text: "journal.pl.lesson.veryPoorAttendance"
|
||||
}
|
||||
},
|
||||
// Very Low (1)
|
||||
{
|
||||
light: {
|
||||
primary: "#DD6B20", // orange.500
|
||||
secondary: "#FEEBC8", // orange.100
|
||||
accent: "#C05621", // orange.700
|
||||
text: "journal.pl.lesson.poorAttendance"
|
||||
},
|
||||
dark: {
|
||||
primary: "#ED8936", // orange.400
|
||||
secondary: "#9C4221", // orange.800
|
||||
accent: "#FBD38D", // orange.200
|
||||
text: "journal.pl.lesson.poorAttendance"
|
||||
}
|
||||
},
|
||||
// Low (2)
|
||||
{
|
||||
light: {
|
||||
primary: "#D69E2E", // yellow.500
|
||||
secondary: "#FEFCBF", // yellow.100
|
||||
accent: "#B7791F", // yellow.700
|
||||
text: "journal.pl.lesson.lowAttendance"
|
||||
},
|
||||
dark: {
|
||||
primary: "#ECC94B", // yellow.400
|
||||
secondary: "#975A16", // yellow.800
|
||||
accent: "#F6E05E", // yellow.200
|
||||
text: "journal.pl.lesson.lowAttendance"
|
||||
}
|
||||
},
|
||||
// Medium (3)
|
||||
{
|
||||
light: {
|
||||
primary: "#38B2AC", // teal.500
|
||||
secondary: "#B2F5EA", // teal.100
|
||||
accent: "#285E61", // teal.700
|
||||
text: "journal.pl.lesson.mediumAttendance"
|
||||
},
|
||||
dark: {
|
||||
primary: "#4FD1C5", // teal.400
|
||||
secondary: "#234E52", // teal.800
|
||||
accent: "#81E6D9", // teal.200
|
||||
text: "journal.pl.lesson.mediumAttendance"
|
||||
}
|
||||
},
|
||||
// Good (4)
|
||||
{
|
||||
light: {
|
||||
primary: "#3182CE", // blue.500
|
||||
secondary: "#BEE3F8", // blue.100
|
||||
accent: "#2C5282", // blue.700
|
||||
text: "journal.pl.lesson.goodAttendance"
|
||||
},
|
||||
dark: {
|
||||
primary: "#4299E1", // blue.400
|
||||
secondary: "#2A4365", // blue.800
|
||||
accent: "#90CDF4", // blue.200
|
||||
text: "journal.pl.lesson.goodAttendance"
|
||||
}
|
||||
},
|
||||
// Excellent (5)
|
||||
{
|
||||
light: {
|
||||
primary: "#38A169", // green.500
|
||||
secondary: "#C6F6D5", // green.100
|
||||
accent: "#276749", // green.700
|
||||
text: "journal.pl.lesson.excellentAttendance"
|
||||
},
|
||||
dark: {
|
||||
primary: "#48BB78", // green.400
|
||||
secondary: "#22543D", // green.800
|
||||
accent: "#9AE6B4", // green.200
|
||||
text: "journal.pl.lesson.excellentAttendance"
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
const scheme = colorSchemes[batteryLevel];
|
||||
return scheme[colorMode === 'light' ? 'light' : 'dark'];
|
||||
}
|
||||
|
||||
const colors = getBatteryColors();
|
||||
|
||||
// Function to determine which battery segments to fill based on level
|
||||
const getSegmentFill = (segmentIndex: number) => {
|
||||
const isActive = segmentIndex <= batteryLevel;
|
||||
|
||||
return isActive ? colors.primary : 'transparent';
|
||||
}
|
||||
// Получаем стили для текущей темы
|
||||
const theme = getCurrentTheme();
|
||||
|
||||
return (
|
||||
<Flex
|
||||
@ -155,7 +123,7 @@ export const StudentCardBack: React.FC<StudentCardBackProps> = ({ student, onAdd
|
||||
left="0"
|
||||
width="100%"
|
||||
height="100%"
|
||||
bg={colorMode === "light" ? "white" : "gray.700"}
|
||||
bg={theme.background}
|
||||
borderRadius="12px"
|
||||
align="center"
|
||||
justify="center"
|
||||
@ -168,113 +136,85 @@ export const StudentCardBack: React.FC<StudentCardBackProps> = ({ student, onAdd
|
||||
aspectRatio: "1"
|
||||
}}
|
||||
>
|
||||
<AddMissedButton onClick={() => onAddUser(student)} aria-label={t('journal.pl.common.add')}>
|
||||
{/* Add button */}
|
||||
{!isPresent && (
|
||||
<AddMissedButton
|
||||
onClick={() => onAddUser?.(student)}
|
||||
aria-label={t('journal.pl.common.add')}
|
||||
>
|
||||
<AddIcon boxSize={3} />
|
||||
</AddMissedButton>
|
||||
)}
|
||||
|
||||
{/* Фон с градиентом */}
|
||||
<Box
|
||||
position="absolute"
|
||||
top="0"
|
||||
left="0"
|
||||
right="0"
|
||||
bottom="0"
|
||||
opacity="0.15"
|
||||
className="animated-bg"
|
||||
sx={{
|
||||
background: `linear-gradient(135deg,
|
||||
${colors.secondary}, ${colors.primary}, ${colors.accent})`,
|
||||
backgroundSize: "400% 400%",
|
||||
animation: "gradientAnimation 8s ease infinite",
|
||||
"@keyframes gradientAnimation": {
|
||||
"0%": { backgroundPosition: "0% 50%" },
|
||||
"50%": { backgroundPosition: "100% 50%" },
|
||||
"100%": { backgroundPosition: "0% 50%" }
|
||||
}
|
||||
}}
|
||||
background={theme.gradient}
|
||||
opacity="0.7"
|
||||
/>
|
||||
<Box
|
||||
position="relative"
|
||||
textAlign="center"
|
||||
|
||||
{/* Content */}
|
||||
<Flex
|
||||
direction="column"
|
||||
align="center"
|
||||
justify="center"
|
||||
width="100%"
|
||||
zIndex="1"
|
||||
>
|
||||
{/* Аватар */}
|
||||
<Box
|
||||
width="70px"
|
||||
height="70px"
|
||||
mx="auto"
|
||||
mb={3}
|
||||
position="relative"
|
||||
width="80px"
|
||||
height="80px"
|
||||
borderRadius="full"
|
||||
overflow="hidden"
|
||||
mb={4}
|
||||
boxShadow={`0 0 15px ${theme.shadowColor}`}
|
||||
>
|
||||
{isRecentlyJoined && (
|
||||
<Box
|
||||
position="absolute"
|
||||
top="-5px"
|
||||
left="-5px"
|
||||
right="-5px"
|
||||
bottom="-5px"
|
||||
borderRadius="full"
|
||||
zIndex="0"
|
||||
sx={{
|
||||
animation: "pulse 2s infinite",
|
||||
background: `radial-gradient(circle, ${colors.accent} 0%, transparent 70%)`,
|
||||
"@keyframes pulse": {
|
||||
"0%": { opacity: 0.7, transform: "scale(0.95)" },
|
||||
"70%": { opacity: 0, transform: "scale(1.2)" },
|
||||
"100%": { opacity: 0, transform: "scale(1.5)" }
|
||||
}
|
||||
}}
|
||||
<Box
|
||||
position="absolute"
|
||||
top="0"
|
||||
left="0"
|
||||
right="0"
|
||||
bottom="0"
|
||||
zIndex="1"
|
||||
overflow="hidden"
|
||||
>
|
||||
<UserCard
|
||||
wrapperAS="div"
|
||||
student={student}
|
||||
present={false}
|
||||
width="100%"
|
||||
/>
|
||||
)}
|
||||
{/* Battery icon with 6 segments */}
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
{/* Battery outline */}
|
||||
<rect
|
||||
x="2"
|
||||
y="6"
|
||||
width="18"
|
||||
height="12"
|
||||
rx="2"
|
||||
stroke={colors.primary}
|
||||
strokeWidth="1.5"
|
||||
fill="transparent"
|
||||
/>
|
||||
{/* Battery cap */}
|
||||
<path
|
||||
d="M20 10H22V14H20V10Z"
|
||||
fill={colors.primary}
|
||||
/>
|
||||
|
||||
{/* Battery segments - from lowest to highest */}
|
||||
<rect x="4" y="8" width="2.3" height="8" rx="1" fill={getSegmentFill(0)} />
|
||||
<rect x="7" y="8" width="2.3" height="8" rx="1" fill={getSegmentFill(1)} />
|
||||
<rect x="10" y="8" width="2.3" height="8" rx="1" fill={getSegmentFill(2)} />
|
||||
<rect x="13" y="8" width="2.3" height="8" rx="1" fill={getSegmentFill(3)} />
|
||||
<rect x="16" y="8" width="2.3" height="8" rx="1" fill={getSegmentFill(4)} />
|
||||
|
||||
{/* Lightning icon if recently joined or fully charged */}
|
||||
{(isRecentlyJoined || batteryLevel === 5) && (
|
||||
<path
|
||||
d="M11.5 7.5L8 12H11L9.5 16.5L13 12H10L11.5 7.5Z"
|
||||
fill={colors.accent}
|
||||
stroke={colors.primary}
|
||||
strokeWidth="0.3"
|
||||
opacity="0.9"
|
||||
/>
|
||||
)}
|
||||
</svg>
|
||||
</Box>
|
||||
</Box>
|
||||
<Box fontSize="sm" fontWeight="medium" color={colorMode === "light" ? "gray.800" : "white"}>
|
||||
|
||||
{/* Student name */}
|
||||
<Box
|
||||
fontSize="sm"
|
||||
fontWeight="medium"
|
||||
textAlign="center"
|
||||
color={theme.text}
|
||||
mb={3}
|
||||
>
|
||||
{student.name || student.preferred_username}
|
||||
</Box>
|
||||
<Box
|
||||
fontSize="xs"
|
||||
mt={1}
|
||||
color={colors.primary}
|
||||
fontWeight="medium"
|
||||
|
||||
{/* Основная информация */}
|
||||
<Text
|
||||
fontSize="sm"
|
||||
textAlign="center"
|
||||
color={theme.infoText}
|
||||
>
|
||||
{isRecentlyJoined
|
||||
? t('journal.pl.lesson.recentlyJoined')
|
||||
: t(colors.text || 'journal.pl.lesson.attendance')}
|
||||
</Box>
|
||||
</Box>
|
||||
{isPresent
|
||||
? t('journal.pl.lesson.presentToday')
|
||||
: t('journal.pl.lesson.notMarked')}
|
||||
</Text>
|
||||
</Flex>
|
||||
</Flex>
|
||||
)
|
||||
}
|
@ -5,9 +5,10 @@ import {
|
||||
Center,
|
||||
useColorMode
|
||||
} from '@chakra-ui/react'
|
||||
import { useThemeManager } from '../../hooks/useThemeManager';
|
||||
|
||||
export const PageLoader = () => {
|
||||
const { colorMode } = useColorMode();
|
||||
const { isLightVariant } = useThemeManager();
|
||||
|
||||
return (
|
||||
<Container maxW="container.xl">
|
||||
@ -15,8 +16,8 @@ export const PageLoader = () => {
|
||||
<Spinner
|
||||
thickness="4px"
|
||||
speed="0.65s"
|
||||
emptyColor={colorMode === 'light' ? 'gray.200' : 'gray.600'}
|
||||
color={colorMode === 'light' ? 'blue.500' : 'blue.300'}
|
||||
emptyColor={isLightVariant ? 'gray.200' : 'gray.600'}
|
||||
color={isLightVariant ? 'blue.500' : 'blue.300'}
|
||||
size="xl"
|
||||
/>
|
||||
</Center>
|
||||
|
1
src/components/theme-selector/index.ts
Normal file
1
src/components/theme-selector/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export { ThemeSelector } from './theme-selector';
|
133
src/components/theme-selector/theme-selector.tsx
Normal file
133
src/components/theme-selector/theme-selector.tsx
Normal file
@ -0,0 +1,133 @@
|
||||
import React, { useState, useCallback, MouseEvent } from 'react';
|
||||
import {
|
||||
Menu,
|
||||
MenuButton,
|
||||
MenuList,
|
||||
MenuItem,
|
||||
IconButton,
|
||||
Box,
|
||||
Tooltip,
|
||||
} from '@chakra-ui/react';
|
||||
import {
|
||||
MoonIcon,
|
||||
SunIcon,
|
||||
} from '@chakra-ui/icons';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useAppSelector } from '../../__data__/store';
|
||||
import { selectCurrentTheme, setTheme, cycleNextTheme } from '../../__data__/slices/theme';
|
||||
import { ThemeType } from '../../types/theme';
|
||||
import {
|
||||
LIGHT_THEME,
|
||||
DARK_THEME,
|
||||
PINK_THEME,
|
||||
BLUE_THEME,
|
||||
GREEN_THEME,
|
||||
PURPLE_THEME,
|
||||
THEMES
|
||||
} from '../../utils/themes';
|
||||
|
||||
// Иконки для различных тем
|
||||
const ThemeIcon: React.FC<{ theme: ThemeType }> = ({ theme }) => {
|
||||
switch (theme) {
|
||||
case LIGHT_THEME:
|
||||
return <SunIcon />;
|
||||
case DARK_THEME:
|
||||
return <MoonIcon />;
|
||||
case PINK_THEME:
|
||||
return <Box>💕</Box>;
|
||||
case BLUE_THEME:
|
||||
return <Box>🌊</Box>;
|
||||
case GREEN_THEME:
|
||||
return <Box>🌿</Box>;
|
||||
case PURPLE_THEME:
|
||||
return <Box>✨</Box>;
|
||||
default:
|
||||
return <SunIcon />;
|
||||
}
|
||||
};
|
||||
|
||||
// Цвета фона пунктов меню для предпросмотра
|
||||
const getMenuItemStyles = (theme: ThemeType) => {
|
||||
switch (theme) {
|
||||
case LIGHT_THEME:
|
||||
return { bg: 'white', color: 'black' };
|
||||
case DARK_THEME:
|
||||
return { bg: 'gray.800', color: 'white' };
|
||||
case PINK_THEME:
|
||||
return { bg: 'pink.100', color: 'pink.800' };
|
||||
case BLUE_THEME:
|
||||
return { bg: 'blue.100', color: 'blue.800' };
|
||||
case GREEN_THEME:
|
||||
return { bg: 'green.100', color: 'green.800' };
|
||||
case PURPLE_THEME:
|
||||
return { bg: 'purple.100', color: 'purple.800' };
|
||||
default:
|
||||
return {};
|
||||
}
|
||||
};
|
||||
|
||||
interface ThemeSelectorProps {
|
||||
variant?: 'icon' | 'full';
|
||||
size?: string;
|
||||
}
|
||||
|
||||
export const ThemeSelector: React.FC<ThemeSelectorProps> = ({
|
||||
variant = 'icon',
|
||||
size = 'md'
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useDispatch();
|
||||
const currentTheme = useAppSelector(selectCurrentTheme);
|
||||
|
||||
// Состояние для контроля открытия меню
|
||||
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
||||
|
||||
// Обработчик клика по иконке
|
||||
const handleIconClick = useCallback((e: MouseEvent<HTMLButtonElement>) => {
|
||||
// Если нажат Ctrl, отображаем меню
|
||||
if (e.ctrlKey) {
|
||||
setIsMenuOpen(true);
|
||||
} else {
|
||||
// Иначе просто циклически меняем тему
|
||||
dispatch(cycleNextTheme());
|
||||
}
|
||||
}, [dispatch]);
|
||||
|
||||
// Обработчик выбора темы из меню
|
||||
const handleThemeChange = (theme: ThemeType) => {
|
||||
dispatch(setTheme(theme));
|
||||
setIsMenuOpen(false);
|
||||
};
|
||||
|
||||
// Рендерим одиночную иконку с меню при Ctrl+клик
|
||||
return (
|
||||
<Menu isOpen={isMenuOpen} onClose={() => setIsMenuOpen(false)}>
|
||||
<Tooltip
|
||||
label={t('journal.pl.theme.select')}
|
||||
aria-label={t('journal.pl.theme.select')}
|
||||
>
|
||||
<MenuButton
|
||||
as={IconButton}
|
||||
aria-label={t('journal.pl.theme.select')}
|
||||
icon={<ThemeIcon theme={currentTheme} />}
|
||||
variant="ghost"
|
||||
size={size}
|
||||
onClick={handleIconClick}
|
||||
/>
|
||||
</Tooltip>
|
||||
<MenuList zIndex={1000}>
|
||||
{THEMES.map((theme) => (
|
||||
<MenuItem
|
||||
key={theme}
|
||||
onClick={() => handleThemeChange(theme)}
|
||||
icon={<ThemeIcon theme={theme} />}
|
||||
{...getMenuItemStyles(theme)}
|
||||
>
|
||||
{t(`journal.pl.theme.${theme}`)}
|
||||
</MenuItem>
|
||||
))}
|
||||
</MenuList>
|
||||
</Menu>
|
||||
);
|
||||
};
|
@ -109,7 +109,7 @@ export const Wrapper = styled.div<{ warn?: boolean; width?: string | number; pos
|
||||
props.warn
|
||||
? css`
|
||||
opacity: 0.7;
|
||||
filter: grayscale(0.8);
|
||||
filter: grayscale(0.3);
|
||||
`
|
||||
: ''}
|
||||
`
|
||||
|
@ -5,6 +5,7 @@ import { Box, useColorMode, Text } from '@chakra-ui/react'
|
||||
import { CheckCircleIcon, AddIcon } from '@chakra-ui/icons'
|
||||
import { motion, AnimatePresence } from 'framer-motion'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useThemeManager } from '../../hooks/useThemeManager'
|
||||
|
||||
import { Reaction, User } from '../../__data__/model'
|
||||
|
||||
@ -47,6 +48,7 @@ export const UserCard = ({
|
||||
const [imageError, setImageError] = useState(false);
|
||||
const [showReaction, setShowReaction] = useState(false);
|
||||
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
const { isLightVariant } = useThemeManager();
|
||||
|
||||
const randomGravatarPath = useMemo(() => Math.random() * 1000, [])
|
||||
|
||||
@ -74,6 +76,53 @@ export const UserCard = ({
|
||||
};
|
||||
}, [reaction]);
|
||||
|
||||
// Функция для определения цвета статуса онлайн
|
||||
const getStatusColor = (status: string) => {
|
||||
switch (status) {
|
||||
case 'online':
|
||||
return 'green.500';
|
||||
case 'away':
|
||||
return 'yellow.500';
|
||||
default:
|
||||
return 'gray.400';
|
||||
}
|
||||
};
|
||||
|
||||
// Функция для рендера индикатора посещаемости
|
||||
const renderAttendanceIndicator = (attendance: number) => {
|
||||
let color;
|
||||
let text;
|
||||
|
||||
if (attendance >= 90) {
|
||||
color = 'green.500';
|
||||
text = '✓✓✓';
|
||||
} else if (attendance >= 70) {
|
||||
color = 'green.400';
|
||||
text = '✓✓';
|
||||
} else if (attendance >= 50) {
|
||||
color = 'yellow.500';
|
||||
text = '✓';
|
||||
} else if (attendance >= 30) {
|
||||
color = 'orange.500';
|
||||
text = '⚠';
|
||||
} else {
|
||||
color = 'red.500';
|
||||
text = '✗';
|
||||
}
|
||||
|
||||
return (
|
||||
<Text
|
||||
fontSize="xs"
|
||||
color={color}
|
||||
position="absolute"
|
||||
bottom="8px"
|
||||
right="8px"
|
||||
>
|
||||
{text}
|
||||
</Text>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Wrapper
|
||||
warn={!present}
|
||||
@ -137,6 +186,19 @@ export const UserCard = ({
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
{/* Дополнительные декоративные элементы в зависимости от темы */}
|
||||
<Box
|
||||
position="absolute"
|
||||
top={0}
|
||||
left={0}
|
||||
right={0}
|
||||
height="5px"
|
||||
background={isLightVariant ? 'rgba(255, 255, 255, 0.8)' : 'rgba(0, 0, 0, 0.6)'}
|
||||
borderTopLeftRadius="md"
|
||||
borderTopRightRadius="md"
|
||||
zIndex={1}
|
||||
/>
|
||||
</Wrapper>
|
||||
)
|
||||
}
|
||||
|
@ -5,13 +5,14 @@ import {
|
||||
Spinner,
|
||||
useColorMode
|
||||
} from '@chakra-ui/react'
|
||||
import { useThemeManager } from '../../hooks/useThemeManager'
|
||||
|
||||
interface XlSpinnerProps {
|
||||
size?: string;
|
||||
}
|
||||
|
||||
export const XlSpinner: React.FC<XlSpinnerProps> = ({ size = 'xl' }) => {
|
||||
const { colorMode } = useColorMode();
|
||||
const { isLightVariant } = useThemeManager();
|
||||
|
||||
return (
|
||||
<Container maxW="container.xl">
|
||||
@ -19,8 +20,8 @@ export const XlSpinner: React.FC<XlSpinnerProps> = ({ size = 'xl' }) => {
|
||||
<Spinner
|
||||
thickness="4px"
|
||||
speed="0.65s"
|
||||
emptyColor={colorMode === 'light' ? 'gray.200' : 'gray.600'}
|
||||
color={colorMode === 'light' ? 'blue.500' : 'blue.300'}
|
||||
emptyColor={isLightVariant ? 'gray.200' : 'gray.600'}
|
||||
color={isLightVariant ? 'blue.500' : 'blue.300'}
|
||||
size={size}
|
||||
/>
|
||||
</Center>
|
||||
|
@ -1,9 +1,9 @@
|
||||
import React, { useEffect, Suspense, useRef, useState } from 'react'
|
||||
import { Routes, Route, useNavigate } from 'react-router-dom'
|
||||
import { Provider } from 'react-redux'
|
||||
import { getNavigationValue } from '@brojs/cli'
|
||||
import { Box, Container, Spinner, VStack, useColorMode } from '@chakra-ui/react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useThemeManager } from './hooks/useThemeManager'
|
||||
|
||||
import {
|
||||
CourseListPage,
|
||||
@ -68,15 +68,12 @@ const HeaderWithBreadcrumbs = ({ serviceMenuContainerRef }: { serviceMenuContain
|
||||
return <AppHeader serviceMenuContainerRef={serviceMenuContainerRef} breadcrumbs={breadcrumbs} />;
|
||||
};
|
||||
|
||||
interface DashboardProps {
|
||||
store: any; // Используем any, поскольку точный тип store не указан
|
||||
}
|
||||
|
||||
export const Dashboard = ({ store }: DashboardProps) => {
|
||||
export const Dashboard = () => {
|
||||
const serviceMenuContainerRef = useRef<HTMLDivElement>(null);
|
||||
const serviceMenuInstanceRef = useRef<any>(null);
|
||||
const [serviceMenu, setServiceMenu] = useState(false);
|
||||
const { colorMode } = useColorMode();
|
||||
const { isLightVariant, isDarkVariant } = useThemeManager();
|
||||
const { t } = useTranslation();
|
||||
|
||||
useEffect(() => {
|
||||
@ -94,10 +91,10 @@ export const Dashboard = ({ store }: DashboardProps) => {
|
||||
apiUrl: 'https://admin.bro-js.ru',
|
||||
targetElement: serviceMenuContainerRef.current,
|
||||
styles: {
|
||||
dotColor: colorMode === 'light' ? '#333' : '#ccc',
|
||||
hoverColor: colorMode === 'light' ? '#eee' : '#444',
|
||||
backgroundColor: colorMode === 'light' ? '#fff' : '#2D3748',
|
||||
textColor: colorMode === 'light' ? '#333' : '#fff',
|
||||
dotColor: isLightVariant ? '#333' : '#ccc',
|
||||
hoverColor: isLightVariant ? '#eee' : '#444',
|
||||
backgroundColor: isLightVariant ? '#fff' : '#2D3748',
|
||||
textColor: isLightVariant ? '#333' : '#fff',
|
||||
},
|
||||
translations: {
|
||||
menuTitle: t('journal.pl.serviceMenu.title'),
|
||||
@ -113,10 +110,9 @@ export const Dashboard = ({ store }: DashboardProps) => {
|
||||
serviceMenuInstanceRef.current = null;
|
||||
}
|
||||
};
|
||||
}, [keycloak.token, serviceMenu, colorMode, t]);
|
||||
}, [keycloak.token, serviceMenu, isLightVariant, isDarkVariant, t]);
|
||||
|
||||
return (
|
||||
<Provider store={store}>
|
||||
<BreadcrumbsProvider>
|
||||
<HeaderWithBreadcrumbs serviceMenuContainerRef={serviceMenuContainerRef} />
|
||||
<Routes>
|
||||
@ -162,6 +158,5 @@ export const Dashboard = ({ store }: DashboardProps) => {
|
||||
/>
|
||||
</Routes>
|
||||
</BreadcrumbsProvider>
|
||||
</Provider>
|
||||
)
|
||||
}
|
||||
|
@ -7,6 +7,8 @@ html {
|
||||
min-width: 320px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Светлая тема (light) - по умолчанию */
|
||||
body {
|
||||
color: #000;
|
||||
/* background: radial-gradient(circle at top right, rgb(154 227 33), rgb(33 160 56)); */
|
||||
@ -31,7 +33,7 @@ body {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Стили для темной темы */
|
||||
/* Темная тема (dark) */
|
||||
html[data-theme="dark"] body {
|
||||
color: #fff;
|
||||
background: radial-gradient(
|
||||
@ -51,6 +53,86 @@ html[data-theme="dark"] body {
|
||||
);
|
||||
}
|
||||
|
||||
/* Розовая тема (pink) */
|
||||
html[data-theme="pink"] body {
|
||||
color: #702459;
|
||||
background: radial-gradient(
|
||||
farthest-side at bottom left,
|
||||
rgba(251, 182, 206, 0.8),
|
||||
rgba(255, 255, 255, 0) 65%
|
||||
),
|
||||
radial-gradient(
|
||||
farthest-corner at bottom center,
|
||||
rgba(254, 215, 226, 0.7),
|
||||
rgba(255, 255, 255, 0) 40%
|
||||
),
|
||||
radial-gradient(
|
||||
farthest-side at bottom right,
|
||||
rgba(246, 135, 179, 0.6),
|
||||
rgb(255, 245, 247) 65%
|
||||
);
|
||||
}
|
||||
|
||||
/* Синяя тема (blue) */
|
||||
html[data-theme="blue"] body {
|
||||
color: #2c5282;
|
||||
background: radial-gradient(
|
||||
farthest-side at bottom left,
|
||||
rgba(144, 205, 244, 0.8),
|
||||
rgba(255, 255, 255, 0) 65%
|
||||
),
|
||||
radial-gradient(
|
||||
farthest-corner at bottom center,
|
||||
rgba(190, 227, 248, 0.7),
|
||||
rgba(255, 255, 255, 0) 40%
|
||||
),
|
||||
radial-gradient(
|
||||
farthest-side at bottom right,
|
||||
rgba(66, 153, 225, 0.6),
|
||||
rgb(235, 248, 255) 65%
|
||||
);
|
||||
}
|
||||
|
||||
/* Зеленая тема (green) */
|
||||
html[data-theme="green"] body {
|
||||
color: #276749;
|
||||
background: radial-gradient(
|
||||
farthest-side at bottom left,
|
||||
rgba(154, 230, 180, 0.8),
|
||||
rgba(255, 255, 255, 0) 65%
|
||||
),
|
||||
radial-gradient(
|
||||
farthest-corner at bottom center,
|
||||
rgba(198, 246, 213, 0.7),
|
||||
rgba(255, 255, 255, 0) 40%
|
||||
),
|
||||
radial-gradient(
|
||||
farthest-side at bottom right,
|
||||
rgba(72, 187, 120, 0.6),
|
||||
rgb(240, 255, 244) 65%
|
||||
);
|
||||
}
|
||||
|
||||
/* Фиолетовая тема (purple) */
|
||||
html[data-theme="purple"] body {
|
||||
color: #fff;
|
||||
background: radial-gradient(
|
||||
farthest-side at bottom left,
|
||||
rgba(128, 90, 213, 0.8),
|
||||
rgba(0, 0, 0, 0) 65%
|
||||
),
|
||||
radial-gradient(
|
||||
farthest-corner at bottom center,
|
||||
rgba(159, 122, 234, 0.7),
|
||||
rgba(0, 0, 0, 0) 40%
|
||||
),
|
||||
radial-gradient(
|
||||
farthest-side at bottom right,
|
||||
rgba(107, 70, 193, 0.6),
|
||||
rgb(44, 19, 56) 65%
|
||||
);
|
||||
}
|
||||
|
||||
#app {
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
|
132
src/hooks/useThemeManager.ts
Normal file
132
src/hooks/useThemeManager.ts
Normal file
@ -0,0 +1,132 @@
|
||||
import { useColorMode } from '@chakra-ui/react';
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { ThemeType } from '../types/theme';
|
||||
import {
|
||||
LIGHT_THEME,
|
||||
DARK_THEME,
|
||||
PINK_THEME,
|
||||
BLUE_THEME,
|
||||
GREEN_THEME,
|
||||
PURPLE_THEME,
|
||||
THEMES
|
||||
} from '../utils/themes';
|
||||
import { useAppSelector } from '../__data__/store';
|
||||
import {
|
||||
setTheme,
|
||||
cycleNextTheme as cycleTheme,
|
||||
selectCurrentTheme,
|
||||
selectIsLightVariant,
|
||||
selectIsDarkVariant
|
||||
} from '../__data__/slices/theme';
|
||||
|
||||
// Маппинг тем к базовым режимам Chakra UI
|
||||
const themeToColorMode: Record<ThemeType, 'light' | 'dark'> = {
|
||||
[LIGHT_THEME]: 'light',
|
||||
[DARK_THEME]: 'dark',
|
||||
[PINK_THEME]: 'light',
|
||||
[BLUE_THEME]: 'light',
|
||||
[GREEN_THEME]: 'light',
|
||||
[PURPLE_THEME]: 'dark'
|
||||
};
|
||||
|
||||
export const useThemeManager = () => {
|
||||
// Получаем базовый функционал переключения темы из Chakra UI
|
||||
const { colorMode, setColorMode } = useColorMode();
|
||||
|
||||
// Используем Redux для управления темой
|
||||
const dispatch = useDispatch();
|
||||
const currentTheme = useAppSelector(selectCurrentTheme);
|
||||
const isLightVariant = useAppSelector(selectIsLightVariant);
|
||||
const isDarkVariant = useAppSelector(selectIsDarkVariant);
|
||||
|
||||
// Ref для хранения observer
|
||||
const observerRef = useRef<MutationObserver | null>(null);
|
||||
|
||||
// Функция для применения классов и атрибутов темы
|
||||
const applyThemeToDOM = () => {
|
||||
if (typeof document === 'undefined') return;
|
||||
|
||||
// Удаляем все классы тем
|
||||
document.documentElement.classList.remove(...THEMES);
|
||||
|
||||
// Добавляем класс текущей темы
|
||||
document.documentElement.classList.add(currentTheme);
|
||||
|
||||
// Также устанавливаем data-theme атрибут для использования в CSS
|
||||
document.documentElement.setAttribute('data-theme', currentTheme);
|
||||
|
||||
// Устанавливаем специальный флаг, что тема установлена нами
|
||||
document.documentElement.setAttribute('data-custom-theme-source', 'redux');
|
||||
};
|
||||
|
||||
// Эффект для применения дополнительных классов к документу в зависимости от выбранной темы
|
||||
// и создания MutationObserver для отслеживания изменений
|
||||
useEffect(() => {
|
||||
if (typeof document === 'undefined' || typeof window === 'undefined') return;
|
||||
|
||||
// Применяем тему к DOM
|
||||
applyThemeToDOM();
|
||||
|
||||
// Синхронизируем с Chakra UI для светлой/темной темы
|
||||
const requiredColorMode = themeToColorMode[currentTheme];
|
||||
if (colorMode !== requiredColorMode) {
|
||||
setColorMode(requiredColorMode);
|
||||
}
|
||||
|
||||
// Создаем MutationObserver для отслеживания изменений атрибута data-theme
|
||||
if (!observerRef.current) {
|
||||
observerRef.current = new MutationObserver((mutations) => {
|
||||
mutations.forEach((mutation) => {
|
||||
if (
|
||||
mutation.type === 'attributes' &&
|
||||
mutation.attributeName === 'data-theme' &&
|
||||
document.documentElement.getAttribute('data-theme') !== currentTheme
|
||||
) {
|
||||
// Если атрибут был изменен не нами, восстанавливаем его
|
||||
applyThemeToDOM();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Начинаем наблюдение за изменениями атрибута data-theme
|
||||
observerRef.current.observe(document.documentElement, {
|
||||
attributes: true,
|
||||
attributeFilter: ['data-theme']
|
||||
});
|
||||
}
|
||||
|
||||
// Для дополнительной защиты устанавливаем интервал для проверки и восстановления атрибута
|
||||
const intervalId = setInterval(() => {
|
||||
if (document.documentElement.getAttribute('data-theme') !== currentTheme) {
|
||||
applyThemeToDOM();
|
||||
}
|
||||
}, 500);
|
||||
|
||||
// Отключаем observer и интервал при размонтировании компонента
|
||||
return () => {
|
||||
if (observerRef.current) {
|
||||
observerRef.current.disconnect();
|
||||
}
|
||||
clearInterval(intervalId);
|
||||
};
|
||||
}, [currentTheme, setColorMode, colorMode]);
|
||||
|
||||
// Функция для изменения темы
|
||||
const changeTheme = (theme: ThemeType) => {
|
||||
dispatch(setTheme(theme));
|
||||
};
|
||||
|
||||
// Функция для последовательного циклического переключения тем
|
||||
const cycleNextTheme = () => {
|
||||
dispatch(cycleTheme());
|
||||
};
|
||||
|
||||
return {
|
||||
currentTheme,
|
||||
changeTheme,
|
||||
cycleNextTheme,
|
||||
isLightVariant,
|
||||
isDarkVariant,
|
||||
};
|
||||
};
|
@ -1,4 +1,5 @@
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import React, { useEffect, useState, useRef, useCallback } from 'react'
|
||||
import { getFeatures } from '@brojs/cli'
|
||||
import { useForm, Controller } from 'react-hook-form'
|
||||
import {
|
||||
Box,
|
||||
@ -29,7 +30,11 @@ import {
|
||||
Wrap,
|
||||
WrapItem,
|
||||
IconButton,
|
||||
Center
|
||||
Center,
|
||||
InputGroup,
|
||||
InputRightElement,
|
||||
List,
|
||||
ListItem
|
||||
} from '@chakra-ui/react'
|
||||
import { AddIcon, CheckIcon, WarningIcon, RepeatIcon, ChevronLeftIcon, ChevronRightIcon } from '@chakra-ui/icons'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
@ -40,6 +45,10 @@ import { formatDate } from '../../../utils/dayjs-config'
|
||||
import { dateToCalendarFormat } from '../../../utils/time'
|
||||
import { Lesson } from '../../../__data__/model'
|
||||
import { ErrorSpan } from '../style'
|
||||
import { api } from '../../../__data__/api/api'
|
||||
import { useParams } from 'react-router-dom'
|
||||
|
||||
const courceNameSuggestion = getFeatures('journal')['courceNameSuggestion']
|
||||
|
||||
interface NewLessonForm {
|
||||
name: string
|
||||
@ -78,6 +87,7 @@ export const LessonForm = ({
|
||||
onRetryAiGeneration = () => {},
|
||||
existingLessons
|
||||
}: LessonFormProps) => {
|
||||
const { courseId } = useParams()
|
||||
const { t } = useTranslation()
|
||||
const isAiSuggested = lesson && !lesson._id && !lesson.id
|
||||
const aiHighlightColor = useColorModeValue('blue.100', 'blue.800')
|
||||
@ -85,6 +95,72 @@ export const LessonForm = ({
|
||||
const suggestionHoverBgColor = useColorModeValue('blue.100', 'blue.800')
|
||||
const borderColor = useColorModeValue('blue.200', 'blue.700')
|
||||
const textSecondaryColor = useColorModeValue('gray.600', 'gray.400')
|
||||
const [suggestions, setSuggestions] = useState<string[]>([])
|
||||
const [showSuggestions, setShowSuggestions] = useState(false)
|
||||
const [inputValue, setInputValue] = useState('')
|
||||
const inputRef = useRef<HTMLInputElement>(null)
|
||||
const suggestionsContainerRef = useRef<HTMLDivElement>(null)
|
||||
const debounceTimeoutRef = useRef<NodeJS.Timeout | null>(null)
|
||||
|
||||
const [generateLessonName, {
|
||||
data: generateLessonNameData,
|
||||
isLoading: isLoadingGenerateLessonName,
|
||||
error: errorGenerateLessonName,
|
||||
isSuccess: isSuccessGenerateLessonName
|
||||
}] = api.useGenerateLessonNameMutation()
|
||||
|
||||
// Функция debounce для запросов
|
||||
const debouncedGenerateName = useCallback((value: string) => {
|
||||
if (debounceTimeoutRef.current) {
|
||||
clearTimeout(debounceTimeoutRef.current)
|
||||
}
|
||||
|
||||
debounceTimeoutRef.current = setTimeout(() => {
|
||||
if (value.length > 2) {
|
||||
generateLessonName({ courseId: courseId, name: value })
|
||||
} else {
|
||||
setSuggestions([])
|
||||
}
|
||||
}, Number(courceNameSuggestion.value) || 300)
|
||||
}, [courseId, generateLessonName])
|
||||
|
||||
useEffect(() => {
|
||||
if (isSuccessGenerateLessonName) {
|
||||
setSuggestions(generateLessonNameData.body.map(suggestion => suggestion.name))
|
||||
}
|
||||
}, [isSuccessGenerateLessonName])
|
||||
|
||||
// Эффект для корректного позиционирования списка подсказок
|
||||
useEffect(() => {
|
||||
const positionSuggestions = () => {
|
||||
if (inputRef.current && suggestionsContainerRef.current && showSuggestions) {
|
||||
const inputRect = inputRef.current.getBoundingClientRect()
|
||||
suggestionsContainerRef.current.style.top = `${inputRect.bottom + window.scrollY}px`
|
||||
suggestionsContainerRef.current.style.left = `${inputRect.left + window.scrollX}px`
|
||||
suggestionsContainerRef.current.style.width = `${inputRect.width}px`
|
||||
}
|
||||
}
|
||||
|
||||
positionSuggestions()
|
||||
|
||||
// Обновляем позицию при скролле или изменении размера окна
|
||||
window.addEventListener('scroll', positionSuggestions)
|
||||
window.addEventListener('resize', positionSuggestions)
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('scroll', positionSuggestions)
|
||||
window.removeEventListener('resize', positionSuggestions)
|
||||
}
|
||||
}, [showSuggestions, suggestions.length])
|
||||
|
||||
// Эффект для очистки timeout при размонтировании компонента
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (debounceTimeoutRef.current) {
|
||||
clearTimeout(debounceTimeoutRef.current)
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
const getNearestTimeSlot = () => {
|
||||
const now = new Date();
|
||||
@ -149,27 +225,6 @@ export const LessonForm = ({
|
||||
return slots;
|
||||
};
|
||||
|
||||
const getNextTimeSlots = (date: string, count: number = 3) => {
|
||||
const currentDate = new Date();
|
||||
const selectedDate = new Date(date);
|
||||
const isToday = selectedDate.toDateString() === currentDate.toDateString();
|
||||
|
||||
if (!isToday) return [];
|
||||
|
||||
const currentMinutes = currentDate.getHours() * 60 + currentDate.getMinutes();
|
||||
const slots = generateTimeSlots();
|
||||
|
||||
return slots
|
||||
.map(slot => {
|
||||
const [hours, minutes] = slot.split(':').map(Number);
|
||||
const slotMinutes = hours * 60 + minutes;
|
||||
return { slot, minutes: slotMinutes };
|
||||
})
|
||||
.filter(({ minutes }) => minutes > currentMinutes)
|
||||
.slice(0, count)
|
||||
.map(({ slot }) => slot);
|
||||
};
|
||||
|
||||
const timeGroups = {
|
||||
[`${t('journal.pl.days.morning')} (8-12)`]: generateTimeSlots().filter(slot => {
|
||||
const hour = parseInt(slot.split(':')[0]);
|
||||
@ -490,12 +545,30 @@ export const LessonForm = ({
|
||||
render={({ field }) => (
|
||||
<FormControl isRequired isInvalid={Boolean(errors.name)}>
|
||||
<FormLabel>{t('journal.pl.lesson.form.title')}</FormLabel>
|
||||
<Input
|
||||
{...field}
|
||||
required={false}
|
||||
placeholder={t('journal.pl.lesson.form.namePlaceholder')}
|
||||
size="md"
|
||||
/>
|
||||
<Box position="relative">
|
||||
<InputGroup>
|
||||
<Input
|
||||
{...field}
|
||||
ref={inputRef}
|
||||
required={false}
|
||||
placeholder={t('journal.pl.lesson.form.namePlaceholder')}
|
||||
size="md"
|
||||
onChange={(e) => {
|
||||
const value = e.target.value
|
||||
setInputValue(value)
|
||||
field.onChange(value)
|
||||
if (value.length > 2 && courceNameSuggestion) {
|
||||
setShowSuggestions(true)
|
||||
debouncedGenerateName(value)
|
||||
} else {
|
||||
setSuggestions([])
|
||||
}
|
||||
}}
|
||||
onFocus={() => setShowSuggestions(true)}
|
||||
onBlur={() => setTimeout(() => setShowSuggestions(false), 200)}
|
||||
/>
|
||||
</InputGroup>
|
||||
</Box>
|
||||
{errors.name && (
|
||||
<FormErrorMessage>{errors.name.message}</FormErrorMessage>
|
||||
)}
|
||||
@ -610,6 +683,39 @@ export const LessonForm = ({
|
||||
</>
|
||||
)}
|
||||
</CardBody>
|
||||
|
||||
{/* Выпадающий список подсказок (размещаем вне стандартного потока документа) */}
|
||||
{suggestions.length > 0 && showSuggestions && (
|
||||
<Box
|
||||
position="fixed"
|
||||
ref={suggestionsContainerRef}
|
||||
bg={useColorModeValue('white', 'gray.800')}
|
||||
borderRadius="md"
|
||||
boxShadow="md"
|
||||
zIndex={9999}
|
||||
maxH="200px"
|
||||
overflowY="auto"
|
||||
>
|
||||
<List>
|
||||
{suggestions.map((suggestion, index) => (
|
||||
<ListItem
|
||||
key={index}
|
||||
p={2}
|
||||
cursor="pointer"
|
||||
_hover={{ bg: useColorModeValue('gray.100', 'gray.700') }}
|
||||
onClick={() => {
|
||||
setValue('name', suggestion)
|
||||
setInputValue(suggestion)
|
||||
setSuggestions([])
|
||||
setShowSuggestions(false)
|
||||
}}
|
||||
>
|
||||
{suggestion}
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
</Box>
|
||||
)}
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
1
src/types/theme.ts
Normal file
1
src/types/theme.ts
Normal file
@ -0,0 +1 @@
|
||||
export type ThemeType = 'light' | 'dark' | 'pink' | 'blue' | 'green' | 'purple';
|
175
src/user-card/user-card.tsx
Normal file
175
src/user-card/user-card.tsx
Normal file
@ -0,0 +1,175 @@
|
||||
import React from 'react';
|
||||
import { Box, useColorMode, Text } from '@chakra-ui/react';
|
||||
import { useThemeManager } from '../../hooks/useThemeManager';
|
||||
|
||||
interface UserCardProps {
|
||||
user: {
|
||||
id: string;
|
||||
name: string;
|
||||
photo?: string;
|
||||
role?: string;
|
||||
status?: 'online' | 'offline' | 'away';
|
||||
attendance?: number;
|
||||
};
|
||||
onClick?: () => void;
|
||||
showStatus?: boolean;
|
||||
showAttendance?: boolean;
|
||||
}
|
||||
|
||||
export const UserCard: React.FC<UserCardProps> = ({
|
||||
user,
|
||||
onClick,
|
||||
showStatus = false,
|
||||
showAttendance = false
|
||||
}) => {
|
||||
const { isLightVariant } = useThemeManager();
|
||||
|
||||
// Функция для определения цвета статуса онлайн
|
||||
const getStatusColor = (status: string) => {
|
||||
switch (status) {
|
||||
case 'online':
|
||||
return 'green.500';
|
||||
case 'away':
|
||||
return 'yellow.500';
|
||||
default:
|
||||
return 'gray.400';
|
||||
}
|
||||
};
|
||||
|
||||
// Функция для рендера индикатора посещаемости
|
||||
const renderAttendanceIndicator = (attendance: number) => {
|
||||
let color;
|
||||
let text;
|
||||
|
||||
if (attendance >= 90) {
|
||||
color = 'green.500';
|
||||
text = '✓✓✓';
|
||||
} else if (attendance >= 70) {
|
||||
color = 'green.400';
|
||||
text = '✓✓';
|
||||
} else if (attendance >= 50) {
|
||||
color = 'yellow.500';
|
||||
text = '✓';
|
||||
} else if (attendance >= 30) {
|
||||
color = 'orange.500';
|
||||
text = '⚠';
|
||||
} else {
|
||||
color = 'red.500';
|
||||
text = '✗';
|
||||
}
|
||||
|
||||
return (
|
||||
<Text
|
||||
fontSize="xs"
|
||||
color={color}
|
||||
position="absolute"
|
||||
bottom="8px"
|
||||
right="8px"
|
||||
>
|
||||
{text}
|
||||
</Text>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box
|
||||
position="relative"
|
||||
borderRadius="md"
|
||||
overflow="hidden"
|
||||
boxShadow="sm"
|
||||
transition="transform 0.2s, box-shadow 0.2s"
|
||||
_hover={onClick ? {
|
||||
transform: 'translateY(-2px)',
|
||||
boxShadow: 'md',
|
||||
cursor: 'pointer'
|
||||
} : {}}
|
||||
onClick={onClick}
|
||||
bg={isLightVariant ? 'white' : 'gray.700'}
|
||||
p={3}
|
||||
>
|
||||
<Box
|
||||
position="relative"
|
||||
borderRadius="full"
|
||||
width="60px"
|
||||
height="60px"
|
||||
overflow="hidden"
|
||||
margin="0 auto 8px"
|
||||
>
|
||||
{user.photo ? (
|
||||
<Box
|
||||
as="img"
|
||||
src={user.photo}
|
||||
alt={user.name}
|
||||
width="100%"
|
||||
height="100%"
|
||||
objectFit="cover"
|
||||
/>
|
||||
) : (
|
||||
<Box
|
||||
width="100%"
|
||||
height="100%"
|
||||
bg="gray.300"
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
fontSize="xl"
|
||||
fontWeight="bold"
|
||||
color="gray.600"
|
||||
>
|
||||
{user.name.charAt(0).toUpperCase()}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{showStatus && user.status && (
|
||||
<Box
|
||||
position="absolute"
|
||||
bottom="2px"
|
||||
right="2px"
|
||||
width="12px"
|
||||
height="12px"
|
||||
borderRadius="full"
|
||||
bg={getStatusColor(user.status)}
|
||||
border="2px solid"
|
||||
borderColor={isLightVariant ? 'white' : 'gray.700'}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Text
|
||||
textAlign="center"
|
||||
fontWeight="medium"
|
||||
fontSize="sm"
|
||||
noOfLines={1}
|
||||
title={user.name}
|
||||
>
|
||||
{user.name}
|
||||
</Text>
|
||||
|
||||
{user.role && (
|
||||
<Text
|
||||
textAlign="center"
|
||||
fontSize="xs"
|
||||
color={isLightVariant ? 'gray.500' : 'gray.300'}
|
||||
noOfLines={1}
|
||||
>
|
||||
{user.role}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
{showAttendance && typeof user.attendance === 'number' && renderAttendanceIndicator(user.attendance)}
|
||||
|
||||
{/* Дополнительные декоративные элементы в зависимости от темы */}
|
||||
<Box
|
||||
position="absolute"
|
||||
top={0}
|
||||
left={0}
|
||||
right={0}
|
||||
height="5px"
|
||||
background={isLightVariant ? 'rgba(255, 255, 255, 0.8)' : 'rgba(0, 0, 0, 0.6)'}
|
||||
borderTopLeftRadius="md"
|
||||
borderTopRightRadius="md"
|
||||
zIndex={1}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
105
src/utils/theme.ts
Normal file
105
src/utils/theme.ts
Normal file
@ -0,0 +1,105 @@
|
||||
import { extendTheme, ThemeConfig } from '@chakra-ui/react';
|
||||
import {
|
||||
LIGHT_THEME,
|
||||
DARK_THEME,
|
||||
PINK_THEME,
|
||||
BLUE_THEME,
|
||||
GREEN_THEME,
|
||||
PURPLE_THEME
|
||||
} from './themes';
|
||||
|
||||
// Базовая конфигурация темы Chakra UI
|
||||
const config: ThemeConfig = {
|
||||
initialColorMode: LIGHT_THEME,
|
||||
useSystemColorMode: false,
|
||||
};
|
||||
|
||||
// Определяем специфичные цвета для различных тем
|
||||
const colors = {
|
||||
// Цвета для розовой темы
|
||||
pinkTheme: {
|
||||
50: '#fff5f7',
|
||||
100: '#fed7e2',
|
||||
200: '#fbb6ce',
|
||||
300: '#f687b3',
|
||||
400: '#ed64a6',
|
||||
500: '#d53f8c',
|
||||
600: '#b83280',
|
||||
700: '#97266d',
|
||||
800: '#702459',
|
||||
900: '#521B41',
|
||||
},
|
||||
// Цвета для синей темы
|
||||
blueTheme: {
|
||||
50: '#ebf8ff',
|
||||
100: '#bee3f8',
|
||||
200: '#90cdf4',
|
||||
300: '#63b3ed',
|
||||
400: '#4299e1',
|
||||
500: '#3182ce',
|
||||
600: '#2b6cb0',
|
||||
700: '#2c5282',
|
||||
800: '#2a4365',
|
||||
900: '#1A365D',
|
||||
},
|
||||
// Цвета для зеленой темы
|
||||
greenTheme: {
|
||||
50: '#f0fff4',
|
||||
100: '#c6f6d5',
|
||||
200: '#9ae6b4',
|
||||
300: '#68d391',
|
||||
400: '#48bb78',
|
||||
500: '#38a169',
|
||||
600: '#2f855a',
|
||||
700: '#276749',
|
||||
800: '#22543d',
|
||||
900: '#1C4532',
|
||||
},
|
||||
// Цвета для фиолетовой темы
|
||||
purpleTheme: {
|
||||
50: '#faf5ff',
|
||||
100: '#e9d8fd',
|
||||
200: '#d6bcfa',
|
||||
300: '#b794f4',
|
||||
400: '#9f7aea',
|
||||
500: '#805ad5',
|
||||
600: '#6b46c1',
|
||||
700: '#553c9a',
|
||||
800: '#44337a',
|
||||
900: '#322659',
|
||||
},
|
||||
};
|
||||
|
||||
// Создаем и экспортируем расширенную тему
|
||||
export const chakraTheme = extendTheme({
|
||||
config,
|
||||
colors,
|
||||
styles: {
|
||||
global: (props: { colorMode: string }) => ({
|
||||
// Базовые стили для темного и светлого режимов
|
||||
body: {
|
||||
bg: props.colorMode === 'dark' ? 'gray.800' : 'white',
|
||||
color: props.colorMode === 'dark' ? 'white' : 'gray.800',
|
||||
},
|
||||
}),
|
||||
},
|
||||
components: {
|
||||
// Настраиваем компоненты для поддержки дополнительных тем
|
||||
Button: {
|
||||
baseStyle: (props: { colorMode: string }) => ({
|
||||
_focus: {
|
||||
boxShadow:
|
||||
props.colorMode === PINK_THEME
|
||||
? '0 0 0 3px rgba(237, 100, 166, 0.6)'
|
||||
: props.colorMode === BLUE_THEME
|
||||
? '0 0 0 3px rgba(66, 153, 225, 0.6)'
|
||||
: props.colorMode === GREEN_THEME
|
||||
? '0 0 0 3px rgba(72, 187, 120, 0.6)'
|
||||
: props.colorMode === PURPLE_THEME
|
||||
? '0 0 0 3px rgba(159, 122, 234, 0.6)'
|
||||
: undefined,
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
});
|
45
src/utils/themes.ts
Normal file
45
src/utils/themes.ts
Normal file
@ -0,0 +1,45 @@
|
||||
import { ThemeType } from '../types/theme';
|
||||
|
||||
// Константы для названий тем
|
||||
export const LIGHT_THEME = 'light';
|
||||
export const DARK_THEME = 'dark';
|
||||
export const PINK_THEME = 'pink';
|
||||
export const BLUE_THEME = 'blue';
|
||||
export const GREEN_THEME = 'green';
|
||||
export const PURPLE_THEME = 'purple';
|
||||
|
||||
// Массив всех доступных тем для переключения
|
||||
export const THEMES: ThemeType[] = [
|
||||
LIGHT_THEME,
|
||||
DARK_THEME,
|
||||
PINK_THEME,
|
||||
BLUE_THEME,
|
||||
GREEN_THEME,
|
||||
PURPLE_THEME,
|
||||
];
|
||||
|
||||
// Функция для получения следующей темы в списке
|
||||
export const getNextTheme = (currentTheme: ThemeType): ThemeType => {
|
||||
const currentIndex = THEMES.indexOf(currentTheme);
|
||||
return THEMES[(currentIndex + 1) % THEMES.length];
|
||||
};
|
||||
|
||||
// Функция для получения иконки темы в зависимости от её типа
|
||||
export const getThemeIcon = (theme: ThemeType): string => {
|
||||
switch (theme) {
|
||||
case LIGHT_THEME:
|
||||
return 'sun';
|
||||
case DARK_THEME:
|
||||
return 'moon';
|
||||
case PINK_THEME:
|
||||
return 'heart';
|
||||
case BLUE_THEME:
|
||||
return 'water';
|
||||
case GREEN_THEME:
|
||||
return 'leaf';
|
||||
case PURPLE_THEME:
|
||||
return 'stars';
|
||||
default:
|
||||
return 'sun';
|
||||
}
|
||||
};
|
@ -131,6 +131,23 @@ router.get('/lesson/:courseId/ai/generate-lessons', timer(3000), (req, res) => {
|
||||
res.send(modifiedData);
|
||||
})
|
||||
|
||||
router.post('/lesson/:courseId/ai/generate-lesson-name', timer(3000), (req, res) => {
|
||||
res.send({
|
||||
"success": true,
|
||||
"body": [
|
||||
{
|
||||
"name": "Основы CSS"
|
||||
},
|
||||
{
|
||||
"name": "CSS селекторы и свойства"
|
||||
},
|
||||
{
|
||||
"name": "Анимации и переходы на CSS"
|
||||
}
|
||||
]
|
||||
});
|
||||
})
|
||||
|
||||
router.post('/lesson', (req, res) => {
|
||||
const baseData = readJsonFile('../mocks/lessons/create/success.json');
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user