Сайт-визитка: главная, архитектура, проекты

- Главная: фото, факты, соцсети, форма связи, анимации
- Архитектура: карточки проектов и отдельные страницы (Тихий двор, павильон, «Лофт»)
- Проекты: Aman's World (скриншоты, описание) и хоумлаб
- Меню из config.json, шаблон новых страниц, черновики training/work/fixed
- Чистый HTML/CSS/JS без сборки, шрифты и картинки локально

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-04 16:35:52 +05:00
co-authored by Claude Sonnet 5.5
commit 152f1d60b5
94 changed files with 1517 additions and 0 deletions
+10
View File
@@ -0,0 +1,10 @@
// Подключается в <head>: помечает «JS работает», сразу запрашивает config.json и даёт общий сигнал готовности
(function () {
var root = document.documentElement;
root.className += ' js';
window.siteReady = function () { root.classList.add('ready'); };
setTimeout(window.siteReady, 2000); // страховка: страница не останется скрытой
window.siteConfig = fetch('/config.json')
.then(function (r) { if (!r.ok) throw 0; return r.json(); })
.catch(function () { return null; });
})();
+52
View File
@@ -0,0 +1,52 @@
// Заполняет страницу из config.json
(function () {
var $ = function (id) { return document.getElementById(id); };
var make = function (tag, cls, text) { var n = document.createElement(tag); if (cls) n.className = cls; if (text) n.textContent = text; return n; };
var safe = function (u) { return /^https?:\/\//i.test(u) ? u : '#'; };
window.siteConfig.then(function (c) {
if (!c) return;
if (c.name) {
$('name').textContent = c.name;
document.title = c.name + ': давай общаться';
$('initials').textContent = c.name.split(/\s+/).map(function (w) { return w.charAt(0); }).join('').slice(0, 2).toUpperCase();
}
if (c.status) $('status').textContent = c.status;
if (c.tagline) {
// текст раскладывается на слова: каждое появляется по очереди (стили в css, .w)
var tg = $('tagline'); tg.textContent = '';
c.tagline.split(/\s+/).forEach(function (word, i, all) {
var w = make('span', 'w', word); w.style.setProperty('--i', i); tg.appendChild(w);
if (i < all.length - 1) tg.appendChild(document.createTextNode(' '));
});
}
(c.facts || []).forEach(function (f) {
var li = make('li'); li.appendChild(make('span', 'em', f.emoji || '')); li.appendChild(document.createTextNode(' ' + f.text));
$('facts').appendChild(li);
});
if (c.cta) $('cta').textContent = c.cta;
if (c.photo && /^\/[\w\-./]+$/.test(c.photo)) {
var img = document.createElement('img');
img.src = c.photo; img.alt = ''; img.width = 480; img.height = 480; img.fetchPriority = 'high';
$('avatar').textContent = ''; $('avatar').classList.add('has-photo'); $('avatar').appendChild(img);
}
(c.tags || []).forEach(function (t) {
var li = make('li');
li.appendChild(make('span', 'em', t.emoji || ''));
li.appendChild(document.createTextNode(' ' + t.text));
$('tags').appendChild(li);
});
var list = $('links'), mailLi = $('mail').parentNode;
(c.socials || []).forEach(function (s) {
var a = make('a', 'link');
a.href = safe(s.url); a.target = '_blank'; a.rel = 'me noopener';
a.appendChild(make('span', 'ic', (s.label || '?').charAt(0)));
a.appendChild(make('span', 'lk', s.label));
a.appendChild(make('span', 'lh', s.handle || ''));
var li = make('li'); li.appendChild(a);
list.insertBefore(li, mailLi);
});
if (c.email) { $('mail').href = 'mailto:' + c.email; $('mail').querySelector('.lh').textContent = c.email; }
}).then(window.siteReady);
})();
+20
View File
@@ -0,0 +1,20 @@
// Числа в карточках проекта «набегают» от нуля, когда появляются на экране. Без JS и при «уменьшить движение» остаются как есть.
(function () {
var nodes = document.querySelectorAll('[data-count]');
if (!nodes.length || !('IntersectionObserver' in window) || matchMedia('(prefers-reduced-motion: reduce)').matches) return;
var fmt = function (n, dec) { return n.toFixed(dec).replace('.', ','); };
var run = function (el) {
var raw = el.getAttribute('data-count'), end = parseFloat(raw), dec = (raw.split('.')[1] || '').length;
if (isNaN(end)) return;
var t0 = performance.now(), dur = 900;
(function step(now) {
var k = Math.min(1, (now - t0) / dur), e = 1 - Math.pow(1 - k, 3);
el.textContent = fmt(end * e, dec);
if (k < 1) requestAnimationFrame(step);
})(t0);
};
var io = new IntersectionObserver(function (items) {
items.forEach(function (it) { if (it.isIntersecting) { run(it.target); io.unobserve(it.target); } });
}, { threshold: 0.6 });
nodes.forEach(function (n) { io.observe(n); });
})();
+31
View File
@@ -0,0 +1,31 @@
// Верхнее меню из config.json: pages. На главной показывается, только если есть страницы; на остальных всегда.
(function () {
window.siteConfig.then(function (c) {
var nav = document.getElementById('topnav');
if (nav && c) {
var pages = (c.pages || []).filter(function (p) { return !p.draft; }); // черновики в меню не попадают
var norm = function (p) { return p.replace(/\/+$/, '') + '/'; };
var here = norm(location.pathname);
var home = here === '/';
if (pages.length || !home) {
var brand = document.createElement('a');
brand.className = 'brand'; brand.href = '/';
var parts = (c.name || 'Главная').split(' ');
brand.appendChild(document.createTextNode(parts[0]));
if (parts.length > 1) { var rest = document.createElement('span'); rest.className = 'brand-rest'; rest.textContent = ' ' + parts.slice(1).join(' '); brand.appendChild(rest); }
nav.appendChild(brand);
var ul = document.createElement('ul');
pages.forEach(function (p) {
if (!/^\/[\w\-./]*$/.test(p.url)) return; // только внутренние адреса
var a = document.createElement('a');
a.className = 'pill'; a.href = p.url; a.textContent = p.label;
var u = norm(p.url);
if (u === here || (u !== '/' && here.indexOf(u) === 0)) a.setAttribute('aria-current', 'page');
var li = document.createElement('li'); li.appendChild(a); ul.appendChild(li);
});
nav.appendChild(ul);
document.body.classList.add('has-nav');
}
}
}).then(window.siteReady);
})();