Merge pull request 'glist' (#3) from glist into main

Reviewed-on: https://maktraher.ru/enemy01/jopa/pulls/3
This commit is contained in:
enemy01 2024-09-27 04:03:57 +07:00
commit 0d15a82389
10 changed files with 500 additions and 17 deletions

58
package-lock.json generated
View File

@ -22,6 +22,9 @@
"socket.io": "^4.8.0",
"socket.io-client": "^4.8.0",
"web-vitals": "^2.1.4"
},
"devDependencies": {
"sass": "^1.79.3"
}
},
"node_modules/@adobe/css-tools": {
@ -10356,6 +10359,13 @@
"url": "https://opencollective.com/immer"
}
},
"node_modules/immutable": {
"version": "4.3.7",
"resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.7.tgz",
"integrity": "sha512-1hqclzwYwjRDFLjcFxOM5AYkkG0rpFPpr1RLPMEuGczoS7YA8gLhy8SWXYRAA/XwfEHpfo3cw5JGioS32fnMRw==",
"devOptional": true,
"license": "MIT"
},
"node_modules/import-fresh": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
@ -17053,6 +17063,24 @@
"integrity": "sha512-ZRwKbh/eQ6w9vmTjkuG0Ioi3HBwPFce0O+v//ve+aOq1oeCy7jMV2qzzAlpsNuqpqCBjjriM1lbtZbF/Q8jVyA==",
"license": "CC0-1.0"
},
"node_modules/sass": {
"version": "1.79.3",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.79.3.tgz",
"integrity": "sha512-m7dZxh0W9EZ3cw50Me5GOuYm/tVAJAn91SUnohLRo9cXBixGUOdvmryN+dXpwR831bhoY3Zv7rEFt85PUwTmzA==",
"devOptional": true,
"license": "MIT",
"dependencies": {
"chokidar": "^4.0.0",
"immutable": "^4.0.0",
"source-map-js": ">=0.6.2 <2.0.0"
},
"bin": {
"sass": "sass.js"
},
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/sass-loader": {
"version": "12.6.0",
"resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-12.6.0.tgz",
@ -17091,6 +17119,36 @@
}
}
},
"node_modules/sass/node_modules/chokidar": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.1.tgz",
"integrity": "sha512-n8enUVCED/KVRQlab1hr3MVpcVMvxtZjmEa956u+4YijlmQED223XMSYj2tLuKvr4jcCTzNNMpQDUer72MMmzA==",
"devOptional": true,
"license": "MIT",
"dependencies": {
"readdirp": "^4.0.1"
},
"engines": {
"node": ">= 14.16.0"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/sass/node_modules/readdirp": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.0.1.tgz",
"integrity": "sha512-GkMg9uOTpIWWKbSsgwb5fA4EavTR+SG/PMPoAY8hkhHfEEY0/vqljY+XHqtDf2cr2IJtoNRDbrrEpZUiZCkYRw==",
"devOptional": true,
"license": "MIT",
"engines": {
"node": ">= 14.16.0"
},
"funding": {
"type": "individual",
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/sax": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz",

View File

@ -41,5 +41,8 @@
"last 1 firefox version",
"last 1 safari version"
]
},
"devDependencies": {
"sass": "^1.79.3"
}
}

View File

