SideBar
This commit is contained in:
parent
4fd203d44f
commit
d13c98998f
@ -101,4 +101,3 @@ http2 off;
|
||||
# Custom
|
||||
include /data/nginx/custom/server_proxy[.]conf;
|
||||
}
|
||||
|
||||
|
||||
15
frontend/src/components/Header.js
Normal file
15
frontend/src/components/Header.js
Normal file
@ -0,0 +1,15 @@
|
||||
import React from 'react';
|
||||
import styles from './Header.module.css';
|
||||
|
||||
const Header = ({ user, onLogout }) => {
|
||||
return (
|
||||
<header className={styles.header}>
|
||||
<div className={styles.right}>
|
||||
<span className={styles.user}>{user?.email || 'Аккаунт'}</span>
|
||||
<button className={styles.logout} onClick={onLogout}>Выйти</button>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
};
|
||||
|
||||
export default Header;
|
||||
38
frontend/src/components/Header.module.css
Normal file
38
frontend/src/components/Header.module.css
Normal file
@ -0,0 +1,38 @@
|
||||
.header {
|
||||
width: 100%;
|
||||
height: 64px;
|
||||
background: #fff;
|
||||
border-bottom: 1.5px solid #e5e7eb;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
padding: 0 32px;
|
||||
box-sizing: border-box;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
}
|
||||
.right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 18px;
|
||||
}
|
||||
.user {
|
||||
font-size: 16px;
|
||||
color: #6366f1;
|
||||
font-weight: 600;
|
||||
}
|
||||
.logout {
|
||||
background: linear-gradient(90deg, #6366f1 0%, #06b6d4 100%);
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
padding: 8px 18px;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
.logout:hover {
|
||||
background: linear-gradient(90deg, #3730a3 0%, #06b6d4 100%);
|
||||
}
|
||||
23
frontend/src/components/SideMenu.js
Normal file
23
frontend/src/components/SideMenu.js
Normal file
@ -0,0 +1,23 @@
|
||||
import React from 'react';
|
||||
import styles from './SideMenu.module.css';
|
||||
|
||||
const SideMenu = ({ active, onSelect }) => {
|
||||
return (
|
||||
<aside className={styles.menu}>
|
||||
<div className={styles.project}>CoreSync Marketing</div>
|
||||
<nav className={styles.nav}>
|
||||
<div className={styles.section}>Email-рассылки</div>
|
||||
<ul>
|
||||
<li className={active === 'smtp' ? styles.active : ''} onClick={() => onSelect('smtp')}>SMTP-сервера</li>
|
||||
<li className={active === 'template' ? styles.active : ''} onClick={() => onSelect('template')}>Шаблон письма</li>
|
||||
<li className={active === 'unsubscribed' ? styles.active : ''} onClick={() => onSelect('unsubscribed')}>Отписались</li>
|
||||
<li className={active === 'groups' ? styles.active : ''} onClick={() => onSelect('groups')}>Подписчики и группы</li>
|
||||
<li className={active === 'history' ? styles.active : ''} onClick={() => onSelect('history')}>История отправок</li>
|
||||
<li className={active === 'campaign' ? styles.active : ''} onClick={() => onSelect('campaign')}>Кампания</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</aside>
|
||||
);
|
||||
};
|
||||
|
||||
export default SideMenu;
|
||||
53
frontend/src/components/SideMenu.module.css
Normal file
53
frontend/src/components/SideMenu.module.css
Normal file
@ -0,0 +1,53 @@
|
||||
.menu {
|
||||
width: 250px;
|
||||
min-height: 100vh;
|
||||
background: #f3f4f6;
|
||||
border-right: 1.5px solid #e5e7eb;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0 0 24px 0;
|
||||
}
|
||||
.project {
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
color: #3730a3;
|
||||
padding: 32px 24px 16px 24px;
|
||||
letter-spacing: 1px;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.nav {
|
||||
flex: 1;
|
||||
padding: 0 0 0 0;
|
||||
}
|
||||
.section {
|
||||
font-size: 15px;
|
||||
color: #6366f1;
|
||||
font-weight: 600;
|
||||
padding: 12px 24px 6px 24px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
ul {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0 0 0 0;
|
||||
}
|
||||
li {
|
||||
padding: 12px 24px;
|
||||
font-size: 16px;
|
||||
color: #374151;
|
||||
cursor: pointer;
|
||||
border-left: 3px solid transparent;
|
||||
transition: background 0.15s, border 0.15s, color 0.15s;
|
||||
}
|
||||
li:hover {
|
||||
background: #e0e7ff;
|
||||
color: #3730a3;
|
||||
}
|
||||
.active {
|
||||
background: #e0e7ff;
|
||||
color: #3730a3;
|
||||
border-left: 3px solid #6366f1;
|
||||
font-weight: 600;
|
||||
}
|
||||
42
frontend/src/context/UserContext.js
Normal file
42
frontend/src/context/UserContext.js
Normal file
@ -0,0 +1,42 @@
|
||||
import React, { createContext, useContext, useState, useEffect } from 'react';
|
||||
|
||||
const UserContext = createContext();
|
||||
|
||||
export function UserProvider({ children }) {
|
||||
const [user, setUser] = useState(null);
|
||||
const [token, setToken] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
// Инициализация из localStorage
|
||||
const storedUser = localStorage.getItem('user');
|
||||
const storedToken = localStorage.getItem('token');
|
||||
if (storedUser && storedToken) {
|
||||
setUser(JSON.parse(storedUser));
|
||||
setToken(storedToken);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const login = (user, token) => {
|
||||
setUser(user);
|
||||
setToken(token);
|
||||
localStorage.setItem('user', JSON.stringify(user));
|
||||
localStorage.setItem('token', token);
|
||||
};
|
||||
|
||||
const logout = () => {
|
||||
setUser(null);
|
||||
setToken(null);
|
||||
localStorage.removeItem('user');
|
||||
localStorage.removeItem('token');
|
||||
};
|
||||
|
||||
return (
|
||||
<UserContext.Provider value={{ user, token, login, logout }}>
|
||||
{children}
|
||||
</UserContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useUser() {
|
||||
return useContext(UserContext);
|
||||
}
|
||||
@ -3,11 +3,14 @@ import ReactDOM from 'react-dom/client';
|
||||
import './index.css';
|
||||
import App from './App';
|
||||
import * as serviceWorkerRegistration from './serviceWorkerRegistration';
|
||||
import { UserProvider } from './context/UserContext';
|
||||
|
||||
const root = ReactDOM.createRoot(document.getElementById('root'));
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
<UserProvider>
|
||||
<App />
|
||||
</UserProvider>
|
||||
</React.StrictMode>
|
||||
);
|
||||
|
||||
|
||||
@ -1,12 +1,44 @@
|
||||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import SideMenu from '../components/SideMenu';
|
||||
import Header from '../components/Header';
|
||||
import { useUser } from '../context/UserContext';
|
||||
|
||||
const Dashboard = () => {
|
||||
const [active, setActive] = useState('smtp');
|
||||
const { user, logout } = useUser();
|
||||
|
||||
const handleLogout = () => {
|
||||
logout();
|
||||
window.location.href = '/login';
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '100vh', flexDirection: 'column' }}>
|
||||
<h1>Добро пожаловать в Dashboard!</h1>
|
||||
<p>Вы успешно вошли в систему.</p>
|
||||
<div style={{ display: 'flex', minHeight: '100vh', background: '#f8fafc' }}>
|
||||
<SideMenu active={active} onSelect={setActive} />
|
||||
<div style={{ flex: 1, display: 'flex', flexDirection: 'column' }}>
|
||||
<Header user={user} onLogout={handleLogout} />
|
||||
<div style={{ flex: 1, padding: 32 }}>
|
||||
{/* Здесь будет контент выбранного раздела */}
|
||||
<h2>Раздел: {getSectionTitle(active)}</h2>
|
||||
<div style={{ marginTop: 16, color: '#6b7280' }}>
|
||||
Здесь будет содержимое для "{getSectionTitle(active)}".
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
function getSectionTitle(key) {
|
||||
switch (key) {
|
||||
case 'smtp': return 'SMTP-сервера';
|
||||
case 'template': return 'Шаблон письма';
|
||||
case 'unsubscribed': return 'Отписались';
|
||||
case 'groups': return 'Подписчики и группы';
|
||||
case 'history': return 'История отправок';
|
||||
case 'campaign': return 'Кампания';
|
||||
default: return '';
|
||||
}
|
||||
}
|
||||
|
||||
export default Dashboard;
|
||||
@ -1,6 +1,7 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import styles from './Login.module.css';
|
||||
import { useUser } from '../context/UserContext';
|
||||
|
||||
const Login = () => {
|
||||
const [email, setEmail] = useState('');
|
||||
@ -8,6 +9,7 @@ const Login = () => {
|
||||
const [error, setError] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const navigate = useNavigate();
|
||||
const { login } = useUser();
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
@ -23,8 +25,7 @@ const Login = () => {
|
||||
if (!res.ok) {
|
||||
setError(data.error || 'Ошибка входа');
|
||||
} else {
|
||||
localStorage.setItem('token', data.token);
|
||||
// Можно сохранить и user, если нужно: localStorage.setItem('user', JSON.stringify(data.user));
|
||||
login(data.user, data.token);
|
||||
navigate('/dashboard');
|
||||
}
|
||||
} catch (err) {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user