feat: страница /ai-dev/ — AI-разработка и проекты, карточка в Активности
This commit is contained in:
1 parent
39dad71bd1
commit
52485a17c4
10 files changed
+1895
-810
No files matched your search
@@ -0,0 +1,653 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru" data-theme="dark" data-radius="medium">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
||||
<meta
|
||||
name="description"
|
||||
content="Николай Мамадаев, AI-разработчик. Делаю целые продукты: Telegram-боты, MCP-серверы, плагины для Figma и Obsidian, desktop-утилиты."
|
||||
/>
|
||||
<title>AI-разработка · Николай Мамадаев</title>
|
||||
<script>
|
||||
(function () {
|
||||
var FONT_CSS = {
|
||||
merriweather: "/assets/fonts/merriweather.css",
|
||||
geologica: "/assets/fonts/geologica.css",
|
||||
mono: "/assets/fonts/mono.css",
|
||||
};
|
||||
try {
|
||||
var t = JSON.parse(localStorage.getItem("siteThemeV1") || "{}");
|
||||
var d = document.documentElement;
|
||||
var m = t.mode;
|
||||
if (m !== "light" && m !== "dark" && m !== "system") m = "system";
|
||||
var eff =
|
||||
m === "system"
|
||||
? window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches
|
||||
? "dark"
|
||||
: "light"
|
||||
: m;
|
||||
d.dataset.theme = eff;
|
||||
var DEF = { h: 210, s: 90, l: 40 };
|
||||
function pickAccent(o) {
|
||||
if (!o) return null;
|
||||
var h = typeof o.h === "number" ? o.h : typeof o.hue === "number" ? o.hue : null;
|
||||
var s = typeof o.s === "number" ? o.s : typeof o.sat === "number" ? o.sat : null;
|
||||
var l = typeof o.l === "number" ? o.l : typeof o.light === "number" ? o.light : null;
|
||||
if (h === null && s === null && l === null) return null;
|
||||
return {
|
||||
h: h === null ? DEF.h : h,
|
||||
s: s === null ? DEF.s : Math.min(100, Math.max(30, s)),
|
||||
l: l === null ? DEF.l : Math.min(70, Math.max(20, l)),
|
||||
};
|
||||
}
|
||||
// Текущий формат: отдельный акцент для светлой и тёмной темы ({light, dark}).
|
||||
// Старый плоский формат ({h, s, l}) остаётся совместимым.
|
||||
var acc =
|
||||
pickAccent(eff === "dark" ? t.dark : t.light) ||
|
||||
pickAccent(eff === "dark" ? t.light : t.dark) ||
|
||||
pickAccent(t) ||
|
||||
DEF;
|
||||
d.style.setProperty("--theme-h", String(acc.h));
|
||||
d.style.setProperty("--theme-s", String(acc.s));
|
||||
d.style.setProperty("--theme-l", String(acc.l));
|
||||
var H = (((acc.h % 360) + 360) % 360) / 360;
|
||||
var S = Math.min(100, Math.max(0, acc.s)) / 100;
|
||||
var Lv = Math.min(100, Math.max(0, acc.l)) / 100;
|
||||
var r, g, b;
|
||||
if (S === 0) { r = g = b = Lv; }
|
||||
else {
|
||||
var q = Lv < 0.5 ? Lv * (1 + S) : Lv + S - Lv * S;
|
||||
var p = 2 * Lv - q;
|
||||
var hue2rgb = function(p, q, t) {
|
||||
if (t < 0) t += 1; if (t > 1) t -= 1;
|
||||
if (t < 1/6) return p + (q - p) * 6 * t;
|
||||
if (t < 1/2) return q;
|
||||
if (t < 2/3) return p + (q - p) * (2/3 - t) * 6;
|
||||
return p;
|
||||
};
|
||||
r = hue2rgb(p, q, H + 1/3);
|
||||
g = hue2rgb(p, q, H);
|
||||
b = hue2rgb(p, q, H - 1/3);
|
||||
}
|
||||
var lum = 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
||||
d.style.setProperty("--on-accent", lum > 0.58 ? "hsl(220 14% 12%)" : "hsl(0 0% 98%)");
|
||||
var rad = t.radius;
|
||||
if (rad === "soft" || rad === "medium" || rad === "strong") {
|
||||
d.dataset.radius = rad;
|
||||
} else {
|
||||
d.dataset.radius = "medium";
|
||||
}
|
||||
var f = t.font;
|
||||
if (f === "onest") f = "geologica";
|
||||
if (f === "anonymous") f = "mono";
|
||||
if (f !== "merriweather" && f !== "mono" && f !== "geologica") f = "geologica";
|
||||
d.dataset.font = f;
|
||||
var mb = t.meshBg;
|
||||
if (mb === "off") {
|
||||
d.dataset.meshBg = "off";
|
||||
} else {
|
||||
d.dataset.meshBg = "on";
|
||||
}
|
||||
var href = FONT_CSS[f];
|
||||
if (href && !document.querySelector('link[data-theme-font="' + f + '"]')) {
|
||||
var link = document.createElement("link");
|
||||
link.rel = "stylesheet";
|
||||
link.href = href;
|
||||
link.setAttribute("data-theme-font", f);
|
||||
document.head.appendChild(link);
|
||||
}
|
||||
} catch (e) {}
|
||||
})();
|
||||
</script>
|
||||
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/assets/favicon/favicon-32x32.png" />
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/assets/favicon/favicon-16x16.png" />
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/assets/favicon/apple-touch-icon.png" />
|
||||
<link rel="manifest" href="/assets/favicon/site.webmanifest" />
|
||||
<link rel="preload" href="/assets/fonts/merriweather.css" as="style" />
|
||||
<link rel="preload" href="/css/main.css" as="style" />
|
||||
<link rel="preload" href="/js/skill-connectors.js" as="script" />
|
||||
<link rel="modulepreload" href="/js/main.js" />
|
||||
<link rel="modulepreload" href="/js/modules/ui/page-mesh-shader.js" />
|
||||
<link rel="stylesheet" href="/css/main.css" />
|
||||
<link rel="canonical" href="https://mamadaevv.site/ai-dev/" />
|
||||
<link rel="alternate" hreflang="ru" href="https://mamadaevv.site/ai-dev/" />
|
||||
<link rel="alternate" hreflang="en" href="https://mamadaevv.site/ai-dev/" />
|
||||
<link rel="alternate" hreflang="x-default" href="https://mamadaevv.site/ai-dev/" />
|
||||
<meta name="author" content="Николай Мамадаев" />
|
||||
<meta name="robots" content="index, follow" />
|
||||
<meta name="theme-color" content="#0f1115" />
|
||||
|
||||
<!-- Open Graph -->
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:site_name" content="Николай Мамадаев — продуктовый дизайнер" />
|
||||
<meta property="og:locale" content="ru_RU" />
|
||||
<meta property="og:locale:alternate" content="en_US" />
|
||||
<meta property="og:title" content="AI-разработка · Николай Мамадаев" />
|
||||
<meta property="og:description" content="Николай Мамадаев, AI-разработчик. Делаю целые продукты: Telegram-боты, MCP-серверы, плагины для Figma и Obsidian, desktop-утилиты." />
|
||||
<meta property="og:url" content="https://mamadaevv.site/ai-dev/" />
|
||||
<meta property="og:image" content="https://mamadaevv.site/assets/og/og-default.png" />
|
||||
<meta property="og:image:width" content="1200" />
|
||||
<meta property="og:image:height" content="630" />
|
||||
<meta property="og:image:alt" content="AI-разработка · Николай Мамадаев" />
|
||||
|
||||
<!-- Twitter / X -->
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:title" content="AI-разработка · Николай Мамадаев" />
|
||||
<meta name="twitter:description" content="Николай Мамадаев, AI-разработчик. Делаю целые продукты: Telegram-боты, MCP-серверы, плагины для Figma и Obsidian, desktop-утилиты." />
|
||||
<meta name="twitter:image" content="https://mamadaevv.site/assets/og/og-default.png" />
|
||||
<meta name="twitter:image:alt" content="AI-разработка · Николай Мамадаев" />
|
||||
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@graph": [
|
||||
{
|
||||
"@type": "Person",
|
||||
"@id": "https://mamadaevv.site/#person",
|
||||
"name": "Николай Мамадаев",
|
||||
"url": "https://mamadaevv.site",
|
||||
"jobTitle": "Продуктовый дизайнер",
|
||||
"description": "Продуктовый дизайнер с опытом более 10 лет. Проектирую цифровые продукты, интерфейсы и дизайн-системы.",
|
||||
"knowsAbout": [
|
||||
"UX/UI-дизайн",
|
||||
"продуктовый дизайн",
|
||||
"дизайн-системы",
|
||||
"прототипирование",
|
||||
"Figma",
|
||||
"no-code"
|
||||
],
|
||||
"sameAs": [
|
||||
"https://t.me/mamadaevv",
|
||||
"https://vk.com/mamadaevv",
|
||||
"https://github.com/mamadaevv"
|
||||
]
|
||||
},
|
||||
{
|
||||
"@type": "WebSite",
|
||||
"@id": "https://mamadaevv.site/#website",
|
||||
"url": "https://mamadaevv.site/",
|
||||
"name": "Николай Мамадаев — продуктовый дизайнер",
|
||||
"inLanguage": "ru",
|
||||
"author": {
|
||||
"@id": "https://mamadaevv.site/#person"
|
||||
},
|
||||
"publisher": {
|
||||
"@id": "https://mamadaevv.site/#person"
|
||||
}
|
||||
},
|
||||
{
|
||||
"@type": "WebPage",
|
||||
"@id": "https://mamadaevv.site/ai-dev/#page",
|
||||
"url": "https://mamadaevv.site/ai-dev/",
|
||||
"name": "AI-разработка · Николай Мамадаев",
|
||||
"description": "Николай Мамадаев, AI-разработчик. Делаю целые продукты: Telegram-боты, MCP-серверы, плагины для Figma и Obsidian, desktop-утилиты.",
|
||||
"isPartOf": {
|
||||
"@id": "https://mamadaevv.site/#website"
|
||||
},
|
||||
"author": {
|
||||
"@id": "https://mamadaevv.site/#person"
|
||||
},
|
||||
"inLanguage": "ru"
|
||||
},
|
||||
{
|
||||
"@type": "BreadcrumbList",
|
||||
"@id": "https://mamadaevv.site/ai-dev/#breadcrumbs",
|
||||
"itemListElement": [
|
||||
{
|
||||
"@type": "ListItem",
|
||||
"position": 1,
|
||||
"name": "Николай Мамадаев — продуктовый дизайнер",
|
||||
"item": "https://mamadaevv.site/"
|
||||
},
|
||||
{
|
||||
"@type": "ListItem",
|
||||
"position": 2,
|
||||
"name": "AI-разработка · Николай Мамадаев",
|
||||
"item": "https://mamadaevv.site/ai-dev/"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
</script>
|
||||
|
||||
<script src="/js/skill-connectors.js" defer></script>
|
||||
<script type="module" src="/js/main.js"></script>
|
||||
|
||||
|
||||
<link rel="stylesheet" href="/vendor/plyr/plyr.css" />
|
||||
|
||||
<style>
|
||||
.ai-page {
|
||||
max-width: 52rem;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.ai-page h2 {
|
||||
margin: 2.25rem 0 0.85rem;
|
||||
font-size: clamp(1.15rem, 2vw, 1.35rem);
|
||||
font-weight: 700;
|
||||
color: var(--text);
|
||||
}
|
||||
.ai-page .section__prose {
|
||||
margin-bottom: 0.75rem;
|
||||
color: var(--text);
|
||||
}
|
||||
.ai-page a:not(.btn) {
|
||||
color: var(--accent);
|
||||
}
|
||||
/* Сетка карточек проектов: 2 колонки, схлопывается в одну на мобильном */
|
||||
.ai-projects {
|
||||
list-style: none;
|
||||
margin: 1.25rem 0 0;
|
||||
padding: 0;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
.ai-projects > li {
|
||||
margin: 0;
|
||||
}
|
||||
.ai-projects__media {
|
||||
border-radius: var(--radius-md);
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--line);
|
||||
margin-bottom: 0.7rem;
|
||||
background: var(--surface);
|
||||
}
|
||||
.ai-projects__media img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
aspect-ratio: 16 / 10;
|
||||
object-fit: cover;
|
||||
}
|
||||
.ai-projects__head {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
.ai-projects__title {
|
||||
margin: 0;
|
||||
font-size: 1.05rem;
|
||||
font-weight: 600;
|
||||
color: var(--text);
|
||||
}
|
||||
.ai-projects__stack {
|
||||
margin: 0.3rem 0 0;
|
||||
font-family: var(--font-mono, ui-monospace, monospace);
|
||||
font-size: 0.78rem;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
.ai-projects__desc {
|
||||
margin: 0.45rem 0 0;
|
||||
font-size: 0.95rem;
|
||||
color: var(--text);
|
||||
}
|
||||
.ai-projects__links {
|
||||
list-style: none;
|
||||
margin: 0.6rem 0 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.4rem 0.9rem;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
/* Навыки — компактные чипы переиспользуют стили тегов карточек активности */
|
||||
.ai-stack-list {
|
||||
list-style: none;
|
||||
margin: 1rem 0 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.45rem;
|
||||
}
|
||||
.ai-stack-list li {
|
||||
margin: 0;
|
||||
padding: 0.3rem 0.7rem;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 999px;
|
||||
font-family: var(--font-mono, ui-monospace, monospace);
|
||||
font-size: 0.8rem;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<a class="skip-link" href="#case-main">К содержимому кейса</a>
|
||||
<header class="header" data-theme-picker>
|
||||
<div class="header__inner">
|
||||
<a class="logo" href="../" aria-label="На главную">НМ</a>
|
||||
<div class="header__cluster">
|
||||
<nav class="nav" aria-label="Разделы страницы">
|
||||
<a href="#contact">Контакты</a>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="fab-stack" aria-label="Быстрые действия">
|
||||
<div class="theme-panel-fab-wrap">
|
||||
<button type="button" class="fab fab--secondary glass-neutral" data-theme-panel-btn aria-expanded="false" aria-controls="theme-panel" aria-label="Настройки стиля">
|
||||
<span class="theme-panel-fab__icon theme-panel-fab__icon--open-hide" aria-hidden="true"></span>
|
||||
<svg class="theme-panel-fab__icon theme-panel-fab__icon--close" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
|
||||
<path d="M6 6L18 18M18 6L6 18" stroke="currentColor" stroke-width="2" stroke-linecap="round" />
|
||||
</svg>
|
||||
</button>
|
||||
<div id="theme-panel" class="theme-picker__panel glass-neutral" role="dialog" aria-label="Тема и оформление" hidden data-theme-panel>
|
||||
<div class="theme-picker__stack">
|
||||
<section class="theme-picker__section" aria-labelledby="theme-accent-title">
|
||||
<h3 id="theme-accent-title" class="theme-picker__section-title">Акцентный цвет</h3>
|
||||
<div class="colorpicker" data-colorpicker>
|
||||
<div class="colorpicker__plane-box">
|
||||
<canvas class="colorpicker__plane" width="320" height="180" data-sl-canvas aria-label="Насыщенность и светлость акцента"></canvas>
|
||||
<div class="colorpicker__knob" data-sl-knob></div>
|
||||
</div>
|
||||
<div class="colorpicker__hue-wrap">
|
||||
<label class="colorpicker__hue-sr" for="theme-hue-slider">Оттенок акцента</label>
|
||||
<input id="theme-hue-slider" class="colorpicker__hue" type="range" min="0" max="360" value="210" data-hue-slider aria-valuemin="0" aria-valuemax="360" />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="theme-picker__section" aria-labelledby="theme-mode-title">
|
||||
<h3 id="theme-mode-title" class="theme-picker__section-title">Тема</h3>
|
||||
<div class="radius-seg theme-mode-seg" role="radiogroup" aria-labelledby="theme-mode-title">
|
||||
<button type="button" class="radius-seg__btn" role="radio" aria-checked="false" data-theme-mode="system" aria-label="Как в системе"><span class="radius-seg__viz" aria-hidden="true"><span class="theme-mode-seg__ico theme-mode-seg__ico--system"></span></span></button>
|
||||
<button type="button" class="radius-seg__btn" role="radio" aria-checked="false" data-theme-mode="light" aria-label="Светлая тема"><span class="radius-seg__viz" aria-hidden="true"><span class="theme-mode-seg__ico theme-mode-seg__ico--light"></span></span></button>
|
||||
<button type="button" class="radius-seg__btn" role="radio" aria-checked="false" data-theme-mode="dark" aria-label="Тёмная тема"><span class="radius-seg__viz" aria-hidden="true"><span class="theme-mode-seg__ico theme-mode-seg__ico--dark"></span></span></button>
|
||||
</div>
|
||||
</section>
|
||||
<section class="theme-picker__section" aria-labelledby="theme-radius-title">
|
||||
<h3 id="theme-radius-title" class="theme-picker__section-title">Скругление углов</h3>
|
||||
<div class="radius-seg" role="radiogroup" aria-labelledby="theme-radius-title">
|
||||
<button type="button" class="radius-seg__btn" role="radio" aria-checked="false" data-radius-option="soft" aria-label="Слабое скругление"><span class="radius-seg__viz" aria-hidden="true"><span class="radius-seg__arc radius-seg__arc--soft"></span></span></button>
|
||||
<button type="button" class="radius-seg__btn" role="radio" aria-checked="false" data-radius-option="medium" aria-label="Среднее скругление"><span class="radius-seg__viz" aria-hidden="true"><span class="radius-seg__arc radius-seg__arc--medium"></span></span></button>
|
||||
<button type="button" class="radius-seg__btn" role="radio" aria-checked="false" data-radius-option="strong" aria-label="Сильное скругление"><span class="radius-seg__viz" aria-hidden="true"><span class="radius-seg__arc radius-seg__arc--strong"></span></span></button>
|
||||
</div>
|
||||
</section>
|
||||
<section class="theme-picker__section" aria-labelledby="theme-typography-title">
|
||||
<h3 id="theme-typography-title" class="theme-picker__section-title">Типографика</h3>
|
||||
<div class="radius-seg" role="radiogroup" aria-labelledby="theme-typography-title">
|
||||
<button type="button" class="radius-seg__btn theme-font-seg__btn" role="radio" aria-checked="false" data-font-option="geologica" aria-label="Sans"><span class="theme-font-seg__label">Sans</span></button>
|
||||
<button type="button" class="radius-seg__btn theme-font-seg__btn" role="radio" aria-checked="false" data-font-option="merriweather" aria-label="Sans Serif"><span class="theme-font-seg__label">Sans Serif</span></button>
|
||||
<button type="button" class="radius-seg__btn theme-font-seg__btn" role="radio" aria-checked="false" data-font-option="mono" aria-label="Mono"><span class="theme-font-seg__label">Mono</span></button>
|
||||
</div>
|
||||
</section>
|
||||
<section class="theme-picker__section" aria-labelledby="theme-mesh-bg-title">
|
||||
<h3 id="theme-mesh-bg-title" class="theme-picker__section-title">Фон страницы</h3>
|
||||
<div class="radius-seg radius-seg--two" role="radiogroup" aria-labelledby="theme-mesh-bg-title">
|
||||
<button type="button" class="radius-seg__btn theme-mesh-bg-seg__btn" role="radio" aria-checked="false" data-mesh-bg-option="on" aria-label="Показать фоновый градиент"><span class="theme-mesh-bg-seg__label">Вкл</span></button>
|
||||
<button type="button" class="radius-seg__btn theme-mesh-bg-seg__btn" role="radio" aria-checked="false" data-mesh-bg-option="off" aria-label="Скрыть фоновый градиент"><span class="theme-mesh-bg-seg__label">Выкл</span></button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<a class="fab fab--primary" href="#contact" aria-label="Связаться">
|
||||
<span class="fab-primary__ico" aria-hidden="true"></span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="content-edit-fab-stack" aria-label="Правки контента" data-content-edit-ui>
|
||||
<button
|
||||
type="button"
|
||||
class="fab fab--secondary glass-neutral content-edit-fab"
|
||||
data-content-edit-mode-toggle
|
||||
hidden
|
||||
aria-pressed="false"
|
||||
aria-label="Режим правок контента"
|
||||
title="Режим правок"
|
||||
>
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
||||
<path
|
||||
d="M4 20h4l10.5-10.5a2.12 2.12 0 0 0 0-3L16.5 4.5a2.12 2.12 0 0 0-3 0L3 15v5z"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.75"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
<span class="content-edit-fab__badge" hidden aria-hidden="true">0</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
<main id="case-main" class="section section--tight">
|
||||
<div class="section__inner ai-page">
|
||||
<p class="hero__eyebrow">AI-разработка · идея, код, деплой</p>
|
||||
<h1 class="case-page__h1">Делаю целые продукты</h1>
|
||||
<p class="case-page__sub">Telegram-боты, MCP-серверы, плагины и утилиты. Схема, код, деплой</p>
|
||||
|
||||
<p class="section__prose">
|
||||
Больше десяти лет я проектировал интерфейсы. Теперь довожу идеи до работающего
|
||||
кода: бэкенд, база данных, деплой. Инфраструктура тоже моя часть работы.
|
||||
В большинстве проектов код пишет ИИ-агент. Я задаю архитектуру, проверяю
|
||||
результат и отвечаю за то, что продукт вообще работает.
|
||||
</p>
|
||||
|
||||
<h2 id="roles">Что я делаю</h2>
|
||||
<ul class="section__prose ai-stack-list">
|
||||
<li>Продуктовая логика и API</li>
|
||||
<li>Telegram-боты на aiogram 3</li>
|
||||
<li>MCP-серверы</li>
|
||||
<li>Плагины Figma и Obsidian</li>
|
||||
<li>Автоматизация и агентные пайплайны</li>
|
||||
</ul>
|
||||
|
||||
<h2 id="projects">Проекты</h2>
|
||||
<ol class="ai-projects">
|
||||
<li>
|
||||
<div class="ai-projects__media">
|
||||
<img
|
||||
src="../assets/ai-dev/placeholder-01.webp"
|
||||
alt=""
|
||||
width="640"
|
||||
height="400"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
</div>
|
||||
<div class="ai-projects__head">
|
||||
<h3 class="ai-projects__title">Personal Assistant</h3>
|
||||
</div>
|
||||
<p class="ai-projects__stack">Python · aiogram 3 · PostgreSQL</p>
|
||||
<p class="ai-projects__desc">
|
||||
Личный ассистент в Telegram: напоминания, планирование дел и работа с LLM
|
||||
в одном диалоге.
|
||||
</p>
|
||||
<ul class="ai-projects__links">
|
||||
<li><a href="../cases/ai-platform-primo-rpa/" target="_blank" rel="noopener noreferrer">Кейс</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<div class="ai-projects__media">
|
||||
<img
|
||||
src="../assets/ai-dev/placeholder-02.webp"
|
||||
alt=""
|
||||
width="640"
|
||||
height="400"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
</div>
|
||||
<div class="ai-projects__head">
|
||||
<h3 class="ai-projects__title">Image Gen Bot</h3>
|
||||
</div>
|
||||
<p class="ai-projects__stack">Python · aiogram 3 · Redis · PostgreSQL</p>
|
||||
<p class="ai-projects__desc">
|
||||
Бот для генерации изображений: баланс, реферальная программа, админ-логи
|
||||
в отдельные топики супергруппы.
|
||||
</p>
|
||||
<ul class="ai-projects__links">
|
||||
<li><a href="../cases/ai-platform-primo-rpa/" target="_blank" rel="noopener noreferrer">Кейс</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<div class="ai-projects__media">
|
||||
<img
|
||||
src="../assets/ai-dev/placeholder-03.webp"
|
||||
alt=""
|
||||
width="640"
|
||||
height="400"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
</div>
|
||||
<div class="ai-projects__head">
|
||||
<h3 class="ai-projects__title">MCP Image Server</h3>
|
||||
</div>
|
||||
<p class="ai-projects__stack">JavaScript · MCP · OpenRouter</p>
|
||||
<p class="ai-projects__desc">
|
||||
Собственный MCP-сервер для генерации картинок. Подключается к агентам
|
||||
как инструмент.
|
||||
</p>
|
||||
<ul class="ai-projects__links">
|
||||
<li><a href="http://x1qnvk08zec2u5cx6yh5iexh.155.212.186.39.sslip.io/admin/mcp-img-gen" target="_blank" rel="noopener noreferrer">Код</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<div class="ai-projects__media">
|
||||
<img
|
||||
src="../assets/ai-dev/placeholder-01.webp"
|
||||
alt=""
|
||||
width="640"
|
||||
height="400"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
</div>
|
||||
<div class="ai-projects__head">
|
||||
<h3 class="ai-projects__title">AI assistant</h3>
|
||||
</div>
|
||||
<p class="ai-projects__stack">TypeScript · Figma Plugin API</p>
|
||||
<p class="ai-projects__desc">
|
||||
Плагин для Figma, который исполняет скрипты прямо в редакторе.
|
||||
ИИ-ассистент для любого тарифа.
|
||||
</p>
|
||||
<ul class="ai-projects__links">
|
||||
<li><a href="../cases/ai-assistant-figma/" target="_blank" rel="noopener noreferrer">Кейс</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<div class="ai-projects__media">
|
||||
<img
|
||||
src="../assets/ai-dev/placeholder-02.webp"
|
||||
alt=""
|
||||
width="640"
|
||||
height="400"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
</div>
|
||||
<div class="ai-projects__head">
|
||||
<h3 class="ai-projects__title">Card Columns</h3>
|
||||
</div>
|
||||
<p class="ai-projects__stack">TypeScript · Obsidian API</p>
|
||||
<p class="ai-projects__desc">
|
||||
Вью для заметок в стиле канбан-доски: группировка по колонкам с фильтрами
|
||||
по тегам.
|
||||
</p>
|
||||
<ul class="ai-projects__links">
|
||||
<li><a href="../cases/card-columns-obsidian/" target="_blank" rel="noopener noreferrer">Кейс</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<div class="ai-projects__media">
|
||||
<img
|
||||
src="../assets/ai-dev/placeholder-03.webp"
|
||||
alt=""
|
||||
width="640"
|
||||
height="400"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
</div>
|
||||
<div class="ai-projects__head">
|
||||
<h3 class="ai-projects__title">Hermes Desktop Plugins</h3>
|
||||
</div>
|
||||
<p class="ai-projects__stack">JavaScript · TypeScript · Electron</p>
|
||||
<p class="ai-projects__desc">
|
||||
Плагины для рабочего стола Hermes: чип баланса API, звук завершения задачи
|
||||
и контроль трат.
|
||||
</p>
|
||||
<ul class="ai-projects__links">
|
||||
<li><a href="http://x1qnvk08zec2u5cx6yh5iexh.155.212.186.39.sslip.io/admin/unorouter-balance" target="_blank" rel="noopener noreferrer">Код</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<div class="ai-projects__media">
|
||||
<img
|
||||
src="../assets/ai-dev/placeholder-01.webp"
|
||||
alt=""
|
||||
width="640"
|
||||
height="400"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
</div>
|
||||
<div class="ai-projects__head">
|
||||
<h3 class="ai-projects__title">Social Post Writer</h3>
|
||||
</div>
|
||||
<p class="ai-projects__stack">Python · aiogram 3 · SQLite</p>
|
||||
<p class="ai-projects__desc">
|
||||
Бот, который превращает тезисы в готовый пост под нужную площадку и тон,
|
||||
учитывая голос автора.
|
||||
</p>
|
||||
<ul class="ai-projects__links">
|
||||
<li><a href="http://x1qnvk08zec2u5cx6yh5iexh.155.212.186.39.sslip.io/admin/social-post-writer-bot" rel="noopener noreferrer">Код</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<div class="ai-projects__media">
|
||||
<img
|
||||
src="../assets/ai-dev/placeholder-02.webp"
|
||||
alt=""
|
||||
width="640"
|
||||
height="400"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
</div>
|
||||
<div class="ai-projects__head">
|
||||
<h3 class="ai-projects__title">AI Design Assistant</h3>
|
||||
</div>
|
||||
<p class="ai-projects__stack">HTML · CSS · GLSL · WebGL</p>
|
||||
<p class="ai-projects__desc">
|
||||
Этот сайт целиком: структура, дизайн-система, шейдеры, CI/CD.
|
||||
Весь цикл создания вместе с ИИ-агентом.
|
||||
</p>
|
||||
<ul class="ai-projects__links">
|
||||
<li><a href="../cases/site-portfolio/" target="_blank" rel="noopener noreferrer">Кейс</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ol>
|
||||
|
||||
<h2 id="stack">Стек</h2>
|
||||
<ul class="section__prose ai-stack-list">
|
||||
<li>Python</li>
|
||||
<li>TypeScript</li>
|
||||
<li>JavaScript</li>
|
||||
<li>Go</li>
|
||||
<li>HTML/CSS</li>
|
||||
<li>GLSL</li>
|
||||
<li>aiogram 3</li>
|
||||
<li>FastAPI</li>
|
||||
<li>PostgreSQL</li>
|
||||
<li>Redis</li>
|
||||
<li>SQLite</li>
|
||||
<li>Docker</li>
|
||||
<li>Coolify</li>
|
||||
<li>Gitea</li>
|
||||
<li>MCP</li>
|
||||
</ul>
|
||||
|
||||
<h2 id="links">Где я это выкладываю</h2>
|
||||
<p class="section__prose">
|
||||
Код открыт: <a href="https://github.com/mamadaevv" target="_blank" rel="noopener noreferrer">GitHub</a>
|
||||
и <a href="http://x1qnvk08zec2u5cx6yh5iexh.155.212.186.39.sslip.io/admin" target="_blank" rel="noopener noreferrer">Gitea</a>.
|
||||
Сервисы, которые работают прямо сейчас, живут на
|
||||
<a href="https://secrets-4-me.dynv6.net" target="_blank" rel="noopener noreferrer">secrets-4-me</a>.
|
||||
</p>
|
||||
</div>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 594 B |
Binary file not shown.
|
After Width: | Height: | Size: 526 B |
Binary file not shown.
|
After Width: | Height: | Size: 594 B |
+207
-191
@@ -818,197 +818,213 @@
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section id="activity" class="section section--surface" aria-labelledby="mini-projects-title" data-reveal>
|
||||
<div class="section__inner">
|
||||
<h2 id="mini-projects-title" class="section__title">Активность</h2>
|
||||
<div
|
||||
class="skills-intro skills-intro--chat section__prose"
|
||||
data-skills-intro
|
||||
role="region"
|
||||
aria-labelledby="mini-projects-title"
|
||||
>
|
||||
<div class="skills-msg" data-skills-msg>
|
||||
<div class="skills-msg__body">
|
||||
<p class="skills-msg__author">Николай М:</p>
|
||||
<p class="skills-msg__text">
|
||||
Люблю делать разные мини-проекты или делиться чем-то полезным с миром.
|
||||
</p>
|
||||
<time class="skills-msg__time" datetime="" data-skills-msg-time></time>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ol id="mini-projects-list" class="mini-projects-grid" data-mini-projects-list>
|
||||
<li class="mini-projects-grid__item" data-mp-cells="2">
|
||||
<a
|
||||
class="mp-card mp-card--link mp-card--compact"
|
||||
href="cases/ai-assistant-figma/"
|
||||
data-glass="false"
|
||||
aria-label="AI assistant — открыть кейс"
|
||||
>
|
||||
<div class="mp-card__body">
|
||||
<h3 class="mp-card__title">AI assistant</h3>
|
||||
<ul class="mp-card__tags" aria-label="Тип проекта">
|
||||
<li class="mp-card__tag">Плагин для Figma</li>
|
||||
</ul>
|
||||
<p class="mp-card__desc">ИИ помощник для любого тарифа редактора</p>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
<li class="mini-projects-grid__item" data-mp-cells="2">
|
||||
<a
|
||||
class="mp-card mp-card--link mp-card--compact"
|
||||
href="cases/card-columns-obsidian/"
|
||||
data-glass="false"
|
||||
aria-label="Card Columns — открыть кейс"
|
||||
>
|
||||
<div class="mp-card__body">
|
||||
<h3 class="mp-card__title">Card Columns</h3>
|
||||
<ul class="mp-card__tags" aria-label="Тип проекта">
|
||||
<li class="mp-card__tag">Плагин для Obsidian</li>
|
||||
</ul>
|
||||
<p class="mp-card__desc">Отображение карточек с заметками в стиле канбан доски</p>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
<li class="mini-projects-grid__item" data-mp-cells="2">
|
||||
<a
|
||||
class="mp-card mp-card--link mp-card--compact"
|
||||
href="cases/site-portfolio/"
|
||||
data-glass="false"
|
||||
aria-label="Этот сайт — открыть кейс"
|
||||
>
|
||||
<div class="mp-card__body">
|
||||
<h3 class="mp-card__title">Этот сайт</h3>
|
||||
<ul class="mp-card__tags" aria-label="Тип проекта">
|
||||
<li class="mp-card__tag">Разработка с AI IDE</li>
|
||||
</ul>
|
||||
<p class="mp-card__desc">Весь цикл создания сайта при помощи ИИ</p>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
<li class="mini-projects-grid__item" data-mp-cells="2">
|
||||
<a
|
||||
class="mp-card mp-card--link mp-card--horizontal mp-card--hor-image"
|
||||
href="https://www.figma.com/@mamadaevv"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label="Figma Community — открыть профиль"
|
||||
>
|
||||
<div class="mp-card__media" aria-hidden="true">
|
||||
<img
|
||||
class="mp-card__image"
|
||||
src="assets/misc/figma_logo.webp"
|
||||
alt=""
|
||||
width="800"
|
||||
height="800"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
</div>
|
||||
<div class="mp-card__body">
|
||||
<h3 class="mp-card__title">
|
||||
Figma Community
|
||||
<span class="mp-card__ext" aria-hidden="true"></span>
|
||||
</h3>
|
||||
<p class="mp-card__desc">
|
||||
Выкладываю полезные файлы и плагины для сообщества
|
||||
</p>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
<li class="mini-projects-grid__item" data-mp-cells="2">
|
||||
<a
|
||||
class="mp-card mp-card--link mp-card--horizontal mp-card--hor-image"
|
||||
href="https://dribbble.com/mamadaevv"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label="Dribbble — открыть профиль"
|
||||
>
|
||||
<div class="mp-card__media" aria-hidden="true">
|
||||
<img
|
||||
class="mp-card__image"
|
||||
src="assets/misc/dribbble_logo.webp"
|
||||
alt=""
|
||||
width="1024"
|
||||
height="1024"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
</div>
|
||||
<div class="mp-card__body">
|
||||
<h3 class="mp-card__title">
|
||||
Dribbble
|
||||
<span class="mp-card__ext" aria-hidden="true"></span>
|
||||
</h3>
|
||||
<p class="mp-card__desc">
|
||||
Красивые картинки с элементами интерфейсов
|
||||
</p>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
<li class="mini-projects-grid__item" data-mp-cells="2">
|
||||
<a
|
||||
class="mp-card mp-card--link mp-card--horizontal mp-card--hor-image"
|
||||
href="https://t.me/designnotpizza"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label="Дизайн не пицца — открыть канал в Telegram"
|
||||
>
|
||||
<div class="mp-card__media" aria-hidden="true">
|
||||
<img
|
||||
class="mp-card__image"
|
||||
src="assets/misc/tg-channel-logo-design_not_pizza.webp"
|
||||
alt=""
|
||||
width="640"
|
||||
height="640"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
</div>
|
||||
<div class="mp-card__body">
|
||||
<h3 class="mp-card__title">
|
||||
Дизайн не пицца
|
||||
<span class="mp-card__ext" aria-hidden="true"></span>
|
||||
</h3>
|
||||
<p class="mp-card__desc">
|
||||
Мой канал про дизайн. Посты на тему продуктового дизайна и полезные ссылки
|
||||
</p>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
<li class="mini-projects-grid__item" data-mp-cells="2">
|
||||
<a
|
||||
class="mp-card mp-card--link mp-card--horizontal mp-card--hor-image"
|
||||
href="https://github.com/mamadaevv"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label="GitHub — открыть профиль"
|
||||
>
|
||||
<div class="mp-card__media" aria-hidden="true">
|
||||
<img
|
||||
class="mp-card__image"
|
||||
src="assets/misc/github_logo.webp"
|
||||
alt=""
|
||||
width="800"
|
||||
height="800"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
</div>
|
||||
<div class="mp-card__body">
|
||||
<h3 class="mp-card__title">
|
||||
GitHub
|
||||
<span class="mp-card__ext" aria-hidden="true"></span>
|
||||
</h3>
|
||||
<p class="mp-card__desc">
|
||||
Мои проекты и эксперименты с кодом
|
||||
</p>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
</section>
|
||||
<section id="activity" class="section section--surface" aria-labelledby="mini-projects-title" data-reveal>
|
||||
<div class="section__inner">
|
||||
<h2 id="mini-projects-title" class="section__title">Активность</h2>
|
||||
<div
|
||||
class="skills-intro skills-intro--chat section__prose"
|
||||
data-skills-intro
|
||||
role="region"
|
||||
aria-labelledby="mini-projects-title"
|
||||
>
|
||||
<div class="skills-msg" data-skills-msg>
|
||||
<div class="skills-msg__body">
|
||||
<p class="skills-msg__author">Николай М:</p>
|
||||
<p class="skills-msg__text">
|
||||
Люблю делать разные мини-проекты или делиться чем-то полезным с миром.
|
||||
</p>
|
||||
<time class="skills-msg__time" datetime="" data-skills-msg-time></time>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ol id="mini-projects-list" class="mini-projects-grid" data-mini-projects-list>
|
||||
<li class="mini-projects-grid__item" data-mp-cells="2">
|
||||
<a
|
||||
class="mp-card mp-card--link mp-card--compact"
|
||||
href="ai-dev/"
|
||||
data-glass="false"
|
||||
aria-label="AI-разработка — открыть страницу"
|
||||
>
|
||||
<div class="mp-card__body">
|
||||
<h3 class="mp-card__title">AI-разработка</h3>
|
||||
<ul class="mp-card__tags" aria-label="Тип проекта">
|
||||
<li class="mp-card__tag">Боты · MCP · плагины</li>
|
||||
</ul>
|
||||
<p class="mp-card__desc">Не только дизайн, а целые продукты: от API до деплоя</p>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
<li class="mini-projects-grid__item" data-mp-cells="2">
|
||||
<a
|
||||
class="mp-card mp-card--link mp-card--compact"
|
||||
href="cases/ai-assistant-figma/"
|
||||
data-glass="false"
|
||||
aria-label="AI assistant — открыть кейс"
|
||||
>
|
||||
<div class="mp-card__body">
|
||||
<h3 class="mp-card__title">AI assistant</h3>
|
||||
<ul class="mp-card__tags" aria-label="Тип проекта">
|
||||
<li class="mp-card__tag">Плагин для Figma</li>
|
||||
</ul>
|
||||
<p class="mp-card__desc">ИИ помощник для любого тарифа редактора</p>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
<li class="mini-projects-grid__item" data-mp-cells="2">
|
||||
<a
|
||||
class="mp-card mp-card--link mp-card--compact"
|
||||
href="cases/card-columns-obsidian/"
|
||||
data-glass="false"
|
||||
aria-label="Card Columns — открыть кейс"
|
||||
>
|
||||
<div class="mp-card__body">
|
||||
<h3 class="mp-card__title">Card Columns</h3>
|
||||
<ul class="mp-card__tags" aria-label="Тип проекта">
|
||||
<li class="mp-card__tag">Плагин для Obsidian</li>
|
||||
</ul>
|
||||
<p class="mp-card__desc">Отображение карточек с заметками в стиле канбан доски</p>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
<li class="mini-projects-grid__item" data-mp-cells="2">
|
||||
<a
|
||||
class="mp-card mp-card--link mp-card--compact"
|
||||
href="cases/site-portfolio/"
|
||||
data-glass="false"
|
||||
aria-label="Этот сайт — открыть кейс"
|
||||
>
|
||||
<div class="mp-card__body">
|
||||
<h3 class="mp-card__title">Этот сайт</h3>
|
||||
<ul class="mp-card__tags" aria-label="Тип проекта">
|
||||
<li class="mp-card__tag">Разработка с AI IDE</li>
|
||||
</ul>
|
||||
<p class="mp-card__desc">Весь цикл создания сайта при помощи ИИ</p>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
<li class="mini-projects-grid__item" data-mp-cells="2">
|
||||
<a
|
||||
class="mp-card mp-card--link mp-card--horizontal mp-card--hor-image"
|
||||
href="https://www.figma.com/@mamadaevv"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label="Figma Community — открыть профиль"
|
||||
>
|
||||
<div class="mp-card__media" aria-hidden="true">
|
||||
<img
|
||||
class="mp-card__image"
|
||||
src="assets/misc/figma_logo.webp"
|
||||
alt=""
|
||||
width="800"
|
||||
height="800"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
</div>
|
||||
<div class="mp-card__body">
|
||||
<h3 class="mp-card__title">
|
||||
Figma Community
|
||||
<span class="mp-card__ext" aria-hidden="true"></span>
|
||||
</h3>
|
||||
<p class="mp-card__desc">
|
||||
Выкладываю полезные файлы и плагины для сообщества
|
||||
</p>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
<li class="mini-projects-grid__item" data-mp-cells="2">
|
||||
<a
|
||||
class="mp-card mp-card--link mp-card--horizontal mp-card--hor-image"
|
||||
href="https://dribbble.com/mamadaevv"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label="Dribbble — открыть профиль"
|
||||
>
|
||||
<div class="mp-card__media" aria-hidden="true">
|
||||
<img
|
||||
class="mp-card__image"
|
||||
src="assets/misc/dribbble_logo.webp"
|
||||
alt=""
|
||||
width="1024"
|
||||
height="1024"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
</div>
|
||||
<div class="mp-card__body">
|
||||
<h3 class="mp-card__title">
|
||||
Dribbble
|
||||
<span class="mp-card__ext" aria-hidden="true"></span>
|
||||
</h3>
|
||||
<p class="mp-card__desc">
|
||||
Красивые картинки с элементами интерфейсов
|
||||
</p>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
<li class="mini-projects-grid__item" data-mp-cells="2">
|
||||
<a
|
||||
class="mp-card mp-card--link mp-card--horizontal mp-card--hor-image"
|
||||
href="https://t.me/designnotpizza"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label="Дизайн не пицца — открыть канал в Telegram"
|
||||
>
|
||||
<div class="mp-card__media" aria-hidden="true">
|
||||
<img
|
||||
class="mp-card__image"
|
||||
src="assets/misc/tg-channel-logo-design_not_pizza.webp"
|
||||
alt=""
|
||||
width="640"
|
||||
height="640"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
</div>
|
||||
<div class="mp-card__body">
|
||||
<h3 class="mp-card__title">
|
||||
Дизайн не пицца
|
||||
<span class="mp-card__ext" aria-hidden="true"></span>
|
||||
</h3>
|
||||
<p class="mp-card__desc">
|
||||
Мой канал про дизайн. Посты на тему продуктового дизайна и полезные ссылки
|
||||
</p>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
<li class="mini-projects-grid__item" data-mp-cells="2">
|
||||
<a
|
||||
class="mp-card mp-card--link mp-card--horizontal mp-card--hor-image"
|
||||
href="https://github.com/mamadaevv"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label="GitHub — открыть профиль"
|
||||
>
|
||||
<div class="mp-card__media" aria-hidden="true">
|
||||
<img
|
||||
class="mp-card__image"
|
||||
src="assets/misc/github_logo.webp"
|
||||
alt=""
|
||||
width="800"
|
||||
height="800"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
</div>
|
||||
<div class="mp-card__body">
|
||||
<h3 class="mp-card__title">
|
||||
GitHub
|
||||
<span class="mp-card__ext" aria-hidden="true"></span>
|
||||
</h3>
|
||||
<p class="mp-card__desc">
|
||||
Мои проекты и эксперименты с кодом
|
||||
</p>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
<section id="experience" class="section section--surface" aria-labelledby="experience-title" data-reveal>
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
|
||||
xmlns:xhtml="http://www.w3.org/1999/xhtml">
|
||||
<url>
|
||||
<loc>https://mamadaevv.site/ai-dev/</loc>
|
||||
<lastmod>2026-10-07</lastmod>
|
||||
<xhtml:link rel="alternate" hreflang="x-default" href="https://mamadaevv.site/ai-dev/" />
|
||||
<xhtml:link rel="alternate" hreflang="ru" href="https://mamadaevv.site/ai-dev/" />
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://mamadaevv.site/cases/ai-assistant-figma/</loc>
|
||||
<lastmod>2026-09-26</lastmod>
|
||||
|
||||
+439
-428
@@ -1,428 +1,439 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
const rootDir = process.cwd();
|
||||
const srcPagesDir = path.join(rootDir, "src", "pages");
|
||||
const srcPartialsDir = path.join(rootDir, "src", "partials");
|
||||
const srcI18nDir = path.join(rootDir, "src", "i18n");
|
||||
const outDir = path.join(rootDir, "public");
|
||||
|
||||
// Локали: дефолтная (ru) собирается в корень public/, остальные — в public/<lang>/.
|
||||
const LOCALES = ["ru", "en"];
|
||||
const DEFAULT_LOCALE = "ru";
|
||||
|
||||
// SVG-displacement больше не используется: всё стекло — через CSS blur.
|
||||
// --- SEO ---
|
||||
const SITE_ORIGIN = "https://mamadaevv.site";
|
||||
const OG_IMAGE_DEFAULT = `${SITE_ORIGIN}/assets/og/og-default.png`;
|
||||
const AUTHOR_NAME_RU = "Николай Мамадаев";
|
||||
const AUTHOR_NAME_EN = "Nikolai Mamadaev";
|
||||
const SITE_NAME_RU = "Николай Мамадаев — продуктовый дизайнер";
|
||||
const SITE_NAME_EN = "Nikolai Mamadaev — Product Designer";
|
||||
// Служебные страницы-каталоги: закрыты от индексации.
|
||||
const NOINDEX_PAGES = new Set([
|
||||
"cases/design-system",
|
||||
"cases/template-elements-demo",
|
||||
"cases/mini-projects-showcase",
|
||||
]);
|
||||
|
||||
// Страницы, у которых пока нет перевода: собираются только в дефолтную локаль,
|
||||
// чтобы /en/ не отдавал русскоязычный дубль и не попадал в sitemap.
|
||||
const DEFAULT_LOCALE_ONLY_PAGES = new Set();
|
||||
|
||||
// Своя og-картинка для страницы основного AI Server-кейса.
|
||||
const PER_PAGE_OG_IMAGE = {
|
||||
"cases/ai-platform-primo-rpa": `${SITE_ORIGIN}/assets/og/og-primo-star.png`,
|
||||
};
|
||||
|
||||
const titleRe = /<title>([\s\S]*?)<\/title>/i;
|
||||
const descRe = /<meta\s+name="description"\s+content="([\s\S]*?)"\s*\/?>/i;
|
||||
|
||||
function decodeEntities(str) {
|
||||
return String(str)
|
||||
.replace(/ /g, " ")
|
||||
.replace(/&/g, "&")
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, "'")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">");
|
||||
}
|
||||
|
||||
function extractMeta(html) {
|
||||
const t = html.match(titleRe);
|
||||
const d = html.match(descRe);
|
||||
return {
|
||||
title: t ? decodeEntities(t[1]).trim() : "",
|
||||
description: d ? decodeEntities(d[1]).trim() : "",
|
||||
};
|
||||
}
|
||||
|
||||
/** Путь страницы на сайте (без ведущего слэша, с закрывающим). */
|
||||
function sitePath(relNorm, loc) {
|
||||
const base = relNorm
|
||||
.replace(/\.html$/, "")
|
||||
.replace(/(^|\/)index$/, "/");
|
||||
const norm = relNorm.startsWith("cases/")
|
||||
? `cases/${path.basename(relNorm, ".html")}/`
|
||||
: base === "resume" ? "resume/"
|
||||
: base === "cookie-policy" ? "cookie-policy/"
|
||||
: base === "" || base === "/" ? ""
|
||||
: `${base.replace(/\/$/, "")}/`;
|
||||
return loc === DEFAULT_LOCALE ? norm : `${loc}/${norm}`;
|
||||
}
|
||||
|
||||
/** Канонический URL страницы. */
|
||||
function canonicalFor(relNorm, loc) {
|
||||
const p = sitePath(relNorm, loc);
|
||||
return `${SITE_ORIGIN}/${p}`;
|
||||
}
|
||||
|
||||
/** Дата изменения страницы (mtime исходника) в формате YYYY-MM-DD. */
|
||||
function lastModFor(relNorm) {
|
||||
const src = path.join(srcPagesDir, relNorm);
|
||||
const enSrc = src.replace(/\.html$/, ".en.html");
|
||||
let mtime = 0;
|
||||
if (fs.existsSync(src)) mtime = fs.statSync(src).mtimeMs;
|
||||
if (fs.existsSync(enSrc)) mtime = Math.max(mtime, fs.statSync(enSrc).mtimeMs);
|
||||
return new Date(mtime || Date.now()).toISOString().slice(0, 10);
|
||||
}
|
||||
|
||||
/** JSON-LD для страницы: Person+WebSite на главной, CreativeWork на кейсах, BreadcrumbList везде. */
|
||||
function buildJsonLd(relNorm, loc, meta, canonical) {
|
||||
const isRu = loc === DEFAULT_LOCALE;
|
||||
const authorName = isRu ? AUTHOR_NAME_RU : AUTHOR_NAME_EN;
|
||||
const siteName = isRu ? SITE_NAME_RU : SITE_NAME_EN;
|
||||
const graph = [];
|
||||
const personId = `${SITE_ORIGIN}/#person`;
|
||||
const websiteId = `${SITE_ORIGIN}/#website`;
|
||||
|
||||
const person = {
|
||||
"@type": "Person",
|
||||
"@id": personId,
|
||||
name: authorName,
|
||||
url: SITE_ORIGIN,
|
||||
jobTitle: isRu ? "Продуктовый дизайнер" : "Product Designer",
|
||||
description: isRu
|
||||
? "Продуктовый дизайнер с опытом более 10 лет. Проектирую цифровые продукты, интерфейсы и дизайн-системы."
|
||||
: "Product designer with 10+ years of experience. Designing digital products, interfaces and design systems.",
|
||||
knowsAbout: isRu
|
||||
? ["UX/UI-дизайн", "продуктовый дизайн", "дизайн-системы", "прототипирование", "Figma", "no-code"]
|
||||
: ["UX/UI design", "product design", "design systems", "prototyping", "Figma", "no-code"],
|
||||
sameAs: ["https://t.me/mamadaevv", "https://vk.com/mamadaevv", "https://github.com/mamadaevv"],
|
||||
};
|
||||
graph.push(person);
|
||||
|
||||
graph.push({
|
||||
"@type": "WebSite",
|
||||
"@id": websiteId,
|
||||
url: `${SITE_ORIGIN}/`,
|
||||
name: siteName,
|
||||
inLanguage: loc,
|
||||
author: { "@id": personId },
|
||||
publisher: { "@id": personId },
|
||||
});
|
||||
|
||||
const isCase = relNorm.startsWith("cases/");
|
||||
const pageNode = {
|
||||
"@type": isCase ? "CreativeWork" : "WebPage",
|
||||
"@id": `${canonical}#page`,
|
||||
url: canonical,
|
||||
name: meta.title,
|
||||
description: meta.description,
|
||||
isPartOf: { "@id": websiteId },
|
||||
author: { "@id": personId },
|
||||
inLanguage: loc,
|
||||
};
|
||||
if (isCase && !NOINDEX_PAGES.has(relNorm.replace(/\.html$/, ""))) {
|
||||
pageNode.creator = { "@id": personId };
|
||||
pageNode.datePublished = lastModFor(relNorm);
|
||||
pageNode.about = isRu
|
||||
? "UX/UI-дизайн и проектирование интерфейсов"
|
||||
: "UX/UI design and interface design";
|
||||
}
|
||||
graph.push(pageNode);
|
||||
|
||||
// Хлебные крошки для вложенных страниц.
|
||||
const parts = sitePath(relNorm, loc).split("/").filter(Boolean);
|
||||
if (parts.length > 0) {
|
||||
const crumbs = [{ "@type": "ListItem", position: 1, name: siteName, item: `${SITE_ORIGIN}/` }];
|
||||
let acc = SITE_ORIGIN;
|
||||
let pos = 2;
|
||||
for (const part of parts) {
|
||||
if (part === loc) continue;
|
||||
acc += `/${part}`;
|
||||
crumbs.push({
|
||||
"@type": "ListItem",
|
||||
position: pos++,
|
||||
name: part === "cases" ? (isRu ? "Кейсы" : "Cases") : meta.title || part,
|
||||
item: `${acc}/`,
|
||||
});
|
||||
}
|
||||
if (crumbs.length > 1) {
|
||||
graph.push({
|
||||
"@type": "BreadcrumbList",
|
||||
"@id": `${canonical}#breadcrumbs`,
|
||||
itemListElement: crumbs,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
"@context": "https://schema.org",
|
||||
"@graph": graph,
|
||||
};
|
||||
}
|
||||
|
||||
/** Генерация sitemap.xml с hreflang-альтернатами. */
|
||||
function buildSitemap(pageFiles) {
|
||||
const urls = [];
|
||||
const localeSuffixRe = new RegExp(`\\.(${LOCALES.join("|")})\\.html$`, "i");
|
||||
for (const pagePath of pageFiles) {
|
||||
const relNorm = path.relative(srcPagesDir, pagePath).split(path.sep).join("/");
|
||||
const noSlug = relNorm.replace(/\.html$/, "");
|
||||
if (NOINDEX_PAGES.has(noSlug)) continue;
|
||||
const lastmod = lastModFor(relNorm);
|
||||
const locUrl = canonicalFor(relNorm, DEFAULT_LOCALE);
|
||||
const buildLocales = DEFAULT_LOCALE_ONLY_PAGES.has(noSlug) ? [DEFAULT_LOCALE] : LOCALES;
|
||||
const alternates = buildLocales.map(
|
||||
(loc) =>
|
||||
` <xhtml:link rel="alternate" hreflang="${loc}" href="${canonicalFor(relNorm, loc)}" />`
|
||||
);
|
||||
urls.push(
|
||||
` <url>\n <loc>${locUrl}</loc>\n <lastmod>${lastmod}</lastmod>\n` +
|
||||
` <xhtml:link rel="alternate" hreflang="x-default" href="${locUrl}" />\n` +
|
||||
alternates.join("\n") +
|
||||
`\n </url>`
|
||||
);
|
||||
}
|
||||
return (
|
||||
`<?xml version="1.0" encoding="UTF-8"?>\n` +
|
||||
`<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"\n` +
|
||||
` xmlns:xhtml="http://www.w3.org/1999/xhtml">\n` +
|
||||
urls.join("\n") +
|
||||
`\n</urlset>\n`
|
||||
);
|
||||
}
|
||||
|
||||
const includeRe = /\{\{\>\s*([a-z0-9-]+)\s*\}\}/gi;
|
||||
const varRe = /\{\{\s*([a-zA-Z0-9_]+)\s*\}\}/g;
|
||||
const i18nRe = /\{\{\s*i18n:([\w.-]+)\s*\}\}/g;
|
||||
|
||||
function readUtf8(filePath) {
|
||||
return fs.readFileSync(filePath, "utf8");
|
||||
}
|
||||
|
||||
function walkHtmlFiles(dir, acc = []) {
|
||||
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
const abs = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
walkHtmlFiles(abs, acc);
|
||||
continue;
|
||||
}
|
||||
if (entry.isFile() && entry.name.endsWith(".html")) {
|
||||
acc.push(abs);
|
||||
}
|
||||
}
|
||||
return acc;
|
||||
}
|
||||
|
||||
function loadPartials() {
|
||||
const partialMap = new Map();
|
||||
for (const filePath of walkHtmlFiles(srcPartialsDir)) {
|
||||
const name = path.basename(filePath, ".html");
|
||||
partialMap.set(name, readUtf8(filePath));
|
||||
}
|
||||
return partialMap;
|
||||
}
|
||||
|
||||
function loadI18n() {
|
||||
const map = {};
|
||||
for (const loc of LOCALES) {
|
||||
const file = path.join(srcI18nDir, `${loc}.json`);
|
||||
map[loc] = fs.existsSync(file) ? JSON.parse(readUtf8(file)) : {};
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
function resolveI18n(key, dict, fallback) {
|
||||
if (dict && Object.prototype.hasOwnProperty.call(dict, key)) return String(dict[key]);
|
||||
if (fallback && Object.prototype.hasOwnProperty.call(fallback, key)) return String(fallback[key]);
|
||||
return `{{i18n:${key}}}`;
|
||||
}
|
||||
|
||||
function renderTemplate(input, partials, vars, dict, fallback, loc, stack = []) {
|
||||
const withIncludes = input.replace(includeRe, (_, name) => {
|
||||
const key = String(name).trim();
|
||||
const partialKey = loc && loc !== DEFAULT_LOCALE && partials.has(`${key}.${loc}`) ? `${key}.${loc}` : key;
|
||||
if (!partials.has(partialKey)) {
|
||||
throw new Error(`Partial "${key}" not found`);
|
||||
}
|
||||
if (stack.includes(partialKey)) {
|
||||
throw new Error(`Circular partial include: ${[...stack, partialKey].join(" -> ")}`);
|
||||
}
|
||||
const partialContent = partials.get(partialKey);
|
||||
return renderTemplate(partialContent, partials, vars, dict, fallback, loc, [...stack, partialKey]);
|
||||
});
|
||||
|
||||
const withI18n = withIncludes.replace(i18nRe, (_, key) => resolveI18n(String(key).trim(), dict, fallback));
|
||||
|
||||
return withI18n.replace(varRe, (_, name) => {
|
||||
const key = String(name).trim();
|
||||
if (Object.prototype.hasOwnProperty.call(vars, key)) {
|
||||
return String(vars[key]);
|
||||
}
|
||||
return `{{${key}}}`;
|
||||
});
|
||||
}
|
||||
|
||||
function ensureDir(dirPath) {
|
||||
fs.mkdirSync(dirPath, { recursive: true });
|
||||
}
|
||||
|
||||
/** На странице кейса убираем из HTML карусели карточку со ссылкой на текущий файл (href уже с подставленным casesPrefix). */
|
||||
function stripCurrentCasePortfolioCarouselItem(html, omitHrefBasename) {
|
||||
if (!omitHrefBasename) return html;
|
||||
const slug = omitHrefBasename.replace(/\.html$/, "");
|
||||
const hrefDouble = `href="${slug}/"`;
|
||||
const hrefSingle = `href='${slug}/'`;
|
||||
const itemClass = "projects-carousel__item";
|
||||
let search = 0;
|
||||
while (true) {
|
||||
const i = html.indexOf("<article", search);
|
||||
if (i === -1) return html;
|
||||
const tagEnd = html.indexOf(">", i);
|
||||
if (tagEnd === -1) return html;
|
||||
const openTag = html.slice(i, tagEnd + 1);
|
||||
if (!openTag.includes(itemClass)) {
|
||||
search = i + 9;
|
||||
continue;
|
||||
}
|
||||
const contentStart = tagEnd + 1;
|
||||
const closeIdx = html.indexOf("</article>", contentStart);
|
||||
if (closeIdx === -1) return html;
|
||||
const afterArticle = closeIdx + "</article>".length;
|
||||
const block = html.slice(i, afterArticle);
|
||||
if (block.includes(hrefDouble) || block.includes(hrefSingle)) {
|
||||
return html.slice(0, i) + html.slice(afterArticle);
|
||||
}
|
||||
search = contentStart;
|
||||
}
|
||||
}
|
||||
|
||||
/** Базовые переменны для страницы в дефолтной локали (как было до i18n). */
|
||||
function baseVars(relNorm, depth) {
|
||||
if (relNorm.startsWith("cases/")) {
|
||||
return { assetPrefix: "../../", casesPrefix: "../" };
|
||||
}
|
||||
if (relNorm === "resume.html") {
|
||||
return { assetPrefix: "../", casesPrefix: "" };
|
||||
}
|
||||
if (relNorm === "cookie-policy.html") {
|
||||
return { assetPrefix: "../", casesPrefix: "" };
|
||||
}
|
||||
return {
|
||||
assetPrefix: depth === 0 ? "" : "../".repeat(depth),
|
||||
casesPrefix: depth === 0 ? "cases/" : "",
|
||||
};
|
||||
}
|
||||
|
||||
function build() {
|
||||
const partials = loadPartials();
|
||||
const i18n = loadI18n();
|
||||
const defaultDict = i18n[DEFAULT_LOCALE] || {};
|
||||
const localeSuffixRe = new RegExp(`\\.(${LOCALES.join("|")})\\.html$`, "i");
|
||||
const pageFiles = walkHtmlFiles(srcPagesDir).filter((p) => !localeSuffixRe.test(p));
|
||||
|
||||
for (const pagePath of pageFiles) {
|
||||
const rel = path.relative(srcPagesDir, pagePath);
|
||||
const relNorm = rel.split(path.sep).join("/");
|
||||
const pageDirRel = path.dirname(rel);
|
||||
const depth = pageDirRel === "." ? 0 : pageDirRel.split(path.sep).length;
|
||||
const isCase = relNorm.startsWith("cases/");
|
||||
const isResume = relNorm === "resume.html";
|
||||
const isCookiePolicy = relNorm === "cookie-policy.html";
|
||||
const omitFile = isCase ? path.basename(pagePath) : null;
|
||||
const noSlug = relNorm.replace(/\.html$/, "");
|
||||
|
||||
for (const loc of LOCALES) {
|
||||
if (loc !== DEFAULT_LOCALE && DEFAULT_LOCALE_ONLY_PAGES.has(noSlug)) continue;
|
||||
const dict = i18n[loc] || {};
|
||||
const isDefault = loc === DEFAULT_LOCALE;
|
||||
const langPrefix = isDefault ? "" : `${loc}/`;
|
||||
|
||||
// Переведённый исходник страницы для локали, если есть.
|
||||
let sourcePath = pagePath;
|
||||
if (!isDefault) {
|
||||
const override = pagePath.replace(/\.html$/, `.${loc}.html`);
|
||||
if (fs.existsSync(override)) sourcePath = override;
|
||||
}
|
||||
const source = readUtf8(sourcePath);
|
||||
|
||||
// --- SEO-переменные для head-seo / json-ld ---
|
||||
const isNoindex = NOINDEX_PAGES.has(noSlug);
|
||||
const canonicalUrl = canonicalFor(relNorm, loc);
|
||||
const metaSource = extractMeta(source);
|
||||
const pageTitle = metaSource.title || (isDefault ? SITE_NAME_RU : SITE_NAME_EN);
|
||||
const pageDesc = metaSource.description || pageTitle;
|
||||
const isCasePage = relNorm.startsWith("cases/");
|
||||
|
||||
const vars = baseVars(relNorm, depth);
|
||||
vars.canonicalUrl = canonicalUrl;
|
||||
vars.canonicalRu = canonicalFor(relNorm, "ru");
|
||||
vars.canonicalEn = canonicalFor(relNorm, "en");
|
||||
vars.authorName = isDefault ? AUTHOR_NAME_RU : AUTHOR_NAME_EN;
|
||||
vars.siteName = isDefault ? SITE_NAME_RU : SITE_NAME_EN;
|
||||
vars.metaRobots = isNoindex ? "noindex, nofollow" : "index, follow";
|
||||
vars.themeColor = "#0f1115";
|
||||
vars.ogType = isCasePage ? "article" : "website";
|
||||
vars.ogLocale = isDefault ? "ru_RU" : "en_US";
|
||||
vars.ogLocaleAlt = isDefault ? "en_US" : "ru_RU";
|
||||
vars.ogTitle = pageTitle;
|
||||
vars.ogDescription = pageDesc;
|
||||
vars.ogImage = PER_PAGE_OG_IMAGE[noSlug] || OG_IMAGE_DEFAULT;
|
||||
vars.ogImageAlt = pageTitle;
|
||||
vars.jsonLd = JSON.stringify(buildJsonLd(relNorm, loc, { title: pageTitle, description: pageDesc }, canonicalUrl), null, 2);
|
||||
|
||||
// Префикс до КОРНЯ ЛОКАЛИ (без en-сдвига) — для навигационных ссылок,
|
||||
// чтобы они вели в ту же локаль (/en/), а не в корень сайта (/).
|
||||
vars.pagePrefix = vars.assetPrefix;
|
||||
// Для не-дефолтной локали всё содержимое уходит на уровень глубже (public/<lang>/),
|
||||
// поэтому к assetPrefix добавляем один ../, а пустой casesPrefix — в ../cases/.
|
||||
if (!isDefault) {
|
||||
vars.assetPrefix = vars.assetPrefix === "" ? "../" : vars.assetPrefix + "../";
|
||||
if (vars.casesPrefix === "") vars.casesPrefix = "../cases/";
|
||||
}
|
||||
vars.lang = loc;
|
||||
|
||||
let outPath;
|
||||
if (isCase) {
|
||||
const slug = path.basename(pagePath, ".html");
|
||||
outPath = path.join(outDir, langPrefix, "cases", slug, "index.html");
|
||||
} else if (isResume) {
|
||||
outPath = path.join(outDir, langPrefix, "resume", "index.html");
|
||||
} else if (isCookiePolicy) {
|
||||
outPath = path.join(outDir, langPrefix, "cookie-policy", "index.html");
|
||||
} else {
|
||||
outPath = path.join(outDir, langPrefix, rel);
|
||||
}
|
||||
|
||||
let rendered = renderTemplate(source, partials, vars, dict, defaultDict, loc);
|
||||
// Подменяем язык в <html lang="..."> под текущую локаль.
|
||||
rendered = rendered.replace(/<html\s+lang="ru"/, `<html lang="${loc}"`);
|
||||
if (isCase) {
|
||||
rendered = stripCurrentCasePortfolioCarouselItem(rendered, omitFile);
|
||||
}
|
||||
ensureDir(path.dirname(outPath));
|
||||
fs.writeFileSync(outPath, rendered, "utf8");
|
||||
}
|
||||
}
|
||||
|
||||
const sitemapXml = buildSitemap(pageFiles);
|
||||
fs.writeFileSync(path.join(outDir, "sitemap.xml"), sitemapXml, "utf8");
|
||||
|
||||
console.log(`Built ${pageFiles.length} pages × ${LOCALES.length} locales into public/`);
|
||||
console.log(`Generated sitemap.xml (${sitemapXml.split("<url>").length - 1} urls)`);
|
||||
}
|
||||
|
||||
build();
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
const rootDir = process.cwd();
|
||||
const srcPagesDir = path.join(rootDir, "src", "pages");
|
||||
const srcPartialsDir = path.join(rootDir, "src", "partials");
|
||||
const srcI18nDir = path.join(rootDir, "src", "i18n");
|
||||
const outDir = path.join(rootDir, "public");
|
||||
|
||||
// Локали: дефолтная (ru) собирается в корень public/, остальные — в public/<lang>/.
|
||||
const LOCALES = ["ru", "en"];
|
||||
const DEFAULT_LOCALE = "ru";
|
||||
|
||||
// SVG-displacement больше не используется: всё стекло — через CSS blur.
|
||||
// --- SEO ---
|
||||
const SITE_ORIGIN = "https://mamadaevv.site";
|
||||
const OG_IMAGE_DEFAULT = `${SITE_ORIGIN}/assets/og/og-default.png`;
|
||||
const AUTHOR_NAME_RU = "Николай Мамадаев";
|
||||
const AUTHOR_NAME_EN = "Nikolai Mamadaev";
|
||||
const SITE_NAME_RU = "Николай Мамадаев — продуктовый дизайнер";
|
||||
const SITE_NAME_EN = "Nikolai Mamadaev — Product Designer";
|
||||
// Служебные страницы-каталоги: закрыты от индексации.
|
||||
const NOINDEX_PAGES = new Set([
|
||||
"cases/design-system",
|
||||
"cases/template-elements-demo",
|
||||
"cases/mini-projects-showcase",
|
||||
]);
|
||||
|
||||
// Страницы, у которых пока нет перевода: собираются только в дефолтную локаль,
|
||||
// чтобы /en/ не отдавал русскоязычный дубль и не попадал в sitemap.
|
||||
const DEFAULT_LOCALE_ONLY_PAGES = new Set(["ai-dev"]);
|
||||
|
||||
// Своя og-картинка для страницы основного AI Server-кейса.
|
||||
const PER_PAGE_OG_IMAGE = {
|
||||
"cases/ai-platform-primo-rpa": `${SITE_ORIGIN}/assets/og/og-primo-star.png`,
|
||||
};
|
||||
|
||||
const titleRe = /<title>([\s\S]*?)<\/title>/i;
|
||||
const descRe = /<meta\s+name="description"\s+content="([\s\S]*?)"\s*\/?>/i;
|
||||
|
||||
function decodeEntities(str) {
|
||||
return String(str)
|
||||
.replace(/ /g, " ")
|
||||
.replace(/&/g, "&")
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, "'")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">");
|
||||
}
|
||||
|
||||
function extractMeta(html) {
|
||||
const t = html.match(titleRe);
|
||||
const d = html.match(descRe);
|
||||
return {
|
||||
title: t ? decodeEntities(t[1]).trim() : "",
|
||||
description: d ? decodeEntities(d[1]).trim() : "",
|
||||
};
|
||||
}
|
||||
|
||||
/** Путь страницы на сайте (без ведущего слэша, с закрывающим). */
|
||||
function sitePath(relNorm, loc) {
|
||||
const base = relNorm
|
||||
.replace(/\.html$/, "")
|
||||
.replace(/(^|\/)index$/, "/");
|
||||
const norm = relNorm.startsWith("cases/")
|
||||
? `cases/${path.basename(relNorm, ".html")}/`
|
||||
: base === "resume" ? "resume/"
|
||||
: base === "cookie-policy" ? "cookie-policy/"
|
||||
: base === "ai-dev" ? "ai-dev/"
|
||||
: base === "" || base === "/" ? ""
|
||||
: `${base.replace(/\/$/, "")}/`;
|
||||
return loc === DEFAULT_LOCALE ? norm : `${loc}/${norm}`;
|
||||
}
|
||||
|
||||
/** Канонический URL страницы. */
|
||||
function canonicalFor(relNorm, loc) {
|
||||
const p = sitePath(relNorm, loc);
|
||||
return `${SITE_ORIGIN}/${p}`;
|
||||
}
|
||||
|
||||
/** Дата изменения страницы (mtime исходника) в формате YYYY-MM-DD. */
|
||||
function lastModFor(relNorm) {
|
||||
const src = path.join(srcPagesDir, relNorm);
|
||||
const enSrc = src.replace(/\.html$/, ".en.html");
|
||||
let mtime = 0;
|
||||
if (fs.existsSync(src)) mtime = fs.statSync(src).mtimeMs;
|
||||
if (fs.existsSync(enSrc)) mtime = Math.max(mtime, fs.statSync(enSrc).mtimeMs);
|
||||
return new Date(mtime || Date.now()).toISOString().slice(0, 10);
|
||||
}
|
||||
|
||||
/** JSON-LD для страницы: Person+WebSite на главной, CreativeWork на кейсах, BreadcrumbList везде. */
|
||||
function buildJsonLd(relNorm, loc, meta, canonical) {
|
||||
const isRu = loc === DEFAULT_LOCALE;
|
||||
const authorName = isRu ? AUTHOR_NAME_RU : AUTHOR_NAME_EN;
|
||||
const siteName = isRu ? SITE_NAME_RU : SITE_NAME_EN;
|
||||
const graph = [];
|
||||
const personId = `${SITE_ORIGIN}/#person`;
|
||||
const websiteId = `${SITE_ORIGIN}/#website`;
|
||||
|
||||
const person = {
|
||||
"@type": "Person",
|
||||
"@id": personId,
|
||||
name: authorName,
|
||||
url: SITE_ORIGIN,
|
||||
jobTitle: isRu ? "Продуктовый дизайнер" : "Product Designer",
|
||||
description: isRu
|
||||
? "Продуктовый дизайнер с опытом более 10 лет. Проектирую цифровые продукты, интерфейсы и дизайн-системы."
|
||||
: "Product designer with 10+ years of experience. Designing digital products, interfaces and design systems.",
|
||||
knowsAbout: isRu
|
||||
? ["UX/UI-дизайн", "продуктовый дизайн", "дизайн-системы", "прототипирование", "Figma", "no-code"]
|
||||
: ["UX/UI design", "product design", "design systems", "prototyping", "Figma", "no-code"],
|
||||
sameAs: ["https://t.me/mamadaevv", "https://vk.com/mamadaevv", "https://github.com/mamadaevv"],
|
||||
};
|
||||
graph.push(person);
|
||||
|
||||
graph.push({
|
||||
"@type": "WebSite",
|
||||
"@id": websiteId,
|
||||
url: `${SITE_ORIGIN}/`,
|
||||
name: siteName,
|
||||
inLanguage: loc,
|
||||
author: { "@id": personId },
|
||||
publisher: { "@id": personId },
|
||||
});
|
||||
|
||||
const isCase = relNorm.startsWith("cases/");
|
||||
const pageNode = {
|
||||
"@type": isCase ? "CreativeWork" : "WebPage",
|
||||
"@id": `${canonical}#page`,
|
||||
url: canonical,
|
||||
name: meta.title,
|
||||
description: meta.description,
|
||||
isPartOf: { "@id": websiteId },
|
||||
author: { "@id": personId },
|
||||
inLanguage: loc,
|
||||
};
|
||||
if (isCase && !NOINDEX_PAGES.has(relNorm.replace(/\.html$/, ""))) {
|
||||
pageNode.creator = { "@id": personId };
|
||||
pageNode.datePublished = lastModFor(relNorm);
|
||||
pageNode.about = isRu
|
||||
? "UX/UI-дизайн и проектирование интерфейсов"
|
||||
: "UX/UI design and interface design";
|
||||
}
|
||||
graph.push(pageNode);
|
||||
|
||||
// Хлебные крошки для вложенных страниц.
|
||||
const parts = sitePath(relNorm, loc).split("/").filter(Boolean);
|
||||
if (parts.length > 0) {
|
||||
const crumbs = [{ "@type": "ListItem", position: 1, name: siteName, item: `${SITE_ORIGIN}/` }];
|
||||
let acc = SITE_ORIGIN;
|
||||
let pos = 2;
|
||||
for (const part of parts) {
|
||||
if (part === loc) continue;
|
||||
acc += `/${part}`;
|
||||
crumbs.push({
|
||||
"@type": "ListItem",
|
||||
position: pos++,
|
||||
name: part === "cases" ? (isRu ? "Кейсы" : "Cases") : meta.title || part,
|
||||
item: `${acc}/`,
|
||||
});
|
||||
}
|
||||
if (crumbs.length > 1) {
|
||||
graph.push({
|
||||
"@type": "BreadcrumbList",
|
||||
"@id": `${canonical}#breadcrumbs`,
|
||||
itemListElement: crumbs,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
"@context": "https://schema.org",
|
||||
"@graph": graph,
|
||||
};
|
||||
}
|
||||
|
||||
/** Генерация sitemap.xml с hreflang-альтернатами. */
|
||||
function buildSitemap(pageFiles) {
|
||||
const urls = [];
|
||||
const localeSuffixRe = new RegExp(`\\.(${LOCALES.join("|")})\\.html$`, "i");
|
||||
for (const pagePath of pageFiles) {
|
||||
const relNorm = path.relative(srcPagesDir, pagePath).split(path.sep).join("/");
|
||||
const noSlug = relNorm.replace(/\.html$/, "");
|
||||
if (NOINDEX_PAGES.has(noSlug)) continue;
|
||||
const lastmod = lastModFor(relNorm);
|
||||
const locUrl = canonicalFor(relNorm, DEFAULT_LOCALE);
|
||||
const buildLocales = DEFAULT_LOCALE_ONLY_PAGES.has(noSlug) ? [DEFAULT_LOCALE] : LOCALES;
|
||||
const alternates = buildLocales.map(
|
||||
(loc) =>
|
||||
` <xhtml:link rel="alternate" hreflang="${loc}" href="${canonicalFor(relNorm, loc)}" />`
|
||||
);
|
||||
urls.push(
|
||||
` <url>\n <loc>${locUrl}</loc>\n <lastmod>${lastmod}</lastmod>\n` +
|
||||
` <xhtml:link rel="alternate" hreflang="x-default" href="${locUrl}" />\n` +
|
||||
alternates.join("\n") +
|
||||
`\n </url>`
|
||||
);
|
||||
}
|
||||
return (
|
||||
`<?xml version="1.0" encoding="UTF-8"?>\n` +
|
||||
`<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"\n` +
|
||||
` xmlns:xhtml="http://www.w3.org/1999/xhtml">\n` +
|
||||
urls.join("\n") +
|
||||
`\n</urlset>\n`
|
||||
);
|
||||
}
|
||||
|
||||
const includeRe = /\{\{\>\s*([a-z0-9-]+)\s*\}\}/gi;
|
||||
const varRe = /\{\{\s*([a-zA-Z0-9_]+)\s*\}\}/g;
|
||||
const i18nRe = /\{\{\s*i18n:([\w.-]+)\s*\}\}/g;
|
||||
|
||||
function readUtf8(filePath) {
|
||||
return fs.readFileSync(filePath, "utf8");
|
||||
}
|
||||
|
||||
function walkHtmlFiles(dir, acc = []) {
|
||||
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
const abs = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
walkHtmlFiles(abs, acc);
|
||||
continue;
|
||||
}
|
||||
if (entry.isFile() && entry.name.endsWith(".html")) {
|
||||
acc.push(abs);
|
||||
}
|
||||
}
|
||||
return acc;
|
||||
}
|
||||
|
||||
function loadPartials() {
|
||||
const partialMap = new Map();
|
||||
for (const filePath of walkHtmlFiles(srcPartialsDir)) {
|
||||
const name = path.basename(filePath, ".html");
|
||||
partialMap.set(name, readUtf8(filePath));
|
||||
}
|
||||
return partialMap;
|
||||
}
|
||||
|
||||
function loadI18n() {
|
||||
const map = {};
|
||||
for (const loc of LOCALES) {
|
||||
const file = path.join(srcI18nDir, `${loc}.json`);
|
||||
map[loc] = fs.existsSync(file) ? JSON.parse(readUtf8(file)) : {};
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
function resolveI18n(key, dict, fallback) {
|
||||
if (dict && Object.prototype.hasOwnProperty.call(dict, key)) return String(dict[key]);
|
||||
if (fallback && Object.prototype.hasOwnProperty.call(fallback, key)) return String(fallback[key]);
|
||||
return `{{i18n:${key}}}`;
|
||||
}
|
||||
|
||||
function renderTemplate(input, partials, vars, dict, fallback, loc, stack = []) {
|
||||
const withIncludes = input.replace(includeRe, (_, name) => {
|
||||
const key = String(name).trim();
|
||||
const partialKey = loc && loc !== DEFAULT_LOCALE && partials.has(`${key}.${loc}`) ? `${key}.${loc}` : key;
|
||||
if (!partials.has(partialKey)) {
|
||||
throw new Error(`Partial "${key}" not found`);
|
||||
}
|
||||
if (stack.includes(partialKey)) {
|
||||
throw new Error(`Circular partial include: ${[...stack, partialKey].join(" -> ")}`);
|
||||
}
|
||||
const partialContent = partials.get(partialKey);
|
||||
return renderTemplate(partialContent, partials, vars, dict, fallback, loc, [...stack, partialKey]);
|
||||
});
|
||||
|
||||
const withI18n = withIncludes.replace(i18nRe, (_, key) => resolveI18n(String(key).trim(), dict, fallback));
|
||||
|
||||
return withI18n.replace(varRe, (_, name) => {
|
||||
const key = String(name).trim();
|
||||
if (Object.prototype.hasOwnProperty.call(vars, key)) {
|
||||
return String(vars[key]);
|
||||
}
|
||||
return `{{${key}}}`;
|
||||
});
|
||||
}
|
||||
|
||||
function ensureDir(dirPath) {
|
||||
fs.mkdirSync(dirPath, { recursive: true });
|
||||
}
|
||||
|
||||
/** На странице кейса убираем из HTML карусели карточку со ссылкой на текущий файл (href уже с подставленным casesPrefix). */
|
||||
function stripCurrentCasePortfolioCarouselItem(html, omitHrefBasename) {
|
||||
if (!omitHrefBasename) return html;
|
||||
const slug = omitHrefBasename.replace(/\.html$/, "");
|
||||
const hrefDouble = `href="${slug}/"`;
|
||||
const hrefSingle = `href='${slug}/'`;
|
||||
const itemClass = "projects-carousel__item";
|
||||
let search = 0;
|
||||
while (true) {
|
||||
const i = html.indexOf("<article", search);
|
||||
if (i === -1) return html;
|
||||
const tagEnd = html.indexOf(">", i);
|
||||
if (tagEnd === -1) return html;
|
||||
const openTag = html.slice(i, tagEnd + 1);
|
||||
if (!openTag.includes(itemClass)) {
|
||||
search = i + 9;
|
||||
continue;
|
||||
}
|
||||
const contentStart = tagEnd + 1;
|
||||
const closeIdx = html.indexOf("</article>", contentStart);
|
||||
if (closeIdx === -1) return html;
|
||||
const afterArticle = closeIdx + "</article>".length;
|
||||
const block = html.slice(i, afterArticle);
|
||||
if (block.includes(hrefDouble) || block.includes(hrefSingle)) {
|
||||
return html.slice(0, i) + html.slice(afterArticle);
|
||||
}
|
||||
search = contentStart;
|
||||
}
|
||||
}
|
||||
|
||||
/** Базовые переменны для страницы в дефолтной локали (как было до i18n). */
|
||||
function baseVars(relNorm, depth) {
|
||||
if (relNorm.startsWith("cases/")) {
|
||||
return { assetPrefix: "../../", casesPrefix: "../" };
|
||||
}
|
||||
if (relNorm === "resume.html") {
|
||||
return { assetPrefix: "../", casesPrefix: "" };
|
||||
}
|
||||
if (relNorm === "cookie-policy.html") {
|
||||
return { assetPrefix: "../", casesPrefix: "" };
|
||||
}
|
||||
if (relNorm === "ai-dev.html") {
|
||||
return { assetPrefix: "../", casesPrefix: "../cases/" };
|
||||
}
|
||||
return {
|
||||
assetPrefix: depth === 0 ? "" : "../".repeat(depth),
|
||||
casesPrefix: depth === 0 ? "cases/" : "",
|
||||
};
|
||||
}
|
||||
|
||||
function build() {
|
||||
const partials = loadPartials();
|
||||
const i18n = loadI18n();
|
||||
const defaultDict = i18n[DEFAULT_LOCALE] || {};
|
||||
const localeSuffixRe = new RegExp(`\\.(${LOCALES.join("|")})\\.html$`, "i");
|
||||
const pageFiles = walkHtmlFiles(srcPagesDir).filter((p) => !localeSuffixRe.test(p));
|
||||
|
||||
for (const pagePath of pageFiles) {
|
||||
const rel = path.relative(srcPagesDir, pagePath);
|
||||
const relNorm = rel.split(path.sep).join("/");
|
||||
const pageDirRel = path.dirname(rel);
|
||||
const depth = pageDirRel === "." ? 0 : pageDirRel.split(path.sep).length;
|
||||
const isCase = relNorm.startsWith("cases/");
|
||||
const isResume = relNorm === "resume.html";
|
||||
const isCookiePolicy = relNorm === "cookie-policy.html";
|
||||
const isAiDev = relNorm === "ai-dev.html";
|
||||
const omitFile = isCase ? path.basename(pagePath) : null;
|
||||
const noSlug = relNorm.replace(/\.html$/, "");
|
||||
|
||||
for (const loc of LOCALES) {
|
||||
if (loc !== DEFAULT_LOCALE && DEFAULT_LOCALE_ONLY_PAGES.has(noSlug)) continue;
|
||||
const dict = i18n[loc] || {};
|
||||
const isDefault = loc === DEFAULT_LOCALE;
|
||||
const langPrefix = isDefault ? "" : `${loc}/`;
|
||||
|
||||
// Переведённый исходник страницы для локали, если есть.
|
||||
let sourcePath = pagePath;
|
||||
if (!isDefault) {
|
||||
const override = pagePath.replace(/\.html$/, `.${loc}.html`);
|
||||
if (fs.existsSync(override)) sourcePath = override;
|
||||
}
|
||||
const source = readUtf8(sourcePath);
|
||||
|
||||
// --- SEO-переменные для head-seo / json-ld ---
|
||||
const isNoindex = NOINDEX_PAGES.has(noSlug);
|
||||
const canonicalUrl = canonicalFor(relNorm, loc);
|
||||
const metaSource = extractMeta(source);
|
||||
const pageTitle = metaSource.title || (isDefault ? SITE_NAME_RU : SITE_NAME_EN);
|
||||
const pageDesc = metaSource.description || pageTitle;
|
||||
const isCasePage = relNorm.startsWith("cases/");
|
||||
|
||||
const vars = baseVars(relNorm, depth);
|
||||
vars.canonicalUrl = canonicalUrl;
|
||||
vars.canonicalRu = canonicalFor(relNorm, "ru");
|
||||
// Для ru-only страниц en-альтернаты не существует — отдаём ru,
|
||||
// иначе hreflang указывает на несуществующий /en/... URL.
|
||||
vars.canonicalEn = DEFAULT_LOCALE_ONLY_PAGES.has(noSlug)
|
||||
? vars.canonicalRu
|
||||
: canonicalFor(relNorm, "en");
|
||||
vars.authorName = isDefault ? AUTHOR_NAME_RU : AUTHOR_NAME_EN;
|
||||
vars.siteName = isDefault ? SITE_NAME_RU : SITE_NAME_EN;
|
||||
vars.metaRobots = isNoindex ? "noindex, nofollow" : "index, follow";
|
||||
vars.themeColor = "#0f1115";
|
||||
vars.ogType = isCasePage ? "article" : "website";
|
||||
vars.ogLocale = isDefault ? "ru_RU" : "en_US";
|
||||
vars.ogLocaleAlt = isDefault ? "en_US" : "ru_RU";
|
||||
vars.ogTitle = pageTitle;
|
||||
vars.ogDescription = pageDesc;
|
||||
vars.ogImage = PER_PAGE_OG_IMAGE[noSlug] || OG_IMAGE_DEFAULT;
|
||||
vars.ogImageAlt = pageTitle;
|
||||
vars.jsonLd = JSON.stringify(buildJsonLd(relNorm, loc, { title: pageTitle, description: pageDesc }, canonicalUrl), null, 2);
|
||||
|
||||
// Префикс до КОРНЯ ЛОКАЛИ (без en-сдвига) — для навигационных ссылок,
|
||||
// чтобы они вели в ту же локаль (/en/), а не в корень сайта (/).
|
||||
vars.pagePrefix = vars.assetPrefix;
|
||||
// Для не-дефолтной локали всё содержимое уходит на уровень глубже (public/<lang>/),
|
||||
// поэтому к assetPrefix добавляем один ../, а пустой casesPrefix — в ../cases/.
|
||||
if (!isDefault) {
|
||||
vars.assetPrefix = vars.assetPrefix === "" ? "../" : vars.assetPrefix + "../";
|
||||
if (vars.casesPrefix === "") vars.casesPrefix = "../cases/";
|
||||
}
|
||||
vars.lang = loc;
|
||||
|
||||
let outPath;
|
||||
if (isCase) {
|
||||
const slug = path.basename(pagePath, ".html");
|
||||
outPath = path.join(outDir, langPrefix, "cases", slug, "index.html");
|
||||
} else if (isResume) {
|
||||
outPath = path.join(outDir, langPrefix, "resume", "index.html");
|
||||
} else if (isCookiePolicy) {
|
||||
outPath = path.join(outDir, langPrefix, "cookie-policy", "index.html");
|
||||
} else if (isAiDev) {
|
||||
outPath = path.join(outDir, langPrefix, "ai-dev", "index.html");
|
||||
} else {
|
||||
outPath = path.join(outDir, langPrefix, rel);
|
||||
}
|
||||
|
||||
let rendered = renderTemplate(source, partials, vars, dict, defaultDict, loc);
|
||||
// Подменяем язык в <html lang="..."> под текущую локаль.
|
||||
rendered = rendered.replace(/<html\s+lang="ru"/, `<html lang="${loc}"`);
|
||||
if (isCase) {
|
||||
rendered = stripCurrentCasePortfolioCarouselItem(rendered, omitFile);
|
||||
}
|
||||
ensureDir(path.dirname(outPath));
|
||||
fs.writeFileSync(outPath, rendered, "utf8");
|
||||
}
|
||||
}
|
||||
|
||||
const sitemapXml = buildSitemap(pageFiles);
|
||||
fs.writeFileSync(path.join(outDir, "sitemap.xml"), sitemapXml, "utf8");
|
||||
|
||||
console.log(`Built ${pageFiles.length} pages × ${LOCALES.length} locales into public/`);
|
||||
console.log(`Generated sitemap.xml (${sitemapXml.split("<url>").length - 1} urls)`);
|
||||
}
|
||||
|
||||
build();
|
||||
@@ -0,0 +1,32 @@
|
||||
// Генерация серых заглушек (16:10) для страницы ai-dev.
|
||||
// Заглушки — намеренно нейтральный серый, без текста и градиентов.
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import sharp from "sharp";
|
||||
|
||||
const outDir = path.join(process.cwd(), "public", "assets", "ai-dev");
|
||||
fs.mkdirSync(outDir, { recursive: true });
|
||||
|
||||
const W = 640;
|
||||
const H = 400;
|
||||
|
||||
// Разные оттенки серого, чтобы карточки не выглядели одинаковыми при загрузке.
|
||||
const shades = [
|
||||
{ fill: "#2a2d31", label: "01" },
|
||||
{ fill: "#31353a", label: "02" },
|
||||
{ fill: "#25282c", label: "03" },
|
||||
];
|
||||
|
||||
const tiles = await Promise.all(
|
||||
shades.map(async ({ fill, label }) => {
|
||||
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${W}" height="${H}">
|
||||
<rect width="${W}" height="${H}" fill="${fill}" />
|
||||
</svg>`;
|
||||
const file = path.join(outDir, `placeholder-${label}.webp`);
|
||||
await sharp(Buffer.from(svg)).webp({ quality: 72 }).toFile(file);
|
||||
return file;
|
||||
}),
|
||||
);
|
||||
|
||||
console.log(`Generated ${tiles.length} placeholders in ${outDir}`);
|
||||
for (const f of tiles) console.log(" ", path.basename(f), fs.statSync(f).size, "bytes");
|
||||
@@ -0,0 +1,351 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru" data-theme="dark" data-radius="medium">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
||||
<meta
|
||||
name="description"
|
||||
content="Николай Мамадаев, AI-разработчик. Делаю целые продукты: Telegram-боты, MCP-серверы, плагины для Figma и Obsidian, desktop-утилиты."
|
||||
/>
|
||||
<title>AI-разработка · Николай Мамадаев</title>
|
||||
{{> head-links}}
|
||||
{{> head-plyr}}
|
||||
<style>
|
||||
.ai-page {
|
||||
max-width: 52rem;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.ai-page h2 {
|
||||
margin: 2.25rem 0 0.85rem;
|
||||
font-size: clamp(1.15rem, 2vw, 1.35rem);
|
||||
font-weight: 700;
|
||||
color: var(--text);
|
||||
}
|
||||
.ai-page .section__prose {
|
||||
margin-bottom: 0.75rem;
|
||||
color: var(--text);
|
||||
}
|
||||
.ai-page a:not(.btn) {
|
||||
color: var(--accent);
|
||||
}
|
||||
/* Сетка карточек проектов: 2 колонки, схлопывается в одну на мобильном */
|
||||
.ai-projects {
|
||||
list-style: none;
|
||||
margin: 1.25rem 0 0;
|
||||
padding: 0;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
.ai-projects > li {
|
||||
margin: 0;
|
||||
}
|
||||
.ai-projects__media {
|
||||
border-radius: var(--radius-md);
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--line);
|
||||
margin-bottom: 0.7rem;
|
||||
background: var(--surface);
|
||||
}
|
||||
.ai-projects__media img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
aspect-ratio: 16 / 10;
|
||||
object-fit: cover;
|
||||
}
|
||||
.ai-projects__head {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
.ai-projects__title {
|
||||
margin: 0;
|
||||
font-size: 1.05rem;
|
||||
font-weight: 600;
|
||||
color: var(--text);
|
||||
}
|
||||
.ai-projects__stack {
|
||||
margin: 0.3rem 0 0;
|
||||
font-family: var(--font-mono, ui-monospace, monospace);
|
||||
font-size: 0.78rem;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
.ai-projects__desc {
|
||||
margin: 0.45rem 0 0;
|
||||
font-size: 0.95rem;
|
||||
color: var(--text);
|
||||
}
|
||||
.ai-projects__links {
|
||||
list-style: none;
|
||||
margin: 0.6rem 0 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.4rem 0.9rem;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
/* Навыки — компактные чипы переиспользуют стили тегов карточек активности */
|
||||
.ai-stack-list {
|
||||
list-style: none;
|
||||
margin: 1rem 0 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.45rem;
|
||||
}
|
||||
.ai-stack-list li {
|
||||
margin: 0;
|
||||
padding: 0.3rem 0.7rem;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 999px;
|
||||
font-family: var(--font-mono, ui-monospace, monospace);
|
||||
font-size: 0.8rem;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
{{> header-case}}
|
||||
{{> fab-stack}}
|
||||
<main id="case-main" class="section section--tight">
|
||||
<div class="section__inner ai-page">
|
||||
<p class="hero__eyebrow">AI-разработка · идея, код, деплой</p>
|
||||
<h1 class="case-page__h1">Делаю целые продукты</h1>
|
||||
<p class="case-page__sub">Telegram-боты, MCP-серверы, плагины и утилиты. Схема, код, деплой</p>
|
||||
|
||||
<p class="section__prose">
|
||||
Больше десяти лет я проектировал интерфейсы. Теперь довожу идеи до работающего
|
||||
кода: бэкенд, база данных, деплой. Инфраструктура тоже моя часть работы.
|
||||
В большинстве проектов код пишет ИИ-агент. Я задаю архитектуру, проверяю
|
||||
результат и отвечаю за то, что продукт вообще работает.
|
||||
</p>
|
||||
|
||||
<h2 id="roles">Что я делаю</h2>
|
||||
<ul class="section__prose ai-stack-list">
|
||||
<li>Продуктовая логика и API</li>
|
||||
<li>Telegram-боты на aiogram 3</li>
|
||||
<li>MCP-серверы</li>
|
||||
<li>Плагины Figma и Obsidian</li>
|
||||
<li>Автоматизация и агентные пайплайны</li>
|
||||
</ul>
|
||||
|
||||
<h2 id="projects">Проекты</h2>
|
||||
<ol class="ai-projects">
|
||||
<li>
|
||||
<div class="ai-projects__media">
|
||||
<img
|
||||
src="{{assetPrefix}}assets/ai-dev/placeholder-01.webp"
|
||||
alt=""
|
||||
width="640"
|
||||
height="400"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
</div>
|
||||
<div class="ai-projects__head">
|
||||
<h3 class="ai-projects__title">Personal Assistant</h3>
|
||||
</div>
|
||||
<p class="ai-projects__stack">Python · aiogram 3 · PostgreSQL</p>
|
||||
<p class="ai-projects__desc">
|
||||
Личный ассистент в Telegram: напоминания, планирование дел и работа с LLM
|
||||
в одном диалоге.
|
||||
</p>
|
||||
<ul class="ai-projects__links">
|
||||
<li><a href="{{casesPrefix}}ai-platform-primo-rpa/" target="_blank" rel="noopener noreferrer">Кейс</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<div class="ai-projects__media">
|
||||
<img
|
||||
src="{{assetPrefix}}assets/ai-dev/placeholder-02.webp"
|
||||
alt=""
|
||||
width="640"
|
||||
height="400"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
</div>
|
||||
<div class="ai-projects__head">
|
||||
<h3 class="ai-projects__title">Image Gen Bot</h3>
|
||||
</div>
|
||||
<p class="ai-projects__stack">Python · aiogram 3 · Redis · PostgreSQL</p>
|
||||
<p class="ai-projects__desc">
|
||||
Бот для генерации изображений: баланс, реферальная программа, админ-логи
|
||||
в отдельные топики супергруппы.
|
||||
</p>
|
||||
<ul class="ai-projects__links">
|
||||
<li><a href="{{casesPrefix}}ai-platform-primo-rpa/" target="_blank" rel="noopener noreferrer">Кейс</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<div class="ai-projects__media">
|
||||
<img
|
||||
src="{{assetPrefix}}assets/ai-dev/placeholder-03.webp"
|
||||
alt=""
|
||||
width="640"
|
||||
height="400"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
</div>
|
||||
<div class="ai-projects__head">
|
||||
<h3 class="ai-projects__title">MCP Image Server</h3>
|
||||
</div>
|
||||
<p class="ai-projects__stack">JavaScript · MCP · OpenRouter</p>
|
||||
<p class="ai-projects__desc">
|
||||
Собственный MCP-сервер для генерации картинок. Подключается к агентам
|
||||
как инструмент.
|
||||
</p>
|
||||
<ul class="ai-projects__links">
|
||||
<li><a href="http://x1qnvk08zec2u5cx6yh5iexh.155.212.186.39.sslip.io/admin/mcp-img-gen" target="_blank" rel="noopener noreferrer">Код</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<div class="ai-projects__media">
|
||||
<img
|
||||
src="{{assetPrefix}}assets/ai-dev/placeholder-01.webp"
|
||||
alt=""
|
||||
width="640"
|
||||
height="400"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
</div>
|
||||
<div class="ai-projects__head">
|
||||
<h3 class="ai-projects__title">AI assistant</h3>
|
||||
</div>
|
||||
<p class="ai-projects__stack">TypeScript · Figma Plugin API</p>
|
||||
<p class="ai-projects__desc">
|
||||
Плагин для Figma, который исполняет скрипты прямо в редакторе.
|
||||
ИИ-ассистент для любого тарифа.
|
||||
</p>
|
||||
<ul class="ai-projects__links">
|
||||
<li><a href="{{casesPrefix}}ai-assistant-figma/" target="_blank" rel="noopener noreferrer">Кейс</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<div class="ai-projects__media">
|
||||
<img
|
||||
src="{{assetPrefix}}assets/ai-dev/placeholder-02.webp"
|
||||
alt=""
|
||||
width="640"
|
||||
height="400"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
</div>
|
||||
<div class="ai-projects__head">
|
||||
<h3 class="ai-projects__title">Card Columns</h3>
|
||||
</div>
|
||||
<p class="ai-projects__stack">TypeScript · Obsidian API</p>
|
||||
<p class="ai-projects__desc">
|
||||
Вью для заметок в стиле канбан-доски: группировка по колонкам с фильтрами
|
||||
по тегам.
|
||||
</p>
|
||||
<ul class="ai-projects__links">
|
||||
<li><a href="{{casesPrefix}}card-columns-obsidian/" target="_blank" rel="noopener noreferrer">Кейс</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<div class="ai-projects__media">
|
||||
<img
|
||||
src="{{assetPrefix}}assets/ai-dev/placeholder-03.webp"
|
||||
alt=""
|
||||
width="640"
|
||||
height="400"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
</div>
|
||||
<div class="ai-projects__head">
|
||||
<h3 class="ai-projects__title">Hermes Desktop Plugins</h3>
|
||||
</div>
|
||||
<p class="ai-projects__stack">JavaScript · TypeScript · Electron</p>
|
||||
<p class="ai-projects__desc">
|
||||
Плагины для рабочего стола Hermes: чип баланса API, звук завершения задачи
|
||||
и контроль трат.
|
||||
</p>
|
||||
<ul class="ai-projects__links">
|
||||
<li><a href="http://x1qnvk08zec2u5cx6yh5iexh.155.212.186.39.sslip.io/admin/unorouter-balance" target="_blank" rel="noopener noreferrer">Код</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<div class="ai-projects__media">
|
||||
<img
|
||||
src="{{assetPrefix}}assets/ai-dev/placeholder-01.webp"
|
||||
alt=""
|
||||
width="640"
|
||||
height="400"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
</div>
|
||||
<div class="ai-projects__head">
|
||||
<h3 class="ai-projects__title">Social Post Writer</h3>
|
||||
</div>
|
||||
<p class="ai-projects__stack">Python · aiogram 3 · SQLite</p>
|
||||
<p class="ai-projects__desc">
|
||||
Бот, который превращает тезисы в готовый пост под нужную площадку и тон,
|
||||
учитывая голос автора.
|
||||
</p>
|
||||
<ul class="ai-projects__links">
|
||||
<li><a href="http://x1qnvk08zec2u5cx6yh5iexh.155.212.186.39.sslip.io/admin/social-post-writer-bot" rel="noopener noreferrer">Код</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<div class="ai-projects__media">
|
||||
<img
|
||||
src="{{assetPrefix}}assets/ai-dev/placeholder-02.webp"
|
||||
alt=""
|
||||
width="640"
|
||||
height="400"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
</div>
|
||||
<div class="ai-projects__head">
|
||||
<h3 class="ai-projects__title">AI Design Assistant</h3>
|
||||
</div>
|
||||
<p class="ai-projects__stack">HTML · CSS · GLSL · WebGL</p>
|
||||
<p class="ai-projects__desc">
|
||||
Этот сайт целиком: структура, дизайн-система, шейдеры, CI/CD.
|
||||
Весь цикл создания вместе с ИИ-агентом.
|
||||
</p>
|
||||
<ul class="ai-projects__links">
|
||||
<li><a href="{{casesPrefix}}site-portfolio/" target="_blank" rel="noopener noreferrer">Кейс</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ol>
|
||||
|
||||
<h2 id="stack">Стек</h2>
|
||||
<ul class="section__prose ai-stack-list">
|
||||
<li>Python</li>
|
||||
<li>TypeScript</li>
|
||||
<li>JavaScript</li>
|
||||
<li>Go</li>
|
||||
<li>HTML/CSS</li>
|
||||
<li>GLSL</li>
|
||||
<li>aiogram 3</li>
|
||||
<li>FastAPI</li>
|
||||
<li>PostgreSQL</li>
|
||||
<li>Redis</li>
|
||||
<li>SQLite</li>
|
||||
<li>Docker</li>
|
||||
<li>Coolify</li>
|
||||
<li>Gitea</li>
|
||||
<li>MCP</li>
|
||||
</ul>
|
||||
|
||||
<h2 id="links">Где я это выкладываю</h2>
|
||||
<p class="section__prose">
|
||||
Код открыт: <a href="https://github.com/mamadaevv" target="_blank" rel="noopener noreferrer">GitHub</a>
|
||||
и <a href="http://x1qnvk08zec2u5cx6yh5iexh.155.212.186.39.sslip.io/admin" target="_blank" rel="noopener noreferrer">Gitea</a>.
|
||||
Сервисы, которые работают прямо сейчас, живут на
|
||||
<a href="https://secrets-4-me.dynv6.net" target="_blank" rel="noopener noreferrer">secrets-4-me</a>.
|
||||
</p>
|
||||
</div>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,191 +1,207 @@
|
||||
<section id="activity" class="section section--surface" aria-labelledby="mini-projects-title" data-reveal>
|
||||
<div class="section__inner">
|
||||
<h2 id="mini-projects-title" class="section__title">Активность</h2>
|
||||
<div
|
||||
class="skills-intro skills-intro--chat section__prose"
|
||||
data-skills-intro
|
||||
role="region"
|
||||
aria-labelledby="mini-projects-title"
|
||||
>
|
||||
<div class="skills-msg" data-skills-msg>
|
||||
<div class="skills-msg__body">
|
||||
<p class="skills-msg__author">Николай М:</p>
|
||||
<p class="skills-msg__text">
|
||||
Люблю делать разные мини-проекты или делиться чем-то полезным с миром.
|
||||
</p>
|
||||
<time class="skills-msg__time" datetime="" data-skills-msg-time></time>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ol id="mini-projects-list" class="mini-projects-grid" data-mini-projects-list>
|
||||
<li class="mini-projects-grid__item" data-mp-cells="2">
|
||||
<a
|
||||
class="mp-card mp-card--link mp-card--compact"
|
||||
href="cases/ai-assistant-figma/"
|
||||
data-glass="false"
|
||||
aria-label="AI assistant — открыть кейс"
|
||||
>
|
||||
<div class="mp-card__body">
|
||||
<h3 class="mp-card__title">AI assistant</h3>
|
||||
<ul class="mp-card__tags" aria-label="Тип проекта">
|
||||
<li class="mp-card__tag">Плагин для Figma</li>
|
||||
</ul>
|
||||
<p class="mp-card__desc">ИИ помощник для любого тарифа редактора</p>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
<li class="mini-projects-grid__item" data-mp-cells="2">
|
||||
<a
|
||||
class="mp-card mp-card--link mp-card--compact"
|
||||
href="cases/card-columns-obsidian/"
|
||||
data-glass="false"
|
||||
aria-label="Card Columns — открыть кейс"
|
||||
>
|
||||
<div class="mp-card__body">
|
||||
<h3 class="mp-card__title">Card Columns</h3>
|
||||
<ul class="mp-card__tags" aria-label="Тип проекта">
|
||||
<li class="mp-card__tag">Плагин для Obsidian</li>
|
||||
</ul>
|
||||
<p class="mp-card__desc">Отображение карточек с заметками в стиле канбан доски</p>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
<li class="mini-projects-grid__item" data-mp-cells="2">
|
||||
<a
|
||||
class="mp-card mp-card--link mp-card--compact"
|
||||
href="cases/site-portfolio/"
|
||||
data-glass="false"
|
||||
aria-label="Этот сайт — открыть кейс"
|
||||
>
|
||||
<div class="mp-card__body">
|
||||
<h3 class="mp-card__title">Этот сайт</h3>
|
||||
<ul class="mp-card__tags" aria-label="Тип проекта">
|
||||
<li class="mp-card__tag">Разработка с AI IDE</li>
|
||||
</ul>
|
||||
<p class="mp-card__desc">Весь цикл создания сайта при помощи ИИ</p>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
<li class="mini-projects-grid__item" data-mp-cells="2">
|
||||
<a
|
||||
class="mp-card mp-card--link mp-card--horizontal mp-card--hor-image"
|
||||
href="https://www.figma.com/@mamadaevv"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label="Figma Community — открыть профиль"
|
||||
>
|
||||
<div class="mp-card__media" aria-hidden="true">
|
||||
<img
|
||||
class="mp-card__image"
|
||||
src="{{assetPrefix}}assets/misc/figma_logo.webp"
|
||||
alt=""
|
||||
width="800"
|
||||
height="800"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
</div>
|
||||
<div class="mp-card__body">
|
||||
<h3 class="mp-card__title">
|
||||
Figma Community
|
||||
<span class="mp-card__ext" aria-hidden="true"></span>
|
||||
</h3>
|
||||
<p class="mp-card__desc">
|
||||
Выкладываю полезные файлы и плагины для сообщества
|
||||
</p>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
<li class="mini-projects-grid__item" data-mp-cells="2">
|
||||
<a
|
||||
class="mp-card mp-card--link mp-card--horizontal mp-card--hor-image"
|
||||
href="https://dribbble.com/mamadaevv"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label="Dribbble — открыть профиль"
|
||||
>
|
||||
<div class="mp-card__media" aria-hidden="true">
|
||||
<img
|
||||
class="mp-card__image"
|
||||
src="{{assetPrefix}}assets/misc/dribbble_logo.webp"
|
||||
alt=""
|
||||
width="1024"
|
||||
height="1024"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
</div>
|
||||
<div class="mp-card__body">
|
||||
<h3 class="mp-card__title">
|
||||
Dribbble
|
||||
<span class="mp-card__ext" aria-hidden="true"></span>
|
||||
</h3>
|
||||
<p class="mp-card__desc">
|
||||
Красивые картинки с элементами интерфейсов
|
||||
</p>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
<li class="mini-projects-grid__item" data-mp-cells="2">
|
||||
<a
|
||||
class="mp-card mp-card--link mp-card--horizontal mp-card--hor-image"
|
||||
href="https://t.me/designnotpizza"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label="Дизайн не пицца — открыть канал в Telegram"
|
||||
>
|
||||
<div class="mp-card__media" aria-hidden="true">
|
||||
<img
|
||||
class="mp-card__image"
|
||||
src="{{assetPrefix}}assets/misc/tg-channel-logo-design_not_pizza.webp"
|
||||
alt=""
|
||||
width="640"
|
||||
height="640"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
</div>
|
||||
<div class="mp-card__body">
|
||||
<h3 class="mp-card__title">
|
||||
Дизайн не пицца
|
||||
<span class="mp-card__ext" aria-hidden="true"></span>
|
||||
</h3>
|
||||
<p class="mp-card__desc">
|
||||
Мой канал про дизайн. Посты на тему продуктового дизайна и полезные ссылки
|
||||
</p>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
<li class="mini-projects-grid__item" data-mp-cells="2">
|
||||
<a
|
||||
class="mp-card mp-card--link mp-card--horizontal mp-card--hor-image"
|
||||
href="https://github.com/mamadaevv"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label="GitHub — открыть профиль"
|
||||
>
|
||||
<div class="mp-card__media" aria-hidden="true">
|
||||
<img
|
||||
class="mp-card__image"
|
||||
src="{{assetPrefix}}assets/misc/github_logo.webp"
|
||||
alt=""
|
||||
width="800"
|
||||
height="800"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
</div>
|
||||
<div class="mp-card__body">
|
||||
<h3 class="mp-card__title">
|
||||
GitHub
|
||||
<span class="mp-card__ext" aria-hidden="true"></span>
|
||||
</h3>
|
||||
<p class="mp-card__desc">
|
||||
Мои проекты и эксперименты с кодом
|
||||
</p>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
</section>
|
||||
<section id="activity" class="section section--surface" aria-labelledby="mini-projects-title" data-reveal>
|
||||
<div class="section__inner">
|
||||
<h2 id="mini-projects-title" class="section__title">Активность</h2>
|
||||
<div
|
||||
class="skills-intro skills-intro--chat section__prose"
|
||||
data-skills-intro
|
||||
role="region"
|
||||
aria-labelledby="mini-projects-title"
|
||||
>
|
||||
<div class="skills-msg" data-skills-msg>
|
||||
<div class="skills-msg__body">
|
||||
<p class="skills-msg__author">Николай М:</p>
|
||||
<p class="skills-msg__text">
|
||||
Люблю делать разные мини-проекты или делиться чем-то полезным с миром.
|
||||
</p>
|
||||
<time class="skills-msg__time" datetime="" data-skills-msg-time></time>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ol id="mini-projects-list" class="mini-projects-grid" data-mini-projects-list>
|
||||
<li class="mini-projects-grid__item" data-mp-cells="2">
|
||||
<a
|
||||
class="mp-card mp-card--link mp-card--compact"
|
||||
href="ai-dev/"
|
||||
data-glass="false"
|
||||
aria-label="AI-разработка — открыть страницу"
|
||||
>
|
||||
<div class="mp-card__body">
|
||||
<h3 class="mp-card__title">AI-разработка</h3>
|
||||
<ul class="mp-card__tags" aria-label="Тип проекта">
|
||||
<li class="mp-card__tag">Боты · MCP · плагины</li>
|
||||
</ul>
|
||||
<p class="mp-card__desc">Не только дизайн, а целые продукты: от API до деплоя</p>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
<li class="mini-projects-grid__item" data-mp-cells="2">
|
||||
<a
|
||||
class="mp-card mp-card--link mp-card--compact"
|
||||
href="cases/ai-assistant-figma/"
|
||||
data-glass="false"
|
||||
aria-label="AI assistant — открыть кейс"
|
||||
>
|
||||
<div class="mp-card__body">
|
||||
<h3 class="mp-card__title">AI assistant</h3>
|
||||
<ul class="mp-card__tags" aria-label="Тип проекта">
|
||||
<li class="mp-card__tag">Плагин для Figma</li>
|
||||
</ul>
|
||||
<p class="mp-card__desc">ИИ помощник для любого тарифа редактора</p>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
<li class="mini-projects-grid__item" data-mp-cells="2">
|
||||
<a
|
||||
class="mp-card mp-card--link mp-card--compact"
|
||||
href="cases/card-columns-obsidian/"
|
||||
data-glass="false"
|
||||
aria-label="Card Columns — открыть кейс"
|
||||
>
|
||||
<div class="mp-card__body">
|
||||
<h3 class="mp-card__title">Card Columns</h3>
|
||||
<ul class="mp-card__tags" aria-label="Тип проекта">
|
||||
<li class="mp-card__tag">Плагин для Obsidian</li>
|
||||
</ul>
|
||||
<p class="mp-card__desc">Отображение карточек с заметками в стиле канбан доски</p>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
<li class="mini-projects-grid__item" data-mp-cells="2">
|
||||
<a
|
||||
class="mp-card mp-card--link mp-card--compact"
|
||||
href="cases/site-portfolio/"
|
||||
data-glass="false"
|
||||
aria-label="Этот сайт — открыть кейс"
|
||||
>
|
||||
<div class="mp-card__body">
|
||||
<h3 class="mp-card__title">Этот сайт</h3>
|
||||
<ul class="mp-card__tags" aria-label="Тип проекта">
|
||||
<li class="mp-card__tag">Разработка с AI IDE</li>
|
||||
</ul>
|
||||
<p class="mp-card__desc">Весь цикл создания сайта при помощи ИИ</p>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
<li class="mini-projects-grid__item" data-mp-cells="2">
|
||||
<a
|
||||
class="mp-card mp-card--link mp-card--horizontal mp-card--hor-image"
|
||||
href="https://www.figma.com/@mamadaevv"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label="Figma Community — открыть профиль"
|
||||
>
|
||||
<div class="mp-card__media" aria-hidden="true">
|
||||
<img
|
||||
class="mp-card__image"
|
||||
src="{{assetPrefix}}assets/misc/figma_logo.webp"
|
||||
alt=""
|
||||
width="800"
|
||||
height="800"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
</div>
|
||||
<div class="mp-card__body">
|
||||
<h3 class="mp-card__title">
|
||||
Figma Community
|
||||
<span class="mp-card__ext" aria-hidden="true"></span>
|
||||
</h3>
|
||||
<p class="mp-card__desc">
|
||||
Выкладываю полезные файлы и плагины для сообщества
|
||||
</p>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
<li class="mini-projects-grid__item" data-mp-cells="2">
|
||||
<a
|
||||
class="mp-card mp-card--link mp-card--horizontal mp-card--hor-image"
|
||||
href="https://dribbble.com/mamadaevv"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label="Dribbble — открыть профиль"
|
||||
>
|
||||
<div class="mp-card__media" aria-hidden="true">
|
||||
<img
|
||||
class="mp-card__image"
|
||||
src="{{assetPrefix}}assets/misc/dribbble_logo.webp"
|
||||
alt=""
|
||||
width="1024"
|
||||
height="1024"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
</div>
|
||||
<div class="mp-card__body">
|
||||
<h3 class="mp-card__title">
|
||||
Dribbble
|
||||
<span class="mp-card__ext" aria-hidden="true"></span>
|
||||
</h3>
|
||||
<p class="mp-card__desc">
|
||||
Красивые картинки с элементами интерфейсов
|
||||
</p>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
<li class="mini-projects-grid__item" data-mp-cells="2">
|
||||
<a
|
||||
class="mp-card mp-card--link mp-card--horizontal mp-card--hor-image"
|
||||
href="https://t.me/designnotpizza"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label="Дизайн не пицца — открыть канал в Telegram"
|
||||
>
|
||||
<div class="mp-card__media" aria-hidden="true">
|
||||
<img
|
||||
class="mp-card__image"
|
||||
src="{{assetPrefix}}assets/misc/tg-channel-logo-design_not_pizza.webp"
|
||||
alt=""
|
||||
width="640"
|
||||
height="640"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
</div>
|
||||
<div class="mp-card__body">
|
||||
<h3 class="mp-card__title">
|
||||
Дизайн не пицца
|
||||
<span class="mp-card__ext" aria-hidden="true"></span>
|
||||
</h3>
|
||||
<p class="mp-card__desc">
|
||||
Мой канал про дизайн. Посты на тему продуктового дизайна и полезные ссылки
|
||||
</p>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
<li class="mini-projects-grid__item" data-mp-cells="2">
|
||||
<a
|
||||
class="mp-card mp-card--link mp-card--horizontal mp-card--hor-image"
|
||||
href="https://github.com/mamadaevv"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label="GitHub — открыть профиль"
|
||||
>
|
||||
<div class="mp-card__media" aria-hidden="true">
|
||||
<img
|
||||
class="mp-card__image"
|
||||
src="{{assetPrefix}}assets/misc/github_logo.webp"
|
||||
alt=""
|
||||
width="800"
|
||||
height="800"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
</div>
|
||||
<div class="mp-card__body">
|
||||
<h3 class="mp-card__title">
|
||||
GitHub
|
||||
<span class="mp-card__ext" aria-hidden="true"></span>
|
||||
</h3>
|
||||
<p class="mp-card__desc">
|
||||
Мои проекты и эксперименты с кодом
|
||||
</p>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
</section>
|
||||
Reference in new issue
Block a user