@ -3,19 +3,37 @@ import DeviceGroups from "./pages/DeviceGroups";
import Devices from "./pages/Devices";
import Sidebar from "./Sidebar";
import Dashboard from './pages/Dashboard';
import UserAccount from './pages/UserAccount'; // Импортируем компонент UserAccount
import Login from './pages/Login'; // Импортируем страницу логина
import Connections from './pages/Connections'; // Импортируем страницу подключений
const App = () => {
const [activeTab, setActiveTab] = useState('map'); // По умолчанию активная вкладка "Карта"
const [isAuthenticated, setIsAuthenticated] = useState(false); // Статус авторизации
const handleLogin = () => {
setIsAuthenticated(true); // Устанавливаем авторизацию
};
const handleLogout = () => {
setIsAuthenticated(false); // Сбрасываем авторизацию
};
if (!isAuthenticated) {
return <Login onLogin={handleLogin} />; // Показываем страницу логина
}
return (
<div style={{ display: 'flex' }}>
<Sidebar onSelectTab={setActiveTab} activeTab={activeTab} /> {/* Передаем активную вкладку */}
<div style={{ padding: '20px', flex: 1 }}>
{activeTab === 'map' && <Dashboard />} {/* Подключаем компонент Dashboard */}
{activeTab === 'connection' && <div>Подключение</div>}
{activeTab === 'account' && <div>Аккаунт пользователя</div>}
{activeTab === 'groups' && <DeviceGroups />} {/* Группы устройств */}
{activeTab === 'devices' && <Devices />} {/* Устройства */}
<div style={{ display: 'flex', flexDirection: 'column', height: '100vh' }}>
<div style={{ display: 'flex', flex: 1 }}>
<Sidebar onSelectTab={setActiveTab} activeTab={activeTab} onLogout={handleLogout} /> {/* Передаем onLogout */}
<div style={{ padding: '20px', flex: 1 }}>
{activeTab === 'map' && <Dashboard />} {/* Подключаем компонент Dashboard */}
{activeTab === 'connection' && <Connections />} {/* Страница подключений */}
{activeTab === 'account' && <UserAccount />} {/* Подключаем компонент UserAccount */}
{activeTab === 'groups' && <DeviceGroups />} {/* Группы устройств */}
{activeTab === 'devices' && <Devices />} {/* Устройства */}
</div>
</div>
</div>
);

34
src/Connections.scss Normal file
View File

@ -0,0 +1,34 @@
.section {
margin-bottom: 20px;
form {
input {
margin-right: 10px;
padding: 8px;
border-radius: 4px;
border: 1px solid #ddd;
}
button {
padding: 8px 12px;
background-color: #2980b9;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
&:hover {
background-color: #3498db;
}
}
}
ul {
list-style-type: none;
padding: 0;
li {
margin-top: 10px;
}
}
}

70
src/Login.scss Normal file
View File

@ -0,0 +1,70 @@
// styles/Login.scss
.login-container {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
background-color: #f0f0f0;
.login-box {
background-color: #fff;
padding: 40px;
border-radius: 8px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
max-width: 400px;
width: 100%;
display: flex;
flex-direction: column; // Выровнять элементы по колонке
h2 {
text-align: center;
margin-bottom: 20px;
color: #333;
}
form {
display: flex;
flex-direction: column;
.input-group {
display: flex;
flex-direction: column;
margin-bottom: 20px;
label {
margin-bottom: 8px;
color: #666;
font-size: 14px;
}
input {
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 16px;
transition: border-color 0.3s ease;
&:focus {
outline: none;
border-color: #007bff;
}
}
}
button {
padding: 12px;
background-color: #007bff;
color: #fff;
border: none;
border-radius: 4px;
font-size: 16px;
cursor: pointer;
transition: background-color 0.3s ease;
&:hover {
background-color: #0056b3;
}
}
}
}
}

View File

@ -1,7 +1,14 @@
import React from 'react';
import './Sidebar.css';
import React, { useState } from 'react'; // Ensure useState is imported
import './Sidebar.css'; // Change this to Sidebar.scss if using SCSS
const Sidebar = ({ onSelectTab, activeTab, onLogout }) => {
const [isSubMenuOpen, setIsSubMenuOpen] = useState(false); // For managing dropdown items
const toggleSubMenu = () => {
setIsSubMenuOpen(!isSubMenuOpen);
onSelectTab('account'); // Keep the logic to display the account when clicking on "Аккаунт пользователя"
};
const Sidebar = ({ onSelectTab, activeTab }) => {
return (
<div className="sidebar">
<h2>Меню</h2>
@ -12,14 +19,21 @@ const Sidebar = ({ onSelectTab, activeTab }) => {
<li className={activeTab === 'connection' ? 'active' : ''} onClick={() => onSelectTab('connection')}>
Подключение
</li>
<li className={activeTab === 'account' ? 'active' : ''} onClick={() => onSelectTab('account')}>
<li className={activeTab === 'account' ? 'active' : ''} onClick={toggleSubMenu}>
Аккаунт пользователя
</li>
<li className={activeTab === 'groups' ? 'active' : ''} onClick={() => onSelectTab('groups')}>
Группы устройств
</li>
<li className={activeTab === 'devices' ? 'active' : ''} onClick={() => onSelectTab('devices')}>
Устройства
{isSubMenuOpen && ( // Dropdown menu
<>
<li className={activeTab === 'groups' ? 'active' : ''} onClick={() => onSelectTab('groups')}>
Группы устройств
</li>
<li className={activeTab === 'devices' ? 'active' : ''} onClick={() => onSelectTab('devices')}>
Устройства
</li>
</>
)}
<li className="logout" onClick={onLogout}>
Выйти
</li>
</ul>
</div>

87
src/UserAccount.scss Normal file
View File

@ -0,0 +1,87 @@
.user-account {
padding: 20px;
background-color: #f4f4f4;
border-radius: 10px;
max-width: 400px;
margin: 0 auto;
h1 {
text-align: center;
margin-bottom: 20px;
}
.user-info, .edit-form {
display: flex;
flex-direction: column;
gap: 10px;
label {
display: flex;
justify-content: space-between;
font-size: 16px;
}
input {
padding: 8px;
font-size: 14px;
border: 1px solid #ccc;
border-radius: 5px;
}
p {
font-size: 18px;
margin: 10px 0;
}
.buttons {
display: flex;
justify-content: space-between;
button {
padding: 10px 20px;
font-size: 14px;
cursor: pointer;
border: none;
border-radius: 5px;
background-color: #3498db;
color: white;
transition: background-color 0.3s ease;
&:hover {
background-color: #2980b9;
}
&:last-child {
background-color: #e74c3c;
&:hover {
background-color: #c0392b;
}
}
}
}
}
.edit-button {
margin-top: 20px;
padding: 10px 25px;
font-size: 16px;
font-weight: bold;
cursor: pointer;
background-color: #2ecc71; /* Зелёный цвет */
color: white;
border: none;
border-radius: 5px;
transition: background-color 0.3s ease, transform 0.2s ease;
&:hover {
background-color: #27ae60;
transform: scale(1.05); /* Небольшой зум при наведении */
}
&:active {
background-color: #1e8449;
transform: scale(0.98); /* Легкий уменьшенный эффект при нажатии */
}
}
}

63
src/pages/Connections.js Normal file
View File

@ -0,0 +1,63 @@
import React, { useState } from 'react';
import '../Connections.scss';
const Connections = () => {
const [webhooks, setWebhooks] = useState([]);
const [websocketUrl, setWebsocketUrl] = useState('');
const addWebhook = (newWebhook) => {
setWebhooks([...webhooks, newWebhook]);
};
const handleWebhookSubmit = (e) => {
e.preventDefault();
const newWebhook = {
url: e.target.webhookUrl.value,
groups: e.target.groups.value.split(','),
};
addWebhook(newWebhook);
e.target.reset();
};
return (
<div>
<h2>Подключения</h2>
{/* Webhook подключение */}
<div className="section">
<h3>Подключения Webhooks</h3>
<form onSubmit={handleWebhookSubmit}>
<input type="text" name="webhookUrl" placeholder="URL Webhook" required />
<input type="text" name="groups" placeholder="Группы (через запятую)" required />
<button type="submit">Добавить Webhook</button>
</form>
<ul>
{webhooks.map((webhook, index) => (
<li key={index}>
Webhook: {webhook.url} (Группы: {webhook.groups.join(', ')})
</li>
))}
</ul>
</div>
{/* WebSocket подключение */}
<div className="section">
<h3>Подключения WebSockets</h3>
<form
onSubmit={(e) => {
e.preventDefault();
setWebsocketUrl(e.target.websocketUrl.value);
}}
>
<input type="text" name="websocketUrl" placeholder="URL WebSocket" required />
<button type="submit">Подключиться</button>
</form>
{websocketUrl && <p>Подключено по WebSocket: {websocketUrl}</p>}
</div>
</div>
);
};
export default Connections;

52
src/pages/Login.js Normal file
View File

@ -0,0 +1,52 @@
import React, { useState } from 'react';
import '../Login.scss';
const Login = ({ onLogin }) => {
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState('');
const handleSubmit = (e) => {
e.preventDefault();
// Simple login/password check
if (username === 'admin' && password === 'admin') {
onLogin(); // Call function on successful login
} else {
setError('Неверный логин или пароль');
}
};
return (
<div className="login-container">
<div className="login-box"> {/* Added a wrapper for styling */}
<h2>Вход</h2>
{error && <p className="error">{error}</p>}
<form onSubmit={handleSubmit}>
<div className="input-group">
<label htmlFor="username">Логин</label>
<input
id="username"
type="text"
placeholder="Логин"
value={username}
onChange={(e) => setUsername(e.target.value)}
/>
</div>
<div className="input-group">
<label htmlFor="password">Пароль</label>
<input
id="password"
type="password"
placeholder="Пароль"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
</div>
<button type="submit">Войти</button>
</form>
</div>
</div>
);
};
export default Login;

84
src/pages/UserAccount.js Normal file
View File

@ -0,0 +1,84 @@
import React, { useState } from 'react';
import '../UserAccount.scss';
const UserAccount = () => {
const [userData, setUserData] = useState({
username: 'Мамут Рахал',
email: 'yatupoidayn@mail.ru',
phone: '+666',
});
const [isEditing, setIsEditing] = useState(false);
const [formData, setFormData] = useState(userData);
const handleChange = (e) => {
const { name, value } = e.target;
setFormData((prevData) => ({
...prevData,
[name]: value,
}));
};
const handleSave = () => {
setUserData(formData);
setIsEditing(false);
};
const handleCancel = () => {
setFormData(userData);
setIsEditing(false);
};
return (
<div className="user-account">
<h1>Данные</h1>
{isEditing ? (
<div className="edit-form">
<label>
Имя:
<input
type="text"
name="username"
value={formData.username}
onChange={handleChange}
/>
</label>
<label>
Email:
<input
type="email"
name="email"
value={formData.email}
onChange={handleChange}
/>
</label>
<label>
Телефон:
<input
type="tel"
name="phone"
value={formData.phone}
onChange={handleChange}
/>
</label>
<div className="buttons">
<button onClick={handleSave}>Сохранить</button>
<button onClick={handleCancel}>Отмена</button>
</div>
</div>
) : (
<div className="user-info">
<p><strong>Имя:</strong> {userData.username}</p>
<p><strong>Email:</strong> {userData.email}</p>
<p><strong>Телефон:</strong> {userData.phone}</p>
<button className="edit-button" onClick={() => setIsEditing(true)}>
Редактировать
</button>
</div>
)}
</div>
);
};
export default UserAccount;