- Главная: тихая кнопка «Помахать» (web/js/wave.js), общий счётчик для всех посетителей - server/server.mjs: мини-API (/api/waves, /api/wave), счётчик хранится вне репозитория (/var/lib/vizitka-site/waves.json), лимит раз в секунду с IP, проверка Origin - deploy: systemd-сервис vizitka-api, проксирование /api в nginx, setup-server.sh ставит nodejs - CI: после git pull перезапускает API - Домен https://me.ibrash.duckdns.org вместо заглушки, исправлен пробел в шапке - README и заметки обновлены Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -10,13 +10,13 @@
|
||||
<meta property="og:locale" content="ru_RU">
|
||||
<meta property="og:title" content="Выставочный павильон: Аман Ибраш">
|
||||
<meta property="og:description" content="Выставочный павильон в Алматы: двухэтажная галерея в парке со стеклянным первым этажом и террасой для скульптур.">
|
||||
<meta property="og:image" content="https://example.com/og/arch-exhibition-pavilion.jpg">
|
||||
<meta property="og:image" content="https://me.ibrash.duckdns.org/og/arch-exhibition-pavilion.jpg">
|
||||
<meta property="og:image:width" content="1200">
|
||||
<meta property="og:image:height" content="630">
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
<meta name="twitter:title" content="Выставочный павильон: Аман Ибраш">
|
||||
<meta name="twitter:description" content="Выставочный павильон в Алматы: двухэтажная галерея в парке со стеклянным первым этажом и террасой для скульптур.">
|
||||
<meta name="twitter:image" content="https://example.com/og/arch-exhibition-pavilion.jpg">
|
||||
<meta name="twitter:image" content="https://me.ibrash.duckdns.org/og/arch-exhibition-pavilion.jpg">
|
||||
<link rel="icon" href="/favicon.svg" type="image/svg+xml">
|
||||
<link rel="preload" href="/fonts/onest-cyrillic-wght-normal.woff2" as="font" type="font/woff2" crossorigin>
|
||||
<link rel="stylesheet" href="/css/style.css">
|
||||
|
||||
+2
-2
@@ -10,13 +10,13 @@
|
||||
<meta property="og:locale" content="ru_RU">
|
||||
<meta property="og:title" content="Архитектура: Аман Ибраш">
|
||||
<meta property="og:description" content="Учусь архитектуре в KazGASA, Алматы. Мои лучшие проекты: Тихий двор, выставочный павильон и жилой комплекс.">
|
||||
<meta property="og:image" content="https://example.com/og/arch.jpg">
|
||||
<meta property="og:image" content="https://me.ibrash.duckdns.org/og/arch.jpg">
|
||||
<meta property="og:image:width" content="1200">
|
||||
<meta property="og:image:height" content="630">
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
<meta name="twitter:title" content="Архитектура: Аман Ибраш">
|
||||
<meta name="twitter:description" content="Учусь архитектуре в KazGASA, Алматы. Мои лучшие проекты: Тихий двор, выставочный павильон и жилой комплекс.">
|
||||
<meta name="twitter:image" content="https://example.com/og/arch.jpg">
|
||||
<meta name="twitter:image" content="https://me.ibrash.duckdns.org/og/arch.jpg">
|
||||
<link rel="icon" href="/favicon.svg" type="image/svg+xml">
|
||||
<link rel="preload" href="/fonts/onest-cyrillic-wght-normal.woff2" as="font" type="font/woff2" crossorigin>
|
||||
<link rel="stylesheet" href="/css/style.css">
|
||||
|
||||
@@ -10,13 +10,13 @@
|
||||
<meta property="og:locale" content="ru_RU">
|
||||
<meta property="og:title" content="Жилой комплекс «Лофт»: Аман Ибраш">
|
||||
<meta property="og:description" content="Жилой комплекс «Лофт» в Алматы: три корпуса из красного кирпича вокруг общего двора без машин.">
|
||||
<meta property="og:image" content="https://example.com/og/arch-loft-complex.jpg">
|
||||
<meta property="og:image" content="https://me.ibrash.duckdns.org/og/arch-loft-complex.jpg">
|
||||
<meta property="og:image:width" content="1200">
|
||||
<meta property="og:image:height" content="630">
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
<meta name="twitter:title" content="Жилой комплекс «Лофт»: Аман Ибраш">
|
||||
<meta name="twitter:description" content="Жилой комплекс «Лофт» в Алматы: три корпуса из красного кирпича вокруг общего двора без машин.">
|
||||
<meta name="twitter:image" content="https://example.com/og/arch-loft-complex.jpg">
|
||||
<meta name="twitter:image" content="https://me.ibrash.duckdns.org/og/arch-loft-complex.jpg">
|
||||
<link rel="icon" href="/favicon.svg" type="image/svg+xml">
|
||||
<link rel="preload" href="/fonts/onest-cyrillic-wght-normal.woff2" as="font" type="font/woff2" crossorigin>
|
||||
<link rel="stylesheet" href="/css/style.css">
|
||||
|
||||
@@ -10,13 +10,13 @@
|
||||
<meta property="og:locale" content="ru_RU">
|
||||
<meta property="og:title" content="Тихий двор: Аман Ибраш">
|
||||
<meta property="og:description" content="Тихий двор: одноэтажный бетонный дом в Конаеве вокруг внутреннего двора. Проект в диалоге с архитектурой Тадао Андо.">
|
||||
<meta property="og:image" content="https://example.com/og/arch-quiet-courtyard.jpg">
|
||||
<meta property="og:image" content="https://me.ibrash.duckdns.org/og/arch-quiet-courtyard.jpg">
|
||||
<meta property="og:image:width" content="1200">
|
||||
<meta property="og:image:height" content="630">
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
<meta name="twitter:title" content="Тихий двор: Аман Ибраш">
|
||||
<meta name="twitter:description" content="Тихий двор: одноэтажный бетонный дом в Конаеве вокруг внутреннего двора. Проект в диалоге с архитектурой Тадао Андо.">
|
||||
<meta name="twitter:image" content="https://example.com/og/arch-quiet-courtyard.jpg">
|
||||
<meta name="twitter:image" content="https://me.ibrash.duckdns.org/og/arch-quiet-courtyard.jpg">
|
||||
<link rel="icon" href="/favicon.svg" type="image/svg+xml">
|
||||
<link rel="preload" href="/fonts/onest-cyrillic-wght-normal.woff2" as="font" type="font/woff2" crossorigin>
|
||||
<link rel="stylesheet" href="/css/style.css">
|
||||
|
||||
@@ -305,3 +305,19 @@ a.tile:hover { transform: translateY(-4px); box-shadow: 0 16px 36px rgba(18, 26,
|
||||
/* последняя плитка: «список открытый» */
|
||||
.tile--more { background: transparent; box-shadow: none; border: 2px dashed var(--line); align-content: center; }
|
||||
.tile--more h3 { color: var(--accent); }
|
||||
|
||||
/* ===== «Помахать»: тихая кнопка со счётчиком (не выделяется, повторяет приветствие 👋) ===== */
|
||||
.wave-row { display: flex; flex-wrap: wrap; align-items: center; gap: .5rem .9rem; margin-top: .3rem; padding-top: .9rem; border-top: 1px dashed var(--line); min-height: 3.9rem; }
|
||||
.wave-row.off { visibility: hidden; }
|
||||
.wave-btn { position: relative; display: inline-flex; align-items: center; gap: .4rem; min-height: 2.75rem; padding: 0 1rem; border: 1px solid var(--line); border-radius: 99px; background: transparent; color: var(--muted); font: 600 .92rem var(--font); cursor: pointer; -webkit-tap-highlight-color: transparent; transition: color .2s, border-color .2s, background .2s, transform .1s; }
|
||||
.wave-btn:hover { color: var(--ink); border-color: var(--accent); background: var(--card); }
|
||||
.wave-btn:active { transform: scale(.96); }
|
||||
.wave-btn[disabled] { cursor: default; }
|
||||
.wave-em { display: inline-block; transform-origin: 70% 70%; }
|
||||
.wave-btn.waving .wave-em { animation: wave 0.9s; }
|
||||
.wave-count { font-size: .88rem; color: var(--muted); font-variant-numeric: tabular-nums; }
|
||||
.wave-count b { color: var(--ink); font-weight: 700; }
|
||||
.wave-plus { position: absolute; right: .6rem; top: -.2rem; font-size: .85rem; font-weight: 700; color: var(--accent); pointer-events: none; animation: wave-plus .9s ease-out forwards; }
|
||||
@keyframes wave-plus { from { opacity: 0; transform: translateY(.3rem); } 25% { opacity: 1; } to { opacity: 0; transform: translateY(-1.5rem); } }
|
||||
@media (prefers-reduced-motion: no-preference) { .wave-row { animation-delay: .85s; } }
|
||||
.topnav .brand-rest { white-space: pre; } /* в flex-контейнере ведущий пробел иначе схлопывается */
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
<meta property="og:locale" content="ru_RU">
|
||||
<meta property="og:title" content="Фикс: Аман Ибраш">
|
||||
<meta property="og:description" content="Тут будет про фикс: байк, маршруты и километры.">
|
||||
<meta property="og:image" content="https://example.com/og/home.png">
|
||||
<meta property="og:image" content="https://me.ibrash.duckdns.org/og/home.png">
|
||||
<meta property="og:image:width" content="1200">
|
||||
<meta property="og:image:height" content="630">
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
|
||||
+8
-3
@@ -10,13 +10,13 @@
|
||||
<meta property="og:locale" content="ru_RU">
|
||||
<meta property="og:title" content="Аман Ибраш: давай общаться">
|
||||
<meta property="og:description" content="Тренировки, работа и фикс. Пиши, пообщаемся.">
|
||||
<meta property="og:image" content="https://example.com/og/home.png">
|
||||
<meta property="og:image" content="https://me.ibrash.duckdns.org/og/home.png">
|
||||
<meta property="og:image:width" content="1200">
|
||||
<meta property="og:image:height" content="630">
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
<meta name="twitter:title" content="Аман Ибраш: давай общаться">
|
||||
<meta name="twitter:description" content="Тренировки, работа и фикс. Пиши, пообщаемся.">
|
||||
<meta name="twitter:image" content="https://example.com/og/home.png">
|
||||
<meta name="twitter:image" content="https://me.ibrash.duckdns.org/og/home.png">
|
||||
<link rel="icon" href="/favicon.svg" type="image/svg+xml">
|
||||
<link rel="preload" href="/fonts/onest-cyrillic-wght-normal.woff2" as="font" type="font/woff2" crossorigin>
|
||||
<link rel="stylesheet" href="/css/style.css">
|
||||
@@ -43,12 +43,17 @@
|
||||
<h2 id="h-talk">Давай общаться</h2>
|
||||
<p class="cta" id="cta">Хочешь пообщаться? Пиши, отвечу в течение пары дней.</p>
|
||||
<ul class="links" id="links">
|
||||
<li><a class="link link--mail" id="mail" href="mailto:hello@example.com"><span class="ic" aria-hidden="true">@</span><span class="lk">Почта</span><span class="lh">hello@example.com</span></a></li>
|
||||
<li><a class="link link--mail" id="mail" href="mailto:amnibrash@gmail.com"><span class="ic" aria-hidden="true">@</span><span class="lk">Почта</span><span class="lh">amnibrash@gmail.com</span></a></li>
|
||||
</ul>
|
||||
<div class="wave-row" id="wave-row">
|
||||
<button class="wave-btn" id="wave-btn" type="button"><span class="wave-em" aria-hidden="true">👋</span> Помахать</button>
|
||||
<p class="wave-count" id="wave-count" aria-live="polite">Уже помахали: <b id="wave-n">…</b></p>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<script src="/js/nav.js" defer></script>
|
||||
<script src="/js/card.js" defer></script>
|
||||
<script src="/js/wave.js" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
// Тихая кнопка «Помахать»: общий счётчик для всех посетителей хранится на сервере (/api/waves, /api/wave).
|
||||
// Если API недоступен (например, сайт открыт без бэкенда), блок просто прячется, остальная страница не страдает.
|
||||
(function () {
|
||||
var row = document.getElementById('wave-row');
|
||||
var btn = document.getElementById('wave-btn');
|
||||
var num = document.getElementById('wave-n');
|
||||
var line = document.getElementById('wave-count');
|
||||
if (!row || !btn || !num) return;
|
||||
|
||||
var count = null, busy = false, msgTimer = null;
|
||||
var reduce = matchMedia('(prefers-reduced-motion: reduce)').matches;
|
||||
var fmt = function (n) { return n.toLocaleString('ru-RU'); };
|
||||
|
||||
// число плавно «докручивается» до нового значения
|
||||
function setCount(n) {
|
||||
var from = count === null ? n : count; count = n;
|
||||
if (reduce || from === n) { num.textContent = fmt(n); return; }
|
||||
var t0 = performance.now();
|
||||
(function step(now) {
|
||||
var k = Math.min(1, (now - t0) / 350);
|
||||
num.textContent = fmt(Math.round(from + (n - from) * k));
|
||||
if (k < 1) requestAnimationFrame(step);
|
||||
})(t0);
|
||||
}
|
||||
function say(text) { // короткое сообщение вместо счётчика
|
||||
clearTimeout(msgTimer);
|
||||
line.textContent = text;
|
||||
msgTimer = setTimeout(function () { line.textContent = 'Уже помахали: '; line.appendChild(num); }, 2200);
|
||||
}
|
||||
function plusOne() { // маленькое «+1» уплывает вверх
|
||||
if (reduce) return;
|
||||
var s = document.createElement('span');
|
||||
s.className = 'wave-plus'; s.textContent = '+1'; s.setAttribute('aria-hidden', 'true');
|
||||
btn.appendChild(s);
|
||||
s.addEventListener('animationend', function () { s.remove(); });
|
||||
}
|
||||
|
||||
fetch('/api/waves').then(function (r) { if (!r.ok) throw 0; return r.json(); })
|
||||
.then(function (d) { setCount(d.count); })
|
||||
.catch(function () { row.classList.add('off'); });
|
||||
|
||||
btn.addEventListener('click', function () {
|
||||
if (busy || count === null) return;
|
||||
busy = true; btn.disabled = true;
|
||||
var prev = count;
|
||||
setCount(count + 1);
|
||||
btn.classList.remove('waving'); void btn.offsetWidth; btn.classList.add('waving');
|
||||
plusOne();
|
||||
fetch('/api/wave', { method: 'POST' }).then(function (r) {
|
||||
if (r.status === 429) { say('Не так быстро, раз в секунду'); throw 0; }
|
||||
if (!r.ok) throw new Error(r.status);
|
||||
return r.json();
|
||||
}).then(function (d) { setCount(d.count); })
|
||||
.catch(function (e) { count = prev; num.textContent = fmt(prev); if (e !== 0) say('Не засчиталось, попробуй ещё раз'); })
|
||||
.then(function () { setTimeout(function () { busy = false; btn.disabled = false; }, 1000); });
|
||||
});
|
||||
})();
|
||||
@@ -10,13 +10,13 @@
|
||||
<meta property="og:locale" content="ru_RU">
|
||||
<meta property="og:title" content="Aman's World: Аман Ибраш">
|
||||
<meta property="og:description" content="Aman's World: 3D-игра с паркуром и гонками, которая запускается прямо в браузере. Неоновый хаб, гонки Sector 3 Run и Ascend Only.">
|
||||
<meta property="og:image" content="https://example.com/og/projects-amans-world.jpg">
|
||||
<meta property="og:image" content="https://me.ibrash.duckdns.org/og/projects-amans-world.jpg">
|
||||
<meta property="og:image:width" content="1200">
|
||||
<meta property="og:image:height" content="630">
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
<meta name="twitter:title" content="Aman's World: Аман Ибраш">
|
||||
<meta name="twitter:description" content="Aman's World: 3D-игра с паркуром и гонками, которая запускается прямо в браузере. Неоновый хаб, гонки Sector 3 Run и Ascend Only.">
|
||||
<meta name="twitter:image" content="https://example.com/og/projects-amans-world.jpg">
|
||||
<meta name="twitter:image" content="https://me.ibrash.duckdns.org/og/projects-amans-world.jpg">
|
||||
<link rel="icon" href="/favicon.svg" type="image/svg+xml">
|
||||
<link rel="preload" href="/fonts/onest-cyrillic-wght-normal.woff2" as="font" type="font/woff2" crossorigin>
|
||||
<link rel="stylesheet" href="/css/style.css">
|
||||
|
||||
@@ -10,13 +10,13 @@
|
||||
<meta property="og:locale" content="ru_RU">
|
||||
<meta property="og:title" content="Хоумлаб: Аман Ибраш">
|
||||
<meta property="og:description" content="Мой домашний сервер и Fedora Linux: Nextcloud, моя игра и эксперименты, на которых я учусь DevOps.">
|
||||
<meta property="og:image" content="https://example.com/og/projects-homelab.jpg">
|
||||
<meta property="og:image" content="https://me.ibrash.duckdns.org/og/projects-homelab.jpg">
|
||||
<meta property="og:image:width" content="1200">
|
||||
<meta property="og:image:height" content="630">
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
<meta name="twitter:title" content="Хоумлаб: Аман Ибраш">
|
||||
<meta name="twitter:description" content="Мой домашний сервер и Fedora Linux: Nextcloud, моя игра и эксперименты, на которых я учусь DevOps.">
|
||||
<meta name="twitter:image" content="https://example.com/og/projects-homelab.jpg">
|
||||
<meta name="twitter:image" content="https://me.ibrash.duckdns.org/og/projects-homelab.jpg">
|
||||
<link rel="icon" href="/favicon.svg" type="image/svg+xml">
|
||||
<link rel="preload" href="/fonts/onest-cyrillic-wght-normal.woff2" as="font" type="font/woff2" crossorigin>
|
||||
<link rel="stylesheet" href="/css/style.css">
|
||||
|
||||
@@ -10,13 +10,13 @@
|
||||
<meta property="og:locale" content="ru_RU">
|
||||
<meta property="og:title" content="Проекты: Аман Ибраш">
|
||||
<meta property="og:description" content="Мои проекты: Aman's World, 3D-игра с паркуром и гонками, которая запускается прямо в браузере.">
|
||||
<meta property="og:image" content="https://example.com/og/projects.jpg">
|
||||
<meta property="og:image" content="https://me.ibrash.duckdns.org/og/projects.jpg">
|
||||
<meta property="og:image:width" content="1200">
|
||||
<meta property="og:image:height" content="630">
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
<meta name="twitter:title" content="Проекты: Аман Ибраш">
|
||||
<meta name="twitter:description" content="Мои проекты: Aman's World, 3D-игра с паркуром и гонками, которая запускается прямо в браузере.">
|
||||
<meta name="twitter:image" content="https://example.com/og/projects.jpg">
|
||||
<meta name="twitter:image" content="https://me.ibrash.duckdns.org/og/projects.jpg">
|
||||
<link rel="icon" href="/favicon.svg" type="image/svg+xml">
|
||||
<link rel="preload" href="/fonts/onest-cyrillic-wght-normal.woff2" as="font" type="font/woff2" crossorigin>
|
||||
<link rel="stylesheet" href="/css/style.css">
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
User-agent: *
|
||||
Allow: /
|
||||
|
||||
Sitemap: https://example.com/sitemap.xml
|
||||
Sitemap: https://me.ibrash.duckdns.org/sitemap.xml
|
||||
|
||||
+8
-8
@@ -1,11 +1,11 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||
<url><loc>https://example.com/</loc></url>
|
||||
<url><loc>https://example.com/arch/</loc></url>
|
||||
<url><loc>https://example.com/projects/</loc></url>
|
||||
<url><loc>https://example.com/arch/quiet-courtyard/</loc></url>
|
||||
<url><loc>https://example.com/arch/exhibition-pavilion/</loc></url>
|
||||
<url><loc>https://example.com/arch/loft-complex/</loc></url>
|
||||
<url><loc>https://example.com/projects/amans-world/</loc></url>
|
||||
<url><loc>https://example.com/projects/homelab/</loc></url>
|
||||
<url><loc>https://me.ibrash.duckdns.org/</loc></url>
|
||||
<url><loc>https://me.ibrash.duckdns.org/arch/</loc></url>
|
||||
<url><loc>https://me.ibrash.duckdns.org/projects/</loc></url>
|
||||
<url><loc>https://me.ibrash.duckdns.org/arch/quiet-courtyard/</loc></url>
|
||||
<url><loc>https://me.ibrash.duckdns.org/arch/exhibition-pavilion/</loc></url>
|
||||
<url><loc>https://me.ibrash.duckdns.org/arch/loft-complex/</loc></url>
|
||||
<url><loc>https://me.ibrash.duckdns.org/projects/amans-world/</loc></url>
|
||||
<url><loc>https://me.ibrash.duckdns.org/projects/homelab/</loc></url>
|
||||
</urlset>
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
<meta property="og:locale" content="ru_RU">
|
||||
<meta property="og:title" content="Тренировки: Аман Ибраш">
|
||||
<meta property="og:description" content="Тут будет про тренировки: что делаю, как часто и во сколько встаю.">
|
||||
<meta property="og:image" content="https://example.com/og/home.png">
|
||||
<meta property="og:image" content="https://me.ibrash.duckdns.org/og/home.png">
|
||||
<meta property="og:image:width" content="1200">
|
||||
<meta property="og:image:height" content="630">
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
|
||||
+1
-1
@@ -11,7 +11,7 @@
|
||||
<meta property="og:locale" content="ru_RU">
|
||||
<meta property="og:title" content="Работа: Аман Ибраш">
|
||||
<meta property="og:description" content="Тут будет про работу: чем занимаюсь и куда расту.">
|
||||
<meta property="og:image" content="https://example.com/og/home.png">
|
||||
<meta property="og:image" content="https://me.ibrash.duckdns.org/og/home.png">
|
||||
<meta property="og:image:width" content="1200">
|
||||
<meta property="og:image:height" content="630">
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
|
||||
Reference in New Issue
Block a user