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() {
>
- } sx={{ marginBottom: 2 }} onClick={() => {
+ } sx={{ marginBottom: 2 }} onClick={() => {
setDialogContent();
setDialogOpen(true);}}>Add a Lesson
- } sx={{ marginBottom: 2, marginLeft: 1 }} onClick={() => {
+ } sx={{ marginBottom: 2, marginLeft: 1 }} onClick={() => {
setDialogContent( setDialogOpen(false)}
successCallback={resetDialog}
diff --git a/frontend/course/components/ContentTable.jsx b/frontend/course/components/ContentTable.jsx
index 58ca097..781ec02 100644
--- a/frontend/course/components/ContentTable.jsx
+++ b/frontend/course/components/ContentTable.jsx
@@ -1,7 +1,7 @@
-import { TableContainer, Table, TableHead, TableRow, TableBody, TableCell, Paper, Typography } from '@mui/material';
+import { IconButton, TableContainer, Table, TableHead, TableRow, TableBody, TableCell, Paper, Typography } from '@mui/material';
import { useState, useEffect } from 'react';
import { getCookie } from '../../src/utils.js';
-import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline';
+import DeleteIcon from '@mui/icons-material/Delete';
const ContentTable = ({ courseId, eventHandler, loaded = false }) => {
const [contentList, setContentList] = useState([]);
@@ -80,7 +80,9 @@ const ContentTable = ({ courseId, eventHandler, loaded = false }) => {
{formatPeriod(content.waiting_period)}
{content.type}
- deleteContent(content.id)} />
+ deleteContent(content.id)}>
+
+
))}
diff --git a/frontend/course/components/LessonForm.jsx b/frontend/course/components/LessonForm.jsx
index cc2b655..f53296d 100644
--- a/frontend/course/components/LessonForm.jsx
+++ b/frontend/course/components/LessonForm.jsx
@@ -149,10 +149,10 @@ function LessonForm({ header, initialTitle, initialContent, onContentChange, can
-
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}) => {
setAddingOption(true)} >
Add Option
-
+
Delete
@@ -85,7 +85,7 @@ const QuestionForm = ({question, index, deleteCallback}) => {
/>
- {
+ {
if (optionInputRef.current) {
console.log(optionInputRef.current);
addToOptions(optionInputRef.current.value);
diff --git a/frontend/course/components/QuizForm.jsx b/frontend/course/components/QuizForm.jsx
index 9ad300c..a206129 100644
--- a/frontend/course/components/QuizForm.jsx
+++ b/frontend/course/components/QuizForm.jsx
@@ -85,12 +85,12 @@ const QuizForm = ({cancelCallback, successCallback, courseId, quizId }) => {
}}/>
- {
+ {
addToQuestions();
}}>
Add Question
- { setShowQuestionField(false); setNewQuestion(""); }}>
+ { setShowQuestionField(false); setNewQuestion(""); }}>
Cancel
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}
>
-
- } sx={{ marginBottom: 2 }} onClick={() => {
+
+ } sx={{ marginBottom: 2 }} onClick={() => {
setDialogContent(
- {
- showEditCourseDialog(course);}}>Edit
- {
+ {
+ showEditCourseDialog(course);}}>
+ {
setDialogContent( setDialogOpen(false)} handleSuccess={() => {
const index = courses.findIndex(item => item.id === course.id);
setCourses(courses.filter((_, i) => i !== index));
}} />);
setDialogOpen(true);
- }}>
+ }}>
))}
@@ -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 (
-
-
-
-
-
- Email Learning
- {
- showOrganizationSwitcher && organizations.length > 0 &&
- }
-
-
-
-
-
-
+ return (
+
+ // return (
+ //
+ //
+ //
+ //
+ //
+ // Email Learning
+ // {
+ // showOrganizationSwitcher && organizations.length > 0 &&
+ // }
+ //
+ //
+ //
+ //
+ //
+ //
+ //
-
- {pages.map((page) => (
-
- {page.name}
-
- ))}
+ //
+ // {pages.map((page) => (
+ //
+ // {page.name}
+ //
+ // ))}
+ //
+ //
+
+
+
+
-
-
+
+
+
+
+
+
+
+
+
+
+
+
{
- 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 };