This commit is contained in:
romantarkin 2025-07-22 19:14:49 +05:00
parent 4fd203d44f
commit d13c98998f
9 changed files with 214 additions and 8 deletions

View File

@ -101,4 +101,3 @@ http2 off;
# Custom # Custom
include /data/nginx/custom/server_proxy[.]conf; include /data/nginx/custom/server_proxy[.]conf;
} }

View 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;

View 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%);
}

View 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;

View 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;
}

View 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);
}

View File

@ -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>
<UserProvider>
<App /> <App />
</UserProvider>
</React.StrictMode> </React.StrictMode>
); );

View File

@ -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;

View File

@ -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) {