From d22a054ccacca6d521c77e9e75aa89e3c9b2df6b Mon Sep 17 00:00:00 2001 From: Dissenter <118754629+dissenter-web@users.noreply.github.com> Date: Thu, 9 Apr 2026 09:29:56 +0300 Subject: [PATCH 1/9] Fix static base URL normalization in frontend scripts --- app/main.py | 19 ++++++++++++++----- static/js/layout.js | 6 +++++- static/js/project.js | 6 +++++- static/js/theme.js | 5 ++++- templates/base.html | 22 +++++++++++----------- templates/partials/header.html | 2 +- templates/project.html | 2 +- 7 files changed, 41 insertions(+), 21 deletions(-) diff --git a/app/main.py b/app/main.py index 97ebc50..9b45f75 100644 --- a/app/main.py +++ b/app/main.py @@ -1,3 +1,4 @@ +import os from fastapi import FastAPI, Request from fastapi.staticfiles import StaticFiles from fastapi.templating import Jinja2Templates @@ -7,6 +8,14 @@ app.mount('/static', StaticFiles(directory='static'), name='static') +ASSET_VERSION = os.getenv('ASSET_VERSION', '1') + + +def render_template(request: Request, template_name: str, **context): + page_context = {'request': request, 'asset_version': ASSET_VERSION} + page_context.update(context) + return templates.TemplateResponse(template_name, page_context) + @app.get('/healthz', name='healthz') def healthz(): @@ -15,24 +24,24 @@ def healthz(): @app.get('/', name='index') def index(request: Request): - return templates.TemplateResponse('index.html', {'request': request}) + return render_template(request, 'index.html') @app.get('/skills', name='skills') def skills(request: Request): - return templates.TemplateResponse('skills.html', {'request': request}) + return render_template(request, 'skills.html') @app.get('/projects', name='projects') def projects(request: Request): - return templates.TemplateResponse('projects.html', {'request': request}) + return render_template(request, 'projects.html') @app.get('/projects/{project_id}', name='project_details') def project_details(request: Request, project_id: str): - return templates.TemplateResponse('project.html', {'request': request, 'project_id': project_id}) + return render_template(request, 'project.html', project_id=project_id) @app.get('/about', name='about') def about(request: Request): - return templates.TemplateResponse('about.html', {'request': request}) + return render_template(request, 'about.html') diff --git a/static/js/layout.js b/static/js/layout.js index ae991d4..1d6838b 100644 --- a/static/js/layout.js +++ b/static/js/layout.js @@ -1,7 +1,11 @@ +const rawStaticBase = document.documentElement.dataset.staticBase || '/static'; +const staticBase = rawStaticBase.endsWith('/') ? rawStaticBase : `${rawStaticBase}/`; +const contentVersion = document.documentElement.dataset.assetVersion || '1'; + async function loadContent() { const lang = localStorage.getItem('lang') || 'ru'; - const response = await fetch(`/static/content/${lang}.json?v=1`); + const response = await fetch(`${staticBase}content/${lang}.json?v=${contentVersion}`); const data = await response.json(); document.querySelectorAll('[data-key]').forEach(el => { diff --git a/static/js/project.js b/static/js/project.js index ed7051e..7da3759 100644 --- a/static/js/project.js +++ b/static/js/project.js @@ -3,8 +3,12 @@ const id = pathParts[pathParts.length - 1]; if (!id) return; + const rawStaticBase = document.documentElement.dataset.staticBase || '/static'; + const staticBase = rawStaticBase.endsWith('/') ? rawStaticBase : `${rawStaticBase}/`; + const contentVersion = document.documentElement.dataset.assetVersion || '1'; + const lang = localStorage.getItem('lang') || 'ru'; - const data = await fetch(`/static/content/${lang}.json`).then(r => r.json()); + const data = await fetch(`${staticBase}content/${lang}.json?v=${contentVersion}`).then(r => r.json()); const project = data.projects.find(p => p.id === id); if (!project) return; diff --git a/static/js/theme.js b/static/js/theme.js index 8e6e67b..809c5cc 100644 --- a/static/js/theme.js +++ b/static/js/theme.js @@ -1,5 +1,8 @@ const THEME_KEY = 'theme'; const THEME_LINK_ID = 'theme-style'; +const rawStaticBase = document.documentElement.dataset.staticBase || '/static'; +const staticBase = rawStaticBase.endsWith('/') ? rawStaticBase : `${rawStaticBase}/`; +const assetVersion = document.documentElement.dataset.assetVersion || '1'; function applyTheme(theme) { const link = document.getElementById(THEME_LINK_ID); @@ -10,7 +13,7 @@ function applyTheme(theme) { } // cache-busting, иначе Firefox делает вид что ничего не менялось - link.href = `/static/css/${theme}.css?v=${Date.now()}`; + link.href = `${staticBase}css/${theme}.css?v=${assetVersion}-${Date.now()}`; updateThemeIcon(theme); } diff --git a/templates/base.html b/templates/base.html index f140047..ff84561 100644 --- a/templates/base.html +++ b/templates/base.html @@ -1,26 +1,26 @@ - + {% block title %}Personal Site{% endblock %} - - + + {% include "partials/header.html" %}
@@ -30,7 +30,7 @@ {% include "partials/footer.html" %} - - + + diff --git a/templates/partials/header.html b/templates/partials/header.html index 9f1c734..7ae4034 100644 --- a/templates/partials/header.html +++ b/templates/partials/header.html @@ -1,6 +1,6 @@
- + {% endblock %} From 0de0f54b86623265798c1b1f20e6a5cca3a1c088 Mon Sep 17 00:00:00 2001 From: Dissenter <118754629+dissenter-web@users.noreply.github.com> Date: Thu, 9 Apr 2026 09:43:10 +0300 Subject: [PATCH 2/9] Fix JS global const collisions and restore theme toggle --- static/js/layout.js | 232 +++++++++++++++++++++---------------------- static/js/project.js | 20 ++-- static/js/theme.js | 90 +++++++++-------- 3 files changed, 176 insertions(+), 166 deletions(-) diff --git a/static/js/layout.js b/static/js/layout.js index 1d6838b..1b345ca 100644 --- a/static/js/layout.js +++ b/static/js/layout.js @@ -1,126 +1,126 @@ -const rawStaticBase = document.documentElement.dataset.staticBase || '/static'; -const staticBase = rawStaticBase.endsWith('/') ? rawStaticBase : `${rawStaticBase}/`; -const contentVersion = document.documentElement.dataset.assetVersion || '1'; - -async function loadContent() { - const lang = localStorage.getItem('lang') || 'ru'; - - const response = await fetch(`${staticBase}content/${lang}.json?v=${contentVersion}`); - const data = await response.json(); - - document.querySelectorAll('[data-key]').forEach(el => { - const key = el.dataset.key; - if (data[key]) el.textContent = data[key]; - }); - - document.querySelectorAll('[data-aria-label-key]').forEach(el => { - const key = el.dataset.ariaLabelKey; - if (data[key]) el.setAttribute('aria-label', data[key]); - }); - - document.querySelectorAll('[data-placeholder-key]').forEach(el => { - const key = el.dataset.placeholderKey; - if (data[key]) el.setAttribute('placeholder', data[key]); - }); - - - document.querySelectorAll('[data-title-key]').forEach(el => { - const key = el.dataset.titleKey; - if (data[key]) el.setAttribute('title', data[key]); - }); - - renderList('skills_backend', data.skills_backend); - renderList('skills_frontend', data.skills_frontend); - renderList('skills_infrastructure', data.skills_infrastructure); - renderList('skills_industrial', data.skills_industrial); - renderList('about_highlights', data.about_highlights); - - function renderList(id, items) { - const container = document.getElementById(id); - if (!container || !items) return; - - container.innerHTML = ''; - items.forEach(item => { - const li = document.createElement('li'); - li.textContent = String(item).replace(/^\s*[•●▪◦]\s*/, ''); - container.appendChild(li); +(() => { + const rawStaticBase = document.documentElement.dataset.staticBase || '/static'; + const staticBase = rawStaticBase.endsWith('/') ? rawStaticBase : `${rawStaticBase}/`; + const contentVersion = document.documentElement.dataset.assetVersion || '1'; + + async function loadContent() { + const lang = localStorage.getItem('lang') || 'ru'; + + const response = await fetch(`${staticBase}content/${lang}.json?v=${contentVersion}`); + const data = await response.json(); + + document.querySelectorAll('[data-key]').forEach(el => { + const key = el.dataset.key; + if (data[key]) el.textContent = data[key]; }); - } - const projects = document.getElementById('projects'); - if (projects && data.projects) { - projects.innerHTML = ''; - data.projects.forEach(p => { - projects.innerHTML += ` -
-

${p.title}

-

${p.desc}

- ${p.stack} -
- Live Demo - GitHub -
-
`; + document.querySelectorAll('[data-aria-label-key]').forEach(el => { + const key = el.dataset.ariaLabelKey; + if (data[key]) el.setAttribute('aria-label', data[key]); }); - } -} -function setActiveNav() { - const currentPath = window.location.pathname.replace(/\/$/, ''); + document.querySelectorAll('[data-placeholder-key]').forEach(el => { + const key = el.dataset.placeholderKey; + if (data[key]) el.setAttribute('placeholder', data[key]); + }); - document.querySelectorAll('nav a').forEach(link => { - const linkPath = new URL(link.href).pathname.replace(/\/$/, ''); + document.querySelectorAll('[data-title-key]').forEach(el => { + const key = el.dataset.titleKey; + if (data[key]) el.setAttribute('title', data[key]); + }); - if (linkPath === currentPath) { - link.classList.add('active'); - } - }); -} - -document.addEventListener('DOMContentLoaded', setActiveNav); - - -window.toggleLang = () => { - const current = localStorage.getItem('lang') || 'ru'; - const next = current === 'ru' ? 'en' : 'ru'; - localStorage.setItem('lang', next); - location.reload(); -}; - -function updateLangLabel() { - const lang = localStorage.getItem('lang') || 'ru'; - const label = document.getElementById('lang-label'); - if (label) label.textContent = lang.toUpperCase(); -} - -updateLangLabel(); -loadContent(); -setActiveNav(); - -window.copyEmailAddress = async () => { - const email = 'dissenter.rav@gmail.com'; - const notice = document.getElementById('email-copy-notice'); - - try { - if (navigator.clipboard?.writeText) { - await navigator.clipboard.writeText(email); - } else { - const fallback = document.createElement('textarea'); - fallback.value = email; - fallback.setAttribute('readonly', ''); - fallback.style.position = 'absolute'; - fallback.style.left = '-9999px'; - document.body.appendChild(fallback); - fallback.select(); - document.execCommand('copy'); - document.body.removeChild(fallback); + renderList('skills_backend', data.skills_backend); + renderList('skills_frontend', data.skills_frontend); + renderList('skills_infrastructure', data.skills_infrastructure); + renderList('skills_industrial', data.skills_industrial); + renderList('about_highlights', data.about_highlights); + + function renderList(id, items) { + const container = document.getElementById(id); + if (!container || !items) return; + + container.innerHTML = ''; + items.forEach(item => { + const li = document.createElement('li'); + li.textContent = String(item).replace(/^\s*[•●▪◦]\s*/, ''); + container.appendChild(li); + }); } - if (notice) { - notice.classList.add('is-visible'); - setTimeout(() => notice.classList.remove('is-visible'), 1800); + const projects = document.getElementById('projects'); + if (projects && data.projects) { + projects.innerHTML = ''; + data.projects.forEach(p => { + projects.innerHTML += ` +
+

${p.title}

+

${p.desc}

+ ${p.stack} +
+ Live Demo + GitHub +
+
`; + }); } - } catch (error) { - console.error('Failed to copy email:', error); } -}; + + function setActiveNav() { + const currentPath = window.location.pathname.replace(/\/$/, ''); + + document.querySelectorAll('nav a').forEach(link => { + const linkPath = new URL(link.href).pathname.replace(/\/$/, ''); + + if (linkPath === currentPath) { + link.classList.add('active'); + } + }); + } + + window.toggleLang = () => { + const current = localStorage.getItem('lang') || 'ru'; + const next = current === 'ru' ? 'en' : 'ru'; + localStorage.setItem('lang', next); + location.reload(); + }; + + function updateLangLabel() { + const lang = localStorage.getItem('lang') || 'ru'; + const label = document.getElementById('lang-label'); + if (label) label.textContent = lang.toUpperCase(); + } + + window.copyEmailAddress = async () => { + const email = 'dissenter.rav@gmail.com'; + const notice = document.getElementById('email-copy-notice'); + + try { + if (navigator.clipboard?.writeText) { + await navigator.clipboard.writeText(email); + } else { + const fallback = document.createElement('textarea'); + fallback.value = email; + fallback.setAttribute('readonly', ''); + fallback.style.position = 'absolute'; + fallback.style.left = '-9999px'; + document.body.appendChild(fallback); + fallback.select(); + document.execCommand('copy'); + document.body.removeChild(fallback); + } + + if (notice) { + notice.classList.add('is-visible'); + setTimeout(() => notice.classList.remove('is-visible'), 1800); + } + } catch (error) { + console.error('Failed to copy email:', error); + } + }; + + document.addEventListener('DOMContentLoaded', setActiveNav); + + updateLangLabel(); + loadContent(); + setActiveNav(); +})(); diff --git a/static/js/project.js b/static/js/project.js index 7da3759..a1bf466 100644 --- a/static/js/project.js +++ b/static/js/project.js @@ -1,4 +1,4 @@ -(async () => { +(() => { const pathParts = location.pathname.split('/').filter(Boolean); const id = pathParts[pathParts.length - 1]; if (!id) return; @@ -8,11 +8,17 @@ const contentVersion = document.documentElement.dataset.assetVersion || '1'; const lang = localStorage.getItem('lang') || 'ru'; - const data = await fetch(`${staticBase}content/${lang}.json?v=${contentVersion}`).then(r => r.json()); - const project = data.projects.find(p => p.id === id); - if (!project) return; + fetch(`${staticBase}content/${lang}.json?v=${contentVersion}`) + .then(r => r.json()) + .then(data => { + const project = data.projects.find(p => p.id === id); + if (!project) return; - document.getElementById('project-title').textContent = project.title; - document.getElementById('project-details').textContent = project.details; - document.getElementById('project-stack').textContent = project.stack; + document.getElementById('project-title').textContent = project.title; + document.getElementById('project-details').textContent = project.details; + document.getElementById('project-stack').textContent = project.stack; + }) + .catch(error => { + console.error('Failed to load project content:', error); + }); })(); diff --git a/static/js/theme.js b/static/js/theme.js index 809c5cc..c8036c7 100644 --- a/static/js/theme.js +++ b/static/js/theme.js @@ -1,46 +1,50 @@ -const THEME_KEY = 'theme'; -const THEME_LINK_ID = 'theme-style'; -const rawStaticBase = document.documentElement.dataset.staticBase || '/static'; -const staticBase = rawStaticBase.endsWith('/') ? rawStaticBase : `${rawStaticBase}/`; -const assetVersion = document.documentElement.dataset.assetVersion || '1'; - -function applyTheme(theme) { - const link = document.getElementById(THEME_LINK_ID); - document.documentElement.setAttribute('data-theme', theme); - if (!link) { +(() => { + const THEME_KEY = 'theme'; + const THEME_LINK_ID = 'theme-style'; + const rawStaticBase = document.documentElement.dataset.staticBase || '/static'; + const staticBase = rawStaticBase.endsWith('/') ? rawStaticBase : `${rawStaticBase}/`; + const assetVersion = document.documentElement.dataset.assetVersion || '1'; + + function applyTheme(theme) { + const link = document.getElementById(THEME_LINK_ID); + document.documentElement.setAttribute('data-theme', theme); + if (!link) { + updateThemeIcon(theme); + return; + } + + // cache-busting, иначе Firefox делает вид что ничего не менялось + link.href = `${staticBase}css/${theme}.css?v=${assetVersion}-${Date.now()}`; updateThemeIcon(theme); - return; } - // cache-busting, иначе Firefox делает вид что ничего не менялось - link.href = `${staticBase}css/${theme}.css?v=${assetVersion}-${Date.now()}`; - updateThemeIcon(theme); -} - -function updateThemeIcon(theme) { - const icon = document.getElementById('theme-icon'); - const button = document.querySelector('.theme-toggle'); - if (!icon || !button) return; - const isDark = theme === 'dark'; - icon.textContent = ''; - button.setAttribute('aria-label', isDark ? 'Switch to light theme' : 'Switch to dark theme'); -} - -function toggleTheme() { - const current = localStorage.getItem(THEME_KEY) || 'dark'; - const next = current === 'dark' ? 'light' : 'dark'; - - localStorage.setItem(THEME_KEY, next); - applyTheme(next); -} - -function initTheme() { - const saved = localStorage.getItem(THEME_KEY) || 'dark'; - applyTheme(saved); -} - -if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', initTheme); -} else { - initTheme(); -} + function updateThemeIcon(theme) { + const icon = document.getElementById('theme-icon'); + const button = document.querySelector('.theme-toggle'); + if (!icon || !button) return; + const isDark = theme === 'dark'; + icon.textContent = ''; + button.setAttribute('aria-label', isDark ? 'Switch to light theme' : 'Switch to dark theme'); + } + + function toggleTheme() { + const current = localStorage.getItem(THEME_KEY) || 'dark'; + const next = current === 'dark' ? 'light' : 'dark'; + + localStorage.setItem(THEME_KEY, next); + applyTheme(next); + } + + function initTheme() { + const saved = localStorage.getItem(THEME_KEY) || 'dark'; + applyTheme(saved); + } + + window.toggleTheme = toggleTheme; + + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', initTheme); + } else { + initTheme(); + } +})(); From 6ff076bd52a005a85c21fff5bba82b2fcd1f70a1 Mon Sep 17 00:00:00 2001 From: Dissenter <118754629+dissenter-web@users.noreply.github.com> Date: Thu, 9 Apr 2026 09:55:11 +0300 Subject: [PATCH 3/9] Improve responsive layout for HD, tablet, and mobile --- static/css/base.css | 68 +++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 66 insertions(+), 2 deletions(-) diff --git a/static/css/base.css b/static/css/base.css index 3c83fb0..9338f71 100644 --- a/static/css/base.css +++ b/static/css/base.css @@ -71,6 +71,49 @@ body { opacity: 0.75; } + +@media (max-width: 1600px) { + .side-icons { + gap: 110px; + } + + .side-icons-left { + left: 20px; + } + + .side-icons-right { + right: 20px; + } + + .side-icons img { + width: 96px; + height: 96px; + padding: 8px; + } +} + +@media (max-width: 1366px) { + .side-icons { + gap: 72px; + top: 100px; + } + + .side-icons-left { + left: 10px; + } + + .side-icons-right { + right: 10px; + } + + .side-icons img { + width: 72px; + height: 72px; + padding: 6px; + opacity: 0.6; + } +} + main { flex: 1 0 auto; font-size: 1.12rem; @@ -502,18 +545,39 @@ button { /* ===== Responsive ===== */ @media (max-width: 1024px) { + .side-icons { + display: none; + } + .container { padding: 3rem 1.25rem; } .header { - padding: 0.9rem 1.2rem; + height: auto; + flex-wrap: wrap; + gap: 0.6rem; + padding: 0.85rem 1.1rem; font-size: 1rem; } + .logo img { + max-height: 30px; + } + + .nav-links { + width: 100%; + justify-content: center; + gap: 0.3rem; + } + nav a { padding: 0.45rem 0.65rem; - font-size: 0.98rem; + font-size: 0.96rem; + } + + .controls { + margin-left: auto; } .hero h1 { From e7d08926847f12c7e03aeb890f8602db2875321a Mon Sep 17 00:00:00 2001 From: Dissenter <118754629+dissenter-web@users.noreply.github.com> Date: Thu, 9 Apr 2026 10:04:46 +0300 Subject: [PATCH 4/9] Remove unused assets/styles and clean base CSS --- static/assets/industrial_icon/automation.svg | 4 - static/assets/industrial_icon/hmi.svg | 5 - static/assets/industrial_icon/plc.svg | 5 - static/assets/web_icon/api.svg | 5 - static/assets/web_icon/browser.svg | 7 -- static/assets/web_icon/code.svg | 5 - static/css/base.css | 110 +------------------ 7 files changed, 1 insertion(+), 140 deletions(-) delete mode 100644 static/assets/industrial_icon/automation.svg delete mode 100644 static/assets/industrial_icon/hmi.svg delete mode 100644 static/assets/industrial_icon/plc.svg delete mode 100644 static/assets/web_icon/api.svg delete mode 100644 static/assets/web_icon/browser.svg delete mode 100644 static/assets/web_icon/code.svg diff --git a/static/assets/industrial_icon/automation.svg b/static/assets/industrial_icon/automation.svg deleted file mode 100644 index 52f634e..0000000 --- a/static/assets/industrial_icon/automation.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/static/assets/industrial_icon/hmi.svg b/static/assets/industrial_icon/hmi.svg deleted file mode 100644 index 3498679..0000000 --- a/static/assets/industrial_icon/hmi.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/static/assets/industrial_icon/plc.svg b/static/assets/industrial_icon/plc.svg deleted file mode 100644 index aa8470f..0000000 --- a/static/assets/industrial_icon/plc.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/static/assets/web_icon/api.svg b/static/assets/web_icon/api.svg deleted file mode 100644 index c92cc0d..0000000 --- a/static/assets/web_icon/api.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/static/assets/web_icon/browser.svg b/static/assets/web_icon/browser.svg deleted file mode 100644 index 13b4947..0000000 --- a/static/assets/web_icon/browser.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/static/assets/web_icon/code.svg b/static/assets/web_icon/code.svg deleted file mode 100644 index 6b0bfe2..0000000 --- a/static/assets/web_icon/code.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/static/css/base.css b/static/css/base.css index 9338f71..558f12b 100644 --- a/static/css/base.css +++ b/static/css/base.css @@ -136,16 +136,6 @@ main { background: var(--bar-bg); } -.header-inner { - max-width: 1200px; - margin: 0 auto; - padding: 0 16px; - - display: flex; - align-items: center; - justify-content: space-between; -} - .logo { display: flex; align-items: center; @@ -349,55 +339,11 @@ nav { border: 0; } -.skill progress { - width: 100%; - height: 10px; - border-radius: 5px; - background: #222; -} - -.skill progress::-webkit-progress-value { - background: var(--accent); - border-radius: 5px; -} - -.skill progress::-moz-progress-bar { - background: var(--accent); - border-radius: 5px; -} - .section-block { margin-top: 2rem; } -.form-block { - display: grid; - gap: 0.8rem; -} - -.form-block label { - display: grid; - gap: 0.35rem; - font-size: 0.95rem; -} - -.form-block input, -.form-block textarea, -.form-block button, -.inline-form button, -button { - font: inherit; -} - -.form-block input, -.form-block textarea { - background: transparent; - color: var(--text); - border: 1px solid var(--border); - border-radius: 8px; - padding: 0.65rem 0.75rem; -} button { border: 1px solid var(--border); @@ -406,49 +352,7 @@ button { color: var(--text); padding: 0.6rem 0.9rem; cursor: pointer; -} - -.review-list { - display: grid; - gap: 0.9rem; - margin-bottom: 1rem; -} - -.review-item { - border: 1px solid var(--border); - border-radius: 10px; - padding: 0.8rem; -} - -.notice { - border: 1px solid var(--accent); - border-radius: 8px; - padding: 0.6rem; -} - -.notice.error { - border-color: #ff6b6b; -} - -.inline-form { - display: flex; - gap: 0.5rem; -} - -.button-link { - display: inline-block; - text-decoration: none; - border: 1px solid var(--border); - border-radius: 8px; - background: var(--card); - color: var(--text); - padding: 0.6rem 0.9rem; -} - -.contact-disclosure summary { - cursor: pointer; - font-weight: 600; - margin-bottom: 1rem; + font: inherit; } .contact-actions { @@ -677,18 +581,6 @@ button { font-size: 1.7rem; } - .inline-form { - flex-direction: column; - align-items: stretch; - } - - .button-link, - .inline-form button, - .form-block button { - width: 100%; - text-align: center; - } - .contact-actions { flex-direction: column; } From 31aa36fa4ebcf42405bbf1d3feacd07127133dcd Mon Sep 17 00:00:00 2001 From: Dissenter <118754629+dissenter-web@users.noreply.github.com> Date: Thu, 9 Apr 2026 10:30:31 +0300 Subject: [PATCH 5/9] Refactor base.css structure with clear desktop/adaptive sections --- static/css/base.css | 473 ++++++++++++++++++++++++-------------------- 1 file changed, 258 insertions(+), 215 deletions(-) diff --git a/static/css/base.css b/static/css/base.css index 558f12b..1f1aa14 100644 --- a/static/css/base.css +++ b/static/css/base.css @@ -1,16 +1,21 @@ @import url('https://fonts.googleapis.com/css2?family=UnifrakturCook:wght@700&display=swap'); +/* ========================================================= + 1) Design tokens (desktop-first) + ========================================================= */ :root { --bg: #0f1115; --text: #eaeaea; + --text-muted: #9aa0a6; + --accent: #4da3ff; --nav-accent: #8315cc; --nav-hover-bg: rgba(131, 21, 204, 0.16); - --card: #161a22; + --card: #161a22; --border: #222; - --text-muted: #9aa0a6; --icon: #eaeaea; + --control-border: #4f5561; --control-bg: rgba(255, 255, 255, 0.03); --logo-outline: transparent; @@ -18,37 +23,79 @@ --side-icon-filter: brightness(0) invert(0.92); } +/* ========================================================= + 2) Base / reset + ========================================================= */ +* { + box-sizing: border-box; +} - -* { box-sizing: border-box; } - - -img, svg { +img, +svg { max-width: 100%; } body { margin: 0; - font-family: Inter, system-ui, sans-serif; - background: var(--bg); - color: var(--text); - font-size: 16px; min-height: 100vh; - display: flex; - flex-direction: column; position: relative; overflow-x: hidden; + + display: flex; + flex-direction: column; + + font-family: Inter, system-ui, sans-serif; + font-size: 16px; + background: var(--bg); + color: var(--text); +} + +main { + flex: 1 0 auto; + font-size: 1.12rem; +} + +.container { + max-width: 1100px; + margin: 0 auto; + padding: 4rem 1.5rem; +} + +button { + font: inherit; + color: var(--text); + background: var(--card); + border: 1px solid var(--border); + border-radius: 8px; + padding: 0.6rem 0.9rem; + cursor: pointer; +} + +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; } +/* ========================================================= + 3) Desktop layout: decorative side icons + ========================================================= */ .side-icons { position: fixed; top: 92px; bottom: 24px; + z-index: 0; + display: flex; flex-direction: column; justify-content: flex-start; gap: 152px; - z-index: 0; } .side-icons-left { @@ -62,78 +109,29 @@ body { .side-icons img { width: 126px; height: 126px; + padding: 10px; object-fit: contain; + border: none; border-radius: 0; background: transparent; - padding: 10px; filter: var(--side-icon-filter); opacity: 0.75; } - -@media (max-width: 1600px) { - .side-icons { - gap: 110px; - } - - .side-icons-left { - left: 20px; - } - - .side-icons-right { - right: 20px; - } - - .side-icons img { - width: 96px; - height: 96px; - padding: 8px; - } -} - -@media (max-width: 1366px) { - .side-icons { - gap: 72px; - top: 100px; - } - - .side-icons-left { - left: 10px; - } - - .side-icons-right { - right: 10px; - } - - .side-icons img { - width: 72px; - height: 72px; - padding: 6px; - opacity: 0.6; - } -} - -main { - flex: 1 0 auto; - font-size: 1.12rem; -} - -.container { - max-width: 1100px; - margin: 0 auto; - padding: 4rem 1.5rem; -} - +/* ========================================================= + 4) Desktop layout: header / nav / controls + ========================================================= */ .header { - display: flex; - justify-content: space-between; - align-items: center; min-height: 78px; padding: 0.8rem 2rem; border-bottom: 1px solid #222; - font-size: 1.10rem; background: var(--bar-bg); + font-size: 1.1rem; + + display: flex; + justify-content: space-between; + align-items: center; } .logo { @@ -143,28 +141,40 @@ main { } .logo img { + width: auto; height: 70%; max-height: 33px; - width: auto; - filter: drop-shadow(1px 0 0 var(--logo-outline)) - drop-shadow(-1px 0 0 var(--logo-outline)) - drop-shadow(0 1px 0 var(--logo-outline)) - drop-shadow(0 -1px 0 var(--logo-outline)) - drop-shadow(0 0 2px var(--logo-outline)); + filter: + drop-shadow(1px 0 0 var(--logo-outline)) + drop-shadow(-1px 0 0 var(--logo-outline)) + drop-shadow(0 1px 0 var(--logo-outline)) + drop-shadow(0 -1px 0 var(--logo-outline)) + drop-shadow(0 0 2px var(--logo-outline)); +} + +nav { + min-width: 0; +} + +.nav-links { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 0.25rem; } nav a { padding: 8px 14px; border-radius: 8px; + color: var(--text); text-decoration: none; - font-size: 1.10rem; - font-weight: 600; text-transform: uppercase; letter-spacing: 0.03em; - transition: - background-color 0.25s ease, - color 0.25s ease; + font-size: 1.1rem; + font-weight: 600; + + transition: background-color 0.25s ease, color 0.25s ease; } nav a:hover { @@ -172,8 +182,8 @@ nav a:hover { } nav a.active { - opacity: 1; position: relative; + opacity: 1; } nav a.active::after { @@ -183,35 +193,10 @@ nav a.active::after { bottom: -6px; width: 100%; height: 2px; - background: var(--nav-accent); border-radius: 2px; + background: var(--nav-accent); } -.hero h1 { - font-size: 2.7rem; - margin-bottom: 1rem; -} - -.grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); - gap: 2rem; -} - -.card { - background: var(--card); - padding: 2rem; - border-radius: 12px; - transition: transform .3s ease, box-shadow .3s ease; -} - -.card:hover { - transform: translateY(-6px); - box-shadow: 0 20px 40px rgba(0,0,0,.3); -} - - - .controls { display: flex; align-items: center; @@ -222,9 +207,11 @@ nav a.active::after { width: 42px; height: 42px; padding: 0; + display: inline-flex; align-items: center; justify-content: center; + border: 1px solid var(--control-border); background: var(--control-bg); } @@ -234,6 +221,10 @@ nav a.active::after { line-height: 1; } +#theme-icon { + display: inline-block; +} + #theme-icon::before { content: '🌙'; } @@ -242,34 +233,42 @@ html[data-theme='light'] #theme-icon::before { content: '☀️'; } -#theme-icon { - display: inline-block; -} - .lang-toggle { font-size: 0.9rem; font-weight: 700; letter-spacing: 0.02em; } -.nav-links { - display: flex; - flex-wrap: wrap; - gap: 0.25rem; - align-items: center; +/* ========================================================= + 5) Desktop layout: content blocks / cards / lists + ========================================================= */ +.hero h1 { + margin-bottom: 1rem; + font-size: 2.7rem; } -nav { - min-width: 0; +.grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); + gap: 2rem; } .card { - text-decoration: none; + padding: 2rem; + border-radius: 12px; + background: var(--card); color: inherit; + text-decoration: none; + transition: transform 0.3s ease, box-shadow 0.3s ease; +} + +.card:hover { + transform: translateY(-6px); + box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3); } .card small { - opacity: .6; + opacity: 0.6; } .project-card h3 a { @@ -279,89 +278,48 @@ nav { .project-actions { display: flex; + flex-wrap: wrap; gap: 0.55rem; margin-top: 1rem; - flex-wrap: wrap; } -.footer { - border-top: 1px solid #222; - margin-top: auto; - flex-shrink: 0; - background: var(--bar-bg); -} - -.footer-inner { - max-width: 1100px; - margin: 0 auto; - padding: 24px 16px; - display: flex; - justify-content: space-between; - align-items: center; - font-size: 1.08rem; - color: var(--text-muted); +.skills-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 1.4rem; } -.socials { - display: flex; - gap: 16px; +.skill-card h2 { + margin-top: 0; } -.icon { - width: 20px; - height: 20px; - color: var(--icon); /* ВАЖНО */ - opacity: 0.9; - transition: color .2s ease, opacity .2s ease, transform .2s ease; -} +.skill-list { + list-style: none; + margin: 0; + padding: 0; -.icon:hover { - opacity: 1; - transform: translateY(-1px); + display: grid; + gap: 0.55rem; } -.footer .icon { - color: var(--icon); - opacity: 1; - width: 32px; - height: 32px; +.skill-list li { + position: relative; + padding-left: 1rem; } -.sr-only { +.skill-list li::before { + content: '•'; position: absolute; - width: 1px; - height: 1px; - padding: 0; - margin: -1px; - overflow: hidden; - clip: rect(0, 0, 0, 0); - white-space: nowrap; - border: 0; + left: 0; } - +/* ========================================================= + 6) Desktop layout: about/contact blocks + ========================================================= */ .section-block { margin-top: 2rem; } - -button { - border: 1px solid var(--border); - border-radius: 8px; - background: var(--card); - color: var(--text); - padding: 0.6rem 0.9rem; - cursor: pointer; - font: inherit; -} - -.contact-actions { - display: flex; - flex-wrap: wrap; - gap: 0.7rem; - margin-bottom: 1rem; -} - .about-layout { display: grid; grid-template-columns: minmax(0, 1fr) 320px; @@ -375,6 +333,13 @@ button { top: 1rem; } +.contact-actions { + display: flex; + flex-wrap: wrap; + gap: 0.7rem; + margin-bottom: 1rem; +} + .contact-actions-column { flex-direction: column; align-items: stretch; @@ -385,19 +350,16 @@ button { display: inline-flex; align-items: center; gap: 0.45rem; + padding: 0.55rem 0.75rem; border: 1px solid var(--border); border-radius: 8px; + color: var(--text); text-decoration: none; transition: background-color 0.25s ease, color 0.25s ease; } -.gmail-copy-button { - justify-content: flex-start; - text-align: left; -} - .icon-button:hover { background-color: var(--nav-hover-bg); } @@ -407,6 +369,11 @@ button { height: 16px; } +.gmail-copy-button { + justify-content: flex-start; + text-align: left; +} + .copy-notice { margin: 0.65rem 0 0; font-size: 0.9rem; @@ -418,36 +385,105 @@ button { opacity: 1; } -.skills-grid { - display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: 1.4rem; +/* ========================================================= + 7) Desktop layout: footer / social icons + ========================================================= */ +.footer { + margin-top: auto; + flex-shrink: 0; + border-top: 1px solid #222; + background: var(--bar-bg); } -.skill-card h2 { - margin-top: 0; +.footer-inner { + max-width: 1100px; + margin: 0 auto; + padding: 24px 16px; + + display: flex; + align-items: center; + justify-content: space-between; + + font-size: 1.08rem; + color: var(--text-muted); } -.skill-list { - list-style: none; - margin: 0; - padding: 0; - display: grid; - gap: 0.55rem; +.socials { + display: flex; + gap: 16px; } -.skill-list li { - position: relative; - padding-left: 1rem; +.icon { + width: 20px; + height: 20px; + color: var(--icon); + opacity: 0.9; + transition: color 0.2s ease, opacity 0.2s ease, transform 0.2s ease; } -.skill-list li::before { - content: '•'; - position: absolute; - left: 0; +.icon:hover { + opacity: 1; + transform: translateY(-1px); +} + +.footer .icon { + width: 32px; + height: 32px; + color: var(--icon); + opacity: 1; +} + +/* ========================================================= + 8) Adaptive: large desktops (<=1600) + ========================================================= */ +@media (max-width: 1600px) { + .side-icons { + gap: 110px; + } + + .side-icons-left { + left: 20px; + } + + .side-icons-right { + right: 20px; + } + + .side-icons img { + width: 96px; + height: 96px; + padding: 8px; + } +} + +/* ========================================================= + 9) Adaptive: HD laptops (<=1366) + ========================================================= */ +@media (max-width: 1366px) { + .side-icons { + top: 100px; + gap: 72px; + } + + .side-icons-left { + left: 10px; + } + + .side-icons-right { + right: 10px; + } + + .side-icons img { + width: 72px; + height: 72px; + padding: 6px; + opacity: 0.6; + } } -/* ===== Responsive ===== */ +/* ========================================================= + 10) Adaptive: tablets and small laptops (<=1024) + ========================================================= */ @media (max-width: 1024px) { .side-icons { display: none; @@ -493,6 +529,9 @@ button { } } +/* ========================================================= + 11) Adaptive: mobile landscape / small tablets (<=768) + ========================================================= */ @media (max-width: 768px) { .side-icons { display: none; @@ -546,6 +585,9 @@ button { } } +/* ========================================================= + 12) Adaptive: phones (<=520) + ========================================================= */ @media (max-width: 520px) { .header { flex-direction: column; @@ -564,9 +606,9 @@ button { } .nav-links { - justify-content: center; display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); + justify-content: center; width: 100%; gap: 0.4rem; } @@ -596,10 +638,11 @@ button { .icon-button { justify-content: center; } - } - +/* ========================================================= + 13) Adaptive: very small phones (<=360) + ========================================================= */ @media (max-width: 360px) { .nav-links { grid-template-columns: 1fr; From e06cce3c503ec9f2f33b2b1c59825f1ab756de3f Mon Sep 17 00:00:00 2001 From: Dissenter <118754629+dissenter-web@users.noreply.github.com> Date: Thu, 9 Apr 2026 10:53:25 +0300 Subject: [PATCH 6/9] Add deployment-ready baseline for portfolio site --- .dockerignore | 12 ++++++++++++ .env.example | 9 +++++++++ Dockerfile | 10 ++++++++-- README.md | 38 +++++++++++++++++++++++++++++++------- app/main.py | 41 +++++++++++++++++++++++++++++++++++++++-- docker-compose.yml | 15 +++++++++++++-- 6 files changed, 112 insertions(+), 13 deletions(-) create mode 100644 .dockerignore create mode 100644 .env.example diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..f6d3576 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,12 @@ +.git +.gitignore +.venv +__pycache__ +*.pyc +*.pyo +*.pyd +.pytest_cache +.mypy_cache +node_modules +.env +README.md diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..ff26fc9 --- /dev/null +++ b/.env.example @@ -0,0 +1,9 @@ +# Static asset version for cache busting (bump on each deploy) +ASSET_VERSION=1 + +# Optional root path if app is served under a subpath, e.g. /portfolio +ROOT_PATH= + +# Comma-separated allowed hosts for TrustedHostMiddleware +# Example: example.com,www.example.com,localhost,127.0.0.1 +ALLOWED_HOSTS=* diff --git a/Dockerfile b/Dockerfile index a3fc247..48eeadc 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,11 +1,17 @@ FROM python:3.11-slim -WORKDIR /app +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 -COPY requirements.txt . +WORKDIR /app +COPY requirements.txt ./ RUN pip install --no-cache-dir -r requirements.txt COPY . . +RUN useradd --create-home --shell /bin/bash appuser && \ + chown -R appuser:appuser /app +USER appuser + CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/README.md b/README.md index eba1027..abe8e95 100644 --- a/README.md +++ b/README.md @@ -8,13 +8,13 @@ - Docker / docker-compose ## 1) Быстрый запуск (Docker) -1. Создай `.env` в корне (опционально): - ```env - APP_ENV=production +1. Скопируй пример env: + ```bash + cp .env.example .env ``` 2. Подними контейнер: ```bash - docker compose up --build + docker compose up --build -d ``` 3. Открой сайт: `http://localhost:8000` @@ -28,7 +28,27 @@ uvicorn app.main:app --reload ``` -## 3) Проверки +## 3) Минимум для деплоя (production-ready для портфолио) +- Непривилегированный пользователь в контейнере (`appuser`). +- Health/readiness endpoint'ы: `/healthz`, `/readyz`. +- `docker-compose` healthcheck (проверка `/readyz`). +- Базовые security headers + gzip. +- Ограничение хостов через `ALLOWED_HOSTS`. +- Версионирование ассетов через `ASSET_VERSION`. + +## 4) Переменные окружения +- `ASSET_VERSION` — версия статики для cache-busting (повышай при релизе). +- `ROOT_PATH` — если приложение работает не с `/`, а с подпути. +- `ALLOWED_HOSTS` — список разрешённых хостов через запятую. + +Пример: +```env +ASSET_VERSION=5 +ROOT_PATH= +ALLOWED_HOSTS=example.com,www.example.com +``` + +## 5) Проверки - Проверка синтаксиса Python: ```bash python -m compileall app static/js @@ -37,8 +57,12 @@ ```bash curl -s http://localhost:8000/healthz ``` +- Проверка readiness endpoint: + ```bash + curl -s http://localhost:8000/readyz + ``` -## 4) Структура проекта -- `app/` — backend-код (роуты) +## 6) Структура проекта +- `app/` — backend-код (роуты и middleware) - `templates/` — Jinja-шаблоны страниц - `static/` — CSS/JS/контент (`static/content/*.json`) diff --git a/app/main.py b/app/main.py index 9b45f75..f13767e 100644 --- a/app/main.py +++ b/app/main.py @@ -1,14 +1,46 @@ import os from fastapi import FastAPI, Request +from fastapi.responses import JSONResponse from fastapi.staticfiles import StaticFiles from fastapi.templating import Jinja2Templates +from starlette.middleware.gzip import GZipMiddleware +from starlette.middleware.trustedhost import TrustedHostMiddleware -app = FastAPI(title='Personal Site') + +def parse_allowed_hosts(raw: str) -> list[str]: + hosts = [h.strip() for h in raw.split(',') if h.strip()] + return hosts or ['*'] + + +def security_headers() -> dict[str, str]: + # CSP intentionally omitted because current templates use inline onclick handlers. + # Adding CSP without refactoring handlers to JS listeners would break UI controls. + return { + 'X-Content-Type-Options': 'nosniff', + 'X-Frame-Options': 'DENY', + 'Referrer-Policy': 'strict-origin-when-cross-origin', + 'Permissions-Policy': 'camera=(), microphone=(), geolocation=()' + } + + +ROOT_PATH = os.getenv('ROOT_PATH', '') +ASSET_VERSION = os.getenv('ASSET_VERSION', '1') +ALLOWED_HOSTS = parse_allowed_hosts(os.getenv('ALLOWED_HOSTS', '*')) + +app = FastAPI(title='Personal Site', root_path=ROOT_PATH) templates = Jinja2Templates(directory='templates') +app.add_middleware(GZipMiddleware, minimum_size=1024) +app.add_middleware(TrustedHostMiddleware, allowed_hosts=ALLOWED_HOSTS) app.mount('/static', StaticFiles(directory='static'), name='static') -ASSET_VERSION = os.getenv('ASSET_VERSION', '1') + +@app.middleware('http') +async def add_security_headers(request: Request, call_next): + response = await call_next(request) + for header, value in security_headers().items(): + response.headers.setdefault(header, value) + return response def render_template(request: Request, template_name: str, **context): @@ -22,6 +54,11 @@ def healthz(): return {'status': 'ok'} +@app.get('/readyz', name='readyz') +def readyz(): + return JSONResponse({'status': 'ready'}) + + @app.get('/', name='index') def index(request: Request): return render_template(request, 'index.html') diff --git a/docker-compose.yml b/docker-compose.yml index 751ad00..f21acd0 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -3,9 +3,20 @@ services: build: context: . container_name: personal_site_web + restart: unless-stopped ports: - "8000:8000" env_file: - .env - volumes: - - .:/app + healthcheck: + test: + [ + "CMD", + "python", + "-c", + "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/readyz', timeout=3)" + ] + interval: 30s + timeout: 5s + retries: 3 + start_period: 15s From ca900f8a2d0d6a9166835bc46d6e850f81137119 Mon Sep 17 00:00:00 2001 From: Dissenter <118754629+dissenter-web@users.noreply.github.com> Date: Thu, 9 Apr 2026 11:28:13 +0300 Subject: [PATCH 7/9] Update home page copy in RU/EN for backend portfolio focus --- static/content/en.json | 14 +++++++++----- static/content/ru.json | 14 +++++++++----- static/css/base.css | 10 ++++++++++ templates/index.html | 5 ++++- 4 files changed, 32 insertions(+), 11 deletions(-) diff --git a/static/content/en.json b/static/content/en.json index 9bdfbad..795396a 100644 --- a/static/content/en.json +++ b/static/content/en.json @@ -1,9 +1,9 @@ { - "hero_title": "Full-Stack Web Developer & Automation Engineer", - "hero_subtitle": "I design and build reliable web systems and production automation solutions — from backend architecture to industrial control integration.", + "hero_title": "Junior Backend Developer (Python)", + "hero_subtitle": "Junior Backend Developer (Python) with an engineering background in industrial automation.", "projects_title": "Projects", - "hero_desc_title": "Short intro block", - "hero_description": "I work at the intersection of software engineering and industrial automation. My focus is building scalable web applications and integrating them with real-world production systems. I value clean architecture, predictable system behavior, and solutions that remain maintainable under growth.", + "hero_desc_title": "Automation engineer and backend-focused developer", + "hero_description": "Automation engineer with hands-on experience working with industrial equipment, including PLC (Rockwell Automation), HMI development (Weintek), and electrical equipment maintenance.", "projects": [ { "id": "plc-monitoring", @@ -62,5 +62,9 @@ "contact_gmail": "Gmail", "contact_copied": "Email copied to clipboard", "contact_telegram": "Telegram", - "contact_github": "GitHub" + "contact_github": "GitHub", + "hero_description_1": "Automation engineer with hands-on experience working with industrial equipment, including PLC (Rockwell Automation), HMI development (Weintek), and electrical equipment maintenance.", + "hero_description_2": "Currently focused on backend development: building services in Python (FastAPI), designing REST APIs, working with Telegram bots, and integrating with external services. Experienced in deploying applications on VPS using Docker and Nginx.", + "hero_description_3": "Also familiar with frontend technologies (HTML, CSS, JavaScript), which helps to understand the full development cycle and interaction between client and server sides.", + "hero_description_4": "Actively improving backend development skills and looking for a junior backend developer position." } diff --git a/static/content/ru.json b/static/content/ru.json index 0ea5eb4..abda016 100644 --- a/static/content/ru.json +++ b/static/content/ru.json @@ -1,8 +1,8 @@ { - "hero_title": "Fullstack-разработчик и инженер по автоматизации", - "hero_subtitle": "Разрабатываю надёжные веб-системы и решения для автоматизации производства — от backend-архитектуры до интеграции с промышленными системами управления.", - "hero_desc_title": "Краткое описание", - "hero_description": "Работаю на стыке веб-разработки и промышленной автоматизации. Создаю масштабируемые веб-приложения и интегрирую их с реальными производственными системами. Ценю чистую архитектуру, предсказуемость и устойчивость решений к росту нагрузки.", + "hero_title": "Backend-разработчик (Python)", + "hero_subtitle": "Backend-разработчик (Python) с инженерным опытом в промышленной автоматизации.", + "hero_desc_title": "Инженер АСУ ТП и junior backend-разработчик", + "hero_description": "Имею практический опыт работы с промышленным оборудованием: PLC (Rockwell Automation), разработка HMI-интерфейсов (Weintek), обслуживание и ремонт электрооборудования.", "projects_title": "Проекты", "projects": [ { @@ -62,5 +62,9 @@ "contact_gmail": "Gmail", "contact_copied": "Адрес скопирован в буфер обмена", "contact_telegram": "Telegram", - "contact_github": "GitHub" + "contact_github": "GitHub", + "hero_description_1": "Имею практический опыт работы с промышленным оборудованием: PLC (Rockwell Automation), разработка HMI-интерфейсов (Weintek), обслуживание и ремонт электрооборудования.", + "hero_description_2": "В веб-разработке развиваюсь в направлении backend: создаю сервисы на Python (FastAPI), реализую REST API, работаю с Telegram-ботами и интеграциями с внешними сервисами. Имею опыт деплоя приложений на VPS с использованием Docker и Nginx.", + "hero_description_3": "Также знаком с фронтендом (HTML, CSS, JavaScript), что позволяет понимать полный цикл разработки и взаимодействие клиентской и серверной частей.", + "hero_description_4": "Стремлюсь углублять знания в backend-разработке и применять их в реальных задачах. Ищу позицию junior backend-разработчика." } diff --git a/static/css/base.css b/static/css/base.css index 1f1aa14..e056a2f 100644 --- a/static/css/base.css +++ b/static/css/base.css @@ -247,6 +247,16 @@ html[data-theme='light'] #theme-icon::before { font-size: 2.7rem; } + +.hero p { + margin: 0 0 1rem; + line-height: 1.65; +} + +.hero h3 { + margin: 1.6rem 0 0.8rem; +} + .grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); diff --git a/templates/index.html b/templates/index.html index 63094db..e67b869 100644 --- a/templates/index.html +++ b/templates/index.html @@ -8,7 +8,10 @@

-

+

+

+

+

{% endblock %} From 410923bb220b69755c759a181f6b8d48b9a4c8d1 Mon Sep 17 00:00:00 2001 From: Dissenter <118754629+dissenter-web@users.noreply.github.com> Date: Thu, 9 Apr 2026 11:46:37 +0300 Subject: [PATCH 8/9] Rewrite About page content in Russian and English --- static/content/en.json | 22 ++++++++++++++-------- static/content/ru.json | 22 ++++++++++++++-------- templates/about.html | 10 ++++++---- 3 files changed, 34 insertions(+), 20 deletions(-) diff --git a/static/content/en.json b/static/content/en.json index 795396a..ea238de 100644 --- a/static/content/en.json +++ b/static/content/en.json @@ -21,7 +21,7 @@ } ], "about_title": "About Me", - "about_intro": "I am a full-stack engineer with a strong backend focus, working primarily with Python and modern web technologies.", + "about_intro": "I am an industrial automation engineer by profession. I work with industrial equipment, program PLCs (Rockwell Automation), develop HMI interfaces (Weintek, Rockwell Automation), and handle maintenance and repair of electrical systems.", "about_skills_title": "Key skills", "skills_backend_title": "Backend", "skills_backend": [ @@ -47,13 +47,13 @@ "• HMI" ], "about_highlights": [ - "REST API design and implementation", - "Database architecture and optimization", - "Containerized environments (Docker)", - "Integration of web systems with industrial control environments" + "PLC (Rockwell Automation) and HMI (Weintek, Rockwell Automation)", + "Backend with Python (FastAPI), APIs, and Telegram bots", + "VPS deployments with Docker and Nginx", + "Practical mindset and integration-focused projects" ], - "about_background": "With a background in automation engineering, I understand how software interacts with physical processes. This allows me to design systems that are not only functional but also reliable in real production conditions.", - "about_growth": "I continuously improve my skills in scalable system design and infrastructure automation.", + "about_background": "I moved into web development intentionally and currently focus on backend development. I work with Python (FastAPI), build APIs, develop Telegram bots, and create small backend services. I also deploy applications on VPS using Docker and Nginx.", + "about_growth": "Currently looking for a junior backend developer position and continuing to improve my skills through hands-on projects.", "nav_home": "Home", "nav_skills": "Skills", "nav_projects": "Projects", @@ -66,5 +66,11 @@ "hero_description_1": "Automation engineer with hands-on experience working with industrial equipment, including PLC (Rockwell Automation), HMI development (Weintek), and electrical equipment maintenance.", "hero_description_2": "Currently focused on backend development: building services in Python (FastAPI), designing REST APIs, working with Telegram bots, and integrating with external services. Experienced in deploying applications on VPS using Docker and Nginx.", "hero_description_3": "Also familiar with frontend technologies (HTML, CSS, JavaScript), which helps to understand the full development cycle and interaction between client and server sides.", - "hero_description_4": "Actively improving backend development skills and looking for a junior backend developer position." + "hero_description_4": "Actively improving backend development skills and looking for a junior backend developer position.", + "about_p1": "I am an industrial automation engineer by profession. I work with industrial equipment, program PLCs (Rockwell Automation), develop HMI interfaces (Weintek, Rockwell Automation), and handle maintenance and repair of electrical systems.", + "about_p2": "This experience gives me a clear understanding of how software operates in real-world environments, where reliability, predictability, and ease of maintenance are critical.", + "about_p3": "I moved into web development intentionally and currently focus on backend development. I work with Python (FastAPI), build APIs, develop Telegram bots, and create small backend services. I also deploy applications on VPS using Docker and Nginx.", + "about_p4": "In both learning and development, I actively use AI tools to speed up workflows, troubleshoot issues, and explore solutions, while making sure I understand the code and how it works.", + "about_p5": "I prefer a practical approach: breaking down problems, delivering working solutions, and understanding how systems work under the hood. I am particularly interested in automation, integrations, and systems that connect different components.", + "about_p6": "Currently looking for a junior backend developer position and continuing to improve my skills through hands-on projects." } diff --git a/static/content/ru.json b/static/content/ru.json index abda016..ffa7b24 100644 --- a/static/content/ru.json +++ b/static/content/ru.json @@ -21,7 +21,7 @@ } ], "about_title": "Обо мне", - "about_intro": "Я full-stack разработчик с выраженным фокусом на backend, работаю преимущественно с Python и современными веб-технологиями.", + "about_intro": "По основной профессии я инженер АСУ ТП. Работаю с промышленным оборудованием, программирую PLC (Rockwell Automation), разрабатываю HMI-интерфейсы (Weintek, Rockwell Automation) и занимаюсь ремонтом и обслуживанием электрооборудования.", "about_skills_title": "Ключевые навыки", "skills_backend_title": "Backend", "skills_backend": [ @@ -47,13 +47,13 @@ "• Панель оператора" ], "about_highlights": [ - "Проектирование и разработка REST API", - "Архитектура и оптимизация баз данных", - "Контейнеризация и работа с Docker", - "Интеграция веб-систем с промышленными системами управления" + "PLC (Rockwell Automation) и HMI (Weintek, Rockwell Automation)", + "Backend на Python (FastAPI), API и Telegram-боты", + "Деплой на VPS с Docker и Nginx", + "Практический подход и интерес к интеграциям" ], - "about_background": "Имея опыт в области промышленной автоматизации, я понимаю, как программное обеспечение взаимодействует с физическими процессами. Это позволяет мне проектировать системы, которые не только функциональны, но и надёжны в реальных производственных условиях.", - "about_growth": "Я постоянно развиваюсь в области проектирования масштабируемых систем и автоматизации инфраструктуры.", + "about_background": "В веб-разработку пришёл осознанно и сейчас развиваюсь в направлении backend. Работаю с Python (FastAPI), создаю API, Telegram-ботов и небольшие сервисы, настраиваю деплой на VPS с использованием Docker и Nginx.", + "about_growth": "В данный момент рассматриваю позицию junior backend-разработчика и продолжаю развивать навыки через практические проекты.", "nav_home": "Главная", "nav_skills": "Навыки", "nav_projects": "Проекты", @@ -66,5 +66,11 @@ "hero_description_1": "Имею практический опыт работы с промышленным оборудованием: PLC (Rockwell Automation), разработка HMI-интерфейсов (Weintek), обслуживание и ремонт электрооборудования.", "hero_description_2": "В веб-разработке развиваюсь в направлении backend: создаю сервисы на Python (FastAPI), реализую REST API, работаю с Telegram-ботами и интеграциями с внешними сервисами. Имею опыт деплоя приложений на VPS с использованием Docker и Nginx.", "hero_description_3": "Также знаком с фронтендом (HTML, CSS, JavaScript), что позволяет понимать полный цикл разработки и взаимодействие клиентской и серверной частей.", - "hero_description_4": "Стремлюсь углублять знания в backend-разработке и применять их в реальных задачах. Ищу позицию junior backend-разработчика." + "hero_description_4": "Стремлюсь углублять знания в backend-разработке и применять их в реальных задачах. Ищу позицию junior backend-разработчика.", + "about_p1": "По основной профессии я инженер АСУ ТП. Работаю с промышленным оборудованием, программирую PLC (Rockwell Automation), разрабатываю HMI-интерфейсы (Weintek, Rockwell Automation) и занимаюсь ремонтом и обслуживанием электрооборудования.", + "about_p2": "Этот опыт даёт понимание того, как программные решения работают в реальных условиях, где важны стабильность, предсказуемость и простота сопровождения.", + "about_p3": "В веб-разработку пришёл осознанно и сейчас развиваюсь в направлении backend. Работаю с Python (FastAPI), создаю API, Telegram-ботов и небольшие сервисы, настраиваю деплой на VPS с использованием Docker и Nginx.", + "about_p4": "В обучении и работе активно использую AI-инструменты для ускорения разработки, поиска решений и отладки, при этом стараюсь разбираться в коде и понимать, как он работает.", + "about_p5": "Предпочитаю практический подход: разбираться в задачах, доводить решения до рабочего состояния и понимать, как всё устроено под капотом. Интересны проекты, связанные с автоматизацией, интеграциями и взаимодействием разных систем.", + "about_p6": "В данный момент рассматриваю позицию junior backend-разработчика и продолжаю развивать навыки через практические проекты." } diff --git a/templates/about.html b/templates/about.html index 232d968..3744e6b 100644 --- a/templates/about.html +++ b/templates/about.html @@ -6,10 +6,12 @@

-

- -

-

+

+

+

+

+

+