dark theme

This commit is contained in:
romantarkin 2025-08-02 16:49:34 +05:00
parent 8951edbc9b
commit 118bdb058b
19 changed files with 392 additions and 188 deletions

View File

@ -12,6 +12,7 @@ import CampaignPage from './pages/CampaignPage';
import MainLayout from './components/MainLayout';
import './App.css';
import { useUser } from './context/UserContext';
import { ThemeProvider } from './context/ThemeContext';
function App() {
const { user, token, isCheckingAuth } = useUser();
@ -21,108 +22,110 @@ function App() {
}
return (
<Router>
<Routes>
<Route path="/login" element={<Login />} />
<Route
path="/dashboard"
element={
user && token ? (
<MainLayout>
<Dashboard />
</MainLayout>
) : (
<Navigate to="/login" replace />
)
}
/>
<Route
path="/users"
element={
user && token ? (
<MainLayout>
<UsersPage />
</MainLayout>
) : (
<Navigate to="/login" replace />
)
}
/>
<Route
path="/smtp"
element={
user && token ? (
<MainLayout>
<SmtpServersPage />
</MainLayout>
) : (
<Navigate to="/login" replace />
)
}
/>
<Route
path="/template"
element={
user && token ? (
<MainLayout>
<EmailTemplatesPage />
</MainLayout>
) : (
<Navigate to="/login" replace />
)
}
/>
<Route
path="/unsubscribed"
element={
user && token ? (
<MainLayout>
<UnsubscribedPage />
</MainLayout>
) : (
<Navigate to="/login" replace />
)
}
/>
<Route
path="/groups"
element={
user && token ? (
<MainLayout>
<GroupsPage />
</MainLayout>
) : (
<Navigate to="/login" replace />
)
}
/>
<Route
path="/history"
element={
user && token ? (
<MainLayout>
<DeliveryHistoryPage />
</MainLayout>
) : (
<Navigate to="/login" replace />
)
}
/>
<Route
path="/campaign"
element={
user && token ? (
<MainLayout>
<CampaignPage />
</MainLayout>
) : (
<Navigate to="/login" replace />
)
}
/>
<Route path="*" element={<Navigate to="/dashboard" replace />} />
</Routes>
</Router>
<ThemeProvider>
<Router>
<Routes>
<Route path="/login" element={<Login />} />
<Route
path="/dashboard"
element={
user && token ? (
<MainLayout>
<Dashboard />
</MainLayout>
) : (
<Navigate to="/login" replace />
)
}
/>
<Route
path="/users"
element={
user && token ? (
<MainLayout>
<UsersPage />
</MainLayout>
) : (
<Navigate to="/login" replace />
)
}
/>
<Route
path="/smtp"
element={
user && token ? (
<MainLayout>
<SmtpServersPage />
</MainLayout>
) : (
<Navigate to="/login" replace />
)
}
/>
<Route
path="/template"
element={
user && token ? (
<MainLayout>
<EmailTemplatesPage />
</MainLayout>
) : (
<Navigate to="/login" replace />
)
}
/>
<Route
path="/unsubscribed"
element={
user && token ? (
<MainLayout>
<UnsubscribedPage />
</MainLayout>
) : (
<Navigate to="/login" replace />
)
}
/>
<Route
path="/groups"
element={
user && token ? (
<MainLayout>
<GroupsPage />
</MainLayout>
) : (
<Navigate to="/login" replace />
)
}
/>
<Route
path="/history"
element={
user && token ? (
<MainLayout>
<DeliveryHistoryPage />
</MainLayout>
) : (
<Navigate to="/login" replace />
)
}
/>
<Route
path="/campaign"
element={
user && token ? (
<MainLayout>
<CampaignPage />
</MainLayout>
) : (
<Navigate to="/login" replace />
)
}
/>
<Route path="*" element={<Navigate to="/dashboard" replace />} />
</Routes>
</Router>
</ThemeProvider>
);
}

View File

