SideBar
This commit is contained in:
parent
4fd203d44f
commit
d13c98998f
@ -101,4 +101,3 @@ http2 off;
|
|||||||
# Custom
|
# Custom
|
||||||
include /data/nginx/custom/server_proxy[.]conf;
|
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 './index.css';
|
||||||
import App from './App';
|
import App from './App';
|
||||||
import * as serviceWorkerRegistration from './serviceWorkerRegistration';
|
import * as serviceWorkerRegistration from './serviceWorkerRegistration';
|
||||||
|
import { UserProvider } from './context/UserContext';
|
||||||
|
|
||||||
const root = ReactDOM.createRoot(document.getElementById('root'));
|
const root = ReactDOM.createRoot(document.getElementById('root'));
|
||||||
root.render(
|
root.render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<App />
|
<UserProvider>
|
||||||
|
<App />
|
||||||
|
</UserProvider>
|
||||||
</React.StrictMode>
|
</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 Dashboard = () => {
|
||||||
|
const [active, setActive] = useState('smtp');
|
||||||
|
const { user, logout } = useUser();
|
||||||
|
|
||||||
|
const handleLogout = () => {
|
||||||
|
logout();
|
||||||
|
window.location.href = '/login';
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '100vh', flexDirection: 'column' }}>
|
<div style={{ display: 'flex', minHeight: '100vh', background: '#f8fafc' }}>
|
||||||
<h1>Добро пожаловать в Dashboard!</h1>
|
<SideMenu active={active} onSelect={setActive} />
|
||||||
<p>Вы успешно вошли в систему.</p>
|
<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>
|
</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;
|
export default Dashboard;
|
||||||
@ -1,6 +1,7 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import styles from './Login.module.css';
|
import styles from './Login.module.css';
|
||||||
|
import { useUser } from '../context/UserContext';
|
||||||
|
|
||||||
const Login = () => {
|
const Login = () => {
|
||||||
const [email, setEmail] = useState('');
|
const [email, setEmail] = useState('');
|
||||||
@ -8,6 +9,7 @@ const Login = () => {
|
|||||||
const [error, setError] = useState('');
|
const [error, setError] = useState('');
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const { login } = useUser();
|
||||||
|
|
||||||
const handleSubmit = async (e) => {
|
const handleSubmit = async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@ -23,8 +25,7 @@ const Login = () => {
|
|||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
setError(data.error || 'Ошибка входа');
|
setError(data.error || 'Ошибка входа');
|
||||||
} else {
|
} else {
|
||||||
localStorage.setItem('token', data.token);
|
login(data.user, data.token);
|
||||||
// Можно сохранить и user, если нужно: localStorage.setItem('user', JSON.stringify(data.user));
|
|
||||||
navigate('/dashboard');
|
navigate('/dashboard');
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user