Add Yandex Metrika to all 19 pages, clean dashboard about stats

This commit is contained in:
treamz 2026-03-22 11:56:07 +03:00
parent 356362c815
commit f16a2ac32d
19 changed files with 3056 additions and 2834 deletions

View File

@ -366,5 +366,17 @@ function barColor(pct) {
return 'bg-danger'; return 'bg-danger';
} }
</script> </script>
<!-- Yandex.Metrika counter -->
<script type="text/javascript">
(function(m,e,t,r,i,k,a){
m[i]=m[i]||function(){(m[i].a=m[i].a||[]).push(arguments)};
m[i].l=1*new Date();
for (var j = 0; j < document.scripts.length; j++) {if (document.scripts[j].src === r) { return; }}
k=e.createElement(t),a=e.getElementsByTagName(t)[0],k.async=1,k.src=r,a.parentNode.insertBefore(k,a)
})(window, document,'script','https://mc.yandex.ru/metrika/tag.js?id=108185982', 'ym');
ym(108185982, 'init', {ssr:true, webvisor:true, clickmap:true, ecommerce:"dataLayer", referrer: document.referrer, url: location.href, accurateTrackBounce:true, trackLinks:true});
</script>
<noscript><div><img src="https://mc.yandex.ru/watch/108185982" style="position:absolute; left:-9999px;" alt="" /></div></noscript>
<!-- /Yandex.Metrika counter -->
</body> </body>
</html> </html>

View File

@ -197,5 +197,17 @@ function mdToHtml(md) {
return marked.parse(md); return marked.parse(md);
} }
</script> </script>
<!-- Yandex.Metrika counter -->
<script type="text/javascript">
(function(m,e,t,r,i,k,a){
m[i]=m[i]||function(){(m[i].a=m[i].a||[]).push(arguments)};
m[i].l=1*new Date();
for (var j = 0; j < document.scripts.length; j++) {if (document.scripts[j].src === r) { return; }}
k=e.createElement(t),a=e.getElementsByTagName(t)[0],k.async=1,k.src=r,a.parentNode.insertBefore(k,a)
})(window, document,'script','https://mc.yandex.ru/metrika/tag.js?id=108185982', 'ym');
ym(108185982, 'init', {ssr:true, webvisor:true, clickmap:true, ecommerce:"dataLayer", referrer: document.referrer, url: location.href, accurateTrackBounce:true, trackLinks:true});
</script>
<noscript><div><img src="https://mc.yandex.ru/watch/108185982" style="position:absolute; left:-9999px;" alt="" /></div></noscript>
<!-- /Yandex.Metrika counter -->
</body> </body>
</html> </html>

View File

@ -53,13 +53,6 @@
.cat-count { font-size:11px; color:var(--text-muted); font-family:'JetBrains Mono',monospace; background:var(--surface-600); padding:2px 8px; border-radius:6px; } .cat-count { font-size:11px; color:var(--text-muted); font-family:'JetBrains Mono',monospace; background:var(--surface-600); padding:2px 8px; border-radius:6px; }
.cat-desc { font-size:12px; color:var(--text-muted); margin-left:auto; font-family:'JetBrains Mono',monospace; } .cat-desc { font-size:12px; color:var(--text-muted); margin-left:auto; font-family:'JetBrains Mono',monospace; }
.about-card { background:var(--surface-800); border:1px solid var(--surface-600); border-radius:16px; padding:24px; margin-top:40px; }
.about-card h2 { font-size:16px; font-weight:700; color:var(--text-primary); margin-bottom:12px; }
.about-card p { font-size:13px; color:var(--text-secondary); line-height:1.7; }
.about-stats { display:flex; gap:24px; margin-top:16px; flex-wrap:wrap; }
.about-stat { text-align:center; }
.about-stat .num { font-size:24px; font-weight:800; color:var(--accent); font-family:'JetBrains Mono',monospace; }
.about-stat .label { font-size:11px; color:var(--text-muted); margin-top:2px; }
@media(max-width:640px) { .cat-desc { display:none; } } @media(max-width:640px) { .cat-desc { display:none; } }
</style> </style>
@ -200,17 +193,6 @@
</div> </div>
<div class="about-stat">
<div class="num">4</div>
<div class="label">категории</div>
</div>
<div class="about-stat">
<div class="num">14</div>
<div class="label">инструментов</div>
</div>
</div>
</div>
</div> </div>
</div> </div>
@ -236,5 +218,17 @@ fetch('/api/content/dashboard').then(r => r.ok ? r.json() : []).then(blocks => {
} }
}).catch(() => {}); }).catch(() => {});
</script> </script>
<!-- Yandex.Metrika counter -->
<script type="text/javascript">
(function(m,e,t,r,i,k,a){
m[i]=m[i]||function(){(m[i].a=m[i].a||[]).push(arguments)};
m[i].l=1*new Date();
for (var j = 0; j < document.scripts.length; j++) {if (document.scripts[j].src === r) { return; }}
k=e.createElement(t),a=e.getElementsByTagName(t)[0],k.async=1,k.src=r,a.parentNode.insertBefore(k,a)
})(window, document,'script','https://mc.yandex.ru/metrika/tag.js?id=108185982', 'ym');
ym(108185982, 'init', {ssr:true, webvisor:true, clickmap:true, ecommerce:"dataLayer", referrer: document.referrer, url: location.href, accurateTrackBounce:true, trackLinks:true});
</script>
<noscript><div><img src="https://mc.yandex.ru/watch/108185982" style="position:absolute; left:-9999px;" alt="" /></div></noscript>
<!-- /Yandex.Metrika counter -->
</body> </body>
</html> </html>

View File

@ -246,5 +246,17 @@
<input type="file" id="fileInput" accept="image/*" style="display:none"> <input type="file" id="fileInput" accept="image/*" style="display:none">
<script src="/editor.js" defer></script> <script src="/editor.js" defer></script>
<!-- Yandex.Metrika counter -->
<script type="text/javascript">
(function(m,e,t,r,i,k,a){
m[i]=m[i]||function(){(m[i].a=m[i].a||[]).push(arguments)};
m[i].l=1*new Date();
for (var j = 0; j < document.scripts.length; j++) {if (document.scripts[j].src === r) { return; }}
k=e.createElement(t),a=e.getElementsByTagName(t)[0],k.async=1,k.src=r,a.parentNode.insertBefore(k,a)
})(window, document,'script','https://mc.yandex.ru/metrika/tag.js?id=108185982', 'ym');
ym(108185982, 'init', {ssr:true, webvisor:true, clickmap:true, ecommerce:"dataLayer", referrer: document.referrer, url: location.href, accurateTrackBounce:true, trackLinks:true});
</script>
<noscript><div><img src="https://mc.yandex.ru/watch/108185982" style="position:absolute; left:-9999px;" alt="" /></div></noscript>
<!-- /Yandex.Metrika counter -->
</body> </body>
</html> </html>

View File

@ -173,5 +173,17 @@ function formatJs(input) {
return result; return result;
} }
</script> </script>
<!-- Yandex.Metrika counter -->
<script type="text/javascript">
(function(m,e,t,r,i,k,a){
m[i]=m[i]||function(){(m[i].a=m[i].a||[]).push(arguments)};
m[i].l=1*new Date();
for (var j = 0; j < document.scripts.length; j++) {if (document.scripts[j].src === r) { return; }}
k=e.createElement(t),a=e.getElementsByTagName(t)[0],k.async=1,k.src=r,a.parentNode.insertBefore(k,a)
})(window, document,'script','https://mc.yandex.ru/metrika/tag.js?id=108185982', 'ym');
ym(108185982, 'init', {ssr:true, webvisor:true, clickmap:true, ecommerce:"dataLayer", referrer: document.referrer, url: location.href, accurateTrackBounce:true, trackLinks:true});
</script>
<noscript><div><img src="https://mc.yandex.ru/watch/108185982" style="position:absolute; left:-9999px;" alt="" /></div></noscript>
<!-- /Yandex.Metrika counter -->
</body> </body>
</html> </html>

View File

