Dark/Light Mode Implementierung mit MUI Theme

This commit is contained in:
mathusan
2025-05-31 17:46:55 +02:00
parent 1b1924b0b7
commit 6a91a7fc53
11 changed files with 3404 additions and 116 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -13,8 +13,8 @@
"dependencies": {
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.0",
"@mui/icons-material": "^7.0.2",
"@mui/material": "^7.0.2",
"@mui/icons-material": "^7.1.0",
"@mui/material": "^7.1.0",
"i18next": "^25.2.0",
"i18next-browser-languagedetector": "^8.1.0",
"i18next-http-backend": "^3.0.2",
@@ -22,7 +22,8 @@
"react": "^19.0.0",
"react-dom": "^19.1.0",
"react-i18next": "^15.5.1",
"react-router-dom": "^7.5.3"
"react-router-dom": "^7.5.3",
"webshop": "^0.0.1"
},
"devDependencies": {
"@eslint/js": "^9.22.0",

View File

@@ -1,16 +1,37 @@
#root {
width: 100%;
/* App.css - CSS-Variablen-basierte Version */
:root {
--background-color: #fafafa;
--text-color: #000000;
}
#root {
width: 100%;
min-height: 100vh;
background-color: var(--background-color) !important;
color: var(--text-color) !important;
transition: background-color 300ms cubic-bezier(0.4, 0, 0.2, 1);
}
html, body {
margin: 0;
padding: 0;
background-color: var(--background-color) !important;
color: var(--text-color) !important;
transition: background-color 300ms cubic-bezier(0.4, 0, 0.2, 1);
}
/* Rest Ihrer Styles ohne feste Farben */
.logo {
height: 6em;
padding: 1.5em;
will-change: filter;
transition: filter 300ms;
}
.logo:hover {
filter: drop-shadow(0 0 2em #646cffaa);
}
.logo.react:hover {
filter: drop-shadow(0 0 2em #61dafbaa);
}
@@ -32,12 +53,13 @@
.card {
padding: 2em;
}
.read-the-docs {
color: #888;
border-radius: 8px;
}
.navbar-offset {
height: 3rem;
}
.read-the-docs {
opacity: 0.7;
}

View File

@@ -1,5 +1,6 @@
import ReactDOM from 'react-dom/client';
import { BrowserRouter, Route, Routes } from 'react-router-dom';
import { StyledEngineProvider } from '@mui/material/styles';
import './App.css';
import NavBar from './helper/navbar/NavBar';
import Home from './pages/Home';
@@ -9,39 +10,32 @@ import Payment from './pages/Payment';
import Contact from './pages/Contact';
import Category from './pages/Category';
import { BasketProvider } from './helper/BasketProvider';
import { createTheme, ThemeProvider } from '@mui/material/styles';
import { green } from '@mui/material/colors';
import { CustomThemeProvider } from './theme/ThemeContext';
import Orders from './pages/Orders';
import Account from './pages/Account';
export default function App() {
const theme = createTheme({
palette: {
primary: {
main: green[500],
},
},});
return (
<ThemeProvider theme={theme}>
<BasketProvider>
<BrowserRouter>
<NavBar />
<div className='navbar-offset' />
<Routes>
<Route path="/" element={<Home />} />
<Route path="*" element={<NoPage />} />
<Route path="/product/:id" element={<Product />} />
<Route path="/checkout" element={<Payment />} />
<Route path="/categories" element={<Category />} />
<Route path="/contact" element={<Contact />} />
<Route path='/account' element={<Account />} />
<Route path='/orders' element={<Orders />} />
</Routes>
</BrowserRouter>
</BasketProvider>
</ThemeProvider>
<StyledEngineProvider injectFirst>
<CustomThemeProvider>
<BasketProvider>
<BrowserRouter>
<NavBar />
<div className='navbar-offset' />
<Routes>
<Route path="/" element={<Home />} />
<Route path="*" element={<NoPage />} />
<Route path="/product/:id" element={<Product />} />
<Route path="/checkout" element={<Payment />} />
<Route path="/categories" element={<Category />} />
<Route path="/contact" element={<Contact />} />
<Route path='/account' element={<Account />} />
<Route path='/orders' element={<Orders />} />
</Routes>
</BrowserRouter>
</BasketProvider>
</CustomThemeProvider>
</StyledEngineProvider>
)
}

View File

@@ -15,25 +15,26 @@ import AdbIcon from '@mui/icons-material/Adb';
import { alpha, InputBase, styled } from '@mui/material';
import SearchIcon from '@mui/icons-material/Search';
import { useNavigate } from 'react-router-dom';
import {useTranslation} from 'react-i18next';
import { useTranslation } from 'react-i18next';
import './NavBar.css';
import ThemeToggle from '../../theme/ThemeToggle';
const Search = styled('div')(({ theme }) => ({
position: 'relative',
borderRadius: theme.shape.borderRadius,
backgroundColor: alpha(theme.palette.common.white, 0.15),
'&:hover': {
backgroundColor: alpha(theme.palette.common.white, 0.25),
backgroundColor: alpha(theme.palette.common.white, 0.25),
},
marginLeft: 0,
width: '100%',
[theme.breakpoints.up('sm')]: {
marginLeft: theme.spacing(1),
width: 'auto',
marginLeft: theme.spacing(1),
width: 'auto',
},
}));
}));
const SearchIconWrapper = styled('div')(({ theme }) => ({
const SearchIconWrapper = styled('div')(({ theme }) => ({
padding: theme.spacing(0, 2),
height: '100%',
position: 'absolute',
@@ -41,27 +42,25 @@ const Search = styled('div')(({ theme }) => ({
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}));
}));
const StyledInputBase = styled(InputBase)(({ theme }) => ({
const StyledInputBase = styled(InputBase)(({ theme }) => ({
color: 'white',
width: '100%',
'& .MuiInputBase-input': {
padding: theme.spacing(1, 1, 1, 0),
// vertical padding + font size from searchIcon
paddingLeft: `calc(1em + ${theme.spacing(4)})`,
transition: theme.transitions.create('width'),
[theme.breakpoints.up('sm')]: {
width: '12ch',
'&:focus': {
width: '20ch',
padding: theme.spacing(1, 1, 1, 0),
paddingLeft: `calc(1em + ${theme.spacing(4)})`,
transition: theme.transitions.create('width'),
[theme.breakpoints.up('sm')]: {
width: '12ch',
'&:focus': {
width: '20ch',
},
},
},
},
}));
}));
export default function NavBar() {
const { t } = useTranslation();
const navigate = useNavigate();
const [anchorElNav, setAnchorElNav] = React.useState<null | HTMLElement>(null);
@@ -76,6 +75,7 @@ export default function NavBar() {
const handleOpenNavMenu = (event: React.MouseEvent<HTMLElement>) => {
setAnchorElNav(event.currentTarget);
};
const handleOpenUserMenu = (event: React.MouseEvent<HTMLElement>) => {
setAnchorElUser(event.currentTarget);
};
@@ -157,6 +157,7 @@ export default function NavBar() {
))}
</Menu>
</Box>
<AdbIcon sx={{ display: { xs: 'flex', md: 'none' }, mr: 1 }} />
<Typography
variant="h5"
@@ -176,6 +177,7 @@ export default function NavBar() {
>
DPS
</Typography>
<Box sx={{ flexGrow: 1, display: { xs: 'none', md: 'flex' } }}>
{pages.map((page) => (
<Button
@@ -187,7 +189,9 @@ export default function NavBar() {
</Button>
))}
</Box>
<Box sx={{ flexGrow: 0 }}>
<Box sx={{ flexGrow: 0, display: 'flex', alignItems: 'center', gap: 1 }}>
<ThemeToggle />
<Tooltip title="Open settings">
<IconButton onClick={handleOpenUserMenu} sx={{ p: 0 }}>
<Avatar alt="Florian Speicher" src="/static/images/avatar/2.jpg" />
@@ -195,7 +199,7 @@ export default function NavBar() {
</Tooltip>
<Menu
sx={{ mt: '45px' }}
id="menu-appbar"
id="menu-appbar-user"
anchorEl={anchorElUser}
anchorOrigin={{
vertical: 'top',

View File

@@ -82,7 +82,7 @@
padding: 20px;
text-align: left;
background: linear-gradient(135deg, #ece9e6, #ffffff);
color: #333;
color: primary;
min-height: 100vh;
margin: 0; /* Entferne zentrierende Margins */
width: 100%; /* Container nimmt die gesamte Breite ein */
@@ -133,6 +133,7 @@
min-height: 600px;
padding: 20px 0; /* Abstand oben und unten */
box-sizing: border-box;
color: black;
}
.filter-container {

View File

@@ -0,0 +1,183 @@
'use client';
import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react';
import { createTheme, ThemeProvider } from '@mui/material/styles';
import { CssBaseline, GlobalStyles } from '@mui/material';
import useMediaQuery from '@mui/material/useMediaQuery';
type ThemeMode = 'light' | 'dark';
interface ThemeContextType {
mode: ThemeMode;
toggleMode: () => void;
}
const ThemeContext = createContext<ThemeContextType>({
mode: 'light',
toggleMode: () => {},
});
export const useThemeMode = () => useContext(ThemeContext);
interface CustomThemeProviderProps {
children: ReactNode;
}
export const CustomThemeProvider: React.FC<CustomThemeProviderProps> = ({ children }) => {
// ✅ SSR-sichere System-Präferenz-Erkennung
const prefersDarkMode = useMediaQuery('(prefers-color-scheme: dark)', { noSsr: true });
// ✅ SSR-sichere Initialisierung
const [mode, setMode] = useState<ThemeMode>('light');
const [mounted, setMounted] = useState(false);
// ✅ Nach dem ersten Render ausführen (SSR-sicher)
useEffect(() => {
setMounted(true);
if (typeof window !== 'undefined') {
const savedMode = localStorage.getItem('themeMode') as ThemeMode;
if (savedMode === 'light' || savedMode === 'dark') {
setMode(savedMode);
} else {
setMode(prefersDarkMode ? 'dark' : 'light');
}
}
}, [prefersDarkMode]);
// Mode in localStorage speichern
useEffect(() => {
if (mounted && typeof window !== 'undefined') {
localStorage.setItem('themeMode', mode);
}
}, [mode, mounted]);
// ✅ Browser-only DOM-Manipulation
useEffect(() => {
if (mounted && typeof window !== 'undefined') {
const backgroundColor = mode === 'dark' ? '#121212' : '#fafafa';
// Warten bis DOM geladen ist
const updateBackground = () => {
document.documentElement.style.setProperty('--background-color', backgroundColor);
document.documentElement.style.backgroundColor = backgroundColor;
document.body.style.backgroundColor = backgroundColor;
const root = document.getElementById('root');
if (root) {
root.style.backgroundColor = backgroundColor;
}
};
// Sofort ausführen
updateBackground();
// Nach einem kurzen Delay nochmal (für hartnäckige Cases)
const timeoutId = setTimeout(updateBackground, 100);
return () => clearTimeout(timeoutId);
}
}, [mode, mounted]);
const toggleMode = () => {
setMode(prevMode => prevMode === 'light' ? 'dark' : 'light');
};
// Theme basierend auf Mode erstellen
const theme = React.useMemo(() =>
createTheme({
palette: {
mode,
primary: {
main: '#0fd13f', // Ihre grüne NavBar
light: mode === 'dark' ? '#4caf50' : '#42a5f5',
dark: mode === 'dark' ? '#388e3c' : '#1565c0',
contrastText: '#fff',
},
secondary: {
main: mode === 'dark' ? '#bb86fc' : '#9c27b0',
light: mode === 'dark' ? '#d7aefb' : '#ba68c8',
dark: mode === 'dark' ? '#985eff' : '#7b1fa2',
contrastText: '#fff',
},
background: {
default: mode === 'dark' ? '#121212' : '#fafafa',
paper: mode === 'dark' ? '#1e1e1e' : '#ffffff',
},
text: {
primary: mode === 'dark' ? '#ffffff' : '#000000',
secondary: mode === 'dark' ? 'rgba(255, 255, 255, 0.7)' : 'rgba(0, 0, 0, 0.6)',
},
},
components: {
MuiCssBaseline: {
styleOverrides: {
html: {
backgroundColor: `${mode === 'dark' ? '#121212' : '#fafafa'} !important`,
transition: 'background-color 300ms cubic-bezier(0.4, 0, 0.2, 1)',
},
body: {
backgroundColor: `${mode === 'dark' ? '#121212' : '#fafafa'} !important`,
transition: 'background-color 300ms cubic-bezier(0.4, 0, 0.2, 1)',
margin: 0,
},
},
},
MuiAppBar: {
styleOverrides: {
colorPrimary: {
backgroundColor: '#0fd13f !important',
color: '#ffffff',
},
},
},
},
}),
[mode]
);
// ✅ Aggressive GlobalStyles mit CSS-Variablen
const globalStyles = mounted ? (
<GlobalStyles
styles={{
':root': {
'--background-color': mode === 'dark' ? '#121212' : '#fafafa',
'--text-color': mode === 'dark' ? '#ffffff' : '#000000',
},
'*': {
boxSizing: 'border-box',
},
'html, body, #root': {
backgroundColor: `var(--background-color) !important`,
color: `var(--text-color) !important`,
transition: 'background-color 300ms cubic-bezier(0.4, 0, 0.2, 1), color 300ms cubic-bezier(0.4, 0, 0.2, 1)',
margin: 0,
padding: 0,
minHeight: '100vh',
},
'div, section, main, article': {
backgroundColor: 'transparent',
},
}}
/>
) : null;
// ✅ SSR-Fallback während mounted = false
if (!mounted) {
return (
<ThemeProvider theme={createTheme({ palette: { mode: 'light' } })}>
<CssBaseline />
{children}
</ThemeProvider>
);
}
return (
<ThemeContext.Provider value={{ mode, toggleMode }}>
<ThemeProvider theme={theme}>
<CssBaseline enableColorScheme />
{globalStyles}
{children}
</ThemeProvider>
</ThemeContext.Provider>
);
};

View File

@@ -0,0 +1,28 @@
// theme/ThemeToggle.tsx
import React from 'react';
import { IconButton, Tooltip } from '@mui/material';
import { Brightness4, Brightness7 } from '@mui/icons-material';
import { useThemeMode } from './ThemeContext';
const ThemeToggle: React.FC = () => {
const { mode, toggleMode } = useThemeMode();
return (
<Tooltip title={mode === 'dark' ? 'Zu hellem Modus wechseln' : 'Zu dunklem Modus wechseln'}>
<IconButton
onClick={toggleMode}
color="inherit"
sx={{
transition: 'transform 0.2s',
'&:hover': {
transform: 'scale(1.1)',
},
}}
>
{mode === 'dark' ? <Brightness7 /> : <Brightness4 />}
</IconButton>
</Tooltip>
);
};
export default ThemeToggle;

View File

@@ -0,0 +1,11 @@
import '@mui/material/styles';
declare module '@mui/material/styles' {
interface Palette {
tertiary: Palette['primary'];
}
interface PaletteOptions {
tertiary?: PaletteOptions['primary'];
}
}

View File

@@ -0,0 +1,207 @@
import { createTheme } from '@mui/material/styles';
import './theme-augmentation.d.ts'; // Falls vorhanden
export const darkmode = createTheme({
palette: {
primary: {
main: '#0fd13f', // Ihre grüne Farbe
light: '#42a5f5',
dark: '#15650',
contrastText: '#fff',
},
secondary: {
main: '#9c27b0',
light: '#ba68c8',
dark: '#7b1fa2',
contrastText: '#fff',
},
// ✅ 'tertiary' entfernt - ist kein Standard MUI-Property
error: {
main: '#f44336',
light: '#e57373',
dark: '#d32f2f',
contrastText: '#fff',
},
warning: {
main: '#ed6c02',
light: '#ff9800',
dark: '#e65100',
contrastText: '#fff',
},
info: {
main: '#0288d1',
light: '#03a9f4',
dark: '#01579b',
contrastText: '#fff',
},
success: {
main: '#2e7d32',
light: '#4caf50',
dark: '#1b5e20',
contrastText: '#fff',
},
background: {
default: '#fafafa',
paper: '#ffffff',
},
text: {
primary: '#000000',
secondary: 'rgba(0, 0, 0, 0.6)',
},
},
// ✅ Sanfte Übergänge hinzugefügt
transitions: {
duration: {
shortest: 150,
shorter: 200,
short: 250,
standard: 300,
complex: 375,
enteringScreen: 225,
leavingScreen: 195,
},
easing: {
easeInOut: 'cubic-bezier(0.4, 0, 0.2, 1)',
easeOut: 'cubic-bezier(0.0, 0, 0.2, 1)',
easeIn: 'cubic-bezier(0.4, 0, 1, 1)',
sharp: 'cubic-bezier(0.4, 0, 0.6, 1)',
},
},
// ✅ Verbesserte Komponenten-Overrides
components: {
MuiCssBaseline: {
styleOverrides: {
body: {
backgroundColor: '#fafafa',
transition: 'background-color 300ms cubic-bezier(0.4, 0, 0.2, 1)',
},
html: {
backgroundColor: '#fafafa',
transition: 'background-color 300ms cubic-bezier(0.4, 0, 0.2, 1)',
},
},
},
MuiAppBar: {
styleOverrides: {
colorPrimary: {
backgroundColor: '#0fd13f', // Ihre grüne NavBar
color: '#ffffff',
},
},
},
MuiButton: {
styleOverrides: {
root: {
borderRadius: 8,
textTransform: 'none', // Entfernt automatische Großschreibung
fontWeight: 600,
},
contained: {
boxShadow: '0 2px 8px rgba(0,0,0,0.1)',
'&:hover': {
boxShadow: '0 4px 12px rgba(0,0,0,0.15)',
},
},
},
},
MuiCard: {
styleOverrides: {
root: {
borderRadius: 12,
boxShadow: '0 2px 12px rgba(0,0,0,0.08)',
transition: 'box-shadow 300ms cubic-bezier(0.4, 0, 0.2, 1)',
'&:hover': {
boxShadow: '0 4px 20px rgba(0,0,0,0.12)',
},
},
},
},
MuiTextField: {
styleOverrides: {
root: {
'& .MuiOutlinedInput-root': {
borderRadius: 8,
},
},
},
},
MuiChip: {
styleOverrides: {
root: {
borderRadius: 16,
},
},
},
MuiPaper: {
styleOverrides: {
root: {
borderRadius: 8,
},
elevation1: {
boxShadow: '0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.24)',
},
elevation2: {
boxShadow: '0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23)',
},
},
},
},
// ✅ Benutzerdefinierte Typografie
typography: {
fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif',
h1: {
fontWeight: 700,
fontSize: '2.5rem',
lineHeight: 1.2,
},
h2: {
fontWeight: 600,
fontSize: '2rem',
lineHeight: 1.3,
},
h3: {
fontWeight: 600,
fontSize: '1.75rem',
lineHeight: 1.3,
},
h4: {
fontWeight: 600,
fontSize: '1.5rem',
lineHeight: 1.4,
},
h5: {
fontWeight: 600,
fontSize: '1.25rem',
lineHeight: 1.4,
},
h6: {
fontWeight: 600,
fontSize: '1rem',
lineHeight: 1.4,
},
body1: {
fontSize: '1rem',
lineHeight: 1.5,
},
body2: {
fontSize: '0.875rem',
lineHeight: 1.4,
},
button: {
fontWeight: 600,
textTransform: 'none',
},
},
// ✅ Benutzerdefinierte Breakpoints
breakpoints: {
values: {
xs: 0,
sm: 600,
md: 960,
lg: 1280,
xl: 1920,
},
},
// ✅ Angepasste Spacing-Funktion
spacing: 8, // 8px als Basis-Einheit
});

View File

@@ -22,5 +22,8 @@
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["src"]
"include": [
"src/**/*",
"src/**/*.d.ts"
]
}