Виправлення помилок.
Додан новий метод авторізації в застосунок.
This commit is contained in:
+3
-3
@@ -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
@@ -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;
|
||||||
|
|||||||
@@ -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.
@@ -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>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Vendored
+8
File diff suppressed because one or more lines are too long
@@ -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
@@ -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')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -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(); // Видалити всі сповіщення
|
||||||
|
*/
|
||||||
@@ -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 {
|
||||||
|
|||||||
@@ -6,12 +6,37 @@
|
|||||||
name="uuid"
|
name="uuid"
|
||||||
id="auth-forms-uuid"
|
id="auth-forms-uuid"
|
||||||
placeholder="UUID"
|
placeholder="UUID"
|
||||||
required=""
|
required />
|
||||||
/>
|
|
||||||
|
|
||||||
<button type="submit">
|
<button type="submit">
|
||||||
<span>Авторизуватися</span>
|
<span>Авторизуватися</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<button type="button" id="open-scanner-btn" class="btn-scan"
|
||||||
|
style="background: var(--ColorThemes3);color: var(--ColorThemes0);">
|
||||||
|
<span>Сканувати QR-код</span>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</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>
|
||||||
|
|||||||
@@ -3,9 +3,16 @@ 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();
|
||||||
|
|
||||||
|
Auth.submit();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
submit: async () => {
|
||||||
const uuid = document
|
const uuid = document
|
||||||
.getElementById("auth-forms-uuid")
|
.getElementById("auth-forms-uuid")
|
||||||
.value
|
.value
|
||||||
@@ -77,6 +84,110 @@ const Auth = {
|
|||||||
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>`,
|
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'
|
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();
|
||||||
|
});
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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%;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,19 +4,21 @@
|
|||||||
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"
|
||||||
|
onchange="Options.optionFont.edit(this.value)">
|
||||||
<option value="small">Маленький</option>
|
<option value="small">Маленький</option>
|
||||||
<option value="medium">Середній</option>
|
<option value="medium">Середній</option>
|
||||||
<option value="large">Великий</option>
|
<option value="large">Великий</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<a href="/test.html">Тестова сторінка</a>
|
||||||
|
|
||||||
<button onclick="Options.logout()">Вийти з облікового запису</button>
|
<button onclick="Options.logout()">Вийти з облікового запису</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -7,8 +7,7 @@
|
|||||||
<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>
|
||||||
@@ -18,8 +17,7 @@
|
|||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
id="block-sheeps-list-search-name"
|
id="block-sheeps-list-search-name"
|
||||||
placeholder="Імʼя вісника"
|
placeholder="Імʼя вісника" />
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card-profile"></div>
|
<div class="card-profile"></div>
|
||||||
@@ -40,8 +38,7 @@
|
|||||||
<div id="sheep-mess">
|
<div id="sheep-mess">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
||||||
<path
|
<path
|
||||||
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"
|
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>
|
</svg>
|
||||||
<h1>Виберіть вісника для редагування</h1>
|
<h1>Виберіть вісника для редагування</h1>
|
||||||
</div>
|
</div>
|
||||||
@@ -53,27 +50,16 @@
|
|||||||
<button onclick="Sheeps.editor.close()" type="button">
|
<button onclick="Sheeps.editor.close()" type="button">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 26 26">
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 26 26">
|
||||||
<path
|
<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"
|
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>
|
||||||
></path>
|
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<i id="sheep-editor-icon"></i>
|
<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">
|
<div class="editor-blocks-inputs">
|
||||||
<label for="sheep-editor-name">Імʼя</label>
|
<label for="sheep-editor-name">Імʼя</label>
|
||||||
<input id="sheep-editor-name" type="text" name="name" required="" />
|
<input id="sheep-editor-name" type="text" name="name"
|
||||||
|
required />
|
||||||
</div>
|
</div>
|
||||||
<div class="editor-blocks-inputs">
|
<div class="editor-blocks-inputs">
|
||||||
<label for="sheep-editor-group_id">Група</label>
|
<label for="sheep-editor-group_id">Група</label>
|
||||||
@@ -97,20 +83,42 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</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
|
<div
|
||||||
class="editor-blocks-inputs"
|
class="editor-blocks-inputs"
|
||||||
id="editor-blocks-inputs-uuid-moder"
|
id="editor-blocks-inputs-uuid-moder"
|
||||||
style="display: none"
|
style="display: none">
|
||||||
>
|
|
||||||
<label>UUID адміністратора/модератора</label>
|
<label>UUID адміністратора/модератора</label>
|
||||||
<p id="sheep-editor-uuid-moder" style="cursor: copy !important"></p>
|
<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>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="editor-blocks-checkbox"
|
class="editor-blocks-checkbox"
|
||||||
id="sheep-editor-access-moder"
|
id="sheep-editor-access-moder"
|
||||||
style="display: none"
|
style="display: none">
|
||||||
>
|
|
||||||
<p for="editor-access">Дозволи модератора</p>
|
<p for="editor-access">Дозволи модератора</p>
|
||||||
<div>
|
<div>
|
||||||
<div class="checkbox">
|
<div class="checkbox">
|
||||||
@@ -118,35 +126,34 @@
|
|||||||
name="can_view_sheeps"
|
name="can_view_sheeps"
|
||||||
class="custom-checkbox"
|
class="custom-checkbox"
|
||||||
id="sheep-editor-can_view_sheeps"
|
id="sheep-editor-can_view_sheeps"
|
||||||
type="checkbox"
|
type="checkbox" />
|
||||||
/>
|
<label for="sheep-editor-can_view_sheeps"> Перегляд
|
||||||
<label for="sheep-editor-can_view_sheeps"> Перегляд списку вісників </label>
|
списку вісників </label>
|
||||||
</div>
|
</div>
|
||||||
<div class="checkbox">
|
<div class="checkbox">
|
||||||
<input
|
<input
|
||||||
name="can_add_sheeps"
|
name="can_add_sheeps"
|
||||||
class="custom-checkbox"
|
class="custom-checkbox"
|
||||||
id="sheep-editor-can_add_sheeps"
|
id="sheep-editor-can_add_sheeps"
|
||||||
type="checkbox"
|
type="checkbox" />
|
||||||
/>
|
<label for="sheep-editor-can_add_sheeps"> Додавання
|
||||||
<label for="sheep-editor-can_add_sheeps"> Додавання вісників </label>
|
вісників </label>
|
||||||
</div>
|
</div>
|
||||||
<div class="checkbox">
|
<div class="checkbox">
|
||||||
<input
|
<input
|
||||||
name="can_manager_sheeps"
|
name="can_manager_sheeps"
|
||||||
class="custom-checkbox"
|
class="custom-checkbox"
|
||||||
id="sheep-editor-can_manager_sheeps"
|
id="sheep-editor-can_manager_sheeps"
|
||||||
type="checkbox"
|
type="checkbox" />
|
||||||
/>
|
<label for="sheep-editor-can_manager_sheeps"> Керування
|
||||||
<label for="sheep-editor-can_manager_sheeps"> Керування дозволами вісників </label>
|
дозволами вісників </label>
|
||||||
</div>
|
</div>
|
||||||
<div class="checkbox">
|
<div class="checkbox">
|
||||||
<input
|
<input
|
||||||
name="can_add_territory"
|
name="can_add_territory"
|
||||||
class="custom-checkbox"
|
class="custom-checkbox"
|
||||||
id="sheep-editor-can_add_territory"
|
id="sheep-editor-can_add_territory"
|
||||||
type="checkbox"
|
type="checkbox" />
|
||||||
/>
|
|
||||||
<label for="sheep-editor-can_add_territory">
|
<label for="sheep-editor-can_add_territory">
|
||||||
Створення територій
|
Створення територій
|
||||||
</label>
|
</label>
|
||||||
@@ -156,8 +163,7 @@
|
|||||||
name="can_manager_territory"
|
name="can_manager_territory"
|
||||||
class="custom-checkbox"
|
class="custom-checkbox"
|
||||||
id="sheep-editor-can_manager_territory"
|
id="sheep-editor-can_manager_territory"
|
||||||
type="checkbox"
|
type="checkbox" />
|
||||||
/>
|
|
||||||
<label for="sheep-editor-can_manager_territory">
|
<label for="sheep-editor-can_manager_territory">
|
||||||
Керування територіями
|
Керування територіями
|
||||||
</label>
|
</label>
|
||||||
@@ -167,8 +173,7 @@
|
|||||||
name="can_joint_territory"
|
name="can_joint_territory"
|
||||||
class="custom-checkbox"
|
class="custom-checkbox"
|
||||||
id="sheep-editor-can_joint_territory"
|
id="sheep-editor-can_joint_territory"
|
||||||
type="checkbox"
|
type="checkbox" />
|
||||||
/>
|
|
||||||
<label for="sheep-editor-can_joint_territory">
|
<label for="sheep-editor-can_joint_territory">
|
||||||
Спільний доступ до території
|
Спільний доступ до території
|
||||||
</label>
|
</label>
|
||||||
@@ -178,27 +183,27 @@
|
|||||||
name="can_add_stand"
|
name="can_add_stand"
|
||||||
class="custom-checkbox"
|
class="custom-checkbox"
|
||||||
id="sheep-editor-can_add_stand"
|
id="sheep-editor-can_add_stand"
|
||||||
type="checkbox"
|
type="checkbox" />
|
||||||
/>
|
<label for="sheep-editor-can_add_stand"> Створення
|
||||||
<label for="sheep-editor-can_add_stand"> Створення стендів </label>
|
стендів </label>
|
||||||
</div>
|
</div>
|
||||||
<div class="checkbox">
|
<div class="checkbox">
|
||||||
<input
|
<input
|
||||||
name="can_manager_stand"
|
name="can_manager_stand"
|
||||||
class="custom-checkbox"
|
class="custom-checkbox"
|
||||||
id="sheep-editor-can_manager_stand"
|
id="sheep-editor-can_manager_stand"
|
||||||
type="checkbox"
|
type="checkbox" />
|
||||||
/>
|
<label for="sheep-editor-can_manager_stand"> Керування
|
||||||
<label for="sheep-editor-can_manager_stand"> Керування стендами </label>
|
стендами </label>
|
||||||
</div>
|
</div>
|
||||||
<div class="checkbox">
|
<div class="checkbox">
|
||||||
<input
|
<input
|
||||||
name="can_add_schedule"
|
name="can_add_schedule"
|
||||||
class="custom-checkbox"
|
class="custom-checkbox"
|
||||||
id="sheep-editor-can_add_schedule"
|
id="sheep-editor-can_add_schedule"
|
||||||
type="checkbox"
|
type="checkbox" />
|
||||||
/>
|
<label for="sheep-editor-can_add_schedule"> Створення
|
||||||
<label for="sheep-editor-can_add_schedule"> Створення розкладу зібрань </label>
|
розкладу зібрань </label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -211,26 +216,25 @@
|
|||||||
name="can_view_schedule"
|
name="can_view_schedule"
|
||||||
class="custom-checkbox"
|
class="custom-checkbox"
|
||||||
id="sheep-editor-can_view_schedule"
|
id="sheep-editor-can_view_schedule"
|
||||||
type="checkbox"
|
type="checkbox" />
|
||||||
/>
|
<label for="sheep-editor-can_view_schedule"> Перегляд
|
||||||
<label for="sheep-editor-can_view_schedule"> Перегляд розкладу зібрань </label>
|
розкладу зібрань </label>
|
||||||
</div>
|
</div>
|
||||||
<div class="checkbox">
|
<div class="checkbox">
|
||||||
<input
|
<input
|
||||||
name="can_view_stand"
|
name="can_view_stand"
|
||||||
class="custom-checkbox"
|
class="custom-checkbox"
|
||||||
id="sheep-editor-can_view_stand"
|
id="sheep-editor-can_view_stand"
|
||||||
type="checkbox"
|
type="checkbox" />
|
||||||
/>
|
<label for="sheep-editor-can_view_stand"> Перегляд
|
||||||
<label for="sheep-editor-can_view_stand"> Перегляд стендів </label>
|
стендів </label>
|
||||||
</div>
|
</div>
|
||||||
<div class="checkbox">
|
<div class="checkbox">
|
||||||
<input
|
<input
|
||||||
name="can_view_territory"
|
name="can_view_territory"
|
||||||
class="custom-checkbox"
|
class="custom-checkbox"
|
||||||
id="sheep-editor-can_view_territory"
|
id="sheep-editor-can_view_territory"
|
||||||
type="checkbox"
|
type="checkbox" />
|
||||||
/>
|
|
||||||
<label for="sheep-editor-can_view_territory">
|
<label for="sheep-editor-can_view_territory">
|
||||||
Перегляд територій
|
Перегляд територій
|
||||||
</label>
|
</label>
|
||||||
@@ -238,7 +242,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button id="sheep-editor-button" style="display: none" type="submit">
|
<button id="sheep-editor-button" style="display: none"
|
||||||
|
type="submit">
|
||||||
Зберегти
|
Зберегти
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
@@ -252,12 +257,11 @@
|
|||||||
<a
|
<a
|
||||||
href="/territory/card/house/57"
|
href="/territory/card/house/57"
|
||||||
title="Редактор квартир"
|
title="Редактор квартир"
|
||||||
data-route=""
|
data-route>
|
||||||
>
|
<svg xmlns="http://www.w3.org/2000/svg"
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48">
|
viewBox="0 0 48 48">
|
||||||
<path
|
<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"
|
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>
|
||||||
></path>
|
|
||||||
</svg>
|
</svg>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -278,12 +282,11 @@
|
|||||||
<a
|
<a
|
||||||
href="/territory/card/house/57"
|
href="/territory/card/house/57"
|
||||||
title="Редактор квартир"
|
title="Редактор квартир"
|
||||||
data-route=""
|
data-route>
|
||||||
>
|
<svg xmlns="http://www.w3.org/2000/svg"
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48">
|
viewBox="0 0 48 48">
|
||||||
<path
|
<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"
|
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>
|
||||||
></path>
|
|
||||||
</svg>
|
</svg>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -304,12 +307,11 @@
|
|||||||
<a
|
<a
|
||||||
href="/territory/card/house/57"
|
href="/territory/card/house/57"
|
||||||
title="Редактор квартир"
|
title="Редактор квартир"
|
||||||
data-route=""
|
data-route>
|
||||||
>
|
<svg xmlns="http://www.w3.org/2000/svg"
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48">
|
viewBox="0 0 48 48">
|
||||||
<path
|
<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"
|
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>
|
||||||
></path>
|
|
||||||
</svg>
|
</svg>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -330,11 +332,11 @@
|
|||||||
<form id="sheep-addeds">
|
<form id="sheep-addeds">
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<h1>Додавання нового вісника</h1>
|
<h1>Додавання нового вісника</h1>
|
||||||
<button onclick="Sheeps.addeds.close()" class="close" type="button">
|
<button onclick="Sheeps.addeds.close()" class="close"
|
||||||
|
type="button">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 26 26">
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 26 26">
|
||||||
<path
|
<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"
|
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>
|
||||||
></path>
|
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -342,12 +344,13 @@
|
|||||||
<i id="sheep-addeds-icon"></i>
|
<i id="sheep-addeds-icon"></i>
|
||||||
<div class="addeds-blocks-inputs">
|
<div class="addeds-blocks-inputs">
|
||||||
<label for="sheep-addeds-name">Імʼя</label>
|
<label for="sheep-addeds-name">Імʼя</label>
|
||||||
<input id="sheep-addeds-name" type="text" name="name" required="" />
|
<input id="sheep-addeds-name" type="text" name="name"
|
||||||
|
required />
|
||||||
</div>
|
</div>
|
||||||
<div class="addeds-blocks-inputs">
|
<div class="addeds-blocks-inputs">
|
||||||
<label for="sheep-addeds-group_id">Група</label>
|
<label for="sheep-addeds-group_id">Група</label>
|
||||||
<select id="sheep-addeds-group_id" name="group_id" required>
|
<select id="sheep-addeds-group_id" name="group_id" required>
|
||||||
<option value="" selected disabled>Оберіть...</option>
|
<option value selected disabled>Оберіть...</option>
|
||||||
<option value="1">Група 1</option>
|
<option value="1">Група 1</option>
|
||||||
<option value="2">Група 2</option>
|
<option value="2">Група 2</option>
|
||||||
<option value="3">Група 3</option>
|
<option value="3">Група 3</option>
|
||||||
|
|||||||
@@ -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: {
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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="Закрити">×</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>
|
||||||
@@ -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
@@ -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><details></code>,
|
||||||
|
<code><dialog></code>, <code><select></code>,
|
||||||
|
and
|
||||||
|
<code><input></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 won’t 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><dialog></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>
|
||||||
Reference in New Issue
Block a user