@ -701,5 +701,17 @@
renderHistory(); renderHistory();
})(); })();
</script> </script>
<!-- Yandex.Metrika counter -->
<script type="text/javascript">
(function(m,e,t,r,i,k,a){
m[i]=m[i]||function(){(m[i].a=m[i].a||[]).push(arguments)};
m[i].l=1*new Date();
for (var j = 0; j < document.scripts.length; j++) {if (document.scripts[j].src === r) { return; }}
k=e.createElement(t),a=e.getElementsByTagName(t)[0],k.async=1,k.src=r,a.parentNode.insertBefore(k,a)
})(window, document,'script','https://mc.yandex.ru/metrika/tag.js?id=108185982', 'ym');
ym(108185982, 'init', {ssr:true, webvisor:true, clickmap:true, ecommerce:"dataLayer", referrer: document.referrer, url: location.href, accurateTrackBounce:true, trackLinks:true});
</script>
<noscript><div><img src="https://mc.yandex.ru/watch/108185982" style="position:absolute; left:-9999px;" alt="" /></div></noscript>
<!-- /Yandex.Metrika counter -->
</body> </body>
</html> </html>

View File

@ -1580,6 +1580,18 @@
<!-- Yandex.Metrika counter -->
<script type="text/javascript">
(function(m,e,t,r,i,k,a){
m[i]=m[i]||function(){(m[i].a=m[i].a||[]).push(arguments)};
m[i].l=1*new Date();
for (var j = 0; j < document.scripts.length; j++) {if (document.scripts[j].src === r) { return; }}
k=e.createElement(t),a=e.getElementsByTagName(t)[0],k.async=1,k.src=r,a.parentNode.insertBefore(k,a)
})(window, document,'script','https://mc.yandex.ru/metrika/tag.js?id=108185982', 'ym');
ym(108185982, 'init', {ssr:true, webvisor:true, clickmap:true, ecommerce:"dataLayer", referrer: document.referrer, url: location.href, accurateTrackBounce:true, trackLinks:true});
</script>
<noscript><div><img src="https://mc.yandex.ru/watch/108185982" style="position:absolute; left:-9999px;" alt="" /></div></noscript>
<!-- /Yandex.Metrika counter -->
</body> </body>
</html> </html>

View File

