diff --git a/django_email_learning/static/logo-h.png b/django_email_learning/static/logo-h.png new file mode 100644 index 0000000..ffddd1d Binary files /dev/null and b/django_email_learning/static/logo-h.png differ diff --git a/django_service/static/src/assets/logo-h-dark.png b/django_service/static/src/assets/logo-h-dark.png new file mode 100644 index 0000000..b58ff91 Binary files /dev/null and b/django_service/static/src/assets/logo-h-dark.png differ diff --git a/django_service/static/src/assets/logo-h-light.png b/django_service/static/src/assets/logo-h-light.png new file mode 100644 index 0000000..ffddd1d Binary files /dev/null and b/django_service/static/src/assets/logo-h-light.png differ diff --git a/django_service/static/src/assets/logo-v-dark.png b/django_service/static/src/assets/logo-v-dark.png new file mode 100644 index 0000000..367b257 Binary files /dev/null and b/django_service/static/src/assets/logo-v-dark.png differ diff --git a/django_service/static/src/assets/logo-v-light.png b/django_service/static/src/assets/logo-v-light.png new file mode 100644 index 0000000..2db13f6 Binary files /dev/null and b/django_service/static/src/assets/logo-v-light.png differ diff --git a/frontend/course/Course.jsx b/frontend/course/Course.jsx index 1ff6211..e14b11a 100644 --- a/frontend/course/Course.jsx +++ b/frontend/course/Course.jsx @@ -89,7 +89,7 @@ function Course() { > - - - diff --git a/frontend/course/components/QuestionForm.jsx b/frontend/course/components/QuestionForm.jsx index 0947e2c..05c0d1a 100644 --- a/frontend/course/components/QuestionForm.jsx +++ b/frontend/course/components/QuestionForm.jsx @@ -65,7 +65,7 @@ const QuestionForm = ({question, index, deleteCallback}) => { - @@ -85,7 +85,7 @@ const QuestionForm = ({question, index, deleteCallback}) => { /> - - diff --git a/frontend/courses/Courses.jsx b/frontend/courses/Courses.jsx index 2e68587..53e731f 100644 --- a/frontend/courses/Courses.jsx +++ b/frontend/courses/Courses.jsx @@ -1,11 +1,12 @@ import 'vite/modulepreload-polyfill' import { useState, useEffect } from 'react' -import { Grid, Box, Link, Button, Dialog, Paper, Switch, TableContainer, Table, TableHead, TableRow,TableBody, TableCell } from '@mui/material' +import { Grid, Box, Link, Button, IconButton, Dialog, Paper, Switch, TableContainer, Table, TableHead, TableRow,TableBody, TableCell } from '@mui/material' import Base from '../src/components/Base.jsx' import CourseForm from './components/CourseForm.jsx'; import FilterListIcon from '@mui/icons-material/FilterList'; import SchoolIcon from '@mui/icons-material/School'; -import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline'; +import DeleteIcon from '@mui/icons-material/Delete'; +import EditIcon from '@mui/icons-material/Edit'; import render from '../src/render.jsx'; import { getCookie } from '../src/utils.js'; import EnableCourseSwitchPopup from './components/EnableCourseSwitchPopup.jsx'; @@ -104,8 +105,8 @@ function Courses() { organizationIdRefreshCallback={setOrganizationId} > - - - + }}> ))} @@ -160,7 +161,7 @@ function Courses() { - + setQueryParameters(params)} /> diff --git a/frontend/src/assets/logo-h-dark.png b/frontend/src/assets/logo-h-dark.png new file mode 100644 index 0000000..b58ff91 Binary files /dev/null and b/frontend/src/assets/logo-h-dark.png differ diff --git a/frontend/src/assets/logo-h-light.png b/frontend/src/assets/logo-h-light.png new file mode 100644 index 0000000..ffddd1d Binary files /dev/null and b/frontend/src/assets/logo-h-light.png differ diff --git a/frontend/src/assets/logo-v-dark.png b/frontend/src/assets/logo-v-dark.png new file mode 100644 index 0000000..367b257 Binary files /dev/null and b/frontend/src/assets/logo-v-dark.png differ diff --git a/frontend/src/assets/logo-v-light.png b/frontend/src/assets/logo-v-light.png new file mode 100644 index 0000000..2db13f6 Binary files /dev/null and b/frontend/src/assets/logo-v-light.png differ diff --git a/frontend/src/components/Base.jsx b/frontend/src/components/Base.jsx index c743c78..fca9c4c 100644 --- a/frontend/src/components/Base.jsx +++ b/frontend/src/components/Base.jsx @@ -1,13 +1,14 @@ import BottomDrawer from "./BottomDrawer"; import MenuBar from "./MenuBar"; import { useState, useEffect } from "react"; -import { Grid, Breadcrumbs, Typography, Link } from "@mui/material"; +import { Box, GlobalStyles, Grid, Breadcrumbs, Typography, Link } from "@mui/material"; import { getCookie } from "../utils.js"; function Base({breadCrumbList, children, bottomDrawerParams, organizationIdRefreshCallback, showOrganizationSwitcher=true}) { const [activeOrganizationId, setActiveOrganizationId] = useState(null); const baseApiUrl = localStorage.getItem('apiBaseUrl'); + const drawerWidth = 250; useEffect(() => { const orgId = localStorage.getItem('activeOrganizationId'); @@ -57,7 +58,10 @@ function Base({breadCrumbList, children, bottomDrawerParams, organizationIdRefre return ( <> - + ({ body: { margin: 0, padding: 0, backgroundColor: theme.palette.background.dark, color: theme.palette.text.primary } })} /> + + @@ -66,7 +70,7 @@ function Base({breadCrumbList, children, bottomDrawerParams, organizationIdRefre {label} : - + {label} ))} @@ -79,6 +83,7 @@ function Base({breadCrumbList, children, bottomDrawerParams, organizationIdRefre {bottomDrawerParams.children} } + ); } diff --git a/frontend/src/components/ContentEditor.jsx b/frontend/src/components/ContentEditor.jsx index baeb9ef..132414b 100644 --- a/frontend/src/components/ContentEditor.jsx +++ b/frontend/src/components/ContentEditor.jsx @@ -55,7 +55,7 @@ function ContentEditor({ initialContent, contentUpdateCallback }) { {/* Material UI Toolbar */} diff --git a/frontend/src/components/MenuBar.jsx b/frontend/src/components/MenuBar.jsx index 264b173..9efc490 100644 --- a/frontend/src/components/MenuBar.jsx +++ b/frontend/src/components/MenuBar.jsx @@ -1,4 +1,4 @@ -import { useState, useEffect } from 'react' +import { useState, useEffect, use } from 'react' import { AppBar, Toolbar, Drawer, Box, Typography, MenuList, MenuItem, ListItemIcon, ListItemText, Button, Link, Select } from '@mui/material' import IconButton from '@mui/material/IconButton'; import SchoolIcon from '@mui/icons-material/School'; @@ -6,8 +6,14 @@ import PeopleIcon from '@mui/icons-material/People'; import BarChartIcon from '@mui/icons-material/BarChart'; import Diversity3Icon from '@mui/icons-material/Diversity3'; import MenuIcon from '@mui/icons-material/Menu'; -import logoUrl from '../assets/logo.png' +import logoHorizontalLightUrl from '../assets/logo-h-light.png' +import logoHorizontalDarkUrl from '../assets/logo-h-dark.png' +import logoVerticalLightUrl from '../assets/logo-v-light.png' +import logoVerticalDarkUrl from '../assets/logo-v-dark.png' import { getCookie } from '../utils.js'; +import { useThemeContext } from '../theme/ThemeContext.jsx'; +import { useTheme, useMediaQuery } from "@mui/material"; +import ThemeSwitcher from './ThemeSwitcher.jsx'; const apiBaseUrl = localStorage.getItem('apiBaseUrl'); const platformBaseUrl = localStorage.getItem('platformBaseUrl'); @@ -42,10 +48,17 @@ function OrganizationsSelect({organizations, activeOrganizationId, changeOrganiz ) } -function MenuBar({activeOrganizationId, changeOrganizationCallback, showOrganizationSwitcher}) { +function MenuBar({activeOrganizationId, changeOrganizationCallback, showOrganizationSwitcher, drawerWidth}) { const [menuOpen, setMenuOpen] = useState(false) const [organizations, setOrganizations] = useState([]) + const theme = useTheme(); + const isMdUpScreen = useMediaQuery(theme.breakpoints.up('md')); + + const drawerVariant = isMdUpScreen ? "permanent" : "temporary"; + const logoHorizontalUrl = theme.palette.mode === 'light' ? logoHorizontalLightUrl : logoHorizontalDarkUrl; + const logoVerticalUrl = theme.palette.mode === 'light' ? logoVerticalLightUrl : logoVerticalDarkUrl; + useEffect(() => { fetch(apiBaseUrl + '/organizations/', { method: 'GET', @@ -81,47 +94,66 @@ function MenuBar({activeOrganizationId, changeOrganizationCallback, showOrganiza setMenuOpen(newOpen); }; - return ( - - - Logo - - - Email Learning - { - showOrganizationSwitcher && organizations.length > 0 && - } - - - - - - + return ( + + // return ( + // + // + // Logo + // + // + // Email Learning + // { + // showOrganizationSwitcher && organizations.length > 0 && + // } + // + // + // + // + // + // + // - - {pages.map((page) => ( - - ))} + // + // {pages.map((page) => ( + // + // ))} + // + // + + + + Logo - - + + + + + + + + + + + Logo + { - showOrganizationSwitcher && + showOrganizationSwitcher && } { pages.map((page) => ( @@ -136,7 +168,7 @@ function MenuBar({activeOrganizationId, changeOrganizationCallback, showOrganiza )) } - ) + ) } export default MenuBar diff --git a/frontend/src/components/ThemeSwitcher.jsx b/frontend/src/components/ThemeSwitcher.jsx new file mode 100644 index 0000000..12fae67 --- /dev/null +++ b/frontend/src/components/ThemeSwitcher.jsx @@ -0,0 +1,81 @@ +import { Box } from '@mui/material'; +import { styled } from '@mui/material/styles'; +import Switch from '@mui/material/Switch'; +import { useThemeContext } from '../theme/ThemeContext'; +import { lightTheme, darkTheme } from '../theme/themes'; + +const DayNightSwitch = styled(Switch)(({ theme }) => ({ + width: 50, + height: 28, + padding: 7, + '& .MuiSwitch-switchBase': { + margin: 1, + padding: 0, + transform: 'translateX(6px)', + '&.Mui-checked': { + color: '#fff', + transform: 'translateX(22px)', + '& .MuiSwitch-thumb:before': { + backgroundImage: `url('data:image/svg+xml;utf8,')`, + }, + '& + .MuiSwitch-track': { + opacity: 1, + backgroundColor: '#aab4be', + ...theme.applyStyles('dark', { + backgroundColor: '#8796A5', + }), + }, + }, + }, + '& .MuiSwitch-thumb': { + backgroundColor: '#dfdfdfff', + width: 25, + height: 25, + '&::before': { + content: "''", + position: 'absolute', + width: '100%', + height: '100%', + left: 0, + top: 0, + backgroundRepeat: 'no-repeat', + backgroundPosition: 'center', + backgroundImage: `url('data:image/svg+xml;utf8,')`, + }, + ...theme.applyStyles('dark', { + backgroundColor: '#003892', + }), + }, + '& .MuiSwitch-track': { + opacity: 1, + backgroundColor: '#aab4be', + borderRadius: 20 / 2, + ...theme.applyStyles('dark', { + backgroundColor: '#8796A5', + }), + }, +})); + + +const ThemeSwitcher = () => { + const { currentTheme, changeTheme } = useThemeContext(); + + const isLightTheme = currentTheme.palette.mode === 'light'; + + const toggleTheme = () => { + localStorage.setItem('theme', isLightTheme ? 'dark' : 'light'); + changeTheme(isLightTheme ? darkTheme : lightTheme); + }; + + return ( + + + + ); +}; + +export default ThemeSwitcher; diff --git a/frontend/src/render-callback.jsx b/frontend/src/render-callback.jsx new file mode 100644 index 0000000..bb2ae58 --- /dev/null +++ b/frontend/src/render-callback.jsx @@ -0,0 +1,25 @@ +import { StrictMode, useState } from 'react' +import { createRoot } from 'react-dom/client' +import { ThemeProvider } from '@mui/material/styles'; +import { lightTheme, darkTheme } from './theme/themes'; +import './index.css' + +function render({children}) { + const [currentTheme, setCurrentTheme] = useState(lightTheme); + + // Clone children and pass theme setter as prop + const childrenWithProps = React.cloneElement(children, { + onThemeChange: setCurrentTheme, + availableThemes: { lightTheme, darkTheme } + }); + + createRoot(document.getElementById('root')).render( + + + {childrenWithProps} + + , + ) +} + +export default render; diff --git a/frontend/src/render.jsx b/frontend/src/render.jsx index 33ddf0b..777b74f 100644 --- a/frontend/src/render.jsx +++ b/frontend/src/render.jsx @@ -1,59 +1,23 @@ import { StrictMode } from 'react' import { createRoot } from 'react-dom/client' -import { createTheme, ThemeProvider } from '@mui/material/styles'; -import { blueGrey } from '@mui/material/colors'; +import { ThemeContextProvider } from './theme/ThemeContext'; +import { lightTheme, darkTheme } from './theme/themes'; import './index.css' -const theme = createTheme({ - palette: { - mode: 'light', - background: { - paper: '#ffffffff', - default: '#f3f3f3', - }, - primary: { - main: '#00d5be', - }, - secondary: { - main: '#7c86ff', - }, - errorText: { - main: '#a93e6bff', - }, - grey: blueGrey, - }, - components: { - defaultProps: { - size: 'small', - }, - MuiTable: { - defaultProps: { - size: 'small', - }, - }, - MuiTextField: { - defaultProps: { - variant: 'outlined', - size: 'small', - }, - }, - MuiDrawer: { - styleOverrides: { - backdrop: { - backgroundColor: '#fff' - }, - }, - }, - }, -}); - function render({children}) { + const storedTheme = localStorage.getItem('theme'); + if (!storedTheme) { + localStorage.setItem('theme', 'light'); + storedTheme = 'light'; + } + const initialTheme = storedTheme === 'dark' ? darkTheme : lightTheme; + createRoot(document.getElementById('root')).render( - - {children} - + + {children} + , ) } diff --git a/frontend/src/theme/ThemeContext.jsx b/frontend/src/theme/ThemeContext.jsx new file mode 100644 index 0000000..ba36dee --- /dev/null +++ b/frontend/src/theme/ThemeContext.jsx @@ -0,0 +1,29 @@ +import React, { createContext, useContext, useState } from 'react'; +import { ThemeProvider } from '@mui/material/styles'; +import { lightTheme } from './themes'; + +const ThemeContext = createContext(); + +export const useThemeContext = () => { + const context = useContext(ThemeContext); + if (!context) { + throw new Error('useThemeContext must be used within a ThemeContextProvider'); + } + return context; +}; + +export const ThemeContextProvider = ({ children, initialTheme = lightTheme }) => { + const [currentTheme, setCurrentTheme] = useState(initialTheme); + + const changeTheme = (newTheme) => { + setCurrentTheme(newTheme); + }; + + return ( + + + {children} + + + ); +}; diff --git a/frontend/src/theme/themes.js b/frontend/src/theme/themes.js new file mode 100644 index 0000000..9436a03 --- /dev/null +++ b/frontend/src/theme/themes.js @@ -0,0 +1,175 @@ +import { createTheme } from '@mui/material/styles'; +import { blueGrey } from '@mui/material/colors'; +import { Margin, Padding } from '@mui/icons-material'; + +const defaultOptions = { + components: { + MuiTable: { + defaultProps: { + size: 'small', + }, + }, + MuiSwitch: { + defaultProps: { + size: 'small', + }, + }, + MuiRadio: { + defaultProps: { + size: 'small', + }, + }, + MuiTextField: { + defaultProps: { + variant: 'outlined', + size: 'small', + }, + }, + MuiIconButton: { + defaultProps: { + size: 'small', + }, + styleOverrides: { + root: ({theme}) => ({ + color: 'inherit', + padding: '3px', + margin: '2px', + '*': { + fontSize: '1.2rem', + }, + '&:hover': { + backgroundColor: theme.palette.secondary.dark, + color: 'white', + }, + transition: 'color 0.3s, background-color 0.3s', + }), + }, + }, + MuiButton: { + defaultProps: { + size: 'small', + }, + variants: [ + { + props: { variant: 'contained' }, + style: ({ theme }) => ({ + textTransform: 'none', + boxShadow: 'none', + borderRadius: 8, + color: '#ffffff', + backgroundColor: theme.palette.secondary.dark, + '&:hover': { + backgroundColor: theme.palette.primary.dark, + }, + }), + }, + { + props: { variant: 'text' }, + style: ({ theme }) => ({ + textTransform: 'none', + color: theme.palette.secondary.text, + borderRadius: 8, + }), + }, + { + props: { variant: 'outlined' }, + style: ({ theme }) => ({ + textTransform: 'none', + borderRadius: 8, + color: theme.palette.secondary.text, + }), + } + ] + }, + MuiDrawer: { + styleOverrides: { + backdrop: { + backgroundColor: '#fff' + }, + }, + }, + MuiTableHead: { + styleOverrides: { + root: ({ theme }) => ({ + backgroundColor: theme.palette.background.nav, + '* th': { + fontWeight: 'bold', + paddingTop: '10px', + paddingBottom: '10px', + }, + }), + }, + }, + }, +} + +const lightPalette = { + mode: 'light', + text: { + primary: '#000000de', + secondary: '#00000099', + }, + background: { + main: '#f8f8fa', + dark: '#f2f4f5ff', + nav: '#ffffffff', + }, + border: { + main: '#ccccccff', + }, + primary: { + main: '#00d5be', + }, + secondary: { + main: '#7c86ff', + text: '#313ba9ff', + }, + errorText: { + main: '#a93e6bff', + }, + grey: blueGrey, +}; + +const darkPalette = { + mode: 'dark', + text: { + primary: '#ffffffde', + secondary: '#ffffff99', + }, + background: { + main: '#383539', + dark: '#232324ff', + nav: '#2c2c2eff', + }, + border: { + main: '#555555ff', + }, + primary: { + main: '#00d5be', + }, + secondary: { + main: '#7c86ff', + text: '#b8bdfaff', + }, + errorText: { + main: '#ff6b9d', + }, + grey: blueGrey, +}; + +const lightTheme = createTheme({ + palette: lightPalette, + components: { + ...defaultOptions.components + }, +}); + +const darkTheme = createTheme({ + palette: darkPalette, + components: { + ...defaultOptions.components + }, +}); + + +export { lightTheme, darkTheme };