Виправлення помилок.

Додан новий метод авторізації в застосунок.
This commit is contained in:
2026-06-16 16:50:01 +03:00
parent 0b38f137bf
commit 9797ff6137
28 changed files with 2756 additions and 493 deletions
+3 -3
View File
@@ -1,5 +1,5 @@
const CONFIG = { const CONFIG = {
"web": "https://test.sheep-service.com/", "web": "https://sheep-service.com/",
"api": "https://test.sheep-service.com/api/", "api": "https://sheep-service.com/api/",
"wss": "wss://test.sheep-service.com/ws" "wss": "wss://sheep-service.com/ws"
} }
+116 -1
View File
@@ -110,10 +110,11 @@
border: 0; border: 0;
padding: 0; padding: 0;
font-family: 'Roboto', sans-serif; font-family: 'Roboto', sans-serif;
margin: 0; /* margin: 0; */
font-weight: 400; font-weight: 400;
outline: none; outline: none;
-webkit-text-size-adjust: 100%; -webkit-text-size-adjust: 100%;
-webkit-tap-highlight-color: transparent;
} }
*:disabled { *:disabled {
@@ -122,6 +123,7 @@
} }
@media(hover: hover) { @media(hover: hover) {
a:hover, a:hover,
button:hover, button:hover,
select:hover, select:hover,
@@ -175,9 +177,20 @@ a:active {
} }
body { body {
overscroll-behavior-y: none;
background-color: var(--ColorThemes2); background-color: var(--ColorThemes2);
color: var(--ColorThemes3); color: var(--ColorThemes3);
/* transition: .3s ease; */ /* transition: .3s ease; */
margin: 0;
}
h1,
h2,
h3,
h4,
h5,
h6 {
margin: 0;
} }
/* Стили для анимации обновления страници свайпом */ /* Стили для анимации обновления страници свайпом */
@@ -201,6 +214,108 @@ body {
} }
} }
@keyframes slide-in {
from {
opacity: 0;
transform: translateY(-15px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes slide-out {
from {
opacity: 1;
transform: translateY(0);
}
to {
opacity: 0;
transform: translateY(-15);
}
}
dialog {
border: none;
border-radius: 12px;
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2);
background-color: var(--ColorThemes0);
color: var(--ColorThemes3);
animation: slide-in 0.3s ease-out forwards;
}
dialog:open {
box-shadow: 0 8px 40px rgba(227, 182, 0, 0.15);
}
dialog::backdrop {
background: rgba(0, 0, 0, 0.2);
backdrop-filter: blur(3px);
}
dialog>header,
dialog>footer {
padding: 20px;
}
dialog>content {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 0 20px;
}
dialog>header>h1 {
color: var(--ColorThemes3);
font-size: var(--FontSize5);
}
dialog>footer>button {
width: 100%;
border-radius: 5px;
height: 30px;
background: var(--ColorThemes1);
color: var(--ColorThemes3);
font-size: var(--FontSize2);
}
/* Стилизуем само окно */
[popover] {
padding: 24px;
border: none;
border-radius: 12px;
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2);
background-color: var(--ColorThemes2);
color: var(--ColorThemes3);
max-width: 400px;
gap: 10px;
}
/* Стилизуем фон вокруг окна (работает только когда popover открыт) */
[popover]::backdrop {
background: rgba(0, 0, 0, 0.4);
backdrop-filter: blur(3px);
/* Добавляем красивое размытие фона */
}
[popover]>button {
margin-top: 15px;
width: 100%;
border-radius: 5px;
height: 30px;
background: var(--ColorThemes3);
color: var(--ColorThemes0);
font-size: var(--FontSize2);
}
select { select {
-moz-appearance: none; -moz-appearance: none;
-webkit-appearance: none; -webkit-appearance: none;
+97
View File
@@ -0,0 +1,97 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Serverless FaceID Test</title>
<style>
body { font-family: -apple-system, sans-serif; display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100vh; margin: 0; background-color: #f5f5f7; }
.card { background: white; padding: 30px; border-radius: 20px; box-shadow: 0 4px 15px rgba(0,0,0,0.1); text-align: center; max-width: 320px; width: 100%; }
button { background: #0071e3; color: white; border: none; padding: 14px; font-size: 16px; border-radius: 12px; cursor: pointer; margin: 10px 0; width: 100%; font-weight: 500; }
button.secondary { background: #34c759; }
#status { margin-top: 20px; font-size: 14px; color: #86868b; word-break: break-word; }
</style>
</head>
<body>
<div class="card">
<h2>Безсерверный тест</h2>
<p>Проверка вызова Face ID на клиенте</p>
<button id="testFaceIdBtn">Вызвать Face ID</button>
<div id="status">Нажмите кнопку для теста</div>
</div>
<script>
// Нам нужен фейковый challenge в виде ArrayBuffer (минимум 16 байт)
const fakeChallenge = new Uint8Array([
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16
]).buffer;
// Фейковый ID пользователя (тоже ArrayBuffer)
const fakeUserId = new Uint8Array([
99, 98, 97, 96, 95, 94, 93, 92
]).buffer;
const log = (msg, isErr = false) => {
const el = document.getElementById('status');
el.innerText = msg;
el.style.color = isErr ? '#ff3b30' : '#1d1d1f';
};
document.getElementById('testFaceIdBtn').addEventListener('click', async () => {
log('Инициализация API...');
// 1. Проверяем, поддерживает ли вообще браузер/устройство биометрию
if (!window.PublicKeyCredential) {
log('WebAuthn не поддерживается в этом браузере.', true);
return;
}
try {
log('Вызов сканера Face ID...');
// 2. Конфигурируем запрос к системе
const publicKeyCredentialCreationOptions = {
challenge: fakeChallenge,
rp: {
name: "Local Test PWA",
id: window.location.hostname // Автоматически подставит localhost или ваш домен
},
user: {
id: fakeUserId,
name: "[email protected]",
displayName: "Тестовый Пользователь"
},
pubKeyCredParams: [
{ type: "public-key", alg: -7 } // Алгоритм ES256, стандартный для Apple устройств
],
authenticatorSelection: {
authenticatorAttachment: "platform", // Заставляет iOS использовать именно встроенную биометрию
userVerification: "required" // Требует успешного сканирования (FaceID/TouchID/Пин-код)
},
timeout: 60000
};
// 3. Триггерим нативное окно iOS
const credential = await navigator.credentials.create({
publicKey: publicKeyCredentialCreationOptions
});
// Если промис успешно разрешился — Face ID отработал, данные получены
console.log('Успешный ответ от Face ID:', credential);
log('🎉 Успех! Нативное окно Face ID открылось и успешно подтвердило личность.');
} catch (err) {
console.error(err);
// Если пользователь нажал "Отмена" в шторке Face ID, сработает NotAllowedError
if (err.name === 'NotAllowedError') {
log('Действие отменено пользователем или таймаут.', true);
} else {
log('Ошибка: ' + err.message, true);
}
}
});
</script>
</body>
</html>
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+2
View File
@@ -69,6 +69,8 @@
<!-- <script src="/lib/components/metrics.js" defer></script> --> <!-- <script src="/lib/components/metrics.js" defer></script> -->
<script src="/lib/components/qr-code-styling.js" defer></script>
<script src="/lib/components/jsQR.min.js" defer></script>
<script src="/lib/components/clipboard.js" defer></script> <script src="/lib/components/clipboard.js" defer></script>
<script src="/lib/components/colorGroup.js" defer></script> <script src="/lib/components/colorGroup.js" defer></script>
<script src="/lib/components/makeid.js" defer></script> <script src="/lib/components/makeid.js" defer></script>
+10
View File
@@ -105,6 +105,8 @@ window.addEventListener('load', async function () {
editFontStyle(); editFontStyle();
Router.check().listen().delegateLinks(); Router.check().listen().delegateLinks();
clearAppBadge();
} }
}); });
@@ -184,6 +186,14 @@ function applyFontMode(mode) {
}); });
} }
function clearAppBadge() {
if ('clearAppBadge' in navigator) {
navigator.clearAppBadge()
.then(() => console.log('Лічильник на іконці успішно скинутий'))
.catch((error) => console.error('Помилка очищення бейджу:', error));
}
}
if ('serviceWorker' in navigator) { if ('serviceWorker' in navigator) {
let refreshing = false; let refreshing = false;
let updateNode = null; let updateNode = null;
File diff suppressed because one or more lines are too long
+340
View File
@@ -0,0 +1,340 @@
let index_notif = 0;
const Notification = {
_container: null,
_timeout: 5000,
_maxVisible: 5,
init({ timeout = 4000, maxVisible = 5, position = 'top-right' } = {}) {
this._timeout = timeout;
this._maxVisible = maxVisible;
// если уже инициализировано — обновляем настройки
if (!this._container) {
this._container = document.createElement('div');
this._container.className = 'app-notification-container';
this._container.setAttribute('data-position', position);
document.body.appendChild(this._container);
// стили, вставляем один раз
const style = document.createElement('style');
style.textContent = `
.app-notification-container {
position: fixed;
z-index: 9999;
pointer-events: none;
display: flex;
flex-direction: column;
gap: 10px;
padding: 12px;
}
.app-notification-container[data-position="top-right"] { top: 8px; right: 8px; align-items: flex-end; }
.app-notification-container[data-position="top-left"] { top: 8px; left: 8px; align-items: flex-start; }
.app-notification-container[data-position="bottom-right"] { bottom: 8px; right: 8px; align-items: flex-end; }
.app-notification {
pointer-events: auto;
min-width: 220px;
max-width: 360px;
background: #111;
color: #fff;
padding: 10px 12px 10px 12px;
border-radius: var(--border-radius);
box-shadow: 0 6px 18px rgba(0, 0, 0, 0.25);
font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial;
font-size: var(--FontSize2);
line-height: 1.2;
display: flex;
gap: 10px;
align-items: center;
opacity: 0;
transform: translateY(-6px) scale(0.995);
transition: opacity .18s ease, transform .18s ease;
}
.app-notification.show {
opacity: 0.95;
transform: translateY(0) scale(1);
}
.app-notification .icon {
font-size: 18px;
width: 22px;
height: 22px;
display:flex;
align-items:center;
justify-content:center;
border-radius: calc(var(--border-radius) - 5px);
padding: 8px;
}
.app-notification .icon svg{
width: 20px;
height: 20px;
fill: #fff;
}
.app-notification .body { flex:1; }
.app-notification .title { font-weight: 600; margin-bottom: 4px; font-size: 13px; }
.app-notification .blockClose {
width: 20px;
}
.app-notification .blockClose .close {
position: absolute;
right: 10px;
top: 10px;
margin-left: 8px;
background: transparent;
border: none;
color: inherit;
cursor: pointer;
}
.app-notification .blockClose .close svg {
width: 15px;
height: 15px;
fill: #fff;
opacity: 0.8;
}
.app-notification.info {
background: var(--ColorThemes3);
color: var(--ColorThemes0);
}
.app-notification.info .icon { background: var(--ColorThemes0); }
.app-notification.info .icon svg{fill: var(--ColorThemes3);}
.app-notification.info .close svg{fill: var(--ColorThemes0);}
.app-notification.success { background: #52ac56; }
.app-notification.success .icon { background: #6dc450; }
.app-notification.warn { background: #d18515; }
.app-notification.warn .icon { background: #eaad57; }
.app-notification.error { background: #9c2424; }
.app-notification.error .icon { background: #c45050; }
@media (max-width: 700px) {
.app-notification-container {
width: calc(100% - 33px);
}
.app-notification {
width: calc(100% - 30px);
border-radius: var(--border-radius);
}
}
`;
document.head.appendChild(style);
} else {
this._container.setAttribute('data-position', position);
}
},
/**
* show(message, options)
* message: string or { title, text }
* options: { type: 'info'|'success'|'warn'|'error', timeout: ms, title: string }
*/
// show(message, options = {}) {
// if (!this._container) this.init();
// const { type = 'info', timeout = this._timeout, title } = options;
// const content = typeof message === 'string'
// ? { title: title || '', text: message }
// : message;
// // 🔥 УДАЛЯЕМ ЛИШНИЕ СРАЗУ
// while (this._container.children.length >= this._maxVisible) {
// const first = this._container.firstElementChild;
// if (first) first.remove(); // ← никаких переходов и setTimeout
// else break;
// }
// const node = document.createElement('div');
// node.className = `app-notification ${type}`;
// // иконка (можно менять на svg)
// const icons = { info: '️', success: '✅', warn: '⚠️', error: '❌' };
// const icon = document.createElement('div');
// icon.className = 'icon';
// icon.innerHTML = `<div>${icons[type] || icons.info}</div>`;
// const body = document.createElement('div');
// body.className = 'body';
// if (content.title) {
// const t = document.createElement('div');
// t.className = 'title';
// t.textContent = content.title;
// body.appendChild(t);
// }
// const txt = document.createElement('div');
// txt.className = 'text';
// txt.textContent = content.text || '';
// body.appendChild(txt);
// const closeBtn = document.createElement('button');
// closeBtn.className = 'close';
// closeBtn.setAttribute('aria-label', 'Закрыть уведомление');
// closeBtn.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 26 26"><path d="M 6.65625 4 C 6.367188 4 6.105469 4.113281 5.90625 4.3125 L 4.3125 5.90625 C 3.914063 6.304688 3.914063 7 4.3125 7.5 L 9.8125 13 L 4.3125 18.5 C 3.914063 19 3.914063 19.695313 4.3125 20.09375 L 5.90625 21.6875 C 6.40625 22.085938 7.101563 22.085938 7.5 21.6875 L 13 16.1875 L 18.5 21.6875 C 19 22.085938 19.695313 22.085938 20.09375 21.6875 L 21.6875 20.09375 C 22.085938 19.59375 22.085938 18.898438 21.6875 18.5 L 16.1875 13 L 21.6875 7.5 C 22.085938 7 22.085938 6.304688 21.6875 5.90625 L 20.09375 4.3125 C 19.59375 3.914063 18.898438 3.914063 18.5 4.3125 L 13 9.8125 L 7.5 4.3125 C 7.25 4.113281 6.945313 4 6.65625 4 Z"></path></svg>';
// node.appendChild(icon);
// node.appendChild(body);
// node.appendChild(closeBtn);
// // вставляем в контейнер в конец (новые сверху — ставим в конец, контейнер flex column)
// this._container.appendChild(node);
// requestAnimationFrame(() => node.classList.add('show'));
// let timer = null;
// const startTimer = () => {
// if (timeout === 0) return;
// timer = setTimeout(() => this._removeNode(node), timeout);
// };
// const clearTimer = () => { if (timer) { clearTimeout(timer); timer = null; } };
// node.addEventListener('mouseenter', clearTimer);
// node.addEventListener('mouseleave', startTimer);
// node.querySelector('.close').addEventListener('click', () => {
// this._removeNode(node);
// });
// startTimer();
// return node;
// },
// удалить ноду с анимацией
show(message, options = {}) {
if (!this._container) this.init();
const {
type = 'info',
timeout = this._timeout,
title,
onClick,
lock
} = options;
const content = typeof message === 'string'
? { title: title || '', text: message }
: message;
// Удаляем лишние уведомления
while (this._container.children.length >= this._maxVisible) {
const first = this._container.firstElementChild;
if (first) first.remove();
else break;
}
const node = document.createElement('div');
node.className = `app-notification ${type}`;
if (onClick) node.style.cursor = "pointer"
// Иконка
const icons = {
info: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 30 30"><path d="M 15 3 C 13.895 3 13 3.895 13 5 L 13 5.2929688 C 10.109011 6.1538292 8 8.8293311 8 12 L 8 14.757812 C 8 17.474812 6.921 20.079 5 22 A 1 1 0 0 0 4 23 A 1 1 0 0 0 5 24 L 25 24 A 1 1 0 0 0 26 23 A 1 1 0 0 0 25 22 C 23.079 20.079 22 17.474812 22 14.757812 L 22 12 C 22 8.8293311 19.890989 6.1538292 17 5.2929688 L 17 5 C 17 3.895 16.105 3 15 3 z M 3.9550781 7.9882812 A 1.0001 1.0001 0 0 0 3.1054688 8.5527344 C 3.1054688 8.5527344 2 10.666667 2 13 C 2 15.333333 3.1054687 17.447266 3.1054688 17.447266 A 1.0001165 1.0001165 0 0 0 4.8945312 16.552734 C 4.8945312 16.552734 4 14.666667 4 13 C 4 11.333333 4.8945313 9.4472656 4.8945312 9.4472656 A 1.0001 1.0001 0 0 0 3.9550781 7.9882812 z M 26.015625 7.9882812 A 1.0001 1.0001 0 0 0 25.105469 9.4472656 C 25.105469 9.4472656 26 11.333333 26 13 C 26 14.666667 25.105469 16.552734 25.105469 16.552734 A 1.0001163 1.0001163 0 1 0 26.894531 17.447266 C 26.894531 17.447266 28 15.333333 28 13 C 28 10.666667 26.894531 8.5527344 26.894531 8.5527344 A 1.0001 1.0001 0 0 0 26.015625 7.9882812 z M 12 26 C 12 27.657 13.343 29 15 29 C 16.657 29 18 27.657 18 26 L 12 26 z"/></svg>`,
success: `<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8' style="width: 17px;height: 17px;"><path d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/></svg>`,
warn: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 30 30"> <path d="M 15 3 C 14.168432 3 13.456063 3.5067238 13.154297 4.2285156 L 2.3007812 22.947266 L 2.3007812 22.949219 A 2 2 0 0 0 2 24 A 2 2 0 0 0 4 26 A 2 2 0 0 0 4.140625 25.994141 L 4.1445312 26 L 15 26 L 25.855469 26 L 25.859375 25.992188 A 2 2 0 0 0 26 26 A 2 2 0 0 0 28 24 A 2 2 0 0 0 27.699219 22.947266 L 27.683594 22.919922 A 2 2 0 0 0 27.681641 22.917969 L 16.845703 4.2285156 C 16.543937 3.5067238 15.831568 3 15 3 z M 13.787109 11.359375 L 16.212891 11.359375 L 16.011719 17.832031 L 13.988281 17.832031 L 13.787109 11.359375 z M 15.003906 19.810547 C 15.825906 19.810547 16.318359 20.252813 16.318359 21.007812 C 16.318359 21.748812 15.825906 22.189453 15.003906 22.189453 C 14.175906 22.189453 13.679688 21.748813 13.679688 21.007812 C 13.679688 20.252813 14.174906 19.810547 15.003906 19.810547 z"/></svg>`,
error: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 30 30"><path d="M 10.03125 2.9042969 A 1.0001 1.0001 0 0 0 9.0605469 4.2578125 C 9.0605469 4.2578125 9.7494688 5.9996671 11.488281 7.0800781 C 11.080693 7.2039808 10.414387 7.5446908 9.8027344 8.4570312 C 9.4322834 8.3403526 9.1037156 8.2185154 8.8496094 8.0976562 C 8.3997876 7.8837103 8.118603 7.6807271 7.9589844 7.546875 C 7.7993657 7.4130229 7.8046875 7.40625 7.8046875 7.40625 A 1.0001 1.0001 0 0 0 7 7 L 5 7 A 1.0001 1.0001 0 1 0 5 9 L 6.5957031 9 C 6.6368531 9.038228 6.6271651 9.038995 6.6738281 9.078125 C 6.961397 9.3192729 7.3888062 9.6162897 7.9902344 9.9023438 C 9.1930908 10.474451 11.083447 11 13.935547 11 A 1.0001 1.0001 0 0 0 14.140625 10.980469 C 14.430223 10.987386 14.697172 11 15.017578 11 C 15.326932 11 15.582943 10.988887 15.863281 10.982422 A 1.0001 1.0001 0 0 0 16.064453 11 C 18.916553 11 20.806909 10.474451 22.009766 9.9023438 C 22.611194 9.6162897 23.038603 9.3192729 23.326172 9.078125 C 23.372834 9.0389949 23.363147 9.0382279 23.404297 9 L 25 9 A 1.0001 1.0001 0 1 0 25 7 L 23 7 A 1.0001 1.0001 0 0 0 22.195312 7.40625 C 22.195312 7.40625 22.200612 7.41302 22.041016 7.546875 C 21.881397 7.6807271 21.600212 7.8837103 21.150391 8.0976562 C 20.891444 8.2208175 20.55751 8.3444389 20.177734 8.4628906 C 19.558423 7.539139 18.907199 7.1978378 18.517578 7.0761719 C 20.252095 5.9954925 20.939453 4.2578125 20.939453 4.2578125 A 1.0001 1.0001 0 0 0 20.039062 2.9042969 A 1.0001 1.0001 0 0 0 19.060547 3.5761719 C 19.060547 3.5761719 18.556779 5.088719 16.882812 5.6757812 C 16.36708 5.2573881 15.71568 5 15 5 C 14.284868 5 13.632808 5.2576596 13.117188 5.6757812 C 11.443221 5.088719 10.939453 3.5761719 10.939453 3.5761719 A 1.0001 1.0001 0 0 0 10.03125 2.9042969 z M 5.9628906 11 A 1.0001 1.0001 0 0 0 5.6835938 11.050781 L 2.6835938 12.050781 A 1.0005646 1.0005646 0 0 0 3.3164062 13.949219 L 5.9707031 13.064453 C 6.0672386 13.111686 6.3494962 13.235909 6.5917969 13.34375 C 6.2888038 14.107541 6 15.15686 6 16.425781 C 6 17.948961 6.3086267 19.289595 6.7949219 20.453125 L 6.1601562 22.357422 L 3.4453125 24.167969 A 1.0001 1.0001 0 1 0 4.5546875 25.832031 L 7.5546875 23.832031 A 1.0001 1.0001 0 0 0 7.9492188 23.316406 L 8.1367188 22.751953 C 9.8000084 24.902319 11.988204 26 13 26 C 13.742 26 14 25.42 14 25 L 14 16 C 14 15.447 14.448 15 15 15 C 15.552 15 16 15.447 16 16 L 16 25 C 16 25.42 16.258 26 17 26 C 18.011796 26 20.199992 24.902319 21.863281 22.751953 L 22.050781 23.316406 A 1.0001 1.0001 0 0 0 22.445312 23.832031 L 25.445312 25.832031 A 1.0001 1.0001 0 1 0 26.554688 24.167969 L 23.839844 22.357422 L 23.205078 20.453125 C 23.691373 19.289595 24 17.948961 24 16.425781 C 24 15.15686 23.711196 14.107541 23.408203 13.34375 C 23.650504 13.235909 23.932762 13.111686 24.029297 13.064453 L 26.683594 13.949219 A 1.0005646 1.0005646 0 1 0 27.316406 12.050781 L 24.316406 11.050781 A 1.0001 1.0001 0 0 0 24.021484 11 A 1.0001 1.0001 0 0 0 23.552734 11.105469 C 23.552734 11.105469 20.660591 12.508607 17.513672 12.896484 C 16.740098 12.956149 15.915495 13 15 13 C 14.084505 13 13.259902 12.956149 12.486328 12.896484 C 9.3394093 12.508607 6.4472656 11.105469 6.4472656 11.105469 A 1.0001 1.0001 0 0 0 5.9628906 11 z"/></svg>`
};
const icon = document.createElement('div');
icon.className = 'icon';
icon.innerHTML = icons[type] || icons.info;
// Тело уведомления
const body = document.createElement('div');
body.className = 'body';
if (content.title) {
const t = document.createElement('div');
t.className = 'title';
t.textContent = content.title;
body.appendChild(t);
}
const txt = document.createElement('div');
txt.className = 'text';
txt.textContent = content.text || '';
body.appendChild(txt);
node.appendChild(icon);
node.appendChild(body);
// Кнопка закрытия, только если onClick не задан
if (!onClick && !lock) {
const closeDiv = document.createElement('div');
closeDiv.className = 'blockClose';
node.appendChild(closeDiv);
const closeBtn = document.createElement('button');
closeBtn.className = 'close';
closeBtn.setAttribute('aria-label', 'Закрыть уведомление');
closeBtn.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 26 26"><path d="M 6.65625 4 C 6.367188 4 6.105469 4.113281 5.90625 4.3125 L 4.3125 5.90625 C 3.914063 6.304688 3.914063 7 4.3125 7.5 L 9.8125 13 L 4.3125 18.5 C 3.914063 19 3.914063 19.695313 4.3125 20.09375 L 5.90625 21.6875 C 6.40625 22.085938 7.101563 22.085938 7.5 21.6875 L 13 16.1875 L 18.5 21.6875 C 19 22.085938 19.695313 22.085938 20.09375 21.6875 L 21.6875 20.09375 C 22.085938 19.59375 22.085938 18.898438 21.6875 18.5 L 16.1875 13 L 21.6875 7.5 C 22.085938 7 22.085938 6.304688 21.6875 5.90625 L 20.09375 4.3125 C 19.59375 3.914063 18.898438 3.914063 18.5 4.3125 L 13 9.8125 L 7.5 4.3125 C 7.25 4.113281 6.945313 4 6.65625 4 Z"></path></svg>';
closeDiv.appendChild(closeBtn);
closeBtn.addEventListener('click', () => this._removeNode(node));
}
this._container.appendChild(node);
requestAnimationFrame(() => node.classList.add('show'));
let timer = null;
const startTimer = () => {
if (timeout === 0) return;
timer = setTimeout(() => this._removeNode(node), timeout);
};
const clearTimer = () => { if (timer) { clearTimeout(timer); timer = null; } };
node.addEventListener('mouseenter', clearTimer);
node.addEventListener('mouseleave', startTimer);
// Обработчик клика для onClick
if (typeof onClick === 'function') {
node.addEventListener('click', () => {
try { onClick(); } catch (e) { }
this._removeNode(node);
});
}
startTimer();
return node;
},
_removeNode(node) {
if (!node || !node.parentElement) return;
node.classList.remove('show');
// дождемся конца transition (примерно 180ms), потом удалим
setTimeout(() => {
if (node && node.parentElement) node.parentElement.removeChild(node);
}, 200);
},
// очистить все уведомления
clearAll() {
if (!this._container) return;
Array.from(this._container.children).forEach(n => this._removeNode(n));
},
// удобные обёртки
info(message, opts = {}) { return this.show(message, { ...opts, type: 'info' }); },
success(message, opts = {}) { return this.show(message, { ...opts, type: 'success' }); },
warn(message, opts = {}) { return this.show(message, { ...opts, type: 'warn' }); },
error(message, opts = {}) { return this.show(message, { ...opts, type: 'error' }); },
click(message, opts = {}) { return this.show(message, { ...opts, onClick: opts.f }); },
// изменить глобальные настройки во время работы
configure({ timeout, maxVisible, position } = {}) {
if (typeof timeout === 'number') this._timeout = timeout;
if (typeof maxVisible === 'number') this._maxVisible = maxVisible;
if (position && this._container) this._container.setAttribute('data-position', position);
}
};
// Пример использования:
// Notification.init({ timeout: 4000, maxVisible: 4 });
// Notification.success('Готово!', { title: 'Успех' }, { timeout: 8000 });
// Notification.error({ title: 'Ошибка', text: 'Не удалось сохранить' }, { timeout: 8000 });
// Notification.info('Информационное сообщение');
// Notification.clearAll();
// Notification.success({ title: `Metrics`, text: `З'єднання встановлено` }, { timeout: 0 });
// Notification.error({ title: `Metrics`, text: `З'єднання встановлено` }, { timeout: 0 });
// Notification.info({ title: `Metrics`, text: `lore` }, { timeout: 0 });
// Notification.warn({ title: `Metrics`, text: `З'єднання встановлено` }, { timeout: 0 });
// Notification.click({ title: `Test`, text: `Натисни, щоб перезавантажити` }, { type: 'info', f: () => Notification.clearAll(), timeout: 0 });
File diff suppressed because one or more lines are too long
+40
View File
@@ -0,0 +1,40 @@
// Скрипт перезагрузки страници свайпом.
let app_scroll = false;
let animID = document.getElementById('swipe_updater');
let animIconID = document.getElementById('swipe_icon');
window.addEventListener('scroll', function(e) {
if (window.matchMedia('(display-mode: standalone)').matches) {
let a = window.scrollY;
let b = 50;
let c = 125;
a = -a;
a = a;
animIconID.style.top = a/1.5;
console.log(window.scrollY);
if(window.scrollY <= -10){
animID.style.zIndex = 115;
} else {
animID.style.zIndex = 0;
}
if(window.scrollY <= -120){
if(app_scroll == false){
app_scroll = true;
animIconID.style.transform = 'rotate(180deg)';
animIconID.setAttribute('data-state', '')
}
} else if(window.scrollY >= 0){
if(app_scroll == true){
appReload();
app_scroll = false;
animIconID.style.transform = 'rotate(0deg)';
animIconID.setAttribute('data-state', 'active')
}
}
}
});
+408
View File
@@ -0,0 +1,408 @@
/**
* Клас AppNotificationContainer
* Веб-компонент для відображення системних сповіщень.
* Використовує Shadow DOM для інкапсуляції стилів.
*/
class AppNotificationContainer extends HTMLElement {
constructor() {
super();
// Створюємо Shadow DOM
this.attachShadow({ mode: 'open' });
// Налаштування за замовчуванням
this._timeout = 4000; // Час до автоматичного закриття (мс)
this._maxVisible = 5; // Максимальна кількість одночасно видимих сповіщень
this._position = 'top-right'; // Позиція за замовчуванням
this._mobileBottomEnabled = false; // Чи застосовувати позицію 'bottom' на мобільних пристроях
// Створюємо основний контейнер для сповіщень
this._container = document.createElement('div');
this._container.className = 'app-notification-container';
this.shadowRoot.appendChild(this._container);
// Вставляємо стилі
this._insertStyles();
// SVG іконки для різних типів сповіщень
this._icons = {
info: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 30 30"><path d="M 15 3 C 13.895 3 13 3.895 13 5 L 13 5.2929688 C 10.109011 6.1538292 8 8.8293311 8 12 L 8 14.757812 C 8 17.474812 6.921 20.079 5 22 A 1 1 0 0 0 4 23 A 1 1 0 0 0 5 24 L 25 24 A 1 1 0 0 0 26 23 A 1 1 0 0 0 25 22 C 23.079 20.079 22 17.474812 22 14.757812 L 22 12 C 22 8.8293311 19.890989 6.1538292 17 5.2929688 L 17 5 C 17 3.895 16.105 3 15 3 z M 3.9550781 7.9882812 A 1.0001 1.0001 0 0 0 3.1054688 8.5527344 C 3.1054688 8.5527344 2 10.666667 2 13 C 2 15.333333 3.1054687 17.447266 3.1054688 17.447266 A 1.0001165 1.0001165 0 0 0 4.8945312 16.552734 C 4.8945312 16.552734 4 14.666667 4 13 C 4 11.333333 4.8945313 9.4472656 4.8945312 9.4472656 A 1.0001 1.0001 0 0 0 3.9550781 7.9882812 z M 26.015625 7.9882812 A 1.0001 1.0001 0 0 0 25.105469 9.4472656 C 25.105469 9.4472656 26 11.333333 26 13 C 26 14.666667 25.105469 16.552734 25.105469 16.552734 A 1.0001163 1.0001163 0 1 0 26.894531 17.447266 C 26.894531 17.447266 28 15.333333 28 13 C 28 10.666667 26.894531 8.5527344 26.894531 8.5527344 A 1.0001 1.0001 0 0 0 26.015625 7.9882812 z M 12 26 C 12 27.657 13.343 29 15 29 C 16.657 29 18 27.657 18 26 L 12 26 z"/></svg>`,
success: `<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8' style="width: 17px;height: 17px;"><path d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/></svg>`,
warn: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 30 30"> <path d="M 15 3 C 14.168432 3 13.456063 3.5067238 13.154297 4.2285156 L 2.3007812 22.947266 L 2.3007812 22.949219 A 2 2 0 0 0 2 24 A 2 2 0 0 0 4 26 A 2 2 0 0 0 4.140625 25.994141 L 4.1445312 26 L 15 26 L 25.855469 26 L 25.859375 25.992188 A 2 2 0 0 0 26 26 A 2 2 0 0 0 28 24 A 2 2 0 0 0 27.699219 22.947266 L 27.683594 22.919922 A 2 2 0 0 0 27.681641 22.917969 L 16.845703 4.2285156 C 16.543937 3.5067238 15.831568 3 15 3 z M 13.787109 11.359375 L 16.212891 11.359375 L 16.011719 17.832031 L 13.988281 17.832031 L 13.787109 11.359375 z M 15.003906 19.810547 C 15.825906 19.810547 16.318359 20.252813 16.318359 21.007812 C 16.318359 21.748812 15.825906 22.189453 15.003906 22.189453 C 14.175906 22.189453 13.679688 21.748813 13.679688 21.007812 C 13.679688 20.252813 14.174906 19.810547 15.003906 19.810547 z"/></svg>`,
error: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 30 30"><path d="M 10.03125 2.9042969 A 1.0001 1.0001 0 0 0 9.0605469 4.2578125 C 9.0605469 4.2578125 9.7494688 5.9996671 11.488281 7.0800781 C 11.080693 7.2039808 10.414387 7.5446908 9.8027344 8.4570312 C 9.4322834 8.3403526 9.1037156 8.2185154 8.8496094 8.0976562 C 8.3997876 7.8837103 8.118603 7.6807271 7.9589844 7.546875 C 7.7993657 7.4130229 7.8046875 7.40625 7.8046875 7.40625 A 1.0001 1.0001 0 0 0 7 7 L 5 7 A 1.0001 1.0001 0 1 0 5 9 L 6.5957031 9 C 6.6368531 9.038228 6.6271651 9.038995 6.6738281 9.078125 C 6.961397 9.3192729 7.3888062 9.6162897 7.9902344 9.9023438 C 9.1930908 10.474451 11.083447 11 13.935547 11 A 1.0001 1.0001 0 0 0 14.140625 10.980469 C 14.430223 10.987386 14.697172 11 15.017578 11 C 15.326932 11 15.582943 10.988887 15.863281 10.982422 A 1.0001 1.0001 0 0 0 16.064453 11 C 18.916553 11 20.806909 10.474451 22.009766 9.9023438 C 22.611194 9.6162897 23.038603 9.3192729 23.326172 9.078125 C 23.372834 9.0389949 23.363147 9.0382279 23.404297 9 L 25 9 A 1.0001 1.0001 0 1 0 25 7 L 23 7 A 1.0001 1.0001 0 0 0 22.195312 7.40625 C 22.195312 7.40625 22.200612 7.41302 22.041016 7.546875 C 21.881397 7.6807271 21.600212 7.8837103 21.150391 8.0976562 C 20.891444 8.2208175 20.55751 8.3444389 20.177734 8.4628906 C 19.558423 7.539139 18.907199 7.1978378 18.517578 7.0761719 C 20.252095 5.9954925 20.939453 4.2578125 20.939453 4.2578125 A 1.0001 1.0001 0 0 0 20.039062 2.9042969 A 1.0001 1.0001 0 0 0 19.060547 3.5761719 C 19.060547 3.5761719 18.556779 5.088719 16.882812 5.6757812 C 16.36708 5.2573881 15.71568 5 15 5 C 14.284868 5 13.632808 5.2576596 13.117188 5.6757812 C 11.443221 5.088719 10.939453 3.5761719 10.939453 3.5761719 A 1.0001 1.0001 0 0 0 10.03125 2.9042969 z M 5.9628906 11 A 1.0001 1.0001 0 0 0 5.6835938 11.050781 L 2.6835938 12.050781 A 1.0005646 1.0005646 0 0 0 3.3164062 13.949219 L 5.9707031 13.064453 C 6.0672386 13.111686 6.3494962 13.235909 6.5917969 13.34375 C 6.2888038 14.107541 6 15.15686 6 16.425781 C 6 17.948961 6.3086267 19.289595 6.7949219 20.453125 L 6.1601562 22.357422 L 3.4453125 24.167969 A 1.0001 1.0001 0 1 0 4.5546875 25.832031 L 7.5546875 23.832031 A 1.0001 1.0001 0 0 0 7.9492188 23.316406 L 8.1367188 22.751953 C 9.8000084 24.902319 11.988204 26 13 26 C 13.742 26 14 25.42 14 25 L 14 16 C 14 15.447 14.448 15 15 15 C 15.552 15 16 15.447 16 16 L 16 25 C 16 25.42 16.258 26 17 26 C 18.011796 26 20.199992 24.902319 21.863281 22.751953 L 22.050781 23.316406 A 1.0001 1.0001 0 0 0 22.445312 23.832031 L 25.445312 25.832031 A 1.0001 1.0001 0 1 0 26.554688 24.167969 L 23.839844 22.357422 L 23.205078 20.453125 C 23.691373 19.289595 24 17.948961 24 16.425781 C 24 15.15686 23.711196 14.107541 23.408203 13.34375 C 23.650504 13.235909 23.932762 13.111686 24.029297 13.064453 L 26.683594 13.949219 A 1.0005646 1.0005646 0 1 0 27.316406 12.050781 L 24.316406 11.050781 A 1.0001 1.0001 0 0 0 24.021484 11 A 1.0001 1.0001 0 0 0 23.552734 11.105469 C 23.552734 11.105469 20.660591 12.508607 17.513672 12.896484 C 16.740098 12.956149 15.915495 13 15 13 C 14.084505 13 13.259902 12.956149 12.486328 12.896484 C 9.3394093 12.508607 6.4472656 11.105469 6.4472656 11.105469 A 1.0001 1.0001 0 0 0 5.9628906 11 z"/></svg>`
};
}
/**
* Повертає список атрибутів, які слід відстежувати на предмет змін.
*/
static get observedAttributes() {
return ['timeout', 'max-visible', 'position', 'mobile-position'];
}
/**
* Викликається, коли елемент додається в DOM.
*/
connectedCallback() {
this._updateSettings();
}
/**
* Викликається при зміні одного з відстежуваних атрибутів.
*/
attributeChangedCallback(name, oldValue, newValue) {
if (oldValue !== newValue) {
this._updateSettings();
}
}
/**
* Оновлює внутрішні налаштування на основі атрибутів елемента.
*/
_updateSettings() {
this._position = this.getAttribute('position') || 'top-right';
this._maxVisible = parseInt(this.getAttribute('max-visible')) || 5;
this._timeout = parseInt(this.getAttribute('timeout')) || 4000;
const mobilePosAttr = this.getAttribute('mobile-position');
// Якщо 'mobile-position' встановлено (навіть без значення) або 'bottom'
this._mobileBottomEnabled = mobilePosAttr === 'bottom' || mobilePosAttr === '';
this._container.setAttribute('data-position', this._position);
this._applyMobileStyles();
}
/**
* Застосовує CSS-клас для мобільної позиції.
*/
_applyMobileStyles() {
if (this._mobileBottomEnabled) {
this._container.classList.add('mobile-bottom');
} else {
this._container.classList.remove('mobile-bottom');
}
}
/**
* Програмно встановлює або вимикає примусову нижню позицію на мобільних.
* @param {boolean} enable - true для ввімкнення, false для вимкнення.
*/
setMobileBottom(enable) {
this._mobileBottomEnabled = !!enable;
this._applyMobileStyles();
}
/**
* Показує нове сповіщення.
* @param {string | {title?: string, text: string}} message - Текст або об'єкт сповіщення.
* @param {object} options - Налаштування сповіщення.
* @returns {HTMLElement} Створений елемент сповіщення.
*/
show(message, options = {}) {
const {
type = 'info', // Тип: 'info', 'success', 'warn', 'error'
timeout = this._timeout, // Час зникнення
title,
onClick, // Функція при кліку
lock // Якщо true, не зникає і не має кнопки закриття
} = options;
const content = typeof message === 'string'
? { title: title || '', text: message }
: message;
// Обмеження кількості: видаляємо найстаріше сповіщення (FIFO)
while (this._container.children.length >= this._maxVisible) {
const first = this._container.firstElementChild;
if (first) this._removeNode(first);
else break;
}
// Створення DOM елементів
const node = document.createElement('div');
node.className = `app-notification ${type}`;
if (onClick) node.style.cursor = "pointer";
const icon = document.createElement('div');
icon.className = 'icon';
icon.innerHTML = this._icons[type] || this._icons.info;
const body = document.createElement('div');
body.className = 'body';
if (content.title) {
const t = document.createElement('div');
t.className = 'title';
t.textContent = content.title;
body.appendChild(t);
}
const txt = document.createElement('div');
txt.className = 'text';
txt.textContent = content.text || '';
body.appendChild(txt);
node.appendChild(icon);
node.appendChild(body);
// Додаємо кнопку закриття, якщо немає обробника кліку і не заблоковано
if (!onClick && !lock) {
const closeDiv = document.createElement('div');
closeDiv.className = 'blockClose';
node.appendChild(closeDiv);
const closeBtn = document.createElement('button');
closeBtn.className = 'close';
closeBtn.setAttribute('aria-label', 'Закрити повідомлення');
closeBtn.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 26 26"><path d="M 6.65625 4 C 6.367188 4 6.105469 4.113281 5.90625 4.3125 L 4.3125 5.90625 C 3.914063 6.304688 3.914063 7 4.3125 7.5 L 9.8125 13 L 4.3125 18.5 C 3.914063 19 3.914063 19.695313 4.3125 20.09375 L 5.90625 21.6875 C 6.40625 22.085938 7.101563 22.085938 7.5 21.6875 L 13 16.1875 L 18.5 21.6875 C 19 22.085938 19.695313 22.085938 20.09375 21.6875 L 21.6875 20.09375 C 22.085938 19.59375 22.085938 18.898438 21.6875 18.5 L 16.1875 13 L 21.6875 7.5 C 22.085938 7 22.085938 6.304688 21.6875 5.90625 L 20.09375 4.3125 C 19.59375 3.914063 18.898438 3.914063 18.5 4.3125 L 13 9.8125 L 7.5 4.3125 C 7.25 4.113281 6.945313 4 6.65625 4 Z"></path></svg>';
closeDiv.appendChild(closeBtn);
closeBtn.addEventListener('click', () => this._removeNode(node));
}
this._container.appendChild(node);
// Запускаємо анімацію появи через requestAnimationFrame
requestAnimationFrame(() => node.classList.add('show'));
let timer = null;
const startTimer = () => {
if (timeout === 0 || lock) return; // Ігноруємо таймаут, якщо 0 або lock
timer = setTimeout(() => this._removeNode(node), timeout);
};
const clearTimer = () => { if (timer) { clearTimeout(timer); timer = null; } };
// Зупинка таймауту при наведенні
node.addEventListener('mouseenter', clearTimer);
node.addEventListener('mouseleave', startTimer);
// Обробка кліку на сповіщенні
if (typeof onClick === 'function') {
node.addEventListener('click', () => {
try { onClick(); } catch (e) { console.error(e); }
this._removeNode(node); // Закриваємо після виконання функції
});
}
startTimer();
return node;
}
/**
* Приватний метод для видалення ноди з анімацією.
*/
_removeNode(node) {
if (!node || !node.parentElement) return;
node.classList.remove('show');
// Чекаємо завершення анімації зникнення (200мс)
setTimeout(() => {
if (node && node.parentElement) node.parentElement.removeChild(node);
}, 200);
}
/**
* Видаляє всі видимі сповіщення.
*/
clearAll() {
if (!this._container) return;
Array.from(this._container.children).forEach(n => this._removeNode(n));
}
// Допоміжні методи з фіксованим типом сповіщення
info(message, opts = {}) { return this.show(message, { ...opts, type: 'info' }); }
success(message, opts = {}) { return this.show(message, { ...opts, type: 'success' }); }
warn(message, opts = {}) { return this.show(message, { ...opts, type: 'warn' }); }
error(message, opts = {}) { return this.show(message, { ...opts, type: 'error' }); }
// Метод для сповіщень, що реагують на клік (псевдонім 'click' для 'show' з 'onClick')
click(message, opts = {}) { return this.show(message, { ...opts, onClick: opts.f }); }
/**
* Вставляє необхідні CSS стилі в Shadow DOM.
*/
_insertStyles() {
const style = document.createElement('style');
style.textContent = `
/* Контейнер */
.app-notification-container {
position: fixed;
z-index: 9999;
pointer-events: none; /* Дозволяє клікам проходити через контейнер */
display: flex;
flex-direction: column;
gap: 10px;
padding: 12px;
}
/* Позиціонування контейнера */
.app-notification-container[data-position="top-right"] { top: 8px; right: 8px; align-items: flex-end; }
.app-notification-container[data-position="top-left"] { top: 8px; left: 8px; align-items: flex-start; }
.app-notification-container[data-position="bottom-right"] { bottom: 8px; right: 8px; align-items: flex-end; }
.app-notification-container[data-position="bottom-left"] { bottom: 8px; left: 8px; align-items: flex-start; }
/* Одне сповіщення */
.app-notification {
pointer-events: auto; /* Вмикаємо кліки на самому сповіщенні */
min-width: 220px;
max-width: 360px;
background: #111;
color: #fff;
padding: 10px 12px 10px 12px;
border-radius: var(--border-radius, 8px);
box-shadow: 0 6px 18px rgba(0, 0, 0, 0.25);
font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial;
font-size: var(--FontSize2, 14px);
line-height: 1.2;
display: flex;
gap: 10px;
align-items: center;
opacity: 0;
transform: translateY(-6px) scale(0.995); /* Початковий стан для анімації */
transition: opacity .18s ease, transform .18s ease;
position: relative;
}
.app-notification.show {
opacity: 0.95;
transform: translateY(0) scale(1); /* Кінцевий стан */
}
.app-notification .icon {
font-size: 18px;
width: 22px;
height: 22px;
display:flex;
align-items:center;
justify-content:center;
border-radius: calc(var(--border-radius, 8px) - 5px);
padding: 8px;
}
.app-notification .icon svg{
width: 20px;
height: 20px;
fill: #fff;
}
.app-notification .body { flex:1; }
.app-notification .title { font-weight: 600; margin-bottom: 4px; font-size: 13px; }
/* Кнопка закриття */
.app-notification .blockClose {
width: 20px;
height: 20px;
}
.app-notification .blockClose .close {
position: absolute;
right: 10px;
top: 10px;
margin-left: 8px;
background: transparent;
border: none;
color: inherit;
cursor: pointer;
padding: 0;
}
.app-notification .blockClose .close svg {
width: 15px;
height: 15px;
fill: #fff;
opacity: 0.8;
display: block;
}
/* Стилі за типами */
.app-notification.info {
background: var(--ColorThemes3, #2196F3);
color: var(--ColorThemes0, #ffffff);
}
.app-notification.info .icon { background: var(--ColorThemes0, #ffffff); }
.app-notification.info .icon svg{fill: var(--ColorThemes3, #2196F3);}
.app-notification.info .close svg{fill: var(--ColorThemes0, #ffffff);}
.app-notification.success { background: #52ac56; }
.app-notification.success .icon { background: #6dc450; }
.app-notification.success .close svg{fill: #fff;}
.app-notification.warn { background: #d18515; }
.app-notification.warn .icon { background: #eaad57; }
.app-notification.warn .close svg{fill: #fff;}
.app-notification.error { background: #9c2424; }
.app-notification.error .icon { background: #c45050; }
.app-notification.error .close svg{fill: #fff;}
/* Адаптивність для мобільних пристроїв */
@media (max-width: 700px) {
.app-notification-container {
left: 0;
right: 0;
width: calc(100% - 24px); /* Повна ширина мінус паддінги */
align-items: center !important;
}
.app-notification-container .app-notification {
max-width: 95%;
min-width: 95%;
}
/* Спеціальна мобільна позиція знизу */
.app-notification-container.mobile-bottom {
top: auto;
bottom: 0;
}
}
`;
this.shadowRoot.appendChild(style);
}
}
// Реєструємо веб-компонент у браузері
customElements.define('app-notification-container', AppNotificationContainer);
/*
============================
ПРИКЛАД ВИКОРИСТАННЯ
============================
*/
/*
1. Додайте цей елемент у свій HTML:
<app-notification-container
id="notif-manager"
position="top-right"
max-visible="5"
timeout="4000"
mobile-position>
</app-notification-container>
2. Отримайте посилання на компонент у JS:
const Notifier = document.getElementById('notif-manager');
3. Приклади викликів:
💡 Базові сповіщення
Notifier.info('Налаштування мобільної позиції змінено.');
Notifier.success('Успішна операція.');
Notifier.warn('Увага: низький рівень заряду батареї.');
Notifier.error('Критична помилка!');
💡 Сповіщення із заголовком
Notifier.info('Це повідомлення має чіткий заголовок.', {
title: 'Важлива інформація'
});
💡 Сповіщення з об'єктом (заголовок та текст)
Notifier.warn({
title: `Metrics`,
text: `З'єднання встановлено`
});
💡 Сповіщення, яке не зникає (timeout: 0 або lock: true)
Notifier.error('Критична помилка! Необхідне втручання.', {
timeout: 0,
lock: true
});
💡 Сповіщення з обробником кліку (автоматично закривається після кліку)
Notifier.info('Натисніть тут, щоб побачити деталі.', {
onClick: () => alert('Ви клацнули! Дякую.'),
lock: false
});
💡 Програмне керування
Notifier.setMobileBottom(true); // Включити примусову позицію знизу для мобільних
Notifier.setMobileBottom(false); // Вимкнути примусову позицію знизу
Notifier.clearAll(); // Видалити всі сповіщення
*/
+1
View File
@@ -57,6 +57,7 @@ const CARD_STYLES_CSS = `
position: relative; position: relative;
z-index: 1; z-index: 1;
filter: blur(3px); filter: blur(3px);
-webkit-mask-image: -webkit-radial-gradient(white, black);
border-radius: calc(var(--border-radius, 15px) - 5px); border-radius: calc(var(--border-radius, 15px) - 5px);
} }
.contents { .contents {
+39 -14
View File
@@ -1,17 +1,42 @@
<div id="page-auth"> <div id="page-auth">
<form id="page-auth-form"> <form id="page-auth-form">
<div> <div>
<input <input
type="password" type="password"
name="uuid" name="uuid"
id="auth-forms-uuid" id="auth-forms-uuid"
placeholder="UUID" placeholder="UUID"
required="" required />
/> <button type="submit">
<span>Авторизуватися</span>
</button>
<button type="submit"> <hr>
<span>Авторизуватися</span>
</button> <button type="button" id="open-scanner-btn" class="btn-scan"
</div> style="background: var(--ColorThemes3);color: var(--ColorThemes0);">
</form> <span>Сканувати QR-код</span>
</button>
</div>
</form>
</div> </div>
<dialog id="scanner-dialog">
<content style="max-width: 300px;max-height: 300px;margin-top: 20px;">
<div class="scanner-container">
<video id="video" autoplay playsinline></video>
<div class="laser-box">
<div class="laser-line"></div>
</div>
<div id="loading-status">Запуск камери...</div>
</div>
</content>
<footer>
<button class="btn-close" id="close-scanner-btn"
aria-label="Закрити">Закрити</button>
</footer>
</dialog>
<canvas id="canvas" style="display: none;"></canvas>
+182 -71
View File
@@ -3,80 +3,191 @@ const Auth = {
let html = await fetch('/lib/pages/auth/index.html').then((response) => response.text()); let html = await fetch('/lib/pages/auth/index.html').then((response) => response.text());
app.innerHTML = html; app.innerHTML = html;
Auth.qr.init();
document.getElementById("page-auth-form").addEventListener("submit", async function (event) { document.getElementById("page-auth-form").addEventListener("submit", async function (event) {
event.preventDefault(); event.preventDefault();
const uuid = document Auth.submit();
.getElementById("auth-forms-uuid")
.value
.replace(/^https?:\/\/sheep-service\.com\/?\?(uuid|hash)=/, "");
console.log(uuid);
const URL = `${CONFIG.api}auth`;
USER = await fetch(URL, {
method: 'GET',
headers: {
"Content-Type": "application/json, text/plain, */*",
"Authorization": uuid
}
}).then(response => {
if (response.status === 401) {
localStorage.removeItem("uuid");
Router.navigate(`/auth`);
} else {
Cloud.start();
localStorage.setItem("uuid", uuid);
Router.navigate(`/`, false);
}
return response.json();
});
console.log("USER Info: ", USER);
if (USER.possibilities.can_view_stand) {
newMenuItems({
id: 'menu-stand',
title: 'Графік стенду',
icon: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 30 30"><path d="M 6.9707031 4 C 6.8307031 4 6.6807813 4.039375 6.5507812 4.109375 L 2.5507812 6.109375 C 2.0607813 6.349375 1.859375 6.9492188 2.109375 7.4492188 C 2.349375 7.9392188 2.9492187 8.140625 3.4492188 7.890625 L 6.4902344 6.3691406 L 12.5 20.650391 C 12.73 21.180391 13.040156 21.650547 13.410156 22.060547 C 12.040156 22.340547 11 23.56 11 25 C 11 26.65 12.35 28 14 28 C 15.65 28 17 26.65 17 25 C 17 24.52 16.869922 24.070156 16.669922 23.660156 C 17.479922 23.740156 18.319141 23.639062 19.119141 23.289062 L 26.400391 20.099609 C 26.910391 19.889609 27.159219 19.310781 26.949219 18.800781 C 26.749219 18.290781 26.160391 18.040234 25.650391 18.240234 C 25.630391 18.250234 25.619609 18.259531 25.599609 18.269531 L 18.320312 21.460938 C 16.770312 22.130938 14.999609 21.429141 14.349609 19.869141 L 7.9199219 4.609375 C 7.7599219 4.229375 7.3807031 3.99 6.9707031 4 z M 21.359375 8.0605469 C 21.229375 8.0605469 21.100703 8.090625 20.970703 8.140625 L 13.609375 11.269531 C 13.099375 11.479531 12.860078 12.070078 13.080078 12.580078 L 16.029297 19.179688 C 16.249297 19.689688 16.829844 19.930937 17.339844 19.710938 L 24.710938 16.589844 C 25.210938 16.369844 25.450234 15.789297 25.240234 15.279297 L 22.279297 8.6699219 C 22.119297 8.2899219 21.749375 8.0605469 21.359375 8.0605469 z M 14 24 C 14.56 24 15 24.44 15 25 C 15 25.56 14.56 26 14 26 C 13.44 26 13 25.56 13 25 C 13 24.44 13.44 24 14 24 z"/></svg>`,
href: '/stand'
});
}
if (USER.possibilities.can_view_schedule) {
newMenuItems({
id: 'menu-schedule',
title: 'Графіки зібрань',
icon: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><path d="M47 23c3.314 0 6 2.686 6 6v17c0 3.309-2.691 6-6 6H17c-3.309 0-6-2.691-6-6V29c0-3.314 2.686-6 6-6H47zM22 45v-2c0-.552-.448-1-1-1h-2c-.552 0-1 .448-1 1v2c0 .552.448 1 1 1h2C21.552 46 22 45.552 22 45zM22 38v-2c0-.552-.448-1-1-1h-2c-.552 0-1 .448-1 1v2c0 .552.448 1 1 1h2C21.552 39 22 38.552 22 38zM30 45v-2c0-.552-.448-1-1-1h-2c-.552 0-1 .448-1 1v2c0 .552.448 1 1 1h2C29.552 46 30 45.552 30 45zM30 38v-2c0-.552-.448-1-1-1h-2c-.552 0-1 .448-1 1v2c0 .552.448 1 1 1h2C29.552 39 30 38.552 30 38zM30 31v-2c0-.552-.448-1-1-1h-2c-.552 0-1 .448-1 1v2c0 .552.448 1 1 1h2C29.552 32 30 31.552 30 31zM38 45v-2c0-.552-.448-1-1-1h-2c-.552 0-1 .448-1 1v2c0 .552.448 1 1 1h2C37.552 46 38 45.552 38 45zM38 38v-2c0-.552-.448-1-1-1h-2c-.552 0-1 .448-1 1v2c0 .552.448 1 1 1h2C37.552 39 38 38.552 38 38zM38 31v-2c0-.552-.448-1-1-1h-2c-.552 0-1 .448-1 1v2c0 .552.448 1 1 1h2C37.552 32 38 31.552 38 31zM46 38v-2c0-.552-.448-1-1-1h-2c-.552 0-1 .448-1 1v2c0 .552.448 1 1 1h2C45.552 39 46 38.552 46 38zM46 31v-2c0-.552-.448-1-1-1h-2c-.552 0-1 .448-1 1v2c0 .552.448 1 1 1h2C45.552 32 46 31.552 46 31zM17 20c-2.308 0-4.407.876-6 2.305V18c0-3.309 2.691-6 6-6h30c3.309 0 6 2.691 6 6v4.305C51.407 20.876 49.308 20 47 20H17z"/></svg>`,
href: '/schedule'
});
}
if (USER.possibilities.can_view_sheeps) {
newMenuItems({
id: 'menu-sheeps',
title: 'Вісники',
icon: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><path d="M 42.5 14 C 37.813 14 34 18.038 34 23 C 34 27.962 37.813 32 42.5 32 C 47.187 32 51 27.962 51 23 C 51 18.038 47.187 14 42.5 14 z M 21.5 17 C 16.813 17 13 21.038 13 26 C 13 30.962 16.813 35 21.5 35 C 26.187 35 30 30.962 30 26 C 30 21.038 26.187 17 21.5 17 z M 42.5 18 C 44.981 18 47 20.243 47 23 C 47 25.757 44.981 28 42.5 28 C 40.019 28 38 25.757 38 23 C 38 20.243 40.019 18 42.5 18 z M 42.498047 34.136719 C 37.579021 34.136719 33.07724 35.947963 30.054688 38.962891 C 27.67058 37.796576 24.915421 37.136719 22 37.136719 C 14.956 37.136719 8.8129375 40.942422 6.7109375 46.607422 C 5.7409375 49.220422 7.7121406 52 10.494141 52 L 33.505859 52 C 35.43112 52 36.95694 50.674804 37.404297 49 L 53.431641 49 C 56.437641 49 59.121453 45.844281 57.564453 42.613281 C 55.084453 37.463281 49.169047 34.136719 42.498047 34.136719 z M 42.5 38.136719 C 47.565 38.136719 52.171937 40.633609 53.960938 44.349609 C 54.119938 44.687609 53.741687 45 53.429688 45 L 36.544922 45 C 35.777257 43.585465 34.746773 42.317451 33.503906 41.234375 C 35.78496 39.306575 39.034912 38.136719 42.5 38.136719 z" /></svg>`,
href: '/sheeps',
hidden: true
});
await Sheeps.sheeps_list.loadAPI();
}
if (USER.possibilities.can_manager_territory) {
newMenuItems({
id: 'menu-territory',
title: 'Території',
icon: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 30 30"><path d="M24 2H14c-.55 0-1 .45-1 1v4l3.6 2.7c.25.19.4.49.4.8V14h8V3C25 2.45 24.55 2 24 2zM15.5 7C15.22 7 15 6.78 15 6.5v-2C15 4.22 15.22 4 15.5 4h2C17.78 4 18 4.22 18 4.5v2C18 6.78 17.78 7 17.5 7h-1.17H15.5zM23 4.5v2C23 6.78 22.78 7 22.5 7h-2C20.22 7 20 6.78 20 6.5v-2C20 4.22 20.22 4 20.5 4h2C22.78 4 23 4.22 23 4.5zM22.5 12h-2c-.28 0-.5-.22-.5-.5v-2C20 9.22 20.22 9 20.5 9h2C22.78 9 23 9.22 23 9.5v2C23 11.78 22.78 12 22.5 12zM1 11.51V27c0 .55.45 1 1 1h12c.55 0 1-.45 1-1V11.51c0-.32-.16-.62-.42-.81l-6-4.28C8.41 6.29 8.2 6.23 8 6.23S7.59 6.29 7.42 6.42l-6 4.28C1.16 10.89 1 11.19 1 11.51zM6.5 20h-2C4.22 20 4 19.78 4 19.5v-2C4 17.22 4.22 17 4.5 17h2C6.78 17 7 17.22 7 17.5v2C7 19.78 6.78 20 6.5 20zM7 22.5v2C7 24.78 6.78 25 6.5 25h-2C4.22 25 4 24.78 4 24.5v-2C4 22.22 4.22 22 4.5 22h2C6.78 22 7 22.22 7 22.5zM6.5 15h-2C4.22 15 4 14.78 4 14.5v-2C4 12.22 4.22 12 4.5 12h2C6.78 12 7 12.22 7 12.5v2C7 14.78 6.78 15 6.5 15zM9.5 17h2c.28 0 .5.22.5.5v2c0 .28-.22.5-.5.5h-2C9.22 20 9 19.78 9 19.5v-2C9 17.22 9.22 17 9.5 17zM9 14.5v-2C9 12.22 9.22 12 9.5 12h2c.28 0 .5.22.5.5v2c0 .28-.22.5-.5.5h-2C9.22 15 9 14.78 9 14.5zM9.5 22h2c.28 0 .5.22.5.5v2c0 .28-.22.5-.5.5h-2C9.22 25 9 24.78 9 24.5v-2C9 22.22 9.22 22 9.5 22zM17 17v10c0 .55.45 1 1 1h10c.55 0 1-.45 1-1V17c0-.55-.45-1-1-1H18C17.45 16 17 16.45 17 17zM19.5 18h2c.28 0 .5.22.5.5v2c0 .28-.22.5-.5.5h-2c-.28 0-.5-.22-.5-.5v-2C19 18.22 19.22 18 19.5 18zM27 18.5v2c0 .28-.22.5-.5.5h-2c-.28 0-.5-.22-.5-.5v-2c0-.28.22-.5.5-.5h2C26.78 18 27 18.22 27 18.5zM26.5 26h-2c-.28 0-.5-.22-.5-.5v-2c0-.28.22-.5.5-.5h2c.28 0 .5.22.5.5v2C27 25.78 26.78 26 26.5 26zM19.5 23h2c.28 0 .5.22.5.5v2c0 .28-.22.5-.5.5h-2c-.28 0-.5-.22-.5-.5v-2C19 23.22 19.22 23 19.5 23z"/></svg>`,
href: '/territory',
hidden: true
});
}
newMenuItems({
id: 'menu-options',
title: 'Опції',
icon: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 172 172"><path d="M75.18001,14.33333c-3.43283,0 -6.36736,2.42659 -7.02669,5.79492l-2.39355,12.28971c-5.8821,2.22427 -11.32102,5.33176 -16.097,9.25228l-11.78581,-4.05924c-3.2465,-1.118 -6.81841,0.22441 -8.53841,3.19141l-10.80599,18.72852c-1.71283,2.97417 -1.08945,6.74999 1.49772,9.00033l9.44824,8.21647c-0.49137,3.0197 -0.81185,6.09382 -0.81185,9.25228c0,3.15846 0.32048,6.23258 0.81185,9.25228l-9.44824,8.21647c-2.58717,2.25033 -3.21055,6.02616 -1.49772,9.00032l10.80599,18.72852c1.71283,2.97417 5.29191,4.31623 8.53841,3.2054l11.78581,-4.05924c4.77441,3.91806 10.21756,7.01501 16.097,9.23828l2.39355,12.28972c0.65933,3.36833 3.59386,5.79492 7.02669,5.79492h21.63998c3.43283,0 6.36735,-2.42659 7.02669,-5.79492l2.39356,-12.28972c5.88211,-2.22427 11.32102,-5.33176 16.097,-9.25227l11.78581,4.05924c3.2465,1.118 6.81841,-0.21724 8.53841,-3.1914l10.80599,-18.74252c1.71284,-2.97417 1.08945,-6.73599 -1.49772,-8.98633l-9.44824,-8.21647c0.49137,-3.0197 0.81185,-6.09382 0.81185,-9.25228c0,-3.15846 -0.32048,-6.23258 -0.81185,-9.25228l9.44824,-8.21647c2.58717,-2.25033 3.21056,-6.02616 1.49772,-9.00033l-10.80599,-18.72852c-1.71283,-2.97417 -5.29191,-4.31624 -8.53841,-3.2054l-11.78581,4.05924c-4.7744,-3.91806 -10.21755,-7.01501 -16.097,-9.23828l-2.39356,-12.28971c-0.65933,-3.36833 -3.59385,-5.79492 -7.02669,-5.79492zM86,57.33333c15.83117,0 28.66667,12.8355 28.66667,28.66667c0,15.83117 -12.8355,28.66667 -28.66667,28.66667c-15.83117,0 -28.66667,-12.8355 -28.66667,-28.66667c0,-15.83117 12.8355,-28.66667 28.66667,-28.66667z"/></svg>`,
href: '/options'
});
}); });
},
submit: async () => {
const uuid = document
.getElementById("auth-forms-uuid")
.value
.replace(/^https?:\/\/sheep-service\.com\/?\?(uuid|hash)=/, "");
console.log(uuid);
const URL = `${CONFIG.api}auth`;
USER = await fetch(URL, {
method: 'GET',
headers: {
"Content-Type": "application/json, text/plain, */*",
"Authorization": uuid
}
}).then(response => {
if (response.status === 401) {
localStorage.removeItem("uuid");
Router.navigate(`/auth`);
} else {
Cloud.start();
localStorage.setItem("uuid", uuid);
Router.navigate(`/`, false);
}
return response.json();
});
console.log("USER Info: ", USER);
if (USER.possibilities.can_view_stand) {
newMenuItems({
id: 'menu-stand',
title: 'Графік стенду',
icon: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 30 30"><path d="M 6.9707031 4 C 6.8307031 4 6.6807813 4.039375 6.5507812 4.109375 L 2.5507812 6.109375 C 2.0607813 6.349375 1.859375 6.9492188 2.109375 7.4492188 C 2.349375 7.9392188 2.9492187 8.140625 3.4492188 7.890625 L 6.4902344 6.3691406 L 12.5 20.650391 C 12.73 21.180391 13.040156 21.650547 13.410156 22.060547 C 12.040156 22.340547 11 23.56 11 25 C 11 26.65 12.35 28 14 28 C 15.65 28 17 26.65 17 25 C 17 24.52 16.869922 24.070156 16.669922 23.660156 C 17.479922 23.740156 18.319141 23.639062 19.119141 23.289062 L 26.400391 20.099609 C 26.910391 19.889609 27.159219 19.310781 26.949219 18.800781 C 26.749219 18.290781 26.160391 18.040234 25.650391 18.240234 C 25.630391 18.250234 25.619609 18.259531 25.599609 18.269531 L 18.320312 21.460938 C 16.770312 22.130938 14.999609 21.429141 14.349609 19.869141 L 7.9199219 4.609375 C 7.7599219 4.229375 7.3807031 3.99 6.9707031 4 z M 21.359375 8.0605469 C 21.229375 8.0605469 21.100703 8.090625 20.970703 8.140625 L 13.609375 11.269531 C 13.099375 11.479531 12.860078 12.070078 13.080078 12.580078 L 16.029297 19.179688 C 16.249297 19.689688 16.829844 19.930937 17.339844 19.710938 L 24.710938 16.589844 C 25.210938 16.369844 25.450234 15.789297 25.240234 15.279297 L 22.279297 8.6699219 C 22.119297 8.2899219 21.749375 8.0605469 21.359375 8.0605469 z M 14 24 C 14.56 24 15 24.44 15 25 C 15 25.56 14.56 26 14 26 C 13.44 26 13 25.56 13 25 C 13 24.44 13.44 24 14 24 z"/></svg>`,
href: '/stand'
});
}
if (USER.possibilities.can_view_schedule) {
newMenuItems({
id: 'menu-schedule',
title: 'Графіки зібрань',
icon: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><path d="M47 23c3.314 0 6 2.686 6 6v17c0 3.309-2.691 6-6 6H17c-3.309 0-6-2.691-6-6V29c0-3.314 2.686-6 6-6H47zM22 45v-2c0-.552-.448-1-1-1h-2c-.552 0-1 .448-1 1v2c0 .552.448 1 1 1h2C21.552 46 22 45.552 22 45zM22 38v-2c0-.552-.448-1-1-1h-2c-.552 0-1 .448-1 1v2c0 .552.448 1 1 1h2C21.552 39 22 38.552 22 38zM30 45v-2c0-.552-.448-1-1-1h-2c-.552 0-1 .448-1 1v2c0 .552.448 1 1 1h2C29.552 46 30 45.552 30 45zM30 38v-2c0-.552-.448-1-1-1h-2c-.552 0-1 .448-1 1v2c0 .552.448 1 1 1h2C29.552 39 30 38.552 30 38zM30 31v-2c0-.552-.448-1-1-1h-2c-.552 0-1 .448-1 1v2c0 .552.448 1 1 1h2C29.552 32 30 31.552 30 31zM38 45v-2c0-.552-.448-1-1-1h-2c-.552 0-1 .448-1 1v2c0 .552.448 1 1 1h2C37.552 46 38 45.552 38 45zM38 38v-2c0-.552-.448-1-1-1h-2c-.552 0-1 .448-1 1v2c0 .552.448 1 1 1h2C37.552 39 38 38.552 38 38zM38 31v-2c0-.552-.448-1-1-1h-2c-.552 0-1 .448-1 1v2c0 .552.448 1 1 1h2C37.552 32 38 31.552 38 31zM46 38v-2c0-.552-.448-1-1-1h-2c-.552 0-1 .448-1 1v2c0 .552.448 1 1 1h2C45.552 39 46 38.552 46 38zM46 31v-2c0-.552-.448-1-1-1h-2c-.552 0-1 .448-1 1v2c0 .552.448 1 1 1h2C45.552 32 46 31.552 46 31zM17 20c-2.308 0-4.407.876-6 2.305V18c0-3.309 2.691-6 6-6h30c3.309 0 6 2.691 6 6v4.305C51.407 20.876 49.308 20 47 20H17z"/></svg>`,
href: '/schedule'
});
}
if (USER.possibilities.can_view_sheeps) {
newMenuItems({
id: 'menu-sheeps',
title: 'Вісники',
icon: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><path d="M 42.5 14 C 37.813 14 34 18.038 34 23 C 34 27.962 37.813 32 42.5 32 C 47.187 32 51 27.962 51 23 C 51 18.038 47.187 14 42.5 14 z M 21.5 17 C 16.813 17 13 21.038 13 26 C 13 30.962 16.813 35 21.5 35 C 26.187 35 30 30.962 30 26 C 30 21.038 26.187 17 21.5 17 z M 42.5 18 C 44.981 18 47 20.243 47 23 C 47 25.757 44.981 28 42.5 28 C 40.019 28 38 25.757 38 23 C 38 20.243 40.019 18 42.5 18 z M 42.498047 34.136719 C 37.579021 34.136719 33.07724 35.947963 30.054688 38.962891 C 27.67058 37.796576 24.915421 37.136719 22 37.136719 C 14.956 37.136719 8.8129375 40.942422 6.7109375 46.607422 C 5.7409375 49.220422 7.7121406 52 10.494141 52 L 33.505859 52 C 35.43112 52 36.95694 50.674804 37.404297 49 L 53.431641 49 C 56.437641 49 59.121453 45.844281 57.564453 42.613281 C 55.084453 37.463281 49.169047 34.136719 42.498047 34.136719 z M 42.5 38.136719 C 47.565 38.136719 52.171937 40.633609 53.960938 44.349609 C 54.119938 44.687609 53.741687 45 53.429688 45 L 36.544922 45 C 35.777257 43.585465 34.746773 42.317451 33.503906 41.234375 C 35.78496 39.306575 39.034912 38.136719 42.5 38.136719 z" /></svg>`,
href: '/sheeps',
hidden: true
});
await Sheeps.sheeps_list.loadAPI();
}
if (USER.possibilities.can_manager_territory) {
newMenuItems({
id: 'menu-territory',
title: 'Території',
icon: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 30 30"><path d="M24 2H14c-.55 0-1 .45-1 1v4l3.6 2.7c.25.19.4.49.4.8V14h8V3C25 2.45 24.55 2 24 2zM15.5 7C15.22 7 15 6.78 15 6.5v-2C15 4.22 15.22 4 15.5 4h2C17.78 4 18 4.22 18 4.5v2C18 6.78 17.78 7 17.5 7h-1.17H15.5zM23 4.5v2C23 6.78 22.78 7 22.5 7h-2C20.22 7 20 6.78 20 6.5v-2C20 4.22 20.22 4 20.5 4h2C22.78 4 23 4.22 23 4.5zM22.5 12h-2c-.28 0-.5-.22-.5-.5v-2C20 9.22 20.22 9 20.5 9h2C22.78 9 23 9.22 23 9.5v2C23 11.78 22.78 12 22.5 12zM1 11.51V27c0 .55.45 1 1 1h12c.55 0 1-.45 1-1V11.51c0-.32-.16-.62-.42-.81l-6-4.28C8.41 6.29 8.2 6.23 8 6.23S7.59 6.29 7.42 6.42l-6 4.28C1.16 10.89 1 11.19 1 11.51zM6.5 20h-2C4.22 20 4 19.78 4 19.5v-2C4 17.22 4.22 17 4.5 17h2C6.78 17 7 17.22 7 17.5v2C7 19.78 6.78 20 6.5 20zM7 22.5v2C7 24.78 6.78 25 6.5 25h-2C4.22 25 4 24.78 4 24.5v-2C4 22.22 4.22 22 4.5 22h2C6.78 22 7 22.22 7 22.5zM6.5 15h-2C4.22 15 4 14.78 4 14.5v-2C4 12.22 4.22 12 4.5 12h2C6.78 12 7 12.22 7 12.5v2C7 14.78 6.78 15 6.5 15zM9.5 17h2c.28 0 .5.22.5.5v2c0 .28-.22.5-.5.5h-2C9.22 20 9 19.78 9 19.5v-2C9 17.22 9.22 17 9.5 17zM9 14.5v-2C9 12.22 9.22 12 9.5 12h2c.28 0 .5.22.5.5v2c0 .28-.22.5-.5.5h-2C9.22 15 9 14.78 9 14.5zM9.5 22h2c.28 0 .5.22.5.5v2c0 .28-.22.5-.5.5h-2C9.22 25 9 24.78 9 24.5v-2C9 22.22 9.22 22 9.5 22zM17 17v10c0 .55.45 1 1 1h10c.55 0 1-.45 1-1V17c0-.55-.45-1-1-1H18C17.45 16 17 16.45 17 17zM19.5 18h2c.28 0 .5.22.5.5v2c0 .28-.22.5-.5.5h-2c-.28 0-.5-.22-.5-.5v-2C19 18.22 19.22 18 19.5 18zM27 18.5v2c0 .28-.22.5-.5.5h-2c-.28 0-.5-.22-.5-.5v-2c0-.28.22-.5.5-.5h2C26.78 18 27 18.22 27 18.5zM26.5 26h-2c-.28 0-.5-.22-.5-.5v-2c0-.28.22-.5.5-.5h2c.28 0 .5.22.5.5v2C27 25.78 26.78 26 26.5 26zM19.5 23h2c.28 0 .5.22.5.5v2c0 .28-.22.5-.5.5h-2c-.28 0-.5-.22-.5-.5v-2C19 23.22 19.22 23 19.5 23z"/></svg>`,
href: '/territory',
hidden: true
});
}
newMenuItems({
id: 'menu-options',
title: 'Опції',
icon: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 172 172"><path d="M75.18001,14.33333c-3.43283,0 -6.36736,2.42659 -7.02669,5.79492l-2.39355,12.28971c-5.8821,2.22427 -11.32102,5.33176 -16.097,9.25228l-11.78581,-4.05924c-3.2465,-1.118 -6.81841,0.22441 -8.53841,3.19141l-10.80599,18.72852c-1.71283,2.97417 -1.08945,6.74999 1.49772,9.00033l9.44824,8.21647c-0.49137,3.0197 -0.81185,6.09382 -0.81185,9.25228c0,3.15846 0.32048,6.23258 0.81185,9.25228l-9.44824,8.21647c-2.58717,2.25033 -3.21055,6.02616 -1.49772,9.00032l10.80599,18.72852c1.71283,2.97417 5.29191,4.31623 8.53841,3.2054l11.78581,-4.05924c4.77441,3.91806 10.21756,7.01501 16.097,9.23828l2.39355,12.28972c0.65933,3.36833 3.59386,5.79492 7.02669,5.79492h21.63998c3.43283,0 6.36735,-2.42659 7.02669,-5.79492l2.39356,-12.28972c5.88211,-2.22427 11.32102,-5.33176 16.097,-9.25227l11.78581,4.05924c3.2465,1.118 6.81841,-0.21724 8.53841,-3.1914l10.80599,-18.74252c1.71284,-2.97417 1.08945,-6.73599 -1.49772,-8.98633l-9.44824,-8.21647c0.49137,-3.0197 0.81185,-6.09382 0.81185,-9.25228c0,-3.15846 -0.32048,-6.23258 -0.81185,-9.25228l9.44824,-8.21647c2.58717,-2.25033 3.21056,-6.02616 1.49772,-9.00033l-10.80599,-18.72852c-1.71283,-2.97417 -5.29191,-4.31624 -8.53841,-3.2054l-11.78581,4.05924c-4.7744,-3.91806 -10.21755,-7.01501 -16.097,-9.23828l-2.39356,-12.28971c-0.65933,-3.36833 -3.59385,-5.79492 -7.02669,-5.79492zM86,57.33333c15.83117,0 28.66667,12.8355 28.66667,28.66667c0,15.83117 -12.8355,28.66667 -28.66667,28.66667c-15.83117,0 -28.66667,-12.8355 -28.66667,-28.66667c0,-15.83117 12.8355,-28.66667 28.66667,-28.66667z"/></svg>`,
href: '/options'
});
},
qr: {
init: () => {
const dialog = document.getElementById('scanner-dialog');
const openBtn = document.getElementById('open-scanner-btn');
const closeBtn = document.getElementById('close-scanner-btn');
const uuidInput = document.getElementById('auth-forms-uuid');
const video = document.getElementById('video');
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d', { willReadFrequently: true });
const loadingStatus = document.getElementById('loading-status');
let streamRef = null;
let isScanning = false;
// --- Логика управления сканером ---
async function startCamera() {
loadingStatus.style.display = 'block';
loadingStatus.innerText = "Запуск камери...";
isScanning = true;
try {
const stream = await navigator.mediaDevices.getUserMedia({
video: { facingMode: { exact: "environment" } }
});
streamRef = stream;
video.srcObject = stream;
video.setAttribute("playsinline", true);
video.play();
video.addEventListener('loadedmetadata', () => {
loadingStatus.style.display = 'none';
requestAnimationFrame(tick);
});
} catch (err) {
// Фолбек, если основная камера недоступна (например, на ПК)
try {
const fallbackStream = await navigator.mediaDevices.getUserMedia({ video: true });
streamRef = fallbackStream;
video.srcObject = fallbackStream;
video.play();
loadingStatus.style.display = 'none';
requestAnimationFrame(tick);
} catch (fallbackErr) {
loadingStatus.innerText = "Помилка доступу до камери";
}
}
}
function stopCamera() {
isScanning = false;
if (streamRef) {
streamRef.getTracks().forEach(track => track.stop());
streamRef = null;
}
video.srcObject = null;
}
function tick() {
if (!isScanning) return;
if (video.readyState === video.HAVE_ENOUGH_DATA) {
canvas.height = video.videoHeight;
canvas.width = video.videoWidth;
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const code = jsQR(imageData.data, imageData.width, imageData.height, {
inversionAttempts: "dontInvert",
});
if (code && code.data.trim() !== "") {
// Успешно считали UUID
uuidInput.value = code.data;
Auth.submit();
// Закрываем модалку (событие close сработает автоматически)
dialog.close();
return;
}
}
requestAnimationFrame(tick);
}
// --- Слушатели событий модального окна ---
// Открыть сканер
openBtn.addEventListener('click', () => {
dialog.showModal(); // Открывает как модальное окно (с бэкдропом)
startCamera();
});
// Закрыть сканер по кнопке крестика
closeBtn.addEventListener('click', () => {
dialog.close();
});
// Важно: событие 'close' срабатывает всегда (и при клике на крестик, и при нажатии Esc)
dialog.addEventListener('close', () => {
stopCamera();
});
},
} }
} }
+53 -1
View File
@@ -21,7 +21,7 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
height: 160px; height: 200px;
justify-content: center; justify-content: center;
width: 250px; width: 250px;
padding: 10px 20px; padding: 10px 20px;
@@ -51,8 +51,60 @@
height: 40px; height: 40px;
margin: 20px 0 0 0; margin: 20px 0 0 0;
} }
#page-auth-form>div>button>span { #page-auth-form>div>button>span {
font-size: var(--FontSize2); font-size: var(--FontSize2);
font-weight: 400; font-weight: 400;
text-transform: uppercase; text-transform: uppercase;
} }
.scanner-container {
position: relative;
width: 100%;
height: auto;
aspect-ratio: 1 / 1;
border-radius: 5px;
overflow: hidden;
background-color: var(--ColorThemes0);
}
#video {
width: 100%;
height: 100%;
object-fit: cover;
}
.laser-box {
position: absolute;
top: 15%;
left: 15%;
width: 70%;
height: 70%;
border: 2px solid var(--PrimaryColor);
border-radius: 12px;
pointer-events: none;
}
.laser-line {
position: absolute;
width: 100%;
height: 2px;
background-color: var(--PrimaryColor);
/* box-shadow: 0 0 8px var(--PrimaryColor); */
top: 0;
animation: scan 2s linear infinite;
}
@keyframes scan {
0% {
top: 0%;
}
50% {
top: 100%;
}
100% {
top: 0%;
}
}
+20 -18
View File
@@ -1,22 +1,24 @@
<div class="page-options"> <div class="page-options">
<div class="option checkbox"> <div class="option checkbox">
<input <input
class="custom-checkbox" class="custom-checkbox"
id="page-options-notifications" id="page-options-notifications"
type="checkbox" type="checkbox"
onchange="Options.optionPush.edit(this.checked)" onchange="Options.optionPush.edit(this.checked)" />
/> <label for="page-options-notifications"> Повідомлення: </label>
<label for="page-options-notifications"> Повідомлення: </label> </div>
</div>
<div class="option inputs"> <div class="option inputs">
<label for="page-options-fontSize">Розмір шрифту:</label> <label for="page-options-fontSize">Розмір шрифту:</label>
<select id="page-options-fontSize" onchange="Options.optionFont.edit(this.value)"> <select id="page-options-fontSize"
<option value="small">Маленький</option> onchange="Options.optionFont.edit(this.value)">
<option value="medium">Середній</option> <option value="small">Маленький</option>
<option value="large">Великий</option> <option value="medium">Середній</option>
</select> <option value="large">Великий</option>
</div> </select>
</div>
<button onclick="Options.logout()">Вийти з облікового запису</button> <a href="/test.html">Тестова сторінка</a>
<button onclick="Options.logout()">Вийти з облікового запису</button>
</div> </div>
+359 -356
View File
@@ -1,365 +1,368 @@
<div class="page-sheeps"> <div class="page-sheeps">
<div id="block-sheeps-list"> <div id="block-sheeps-list">
<div class="header"> <div class="header">
<h1>Всі вісники</h1> <h1>Всі вісники</h1>
<div> <div>
<button title="Пошук" id="block-sheeps-list-search-buttom"> <button title="Пошук" id="block-sheeps-list-search-buttom">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 72 72"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 72 72">
<path <path
d="M 31 11 C 19.973 11 11 19.973 11 31 C 11 42.027 19.973 51 31 51 C 34.974166 51 38.672385 49.821569 41.789062 47.814453 L 54.726562 60.751953 C 56.390563 62.415953 59.088953 62.415953 60.751953 60.751953 C 62.415953 59.087953 62.415953 56.390563 60.751953 54.726562 L 47.814453 41.789062 C 49.821569 38.672385 51 34.974166 51 31 C 51 19.973 42.027 11 31 11 z M 31 19 C 37.616 19 43 24.384 43 31 C 43 37.616 37.616 43 31 43 C 24.384 43 19 37.616 19 31 C 19 24.384 24.384 19 31 19 z" d="M 31 11 C 19.973 11 11 19.973 11 31 C 11 42.027 19.973 51 31 51 C 34.974166 51 38.672385 49.821569 41.789062 47.814453 L 54.726562 60.751953 C 56.390563 62.415953 59.088953 62.415953 60.751953 60.751953 C 62.415953 59.087953 62.415953 56.390563 60.751953 54.726562 L 47.814453 41.789062 C 49.821569 38.672385 51 34.974166 51 31 C 51 19.973 42.027 11 31 11 z M 31 19 C 37.616 19 43 24.384 43 31 C 43 37.616 37.616 43 31 43 C 24.384 43 19 37.616 19 31 C 19 24.384 24.384 19 31 19 z" />
/> </svg>
</svg> </button>
</button> </div>
</div> </div>
<div class="search" id="block-sheeps-list-search" data-state="closed">
<input
type="text"
id="block-sheeps-list-search-name"
placeholder="Імʼя вісника" />
</div>
<div class="card-profile"></div>
<div class="card-profile"></div>
<div class="card-profile"></div>
<div class="card-profile"></div>
<div class="card-profile"></div>
<div class="card-profile"></div>
<div class="card-profile"></div>
<div class="card-profile"></div>
<div class="card-profile"></div>
<div class="card-profile"></div>
<div class="card-profile"></div>
<div class="card-profile"></div>
</div> </div>
<div class="search" id="block-sheeps-list-search" data-state="closed"> <div id="block-sheep-info">
<input <div id="sheep-mess">
type="text" <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
id="block-sheeps-list-search-name" <path
placeholder="Імʼя вісника" d="M 25.042969 12 C 20.698969 12 18 15.029297 18 19.904297 C 18 24.104297 20.766812 28.599609 24.882812 28.599609 C 27.167812 28.599609 31 27.369813 31 19.132812 C 31 14.999813 28.494969 12 25.042969 12 z M 39.974609 12 C 35.672609 12 33 14.931437 33 19.648438 C 33 24.104438 34.862391 28.599609 39.025391 28.599609 C 43.603391 28.599609 46 23.922734 46 19.302734 C 46 14.285734 42.876609 12 39.974609 12 z M 25.042969 16 C 26.388969 16 27 17.623812 27 19.132812 C 27 20.776813 26.794812 24.599609 24.882812 24.599609 C 23.428813 24.599609 22 22.274297 22 19.904297 C 22 16.000297 24.132969 16 25.042969 16 z M 39.974609 16 C 41.224609 16 42 17.265734 42 19.302734 C 42 21.906734 40.886391 24.599609 39.025391 24.599609 C 37.594391 24.599609 37 21.375438 37 19.648438 C 37 16.000438 39.084609 16 39.974609 16 z M 13.689453 24 C 9.8134531 24 7 27.465234 7 32.240234 C 7 35.462234 9.0299688 40 13.542969 40 C 17.224969 40 20 36.250297 20 31.279297 C 20 27.061297 17.346453 24 13.689453 24 z M 50.75 24 C 46.94 24 43 27.440219 43 33.199219 C 43 36.541219 45.15 40 48.75 40 C 52.612 40 56 35.477312 56 30.320312 C 56 26.480313 53.939 24 50.75 24 z M 13.689453 28 C 15.514453 28 16 30.061297 16 31.279297 C 16 33.793297 14.852969 36 13.542969 36 C 11.603969 36 11 33.244234 11 32.240234 C 11 29.783234 12.131453 28 13.689453 28 z M 50.75 28 C 51.783 28 52 29.262313 52 30.320312 C 52 33.788312 49.866 36 48.75 36 C 47.514 36 47 34.175219 47 33.199219 C 47 29.652219 49.237 28 50.75 28 z M 31.806641 30.001953 C 29.371641 29.947953 27.296656 31.058937 25.597656 33.335938 C 22.515656 37.450938 21.695953 38.210313 19.376953 39.695312 C 17.528953 40.875312 14.998047 42.492141 14.998047 45.994141 C 14.998047 49.769141 18.164641 52.839844 22.056641 52.839844 C 23.872641 52.839844 25.588266 52.174453 27.697266 51.439453 C 29.071266 52.348453 30.722125 52.839844 32.453125 52.839844 C 34.215125 52.839844 35.869422 52.353125 37.232422 51.453125 C 41.805422 53.496125 44.150719 53.039344 45.636719 52.277344 C 47.314719 51.416344 48.403953 49.800703 48.876953 47.470703 C 49.107953 46.334703 48.997375 45.198781 48.609375 44.175781 C 48.012375 42.602781 46.757453 41.293531 45.064453 40.644531 C 43.137453 39.916531 42.576406 39.753172 41.316406 38.826172 C 39.672406 37.616172 39.015453 35.742422 37.814453 33.607422 C 36.601453 31.449422 34.298641 30.067953 31.806641 30.001953 z M 31.701172 34.001953 C 32.777172 34.029953 33.808125 34.644359 34.328125 35.568359 C 35.480125 37.621359 36.442312 40.205828 38.945312 42.048828 C 40.748312 43.375828 41.894813 43.725812 43.632812 44.382812 C 44.073813 44.550813 44.501812 44.883844 44.757812 45.339844 C 44.966813 45.711844 45.061031 46.164781 44.957031 46.675781 C 44.738031 47.753781 44.3535 48.44175 43.8125 48.71875 C 42.4475 49.41975 39.736125 48.192875 38.578125 47.671875 C 38.187125 47.495875 36.729781 46.582125 35.550781 47.703125 C 34.778781 48.436125 33.679125 48.839844 32.453125 48.839844 C 31.288125 48.839844 30.211922 48.444562 29.419922 47.726562 C 28.893922 47.247562 28.151609 47.085828 27.474609 47.298828 C 25.411609 47.948828 23.441594 48.839844 22.058594 48.839844 C 20.372594 48.839844 19.001953 47.562141 19.001953 45.994141 C 19.001953 44.795141 19.69525 44.240406 21.53125 43.066406 C 24.29525 41.302406 25.514688 40.146469 28.804688 35.730469 C 29.975687 34.161469 30.981172 33.980953 31.701172 34.001953 z" />
/> </svg>
<h1>Виберіть вісника для редагування</h1>
</div>
<form id="sheep-editor" style="display: none; opacity: 0">
<div class="header">
<h1>Інформація про вісника</h1>
<button onclick="Sheeps.editor.close()" type="button">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 26 26">
<path
d="M 6.65625 4 C 6.367188 4 6.105469 4.113281 5.90625 4.3125 L 4.3125 5.90625 C 3.914063 6.304688 3.914063 7 4.3125 7.5 L 9.8125 13 L 4.3125 18.5 C 3.914063 19 3.914063 19.695313 4.3125 20.09375 L 5.90625 21.6875 C 6.40625 22.085938 7.101563 22.085938 7.5 21.6875 L 13 16.1875 L 18.5 21.6875 C 19 22.085938 19.695313 22.085938 20.09375 21.6875 L 21.6875 20.09375 C 22.085938 19.59375 22.085938 18.898438 21.6875 18.5 L 16.1875 13 L 21.6875 7.5 C 22.085938 7 22.085938 6.304688 21.6875 5.90625 L 20.09375 4.3125 C 19.59375 3.914063 18.898438 3.914063 18.5 4.3125 L 13 9.8125 L 7.5 4.3125 C 7.25 4.113281 6.945313 4 6.65625 4 Z"></path>
</svg>
</button>
</div>
<i id="sheep-editor-icon"></i>
<div class="editor-blocks-inputs">
<label for="sheep-editor-name">Імʼя</label>
<input id="sheep-editor-name" type="text" name="name"
required />
</div>
<div class="editor-blocks-inputs">
<label for="sheep-editor-group_id">Група</label>
<select id="sheep-editor-group_id" name="group_id">
<option value="1">Група 1</option>
<option value="2" selected>Група 2</option>
<option value="3">Група 3</option>
<option value="4">Група 4</option>
<option value="5">Група 5</option>
<option value="6">Група 6</option>
<option value="7">Група 7</option>
<option value="8">Група 8</option>
</select>
</div>
<div class="editor-blocks-inputs">
<label for="sheep-editor-mode">Права</label>
<select id="sheep-editor-mode" name="mode">
<option value="0" selected>Користувач</option>
<option value="1">Модератор</option>
<option value="2">Адміністратор</option>
</select>
</div>
<button popovertarget="uuid-popover" type="button">Надати доступ</button>
<div id="uuid-popover" popover>
<div class="editor-blocks-inputs"
id="editor-blocks-inputs-uuid">
<label>UUID <span>Натисніть, щоб скопіювати</span></label>
<input
id="sheep-editor-uuid"
type="text"
name="uuid"
value
disabled
style="display: none" />
<p id="sheep-editor-uuid-copy"
style="cursor: copy !important"></p>
</div>
<div
class="editor-blocks-inputs"
id="editor-blocks-inputs-uuid-moder"
style="display: none">
<label>UUID адміністратора/модератора</label>
<p id="sheep-editor-uuid-moder"
style="cursor: copy !important"></p>
</div>
<div id="qr-code"></div>
<button popovertarget="uuid-popover"
popovertargetaction="hide" type="button">Закрити</button>
</div>
<div
class="editor-blocks-checkbox"
id="sheep-editor-access-moder"
style="display: none">
<p for="editor-access">Дозволи модератора</p>
<div>
<div class="checkbox">
<input
name="can_view_sheeps"
class="custom-checkbox"
id="sheep-editor-can_view_sheeps"
type="checkbox" />
<label for="sheep-editor-can_view_sheeps"> Перегляд
списку вісників </label>
</div>
<div class="checkbox">
<input
name="can_add_sheeps"
class="custom-checkbox"
id="sheep-editor-can_add_sheeps"
type="checkbox" />
<label for="sheep-editor-can_add_sheeps"> Додавання
вісників </label>
</div>
<div class="checkbox">
<input
name="can_manager_sheeps"
class="custom-checkbox"
id="sheep-editor-can_manager_sheeps"
type="checkbox" />
<label for="sheep-editor-can_manager_sheeps"> Керування
дозволами вісників </label>
</div>
<div class="checkbox">
<input
name="can_add_territory"
class="custom-checkbox"
id="sheep-editor-can_add_territory"
type="checkbox" />
<label for="sheep-editor-can_add_territory">
Створення територій
</label>
</div>
<div class="checkbox">
<input
name="can_manager_territory"
class="custom-checkbox"
id="sheep-editor-can_manager_territory"
type="checkbox" />
<label for="sheep-editor-can_manager_territory">
Керування територіями
</label>
</div>
<div class="checkbox">
<input
name="can_joint_territory"
class="custom-checkbox"
id="sheep-editor-can_joint_territory"
type="checkbox" />
<label for="sheep-editor-can_joint_territory">
Спільний доступ до території
</label>
</div>
<div class="checkbox">
<input
name="can_add_stand"
class="custom-checkbox"
id="sheep-editor-can_add_stand"
type="checkbox" />
<label for="sheep-editor-can_add_stand"> Створення
стендів </label>
</div>
<div class="checkbox">
<input
name="can_manager_stand"
class="custom-checkbox"
id="sheep-editor-can_manager_stand"
type="checkbox" />
<label for="sheep-editor-can_manager_stand"> Керування
стендами </label>
</div>
<div class="checkbox">
<input
name="can_add_schedule"
class="custom-checkbox"
id="sheep-editor-can_add_schedule"
type="checkbox" />
<label for="sheep-editor-can_add_schedule"> Створення
розкладу зібрань </label>
</div>
</div>
</div>
<div class="editor-blocks-checkbox">
<p for="editor-access">Дозволи вісника</p>
<div>
<div class="checkbox">
<input
name="can_view_schedule"
class="custom-checkbox"
id="sheep-editor-can_view_schedule"
type="checkbox" />
<label for="sheep-editor-can_view_schedule"> Перегляд
розкладу зібрань </label>
</div>
<div class="checkbox">
<input
name="can_view_stand"
class="custom-checkbox"
id="sheep-editor-can_view_stand"
type="checkbox" />
<label for="sheep-editor-can_view_stand"> Перегляд
стендів </label>
</div>
<div class="checkbox">
<input
name="can_view_territory"
class="custom-checkbox"
id="sheep-editor-can_view_territory"
type="checkbox" />
<label for="sheep-editor-can_view_territory">
Перегляд територій
</label>
</div>
</div>
</div>
<button id="sheep-editor-button" style="display: none"
type="submit">
Зберегти
</button>
<details id="editor-blocks-territory" style="display: none;">
<summary>Території вісника</summary>
<div>
<div id="title">
<h1>вул. Бродівська 50Б (Під'їзд 1)</h1>
<a
href="/territory/card/house/57"
title="Редактор квартир"
data-route>
<svg xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 48 48">
<path
d="M 12.5 6 C 8.9280619 6 6 8.9280619 6 12.5 L 6 35.5 C 6 39.071938 8.9280619 42 12.5 42 L 35.5 42 C 39.071938 42 42 39.071938 42 35.5 L 42 12.5 C 42 8.9280619 39.071938 6 35.5 6 L 12.5 6 z M 12.5 9 L 14 9 L 14 15 L 9 15 L 9 12.5 C 9 10.549938 10.549938 9 12.5 9 z M 17 9 L 35.5 9 C 37.450062 9 39 10.549938 39 12.5 L 39 15 L 17 15 L 17 9 z M 9 18 L 14 18 L 14 23 L 9 23 L 9 18 z M 17 18 L 39 18 L 39 23 L 17 23 L 17 18 z M 9 26 L 14 26 L 14 31 L 9 31 L 9 26 z M 17 26 L 39 26 L 39 31 L 17 31 L 17 26 z M 9 34 L 14 34 L 14 39 L 12.5 39 C 10.549938 39 9 37.450062 9 35.5 L 9 34 z M 17 34 L 39 34 L 39 35.5 C 39 37.450062 37.450062 39 35.5 39 L 17 39 L 17 34 z"></path>
</svg>
</a>
</div>
<div>
<h1>Територія видана:</h1>
<h2>02.07.2025</h2>
</div>
<div>
<h1>Варто забрати:</h1>
<h2>01.11.2025</h2>
</div>
</div>
<div>
<div id="title">
<h1>вул. Бродівська 50Б (Під'їзд 1)</h1>
<a
href="/territory/card/house/57"
title="Редактор квартир"
data-route>
<svg xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 48 48">
<path
d="M 12.5 6 C 8.9280619 6 6 8.9280619 6 12.5 L 6 35.5 C 6 39.071938 8.9280619 42 12.5 42 L 35.5 42 C 39.071938 42 42 39.071938 42 35.5 L 42 12.5 C 42 8.9280619 39.071938 6 35.5 6 L 12.5 6 z M 12.5 9 L 14 9 L 14 15 L 9 15 L 9 12.5 C 9 10.549938 10.549938 9 12.5 9 z M 17 9 L 35.5 9 C 37.450062 9 39 10.549938 39 12.5 L 39 15 L 17 15 L 17 9 z M 9 18 L 14 18 L 14 23 L 9 23 L 9 18 z M 17 18 L 39 18 L 39 23 L 17 23 L 17 18 z M 9 26 L 14 26 L 14 31 L 9 31 L 9 26 z M 17 26 L 39 26 L 39 31 L 17 31 L 17 26 z M 9 34 L 14 34 L 14 39 L 12.5 39 C 10.549938 39 9 37.450062 9 35.5 L 9 34 z M 17 34 L 39 34 L 39 35.5 C 39 37.450062 37.450062 39 35.5 39 L 17 39 L 17 34 z"></path>
</svg>
</a>
</div>
<div>
<h1>Територія видана:</h1>
<h2>02.07.2025</h2>
</div>
<div>
<h1>Варто забрати:</h1>
<h2>01.11.2025</h2>
</div>
</div>
<div>
<div id="title">
<h1>вул. Бродівська 50Б (Під'їзд 1)</h1>
<a
href="/territory/card/house/57"
title="Редактор квартир"
data-route>
<svg xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 48 48">
<path
d="M 12.5 6 C 8.9280619 6 6 8.9280619 6 12.5 L 6 35.5 C 6 39.071938 8.9280619 42 12.5 42 L 35.5 42 C 39.071938 42 42 39.071938 42 35.5 L 42 12.5 C 42 8.9280619 39.071938 6 35.5 6 L 12.5 6 z M 12.5 9 L 14 9 L 14 15 L 9 15 L 9 12.5 C 9 10.549938 10.549938 9 12.5 9 z M 17 9 L 35.5 9 C 37.450062 9 39 10.549938 39 12.5 L 39 15 L 17 15 L 17 9 z M 9 18 L 14 18 L 14 23 L 9 23 L 9 18 z M 17 18 L 39 18 L 39 23 L 17 23 L 17 18 z M 9 26 L 14 26 L 14 31 L 9 31 L 9 26 z M 17 26 L 39 26 L 39 31 L 17 31 L 17 26 z M 9 34 L 14 34 L 14 39 L 12.5 39 C 10.549938 39 9 37.450062 9 35.5 L 9 34 z M 17 34 L 39 34 L 39 35.5 C 39 37.450062 37.450062 39 35.5 39 L 17 39 L 17 34 z"></path>
</svg>
</a>
</div>
<div>
<h1>Територія видана:</h1>
<h2>02.07.2025</h2>
</div>
<div>
<h1>Варто забрати:</h1>
<h2>01.11.2025</h2>
</div>
</div>
</details>
</form>
</div> </div>
<div class="card-profile"></div> <div id="block-sheep-addeds" style="display: none; opacity: 0">
<div class="card-profile"></div> <form id="sheep-addeds">
<div class="card-profile"></div> <div class="header">
<div class="card-profile"></div> <h1>Додавання нового вісника</h1>
<div class="card-profile"></div> <button onclick="Sheeps.addeds.close()" class="close"
<div class="card-profile"></div> type="button">
<div class="card-profile"></div> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 26 26">
<div class="card-profile"></div> <path
<div class="card-profile"></div> d="M 6.65625 4 C 6.367188 4 6.105469 4.113281 5.90625 4.3125 L 4.3125 5.90625 C 3.914063 6.304688 3.914063 7 4.3125 7.5 L 9.8125 13 L 4.3125 18.5 C 3.914063 19 3.914063 19.695313 4.3125 20.09375 L 5.90625 21.6875 C 6.40625 22.085938 7.101563 22.085938 7.5 21.6875 L 13 16.1875 L 18.5 21.6875 C 19 22.085938 19.695313 22.085938 20.09375 21.6875 L 21.6875 20.09375 C 22.085938 19.59375 22.085938 18.898438 21.6875 18.5 L 16.1875 13 L 21.6875 7.5 C 22.085938 7 22.085938 6.304688 21.6875 5.90625 L 20.09375 4.3125 C 19.59375 3.914063 18.898438 3.914063 18.5 4.3125 L 13 9.8125 L 7.5 4.3125 C 7.25 4.113281 6.945313 4 6.65625 4 Z"></path>
<div class="card-profile"></div> </svg>
<div class="card-profile"></div> </button>
<div class="card-profile"></div> </div>
</div>
<div id="block-sheep-info"> <i id="sheep-addeds-icon"></i>
<div id="sheep-mess"> <div class="addeds-blocks-inputs">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"> <label for="sheep-addeds-name">Імʼя</label>
<path <input id="sheep-addeds-name" type="text" name="name"
d="M 25.042969 12 C 20.698969 12 18 15.029297 18 19.904297 C 18 24.104297 20.766812 28.599609 24.882812 28.599609 C 27.167812 28.599609 31 27.369813 31 19.132812 C 31 14.999813 28.494969 12 25.042969 12 z M 39.974609 12 C 35.672609 12 33 14.931437 33 19.648438 C 33 24.104438 34.862391 28.599609 39.025391 28.599609 C 43.603391 28.599609 46 23.922734 46 19.302734 C 46 14.285734 42.876609 12 39.974609 12 z M 25.042969 16 C 26.388969 16 27 17.623812 27 19.132812 C 27 20.776813 26.794812 24.599609 24.882812 24.599609 C 23.428813 24.599609 22 22.274297 22 19.904297 C 22 16.000297 24.132969 16 25.042969 16 z M 39.974609 16 C 41.224609 16 42 17.265734 42 19.302734 C 42 21.906734 40.886391 24.599609 39.025391 24.599609 C 37.594391 24.599609 37 21.375438 37 19.648438 C 37 16.000438 39.084609 16 39.974609 16 z M 13.689453 24 C 9.8134531 24 7 27.465234 7 32.240234 C 7 35.462234 9.0299688 40 13.542969 40 C 17.224969 40 20 36.250297 20 31.279297 C 20 27.061297 17.346453 24 13.689453 24 z M 50.75 24 C 46.94 24 43 27.440219 43 33.199219 C 43 36.541219 45.15 40 48.75 40 C 52.612 40 56 35.477312 56 30.320312 C 56 26.480313 53.939 24 50.75 24 z M 13.689453 28 C 15.514453 28 16 30.061297 16 31.279297 C 16 33.793297 14.852969 36 13.542969 36 C 11.603969 36 11 33.244234 11 32.240234 C 11 29.783234 12.131453 28 13.689453 28 z M 50.75 28 C 51.783 28 52 29.262313 52 30.320312 C 52 33.788312 49.866 36 48.75 36 C 47.514 36 47 34.175219 47 33.199219 C 47 29.652219 49.237 28 50.75 28 z M 31.806641 30.001953 C 29.371641 29.947953 27.296656 31.058937 25.597656 33.335938 C 22.515656 37.450938 21.695953 38.210313 19.376953 39.695312 C 17.528953 40.875312 14.998047 42.492141 14.998047 45.994141 C 14.998047 49.769141 18.164641 52.839844 22.056641 52.839844 C 23.872641 52.839844 25.588266 52.174453 27.697266 51.439453 C 29.071266 52.348453 30.722125 52.839844 32.453125 52.839844 C 34.215125 52.839844 35.869422 52.353125 37.232422 51.453125 C 41.805422 53.496125 44.150719 53.039344 45.636719 52.277344 C 47.314719 51.416344 48.403953 49.800703 48.876953 47.470703 C 49.107953 46.334703 48.997375 45.198781 48.609375 44.175781 C 48.012375 42.602781 46.757453 41.293531 45.064453 40.644531 C 43.137453 39.916531 42.576406 39.753172 41.316406 38.826172 C 39.672406 37.616172 39.015453 35.742422 37.814453 33.607422 C 36.601453 31.449422 34.298641 30.067953 31.806641 30.001953 z M 31.701172 34.001953 C 32.777172 34.029953 33.808125 34.644359 34.328125 35.568359 C 35.480125 37.621359 36.442312 40.205828 38.945312 42.048828 C 40.748312 43.375828 41.894813 43.725812 43.632812 44.382812 C 44.073813 44.550813 44.501812 44.883844 44.757812 45.339844 C 44.966813 45.711844 45.061031 46.164781 44.957031 46.675781 C 44.738031 47.753781 44.3535 48.44175 43.8125 48.71875 C 42.4475 49.41975 39.736125 48.192875 38.578125 47.671875 C 38.187125 47.495875 36.729781 46.582125 35.550781 47.703125 C 34.778781 48.436125 33.679125 48.839844 32.453125 48.839844 C 31.288125 48.839844 30.211922 48.444562 29.419922 47.726562 C 28.893922 47.247562 28.151609 47.085828 27.474609 47.298828 C 25.411609 47.948828 23.441594 48.839844 22.058594 48.839844 C 20.372594 48.839844 19.001953 47.562141 19.001953 45.994141 C 19.001953 44.795141 19.69525 44.240406 21.53125 43.066406 C 24.29525 41.302406 25.514688 40.146469 28.804688 35.730469 C 29.975687 34.161469 30.981172 33.980953 31.701172 34.001953 z" required />
/> </div>
</svg> <div class="addeds-blocks-inputs">
<h1>Виберіть вісника для редагування</h1> <label for="sheep-addeds-group_id">Група</label>
<select id="sheep-addeds-group_id" name="group_id" required>
<option value selected disabled>Оберіть...</option>
<option value="1">Група 1</option>
<option value="2">Група 2</option>
<option value="3">Група 3</option>
<option value="4">Група 4</option>
<option value="5">Група 5</option>
<option value="6">Група 6</option>
<option value="7">Група 7</option>
<option value="8">Група 8</option>
</select>
</div>
<button id="sheep-addeds-button">Додати</button>
</form>
</div> </div>
<form id="sheep-editor" style="display: none; opacity: 0">
<div class="header">
<h1>Інформація про вісника</h1>
<button onclick="Sheeps.editor.close()" type="button">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 26 26">
<path
d="M 6.65625 4 C 6.367188 4 6.105469 4.113281 5.90625 4.3125 L 4.3125 5.90625 C 3.914063 6.304688 3.914063 7 4.3125 7.5 L 9.8125 13 L 4.3125 18.5 C 3.914063 19 3.914063 19.695313 4.3125 20.09375 L 5.90625 21.6875 C 6.40625 22.085938 7.101563 22.085938 7.5 21.6875 L 13 16.1875 L 18.5 21.6875 C 19 22.085938 19.695313 22.085938 20.09375 21.6875 L 21.6875 20.09375 C 22.085938 19.59375 22.085938 18.898438 21.6875 18.5 L 16.1875 13 L 21.6875 7.5 C 22.085938 7 22.085938 6.304688 21.6875 5.90625 L 20.09375 4.3125 C 19.59375 3.914063 18.898438 3.914063 18.5 4.3125 L 13 9.8125 L 7.5 4.3125 C 7.25 4.113281 6.945313 4 6.65625 4 Z"
></path>
</svg>
</button>
</div>
<i id="sheep-editor-icon"></i>
<div class="editor-blocks-inputs" id="editor-blocks-inputs-uuid">
<label>UUID <span>Натисніть, щоб скопіювати</span></label>
<input
id="sheep-editor-uuid"
type="text"
name="uuid"
value=""
disabled
style="display: none"
/>
<p id="sheep-editor-uuid-copy" style="cursor: copy !important"></p>
</div>
<div class="editor-blocks-inputs">
<label for="sheep-editor-name">Імʼя</label>
<input id="sheep-editor-name" type="text" name="name" required="" />
</div>
<div class="editor-blocks-inputs">
<label for="sheep-editor-group_id">Група</label>
<select id="sheep-editor-group_id" name="group_id">
<option value="1">Група 1</option>
<option value="2" selected>Група 2</option>
<option value="3">Група 3</option>
<option value="4">Група 4</option>
<option value="5">Група 5</option>
<option value="6">Група 6</option>
<option value="7">Група 7</option>
<option value="8">Група 8</option>
</select>
</div>
<div class="editor-blocks-inputs">
<label for="sheep-editor-mode">Права</label>
<select id="sheep-editor-mode" name="mode">
<option value="0" selected>Користувач</option>
<option value="1">Модератор</option>
<option value="2">Адміністратор</option>
</select>
</div>
<div
class="editor-blocks-inputs"
id="editor-blocks-inputs-uuid-moder"
style="display: none"
>
<label>UUID адміністратора/модератора</label>
<p id="sheep-editor-uuid-moder" style="cursor: copy !important"></p>
</div>
<div
class="editor-blocks-checkbox"
id="sheep-editor-access-moder"
style="display: none"
>
<p for="editor-access">Дозволи модератора</p>
<div>
<div class="checkbox">
<input
name="can_view_sheeps"
class="custom-checkbox"
id="sheep-editor-can_view_sheeps"
type="checkbox"
/>
<label for="sheep-editor-can_view_sheeps"> Перегляд списку вісників </label>
</div>
<div class="checkbox">
<input
name="can_add_sheeps"
class="custom-checkbox"
id="sheep-editor-can_add_sheeps"
type="checkbox"
/>
<label for="sheep-editor-can_add_sheeps"> Додавання вісників </label>
</div>
<div class="checkbox">
<input
name="can_manager_sheeps"
class="custom-checkbox"
id="sheep-editor-can_manager_sheeps"
type="checkbox"
/>
<label for="sheep-editor-can_manager_sheeps"> Керування дозволами вісників </label>
</div>
<div class="checkbox">
<input
name="can_add_territory"
class="custom-checkbox"
id="sheep-editor-can_add_territory"
type="checkbox"
/>
<label for="sheep-editor-can_add_territory">
Створення територій
</label>
</div>
<div class="checkbox">
<input
name="can_manager_territory"
class="custom-checkbox"
id="sheep-editor-can_manager_territory"
type="checkbox"
/>
<label for="sheep-editor-can_manager_territory">
Керування територіями
</label>
</div>
<div class="checkbox">
<input
name="can_joint_territory"
class="custom-checkbox"
id="sheep-editor-can_joint_territory"
type="checkbox"
/>
<label for="sheep-editor-can_joint_territory">
Спільний доступ до території
</label>
</div>
<div class="checkbox">
<input
name="can_add_stand"
class="custom-checkbox"
id="sheep-editor-can_add_stand"
type="checkbox"
/>
<label for="sheep-editor-can_add_stand"> Створення стендів </label>
</div>
<div class="checkbox">
<input
name="can_manager_stand"
class="custom-checkbox"
id="sheep-editor-can_manager_stand"
type="checkbox"
/>
<label for="sheep-editor-can_manager_stand"> Керування стендами </label>
</div>
<div class="checkbox">
<input
name="can_add_schedule"
class="custom-checkbox"
id="sheep-editor-can_add_schedule"
type="checkbox"
/>
<label for="sheep-editor-can_add_schedule"> Створення розкладу зібрань </label>
</div>
</div>
</div>
<div class="editor-blocks-checkbox">
<p for="editor-access">Дозволи вісника</p>
<div>
<div class="checkbox">
<input
name="can_view_schedule"
class="custom-checkbox"
id="sheep-editor-can_view_schedule"
type="checkbox"
/>
<label for="sheep-editor-can_view_schedule"> Перегляд розкладу зібрань </label>
</div>
<div class="checkbox">
<input
name="can_view_stand"
class="custom-checkbox"
id="sheep-editor-can_view_stand"
type="checkbox"
/>
<label for="sheep-editor-can_view_stand"> Перегляд стендів </label>
</div>
<div class="checkbox">
<input
name="can_view_territory"
class="custom-checkbox"
id="sheep-editor-can_view_territory"
type="checkbox"
/>
<label for="sheep-editor-can_view_territory">
Перегляд територій
</label>
</div>
</div>
</div>
<button id="sheep-editor-button" style="display: none" type="submit">
Зберегти
</button>
<details id="editor-blocks-territory" style="display: none;">
<summary>Території вісника</summary>
<div>
<div id="title">
<h1>вул. Бродівська 50Б (Під'їзд 1)</h1>
<a
href="/territory/card/house/57"
title="Редактор квартир"
data-route=""
>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48">
<path
d="M 12.5 6 C 8.9280619 6 6 8.9280619 6 12.5 L 6 35.5 C 6 39.071938 8.9280619 42 12.5 42 L 35.5 42 C 39.071938 42 42 39.071938 42 35.5 L 42 12.5 C 42 8.9280619 39.071938 6 35.5 6 L 12.5 6 z M 12.5 9 L 14 9 L 14 15 L 9 15 L 9 12.5 C 9 10.549938 10.549938 9 12.5 9 z M 17 9 L 35.5 9 C 37.450062 9 39 10.549938 39 12.5 L 39 15 L 17 15 L 17 9 z M 9 18 L 14 18 L 14 23 L 9 23 L 9 18 z M 17 18 L 39 18 L 39 23 L 17 23 L 17 18 z M 9 26 L 14 26 L 14 31 L 9 31 L 9 26 z M 17 26 L 39 26 L 39 31 L 17 31 L 17 26 z M 9 34 L 14 34 L 14 39 L 12.5 39 C 10.549938 39 9 37.450062 9 35.5 L 9 34 z M 17 34 L 39 34 L 39 35.5 C 39 37.450062 37.450062 39 35.5 39 L 17 39 L 17 34 z"
></path>
</svg>
</a>
</div>
<div>
<h1>Територія видана:</h1>
<h2>02.07.2025</h2>
</div>
<div>
<h1>Варто забрати:</h1>
<h2>01.11.2025</h2>
</div>
</div>
<div>
<div id="title">
<h1>вул. Бродівська 50Б (Під'їзд 1)</h1>
<a
href="/territory/card/house/57"
title="Редактор квартир"
data-route=""
>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48">
<path
d="M 12.5 6 C 8.9280619 6 6 8.9280619 6 12.5 L 6 35.5 C 6 39.071938 8.9280619 42 12.5 42 L 35.5 42 C 39.071938 42 42 39.071938 42 35.5 L 42 12.5 C 42 8.9280619 39.071938 6 35.5 6 L 12.5 6 z M 12.5 9 L 14 9 L 14 15 L 9 15 L 9 12.5 C 9 10.549938 10.549938 9 12.5 9 z M 17 9 L 35.5 9 C 37.450062 9 39 10.549938 39 12.5 L 39 15 L 17 15 L 17 9 z M 9 18 L 14 18 L 14 23 L 9 23 L 9 18 z M 17 18 L 39 18 L 39 23 L 17 23 L 17 18 z M 9 26 L 14 26 L 14 31 L 9 31 L 9 26 z M 17 26 L 39 26 L 39 31 L 17 31 L 17 26 z M 9 34 L 14 34 L 14 39 L 12.5 39 C 10.549938 39 9 37.450062 9 35.5 L 9 34 z M 17 34 L 39 34 L 39 35.5 C 39 37.450062 37.450062 39 35.5 39 L 17 39 L 17 34 z"
></path>
</svg>
</a>
</div>
<div>
<h1>Територія видана:</h1>
<h2>02.07.2025</h2>
</div>
<div>
<h1>Варто забрати:</h1>
<h2>01.11.2025</h2>
</div>
</div>
<div>
<div id="title">
<h1>вул. Бродівська 50Б (Під'їзд 1)</h1>
<a
href="/territory/card/house/57"
title="Редактор квартир"
data-route=""
>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48">
<path
d="M 12.5 6 C 8.9280619 6 6 8.9280619 6 12.5 L 6 35.5 C 6 39.071938 8.9280619 42 12.5 42 L 35.5 42 C 39.071938 42 42 39.071938 42 35.5 L 42 12.5 C 42 8.9280619 39.071938 6 35.5 6 L 12.5 6 z M 12.5 9 L 14 9 L 14 15 L 9 15 L 9 12.5 C 9 10.549938 10.549938 9 12.5 9 z M 17 9 L 35.5 9 C 37.450062 9 39 10.549938 39 12.5 L 39 15 L 17 15 L 17 9 z M 9 18 L 14 18 L 14 23 L 9 23 L 9 18 z M 17 18 L 39 18 L 39 23 L 17 23 L 17 18 z M 9 26 L 14 26 L 14 31 L 9 31 L 9 26 z M 17 26 L 39 26 L 39 31 L 17 31 L 17 26 z M 9 34 L 14 34 L 14 39 L 12.5 39 C 10.549938 39 9 37.450062 9 35.5 L 9 34 z M 17 34 L 39 34 L 39 35.5 C 39 37.450062 37.450062 39 35.5 39 L 17 39 L 17 34 z"
></path>
</svg>
</a>
</div>
<div>
<h1>Територія видана:</h1>
<h2>02.07.2025</h2>
</div>
<div>
<h1>Варто забрати:</h1>
<h2>01.11.2025</h2>
</div>
</div>
</details>
</form>
</div>
<div id="block-sheep-addeds" style="display: none; opacity: 0">
<form id="sheep-addeds">
<div class="header">
<h1>Додавання нового вісника</h1>
<button onclick="Sheeps.addeds.close()" class="close" type="button">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 26 26">
<path
d="M 6.65625 4 C 6.367188 4 6.105469 4.113281 5.90625 4.3125 L 4.3125 5.90625 C 3.914063 6.304688 3.914063 7 4.3125 7.5 L 9.8125 13 L 4.3125 18.5 C 3.914063 19 3.914063 19.695313 4.3125 20.09375 L 5.90625 21.6875 C 6.40625 22.085938 7.101563 22.085938 7.5 21.6875 L 13 16.1875 L 18.5 21.6875 C 19 22.085938 19.695313 22.085938 20.09375 21.6875 L 21.6875 20.09375 C 22.085938 19.59375 22.085938 18.898438 21.6875 18.5 L 16.1875 13 L 21.6875 7.5 C 22.085938 7 22.085938 6.304688 21.6875 5.90625 L 20.09375 4.3125 C 19.59375 3.914063 18.898438 3.914063 18.5 4.3125 L 13 9.8125 L 7.5 4.3125 C 7.25 4.113281 6.945313 4 6.65625 4 Z"
></path>
</svg>
</button>
</div>
<i id="sheep-addeds-icon"></i>
<div class="addeds-blocks-inputs">
<label for="sheep-addeds-name">Імʼя</label>
<input id="sheep-addeds-name" type="text" name="name" required="" />
</div>
<div class="addeds-blocks-inputs">
<label for="sheep-addeds-group_id">Група</label>
<select id="sheep-addeds-group_id" name="group_id" required>
<option value="" selected disabled>Оберіть...</option>
<option value="1">Група 1</option>
<option value="2">Група 2</option>
<option value="3">Група 3</option>
<option value="4">Група 4</option>
<option value="5">Група 5</option>
<option value="6">Група 6</option>
<option value="7">Група 7</option>
<option value="8">Група 8</option>
</select>
</div>
<button id="sheep-addeds-button">Додати</button>
</form>
</div>
</div> </div>
+43 -13
View File
@@ -88,20 +88,20 @@ const SheepsEvents = {
if (response.ok) { if (response.ok) {
sheepEditorButton.innerText = "Зберегти"; sheepEditorButton.innerText = "Зберегти";
Notifier.success("Успішно збережено!", {timeout: 2000}) Notifier.success("Успішно збережено!", { timeout: 2000 })
const data = await response.json(); const data = await response.json();
console.log(data); console.log(data);
Sheeps.sheeps_list.list = []; Sheeps.sheeps_list.list = [];
await Sheeps.sheeps_list.setHTML(); await Sheeps.sheeps_list.setHTML({});
} else { } else {
sheepEditorButton.innerText = "Зберегти"; sheepEditorButton.innerText = "Зберегти";
console.error('Помилка збереження'); console.error('Помилка збереження');
Notifier.error("Помилка збереження!", {timeout: 3000}); Notifier.error("Помилка збереження!", { timeout: 3000 });
} }
} catch (err) { } catch (err) {
console.error(err); console.error(err);
Notifier.error("Помилка збереження!", {timeout: 3000}); Notifier.error("Помилка збереження!", { timeout: 3000 });
} }
return; return;
@@ -143,11 +143,11 @@ const SheepsEvents = {
} else { } else {
sheepEditorButton.innerText = "Додати"; sheepEditorButton.innerText = "Додати";
console.error('Помилка додавання'); console.error('Помилка додавання');
Notifier.error("Помилка додавання!", {timeout: 3000}); Notifier.error("Помилка додавання!", { timeout: 3000 });
} }
} catch (err) { } catch (err) {
console.error(err); console.error(err);
Notifier.error("Помилка додавання!", {timeout: 3000}); Notifier.error("Помилка додавання!", { timeout: 3000 });
} }
return; return;
} }
@@ -182,7 +182,7 @@ const Sheeps = {
return Sheeps.sheeps_list.list return Sheeps.sheeps_list.list
}, },
setHTML: async ({search, filter}) => { setHTML: async ({ search, filter }) => {
let block_sheep_list = document.getElementById('block-sheeps-list'); let block_sheep_list = document.getElementById('block-sheeps-list');
let block_sheep_info = document.getElementById('block-sheep-info'); let block_sheep_info = document.getElementById('block-sheep-info');
block_sheep_list.style.display = "flex"; block_sheep_list.style.display = "flex";
@@ -366,7 +366,7 @@ const Sheeps = {
sheep_editor_uuid.value = sheep.uuid; sheep_editor_uuid.value = sheep.uuid;
sheep_editor_uuid_copy.innerText = sheep.uuid; sheep_editor_uuid_copy.innerText = sheep.uuid;
sheep_editor_uuid_copy.setAttribute("onclick", `clipboard('https://sheep-service.com/?uuid=${sheep.uuid}')`); sheep_editor_uuid_copy.setAttribute("onclick", `clipboard('https://sheep-service.com/?uuid=${sheep.uuid}');Sheeps.editor.genQR('${sheep.uuid}');`);
sheep_editor_mode.value = sheep.mode; sheep_editor_mode.value = sheep.mode;
@@ -379,7 +379,7 @@ const Sheeps = {
sheep_editor_access_moder.style.display = ""; sheep_editor_access_moder.style.display = "";
editor_blocks_inputs_uuid_moder.style.display = ""; editor_blocks_inputs_uuid_moder.style.display = "";
sheep_editor_uuid_moder.innerText = sheep.uuid_manager; sheep_editor_uuid_moder.innerText = sheep.uuid_manager;
sheep_editor_uuid_moder.setAttribute("onclick", `clipboard('https://sheep-service.com/?uuid=${sheep.uuid_manager}')`); sheep_editor_uuid_moder.setAttribute("onclick", `clipboard('https://sheep-service.com/?uuid=${sheep.uuid_manager}');Sheeps.editor.genQR('${sheep.uuid_manager}');`);
} else if (USER.mode == 2 && (sheep.mode == 0)) { } else if (USER.mode == 2 && (sheep.mode == 0)) {
sheep_editor_access_moder.style.display = "none"; sheep_editor_access_moder.style.display = "none";
editor_blocks_inputs_uuid_moder.style.display = "none"; editor_blocks_inputs_uuid_moder.style.display = "none";
@@ -405,6 +405,8 @@ const Sheeps = {
sheep_editor_can_view_stand.checked = sheep.possibilities.can_view_stand; sheep_editor_can_view_stand.checked = sheep.possibilities.can_view_stand;
sheep_editor_can_view_territory.checked = sheep.possibilities.can_view_territory; sheep_editor_can_view_territory.checked = sheep.possibilities.can_view_territory;
Sheeps.editor.genQR(sheep.uuid);
if (USER.possibilities.can_manager_territory) { if (USER.possibilities.can_manager_territory) {
document.getElementById('editor-blocks-territory').style.display = "none"; document.getElementById('editor-blocks-territory').style.display = "none";
document.getElementById(`editor-blocks-territory`).innerHTML = "<summary>Території вісника</summary>"; document.getElementById(`editor-blocks-territory`).innerHTML = "<summary>Території вісника</summary>";
@@ -437,6 +439,34 @@ const Sheeps = {
}, 10); }, 10);
Router.navigate(`sheeps`); Router.navigate(`sheeps`);
},
genQR: (hash) => {
document.getElementById("qr-code").innerHTML = "";
const qrCode = new QRCodeStyling({
width: 250,
height: 250,
type: "svg",
data: `https://sheep-service.com/?uuid=${hash}`,
backgroundOptions: {
color: "#fbfbfb",
},
dotsOptions: {
color: "#313131",
type: "rounded"
},
cornersSquareOptions: {
color: "#313131",
type: "extra-rounded"
},
cornersDotOptions: {
color: "#313131",
type: "dot"
},
});
// 2. Відмальовуємо QR-код у вибраний контейнер
qrCode.append(document.getElementById("qr-code"));
} }
}, },
addeds: { addeds: {
@@ -479,7 +509,7 @@ const Sheeps = {
const sheepFilterForm = document.getElementById("block-sheeps-list-filter"); const sheepFilterForm = document.getElementById("block-sheeps-list-filter");
const current = sheepFilterForm.dataset.state; const current = sheepFilterForm.dataset.state;
if(current === 'closed') Sheeps.search.close(); if (current === 'closed') Sheeps.search.close();
sheepFilterForm.dataset.state = current === 'open' ? 'closed' : 'open'; sheepFilterForm.dataset.state = current === 'open' ? 'closed' : 'open';
}, },
close: () => { close: () => {
@@ -490,7 +520,7 @@ const Sheeps = {
console.log(value); console.log(value);
filter_value = value || ""; filter_value = value || "";
Sheeps.sheeps_list.setHTML({filter: filter_value}); Sheeps.sheeps_list.setHTML({ filter: filter_value });
} }
}, },
search: { search: {
@@ -498,7 +528,7 @@ const Sheeps = {
const sheepSearchForm = document.getElementById("block-sheeps-list-search"); const sheepSearchForm = document.getElementById("block-sheeps-list-search");
const current = sheepSearchForm.dataset.state; const current = sheepSearchForm.dataset.state;
if(current === 'closed') Sheeps.filter.close(); if (current === 'closed') Sheeps.filter.close();
sheepSearchForm.dataset.state = current === 'open' ? 'closed' : 'open'; sheepSearchForm.dataset.state = current === 'open' ? 'closed' : 'open';
}, },
close: () => { close: () => {
@@ -509,7 +539,7 @@ const Sheeps = {
console.log(value); console.log(value);
search_value = value?.trim()?.toLowerCase() || ""; search_value = value?.trim()?.toLowerCase() || "";
Sheeps.sheeps_list.setHTML({search: search_value}); Sheeps.sheeps_list.setHTML({ search: search_value });
} }
}, },
territory: { territory: {
+27 -8
View File
@@ -112,7 +112,7 @@
border-radius: 0px 0px 10px 10px; border-radius: 0px 0px 10px 10px;
} }
#block-sheeps-list>.filter>select{ #block-sheeps-list>.filter>select {
width: 100%; width: 100%;
padding: 0 5px; padding: 0 5px;
border-radius: calc(var(--border-radius) - 5px - 4px); border-radius: calc(var(--border-radius) - 5px - 4px);
@@ -297,15 +297,15 @@
fill: var(--PrimaryColor); fill: var(--PrimaryColor);
} }
#block-sheep-info>#sheep-editor>.editor-blocks-inputs { #block-sheep-info>#sheep-editor .editor-blocks-inputs {
width: 100%; width: 100%;
display: flex; display: flex;
margin: 10px 0; margin: 0 0 10px 0;
align-items: flex-start; align-items: flex-start;
flex-direction: column; flex-direction: column;
} }
#block-sheep-info>#sheep-editor>.editor-blocks-inputs label { #block-sheep-info>#sheep-editor .editor-blocks-inputs label {
display: flex; display: flex;
justify-content: center; justify-content: center;
flex-direction: row; flex-direction: row;
@@ -314,12 +314,12 @@
margin-bottom: 5px; margin-bottom: 5px;
} }
#block-sheep-info>#sheep-editor>.editor-blocks-inputs label>span { #block-sheep-info>#sheep-editor .editor-blocks-inputs label>span {
opacity: 0.5; opacity: 0.5;
margin-left: 5px; margin-left: 5px;
} }
#block-sheep-info>#sheep-editor>.editor-blocks-inputs input { #block-sheep-info>#sheep-editor .editor-blocks-inputs input {
width: calc(100% - 10px); width: calc(100% - 10px);
min-width: 140px; min-width: 140px;
padding: 0 5px; padding: 0 5px;
@@ -330,7 +330,7 @@
font-size: var(--FontSize2); font-size: var(--FontSize2);
} }
#block-sheep-info>#sheep-editor>.editor-blocks-inputs select { #block-sheep-info>#sheep-editor .editor-blocks-inputs select {
width: 100%; width: 100%;
min-width: 140px; min-width: 140px;
padding: 0 5px; padding: 0 5px;
@@ -340,7 +340,7 @@
color: var(--ColorThemes3); color: var(--ColorThemes3);
} }
#block-sheep-info>#sheep-editor>.editor-blocks-inputs p { #block-sheep-info>#sheep-editor .editor-blocks-inputs p {
display: flex; display: flex;
width: calc(100% - 10px); width: calc(100% - 10px);
min-width: 140px; min-width: 140px;
@@ -352,6 +352,7 @@
color: var(--ColorThemes3); color: var(--ColorThemes3);
align-items: center; align-items: center;
cursor: copy; cursor: copy;
margin: 0;
} }
#block-sheep-info>#sheep-editor>.editor-blocks-checkbox { #block-sheep-info>#sheep-editor>.editor-blocks-checkbox {
@@ -405,6 +406,13 @@
margin: 10px 0 0 0; margin: 10px 0 0 0;
text-transform: uppercase; text-transform: uppercase;
} }
#block-sheep-info>#sheep-editor>button[type="button"]{
background: var(--ColorThemes3);
color: var(--ColorThemes0);
height: 30px;
font-size: var(--FontSize1);
font-weight: 400;
}
#block-sheep-addeds { #block-sheep-addeds {
width: 100%; width: 100%;
@@ -640,6 +648,17 @@
align-items: flex-start; align-items: flex-start;
} }
#qr-code {
width: 100%;
padding: 10px 0;
display: flex;
align-items: center;
justify-content: center;
background: #fbfbfb;
border-radius: calc(var(--border-radius) - 5px - 4px);
box-shadow: var(--shadow-l1);
}
@media (min-width: 1001px), @media (min-width: 1001px),
+1
View File
@@ -63,6 +63,7 @@
opacity: 0.9; opacity: 0.9;
font-weight: 400; font-weight: 400;
font-size: var(--FontSize3); font-size: var(--FontSize3);
margin: 0;
} }
#stand-info img { #stand-info img {
+5 -2
View File
@@ -117,7 +117,7 @@
} }
.page-stand-list>details>#list { .page-stand-list>details>#list {
width: 100%; width: calc(100% - 20px);
margin: 0; margin: 0;
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
@@ -127,6 +127,10 @@
overflow-y: auto; overflow-y: auto;
align-items: flex-start; align-items: flex-start;
transition: .3s ease; transition: .3s ease;
gap: 20px;
padding: 10px;
justify-content: center;
flex-wrap: wrap;
} }
.page-stand-list>details>#list>.mess-list { .page-stand-list>details>#list>.mess-list {
@@ -159,7 +163,6 @@
width: 300px; width: 300px;
height: 200px; height: 200px;
background-color: var(--ColorThemes2); background-color: var(--ColorThemes2);
margin: 10px;
overflow: hidden; overflow: hidden;
cursor: pointer; cursor: pointer;
border-radius: calc(var(--border-radius) - 5px); border-radius: calc(var(--border-radius) - 5px);
+563
View File
@@ -0,0 +1,563 @@
<!DOCTYPE html>
<html lang="uk">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Вхід - Sheep Service</title>
<link rel="icon" href="/img/favicon.ico">
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
.container {
background: white;
border-radius: 12px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
max-width: 500px;
width: 100%;
padding: 40px;
}
.logo {
text-align: center;
margin-bottom: 30px;
}
.logo img {
width: 80px;
height: 80px;
}
h1 {
text-align: center;
color: #333;
margin-bottom: 10px;
font-size: 28px;
}
.subtitle {
text-align: center;
color: #999;
margin-bottom: 30px;
font-size: 14px;
}
.tabs {
display: flex;
gap: 10px;
margin-bottom: 30px;
border-bottom: 2px solid #eee;
}
.tab-button {
flex: 1;
padding: 12px;
border: none;
background: none;
font-size: 16px;
cursor: pointer;
color: #999;
transition: all 0.3s;
border-bottom: 3px solid transparent;
margin-bottom: -2px;
font-weight: 500;
}
.tab-button.active {
color: #667eea;
border-bottom-color: #667eea;
}
.tab-content {
display: none;
}
.tab-content.active {
display: block;
}
.form-group {
margin-bottom: 20px;
}
label {
display: block;
margin-bottom: 8px;
color: #333;
font-weight: 500;
font-size: 14px;
}
input {
width: 100%;
padding: 12px;
border: 2px solid #eee;
border-radius: 6px;
font-size: 14px;
transition: border-color 0.3s;
}
input:focus {
outline: none;
border-color: #667eea;
}
button {
width: 100%;
padding: 12px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
border-radius: 6px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: transform 0.2s;
margin-top: 10px;
}
button:hover:not(:disabled) {
transform: translateY(-2px);
}
button:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.message {
padding: 12px;
border-radius: 6px;
margin-bottom: 20px;
font-size: 14px;
display: none;
}
.message.show {
display: block;
}
.message.success {
background: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.message.error {
background: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
.loading {
text-align: center;
color: #999;
font-size: 14px;
margin: 20px 0;
}
.info {
background: #f0f4ff;
padding: 12px;
border-radius: 6px;
margin-bottom: 20px;
font-size: 13px;
color: #555;
line-height: 1.6;
}
.sessions-list {
margin-top: 20px;
}
.session-item {
background: #f8f9fa;
padding: 12px;
border-radius: 6px;
margin-bottom: 10px;
display: flex;
justify-content: space-between;
align-items: center;
font-size: 13px;
}
.session-info {
flex: 1;
}
.session-device {
font-weight: 600;
color: #333;
}
.session-date {
font-size: 12px;
color: #999;
margin-top: 3px;
}
.session-status {
font-size: 11px;
padding: 3px 8px;
background: #d4edda;
color: #155724;
border-radius: 3px;
display: inline-block;
margin-top: 3px;
}
.session-action {
background: #f08080;
color: white;
padding: 6px 10px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
}
.session-action:hover {
background: #dc6b6b;
}
.logout-all {
background: #c82333;
width: 100%;
margin-top: 15px;
}
.logout-all:hover:not(:disabled) {
background: #a01a2a;
}
.logout-btn {
background: #6c757d;
margin-top: 15px;
}
.logout-btn:hover:not(:disabled) {
background: #5a6268;
}
.user-info {
background: #f0f4ff;
padding: 15px;
border-radius: 6px;
margin-bottom: 20px;
text-align: center;
}
.user-name {
font-weight: 600;
color: #333;
font-size: 16px;
margin-bottom: 5px;
}
.user-id {
font-size: 12px;
color: #999;
}
</style>
</head>
<body>
<div class="container">
<div class="logo">
<img src="/img/sheep-icon.png" alt="Sheep Service" onerror="this.style.display='none'">
</div>
<h1>🐑 Sheep Service</h1>
<p class="subtitle">Безпечна автентифікація через WebAuthn</p>
<div class="tabs">
<button class="tab-button active" data-tab="login">Вхід</button>
<button class="tab-button" data-tab="register">Реєстрація</button>
<button class="tab-button" data-tab="sessions">Сесії</button>
</div>
<!-- Login Tab -->
<div id="login" class="tab-content active">
<div id="loginMessage" class="message"></div>
<div class="info">
💡 Введіть UUID вашого облікового запису для входу.
</div>
<div class="form-group">
<label>UUID облікового запису</label>
<input type="text" id="loginUUID" placeholder="Введіть UUID" required>
</div>
<div class="form-group">
<label>Назва пристрою (опціонально)</label>
<input type="text" id="deviceName" placeholder="наприклад: iPhone 14 Pro">
</div>
<button id="loginBtn" onclick="handleLogin()">Увійти з WebAuthn</button>
</div>
<!-- Register Tab -->
<div id="register" class="tab-content">
<div id="registerMessage" class="message"></div>
<div class="info">
✅ Зареєструвати новий пристрій для входу через біометрію або PIN код.
</div>
<div class="form-group">
<label>UUID облікового запису</label>
<input type="text" id="registerUUID" placeholder="Введіть UUID" required>
</div>
<button id="registerBtn" onclick="handleRegister()">Зареєструвати WebAuthn</button>
</div>
<!-- Sessions Tab -->
<div id="sessions" class="tab-content">
<div id="sessionsMessage" class="message"></div>
<div id="userInfoContainer"></div>
<div id="sessionsList" class="sessions-list"></div>
<div id="sessionsActionContainer"></div>
</div>
</div>
<script src="/config.js"></script>
<script src="/js/auth-service.js"></script>
<script>
// Tab switching
document.querySelectorAll('.tab-button').forEach(btn => {
btn.addEventListener('click', () => {
const tabName = btn.dataset.tab;
document.querySelectorAll('.tab-content').forEach(tab => {
tab.classList.remove('active');
});
document.querySelectorAll('.tab-button').forEach(b => {
b.classList.remove('active');
});
document.getElementById(tabName).classList.add('active');
btn.classList.add('active');
if (tabName === 'sessions') {
loadSessions();
}
});
});
// Redirect if already authenticated
if (authService.isAuthenticated()) {
// Show sessions tab
document.querySelector('[data-tab="sessions"]').click();
}
function showMessage(elementId, message, type = 'success') {
const el = document.getElementById(elementId);
el.textContent = message;
el.className = `message ${type} show`;
setTimeout(() => {
el.classList.remove('show');
}, 5000);
}
async function handleRegister() {
const uuid = document.getElementById('registerUUID').value;
if (!uuid) {
showMessage('registerMessage', 'Будь ласка, введіть UUID', 'error');
return;
}
try {
document.getElementById('registerBtn').disabled = true;
// Get registration options
const { sheep_id, options } = await authService.startRegistration(uuid);
// Create credential
const credential = await navigator.credentials.create({
publicKey: {
...options,
challenge: new Uint8Array(atob(options.challenge).split('').map(c => c.charCodeAt(0))),
user: {
...options.user,
id: new Uint8Array(atob(options.user.id).split('').map(c => c.charCodeAt(0)))
}
}
});
if (!credential) {
throw new Error('Не вдалося створити облік');
}
// Verify registration
await authService.completeRegistration(sheep_id, credential, options.challenge);
showMessage('registerMessage', 'WebAuthn успішно зареєстрований! Тепер ви можете входити.', 'success');
document.getElementById('registerUUID').value = '';
setTimeout(() => {
document.querySelector('[data-tab="login"]').click();
}, 2000);
} catch (error) {
showMessage('registerMessage', `Помилка: ${error.message}`, 'error');
} finally {
document.getElementById('registerBtn').disabled = false;
}
}
async function handleLogin() {
const uuid = document.getElementById('loginUUID').value;
const deviceName = document.getElementById('deviceName').value;
if (!uuid) {
showMessage('loginMessage', 'Будь ласка, введіть UUID', 'error');
return;
}
try {
document.getElementById('loginBtn').disabled = true;
// Get authentication options
const { sheep_id, options } = await authService.startLogin(uuid);
// Get credential
const credential = await navigator.credentials.get({
publicKey: {
...options,
challenge: new Uint8Array(atob(options.challenge).split('').map(c => c.charCodeAt(0))),
allowCredentials: options.allowCredentials.map(cred => ({
...cred,
id: new Uint8Array(atob(cred.id).split('').map(c => c.charCodeAt(0)))
}))
}
});
if (!credential) {
throw new Error('Не вдалося отримати облік');
}
// Verify authentication
const result = await authService.completeLogin(sheep_id, credential, options.challenge, deviceName);
showMessage('loginMessage', `Ласкаво просимо, ${result.user.name}!`, 'success');
document.getElementById('loginUUID').value = '';
document.getElementById('deviceName').value = '';
// Redirect to main page
setTimeout(() => {
window.location.href = '/';
}, 2000);
} catch (error) {
showMessage('loginMessage', `Помилка: ${error.message}`, 'error');
} finally {
document.getElementById('loginBtn').disabled = false;
}
}
async function loadSessions() {
try {
if (!authService.isAuthenticated()) {
showMessage('sessionsMessage', 'Будь ласка, спочатку увійдіть', 'error');
document.getElementById('userInfoContainer').innerHTML = '';
document.getElementById('sessionsList').innerHTML = '';
document.getElementById('sessionsActionContainer').innerHTML = '';
return;
}
// Show user info
const user = authService.getCurrentUser();
if (user) {
document.getElementById('userInfoContainer').innerHTML = `
<div class="user-info">
<div class="user-name">${user.name}</div>
<div class="user-id">ID: ${user.id}</div>
</div>
`;
}
// Load sessions
const sessions = await authService.getSessions();
if (sessions.length === 0) {
document.getElementById('sessionsList').innerHTML = '<p style="text-align: center; color: #999;">Немає активних сесій</p>';
} else {
const listHtml = sessions.map(session => `
<div class="session-item">
<div class="session-info">
<div class="session-device">📱 ${session.device_name}</div>
<div class="session-date">Створена: ${new Date(session.created_at).toLocaleString('uk-UA')}</div>
<div class="session-date">IP: ${session.ip_address}</div>
<span class="session-status">${session.status.toUpperCase()}</span>
</div>
<button class="session-action" onclick="deleteSession(${session.id})">Видалити</button>
</div>
`).join('');
document.getElementById('sessionsList').innerHTML = listHtml;
}
// Show action buttons
document.getElementById('sessionsActionContainer').innerHTML = `
<button class="logout-all" onclick="logoutAll()">Вихід зі всіх пристроїв</button>
<button class="logout-btn" onclick="logout()">Вийти з цього пристрою</button>
`;
} catch (error) {
showMessage('sessionsMessage', `Помилка: ${error.message}`, 'error');
}
}
async function deleteSession(sessionId) {
try {
await authService.deleteSession(sessionId);
loadSessions();
} catch (error) {
showMessage('sessionsMessage', `Помилка: ${error.message}`, 'error');
}
}
async function logoutAll() {
if (!confirm('Ви впевнені? Це видалить вас зі всіх пристроїв.')) {
return;
}
try {
await authService.logoutAll();
showMessage('sessionsMessage', 'Ви вийшли зі всіх пристроїв', 'success');
setTimeout(() => {
window.location.href = '/login.html';
}, 2000);
} catch (error) {
showMessage('sessionsMessage', `Помилка: ${error.message}`, 'error');
}
}
async function logout() {
try {
await authService.logout();
showMessage('sessionsMessage', 'Ви вийшли', 'success');
setTimeout(() => {
window.location.href = '/login.html';
}, 1000);
} catch (error) {
showMessage('sessionsMessage', `Помилка: ${error.message}`, 'error');
}
}
// Auto load sessions on init
if (authService.isAuthenticated()) {
loadSessions();
}
</script>
</body>
</html>
+298
View File
@@ -0,0 +1,298 @@
<!DOCTYPE html>
<html lang="uk">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Авторизація</title>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/jsQR.min.js"></script>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
background-color: #121212;
color: #ffffff;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
}
/* Контейнер авторизации */
#page-auth {
background: #1e1e1e;
padding: 30px;
border-radius: 16px;
border: 1px solid #2c2c2e;
box-shadow: 0 4px 20px rgba(0,0,0,0.3);
width: 100%;
max-width: 320px;
}
#page-auth-form div {
display: flex;
flex-direction: column;
gap: 12px;
}
input[type="password"] {
background-color: #2c2c2e;
border: 1px solid #3a3a3c;
color: #fff;
padding: 12px;
border-radius: 8px;
font-size: 16px;
outline: none;
}
button[type="submit"], .btn-scan {
background-color: #007aff;
color: white;
border: none;
padding: 12px;
border-radius: 8px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: background 0.2s;
}
button[type="submit"]:hover { background-color: #0056b3; }
.btn-scan {
background-color: #34c759;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
}
.btn-scan:hover { background-color: #248a3d; }
/* Стили для нативного <dialog> */
dialog {
background: #1e1e1e;
color: white;
border: 1px solid #2c2c2e;
border-radius: 20px;
padding: 20px;
width: 90%;
max-width: 360px;
margin: auto;
box-shadow: 0 10px 30px rgba(0,0,0,0.5);
}
/* Затемнение фона вокруг модалки */
dialog::backdrop {
background: rgba(0, 0, 0, 0.75);
backdrop-filter: blur(4px); /* Легкое размытие заднего плана */
}
.dialog-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}
.btn-close {
background: none;
border: none;
color: #8e8e93;
font-size: 24px;
cursor: pointer;
padding: 0 5px;
}
/* Контейнер камеры внутри модалки */
.scanner-container {
position: relative;
width: 100%;
aspect-ratio: 1 / 1;
border-radius: 12px;
overflow: hidden;
background-color: #000;
}
#video {
width: 100%;
height: 100%;
object-fit: cover;
}
.laser-box {
position: absolute;
top: 15%; left: 15%; width: 70%; height: 70%;
border: 2px solid rgba(52, 199, 89, 0.5);
border-radius: 12px;
pointer-events: none;
}
.laser-line {
position: absolute;
width: 100%; height: 2px;
background-color: #34c759;
box-shadow: 0 0 8px #34c759;
top: 0;
animation: scan 2s linear infinite;
}
@keyframes scan {
0% { top: 0%; }
50% { top: 100%; }
100% { top: 0%; }
}
#loading-status {
position: absolute;
top: 50%; left: 50%;
transform: translate(-50%, -50%);
color: #8e8e93;
text-align: center;
}
</style>
</head>
<body>
<div id="page-auth">
<form id="page-auth-form">
<div>
<input
type="password"
name="uuid"
id="auth-forms-uuid"
placeholder="UUID"
required=""
/>
<button type="button" id="open-scanner-btn" class="btn-scan">
<span>Сканувати QR-код</span>
</button>
<button type="submit">
<span>Авторизуватися</span>
</button>
</div>
</form>
</div>
<dialog id="scanner-dialog">
<div class="dialog-header">
<h3 style="margin:0; font-size:18px;">Сканування коду</h3>
<button class="btn-close" id="close-scanner-btn" aria-label="Закрити">&times;</button>
</div>
<div class="scanner-container">
<div id="loading-status">Запуск камери...</div>
<video id="video" autoplay playsinline></video>
<div class="laser-box">
<div class="laser-line"></div>
</div>
</div>
</dialog>
<canvas id="canvas" style="display: none;"></canvas>
<script>
const dialog = document.getElementById('scanner-dialog');
const openBtn = document.getElementById('open-scanner-btn');
const closeBtn = document.getElementById('close-scanner-btn');
const uuidInput = document.getElementById('auth-forms-uuid');
const video = document.getElementById('video');
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d', { willReadFrequently: true });
const loadingStatus = document.getElementById('loading-status');
let streamRef = null;
let isScanning = false;
// --- Логика управления сканером ---
async function startCamera() {
loadingStatus.style.display = 'block';
loadingStatus.innerText = "Запуск камери...";
isScanning = true;
try {
const stream = await navigator.mediaDevices.getUserMedia({
video: { facingMode: { exact: "environment" } }
});
streamRef = stream;
video.srcObject = stream;
video.setAttribute("playsinline", true);
video.play();
video.addEventListener('loadedmetadata', () => {
loadingStatus.style.display = 'none';
requestAnimationFrame(tick);
});
} catch (err) {
// Фолбек, если основная камера недоступна (например, на ПК)
try {
const fallbackStream = await navigator.mediaDevices.getUserMedia({ video: true });
streamRef = fallbackStream;
video.srcObject = fallbackStream;
video.play();
loadingStatus.style.display = 'none';
requestAnimationFrame(tick);
} catch (fallbackErr) {
loadingStatus.innerText = "Помилка доступу до камери";
}
}
}
function stopCamera() {
isScanning = false;
if (streamRef) {
streamRef.getTracks().forEach(track => track.stop());
streamRef = null;
}
video.srcObject = null;
}
function tick() {
if (!isScanning) return;
if (video.readyState === video.HAVE_ENOUGH_DATA) {
canvas.height = video.videoHeight;
canvas.width = video.videoWidth;
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const code = jsQR(imageData.data, imageData.width, imageData.height, {
inversionAttempts: "dontInvert",
});
if (code && code.data.trim() !== "") {
// Успешно считали UUID
uuidInput.value = code.data;
// Закрываем модалку (событие close сработает автоматически)
dialog.close();
return;
}
}
requestAnimationFrame(tick);
}
// --- Слушатели событий модального окна ---
// Открыть сканер
openBtn.addEventListener('click', () => {
dialog.showModal(); // Открывает как модальное окно (с бэкдропом)
startCamera();
});
// Закрыть сканер по кнопке крестика
closeBtn.addEventListener('click', () => {
dialog.close();
});
// Важно: событие 'close' срабатывает всегда (и при клике на крестик, и при нажатии Esc)
dialog.addEventListener('close', () => {
stopCamera();
});
</script>
</body>
</html>
+13 -2
View File
@@ -1,4 +1,4 @@
const STATIC_CACHE_NAME = 'v2.2.9'; const STATIC_CACHE_NAME = 'v2.2.32';
const FILES_TO_CACHE = [ const FILES_TO_CACHE = [
'/', '/',
@@ -24,6 +24,8 @@ const FILES_TO_CACHE = [
"/lib/components/cloud.js", "/lib/components/cloud.js",
"/lib/components/qr-code-styling.js",
"/lib/components/jsQR.min.js",
"/lib/components/clipboard.js", "/lib/components/clipboard.js",
"/lib/components/colorGroup.js", "/lib/components/colorGroup.js",
"/lib/components/makeid.js", "/lib/components/makeid.js",
@@ -153,7 +155,16 @@ self.addEventListener("push", event => {
data: data.url || "/" data: data.url || "/"
}; };
event.waitUntil(self.registration.showNotification(title, options)); const notificationPromise = self.registration.showNotification(title, options);
let badgePromise = Promise.resolve();
if ('setAppBadge' in navigator) {
const count = data.badgeCount || 1;
badgePromise = navigator.setAppBadge(count);
}
event.waitUntil(Promise.all([notificationPromise, badgePromise]));
}); });
self.addEventListener("notificationclick", event => { self.addEventListener("notificationclick", event => {
+122
View File
@@ -0,0 +1,122 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta charset="UTF-8" />
<!-- <meta name="viewport" content="width=device-width, initial-scale=1.0" /> -->
<meta
name="viewport"
content="user-scalable=no, initial-scale=1.0, maximum-scale=1.0" />
<title>Document Test</title>
<link rel="icon" href="/img/favicon.ico" sizes="any" />
<link rel="apple-touch-icon" href="/img/apple-touch-icon.png" />
<link rel="manifest" href="/manifest.json" />
<meta
name="theme-color"
content="#e5e5df"
media="(prefers-color-scheme: light)" />
<meta
name="theme-color"
content="#252523"
media="(prefers-color-scheme: dark)" />
<!-- Конфигурация SW -->
<script src="/sw.js"></script>
<link rel="stylesheet" href="/css/main.css" />
</head>
<body>
<style>
.test {
padding: 20px;
display: flex;
gap: 10px;
flex-direction: column;
}
.test label {
display: flex;
align-items: center;
gap: 10px;
justify-content: space-between;
}
</style>
<div class="test">
<details open>
<summary>What does <code>:open</code> do?</summary>
<p>The <code>:open</code> pseudo-class matches when certain
elements
are in an open state.</p>
</details>
<details>
<summary>Which elements support it?</summary>
<p>It works with <code>&lt;details&gt;</code>,
<code>&lt;dialog&gt;</code>, <code>&lt;select&gt;</code>,
and
<code>&lt;input&gt;</code> elements like <code>date</code>
and
<code>color</code>.</p>
</details>
<label>
<code>hanging-punctuation: first last</code>
<input type="checkbox" switch checked class="hang-toggle">
</label>
<label>
Show guide
<input type="checkbox" switch class="guide-toggle">
</label>
<div popover="hint" id="tooltip">
This tooltip wont close the menu!
</div>
<button popovertarget="my-cool-popover">Открыть всплывающее
окно</button>
<div id="my-cool-popover" popover>
<h3>Привет, я Popover! 👋</h3>
<p>Я открылся без единой строчки JavaScript.</p>
<button popovertarget="my-cool-popover"
popovertargetaction="hide">Закрыть</button>
</div>
<div class="example">
<h3>&lt;dialog&gt;</h3>
<button id="open-dialog"
onclick="document.getElementById('demo-dialog').showModal()">Open
dialog</button>
<dialog id="demo-dialog">
<h4>This dialog matches <code>:open</code></h4>
<p>While showing, the dialog is styled using the
<code>:open</code>
pseudo-class.</p>
<button
onclick="navigator.setAppBadge(2);document.getElementById('demo-dialog').close()">Badge</button>
<button id="close-dialog"
onclick="document.getElementById('demo-dialog').close()">Close</button>
</dialog>
</div>
</div>
<br><br>
<a href="/faceid.html">Face ID</a>
<br><br>
<a href="/qr.html">QR Code Scanner</a>
<br><br>
<a href="/">Go back</a>
</body>
</html>