Dynamic content: landing and dashboard load texts from DB via API

- Landing hero texts (headline, description, badge) from settings table
- Landing categories from /api/tools (dynamic from DB)
- Landing advantages from /api/content/advantages
- Dashboard about block from /api/content/dashboard
- Static HTML remains as fallback if API fails
- All editable via AdminJS admin panel
This commit is contained in:
treamz 2026-03-21 23:53:37 +03:00
parent 1837f42a91
commit e8aef7aa28
4 changed files with 80 additions and 3 deletions

7
.adminjs/bundle.js Normal file
View File

@ -0,0 +1,7 @@
(function () {
'use strict';
AdminJS.UserComponents = {};
})();
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiYnVuZGxlLmpzIiwic291cmNlcyI6WyJlbnRyeS5qcyJdLCJzb3VyY2VzQ29udGVudCI6WyJBZG1pbkpTLlVzZXJDb21wb25lbnRzID0ge31cbiJdLCJuYW1lcyI6WyJBZG1pbkpTIiwiVXNlckNvbXBvbmVudHMiXSwibWFwcGluZ3MiOiI7OztDQUFBQSxPQUFPLENBQUNDLGNBQWMsR0FBRyxFQUFFOzs7Ozs7In0=

1
.adminjs/entry.js Normal file
View File

@ -0,0 +1 @@
AdminJS.UserComponents = {}

View File

@ -237,6 +237,17 @@ fetch('/auth/me').then(r => r.ok ? r.json() : null).then(user => {
const hu = document.getElementById('headerUser'); const hu = document.getElementById('headerUser');
if (hu) hu.textContent = name; if (hu) hu.textContent = name;
}).catch(() => {}); }).catch(() => {});
// Load about section from DB
fetch('/api/content/dashboard').then(r => r.ok ? r.json() : []).then(blocks => {
const about = blocks.find(b => b.block_key === 'about');
if (about) {
const h = document.querySelector('.about-card h2');
const p = document.querySelector('.about-card p');
if (h) h.textContent = about.title;
if (p) p.innerHTML = '<strong style="color:var(--text-primary);">WA Dev Tools</strong> — ' + about.body;
}
}).catch(() => {});
</script> </script>
</body> </body>
</html> </html>

View File

@ -612,19 +612,19 @@
<div style="position:relative;z-index:1;width:100%;"> <div style="position:relative;z-index:1;width:100%;">
<div class="hero-badge animate-fade-in"> <div class="hero-badge animate-fade-in">
<span class="dot"></span> <span class="dot"></span>
<span>13 инструментов — бесплатно навсегда</span> <span id="hero-badge-text">13 инструментов — бесплатно навсегда</span>
</div> </div>
<div class="hero-logo animate-slide-up delay-100"> <div class="hero-logo animate-slide-up delay-100">
WA Dev <span>Tools</span> WA Dev <span>Tools</span>
</div> </div>
<h1 class="hero-headline animate-slide-up delay-200"> <h1 class="hero-headline animate-slide-up delay-200" id="hero-headline">
Инструменты для<br> Инструменты для<br>
<span class="accent">веб-разработчика</span> <span class="accent">веб-разработчика</span>
</h1> </h1>
<p class="hero-sub animate-slide-up delay-300"> <p class="hero-sub animate-slide-up delay-300" id="hero-sub">
Сжатие картинок, форматирование кода, HTTP-клиент и ещё 10 инструментов — Сжатие картинок, форматирование кода, HTTP-клиент и ещё 10 инструментов —
всё в браузере, бесплатно и без регистрации карты. всё в браузере, бесплатно и без регистрации карты.
</p> </p>
@ -901,3 +901,61 @@
</body> </body>
</html> </html>
<script>
/* ── Dynamic content from API ── */
(async function() {
try {
const [settingsRes, toolsRes, advRes] = await Promise.all([
fetch('/api/settings'),
fetch('/api/tools'),
fetch('/api/content/advantages'),
]);
if (settingsRes.ok) {
const s = await settingsRes.json();
const headline = document.getElementById('hero-headline');
if (headline && s.hero_headline) {
const parts = s.hero_headline.split(' ');
const last = parts.pop();
headline.innerHTML = parts.join(' ') + '<br><span class="accent">' + last + '</span>';
}
const sub = document.getElementById('hero-sub');
if (sub && s.hero_description) sub.textContent = s.hero_description;
const badge = document.getElementById('hero-badge-text');
if (badge && s.site_tagline) badge.textContent = s.site_tagline;
}
if (toolsRes.ok) {
const cats = await toolsRes.json();
const grid = document.querySelector('.cat-grid');
if (grid && cats.length) {
grid.innerHTML = cats.map((cat, i) => {
const toolsList = cat.tools.map(t => '<li class="cat-tool-item">' + t.title + '</li>').join('');
return '<div class="cat-card reveal visible reveal-delay-' + (i+1) + '">' +
'<div class="cat-icon-wrap" style="color:' + (cat.color || '#0054e6') + '">' +
'<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75"><rect x="3" y="3" width="18" height="18" rx="3"/></svg>' +
'</div><div><div class="cat-header"><span class="cat-title">' + cat.title + '</span>' +
'<span class="cat-badge">' + cat.tools.length + ' инстр.</span></div></div>' +
'<p class="cat-desc">' + (cat.description || '') + '</p>' +
'<ul class="cat-tools">' + toolsList + '</ul></div>';
}).join('');
}
}
if (advRes.ok) {
const blocks = await advRes.json();
const advGrid = document.querySelector('.adv-grid');
if (advGrid && blocks.length) {
advGrid.innerHTML = blocks.map((b, i) =>
'<div class="adv-card reveal visible reveal-delay-' + i + '">' +
'<div class="adv-icon-wrap"><svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75"><path stroke-linecap="round" stroke-linejoin="round" d="M11.48 3.499a.562.562 0 011.04 0l2.125 5.111a.563.563 0 00.475.345l5.518.442c.499.04.701.663.321.988l-4.204 3.602a.563.563 0 00-.182.557l1.285 5.385a.562.562 0 01-.84.61l-4.725-2.885a.563.563 0 00-.586 0L6.982 20.54a.562.562 0 01-.84-.61l1.285-5.386a.562.562 0 00-.182-.557l-4.204-3.602a.563.563 0 01.321-.988l5.518-.442a.563.563 0 00.475-.345L11.48 3.5z"/></svg></div>' +
'<div class="adv-title">' + b.title + '</div>' +
'<p class="adv-desc">' + b.body + '</p></div>'
).join('');
}
}
} catch(e) { /* silent — static content stays as fallback */ }
})();
</script>