@ -1,10 +1,12 @@
import React from 'react';
import styles from './Header.module.css';
import ThemeToggle from './ThemeToggle';
const Header = ({ user, onLogout }) => {
return (
<header className={styles.header}>
<div className={styles.right}>
<ThemeToggle />
<span className={styles.user}>{user?.email || 'Аккаунт'}</span>
<button className={styles.logout} onClick={onLogout}>Выйти</button>
</div>

View File

@ -1,8 +1,8 @@
.header {
width: 100%;
height: 64px;
background: #fff;
border-bottom: 1.5px solid #e5e7eb;
background: var(--card-background);
border-bottom: 1.5px solid var(--border-color);
display: flex;
align-items: center;
justify-content: flex-end;
@ -38,7 +38,7 @@
}
.logout:hover {
background: linear-gradient(90deg, #3730a3 0%, #06b6d4 100%);
background: linear-gradient(90deg, #4f46e5 0%, #06b6d4 100%);
}
/* Адаптивные стили для мобильных устройств */

View File

@ -1,8 +1,8 @@
.menu {
width: 250px;
min-height: 100vh;
background: #f3f4f6;
border-right: 1.5px solid #e5e7eb;
background: var(--card-background);
border-right: 1.5px solid var(--border-color);
display: flex;
flex-direction: column;
padding: 0 0 24px 0;
@ -12,10 +12,10 @@
.project {
font-size: 22px;
font-weight: 700;
color: #3730a3;
color: #6366f1;
padding: 32px 24px 16px 24px;
letter-spacing: 1px;
border-bottom: 1px solid #e5e7eb;
border-bottom: 1px solid var(--border-color);
margin-bottom: 12px;
}
@ -42,20 +42,20 @@ ul {
li {
padding: 12px 24px;
font-size: 16px;
color: #374151;
color: var(--text-color);
cursor: pointer;
border-left: 3px solid transparent;
transition: background 0.15s, border 0.15s, color 0.15s;
}
li:hover {
background: #e0e7ff;
color: #3730a3;
background: var(--hover-background);
color: #6366f1;
}
.active {
background: #e0e7ff;
color: #3730a3;
background: var(--hover-background);
color: #6366f1;
border-left: 3px solid #6366f1;
font-weight: 600;
}
@ -120,7 +120,7 @@ li:hover {
top: 0;
z-index: 1000;
width: 250px;
box-shadow: 2px 0 10px rgba(0, 0, 0, 0.1);
box-shadow: 2px 0 10px var(--shadow-color);
}
.menu.open {

View File

@ -0,0 +1,36 @@
import React from 'react';
import { useTheme } from '../context/ThemeContext';
import styles from './ThemeToggle.module.css';
const ThemeToggle = () => {
const { theme, toggleTheme, isDark } = useTheme();
return (
<button
className={styles.themeToggle}
onClick={toggleTheme}
aria-label={`Переключить на ${isDark ? 'светлую' : 'темную'} тему`}
title={`Переключить на ${isDark ? 'светлую' : 'темную'} тему`}
>
{isDark ? (
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<circle cx="12" cy="12" r="5"/>
<line x1="12" y1="1" x2="12" y2="3"/>
<line x1="12" y1="21" x2="12" y2="23"/>
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/>
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/>
<line x1="1" y1="12" x2="3" y2="12"/>
<line x1="21" y1="12" x2="23" y2="12"/>
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/>
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/>
</svg>
) : (
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/>
</svg>
)}
</button>
);
};
export default ThemeToggle;

View File

@ -0,0 +1,50 @@
.themeToggle {
background: none;
border: none;
cursor: pointer;
padding: 8px;
border-radius: 8px;
color: var(--text-color);
transition: all 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
background-color: var(--card-background);
border: 1px solid var(--border-color);
}
.themeToggle:hover {
background-color: var(--hover-background);
transform: scale(1.05);
}
.themeToggle:active {
transform: scale(0.95);
}
.themeToggle svg {
transition: transform 0.3s ease;
}
.themeToggle:hover svg {
transform: rotate(15deg);
}
/* Адаптивные стили */
@media (max-width: 768px) {
.themeToggle {
width: 36px;
height: 36px;
padding: 6px;
}
}
@media (max-width: 480px) {
.themeToggle {
width: 32px;
height: 32px;
padding: 4px;
}
}

View File

@ -0,0 +1,67 @@
import React, { createContext, useContext, useState, useEffect } from 'react';
const ThemeContext = createContext();
export const useTheme = () => {
const context = useContext(ThemeContext);
if (!context) {
throw new Error('useTheme must be used within a ThemeProvider');
}
return context;
};
export const ThemeProvider = ({ children }) => {
const [theme, setTheme] = useState(() => {
// Проверяем сохраненную тему в localStorage
const savedTheme = localStorage.getItem('theme');
if (savedTheme) {
return savedTheme;
}
// Если нет сохраненной темы, используем системную
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
});
useEffect(() => {
// Сохраняем тему в localStorage
localStorage.setItem('theme', theme);
// Применяем тему к документу
document.documentElement.setAttribute('data-theme', theme);
// Обновляем CSS переменные
const root = document.documentElement;
if (theme === 'dark') {
root.style.setProperty('--background-color', '#1a1a1a');
root.style.setProperty('--text-color', '#ffffff');
root.style.setProperty('--border-color', '#333333');
root.style.setProperty('--card-background', '#2d2d2d');
root.style.setProperty('--hover-background', '#3a3a3a');
root.style.setProperty('--secondary-text', '#a0a0a0');
root.style.setProperty('--shadow-color', 'rgba(0, 0, 0, 0.3)');
} else {
root.style.setProperty('--background-color', '#ffffff');
root.style.setProperty('--text-color', '#000000');
root.style.setProperty('--border-color', '#e5e7eb');
root.style.setProperty('--card-background', '#ffffff');
root.style.setProperty('--hover-background', '#f9fafb');
root.style.setProperty('--secondary-text', '#6b7280');
root.style.setProperty('--shadow-color', 'rgba(0, 0, 0, 0.1)');
}
}, [theme]);
const toggleTheme = () => {
setTheme(prevTheme => prevTheme === 'light' ? 'dark' : 'light');
};
const value = {
theme,
toggleTheme,
isDark: theme === 'dark'
};
return (
<ThemeContext.Provider value={value}>
{children}
</ThemeContext.Provider>
);
};

View File

@ -6,6 +6,9 @@ body {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
overflow-x: hidden;
background-color: var(--background-color);
color: var(--text-color);
transition: background-color 0.3s ease, color 0.3s ease;
}
code {
@ -18,6 +21,27 @@ code {
box-sizing: border-box;
}
/* CSS переменные для тем */
:root {
--background-color: #ffffff;
--text-color: #000000;
--border-color: #e5e7eb;
--card-background: #ffffff;
--hover-background: #f9fafb;
--secondary-text: #6b7280;
--shadow-color: rgba(0, 0, 0, 0.1);
}
[data-theme="dark"] {
--background-color: #1a1a1a;
--text-color: #ffffff;
--border-color: #333333;
--card-background: #2d2d2d;
--hover-background: #3a3a3a;
--secondary-text: #a0a0a0;
--shadow-color: rgba(0, 0, 0, 0.3);
}
/* Медиа-запросы для разных устройств */
@media (max-width: 768px) {
html {
@ -39,19 +63,7 @@ code {
}
}
/* Поддержка темной темы */
@media (prefers-color-scheme: dark) {
:root {
--background-color: #1a1a1a;
--text-color: #ffffff;
--border-color: #333333;
}
}
@media (prefers-color-scheme: light) {
:root {
--background-color: #ffffff;
--text-color: #000000;
--border-color: #e5e7eb;
}
/* Плавные переходы для всех элементов */
* {
transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease, box-shadow 0.3s ease;
}

View File

@ -2,6 +2,7 @@ import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import styles from './Login.module.css';
import { useUser } from '../context/UserContext';
import ThemeToggle from '../components/ThemeToggle';
const Login = () => {
const [email, setEmail] = useState('');
@ -37,6 +38,9 @@ const Login = () => {
return (
<div className={styles.wrapper}>
<div className={styles.themeToggleContainer}>
<ThemeToggle />
</div>
<form onSubmit={handleSubmit} className={styles.form}>
<h2 className={styles.title}>Вход в систему</h2>
<input

View File

@ -5,6 +5,17 @@
align-items: center;
background: linear-gradient(135deg, #e0e7ff 0%, #f0fdfa 100%);
padding: 16px;
position: relative;
}
[data-theme="dark"] .wrapper {
background: linear-gradient(135deg, #1e1b4b 0%, #0f172a 100%);
}
.themeToggleContainer {
position: absolute;
top: 20px;
right: 20px;
}
.form {
@ -15,9 +26,9 @@
gap: 20px;
padding: 36px;
border-radius: 16px;
background: #fff;
background: var(--card-background);
box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.15);
border: 1px solid #e5e7eb;
border: 1px solid var(--border-color);
transition: box-shadow 0.3s;
}
@ -26,7 +37,7 @@
margin-bottom: 8px;
font-weight: 700;
font-size: 28px;
color: #3730a3;
color: #6366f1;
text-align: center;
letter-spacing: 1px;
}
@ -34,11 +45,12 @@
.input {
padding: 12px 14px;
border-radius: 8px;
border: 1px solid #c7d2fe;
border: 1px solid var(--border-color);
font-size: 16px;
outline: none;
transition: border 0.2s;
background: #f8fafc;
background: var(--card-background);
color: var(--text-color);
min-height: 48px;
}
@ -110,6 +122,11 @@
padding-top: 60px;
}
.themeToggleContainer {
top: 16px;
right: 16px;
}
.form {
width: 100%;
max-width: 400px;
@ -137,6 +154,11 @@
padding-top: 40px;
}
.themeToggleContainer {
top: 12px;
right: 12px;
}
.form {
padding: 24px 20px;
border-radius: 10px;

View File

@ -1,7 +1,7 @@
.title {
margin: 0 0 18px 0;
text-align: center;
color: #3730a3;
color: #6366f1;
font-weight: 700;
}
.form {

View File

@ -2,7 +2,7 @@
.dashboard {
display: flex;
min-height: 100vh;
background: #f8fafc;
background: var(--background-color);
}
.content {
@ -16,6 +16,7 @@
flex: 1;
padding: 32px;
overflow-x: auto;
background: var(--background-color);
}
/* Адаптивные стили для планшетов */
@ -58,45 +59,46 @@
width: 100%;
border-collapse: collapse;
margin-top: 16px;
background: #fff;
background: var(--card-background);
border-radius: 8px;
overflow: hidden;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
box-shadow: 0 1px 3px var(--shadow-color);
border: 1px solid var(--border-color);
}
.tableHeader {
background: #f3f4f6;
background: var(--hover-background);
font-weight: 600;
border-bottom: 2px solid #e5e7eb;
border-bottom: 2px solid var(--border-color);
}
.tableHeaderCell {
padding: 12px 16px;
text-align: left;
font-size: 14px;
color: #374151;
color: var(--text-color);
}
.tableRow {
border-bottom: 1px solid #e5e7eb;
border-bottom: 1px solid var(--border-color);
transition: background-color 0.15s;
}
.tableRow:hover {
background-color: #f9fafb;
background-color: var(--hover-background);
}
.tableCell {
padding: 12px 16px;
background: #fff;
background: var(--card-background);
font-size: 14px;
color: #374151;
color: var(--text-color);
vertical-align: top;
}
.tableCellActions {
padding: 8px 16px;
background: #fff;
background: var(--card-background);
white-space: nowrap;
}
@ -126,13 +128,13 @@
}
.buttonSecondary {
background: #f3f4f6;
color: #374151;
border: 1px solid #d1d5db;
background: var(--hover-background);
color: var(--text-color);
border: 1px solid var(--border-color);
}
.buttonSecondary:hover {
background: #e5e7eb;
background: var(--border-color);
}
.buttonDanger {
@ -179,16 +181,17 @@
.formLabel {
font-size: 14px;
font-weight: 500;
color: #374151;
color: var(--text-color);
}
.formInput {
padding: 10px 12px;
border: 1.5px solid #d1d5db;
border: 1.5px solid var(--border-color);
border-radius: 6px;
font-size: 14px;
transition: border-color 0.2s;
background: #fff;
background: var(--card-background);
color: var(--text-color);
}
.formInput:focus {
@ -199,14 +202,15 @@
.formTextarea {
padding: 10px 12px;
border: 1.5px solid #d1d5db;
border: 1.5px solid var(--border-color);
border-radius: 6px;
font-size: 14px;
font-family: inherit;
resize: vertical;
min-height: 80px;
transition: border-color 0.2s;
background: #fff;
background: var(--card-background);
color: var(--text-color);
}
.formTextarea:focus {
@ -217,10 +221,11 @@
.formSelect {
padding: 10px 12px;
border: 1.5px solid #d1d5db;
border: 1.5px solid var(--border-color);
border-radius: 6px;
font-size: 14px;
background: #fff;
background: var(--card-background);
color: var(--text-color);
cursor: pointer;
transition: border-color 0.2s;
}
@ -251,7 +256,7 @@
.pageTitle {
font-size: 24px;
font-weight: 700;
color: #111827;
color: var(--text-color);
margin: 0;
}
@ -394,7 +399,7 @@
align-items: center;
justify-content: center;
padding: 20px;
color: #6b7280;
color: var(--secondary-text);
}
.error {
@ -419,7 +424,7 @@
.emptyState {
text-align: center;
padding: 40px 20px;
color: #9ca3af;
color: var(--secondary-text);
}
.emptyStateIcon {
@ -432,12 +437,12 @@
font-size: 18px;
font-weight: 600;
margin-bottom: 8px;
color: #6b7280;
color: var(--secondary-text);
}
.emptyStateText {
font-size: 14px;
color: #9ca3af;
color: var(--secondary-text);
}
/* Стили для главной страницы с аналитикой */
@ -449,19 +454,20 @@
}
.statCard {
background: #fff;
background: var(--card-background);
border-radius: 8px;
padding: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
box-shadow: 0 2px 8px var(--shadow-color);
display: flex;
align-items: center;
gap: 8px;
transition: transform 0.2s, box-shadow 0.2s;
border: 1px solid var(--border-color);
}
.statCard:hover {
transform: translateY(-2px);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
box-shadow: 0 4px 16px var(--shadow-color);
}
.statIcon {
@ -485,24 +491,25 @@
.statNumber {
font-size: 18px;
font-weight: 700;
color: #111827;
color: var(--text-color);
margin-bottom: 2px;
line-height: 1.1;
}
.statLabel {
font-size: 11px;
color: #6b7280;
color: var(--secondary-text);
font-weight: 500;
line-height: 1.1;
}
.additionalStats {
background: #fff;
background: var(--card-background);
border-radius: 12px;
padding: 24px;
margin-bottom: 32px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
box-shadow: 0 2px 8px var(--shadow-color);
border: 1px solid var(--border-color);
}
.statRow {
@ -516,7 +523,7 @@
justify-content: space-between;
align-items: center;
padding: 12px 0;
border-bottom: 1px solid #f3f4f6;
border-bottom: 1px solid var(--border-color);
}
.statItem:last-child {
@ -525,28 +532,29 @@
.statItem .statLabel {
font-size: 14px;
color: #6b7280;
color: var(--secondary-text);
font-weight: 500;
}
.statItem .statValue {
font-size: 16px;
font-weight: 600;
color: #111827;
color: var(--text-color);
}
.section {
background: #fff;
background: var(--card-background);
border-radius: 12px;
padding: 24px;
margin-bottom: 24px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
box-shadow: 0 2px 8px var(--shadow-color);
border: 1px solid var(--border-color);
}
.sectionTitle {
font-size: 20px;
font-weight: 600;
color: #111827;
color: var(--text-color);
margin: 0 0 20px 0;
}
@ -563,9 +571,9 @@
justify-content: space-between;
align-items: center;
padding: 16px;
background: #f9fafb;
background: var(--hover-background);
border-radius: 8px;
border: 1px solid #e5e7eb;
border: 1px solid var(--border-color);
}
.campaignInfo,
@ -579,13 +587,13 @@
.deliveryEmail {
font-size: 16px;
font-weight: 500;
color: #111827;
color: var(--text-color);
}
.campaignDate,
.deliveryDate {
font-size: 14px;
color: #6b7280;
color: var(--secondary-text);
}
.campaignStatus,
@ -635,7 +643,7 @@
.statusclicked {
background: #e0e7ff;
color: #3730a3;
color: #6366f1;
}
/* Адаптивные стили для главной страницы */

View File

@ -1,7 +1,7 @@
.title {
margin: 0 0 18px 0;
text-align: center;
color: #3730a3;
color: #6366f1;
font-weight: 700;
}
.form {

View File

@ -76,7 +76,7 @@
.selected {
background: #e0e7ff;
color: #3730a3;
color: #6366f1;
}
/* Адаптивные стили для планшетов */

View File

@ -1,7 +1,7 @@
.title {
margin: 0 0 18px 0;
text-align: center;
color: #3730a3;
color: #6366f1;
font-weight: 700;
}
.form {

View File

@ -1,7 +1,7 @@
.title {
margin: 0 0 18px 0;
text-align: center;
color: #3730a3;
color: #6366f1;
font-weight: 700;
}
.form {

View File

@ -1,7 +1,7 @@
.title {
margin: 0 0 18px 0;
text-align: center;
color: #3730a3;
color: #6366f1;
font-weight: 700;
}
.form {

View File

@ -1,7 +1,7 @@
.title {
margin: 0 0 18px 0;
text-align: center;
color: #3730a3;
color: #6366f1;
font-weight: 700;
}
.form {

View File

@ -1,7 +1,7 @@
.title {
margin: 0 0 18px 0;
text-align: center;
color: #3730a3;
color: #6366f1;
font-weight: 700;
}
.form {