@ -1,125 +1,137 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="ru" class="dark"> <html lang="ru" class="dark">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Вход — WA Dev Tools</title> <title>Вход — WA Dev Tools</title>
<script src="/vendor/tailwind.js"></script> <script src="/vendor/tailwind.js"></script>
<script> <script>
tailwind.config = { tailwind.config = {
darkMode: 'class', darkMode: 'class',
theme: { extend: { theme: { extend: {
fontFamily: { mono: ['JetBrains Mono', 'monospace'], sans: ['Manrope', 'sans-serif'] }, fontFamily: { mono: ['JetBrains Mono', 'monospace'], sans: ['Manrope', 'sans-serif'] },
colors: { surface: { 900: '#060c18', 800: '#091020', 700: '#0d1828', 600: '#162040' }, accent: { DEFAULT: '#0054e6', dim: '#0043b8', bright: '#6b9fff' } } colors: { surface: { 900: '#060c18', 800: '#091020', 700: '#0d1828', 600: '#162040' }, accent: { DEFAULT: '#0054e6', dim: '#0043b8', bright: '#6b9fff' } }
}} }}
}; };
</script> </script>
<link href="/vendor/fonts.css" rel="stylesheet"> <link href="/vendor/fonts.css" rel="stylesheet">
<link href="/shared.css" rel="stylesheet"> <link href="/shared.css" rel="stylesheet">
<style> <style>
/* Content */ /* Content */
.page-body { padding-top:56px; min-height:100vh; display:flex; align-items:center; justify-content:center; } .page-body { padding-top:56px; min-height:100vh; display:flex; align-items:center; justify-content:center; }
.auth-card { max-width:420px; width:100%; padding:20px; } .auth-card { max-width:420px; width:100%; padding:20px; }
.auth-card-inner { background:var(--surface-700); border:1px solid var(--surface-600); border-radius:16px; padding:36px 32px; box-shadow:0 4px 24px rgba(0,0,0,.1); } .auth-card-inner { background:var(--surface-700); border:1px solid var(--surface-600); border-radius:16px; padding:36px 32px; box-shadow:0 4px 24px rgba(0,0,0,.1); }
.auth-title { font-size:24px; font-weight:800; color:var(--text-primary); text-align:center; } .auth-title { font-size:24px; font-weight:800; color:var(--text-primary); text-align:center; }
.auth-subtitle { font-size:14px; color:var(--text-muted); text-align:center; margin-top:6px; } .auth-subtitle { font-size:14px; color:var(--text-muted); text-align:center; margin-top:6px; }
.auth-label { display:block; font-size:13px; color:var(--text-muted); margin-bottom:6px; font-family:'JetBrains Mono',monospace; font-weight:500; } .auth-label { display:block; font-size:13px; color:var(--text-muted); margin-bottom:6px; font-family:'JetBrains Mono',monospace; font-weight:500; }
.auth-input { width:100%; background:var(--select-bg); border:1px solid var(--surface-600); color:var(--select-color); padding:12px 16px; border-radius:10px; font-family:'Manrope',sans-serif; font-size:15px; outline:none; transition:border-color .2s; } .auth-input { width:100%; background:var(--select-bg); border:1px solid var(--surface-600); color:var(--select-color); padding:12px 16px; border-radius:10px; font-family:'Manrope',sans-serif; font-size:15px; outline:none; transition:border-color .2s; }
.auth-input:focus { border-color:var(--accent); } .auth-input:focus { border-color:var(--accent); }
.auth-input::placeholder { color:var(--text-muted); opacity:.6; } .auth-input::placeholder { color:var(--text-muted); opacity:.6; }
.auth-btn { width:100%; padding:14px; background:var(--accent); color:#fff; border:none; border-radius:10px; font-family:'Manrope',sans-serif; font-weight:700; font-size:16px; cursor:pointer; transition:background .2s; } .auth-btn { width:100%; padding:14px; background:var(--accent); color:#fff; border:none; border-radius:10px; font-family:'Manrope',sans-serif; font-weight:700; font-size:16px; cursor:pointer; transition:background .2s; }
.auth-btn:hover { background:var(--accent-dim); } .auth-btn:hover { background:var(--accent-dim); }
.auth-btn:disabled { opacity:0.5; cursor:not-allowed; } .auth-btn:disabled { opacity:0.5; cursor:not-allowed; }
.auth-error { background:rgba(255,82,82,.08); border:1px solid rgba(255,82,82,.25); color:#ff5252; padding:10px 14px; border-radius:8px; font-size:13px; display:none; } .auth-error { background:rgba(255,82,82,.08); border:1px solid rgba(255,82,82,.25); color:#ff5252; padding:10px 14px; border-radius:8px; font-size:13px; display:none; }
.auth-error.visible { display:block; } .auth-error.visible { display:block; }
.auth-footer { text-align:center; margin-top:20px; font-size:14px; color:var(--text-muted); } .auth-footer { text-align:center; margin-top:20px; font-size:14px; color:var(--text-muted); }
.auth-footer a { color:var(--accent); text-decoration:none; font-weight:600; } .auth-footer a { color:var(--accent); text-decoration:none; font-weight:600; }
.auth-footer a:hover { text-decoration:underline; } .auth-footer a:hover { text-decoration:underline; }
.auth-divider { height:1px; background:var(--surface-600); margin:20px 0; } .auth-divider { height:1px; background:var(--surface-600); margin:20px 0; }
</style> </style>
<script> <script>
// Theme init (before render) // Theme init (before render)
(function(){ (function(){
const s = localStorage.getItem('theme'); const s = localStorage.getItem('theme');
if (s === '' || (s === null && !window.matchMedia('(prefers-color-scheme: dark)').matches)) { if (s === '' || (s === null && !window.matchMedia('(prefers-color-scheme: dark)').matches)) {
document.documentElement.classList.remove('dark'); document.documentElement.classList.remove('dark');
} }
})(); })();
</script> </script>
</head> </head>
<body> <body>
<header class="top-header"> <header class="top-header">
<a href="/" class="top-logo"> <a href="/" class="top-logo">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="color:var(--accent)"><path stroke-linecap="round" stroke-linejoin="round" d="M17.25 6.75L22.5 12l-5.25 5.25m-10.5 0L1.5 12l5.25-5.25m7.5-3l-4.5 16.5"/></svg> <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="color:var(--accent)"><path stroke-linecap="round" stroke-linejoin="round" d="M17.25 6.75L22.5 12l-5.25 5.25m-10.5 0L1.5 12l5.25-5.25m7.5-3l-4.5 16.5"/></svg>
WA Dev <span>Tools</span> WA Dev <span>Tools</span>
</a> </a>
<nav class="top-nav"> <nav class="top-nav">
<a href="/">Главная</a> <a href="/">Главная</a>
<a href="/#tools">Инструменты</a> <a href="/#tools">Инструменты</a>
</nav> </nav>
<div class="top-right"> <div class="top-right">
<button class="theme-btn" id="themeBtn" title="Переключить тему"> <button class="theme-btn" id="themeBtn" title="Переключить тему">
<svg class="sun-i" width="16" height="16" fill="currentColor" viewBox="0 0 20 20"><path d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z"/></svg> <svg class="sun-i" width="16" height="16" fill="currentColor" viewBox="0 0 20 20"><path d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z"/></svg>
<svg class="moon-i" width="16" height="16" fill="currentColor" viewBox="0 0 20 20"><path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z"/></svg> <svg class="moon-i" width="16" height="16" fill="currentColor" viewBox="0 0 20 20"><path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z"/></svg>
</button> </button>
<a href="/auth/login" class="btn-ghost active">Войти</a> <a href="/auth/login" class="btn-ghost active">Войти</a>
<a href="/auth/register" class="btn-primary-sm">Регистрация</a> <a href="/auth/register" class="btn-primary-sm">Регистрация</a>
</div> </div>
</header> </header>
<div class="page-body"> <div class="page-body">
<div class="auth-card"> <div class="auth-card">
<div class="auth-card-inner"> <div class="auth-card-inner">
<div class="auth-title">Вход в аккаунт</div> <div class="auth-title">Вход в аккаунт</div>
<div class="auth-subtitle">Добро пожаловать обратно</div> <div class="auth-subtitle">Добро пожаловать обратно</div>
<div id="error" class="auth-error" style="margin-top:20px;"></div> <div id="error" class="auth-error" style="margin-top:20px;"></div>
<form id="loginForm" style="display:flex;flex-direction:column;gap:16px;margin-top:24px;" onsubmit="return handleLogin(event)"> <form id="loginForm" style="display:flex;flex-direction:column;gap:16px;margin-top:24px;" onsubmit="return handleLogin(event)">
<div> <div>
<label class="auth-label">Email</label> <label class="auth-label">Email</label>
<input class="auth-input" type="email" name="email" required autofocus autocomplete="email" placeholder="you@example.com" /> <input class="auth-input" type="email" name="email" required autofocus autocomplete="email" placeholder="you@example.com" />
</div> </div>
<div> <div>
<label class="auth-label">Пароль</label> <label class="auth-label">Пароль</label>
<input class="auth-input" type="password" name="password" required autocomplete="current-password" placeholder="Минимум 6 символов" /> <input class="auth-input" type="password" name="password" required autocomplete="current-password" placeholder="Минимум 6 символов" />
</div> </div>
<button class="auth-btn" type="submit" id="submitBtn">Войти</button> <button class="auth-btn" type="submit" id="submitBtn">Войти</button>
</form> </form>
<div class="auth-divider"></div> <div class="auth-divider"></div>
<div class="auth-footer">Нет аккаунта? <a href="/auth/register">Зарегистрируйтесь</a></div> <div class="auth-footer">Нет аккаунта? <a href="/auth/register">Зарегистрируйтесь</a></div>
</div> </div>
</div> </div>
</div> </div>
<script> <script>
// Theme toggle // Theme toggle
const _tb = document.getElementById('themeBtn'); const _tb = document.getElementById('themeBtn');
function _ut() { function _ut() {
const d = document.documentElement.classList.contains('dark'); const d = document.documentElement.classList.contains('dark');
document.querySelectorAll('.sun-i').forEach(e => e.style.display = d ? 'none' : 'block'); document.querySelectorAll('.sun-i').forEach(e => e.style.display = d ? 'none' : 'block');
document.querySelectorAll('.moon-i').forEach(e => e.style.display = d ? 'block' : 'none'); document.querySelectorAll('.moon-i').forEach(e => e.style.display = d ? 'block' : 'none');
} }
_ut(); _ut();
_tb.addEventListener('click', () => { _tb.addEventListener('click', () => {
const d = document.documentElement.classList.toggle('dark'); const d = document.documentElement.classList.toggle('dark');
localStorage.setItem('theme', d ? 'dark' : ''); localStorage.setItem('theme', d ? 'dark' : '');
_ut(); _ut();
}); });
// Login // Login
async function handleLogin(e) { async function handleLogin(e) {
e.preventDefault(); e.preventDefault();
const form = e.target, btn = document.getElementById('submitBtn'), err = document.getElementById('error'); const form = e.target, btn = document.getElementById('submitBtn'), err = document.getElementById('error');
err.classList.remove('visible'); btn.disabled = true; btn.textContent = 'Вход...'; err.classList.remove('visible'); btn.disabled = true; btn.textContent = 'Вход...';
try { try {
const res = await fetch('/auth/login', { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({email:form.email.value,password:form.password.value}) }); const res = await fetch('/auth/login', { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({email:form.email.value,password:form.password.value}) });
const data = await res.json(); const data = await res.json();
if (data.ok) window.location.href = data.redirect || '/dashboard'; if (data.ok) window.location.href = data.redirect || '/dashboard';
else { err.textContent = data.error; err.classList.add('visible'); } else { err.textContent = data.error; err.classList.add('visible'); }
} catch { err.textContent = 'Ошибка сети'; err.classList.add('visible'); } } catch { err.textContent = 'Ошибка сети'; err.classList.add('visible'); }
btn.disabled = false; btn.textContent = 'Войти'; btn.disabled = false; btn.textContent = 'Войти';
} }
</script> </script>
</body> <!-- Yandex.Metrika counter -->
</html> <script type="text/javascript">
(function(m,e,t,r,i,k,a){
m[i]=m[i]||function(){(m[i].a=m[i].a||[]).push(arguments)};
m[i].l=1*new Date();
for (var j = 0; j < document.scripts.length; j++) {if (document.scripts[j].src === r) { return; }}
k=e.createElement(t),a=e.getElementsByTagName(t)[0],k.async=1,k.src=r,a.parentNode.insertBefore(k,a)
})(window, document,'script','https://mc.yandex.ru/metrika/tag.js?id=108185982', 'ym');
ym(108185982, 'init', {ssr:true, webvisor:true, clickmap:true, ecommerce:"dataLayer", referrer: document.referrer, url: location.href, accurateTrackBounce:true, trackLinks:true});
</script>
<noscript><div><img src="https://mc.yandex.ru/watch/108185982" style="position:absolute; left:-9999px;" alt="" /></div></noscript>
<!-- /Yandex.Metrika counter -->
</body>
</html>

View File

@ -231,7 +231,19 @@ document.getElementById('copyHtmlBtn').addEventListener('click', () => {
// Download HTML // Download HTML
document.getElementById('downloadHtmlBtn').addEventListener('click', () => { document.getElementById('downloadHtmlBtn').addEventListener('click', () => {
const body = document.getElementById('mdOutput').innerHTML; const body = document.getElementById('mdOutput').innerHTML;
const fullHtml = '<!DOCTYPE html>\n<html>\n<head>\n<meta charset="UTF-8">\n<title>Markdown</title>\n<style>\nbody{font-family:sans-serif;max-width:800px;margin:40px auto;padding:0 20px;line-height:1.7;color:#333}\npre{background:#f5f5f5;padding:1em;border-radius:6px;overflow-x:auto}\ncode{font-family:monospace;font-size:.9em;background:#f0f0f0;padding:2px 5px;border-radius:3px}\npre code{background:none;padding:0}\ntable{border-collapse:collapse;width:100%}\nth,td{border:1px solid #ddd;padding:8px;text-align:left}\nth{background:#f5f5f5}\nblockquote{border-left:3px solid #0054e6;padding:.5em 1em;margin:1em 0;color:#666;background:#f9f9f9}\nimg{max-width:100%}\n</style>\n</head>\n<body>\n' + body + '\n</body>\n</html>'; const fullHtml = '<!DOCTYPE html>\n<html>\n<head>\n<meta charset="UTF-8">\n<title>Markdown</title>\n<style>\nbody{font-family:sans-serif;max-width:800px;margin:40px auto;padding:0 20px;line-height:1.7;color:#333}\npre{background:#f5f5f5;padding:1em;border-radius:6px;overflow-x:auto}\ncode{font-family:monospace;font-size:.9em;background:#f0f0f0;padding:2px 5px;border-radius:3px}\npre code{background:none;padding:0}\ntable{border-collapse:collapse;width:100%}\nth,td{border:1px solid #ddd;padding:8px;text-align:left}\nth{background:#f5f5f5}\nblockquote{border-left:3px solid #0054e6;padding:.5em 1em;margin:1em 0;color:#666;background:#f9f9f9}\nimg{max-width:100%}\n</style>\n</head>\n<body>\n' + body + '\n<!-- Yandex.Metrika counter -->
<script type="text/javascript">
(function(m,e,t,r,i,k,a){
m[i]=m[i]||function(){(m[i].a=m[i].a||[]).push(arguments)};
m[i].l=1*new Date();
for (var j = 0; j < document.scripts.length; j++) {if (document.scripts[j].src === r) { return; }}
k=e.createElement(t),a=e.getElementsByTagName(t)[0],k.async=1,k.src=r,a.parentNode.insertBefore(k,a)
})(window, document,'script','https://mc.yandex.ru/metrika/tag.js?id=108185982', 'ym');
ym(108185982, 'init', {ssr:true, webvisor:true, clickmap:true, ecommerce:"dataLayer", referrer: document.referrer, url: location.href, accurateTrackBounce:true, trackLinks:true});
</script>
<noscript><div><img src="https://mc.yandex.ru/watch/108185982" style="position:absolute; left:-9999px;" alt="" /></div></noscript>
<!-- /Yandex.Metrika counter -->
</body>\n</html>';
const blob = new Blob([fullHtml], { type: 'text/html' }); const blob = new Blob([fullHtml], { type: 'text/html' });
const url = URL.createObjectURL(blob); const url = URL.createObjectURL(blob);
const a = document.createElement('a'); const a = document.createElement('a');
@ -250,5 +262,17 @@ document.getElementById('clearBtn').addEventListener('click', () => {
fileInput.value = ''; fileInput.value = '';
}); });
</script> </script>
<!-- Yandex.Metrika counter -->
<script type="text/javascript">
(function(m,e,t,r,i,k,a){
m[i]=m[i]||function(){(m[i].a=m[i].a||[]).push(arguments)};
m[i].l=1*new Date();
for (var j = 0; j < document.scripts.length; j++) {if (document.scripts[j].src === r) { return; }}
k=e.createElement(t),a=e.getElementsByTagName(t)[0],k.async=1,k.src=r,a.parentNode.insertBefore(k,a)
})(window, document,'script','https://mc.yandex.ru/metrika/tag.js?id=108185982', 'ym');
ym(108185982, 'init', {ssr:true, webvisor:true, clickmap:true, ecommerce:"dataLayer", referrer: document.referrer, url: location.href, accurateTrackBounce:true, trackLinks:true});
</script>
<noscript><div><img src="https://mc.yandex.ru/watch/108185982" style="position:absolute; left:-9999px;" alt="" /></div></noscript>
<!-- /Yandex.Metrika counter -->
</body> </body>
</html> </html>

View File

@ -407,5 +407,17 @@ $('clearBtn').addEventListener('click', () => {
// Auto-focus // Auto-focus
$('urlInput').focus(); $('urlInput').focus();
</script> </script>
<!-- Yandex.Metrika counter -->
<script type="text/javascript">
(function(m,e,t,r,i,k,a){
m[i]=m[i]||function(){(m[i].a=m[i].a||[]).push(arguments)};
m[i].l=1*new Date();
for (var j = 0; j < document.scripts.length; j++) {if (document.scripts[j].src === r) { return; }}
k=e.createElement(t),a=e.getElementsByTagName(t)[0],k.async=1,k.src=r,a.parentNode.insertBefore(k,a)
})(window, document,'script','https://mc.yandex.ru/metrika/tag.js?id=108185982', 'ym');
ym(108185982, 'init', {ssr:true, webvisor:true, clickmap:true, ecommerce:"dataLayer", referrer: document.referrer, url: location.href, accurateTrackBounce:true, trackLinks:true});
</script>
<noscript><div><img src="https://mc.yandex.ru/watch/108185982" style="position:absolute; left:-9999px;" alt="" /></div></noscript>
<!-- /Yandex.Metrika counter -->
</body> </body>
</html> </html>

View File

@ -330,5 +330,17 @@ function escapeHtml(str) {
// Generate on load // Generate on load
generate(); generate();
</script> </script>
<!-- Yandex.Metrika counter -->
<script type="text/javascript">
(function(m,e,t,r,i,k,a){
m[i]=m[i]||function(){(m[i].a=m[i].a||[]).push(arguments)};
m[i].l=1*new Date();
for (var j = 0; j < document.scripts.length; j++) {if (document.scripts[j].src === r) { return; }}
k=e.createElement(t),a=e.getElementsByTagName(t)[0],k.async=1,k.src=r,a.parentNode.insertBefore(k,a)
})(window, document,'script','https://mc.yandex.ru/metrika/tag.js?id=108185982', 'ym');
ym(108185982, 'init', {ssr:true, webvisor:true, clickmap:true, ecommerce:"dataLayer", referrer: document.referrer, url: location.href, accurateTrackBounce:true, trackLinks:true});
</script>
<noscript><div><img src="https://mc.yandex.ru/watch/108185982" style="position:absolute; left:-9999px;" alt="" /></div></noscript>
<!-- /Yandex.Metrika counter -->
</body> </body>
</html> </html>

View File

@ -2045,5 +2045,17 @@
</script> </script>
<!-- Yandex.Metrika counter -->
<script type="text/javascript">
(function(m,e,t,r,i,k,a){
m[i]=m[i]||function(){(m[i].a=m[i].a||[]).push(arguments)};
m[i].l=1*new Date();
for (var j = 0; j < document.scripts.length; j++) {if (document.scripts[j].src === r) { return; }}
k=e.createElement(t),a=e.getElementsByTagName(t)[0],k.async=1,k.src=r,a.parentNode.insertBefore(k,a)
})(window, document,'script','https://mc.yandex.ru/metrika/tag.js?id=108185982', 'ym');
ym(108185982, 'init', {ssr:true, webvisor:true, clickmap:true, ecommerce:"dataLayer", referrer: document.referrer, url: location.href, accurateTrackBounce:true, trackLinks:true});
</script>
<noscript><div><img src="https://mc.yandex.ru/watch/108185982" style="position:absolute; left:-9999px;" alt="" /></div></noscript>
<!-- /Yandex.Metrika counter -->
</body> </body>
</html> </html>

View File

@ -258,5 +258,17 @@ document.getElementById('copyUrlBtn').addEventListener('click', () => {
// Init // Init
updateUrl(); updateUrl();
</script> </script>
<!-- Yandex.Metrika counter -->
<script type="text/javascript">
(function(m,e,t,r,i,k,a){
m[i]=m[i]||function(){(m[i].a=m[i].a||[]).push(arguments)};
m[i].l=1*new Date();
for (var j = 0; j < document.scripts.length; j++) {if (document.scripts[j].src === r) { return; }}
k=e.createElement(t),a=e.getElementsByTagName(t)[0],k.async=1,k.src=r,a.parentNode.insertBefore(k,a)
})(window, document,'script','https://mc.yandex.ru/metrika/tag.js?id=108185982', 'ym');
ym(108185982, 'init', {ssr:true, webvisor:true, clickmap:true, ecommerce:"dataLayer", referrer: document.referrer, url: location.href, accurateTrackBounce:true, trackLinks:true});
</script>
<noscript><div><img src="https://mc.yandex.ru/watch/108185982" style="position:absolute; left:-9999px;" alt="" /></div></noscript>
<!-- /Yandex.Metrika counter -->
</body> </body>
</html> </html>

View File

@ -600,5 +600,17 @@
})(); })();
</script> </script>
<!-- Yandex.Metrika counter -->
<script type="text/javascript">
(function(m,e,t,r,i,k,a){
m[i]=m[i]||function(){(m[i].a=m[i].a||[]).push(arguments)};
m[i].l=1*new Date();
for (var j = 0; j < document.scripts.length; j++) {if (document.scripts[j].src === r) { return; }}
k=e.createElement(t),a=e.getElementsByTagName(t)[0],k.async=1,k.src=r,a.parentNode.insertBefore(k,a)
})(window, document,'script','https://mc.yandex.ru/metrika/tag.js?id=108185982', 'ym');
ym(108185982, 'init', {ssr:true, webvisor:true, clickmap:true, ecommerce:"dataLayer", referrer: document.referrer, url: location.href, accurateTrackBounce:true, trackLinks:true});
</script>
<noscript><div><img src="https://mc.yandex.ru/watch/108185982" style="position:absolute; left:-9999px;" alt="" /></div></noscript>
<!-- /Yandex.Metrika counter -->
</body> </body>
</html> </html>

View File

@ -1,134 +1,146 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="ru" class="dark"> <html lang="ru" class="dark">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Регистрация — WA Dev Tools</title> <title>Регистрация — WA Dev Tools</title>
<script src="/vendor/tailwind.js"></script> <script src="/vendor/tailwind.js"></script>
<script> <script>
tailwind.config = { tailwind.config = {
darkMode: 'class', darkMode: 'class',
theme: { extend: { theme: { extend: {
fontFamily: { mono: ['JetBrains Mono', 'monospace'], sans: ['Manrope', 'sans-serif'] }, fontFamily: { mono: ['JetBrains Mono', 'monospace'], sans: ['Manrope', 'sans-serif'] },
colors: { surface: { 900: '#060c18', 800: '#091020', 700: '#0d1828', 600: '#162040' }, accent: { DEFAULT: '#0054e6', dim: '#0043b8', bright: '#6b9fff' } } colors: { surface: { 900: '#060c18', 800: '#091020', 700: '#0d1828', 600: '#162040' }, accent: { DEFAULT: '#0054e6', dim: '#0043b8', bright: '#6b9fff' } }
}} }}
}; };
</script> </script>
<link href="/vendor/fonts.css" rel="stylesheet"> <link href="/vendor/fonts.css" rel="stylesheet">
<link href="/shared.css" rel="stylesheet"> <link href="/shared.css" rel="stylesheet">
<style> <style>
/* Content */ /* Content */
.page-body { padding-top:56px; min-height:100vh; display:flex; align-items:center; justify-content:center; } .page-body { padding-top:56px; min-height:100vh; display:flex; align-items:center; justify-content:center; }
.auth-card { max-width:420px; width:100%; padding:20px; } .auth-card { max-width:420px; width:100%; padding:20px; }
.auth-card-inner { background:var(--surface-700); border:1px solid var(--surface-600); border-radius:16px; padding:36px 32px; box-shadow:0 4px 24px rgba(0,0,0,.1); } .auth-card-inner { background:var(--surface-700); border:1px solid var(--surface-600); border-radius:16px; padding:36px 32px; box-shadow:0 4px 24px rgba(0,0,0,.1); }
.auth-title { font-size:24px; font-weight:800; color:var(--text-primary); text-align:center; } .auth-title { font-size:24px; font-weight:800; color:var(--text-primary); text-align:center; }
.auth-subtitle { font-size:14px; color:var(--text-muted); text-align:center; margin-top:6px; } .auth-subtitle { font-size:14px; color:var(--text-muted); text-align:center; margin-top:6px; }
.auth-label { display:block; font-size:13px; color:var(--text-muted); margin-bottom:6px; font-family:'JetBrains Mono',monospace; font-weight:500; } .auth-label { display:block; font-size:13px; color:var(--text-muted); margin-bottom:6px; font-family:'JetBrains Mono',monospace; font-weight:500; }
.auth-input { width:100%; background:var(--select-bg); border:1px solid var(--surface-600); color:var(--select-color); padding:12px 16px; border-radius:10px; font-family:'Manrope',sans-serif; font-size:15px; outline:none; transition:border-color .2s; } .auth-input { width:100%; background:var(--select-bg); border:1px solid var(--surface-600); color:var(--select-color); padding:12px 16px; border-radius:10px; font-family:'Manrope',sans-serif; font-size:15px; outline:none; transition:border-color .2s; }
.auth-input:focus { border-color:var(--accent); } .auth-input:focus { border-color:var(--accent); }
.auth-input::placeholder { color:var(--text-muted); opacity:.6; } .auth-input::placeholder { color:var(--text-muted); opacity:.6; }
.auth-btn { width:100%; padding:14px; background:var(--accent); color:#fff; border:none; border-radius:10px; font-family:'Manrope',sans-serif; font-weight:700; font-size:16px; cursor:pointer; transition:background .2s; } .auth-btn { width:100%; padding:14px; background:var(--accent); color:#fff; border:none; border-radius:10px; font-family:'Manrope',sans-serif; font-weight:700; font-size:16px; cursor:pointer; transition:background .2s; }
.auth-btn:hover { background:var(--accent-dim); } .auth-btn:hover { background:var(--accent-dim); }
.auth-btn:disabled { opacity:0.5; cursor:not-allowed; } .auth-btn:disabled { opacity:0.5; cursor:not-allowed; }
.auth-error { background:rgba(255,82,82,.08); border:1px solid rgba(255,82,82,.25); color:#ff5252; padding:10px 14px; border-radius:8px; font-size:13px; display:none; } .auth-error { background:rgba(255,82,82,.08); border:1px solid rgba(255,82,82,.25); color:#ff5252; padding:10px 14px; border-radius:8px; font-size:13px; display:none; }
.auth-error.visible { display:block; } .auth-error.visible { display:block; }
.auth-footer { text-align:center; margin-top:20px; font-size:14px; color:var(--text-muted); } .auth-footer { text-align:center; margin-top:20px; font-size:14px; color:var(--text-muted); }
.auth-footer a { color:var(--accent); text-decoration:none; font-weight:600; } .auth-footer a { color:var(--accent); text-decoration:none; font-weight:600; }
.auth-footer a:hover { text-decoration:underline; } .auth-footer a:hover { text-decoration:underline; }
.auth-divider { height:1px; background:var(--surface-600); margin:20px 0; } .auth-divider { height:1px; background:var(--surface-600); margin:20px 0; }
.auth-hint { font-size:11px; color:var(--text-muted); margin-top:4px; font-family:'JetBrains Mono',monospace; opacity:.7; } .auth-hint { font-size:11px; color:var(--text-muted); margin-top:4px; font-family:'JetBrains Mono',monospace; opacity:.7; }
</style> </style>
<script> <script>
(function(){ (function(){
const s = localStorage.getItem('theme'); const s = localStorage.getItem('theme');
if (s === '' || (s === null && !window.matchMedia('(prefers-color-scheme: dark)').matches)) { if (s === '' || (s === null && !window.matchMedia('(prefers-color-scheme: dark)').matches)) {
document.documentElement.classList.remove('dark'); document.documentElement.classList.remove('dark');
} }
})(); })();
</script> </script>
</head> </head>
<body> <body>
<header class="top-header"> <header class="top-header">
<a href="/" class="top-logo"> <a href="/" class="top-logo">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="color:var(--accent)"><path stroke-linecap="round" stroke-linejoin="round" d="M17.25 6.75L22.5 12l-5.25 5.25m-10.5 0L1.5 12l5.25-5.25m7.5-3l-4.5 16.5"/></svg> <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="color:var(--accent)"><path stroke-linecap="round" stroke-linejoin="round" d="M17.25 6.75L22.5 12l-5.25 5.25m-10.5 0L1.5 12l5.25-5.25m7.5-3l-4.5 16.5"/></svg>
WA Dev <span>Tools</span> WA Dev <span>Tools</span>
</a> </a>
<nav class="top-nav"> <nav class="top-nav">
<a href="/">Главная</a> <a href="/">Главная</a>
<a href="/#tools">Инструменты</a> <a href="/#tools">Инструменты</a>
</nav> </nav>
<div class="top-right"> <div class="top-right">
<button class="theme-btn" id="themeBtn" title="Переключить тему"> <button class="theme-btn" id="themeBtn" title="Переключить тему">
<svg class="sun-i" width="16" height="16" fill="currentColor" viewBox="0 0 20 20"><path d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z"/></svg> <svg class="sun-i" width="16" height="16" fill="currentColor" viewBox="0 0 20 20"><path d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z"/></svg>
<svg class="moon-i" width="16" height="16" fill="currentColor" viewBox="0 0 20 20"><path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z"/></svg> <svg class="moon-i" width="16" height="16" fill="currentColor" viewBox="0 0 20 20"><path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z"/></svg>
</button> </button>
<a href="/auth/login" class="btn-ghost">Войти</a> <a href="/auth/login" class="btn-ghost">Войти</a>
<a href="/auth/register" class="btn-primary-sm active">Регистрация</a> <a href="/auth/register" class="btn-primary-sm active">Регистрация</a>
</div> </div>
</header> </header>
<div class="page-body"> <div class="page-body">
<div class="auth-card"> <div class="auth-card">
<div class="auth-card-inner"> <div class="auth-card-inner">
<div class="auth-title">Создать аккаунт</div> <div class="auth-title">Создать аккаунт</div>
<div class="auth-subtitle">Бесплатный доступ ко всем инструментам</div> <div class="auth-subtitle">Бесплатный доступ ко всем инструментам</div>
<div id="error" class="auth-error" style="margin-top:20px;"></div> <div id="error" class="auth-error" style="margin-top:20px;"></div>
<form id="regForm" style="display:flex;flex-direction:column;gap:16px;margin-top:24px;" onsubmit="return handleRegister(event)"> <form id="regForm" style="display:flex;flex-direction:column;gap:16px;margin-top:24px;" onsubmit="return handleRegister(event)">
<div> <div>
<label class="auth-label">Имя</label> <label class="auth-label">Имя</label>
<input class="auth-input" type="text" name="name" placeholder="Как вас зовут" autocomplete="name" /> <input class="auth-input" type="text" name="name" placeholder="Как вас зовут" autocomplete="name" />
</div> </div>
<div> <div>
<label class="auth-label">Email</label> <label class="auth-label">Email</label>
<input class="auth-input" type="email" name="email" required autofocus autocomplete="email" placeholder="you@example.com" /> <input class="auth-input" type="email" name="email" required autofocus autocomplete="email" placeholder="you@example.com" />
</div> </div>
<div> <div>
<label class="auth-label">Пароль</label> <label class="auth-label">Пароль</label>
<input class="auth-input" type="password" name="password" required minlength="6" autocomplete="new-password" placeholder="Придумайте пароль" /> <input class="auth-input" type="password" name="password" required minlength="6" autocomplete="new-password" placeholder="Придумайте пароль" />
<div class="auth-hint">минимум 6 символов</div> <div class="auth-hint">минимум 6 символов</div>
</div> </div>
<div> <div>
<label class="auth-label">Подтвердите пароль</label> <label class="auth-label">Подтвердите пароль</label>
<input class="auth-input" type="password" name="password2" required minlength="6" autocomplete="new-password" placeholder="Повторите пароль" /> <input class="auth-input" type="password" name="password2" required minlength="6" autocomplete="new-password" placeholder="Повторите пароль" />
</div> </div>
<button class="auth-btn" type="submit" id="submitBtn">Зарегистрироваться</button> <button class="auth-btn" type="submit" id="submitBtn">Зарегистрироваться</button>
</form> </form>
<div class="auth-divider"></div> <div class="auth-divider"></div>
<div class="auth-footer">Уже есть аккаунт? <a href="/auth/login">Войти</a></div> <div class="auth-footer">Уже есть аккаунт? <a href="/auth/login">Войти</a></div>
</div> </div>
</div> </div>
</div> </div>
<script> <script>
const _tb = document.getElementById('themeBtn'); const _tb = document.getElementById('themeBtn');
function _ut() { function _ut() {
const d = document.documentElement.classList.contains('dark'); const d = document.documentElement.classList.contains('dark');
document.querySelectorAll('.sun-i').forEach(e => e.style.display = d ? 'none' : 'block'); document.querySelectorAll('.sun-i').forEach(e => e.style.display = d ? 'none' : 'block');
document.querySelectorAll('.moon-i').forEach(e => e.style.display = d ? 'block' : 'none'); document.querySelectorAll('.moon-i').forEach(e => e.style.display = d ? 'block' : 'none');
} }
_ut(); _ut();
_tb.addEventListener('click', () => { _tb.addEventListener('click', () => {
const d = document.documentElement.classList.toggle('dark'); const d = document.documentElement.classList.toggle('dark');
localStorage.setItem('theme', d ? 'dark' : ''); localStorage.setItem('theme', d ? 'dark' : '');
_ut(); _ut();
}); });
async function handleRegister(e) { async function handleRegister(e) {
e.preventDefault(); e.preventDefault();
const form = e.target, btn = document.getElementById('submitBtn'), err = document.getElementById('error'); const form = e.target, btn = document.getElementById('submitBtn'), err = document.getElementById('error');
err.classList.remove('visible'); err.classList.remove('visible');
if (form.password.value !== form.password2.value) { err.textContent = 'Пароли не совпадают'; err.classList.add('visible'); return; } if (form.password.value !== form.password2.value) { err.textContent = 'Пароли не совпадают'; err.classList.add('visible'); return; }
btn.disabled = true; btn.textContent = 'Регистрация...'; btn.disabled = true; btn.textContent = 'Регистрация...';
try { try {
const res = await fetch('/auth/register', { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({name:form.name.value,email:form.email.value,password:form.password.value,password2:form.password2.value}) }); const res = await fetch('/auth/register', { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({name:form.name.value,email:form.email.value,password:form.password.value,password2:form.password2.value}) });
const data = await res.json(); const data = await res.json();
if (data.ok) window.location.href = data.redirect || '/dashboard'; if (data.ok) window.location.href = data.redirect || '/dashboard';
else { err.textContent = data.error; err.classList.add('visible'); } else { err.textContent = data.error; err.classList.add('visible'); }
} catch { err.textContent = 'Ошибка сети'; err.classList.add('visible'); } } catch { err.textContent = 'Ошибка сети'; err.classList.add('visible'); }
btn.disabled = false; btn.textContent = 'Зарегистрироваться'; btn.disabled = false; btn.textContent = 'Зарегистрироваться';
} }
</script> </script>
</body> <!-- Yandex.Metrika counter -->
</html> <script type="text/javascript">
(function(m,e,t,r,i,k,a){
m[i]=m[i]||function(){(m[i].a=m[i].a||[]).push(arguments)};
m[i].l=1*new Date();
for (var j = 0; j < document.scripts.length; j++) {if (document.scripts[j].src === r) { return; }}
k=e.createElement(t),a=e.getElementsByTagName(t)[0],k.async=1,k.src=r,a.parentNode.insertBefore(k,a)
})(window, document,'script','https://mc.yandex.ru/metrika/tag.js?id=108185982', 'ym');
ym(108185982, 'init', {ssr:true, webvisor:true, clickmap:true, ecommerce:"dataLayer", referrer: document.referrer, url: location.href, accurateTrackBounce:true, trackLinks:true});
</script>
<noscript><div><img src="https://mc.yandex.ru/watch/108185982" style="position:absolute; left:-9999px;" alt="" /></div></noscript>
<!-- /Yandex.Metrika counter -->
</body>
</html>

View File

@ -222,5 +222,17 @@ function minifyHtml(html) {
return html.replace(/<!--[\s\S]*?-->/g,'').replace(/\s+/g,' ').replace(/>\s+</g,'><').replace(/\s+>/g,'>').replace(/<\s+/g,'<').trim(); return html.replace(/<!--[\s\S]*?-->/g,'').replace(/\s+/g,' ').replace(/>\s+</g,'><').replace(/\s+>/g,'>').replace(/<\s+/g,'<').trim();
} }
</script> </script>
<!-- Yandex.Metrika counter -->
<script type="text/javascript">
(function(m,e,t,r,i,k,a){
m[i]=m[i]||function(){(m[i].a=m[i].a||[]).push(arguments)};
m[i].l=1*new Date();
for (var j = 0; j < document.scripts.length; j++) {if (document.scripts[j].src === r) { return; }}
k=e.createElement(t),a=e.getElementsByTagName(t)[0],k.async=1,k.src=r,a.parentNode.insertBefore(k,a)
})(window, document,'script','https://mc.yandex.ru/metrika/tag.js?id=108185982', 'ym');
ym(108185982, 'init', {ssr:true, webvisor:true, clickmap:true, ecommerce:"dataLayer", referrer: document.referrer, url: location.href, accurateTrackBounce:true, trackLinks:true});
</script>
<noscript><div><img src="https://mc.yandex.ru/watch/108185982" style="position:absolute; left:-9999px;" alt="" /></div></noscript>
<!-- /Yandex.Metrika counter -->
</body> </body>
</html> </html>

View File

@ -1,129 +1,141 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="ru"> <html lang="ru">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Status — WA Dev Tools</title> <title>Status — WA Dev Tools</title>
<script src="/vendor/tailwind.js"></script> <script src="/vendor/tailwind.js"></script>
<script> <script>
tailwind.config = { tailwind.config = {
darkMode: 'class', darkMode: 'class',
theme: { extend: { theme: { extend: {
fontFamily: { mono: ['JetBrains Mono', 'monospace'], sans: ['Manrope', 'sans-serif'] }, fontFamily: { mono: ['JetBrains Mono', 'monospace'], sans: ['Manrope', 'sans-serif'] },
colors: { surface: { 900: '#060c18', 800: '#091020', 700: '#0d1828', 600: '#162040' }, accent: { DEFAULT: '#0054e6', dim: '#0043b8', bright: '#6b9fff' } } colors: { surface: { 900: '#060c18', 800: '#091020', 700: '#0d1828', 600: '#162040' }, accent: { DEFAULT: '#0054e6', dim: '#0043b8', bright: '#6b9fff' } }
}} }}
}; };
</script> </script>
<link href="/vendor/fonts.css" rel="stylesheet"> <link href="/vendor/fonts.css" rel="stylesheet">
<link href="/shared.css" rel="stylesheet"> <link href="/shared.css" rel="stylesheet">
<style> <style>
.status-page { padding-top:80px; max-width:680px; margin:0 auto; padding-left:20px; padding-right:20px; padding-bottom:60px; } .status-page { padding-top:80px; max-width:680px; margin:0 auto; padding-left:20px; padding-right:20px; padding-bottom:60px; }
.overall-banner { border-radius:12px; padding:24px 28px; margin-bottom:32px; display:flex; align-items:center; gap:14px; } .overall-banner { border-radius:12px; padding:24px 28px; margin-bottom:32px; display:flex; align-items:center; gap:14px; }
.overall-banner.operational { background:rgba(34,197,94,.08); border:1px solid rgba(34,197,94,.2); } .overall-banner.operational { background:rgba(34,197,94,.08); border:1px solid rgba(34,197,94,.2); }
.overall-banner.degraded { background:rgba(234,179,8,.08); border:1px solid rgba(234,179,8,.2); } .overall-banner.degraded { background:rgba(234,179,8,.08); border:1px solid rgba(234,179,8,.2); }
.overall-banner.outage { background:rgba(239,68,68,.08); border:1px solid rgba(239,68,68,.2); } .overall-banner.outage { background:rgba(239,68,68,.08); border:1px solid rgba(239,68,68,.2); }
.overall-dot { width:10px; height:10px; border-radius:50%; flex-shrink:0; } .overall-dot { width:10px; height:10px; border-radius:50%; flex-shrink:0; }
.overall-dot.operational { background:#22c55e; } .overall-dot.operational { background:#22c55e; }
.overall-dot.degraded { background:#eab308; } .overall-dot.degraded { background:#eab308; }
.overall-dot.outage { background:#ef4444; } .overall-dot.outage { background:#ef4444; }
.overall-text { font-size:16px; font-weight:700; color:var(--text-primary); } .overall-text { font-size:16px; font-weight:700; color:var(--text-primary); }
.service-list { display:flex; flex-direction:column; } .service-list { display:flex; flex-direction:column; }
.service-row { display:flex; align-items:center; justify-content:space-between; padding:16px 0; border-bottom:1px solid var(--surface-600); } .service-row { display:flex; align-items:center; justify-content:space-between; padding:16px 0; border-bottom:1px solid var(--surface-600); }
.service-row:last-child { border-bottom:none; } .service-row:last-child { border-bottom:none; }
.service-name { font-size:15px; font-weight:500; color:var(--text-primary); } .service-name { font-size:15px; font-weight:500; color:var(--text-primary); }
.service-badge { display:flex; align-items:center; gap:8px; font-size:13px; font-weight:500; } .service-badge { display:flex; align-items:center; gap:8px; font-size:13px; font-weight:500; }
.service-badge .dot { width:8px; height:8px; border-radius:50%; } .service-badge .dot { width:8px; height:8px; border-radius:50%; }
.service-badge .dot.operational { background:#22c55e; } .service-badge .dot.operational { background:#22c55e; }
.service-badge .dot.degraded { background:#eab308; } .service-badge .dot.degraded { background:#eab308; }
.service-badge .dot.outage { background:#ef4444; } .service-badge .dot.outage { background:#ef4444; }
.service-badge .label { color:var(--text-muted); } .service-badge .label { color:var(--text-muted); }
.uptime-footer { margin-top:32px; text-align:center; color:var(--text-muted); font-size:12px; font-family:'JetBrains Mono',monospace; } .uptime-footer { margin-top:32px; text-align:center; color:var(--text-muted); font-size:12px; font-family:'JetBrains Mono',monospace; }
.uptime-footer .uptime-val { font-size:14px; color:var(--text-secondary); font-weight:600; margin-bottom:4px; } .uptime-footer .uptime-val { font-size:14px; color:var(--text-secondary); font-weight:600; margin-bottom:4px; }
</style> </style>
<script> <script>
(function(){ if (localStorage.getItem('theme') === 'dark') document.documentElement.classList.add('dark'); })(); (function(){ if (localStorage.getItem('theme') === 'dark') document.documentElement.classList.add('dark'); })();
</script> </script>
</head> </head>
<body> <body>
<header class="top-header"> <header class="top-header">
<a href="/" class="top-logo"> <a href="/" class="top-logo">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="color:var(--accent)"><path stroke-linecap="round" stroke-linejoin="round" d="M17.25 6.75L22.5 12l-5.25 5.25m-10.5 0L1.5 12l5.25-5.25m7.5-3l-4.5 16.5"/></svg> <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="color:var(--accent)"><path stroke-linecap="round" stroke-linejoin="round" d="M17.25 6.75L22.5 12l-5.25 5.25m-10.5 0L1.5 12l5.25-5.25m7.5-3l-4.5 16.5"/></svg>
WA Dev <span>Tools</span> WA Dev <span>Tools</span>
</a> </a>
<nav class="top-nav"> <nav class="top-nav">
<a href="/">Главная</a> <a href="/">Главная</a>
<a href="/#tools">Инструменты</a> <a href="/#tools">Инструменты</a>
</nav> </nav>
<div class="top-right"> <div class="top-right">
<button class="theme-btn" id="themeBtn" title="Переключить тему"> <button class="theme-btn" id="themeBtn" title="Переключить тему">
<svg class="sun-i" width="16" height="16" fill="currentColor" viewBox="0 0 20 20"><path d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z"/></svg> <svg class="sun-i" width="16" height="16" fill="currentColor" viewBox="0 0 20 20"><path d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z"/></svg>
<svg class="moon-i" width="16" height="16" fill="currentColor" viewBox="0 0 20 20"><path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z"/></svg> <svg class="moon-i" width="16" height="16" fill="currentColor" viewBox="0 0 20 20"><path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z"/></svg>
</button> </button>
<a href="/auth/login" class="btn-ghost">Войти</a> <a href="/auth/login" class="btn-ghost">Войти</a>
<a href="/auth/register" class="btn-primary-sm">Регистрация</a> <a href="/auth/register" class="btn-primary-sm">Регистрация</a>
</div> </div>
</header> </header>
<div class="status-page"> <div class="status-page">
<div id="banner" class="overall-banner operational"> <div id="banner" class="overall-banner operational">
<div class="overall-dot operational" id="bannerDot"></div> <div class="overall-dot operational" id="bannerDot"></div>
<div class="overall-text" id="bannerText">All systems operational</div> <div class="overall-text" id="bannerText">All systems operational</div>
</div> </div>
<div class="service-list" id="serviceList"> <div class="service-list" id="serviceList">
<div style="text-align:center;padding:40px 0;color:var(--text-muted);font-size:13px;">Loading...</div> <div style="text-align:center;padding:40px 0;color:var(--text-muted);font-size:13px;">Loading...</div>
</div> </div>
<div class="uptime-footer"> <div class="uptime-footer">
<div class="uptime-val" id="uptimeVal"></div> <div class="uptime-val" id="uptimeVal"></div>
<div id="lastCheck"></div> <div id="lastCheck"></div>
</div> </div>
</div> </div>
<script> <script>
const OVERALL = { operational: 'All systems operational', degraded: 'Partial system outage', outage: 'Major system outage' }; const OVERALL = { operational: 'All systems operational', degraded: 'Partial system outage', outage: 'Major system outage' };
const STATUS = { operational: 'Operational', degraded: 'Degraded', outage: 'Outage' }; const STATUS = { operational: 'Operational', degraded: 'Degraded', outage: 'Outage' };
function fmtUptime(s) { function fmtUptime(s) {
const d = Math.floor(s / 86400), h = Math.floor((s % 86400) / 3600), m = Math.floor((s % 3600) / 60); const d = Math.floor(s / 86400), h = Math.floor((s % 86400) / 3600), m = Math.floor((s % 3600) / 60);
const p = []; const p = [];
if (d) p.push(d + 'd'); if (d) p.push(d + 'd');
if (h) p.push(h + 'h'); if (h) p.push(h + 'h');
p.push(m + 'm'); p.push(m + 'm');
return 'Uptime: ' + p.join(' '); return 'Uptime: ' + p.join(' ');
} }
function render(data) { function render(data) {
const b = document.getElementById('banner'); const b = document.getElementById('banner');
b.className = 'overall-banner ' + data.overall; b.className = 'overall-banner ' + data.overall;
document.getElementById('bannerDot').className = 'overall-dot ' + data.overall; document.getElementById('bannerDot').className = 'overall-dot ' + data.overall;
document.getElementById('bannerText').textContent = OVERALL[data.overall] || data.overall; document.getElementById('bannerText').textContent = OVERALL[data.overall] || data.overall;
document.getElementById('serviceList').innerHTML = data.services.map(s => document.getElementById('serviceList').innerHTML = data.services.map(s =>
'<div class="service-row"><div class="service-name">' + s.name + '</div>' + '<div class="service-row"><div class="service-name">' + s.name + '</div>' +
'<div class="service-badge"><span class="dot ' + s.status + '"></span>' + '<div class="service-badge"><span class="dot ' + s.status + '"></span>' +
'<span class="label">' + (STATUS[s.status] || s.status) + '</span></div></div>' '<span class="label">' + (STATUS[s.status] || s.status) + '</span></div></div>'
).join(''); ).join('');
document.getElementById('uptimeVal').textContent = fmtUptime(data.uptime); document.getElementById('uptimeVal').textContent = fmtUptime(data.uptime);
document.getElementById('lastCheck').textContent = 'Last checked: ' + new Date(data.timestamp).toLocaleTimeString(); document.getElementById('lastCheck').textContent = 'Last checked: ' + new Date(data.timestamp).toLocaleTimeString();
} }
async function poll() { try { const r = await fetch('/status/api'); if (r.ok) render(await r.json()); } catch {} } async function poll() { try { const r = await fetch('/status/api'); if (r.ok) render(await r.json()); } catch {} }
poll(); poll();
setInterval(poll, 30000); setInterval(poll, 30000);
// Theme // Theme
const tb = document.getElementById('themeBtn'); const tb = document.getElementById('themeBtn');
function ut() { function ut() {
const d = document.documentElement.classList.contains('dark'); const d = document.documentElement.classList.contains('dark');
document.querySelectorAll('.sun-i').forEach(e => e.style.display = d ? 'none' : 'block'); document.querySelectorAll('.sun-i').forEach(e => e.style.display = d ? 'none' : 'block');
document.querySelectorAll('.moon-i').forEach(e => e.style.display = d ? 'block' : 'none'); document.querySelectorAll('.moon-i').forEach(e => e.style.display = d ? 'block' : 'none');
} }
ut(); ut();
tb.addEventListener('click', () => { const d = document.documentElement.classList.toggle('dark'); localStorage.setItem('theme', d ? 'dark' : ''); ut(); }); tb.addEventListener('click', () => { const d = document.documentElement.classList.toggle('dark'); localStorage.setItem('theme', d ? 'dark' : ''); ut(); });
</script> </script>
</body> <!-- Yandex.Metrika counter -->
</html> <script type="text/javascript">
(function(m,e,t,r,i,k,a){
m[i]=m[i]||function(){(m[i].a=m[i].a||[]).push(arguments)};
m[i].l=1*new Date();
for (var j = 0; j < document.scripts.length; j++) {if (document.scripts[j].src === r) { return; }}
k=e.createElement(t),a=e.getElementsByTagName(t)[0],k.async=1,k.src=r,a.parentNode.insertBefore(k,a)
})(window, document,'script','https://mc.yandex.ru/metrika/tag.js?id=108185982', 'ym');
ym(108185982, 'init', {ssr:true, webvisor:true, clickmap:true, ecommerce:"dataLayer", referrer: document.referrer, url: location.href, accurateTrackBounce:true, trackLinks:true});
</script>
<noscript><div><img src="https://mc.yandex.ru/watch/108185982" style="position:absolute; left:-9999px;" alt="" /></div></noscript>
<!-- /Yandex.Metrika counter -->
</body>
</html>

File diff suppressed because it is too large Load Diff

View File

@ -1091,5 +1091,17 @@
} }
})(); })();
</script> </script>
<!-- Yandex.Metrika counter -->
<script type="text/javascript">
(function(m,e,t,r,i,k,a){
m[i]=m[i]||function(){(m[i].a=m[i].a||[]).push(arguments)};
m[i].l=1*new Date();
for (var j = 0; j < document.scripts.length; j++) {if (document.scripts[j].src === r) { return; }}
k=e.createElement(t),a=e.getElementsByTagName(t)[0],k.async=1,k.src=r,a.parentNode.insertBefore(k,a)
})(window, document,'script','https://mc.yandex.ru/metrika/tag.js?id=108185982', 'ym');
ym(108185982, 'init', {ssr:true, webvisor:true, clickmap:true, ecommerce:"dataLayer", referrer: document.referrer, url: location.href, accurateTrackBounce:true, trackLinks:true});
</script>
<noscript><div><img src="https://mc.yandex.ru/watch/108185982" style="position:absolute; left:-9999px;" alt="" /></div></noscript>
<!-- /Yandex.Metrika counter -->
</body> </body>
</html> </html>