window.A3DATA={"lang":"uk","t":{"clSym":"ЯК ЦЕ ВИГЛЯДАЄ ЗБОКУ","clCheck":"ПЕРЕВІР ЗА 2 ХВИЛИНИ","clFix":"ЩО ЗРОБИТИ","clMore":"…і ще шість точок нижче","clSending":"надсилаю…","clErrFields":"Перевір імʼя і пошту.","formOk":"✓ отримав. відповім особисто, зазвичай того ж дня.","formErr":"не пройшло. напиши напряму:","formEmpty":"Заповни всі три поля, будь ласка.","formSending":"надсилаю…","bootInit":"ініціалізація","bootProjects":"монтую /projects","bootGit":"читаю git","bootField":"будую поле частинок","bootCvr":"підключаю CVR","bootEyes":"перевіряю зір","bootEyesV":"очі активні","bootDone":"готово","bootDoneV":"ласкаво просимо","bootRecords":"записів","bootRepos":"репозиторіїв","bootCommits":"комітів","bootDots":"точок","bootSkip":"пропустити →","termTitle":"andrii.dk — консоль","termBuilt":"версія 3.0 · зібрано","termCmds":"команди","termExit":"вихід — Esc","termNoCmd":"команди «%s» немає. спробуй","termWho":"підприємець · IT-практик · засновник · спікер","termDays":"днів у Данії","termRepos":"репозиторіїв","termPrivate":"приватних","termCommits":"комітів","termProjects":"проєктів","termCompanies":"компаній","termCommunities":"громадських організацій","termOrgs":"громадські організації","termSince":"у бізнесі з","termTop":"топ репозиторіїв за комітами:","termNothing":"нічого не знайшов. домени:","termTotal":"всього","termDetails":"деталі:","termCatalog":"каталог:","termNoSuch":"нема такого. спробуй","termStack":"стек:","termProof":"доказ:","termHire":"напиши:","termHire2":"або заповни форму нижче на сторінці. читаю сам, відповідаю зазвичай того ж дня.","termHelp":"— хто я|— список проєктів|— чим працюю|— цифри|— теми виступів|— як почати роботу|— коротка біографія|— відкрити проєкт|— очистити","timeIn":"Копенгаген","timeHere":"на місці","timeAsleep":"сплю","seeAll":"усі","restCatalog":"решта каталогу","moreProjects":"Ще %s проєктів","moreProjectsD":"Данські дата-API, SaaS, боти, платежі, скрейпінг, інфраструктура — з фільтрами за доменом і стеком.","openCatalog":"Відкрити каталог →","filterAll":"усі домени","filterAnyState":"будь-який стан","filterSearch":"пошук","filterShown":"показано","filterOf":"з","legendProd":"● у проді","legendWip":"○ в роботі / дослідження","copy":"копіювати","copied":"скопійовано","copyManual":"виділи вручну","nowUpdated":"оновлено","nowAgo":"дн. тому","nowToday":"сьогодні","commitsWord":"комітів","mailSubject":"Запрошення на подію","mailBody":"Вітаю, Андрію!\n\nПодія: \nДата: \nМісто або платформа: \nАудиторія (хто буде в залі): \nФормат і тривалість: \nТема, яка цікавить: \n\nДодатково: \n","spWho":"для кого","spLen":"тривалість","spLang":"мова"},"portfolio":{"meta":{"generated":"2026-09-29","source":"GitHub andrey-tut (88 власних репо) + claude-brain/PROJECTS.md + локальні репозиторії","note":"Факти з реальних репо і нотаток, не з назв папок."},"domains":[{"id":"ai","label":"AI та агенти","color":"#7c5cff"},{"id":"data","label":"Дані й API Данії","color":"#00d4a0"},{"id":"saas","label":"SaaS-платформи","color":"#ff5c7c"},{"id":"web","label":"Сайти й WordPress","color":"#ffb020"},{"id":"bots","label":"Боти й месенджери","color":"#4cc3ff"},{"id":"fintech","label":"Платежі й бухгалтерія","color":"#c0ff3e"},{"id":"scraping","label":"Збір даних","color":"#ff8a3d"},{"id":"infra","label":"Інфраструктура","color":"#9aa4b2"},{"id":"community","label":"Спільноти й події","color":"#ffd166"},{"id":"business","label":"Бізнес (до IT)","color":"#e0e0e0"}],"statuses":{"prod":{"label":"У проді","dot":"#00d4a0"},"building":{"label":"В роботі","dot":"#ffb020"},"research":{"label":"Дослідження","dot":"#4cc3ff"},"archive":{"label":"Архів","dot":"#6b7280"}},"projects":[{"id":"virtual_pbx","name":"AI-АТС з голосом реального часу","domain":"ai","status":"prod","year":"2026","commits":null,"url":"https://lynbro.dk/uk/products/lynbro-pbx/","urlByLang":{"en":"https://lynbro.dk/en/products/lynbro-pbx/","da":"https://lynbro.dk/da/products/lynbro-pbx/"},"one":"Телефонний робот, що говорить українською й сам кладе трубку.","full":"Zadarma → OpenAI Realtime через native SIP. Агент розмовляє живою українською, має доступ до бази знань, CRM і запису на послуги, сам завершує дзвінок. Плюс власна CRM і дашборд дзвінків.","stack":["FastAPI","OpenAI Realtime","SIP","MySQL"],"proof":"Живий прод · продукт LynBro PBX"},{"id":"claude-brain","name":"claude-brain — спільна пам'ять","domain":"ai","status":"prod","year":"2026","commits":1822,"one":"Мій AI пам'ятає всі 100+ проєктів між сесіями.","full":"Git-сховище знань про всі проєкти: нотатки, уроки, патерни, конвенції. FTS5-пошук за ~8 мс, автоінжект контексту в кожну сесію через хуки, синхрон між машинами. 1 822 коміти — найактивніший мій репозиторій.","stack":["Shell","SQLite FTS5","Node","Markdown"],"proof":"1 822 коміти · щоденне використання"},{"id":"danadresse","name":"danadresse.dk — адресний API Данії","domain":"data","status":"prod","year":"2026","commits":409,"url":"https://danadresse.dk","one":"Drop-in заміна державного DAWA — з SDK на 4 мови.","full":"Сумісний з DAWA REST API для данських адрес. Bootstrap + інкрементальна синхронізація. Навколо — екосистема: SDK для Python, JS/TS, WordPress-плагін для Woo-чекауту і CLI для автоматичної міграції коду з DAWA.","stack":["FastAPI","PostgreSQL","ETL"],"proof":"Прод + 5 публічних пакетів на GitHub"},{"id":"phone_check","name":"nummeropslag.dk — визначник номерів","domain":"data","status":"prod","year":"2026","commits":496,"url":"https://nummeropslag.dk","one":"Хто дзвонить і чи це спам — портал + iOS + Android.","full":"Данський caller-ID і спам-портал. Бекенд на власних DK-даних, збагачення через CVR та операторів. Застосунок iOS 1.2.x у App Store, Android — теж. Публічний Partner API + MCP-сервер для AI-агентів.","stack":["FastAPI","MySQL","Meilisearch","Swift","Kotlin"],"proof":"Прод + iOS та Android у сторах"},{"id":"soge","name":"Soge.dk — пошук авто і компаній","domain":"data","status":"building","year":"2026","commits":385,"url":"https://soge.dk","one":"130+ млн записів: авто (DMR), компанії (CVR), адреси.","full":"Пошуковик по данських державних реєстрах: транспорт з DMR (100+ ГБ XML), компанії з CVR, адреси. Публічний REST API + MCP-сервер, щоб AI-агенти могли ним користуватись. Документація — andrey-tut.github.io/soge-api.","stack":["Python","SQL","XML ETL","MCP"],"proof":"130M+ записів · публічний API"},{"id":"auctions_history","name":"Історія автоаукціонів США","domain":"scraping","status":"building","year":"2026","commits":469,"one":"Copart, IAAI, Manheim — каталог, VIN, фото, ставки, історія.","full":"Наступник parsing_vin: збір із Copart, IAAI, Manheim і агрегаторів. Повна історія лотів по VIN, фото, ставки, buy-now. Окремий auth-тулкіт: 4 канали логіну, mobile-API логіняться без людини. Це той самий домен, де я 4 роки вів реальний бізнес експорту авто.","stack":["Python","curl_cffi","Camoufox","Playwright","residential proxy"],"proof":"469 комітів · дослідження джерел завершено"},{"id":"renet","name":"renet.dk — нетворкінг на подіях","domain":"community","status":"building","year":"2026","commits":499,"url":"https://renet.dk","one":"Щоб на івенті знаходити потрібних людей, а не візитівки.","full":"Платформа нетворкінгу для учасників однієї події: профілі, матчинг за інтересами, повідомлення. Другий за кількістю комітів мій продукт.","stack":["Python","Web"],"proof":"499 комітів"},{"id":"brella","name":"Brella-аутріч на TechBBQ 2026","domain":"community","status":"prod","year":"2026","one":"5 290 повідомлень, ~300 відповідей — за одну конференцію.","full":"Власний інструмент для холодних інтро й ведення інбоксу на платформі Brella. Працює через ActionCable-вебсокети. Бойове використання 27.08.2026 на TechBBQ: 5 290 меседжів, близько 300 відповідей.","stack":["Python","httpx","websockets"],"proof":"5 290 меседжів · 27.08.2026"},{"id":"bkg","name":"bkg.dk — мульти-тенант booking","domain":"saas","status":"prod","year":"2026","commits":215,"url":"https://bkg.dk","one":"Онлайн-запис для сервісних бізнесів, кілька салонів на одній базі.","full":"Booking-платформа для сервісних бізнесів: майстри, послуги, розклад, нагадування. Мульти-тенант архітектура.","stack":["FastAPI","MySQL"],"proof":"215 комітів · прод"},{"id":"lynbro","name":"lynbro.dk — сайт власної IT-компанії","domain":"web","status":"prod","year":"2026","commits":27,"url":"https://lynbro.dk","one":"366 сторінок трьома мовами + AI-чат.","full":"Сайт LynBro ApS: 366 сторінок данською, англійською і українською, блог, кейси, AI-чат на n8n. Ім'я: Lyn (блискавка) + Bro (міст).","stack":["Node","n8n","i18n"],"proof":"Прод · 366 сторінок · da/en/uk"},{"id":"ferrysport","name":"ferrysport.dk — реанімація вебшопу","domain":"web","status":"prod","year":"2026","commits":110,"url":"https://ferrysport.dk","one":"56% варіацій товару були мертві. 69% — без опису. Полагодив.","full":"Данський вебшоп профодягу. Розшифрував паперовий аудит на 30 сторінок (da/en/uk), склав план на 8 етапів. Зробив: спам-реєстрації 92→16, тонкі сторінки 67→15, логотип 65→41 КБ, Google-шрифти геть (7→0 звернень), розсилка нарешті працює. Клієнт бачить прогрес у власній панелі.","stack":["WordPress","WooCommerce","Python"],"proof":"Живий клієнтський проєкт з вересня 2026"},{"id":"lynbro-projektportal","name":"Портал «замовник ↔ виконавець»","domain":"saas","status":"prod","year":"2026","one":"Клієнт тикає в місце на своєму сайті й пише, що не так.","full":"Власний продукт: портал прогресу всередині сайту клієнта. Коментар із вибором області прямо на сторінці, облік часу, пояснення «навіщо» мовою клієнта, порівняння «було/стало». v1.0 у бою на ferrysport.dk.","stack":["Python","JS"],"proof":"v1.0 у бою на реальному клієнті"},{"id":"plugins_wp","name":"plugins.lynbro.dk — студія WP-плагінів","domain":"web","status":"prod","year":"2026","commits":266,"url":"https://plugins.lynbro.dk","one":"Власні плагіни WordPress для данського ринку.","full":"Портал і вихідники плагінів: пошук по данських реєстрах прямо в WP, безкоштовний багатомовний GDPR/ePrivacy cookie-банер, адресний автокомпліт для WooCommerce. Переклади йдуть через translate.wordpress.org.","stack":["PHP","WordPress","FastAPI"],"proof":"3 публічні плагіни · переклади в WP.org"},{"id":"test_mobilepay","name":"MobilePay / Vipps — платіжний харнес","domain":"fintech","status":"prod","year":"2026","one":"Розібрав API головної платіжної системи Данії до кінця.","full":"Дослідницький харнес Vipps/MobilePay: власні й клієнтські модулі, e2e-прогін 09.06.2026, FastAPI + PHP-міст, перевірка HMAC-вебхуків. Recurring і Report чекають прод. Результат — готова шпаргалка по API.","stack":["FastAPI","PHP","HMAC webhooks"],"proof":"e2e-прогін пройдено"},{"id":"skat_moms","name":"Moms API — ПДВ-декларації в Skat","domain":"fintech","status":"building","year":"2026","commits":6,"one":"SOAP-клієнт данської податкової. WS-Security з нуля на Python.","full":"NemVirksomhed / RSU B2B: подача ПДВ-декларацій у Skattestyrelsen. Написав WS-Security з нуля, бо готового під Python не було. Тест живий, прод чекає OCES3-сертифікат.","stack":["Python","lxml","WS-Security","SOAP"],"proof":"Тестове середовище — живе"},{"id":"regnskabhub","name":"RegnskabHub — AI-бухгалтер","domain":"fintech","status":"building","year":"2026","one":"Один API поверх усіх данських бухсистем.","full":"Уніфікований API данських бухгалтерських систем плюс AI-бухгалтер: мульти-тенант, плани й метеринг, 3 read-адаптери, sync-кеш, детект аномалій, збагачення через CVR. 54 тести, 20+ ендпоінтів.","stack":["FastAPI","PostgreSQL","LLM"],"proof":"54 тести · 20+ ендпоінтів"},{"id":"dinero","name":"Інтеграція Dinero (Visma)","domain":"fintech","status":"building","year":"2026","commits":5,"one":"Змаплено 148 ендпоінтів данської бухгалтерії.","full":"Публічна інтеграція Dinero для LynBro: OAuth + персональна авторизація, 148 ендпоінтів змаплено, e2e-читання на живій Pro-організації. Провайдер-агностичний шар, щоб поруч став e-conomic.","stack":["Python","OAuth"],"proof":"148 ендпоінтів · e2e на живій орг."},{"id":"rykker","name":"rykkerbot.dk — нагадування про оплату","domain":"fintech","status":"research","year":"2026","one":"Автоматичні рykker'и за данським renteloven.","full":"Rykker-as-a-Service: автоматизація нагадувань про оплату за данським законом. Сегментовані ставки, збори за §9b, машина станів, ескалація в інкасо. Данський лендінг із живим калькулятором.","stack":["FastAPI","Python 3.12"],"proof":"Ядро + 37 тестів"},{"id":"jakun","name":"JaKun — CRM з метаданих","domain":"saas","status":"building","year":"2026","commits":55,"one":"Схема БД, API і права породжуються з метаданих.","full":"CRM-платформа: замість писати таблиці й ендпоінти руками — описуєш метадані, з них генерується DDL, API і матриця прав. Мульти-тенант SaaS + self-host у Docker. Є MCP-сервер, щоб AI-агент працював з CRM напряму. 100 Python-файлів.","stack":["FastAPI","PostgreSQL","React/TS","MCP"],"proof":"100 файлів ядра · MCP-сервер"},{"id":"jadel","name":"JaDel — кроспостинг у соцмережі","domain":"saas","status":"research","year":"2026","commits":29,"one":"Аналог Postiz/Mixpost. Спершу — повний реверс ринку.","full":"Планування, AI-генерація і публікація в соцмережі. Перед кодом — розбір Postiz (AGPL, 34.5k зірок) і Mixpost, матриця API 20+ мереж, болі з issues і відгуків. Головний висновок: перевага не в коді, а в пройдених платформних аудитах.","stack":["FastAPI","TypeScript"],"proof":"Повне дослідження ринку 08.2026"},{"id":"imin","name":"imin.dk — білдер сайтів","domain":"saas","status":"building","year":"2026","commits":148,"url":"https://imin.dk","one":"Візуальний редактор, домени, модулі-доповнення.","full":"Мульти-тенант SaaS-конструктор сайтів: візуальний редактор, підключення доменів, add-on модулі. Ідея плагінної архітектури, яку потім узяв в інші продукти.","stack":["FastAPI","MySQL","Stripe"],"proof":"148 комітів"},{"id":"gyk","name":"gyk.dk — скорочувач посилань","domain":"saas","status":"building","year":"2026","commits":153,"url":"https://gyk.dk","one":"Короткі лінки + QR + статистика кліків.","full":"Свій bit.ly: короткі посилання, генерація QR, аналітика кліків.","stack":["FastAPI","MariaDB"],"proof":"153 коміти"},{"id":"ommig","name":"ommig.dk — bio-сторінки","domain":"saas","status":"prod","year":"2026","commits":146,"url":"https://ommig.dk","one":"Linktree для Данії, з повагою до приватності.","full":"Персональні bio-сторінки на ommig.dk/username. Акцент на приватності — без трекерів третіх сторін.","stack":["FastAPI async"],"proof":"146 комітів · прод"},{"id":"srvc","name":"srvc.dk — маркетплейс послуг","domain":"saas","status":"building","year":"2026","commits":36,"one":"Escrow, спори, виплати, договори.","full":"Сервісний маркетплейс із повним фінансовим циклом: депонування коштів, механізм спорів, виплати виконавцям, контракти.","stack":["FastAPI","Alembic"],"proof":"36 комітів"},{"id":"jajob","name":"jajob.dk — AI-first джоб-платформа","domain":"saas","status":"building","year":"2026","commits":59,"one":"Вакансії, резюме, ATS і матчинг — багатомовним вектор-пошуком.","full":"Платформа працевлаштування для Данії: вакансії, резюме, ATS, матчинг кандидатів. Пошук — векторний і багатомовний, бо ринок DK тримовний.","stack":["FastAPI","MySQL","vector search"],"proof":"59 комітів"},{"id":"jatid","name":"jatid.dk — облік робочого часу","domain":"saas","status":"building","year":"2026","url":"https://jatid.dk","one":"Модульний контроль часу для бізнесу.","full":"Платформа обліку робочого часу: зміни, проєкти, звіти. Модульна архітектура під різні типи бізнесу.","stack":["FastAPI","Alembic"],"proof":"Розробка"},{"id":"games","name":"Party Game OS","domain":"saas","status":"building","year":"2026","commits":55,"one":"Jackbox по-своєму: телевізор — екран, телефони — джойстики.","full":"Платформа паті-ігор: спільний екран на ТБ, телефони гостей як контролери. 8 ігор, озвучка через Piper TTS.","stack":["FastAPI","vanilla JS","SQLite","Piper TTS"],"proof":"8 ігор · 55 комітів"},{"id":"video_editor","name":"AI-відео пайплайн без платних SaaS","domain":"ai","status":"building","year":"2026","one":"Повний відеоцех локально на Apple Silicon.","full":"Дослідив 5 кластерів open-source: авто-нарізка, реframe у вертикаль, субтитри, клон голосу, дубляж, аватар/генерація. Оркестратор — Claude Code зі скілами й MCP. Pipeline A v0.1 зібрано.","stack":["ffmpeg","Whisper","MLX","Remotion"],"proof":"5-кластерний ресерч + робочий пайплайн"},{"id":"localmind","name":"LocalMind — LLM-чат на iPhone","domain":"ai","status":"building","year":"2026","one":"Модель крутиться на телефоні, не в чужій хмарі.","full":"iOS-застосунок для локальних мовних моделей. Нативний Swift.","stack":["Swift","SwiftUI"],"proof":"Розробка"},{"id":"cbot","name":"cbot.dk — розмовний лідген","domain":"bots","status":"building","year":"2026","commits":57,"url":"https://cbot.dk","one":"Бот ловить ліда й одразу кладе в CRM.","full":"Conversational lead-gen: бот веде розмову, кваліфікує, передає в CRM. Веб, бекенд і мобільна частина.","stack":["Python","JS","mobile"],"proof":"57 комітів"},{"id":"whatsapp_privat","name":"WhatsApp → канонічна база → AI","domain":"bots","status":"building","year":"2026","commits":30,"one":"Роки переписки перетворені на базу, з якою говорить AI.","full":"Збір власних WhatsApp-акаунтів у єдину канонічну базу з подальшим AI-доступом. Архітектура одразу SaaS-ready.","stack":["FastAPI","WAHA"],"proof":"30 комітів"},{"id":"uaeuhub_bots","name":"Боти українських спільнот","domain":"bots","status":"prod","year":"2023–2026","one":"Модерація, розсилки й анти-спам для груп на тисячі людей.","full":"Сімейство Telegram-ботів для українських спільнот у Європі: модерація, антиспам, розсилки, AI-відповіді. П'ять ітерацій одного бота — чесно, їх треба звести в один.","stack":["Python","aiogram","OpenAI"],"proof":"Живі групи · ~300 користувачів у Mini App"},{"id":"padel","name":"Розсадка ігор Padel Copenhagen","domain":"bots","status":"prod","year":"2026","one":"Опитування в групі → готовий ростер і пари.","full":"Читає опитування в Telegram-групі падел-спільноти Копенгагена і складає ростер та розсадку по кортах.","stack":["Python","Telethon"],"proof":"Використовується спільнотою"},{"id":"sms_gateway","name":"SMS-шлюз","domain":"infra","status":"prod","year":"2026","commits":164,"one":"Спільний канал сповіщень для всіх моїх продуктів.","full":"Власний SMS-шлюз, який використовують інші проєкти як сервіс нотифікацій.","stack":["Python"],"proof":"164 коміти · additive-prod"},{"id":"aapanel","name":"Керування aaPanel-серверами","domain":"infra","status":"prod","year":"2026","commits":147,"one":"Мої VPS керуються кодом, а не кліками в панелі.","full":"Автоматизація aaPanel: сайти, БД, SSL, деплой. Окремо розв'язав болючу історію з поштовим сервером — SSL через DNS-01 у Simply.com і Postfix SNI.","stack":["Python","nginx","MySQL"],"proof":"147 комітів · усі мої сервери"},{"id":"vps_monitor","name":"Claude як черговий SRE","domain":"infra","status":"research","year":"2026","one":"AI дивиться за серверами і сам лагодить дрібне.","full":"Моніторинг VPS і проєктів із звітами в Telegram та на пошту, автоматичні безпечні дії плюс запит підтвердження на ризикові.","stack":["Claude Code","Python"],"proof":"Стартер-кіт"},{"id":"push_projects","name":"Двигун просування проєктів","domain":"infra","status":"building","year":"2026","one":"Автоматична подача моїх продуктів у каталоги й форуми.","full":"Система просування: каталоги, API-каталоги, GitHub, форуми, email. Перша хвиля — danadresse, phone_check, soge.","stack":["Python"],"proof":"Хвиля 1 запущена"},{"id":"ideamine","name":"ideamine — радар ідей і болей","domain":"ai","status":"building","year":"2026","one":"24 джерела скарг користувачів → мій щоденний дайджест.","full":"Радар продуктових сигналів: HN, feature-requests конкурентів на GitHub, відгуки 1–3★ в App Store DK, відгуки на WP-плагіни, RSS, данські медіа. Дедуплікація, скоринг під мій профіль, український дайджест і матчинг сигналів під поточні проєкти.","stack":["Python","LLM"],"proof":"24 zero-auth джерела"},{"id":"freelancehunt","name":"Обхід Cloudflare для freelancehunt","domain":"scraping","status":"prod","year":"2026","one":"API там, де є. Браузер там, де API немає.","full":"Автологін і читання акаунта: API v2 як основний канал, nodriver-браузер для того, чого в API немає — включно з обходом Cloudflare.","stack":["Python","httpx","nodriver"],"proof":"Працює"},{"id":"virkdata","name":"Скрейп данських бізнес-даних","domain":"scraping","status":"building","year":"2026","one":"CVR і virk — у власну базу.","full":"Збір данських бізнес-даних з virk/CVR через ротацію проксі. Живить збагачення в інших продуктах.","stack":["Python","proxy"],"proof":"Живить інші проєкти"},{"id":"sifjakobs","name":"Скрейпер Shopify-каталогу","domain":"scraping","status":"prod","year":"2026","one":"Каталог + AI-fallback, коли розмітка змінюється.","full":"Скрейпер повного Shopify-каталогу з ротацією проксі та AI-fallback на випадок зміни верстки.","stack":["Python","uv"],"proof":"Робочий"},{"id":"offline_cph","name":"Дослідження офлайн-ніш Копенгагена","domain":"business","status":"research","year":"2026","one":"Куди вкласти 150 000 DKK у реальний бізнес.","full":"Системний розбір ніш офлайн-бізнесу в Копенгагені під бюджет 150k DKK. Топ-кандидати: skin-studio, кераміка, велосервіс.","stack":["Research"],"proof":"Повний RESEARCH.md"},{"id":"make_money","name":"Система отримання замовлень","domain":"business","status":"research","year":"2026","commits":33,"one":"11 агентів розібрали данський ринок фрілансу.","full":"Дослідження каналів замовлень у Данії: брокери беруть 1400–1750 DKK/год, ApS — ключ доступу до корпоративних замовників, біддинг-майданчики мертві, email-агрегація — легальний канал.","stack":["Research","FastAPI"],"proof":"Дослідження 11 агентів"},{"id":"mitid","name":"Дослідження MitID-брокерів","domain":"data","status":"research","year":"2026","one":"Як легально логінити данців через державний eID.","full":"Розбір брокерів MitID для входу через данський eID. Рекомендація: Criipto/Idura — €67/міс + €0.037 за автентифікацію, OIDC, пісочниця без картки.","stack":["OIDC","Research"],"proof":"Рішення прийнято"},{"id":"carsexport","name":"Cars Export LLC","domain":"business","status":"archive","year":"2018–2022","one":"Експорт авто зі США і Китаю. ×10 за три роки.","full":"CEO. Закупівля авто на Copart, IAAI, Manheim, ADESA, морська логістика, брокераж, митниця. Маршрути UA/GE/USA/EU/CN. Зростання понад 1000% за три роки, мільйонні обороти на місяць. Саме тут я вивчив, як насправді працюють контракти, ризик і гроші.","stack":["Логістика","Митниця","Міжнародна торгівля"],"proof":"4 роки · +1000% за 3 роки"},{"id":"lintek","name":"Lintek","domain":"business","status":"archive","year":"2008–2012","one":"CEO у 25. Кабельна продукція й інтернет-дистрибуція, Харків.","full":"Кабельна продукція, інтернет-індустрія, дистрибуція. Перший досвід керування компанією.","stack":["Дистрибуція","Продажі"],"proof":"4 роки"},{"id":"ltv","name":"LTV — міжнародні закупівлі","domain":"business","status":"archive","year":"2013–2017","one":"Велосипеди і запчастини. Закупівлі через півсвіту.","full":"Менеджер із зовнішньоекономічних закупівель: велосипеди й запчастини, міжнародна торгівля, постачальники з Азії та Європи.","stack":["ЗЕД","Закупівлі"],"proof":"5 років"},{"id":"bestchoice","name":"Best Choice","domain":"business","status":"prod","year":"2022–","url":"https://best-choice.dk","one":"З чого почалась моя IT-практика в Данії. CVR 44079844.","full":"IT-послуги: автоматизація, AI-інструменти, CRM, Telegram-боти, SEO, хостинг. Lead Consultant & Solution Architect.","stack":["Консалтинг","Розробка","CVR 44079844"],"proof":"Діюча практика з 2022"},{"id":"lynbro_aps","name":"LynBro ApS","domain":"business","status":"prod","year":"2025–","url":"https://lynbro.dk","one":"Данська IT-компанія. CVR 46321499.","full":"Lyn — блискавка, швидкість. Bro — міст. Сайти, AI-агенти, автоматизація. Зареєстрована в Rødovre.","stack":["ApS","CVR 46321499"],"proof":"Діюча компанія"},{"id":"aktive","name":"Aktive Ukrainere","domain":"community","status":"prod","year":"2023–","url":"https://aktive-ukrainere.dk","one":"Громадська організація українців у Данії (frivillig forening). CVR 43922939.","full":"Засновник і організатор некомерційного об'єднання: допомога, волонтерство, зустрічі, AI-воркшопи й події для українців у Данії.","stack":["Frivillig forening","CVR 43922939"],"proof":"Регулярні події з 2023"},{"id":"uahub","name":"UaHub","domain":"community","status":"prod","year":"2025–","url":"https://uahub.dk","one":"Об'єднання українських підприємців і бізнесів (frivillig forening). CVR 45599841.","full":"Засновник некомерційного об'єднання. Воркшопи, форуми, нетворкінг і каталог українських бізнесів на uahub.dk/katalog.","stack":["Frivillig forening","CVR 45599841"],"proof":"Діюче об'єднання · каталог бізнесів"},{"id":"oksana","name":"oksana.dk — сайт PMU-студії","domain":"web","status":"building","year":"2026","commits":39,"url":"https://oksana.dk","one":"Редизайн сайту б'юті-студії.","full":"Повний редизайн сайту студії перманентного макіяжу: WordPress + Node-частина.","stack":["WordPress","Node","PHP"],"proof":"39 комітів"},{"id":"msdf","name":"msdf.dk — збірка меблів","domain":"web","status":"building","year":"2026","url":"https://msdf.dk","one":"Вийти в №1 по збірці меблів у Копенгагені.","full":"Сервіс збірки й монтажу меблів у Storkøbenhavn. SEO + лідген, прозора фікс-ціна з калькулятором, програмне SEO «меблі × локація», скрейп каталогів JYSK та IKEA.","stack":["WordPress","pSEO"],"proof":"У роботі"},{"id":"nailshub","name":"nailshub.dk — автоматизація салону","domain":"web","status":"prod","year":"2026","commits":17,"url":"https://nailshub.dk","one":"Салон, який більше не відповідає вручну на записи.","full":"Підтримка й автоматизація салону на WordPress + Amelia: онлайн-запис, підтвердження через WhatsApp Cloud API, автозвіти з AI-аудитом, міграція зі старої системи.","stack":["WordPress","Amelia","n8n","WhatsApp API"],"proof":"Прод · живий салон"},{"id":"zhanna","name":"zhanna.dk — сайт майстрині й коуча","domain":"web","status":"prod","year":"2026","url":"https://zhanna.dk","one":"Особистий бренд nail-артистки: п'ять мов, запис через NailsHub.","full":"Клієнтський сайт Best Choice для Жанни Осауленко — nail-артистки, коуча й співвласниці NailsHub. П'ять мов (UK/EN/DA/PL/RU), світла й темна тема, запис на послуги веде в салон NailsHub.","stack":["Web","i18n"],"proof":"Живий клієнтський сайт"},{"id":"bilafgift","name":"Калькулятор реєстраційного податку на авто","domain":"data","status":"building","year":"2026","one":"Скільки коштує пригнати авто в Данію. PWA без залежностей.","full":"Калькулятор данського registreringsafgift. Чистий HTML/CSS/JS, працює офлайн.","stack":["vanilla JS","PWA"],"proof":"Робочий"},{"id":"speed_cam","name":"Радар швидкості з відео","domain":"ai","status":"research","year":"2026","one":"Телефон вимірює швидкість авто по відео.","full":"Вимірювання швидкості об'єктів із відео: YOLO + трекінг + розпізнавання номерів, масштаб рахується по номерному знаку чи колісній базі. Звіт-«доказ», рейтинги, шеринг. Позиціювання — універсальний інструмент, не лише для контролю.","stack":["YOLO","iOS/Android","FastAPI","GPU"],"proof":"Дослідження завершено"},{"id":"runners","name":"Біговий застосунок із захопленням територій","domain":"saas","status":"research","year":"2026","one":"Біжиш — захоплюєш райони міста.","full":"Гейміфікований біговий застосунок: захоплення територій, ліга, веб-частина. Стартовий ринок — Копенгаген і Скандинавія.","stack":["greenfield"],"proof":"Концепція"},{"id":"looi","name":"LOOI Robot BLE toolkit","domain":"ai","status":"prod","year":"2025","commits":4,"url":"https://github.com/andrey-tut/LOOI-Robot","one":"Розібрав BLE-протокол робота. 12 зірок на GitHub.","full":"Неофіційний тулкіт для робота LOOI: керування з клавіатури, декодування сенсорів, телеметрія, роадмап Flutter-компаньйона. Публічний, з реальними зірками від незнайомих людей.","stack":["Python","BLE","reverse engineering"],"proof":"12 зірок · публічний репозиторій"},{"id":"azbyg","name":"Архів переписки як доказ робіт","domain":"business","status":"building","year":"2026","url":"https://azbyg.dk","one":"Робочий чат у Telegram → юридичний доказ виконаних робіт.","full":"Архів переписки робочого чату для замовника (об'єкт «Отель Hornbek», AZ Byg ApS): Telegram-подібний веб-в'ювер, PDF, переклад DA/EN/UA, зведення по днях із годинами бригад.","stack":["Python","Web","PDF"],"proof":"В'ювер готовий"},{"id":"hub_pools","name":"Автоматизація TEUH","domain":"community","status":"prod","year":"2026","one":"Реєстрація, опитування й матеріали — без ручної роботи.","full":"Автоматизація платформи TEUH: реєстрація учасників, опитування, роздача матеріалів із Google Drive.","stack":["Playwright","httpx"],"proof":"Робоча автоматизація"},{"id":"job_search","name":"Системний пошук роботи як інженерна задача","domain":"business","status":"prod","year":"2026","one":"Не розсилка резюме, а керований пайплайн.","full":"Пошук роботи в Hvidovre/Rødovre зведено до системи: трекер у таблиці, автоматичні чернетки в Gmail, джерела з CVR. Не-IT задача, розв'язана IT-методом.","stack":["Python","Gmail API","CVR"],"proof":"Працює"}]},"stats":{"asOf":"2026-09-14","repos":88,"reposPrivate":75,"commits":6119,"projects":62,"companies":2,"communities":2,"inDenmarkSince":"2022-04-01","businessSince":1996,"topRepos":[["claude-brain",1822],["renet.dk",499],["phone_check",496],["auctions_history",469],["danadresse",409],["soge",385]]},"faq":[{"q":"Чим ти займаєшся, якщо коротко?","a":"Будую системи для бізнесу: сайти, автоматизацію, AI-агентів, CRM, API та дата-системи. Плюс консультую щодо операцій і присутності онлайн. Роблю це через власну IT-компанію LynBro ApS і практику Best Choice."},{"q":"Ти розробник чи підприємець?","a":"Обидва, і в такому порядку: спершу двадцять років бізнесу, потім код. Це головна різниця. Я не питаю «яку фічу зробити» — я питаю, де в тебе тече маржа й чи варто це взагалі автоматизувати. Часто найкраща відповідь — не будувати нічого."},{"q":"Скільки коштує зробити сайт або автоматизацію?","a":"Чесно: до розмови — не знаю. Діапазон надто широкий: лендінг на тиждень і мульти-тенант платформа на пів року — це різні порядки. Перша розмова безкоштовна, і після неї ти отримаєш або вилку з обґрунтуванням, або пораду не робити цього."},{"q":"Працюєш тільки в Данії?","a":"Живу в Данії, працюю звідусіль. Данський ринок знаю глибше за інші: CVR, MitID, MobilePay, DAWA, Skat, e-conomic і Dinero — це я вже проходив і маю готові інтеграції."},{"q":"Якими мовами розмовляєш?","a":"Українською і російською вільно, англійською робоче, данська — в процесі, розумію більше, ніж говорю. Виступати можу українською або англійською."},{"q":"Ти справді зробив 62 проєкти?","a":"Задокументованих — 62, і це лише те, що я вважаю вартим показу. Репозиторіїв 88, комітів 6 119. Частина в проді й приносить гроші, частина в роботі, частина — чесно, дослідження, яке не пішло далі. Я показую й такі: не пішло — теж результат."},{"q":"Як виглядає робота з тобою?","a":"Спершу розмова, де більше говориш ти. Потім коротке дослідження: що вже є на ринку, що можна не писати. Потім найкоротший шлях до перевірки ідеї, а не красивий макет. Потім — у прод, з логами й моніторингом. Ти бачиш прогрес у власній панелі, а не в листуванні."},{"q":"Ти береш маленькі задачі?","a":"Так, якщо вони справді маленькі. Полагодити форму, налаштувати Google-картку, під'єднати онлайн-запис — це нормально. Гірше, коли «маленька задача» насправді велика, а бюджет розрахований на маленьку — про це я скажу одразу."},{"q":"Що потрібно, щоб запросити тебе на виступ?","a":"Дата, місто або онлайн, аудиторія і скільки маю часу. Регулярно виступаю в Копенгагені та онлайн. Формати: доповідь 20–45 хвилин, панель, воркшоп на 2 години з ноутбуками. Гонорар обговорюється; для українських спільнот і волонтерських подій — безкоштовно."},{"q":"Чому сайт так виглядає?","a":"Бо це теж портфоліо. Тривимірне поле на фоні — це 6 119 частинок, по одній на кожен мій коміт. Міст будується з кадрів згенерованого відео, які прокручуються скролом. Очі на портреті стежать за курсором. Усе написано з нуля, без конструкторів і бібліотек анімації. Натисни «/» — там ще консоль."}],"speaking":{"formats":[{"t":"Доповідь","m":"20–45 хв","d":"Класичний слот на конференції або форумі. Кейси, цифри, висновки. Лишаю 10 хвилин на питання — зазвичай їх більше."},{"t":"Воркшоп","m":"2–3 години","d":"З ноутбуками. Кожен учасник іде не з конспектом, а з чимось працюючим: налаштованою карткою в Google, готовим ботом або першою автоматизацією."},{"t":"Панель / круглий стіл","m":"за програмою","d":"Модерація або участь. Добре працюю там, де треба говорити конкретно, а не загальними словами."},{"t":"Онлайн","m":"будь-який","d":"Zoom, Teams, Meet, стрім. Маю нормальний мікрофон, світло і резервний канал — технічних сюрпризів не буде."}],"topics":[{"t":"Клієнт уже шукає вас. Чому обирає не вас?","d":"Найпопулярніша тема. Розбираю 12 точок, у яких малий бізнес втрачає клієнта ще до першого контакту: картка в Google, відгуки, швидкість сайту, зрозумілість пропозиції, спосіб зв'язку, відповідь у неробочий час. Не теорія — на живих прикладах данських і українських бізнесів.","take":["Чек-лист на 12 пунктів, який можна пройти того ж вечора","Що коштує нуль грошей і дає ефект уже цього тижня","Як виміряти, чи справді стало краще"],"aud":"Малий бізнес, майстрині, експертки, початківці","len":"30–45 хв · або воркшоп 2 год","lang":"UK / EN"},{"t":"AI без магії: що реально працює в малому бізнесі","d":"Без «AI змінить усе». Конкретно: які задачі автоматизація закриває сьогодні, скільки це коштує на місяць, де AI дорожчий за людину і які помилки я зробив сам. Показую власні системи — телефонного агента, який говорить українською, і базу знань, що пам'ятає 100 проєктів.","take":["Список задач, які варто автоматизувати першими","Реальні цифри вартості, не маркетингові","Три способи втратити гроші на AI — і як їх обійти"],"aud":"Власники бізнесу, менеджери, IT-спільноти","len":"30–45 хв","lang":"UK / EN"},{"t":"Почати бізнес у Данії, коли ти тут нікого не знаєш","d":"Мій шлях з 2022 року без прикрас: CVR і ApS, податки, NemKonto, MitID, перші клієнти, чому данський нетворкінг працює інакше і що я зробив би по-іншому. Тема, яку просять найчастіше після виступів для української аудиторії.","take":["Порядок дій на перші три місяці","Скільки насправді коштує ApS і коли вона потрібна","Де шукати перших клієнтів, якщо в тебе нуль контактів"],"aud":"Українці в Данії, релоканти, початківці","len":"30–60 хв","lang":"UK"},{"t":"Від експорту авто до державних API","d":"Історія про те, як досвід у «важкому» офлайн-бізнесі — митниця, контракти, морський фрахт — стає перевагою в цифрі. Чому людина, яка возила автомобілі через океан, інакше проєктує системи. Для тих, хто змінює галузь і думає, що досвід втрачено.","take":["Що з офлайн-досвіду переноситься в IT один в один","Як не почати кар'єру з нуля у 40","Чому галузеві знання дорожчі за мову програмування"],"aud":"Конференції, спільноти, кар'єрні події","len":"20–40 хв","lang":"UK / EN"},{"t":"Воркшоп: твій AI-помічник за 2 години","d":"Практика. Кожен учасник за дві години збирає власного помічника під свою задачу — відповіді клієнтам, обробка заявок, підготовка контенту. Працюємо на безкоштовних інструментах, щоб після воркшопу нічого не зламалось через передплату.","take":["Працюючий помічник на власних даних","Розуміння, де проходить межа можливостей","Що робити далі самостійно"],"aud":"До 25 осіб з ноутбуками","len":"2–3 години","lang":"UK / EN"}],"past":[{"y":"2026","e":"Woman's Day Copenhagen","d":"Блок Beauty Business — присутність малого бізнесу онлайн"},{"y":"2026","e":"TechBBQ","d":"Нетворкінг-кампанія: 5 290 повідомлень, ~300 розмов"},{"y":"2026","e":"Бізнес і податки в Данії","d":"Круглий стіл для українських підприємців"},{"y":"2025","e":"EU Funding & Mentorship","d":"Zoku Copenhagen"},{"y":"2025","e":"Blockchain Impact Forum","d":"Копенгаген"},{"y":"2023–","e":"AI-воркшопи Aktive Ukrainere","d":"Регулярні практичні зустрічі, які я організовую сам"}],"bioShort":"Андрій Линник — український підприємець та IT-практик у Данії. Засновник IT-компанії LynBro ApS і двох громадських організацій — Aktive Ukrainere та UaHub. У бізнесі з 1996 року: від кабельної дистрибуції та експорту авто з американських аукціонів до AI-агентів і державних дата-API. 62 задокументовані проєкти, 88 репозиторіїв.","bioOneLine":"Андрій Линник — підприємець, IT-практик і засновник LynBro ApS (Данія). Будує системи, які працюють без нього."},"now":{"updated":"2026-09-14","intro":"Ця сторінка — про те, чим я реально зайнятий зараз, а не про те, що вмію взагалі. Оновлюю її, коли змінюються пріоритети. Ідея запозичена з nownownow.com.","blocks":[{"t":"Над чим працюю","items":[{"n":"ferrysport.dk","d":"Реанімація данського вебшопу профодягу. Восьмиетапний план, клієнт бачить прогрес у власній панелі. Зроблено: спам-реєстрації 92→16, мертві варіації товарів, швидкість, розсилка. Далі — оплати онлайн і описи товарів."},{"n":"RegnskabHub","d":"Уніфікований API данських бухгалтерських систем плюс AI-бухгалтер. 54 тести, 20+ ендпоінтів, три read-адаптери. Найамбітніший з поточних."},{"n":"ideamine","d":"Радар продуктових ідей: 24 джерела скарг і запитів користувачів, дедуп, скоринг під мій профіль, щоденний дайджест українською."},{"n":"soge.dk і danadresse.dk","d":"Данські дата-API: транспорт, компанії, адреси. 130+ млн записів. Публічні API та MCP-сервери, щоб AI-агенти могли ними користуватись."},{"n":"andrii.dk","d":"Оцей сайт. Переписаний з нуля у вересні 2026: WebGL-поле з 6 119 частинок, скрол-скрабоване відео, каталог на 62 проєкти."}]},{"t":"Що вивчаю","items":[{"n":"Голосові AI-агенти в реальному часі","d":"Після запуску AI-АТС копаю глибше: переривання, затримки, поведінка в шумі, коли агент має здатися й покликати людину."},{"n":"Данська","d":"Читаю вільніше, ніж говорю. Мета на рік — провести виступ данською без переходу на англійську."},{"n":"Локальні моделі на Apple Silicon","d":"Скільки корисного можна зробити без хмари: MLX, квантизація, межа якості."}]},{"t":"Чого шукаю","items":[{"n":"Один-два клієнтські проєкти на Q4","d":"Найкраще заходять задачі, де є реальні операції: облік, логістика, запис клієнтів, дані. Не люблю «зробіть нам красиво»."},{"n":"Виступи й воркшопи","d":"Особливо там, де в залі власники малого бізнесу, а не колеги-розробники."},{"n":"Партнерів по данських дата-продуктах","d":"Якщо ти працюєш з CVR, DMR, адресами чи телефонією в Данії — напиши, ймовірно нам є що обміняти."}]},{"t":"Поза роботою","items":[{"n":"Двоє дітей","d":"Головний проєкт, дедлайн — усе життя."},{"n":"Теніс, сквош, падел","d":"Падел-спільнота Копенгагена; ростери на ігри складає мій же бот."},{"n":"3D-друк і лазерна гравіювальна машина","d":"Гараж як другий сервер. Якщо річ можна розібрати — я її розберу."}]}]},"checklist":{"slug":"cheklist","title":"12 точок, де малий бізнес втрачає клієнтів онлайн","subtitle":"Робочий чек-лист. Пройти можна за вечір, більшість пунктів коштують нуль грошей.","intro":"Це не теорія з курсу. Це те, що я бачу в кожному другому бізнесі, який до мене приходить — у Данії й не тільки. Клієнт уже шукає тебе. Питання лише в тому, на якому кроці він іде до когось іншого.\n\nПроходь згори вниз. Біля кожного пункту є перевірка на дві хвилини — роби її одразу, не відкладаючи. Якщо пункт зелений, іди далі. Якщо ні — там же написано, що робити.","howto":"Постав собі чесну оцінку по кожній точці: ✓ працює · ~ наполовину · ✗ немає. Порахуй хрестики. Сім і більше — ти втрачаєш більшість тих, хто тебе вже шукав.","points":[{"n":1,"t":"Тебе немає на Google Картах","sym":"Людина шукає «[твоя послуга] + місто», бачить конкурентів у картках згори, тебе — ніде.","check":"Відкрий google.com у режимі інкогніто й вбий назву свого бізнесу. Потім — послугу та місто. Бачиш себе в перших трьох картках?","fix":"Заведи Google Business Profile і підтверди його листівкою або відео. Це безкоштовно і дає найбільший приріст дзвінків серед усього, що є в цьому списку.","cost":"0 kr · 30 хв + 2 тижні на підтвердження"},{"n":2,"t":"Картка є, але порожня","sym":"Категорія не та, годин роботи немає, фото — одне й розмите. Google показує таку картку нижче.","check":"Відкрий свою картку й порахуй: категорія, години, телефон, сайт, район обслуговування, опис, 10+ фото. Скільки з семи заповнено?","fix":"Заповни все. Фото — не стокові: робоче місце, результат роботи, ти сам. Google віддає перевагу карткам, які оновлюються.","cost":"0 kr · 1 година"},{"n":3,"t":"Нема відгуків — або вони без відповіді","sym":"Нуль відгуків читається як «нуль клієнтів». Відгук без відповіді — як «власнику байдуже».","check":"Скільки в тебе відгуків і на скільки з них ти відповів? На негативні теж?","fix":"Попроси десятьох задоволених клієнтів — особисто, з прямим посиланням. Відповідай на кожен відгук, у тому числі поганий: майбутній клієнт читає саме твою відповідь, а не скаргу.","cost":"0 kr · 15 хв на тиждень постійно"},{"n":4,"t":"Сайт відкривається довше трьох секунд","sym":"Половина людей з телефона йде раніше, ніж побачить перший екран. Ти платиш за рекламу, яка не доходить.","check":"pagespeed.web.dev, вставити адресу, дивитись мобільну вкладку. LCP більше 2,5 секунди — червона зона.","fix":"Найчастіші винуватці: важкі фото без стиснення, десяток плагінів, шрифти з чужого домену, слайдер на головній. Прибери слайдер, стисни фото, вимкни те, чим не користуєшся.","cost":"0–2 000 kr · залежно від того, наскільки все запущено"},{"n":5,"t":"З телефона сайтом користуватись незручно","sym":"Текст дрібний, кнопки не потрапляють під палець, меню ховається, форма не влазить в екран.","check":"Відкрий свій сайт на своєму ж телефоні й спробуй зробити те, чого ти хочеш від клієнта — записатись, замовити, подзвонити. Скільки дотиків це зайняло?","fix":"Більше трьох дотиків до цілі — вже забагато. Головна кнопка має бути на першому екрані й не зникати при прокрутці.","cost":"0 kr на перевірку · далі залежить"},{"n":6,"t":"За пʼять секунд незрозуміло, що ти продаєш","sym":"На першому екрані «Ласкаво просимо» і красива картинка. Що саме, для кого і скільки коштує — десь нижче або ніде.","check":"Покажи першого екрана людині, яка не знає твого бізнесу. Спитай: що тут продають і кому? Не вгадала за пʼять секунд — переписуй.","fix":"Формула без прикрас: що ти робиш + для кого + у якому місті + що зробити далі. «Збираємо меблі в Копенгагені за фіксовану ціну. Порахувати вартість».","cost":"0 kr · півгодини і чесність"},{"n":7,"t":"Ніде немає ціни, навіть приблизної","sym":"«Ціна за запитом» читається як «дорого» або «залежить від того, наскільки ви багаті». Більшість просто не напише.","check":"Чи може людина дізнатись хоча б порядок суми, не спілкуючись із тобою?","fix":"Дай вилку, приклади типових робіт із сумами або калькулятор. Ціна відсіює тих, хто все одно не купив би — і твій час звільняється для решти.","cost":"0 kr · але потребує рішення"},{"n":8,"t":"Звʼязатися можна тільки через форму","sym":"Людина хоче написати в месенджер, а має заповнювати поля й чекати. Вона просто закриває вкладку.","check":"Порахуй канали на сайті: телефон, який набирається одним дотиком, месенджер, пошта, форма. Менше трьох — мало.","fix":"Додай те, чим твоя аудиторія реально користується. У Данії це часто телефон і пошта, в українській спільноті — Telegram і WhatsApp. Постав кнопки так, щоб вони були видно на телефоні.","cost":"0 kr · година"},{"n":9,"t":"Поза робочим часом ніхто не відповідає","sym":"Більшість звернень приходить увечері й у вихідні. Відповідь у понеділок о десятій — це вже інший підрядник.","check":"Подивись у пошті або телефоні, о котрій годині приходять запити. Скільки з них отримало відповідь того ж дня?","fix":"Мінімум — автовідповідь, яка чесно каже, коли буде відповідь, і дає щось корисне вже зараз. Максимум — простий бот або AI-помічник, який відповідає на типові питання і збирає контакт.","cost":"0 kr на автовідповідь · бот — від кількох тисяч"},{"n":10,"t":"Немає онлайн-запису","sym":"Щоб записатись, треба подзвонити. Частина людей не любить дзвонити взагалі — і це не мала частина.","check":"Чи може клієнт обрати час і записатись сам, без тебе, о десятій вечора?","fix":"Постав будь-яку систему онлайн-запису й дай посилання в картці Google, у профілі й на сайті. Вона працює тоді, коли ти спиш.","cost":"0–300 kr на місяць"},{"n":11,"t":"Немає доказів, що ти справді вмієш","sym":"Тексти про «індивідуальний підхід» і «якість» є, а фото робіт, кейсів і «було/стало» немає.","check":"Знайди на своєму сайті три конкретні докази: фото твоєї роботи, історію клієнта з цифрою, відгук з іменем. Знайшов?","fix":"Фотографуй кожну роботу, навіть на телефон. Один кейс на місяць — це дванадцять на рік. Цифра в кейсі важить більше за будь-який епітет.","cost":"0 kr · дисципліна"},{"n":12,"t":"Клієнт пішов — і ти більше нічого не робиш","sym":"Людина подивилась, не написала, пішла назавжди. Нового доводиться купувати за гроші знову.","check":"Чи є в тебе хоч один спосіб повернути людину: список розсилки, канал, ретаргетинг, нагадування постійним клієнтам?","fix":"Почни з найпростішого — збирай пошту в обмін на щось корисне і пиши раз на місяць. Другий за простотою крок — нагадування тим, хто вже купував: їм продати вдесятеро дешевше, ніж новим.","cost":"0 kr на старті"}],"after":{"t":"Порахував хрестики?","items":[{"r":"0–2 ✗","d":"У тебе все добре. Займись 12-ю точкою — повторними клієнтами, там найбільше грошей."},{"r":"3–6 ✗","d":"Типова ситуація. Візьми три найдешевші пункти зі списку й закрий їх цього тижня — результат буде видно за місяць."},{"r":"7+ ✗","d":"Ти платиш за рекламу й нетворкінг, а потім втрачаєш людей на порозі. Почни з пунктів 1, 2, 6 — вони безкоштовні й дають найбільше."}]},"author":"Андрій Линник · LynBro ApS · andrii.dk"},"seq":{"count":95,"base":"","poster":"https://andrii.best-choice.dk/wp-content/uploads/sites/3/2026/09/hero-poster-1.jpg","urls":["https://andrii.best-choice.dk/wp-content/uploads/sites/3/2026/09/f_001.webp","https://andrii.best-choice.dk/wp-content/uploads/sites/3/2026/09/f_002.webp","https://andrii.best-choice.dk/wp-content/uploads/sites/3/2026/09/f_003.webp","https://andrii.best-choice.dk/wp-content/uploads/sites/3/2026/09/f_004.webp","https://andrii.best-choice.dk/wp-content/uploads/sites/3/2026/09/f_005.webp","https://andrii.best-choice.dk/wp-content/uploads/sites/3/2026/09/f_006.webp","https://andrii.best-choice.dk/wp-content/uploads/sites/3/2026/09/f_007.webp","https://andrii.best-choice.dk/wp-content/uploads/sites/3/2026/09/f_008.webp","https://andrii.best-choice.dk/wp-content/uploads/sites/3/2026/09/f_009.webp","https://andrii.best-choice.dk/wp-content/uploads/sites/3/2026/09/f_010.webp","https://andrii.best-choice.dk/wp-content/uploads/sites/3/2026/09/f_011.webp","https://andrii.best-choice.dk/wp-content/uploads/sites/3/2026/09/f_012.webp","https://andrii.best-choice.dk/wp-content/uploads/sites/3/2026/09/f_013.webp","https://andrii.best-choice.dk/wp-content/uploads/sites/3/2026/09/f_014.webp","https://andrii.best-choice.dk/wp-content/uploads/sites/3/2026/09/f_015.webp","https://andrii.best-choice.dk/wp-content/uploads/sites/3/2026/09/f_016.webp","https://andrii.best-choice.dk/wp-content/uploads/sites/3/2026/09/f_017.webp","https://andrii.best-choice.dk/wp-content/uploads/sites/3/2026/09/f_018.webp","https://andrii.best-choice.dk/wp-content/uploads/sites/3/2026/09/f_019.webp","https://andrii.best-choice.dk/wp-content/uploads/sites/3/2026/09/f_020.webp","https://andrii.best-choice.dk/wp-content/uploads/sites/3/2026/09/f_021.webp","https://andrii.best-choice.dk/wp-content/uploads/sites/3/2026/09/f_022.webp","https://andrii.best-choice.dk/wp-content/uploads/sites/3/2026/09/f_023.webp","https://andrii.best-choice.dk/wp-content/uploads/sites/3/2026/09/f_024.webp","https://andrii.best-choice.dk/wp-content/uploads/sites/3/2026/09/f_025.webp","https://andrii.best-choice.dk/wp-content/uploads/sites/3/2026/09/f_026.webp","https://andrii.best-choice.dk/wp-content/uploads/sites/3/2026/09/f_027.webp","https://andrii.best-choice.dk/wp-content/uploads/sites/3/2026/09/f_028.webp","https://andrii.best-choice.dk/wp-content/uploads/sites/3/2026/09/f_029.webp","https://andrii.best-choice.dk/wp-content/uploads/sites/3/2026/09/f_030.webp","https://andrii.best-choice.dk/wp-content/uploads/sites/3/2026/09/f_031.webp","https://andrii.best-choice.dk/wp-content/uploads/sites/3/2026/09/f_032.webp","https://andrii.best-choice.dk/wp-content/uploads/sites/3/2026/09/f_033.webp","https://andrii.best-choice.dk/wp-content/uploads/sites/3/2026/09/f_034.webp","https://andrii.best-choice.dk/wp-content/uploads/sites/3/2026/09/f_035.webp","https://andrii.best-choice.dk/wp-content/uploads/sites/3/2026/09/f_036.webp","https://andrii.best-choice.dk/wp-content/uploads/sites/3/2026/09/f_037.webp","https://andrii.best-choice.dk/wp-content/uploads/sites/3/2026/09/f_038.webp","https://andrii.best-choice.dk/wp-content/uploads/sites/3/2026/09/f_039.webp","https://andrii.best-choice.dk/wp-content/uploads/sites/3/2026/09/f_040.webp","https://andrii.best-choice.dk/wp-content/uploads/sites/3/2026/09/f_041.webp","https://andrii.best-choice.dk/wp-content/uploads/sites/3/2026/09/f_042.webp","https://andrii.best-choice.dk/wp-content/uploads/sites/3/2026/09/f_043.webp","https://andrii.best-choice.dk/wp-content/uploads/sites/3/2026/09/f_044.webp","https://andrii.best-choice.dk/wp-content/uploads/sites/3/2026/09/f_045.webp","https://andrii.best-choice.dk/wp-content/uploads/sites/3/2026/09/f_046.webp","https://andrii.best-choice.dk/wp-content/uploads/sites/3/2026/09/f_047.webp","https://andrii.best-choice.dk/wp-content/uploads/sites/3/2026/09/f_048.webp","https://andrii.best-choice.dk/wp-content/uploads/sites/3/2026/09/f_049.webp","https://andrii.best-choice.dk/wp-content/uploads/sites/3/2026/09/f_050.webp","https://andrii.best-choice.dk/wp-content/uploads/sites/3/2026/09/f_051.webp","https://andrii.best-choice.dk/wp-content/uploads/sites/3/2026/09/f_052.webp","https://andrii.best-choice.dk/wp-content/uploads/sites/3/2026/09/f_053.webp","https://andrii.best-choice.dk/wp-content/uploads/sites/3/2026/09/f_054.webp","https://andrii.best-choice.dk/wp-content/uploads/sites/3/2026/09/f_055.webp","https://andrii.best-choice.dk/wp-content/uploads/sites/3/2026/09/f_056.webp","https://andrii.best-choice.dk/wp-content/uploads/sites/3/2026/09/f_057.webp","https://andrii.best-choice.dk/wp-content/uploads/sites/3/2026/09/f_058.webp","https://andrii.best-choice.dk/wp-content/uploads/sites/3/2026/09/f_059.webp","https://andrii.best-choice.dk/wp-content/uploads/sites/3/2026/09/f_060.webp","https://andrii.best-choice.dk/wp-content/uploads/sites/3/2026/09/f_061.webp","https://andrii.best-choice.dk/wp-content/uploads/sites/3/2026/09/f_062.webp","https://andrii.best-choice.dk/wp-content/uploads/sites/3/2026/09/f_063.webp","https://andrii.best-choice.dk/wp-content/uploads/sites/3/2026/09/f_064.webp","https://andrii.best-choice.dk/wp-content/uploads/sites/3/2026/09/f_065.webp","https://andrii.best-choice.dk/wp-content/uploads/sites/3/2026/09/f_066.webp","https://andrii.best-choice.dk/wp-content/uploads/sites/3/2026/09/f_067.webp","https://andrii.best-choice.dk/wp-content/uploads/sites/3/2026/09/f_068.webp","https://andrii.best-choice.dk/wp-content/uploads/sites/3/2026/09/f_069.webp","https://andrii.best-choice.dk/wp-content/uploads/sites/3/2026/09/f_070.webp","https://andrii.best-choice.dk/wp-content/uploads/sites/3/2026/09/f_071.webp","https://andrii.best-choice.dk/wp-content/uploads/sites/3/2026/09/f_072.webp","https://andrii.best-choice.dk/wp-content/uploads/sites/3/2026/09/f_073.webp","https://andrii.best-choice.dk/wp-content/uploads/sites/3/2026/09/f_074.webp","https://andrii.best-choice.dk/wp-content/uploads/sites/3/2026/09/f_075.webp","https://andrii.best-choice.dk/wp-content/uploads/sites/3/2026/09/f_076.webp","https://andrii.best-choice.dk/wp-content/uploads/sites/3/2026/09/f_077.webp","https://andrii.best-choice.dk/wp-content/uploads/sites/3/2026/09/f_078.webp","https://andrii.best-choice.dk/wp-content/uploads/sites/3/2026/09/f_079.webp","https://andrii.best-choice.dk/wp-content/uploads/sites/3/2026/09/f_080.webp","https://andrii.best-choice.dk/wp-content/uploads/sites/3/2026/09/f_081.webp","https://andrii.best-choice.dk/wp-content/uploads/sites/3/2026/09/f_082.webp","https://andrii.best-choice.dk/wp-content/uploads/sites/3/2026/09/f_083.webp","https://andrii.best-choice.dk/wp-content/uploads/sites/3/2026/09/f_084.webp","https://andrii.best-choice.dk/wp-content/uploads/sites/3/2026/09/f_085.webp","https://andrii.best-choice.dk/wp-content/uploads/sites/3/2026/09/f_086.webp","https://andrii.best-choice.dk/wp-content/uploads/sites/3/2026/09/f_087.webp","https://andrii.best-choice.dk/wp-content/uploads/sites/3/2026/09/f_088.webp","https://andrii.best-choice.dk/wp-content/uploads/sites/3/2026/09/f_089.webp","https://andrii.best-choice.dk/wp-content/uploads/sites/3/2026/09/f_090.webp","https://andrii.best-choice.dk/wp-content/uploads/sites/3/2026/09/f_091.webp","https://andrii.best-choice.dk/wp-content/uploads/sites/3/2026/09/f_092.webp","https://andrii.best-choice.dk/wp-content/uploads/sites/3/2026/09/f_093.webp","https://andrii.best-choice.dk/wp-content/uploads/sites/3/2026/09/f_094.webp","https://andrii.best-choice.dk/wp-content/uploads/sites/3/2026/09/f_095.webp"]}}; ; window.A3URLS={"home": "/uk/andrii/", "projects": "/uk/proekty/", "speaking": "/uk/vystupy/", "now": "/uk/zaraz/", "checklist": "/uk/cheklist/", "quiz": "/uk/womans-day/", "ommig": "https://ommig.dk/andrii/uk", "email": "info@andrii.dk"}; ; window.A3TRACK={}; ; /* ══════════════════════════════════════════════════════════════════════ a3.gl — частинкове поле на чистому WebGL2. Без бібліотек (~9 КБ). Одна частинка = один реальний коміт. Формації морфують по скролу. ══════════════════════════════════════════════════════════════════════ */ (function (A3) { 'use strict'; var VERT = [ '#version 300 es', 'in vec3 aA; in vec3 aB; in vec3 aCol; in vec2 aSeed;', 'uniform float uMix, uTime, uAspect, uScale;', 'uniform vec2 uPointer;', 'uniform mat3 uRot;', 'out vec3 vCol; out float vFade;', 'float ease(float t){ return t<.5 ? 4.*t*t*t : 1.-pow(-2.*t+2.,3.)/2.; }', 'void main(){', ' float d = ease(clamp(uMix*1.35 - aSeed.x*0.35, 0., 1.));', ' vec3 p = mix(aA, aB, d);', // легке дихання, щоб поле не було мертвим ' p += 0.035*vec3(sin(uTime*.6+aSeed.y*9.4), cos(uTime*.5+aSeed.x*7.7), sin(uTime*.4+aSeed.y*5.1));', ' p = uRot * p;', // паралакс за курсором ' p.xy += uPointer * (0.09 + 0.22*(p.z+1.6));', ' float persp = 1.0 / (2.9 - p.z*0.55);', ' vec2 s = p.xy * persp * uScale;', ' s.x /= uAspect;', ' gl_Position = vec4(s, 0., 1.);', ' gl_PointSize = (1.1 + 2.5*aSeed.x) * persp * 2.4 * uScale;', ' vCol = aCol;', ' vFade = clamp(persp*1.05, 0.06, 1.0);', '}' ].join('\n'); var FRAG = [ '#version 300 es', 'precision mediump float;', 'in vec3 vCol; in float vFade; out vec4 o;', 'void main(){', ' vec2 c = gl_PointCoord - 0.5;', ' float r = dot(c,c);', ' if(r > 0.25) discard;', ' float a = smoothstep(0.25, 0.0, r);', ' o = vec4(vCol * (0.45 + 0.75*a), a * vFade * 0.86);', '}' ].join('\n'); // ─────────────────────── формації ─────────────────────── // Кожна повертає Float32Array(n*3) у просторі приблизно [-1.6..1.6] function rnd(i, s) { var x = Math.sin(i * 127.1 + s * 311.7) * 43758.5453; return x - Math.floor(x); } var F = {}; F.chaos = function (n) { var a = new Float32Array(n * 3); for (var i = 0; i < n; i++) { var th = rnd(i, 1) * 6.2832, ph = Math.acos(2 * rnd(i, 2) - 1), rr = 0.7 + rnd(i, 3) * 1.5; a[i * 3] = rr * Math.sin(ph) * Math.cos(th); a[i * 3 + 1] = rr * Math.sin(ph) * Math.sin(th) * 0.7; a[i * 3 + 2] = rr * Math.cos(ph) * 0.6; } return a; }; // блискавка + міст: Lyn + Bro F.bolt = function (n) { var a = new Float32Array(n * 3); // ламана блискавки var bolt = [[-.28, .95], [.06, .22], [-.12, .18], [.26, -.5], [-.02, -.46], [.2, -1.0]]; for (var i = 0; i < n; i++) { var t = i / n; if (t < 0.42) { // блискавка var u = (t / 0.42) * (bolt.length - 1), k = Math.floor(u), f = u - k; k = Math.min(k, bolt.length - 2); var x = bolt[k][0] + (bolt[k + 1][0] - bolt[k][0]) * f; var y = bolt[k][1] + (bolt[k + 1][1] - bolt[k][1]) * f; var w = 0.055; a[i * 3] = x + (rnd(i, 4) - .5) * w; a[i * 3 + 1] = y + (rnd(i, 5) - .5) * w; a[i * 3 + 2] = (rnd(i, 6) - .5) * 0.5; } else { // настил мосту + троси var s = (t - 0.42) / 0.58; var bx = -1.55 + s * 3.1; var sag = 0.34 * Math.cos(bx * 1.02); var lane = rnd(i, 7); var by = -0.62 + (lane < .55 ? 0 : sag * 0.9); a[i * 3] = bx + (rnd(i, 8) - .5) * 0.02; a[i * 3 + 1] = by + (rnd(i, 9) - .5) * 0.03; a[i * 3 + 2] = (rnd(i, 10) - .5) * 0.7; } } return a; }; // дуга Харків → Копенгаген над сіткою-меридіанами F.arc = function (n) { var a = new Float32Array(n * 3); var A = [0.92, -0.32], B = [-0.86, -0.12]; for (var i = 0; i < n; i++) { var t = i / n; if (t < 0.5) { // сітка var gx = (Math.floor(rnd(i, 11) * 26) / 25 - .5) * 3.0; var gy = (Math.floor(rnd(i, 12) * 12) / 11 - .5) * 1.5; a[i * 3] = gx; a[i * 3 + 1] = gy - 0.45; a[i * 3 + 2] = -0.55 + (rnd(i, 13) - .5) * .2; } else { // траєкторія var s = (t - .5) / .5, j = (rnd(i, 14) - .5) * .06; a[i * 3] = A[0] + (B[0] - A[0]) * s + j; a[i * 3 + 1] = A[1] + (B[1] - A[1]) * s + Math.sin(s * Math.PI) * 0.85 + j; a[i * 3 + 2] = Math.sin(s * Math.PI) * 0.5 + (rnd(i, 15) - .5) * .12; } } return a; }; // сузір'я проєктів: кластери за доменами F.constellation = function (n, clusters) { var a = new Float32Array(n * 3), C = clusters.length; for (var i = 0; i < n; i++) { var c = clusters[i % C]; var g1 = (rnd(i, 16) + rnd(i, 17) + rnd(i, 18) - 1.5) * 0.82; var g2 = (rnd(i, 19) + rnd(i, 20) + rnd(i, 21) - 1.5) * 0.82; var g3 = (rnd(i, 22) + rnd(i, 23) + rnd(i, 24) - 1.5) * 0.82; a[i * 3] = c[0] + g1 * c[3]; a[i * 3 + 1] = c[1] + g2 * c[3] * 0.8; a[i * 3 + 2] = c[2] + g3 * c[3] * 0.7; } return a; }; // конвеєр: паралельні доріжки зі згустками F.pipeline = function (n) { var a = new Float32Array(n * 3); for (var i = 0; i < n; i++) { var lane = Math.floor(rnd(i, 25) * 5); var t = rnd(i, 26); var stage = Math.floor(t * 5) / 5; var x = -1.5 + (stage + (t * 5 % 1) * 0.16) * 3.0; a[i * 3] = x + (rnd(i, 27) - .5) * .06; a[i * 3 + 1] = (lane - 2) * 0.27 + (rnd(i, 28) - .5) * .05; a[i * 3 + 2] = (rnd(i, 29) - .5) * .5; } return a; }; // «зал»: дуги крісел, розвернуті до сцени F.hall = function (n) { var a = new Float32Array(n * 3); for (var i = 0; i < n; i++) { var row = Math.floor(rnd(i, 30) * 11); var rad = 0.62 + row * 0.105; var ang = (rnd(i, 31) - .5) * 2.1; a[i * 3] = Math.sin(ang) * rad * 1.5; a[i * 3 + 1] = -0.55 + row * 0.052 + (rnd(i, 32) - .5) * .02; a[i * 3 + 2] = -Math.cos(ang) * rad * .8 + .5; } return a; }; F.caret = function (n) { var a = new Float32Array(n * 3); for (var i = 0; i < n; i++) { var inBar = rnd(i, 33) < 0.55; if (inBar) { a[i * 3] = (rnd(i, 34) - .5) * .05; a[i * 3 + 1] = (rnd(i, 35) - .5) * .62; a[i * 3 + 2] = (rnd(i, 36) - .5) * .05; } else { var th = rnd(i, 37) * 6.2832, rr = 1.4 + rnd(i, 38) * 1.4; a[i * 3] = Math.cos(th) * rr; a[i * 3 + 1] = Math.sin(th) * rr * .6; a[i * 3 + 2] = (rnd(i, 39) - .5) * 1.4; } } return a; }; // ─────────────────────── рушій ─────────────────────── function compile(gl, type, src) { var s = gl.createShader(type); gl.shaderSource(s, src); gl.compileShader(s); if (!gl.getShaderParameter(s, gl.COMPILE_STATUS)) throw new Error(gl.getShaderInfoLog(s)); return s; } A3.gl = function (canvas, opts) { opts = opts || {}; var gl = canvas.getContext('webgl2', { alpha: true, antialias: false, powerPreference: 'low-power', premultipliedAlpha: false }); if (!gl) return null; var N = opts.count || 4000; var clusters = opts.clusters || [[0, 0, 0, .5]]; var palette = opts.palette || [[1, 1, 1]]; var prog = gl.createProgram(); gl.attachShader(prog, compile(gl, gl.VERTEX_SHADER, VERT)); gl.attachShader(prog, compile(gl, gl.FRAGMENT_SHADER, FRAG)); gl.linkProgram(prog); if (!gl.getProgramParameter(prog, gl.LINK_STATUS)) return null; gl.useProgram(prog); var vao = gl.createVertexArray(); gl.bindVertexArray(vao); function buf(data, loc, size, dyn) { var b = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, b); gl.bufferData(gl.ARRAY_BUFFER, data, dyn ? gl.DYNAMIC_DRAW : gl.STATIC_DRAW); var l = gl.getAttribLocation(prog, loc); gl.enableVertexAttribArray(l); gl.vertexAttribPointer(l, size, gl.FLOAT, false, 0, 0); return b; } var posA = F.chaos(N), posB = F.chaos(N); var bufA = buf(posA, 'aA', 3, true); var bufB = buf(posB, 'aB', 3, true); var col = new Float32Array(N * 3); for (var i = 0; i < N; i++) { var c = palette[i % palette.length]; var v = 0.72 + rnd(i, 40) * 0.5; col[i * 3] = c[0] * v; col[i * 3 + 1] = c[1] * v; col[i * 3 + 2] = c[2] * v; } buf(col, 'aCol', 3); var seed = new Float32Array(N * 2); for (i = 0; i < N; i++) { seed[i * 2] = rnd(i, 41); seed[i * 2 + 1] = rnd(i, 42); } buf(seed, 'aSeed', 2); var uMix = gl.getUniformLocation(prog, 'uMix'), uTime = gl.getUniformLocation(prog, 'uTime'), uAspect = gl.getUniformLocation(prog, 'uAspect'), uScale = gl.getUniformLocation(prog, 'uScale'), uPointer = gl.getUniformLocation(prog, 'uPointer'), uRot = gl.getUniformLocation(prog, 'uRot'); gl.enable(gl.BLEND); gl.blendFunc(gl.SRC_ALPHA, gl.ONE); // адитивне змішування var state = { mix: 1, target: 1, time: 0, ptr: [0, 0], ptrT: [0, 0], rotY: 0, rotTarget: 0, scale: 1, scaleT: 1, current: 'chaos', running: false }; function resize() { var dpr = Math.min(window.devicePixelRatio || 1, 1.75); var w = canvas.clientWidth, h = canvas.clientHeight; if (!w || !h) return; canvas.width = w * dpr | 0; canvas.height = h * dpr | 0; gl.viewport(0, 0, canvas.width, canvas.height); gl.uniform1f(uAspect, w / h); state.scaleT = w < 720 ? 0.78 : 1.0; } function setFormation(name, arg) { if (name === state.current) return; // поточні інтерпольовані позиції стають новим A var d = state.mix; var cur = new Float32Array(N * 3); for (var k = 0; k < N * 3; k++) cur[k] = posA[k] + (posB[k] - posA[k]) * d; posA = cur; posB = (name === 'constellation') ? F.constellation(N, clusters) : F[name](N, arg); gl.bindBuffer(gl.ARRAY_BUFFER, bufA); gl.bufferData(gl.ARRAY_BUFFER, posA, gl.DYNAMIC_DRAW); gl.bindBuffer(gl.ARRAY_BUFFER, bufB); gl.bufferData(gl.ARRAY_BUFFER, posB, gl.DYNAMIC_DRAW); state.mix = 0; state.current = name; } var last = 0; function frame(t) { if (!state.running) return; var dt = Math.min((t - last) / 1000 || 0.016, 0.05); last = t; state.time += dt; state.mix += (1 - state.mix) * Math.min(dt * 1.6, 1); state.ptr[0] += (state.ptrT[0] - state.ptr[0]) * Math.min(dt * 3.2, 1); state.ptr[1] += (state.ptrT[1] - state.ptr[1]) * Math.min(dt * 3.2, 1); state.rotY += (state.rotTarget - state.rotY) * Math.min(dt * 1.4, 1); state.scale += (state.scaleT - state.scale) * Math.min(dt * 2, 1); var ry = state.rotY + state.time * 0.035; var c = Math.cos(ry), s = Math.sin(ry); var tilt = 0.14; var ct = Math.cos(tilt), st = Math.sin(tilt); // R = Rx(tilt) * Ry(ry), column-major gl.uniformMatrix3fv(uRot, false, new Float32Array([ c, st * s, -ct * s, 0, ct, st, s, -st * c, ct * c ])); gl.uniform1f(uMix, state.mix); gl.uniform1f(uTime, state.time); gl.uniform1f(uScale, state.scale); gl.uniform2f(uPointer, state.ptr[0], state.ptr[1]); gl.clearColor(0, 0, 0, 0); gl.clear(gl.COLOR_BUFFER_BIT); gl.drawArrays(gl.POINTS, 0, N); requestAnimationFrame(frame); } resize(); window.addEventListener('resize', resize, { passive: true }); return { start: function () { if (!state.running) { state.running = true; last = performance.now(); requestAnimationFrame(frame); } }, stop: function () { state.running = false; }, formation: setFormation, pointer: function (x, y) { state.ptrT[0] = x; state.ptrT[1] = y; }, spin: function (v) { state.rotTarget = v; }, zoom: function (v) { state.scaleT = v; }, count: N }; }; A3.gl.formations = F; })(window.A3 = window.A3 || {}); ; /* ══════════════════════════════════════════════════════════════════════ a3.ui — взаємодія: очі, курсор, скрол-кадри, термінал, лічильники. ══════════════════════════════════════════════════════════════════════ */ (function (A3) { 'use strict'; // курсор і термінал мусять жити ВСЕРЕДИНІ .a3 — інакше не бачать токенів теми var host = function () { return document.querySelector('.a3') || document.body; }; var reduced = matchMedia('(prefers-reduced-motion: reduce)').matches; var coarse = matchMedia('(pointer: coarse)').matches; var lerp = function (a, b, t) { return a + (b - a) * t; }; var clamp = function (v, a, b) { return v < a ? a : v > b ? b : v; }; /* ─────────────── ОЧІ, ЩО СТЕЖАТЬ ЗА КУРСОРОМ ─────────────── */ A3.eyes = function (root) { if (!root) return; var geo = JSON.parse(root.dataset.eyes || '{}'); if (!geo.eyes) return; var irises = [], lids = []; geo.eyes.forEach(function (e, i) { var wrap = document.createElement('span'); wrap.className = 'a3-eye'; wrap.style.cssText = 'position:absolute;left:' + (e.x * 100) + '%;top:' + (e.y * 100) + '%;' + 'width:' + (e.r * 2 * geo.irisSprite.scale * 100) + '%;aspect-ratio:1;' + 'transform:translate(-50%,-50%);pointer-events:none;will-change:transform'; var img = document.createElement('img'); img.src = root.dataset.iris; img.alt = ''; img.decoding = 'async'; img.style.cssText = 'width:100%;height:100%;display:block'; wrap.appendChild(img); root.appendChild(wrap); irises.push(wrap); // повіка — вузька смужка кольору шкіри, що падає вниз під час моргання var lid = document.createElement('span'); lid.className = 'a3-lid'; var lc = (geo.lid && geo.lid[i]) || [201, 162, 136]; var lc2 = lc.map(function (v) { return Math.max(0, v - 26); }); lid.style.cssText = 'position:absolute;left:' + (e.x * 100) + '%;top:' + ((e.y - e.r * 1.55) * 100) + '%;' + 'width:' + (e.r * 3.6 * 100) + '%;height:' + (e.r * 3.3 * 100) + '%;' + 'transform:translate(-50%,0) scaleY(0);transform-origin:top center;' + 'background:linear-gradient(180deg,rgb(' + lc.join(',') + ') 0%,rgb(' + lc2.join(',') + ') 100%);' + 'border-radius:0 0 50% 50%/0 0 46% 46%;pointer-events:none;filter:blur(.6px)'; root.appendChild(lid); lids.push(lid); }); // зміщення задано в частках ширини портрета, а transform рахується // у відсотках ВІД САМОЇ райдужки — переводимо var eyeW = geo.eyes[0].r * 2 * (geo.irisSprite.scale || 1.15); var max = (geo.maxOffset || 0.005) / eyeW * 100; var tx = 0, ty = 0, cx = 0, cy = 0; function aim(px, py) { var r = root.getBoundingClientRect(); var ox = (px - (r.left + r.width * 0.5)) / (r.width * 1.6); var oy = (py - (r.top + r.height * 0.42)) / (r.height * 1.6); var d = Math.hypot(ox, oy) || 1; var k = Math.min(d, 1) / d; tx = clamp(ox * k, -1, 1); ty = clamp(oy * k, -1, 1); } if (!coarse) { addEventListener('pointermove', function (e) { aim(e.clientX, e.clientY); }, { passive: true }); } else { // на тачі очі стежать за прокруткою addEventListener('scroll', function () { var r = root.getBoundingClientRect(); ty = clamp((innerHeight * 0.5 - (r.top + r.height / 2)) / innerHeight, -1, 1); tx = Math.sin(scrollY / 420) * 0.6; }, { passive: true }); } // моргання + «озирання» в простої var idle = 0, lastMove = Date.now(); addEventListener('pointermove', function () { lastMove = Date.now(); }, { passive: true }); function blink() { lids.forEach(function (l) { l.style.transition = 'transform .09s ease-in'; l.style.transform = 'translate(-50%,0) scaleY(1)'; setTimeout(function () { l.style.transition = 'transform .16s ease-out'; l.style.transform = 'translate(-50%,0) scaleY(0)'; }, 95); }); setTimeout(blink, 2600 + Math.random() * 4800); } if (!reduced) setTimeout(blink, 1800); (function loop() { requestAnimationFrame(loop); // у простої — повільно озирається if (Date.now() - lastMove > 4000 && !coarse) { idle += 0.006; tx = Math.sin(idle) * 0.75; ty = Math.sin(idle * 0.62) * 0.4 - 0.1; } cx = lerp(cx, tx, 0.085); cy = lerp(cy, ty, 0.085); var t = 'translate(calc(-50% + ' + (cx * max).toFixed(3) + '%), calc(-50% + ' + (cy * max * 0.72).toFixed(3) + '%))'; for (var i = 0; i < irises.length; i++) irises[i].style.transform = t; })(); }; /* ─────────────── МАГНІТНИЙ КУРСОР ─────────────── */ A3.cursor = function () { if (coarse || reduced) return; var el = document.createElement('div'); el.className = 'a3-cur'; el.innerHTML = ''; host().appendChild(el); var lab = el.firstChild; var x = innerWidth / 2, y = innerHeight / 2, tx = x, ty = y; addEventListener('pointermove', function (e) { tx = e.clientX; ty = e.clientY; }, { passive: true }); (function loop() { requestAnimationFrame(loop); x = lerp(x, tx, 0.22); y = lerp(y, ty, 0.22); el.style.transform = 'translate(' + (x - 4) + 'px,' + (y - 4) + 'px) translate(-50%,-50%)'; })(); document.addEventListener('pointerover', function (e) { var m = e.target.closest('[data-cur]'); if (m) { el.classList.add('mag'); lab.textContent = m.dataset.cur || ''; el.classList.remove('txt'); } else if (e.target.closest('p,li,h1,h2,h3,input,textarea')) { el.classList.add('txt'); el.classList.remove('mag'); } else { el.classList.remove('mag', 'txt'); } }); document.documentElement.style.cursor = 'none'; }; /* ─────────────── СКРОЛ-СКРАБ КАДРІВ ─────────────── */ A3.sequence = function (cfg) { var cv = document.getElementById(cfg.canvas); if (!cv) return; var ctx = cv.getContext('2d', { alpha: false }); var host = cv.closest('[data-seq-host]') || cv.parentElement; var n = cfg.count, imgs = new Array(n), loaded = 0, ready = false, cur = -1; var poster = new Image(); poster.src = cfg.poster; var status = document.querySelector(cfg.statusSel || '#seqStatus'); function fit() { var dpr = Math.min(devicePixelRatio || 1, 2); cv.width = cv.clientWidth * dpr | 0; cv.height = cv.clientHeight * dpr | 0; draw(cur < 0 ? 0 : cur, true); } function cover(img) { if (!img || !img.width) return; var cw = cv.width, ch = cv.height; var s = Math.max(cw / img.width, ch / img.height); var w = img.width * s, h = img.height * s; ctx.fillStyle = '#07080c'; ctx.fillRect(0, 0, cw, ch); ctx.drawImage(img, (cw - w) / 2, (ch - h) / 2, w, h); } function draw(i, force) { i = clamp(i | 0, 0, n - 1); if (i === cur && !force) return; cur = i; var img = imgs[i]; cover(img && img.complete && img.width ? img : (poster.complete ? poster : null)); } // прогресивне завантаження: спершу кожен 8-й, потім решта function load(order, done) { var idx = 0, finished = false; function next() { if (idx >= order.length) { if (!finished) { finished = true; if (done) done(); } return; } var k = order[idx++]; if (imgs[k]) return next(); var im = new Image(); im.decoding = 'async'; im.onload = im.onerror = function () { loaded++; if (status) status.textContent = Math.round(loaded / n * 100) + '%'; if (!ready && loaded > 8) { ready = true; onScroll(); } next(); }; im.src = cfg.url(k + 1); imgs[k] = im; } for (var p = 0; p < 4; p++) next(); // 4 паралельні } var coarseOrder = [], fineOrder = []; for (var i = 0; i < n; i++) (i % 8 === 0 ? coarseOrder : fineOrder).push(i); poster.onload = function () { draw(0, true); }; function onScroll() { var r = host.getBoundingClientRect(); var total = r.height - innerHeight; if (total <= 0) return; var p = clamp(-r.top / total, 0, 1); draw(Math.round(p * (n - 1))); if (cfg.onProgress) cfg.onProgress(p); } addEventListener('resize', fit, { passive: true }); addEventListener('scroll', function () { requestAnimationFrame(onScroll); }, { passive: true }); fit(); // вантажимо тільки коли секція наближається var io = new IntersectionObserver(function (es) { if (es[0].isIntersecting) { io.disconnect(); load(coarseOrder, function () { load(fineOrder); }); } }, { rootMargin: '900px' }); io.observe(host); return { refresh: onScroll }; }; /* ─────────────── ЛІЧИЛЬНИКИ ─────────────── */ A3.counters = function (sel) { var els = document.querySelectorAll(sel || '[data-count]'); var io = new IntersectionObserver(function (entries) { entries.forEach(function (en) { if (!en.isIntersecting) return; io.unobserve(en.target); var el = en.target, to = parseFloat(el.dataset.count), t0 = 0; var dec = (el.dataset.dec | 0), pre = el.dataset.pre || '', suf = el.dataset.suf || ''; if (reduced) { el.textContent = pre + to.toLocaleString('uk-UA') + suf; return; } function step(t) { if (!t0) t0 = t; var p = clamp((t - t0) / 1500, 0, 1); var e = 1 - Math.pow(1 - p, 3.2); var v = to * e; el.textContent = pre + (dec ? v.toFixed(dec) : Math.round(v).toLocaleString('uk-UA')) + suf; if (p < 1) requestAnimationFrame(step); } requestAnimationFrame(step); }); }, { threshold: 0.4 }); els.forEach(function (e) { io.observe(e); }); }; /* ─────────────── ПОЯВИ + КІНЕТИЧНА ВАГА ─────────────── */ A3.motion = function () { // появи рахуємо в тому самому rAF-циклі, що й решту: // IntersectionObserver тут виявився ненадійним, а невидимий текст — неприпустимий var pending = [].slice.call(document.querySelectorAll('[data-rise],.lines')); var fcount = 0; function sweep(force) { for (var i = pending.length - 1; i >= 0; i--) { var el = pending[i]; var r = el.getBoundingClientRect(); if (force || (r.top < innerHeight * 0.94 && r.bottom > 0)) { el.classList.add('in'); pending.splice(i, 1); } } } requestAnimationFrame(function () { sweep(false); }); var bar = document.querySelector('.a3-prog'), nav = document.querySelector('.a3-nav'); var navAlways = nav && nav.hasAttribute('data-always'); var lastY = scrollY, vel = 0, kin = document.querySelectorAll('[data-kin]'); function tick() { requestAnimationFrame(tick); if (pending.length && (++fcount & 3) === 0) sweep(false); var y = scrollY; var dy = y - lastY; lastY = y; vel = lerp(vel, Math.min(Math.abs(dy), 90), 0.12); var h = document.documentElement.scrollHeight - innerHeight; if (bar) bar.style.width = (h > 0 ? (y / h) * 100 : 0) + '%'; if (nav && !navAlways) nav.classList.toggle('on', y > innerHeight * 0.75); if (!reduced && kin.length) { // вага шрифту стискається від швидкості прокрутки + RGB-розшарування var w = Math.round(760 - vel * 4.6); var off = (vel * 0.045).toFixed(2); for (var i = 0; i < kin.length; i++) { kin[i].style.setProperty('--kin-w', clamp(w, 320, 800)); kin[i].style.textShadow = vel > 2 ? (-off + 'px 0 rgba(255,80,80,.6), ' + off + 'px 0 rgba(80,210,255,.6)') : 'none'; } } // активний пункт навігації var best = null, bd = 1e9; secs.forEach(function (s) { var d = Math.abs(s.el.getBoundingClientRect().top - innerHeight * 0.32); if (d < bd) { bd = d; best = s; } }); if (best && best.id !== activeId) { activeId = best.id; document.querySelectorAll('.a3-nav__links a').forEach(function (a) { a.toggleAttribute('aria-current', a.getAttribute('href') === '#' + activeId); }); if (A3.onSection) A3.onSection(activeId, best.el); } } var secs = [].map.call(document.querySelectorAll('[data-section]'), function (el) { return { id: el.id, el: el }; }); var activeId = null; tick(); }; /* ─────────────── ТЕРМІНАЛ (клавіша /) ─────────────── */ A3.terminal = function (commands) { var box = document.createElement('div'); box.className = 'a3-term'; box.setAttribute('role', 'dialog'); box.setAttribute('aria-label', 'Консоль сайту'); box.innerHTML = '
' + '
andrii@dk:~$' + '
'; host().appendChild(box); var out = box.querySelector('#a3TermOut'), inp = box.querySelector('#a3TermIn'); var hist = [], hi = -1, open = false; function print(html, cls) { var d = document.createElement('div'); if (cls) d.className = cls; d.innerHTML = html; out.appendChild(d); out.scrollTop = out.scrollHeight; } function toggle(v) { open = v === undefined ? !open : v; box.classList.toggle('on', open); if (open) { inp.focus(); if (!out.children.length) print(commands.__banner, 'b'); } else inp.blur(); } A3.termToggle = toggle; addEventListener('keydown', function (e) { if (e.key === '/' && !open && !/^(INPUT|TEXTAREA)$/.test(document.activeElement.tagName)) { e.preventDefault(); toggle(true); } else if (e.key === 'Escape' && open) { toggle(false); } }); inp.addEventListener('keydown', function (e) { if (e.key === 'Enter') { var v = inp.value.trim(); inp.value = ''; if (!v) return; hist.push(v); hi = hist.length; print('andrii@dk:~$ ' + v.replace(/' + ((window.A3DATA && A3DATA.t && A3DATA.t.termNoCmd) || 'команди «%s» немає. спробуй') .replace('%s', parts[0].replace(/help'); } else if (e.key === 'ArrowUp') { if (hi > 0) inp.value = hist[--hi]; e.preventDefault(); } else if (e.key === 'ArrowDown') { hi = Math.min(hi + 1, hist.length); inp.value = hist[hi] || ''; } }); box.addEventListener('click', function (e) { if (e.target === box) toggle(false); }); }; /* ─────────────── АКОРДЕОН ─────────────── */ A3.accordion = function (sel) { document.querySelectorAll(sel).forEach(function (item) { var btn = item.querySelector('button'), panel = item.querySelector('[data-panel]'); if (!btn || !panel) return; btn.addEventListener('click', function () { var isOpen = item.classList.toggle('on'); btn.setAttribute('aria-expanded', isOpen); panel.style.height = isOpen ? panel.scrollHeight + 'px' : '0px'; }); }); }; /* ─────────────── ПЕРЕМИКАЧ МОВ ─────────────── */ A3.langSwitch = function (cfg) { var host = document.getElementById('a3Lang'); if (!host || !cfg || !cfg.alt) return; host.innerHTML = cfg.order.map(function (l) { var cur = l === cfg.current; return '' + l + ''; }).join(''); }; A3.reduced = reduced; A3.coarse = coarse; })(window.A3 = window.A3 || {}); ; /* ══════════════════════════════════════════════════════════════════════ a3.track — легка cookieless-аналітика. Принцип: НІЧОГО не зберігаємо на пристрої відвідувача й не ставимо ідентифікаторів. Кожна подія самодостатня, агрегація — на приймачі. Через це cookie-банер не потрібен (ePrivacy), і в payload немає персональних даних. Приймач конфігурується одним обʼєктом: window.A3TRACK = { matomo: { url: 'https://matomo.best-choice.dk/', siteId: 1 }, // або endpoint:'https://collector.example/e', // свій debug: false } Якщо приймача не задано — трекер мовчки збирає контекст сесії (у памʼяті вкладки) і віддає його формам, щоб лід приходив із джерелом. ══════════════════════════════════════════════════════════════════════ */ (function (A3) { 'use strict'; var CFG = window.A3TRACK || {}; var dnt = navigator.doNotTrack === '1' || window.doNotTrack === '1' || navigator.globalPrivacyControl === true; // ── контекст візиту (живе тільки в памʼяті вкладки) ──────────────── var q = new URLSearchParams(location.search); var ctx = { path: location.pathname, lang: (document.documentElement.lang || 'uk').slice(0, 2), ref: (function () { try { if (!document.referrer) return 'прямий'; var h = new URL(document.referrer).hostname.replace(/^www\./, ''); return h === location.hostname ? 'внутрішній' : h; } catch (e) { return 'невідомо'; } })(), utm: ['source', 'medium', 'campaign', 'content'].reduce(function (o, k) { var v = q.get('utm_' + k); if (v) o[k] = v.slice(0, 60); return o; }, {}), screen: innerWidth + '×' + innerHeight, device: innerWidth < 760 ? 'телефон' : innerWidth < 1100 ? 'планшет' : 'компʼютер', t0: Date.now(), depth: 0, events: [] }; // подорож між сторінками — sessionStorage лише якщо він доступний, // і тільки шляхи, без ідентифікаторів try { var prev = JSON.parse(sessionStorage.getItem('a3j') || '[]'); if (prev.indexOf(ctx.path) < 0) prev.push(ctx.path); ctx.journey = prev.slice(-12); sessionStorage.setItem('a3j', JSON.stringify(ctx.journey)); } catch (e) { ctx.journey = [ctx.path]; } // ── відправка ────────────────────────────────────────────────────── function beacon(url) { try { if (navigator.sendBeacon) navigator.sendBeacon(url); else { var i = new Image(); i.src = url; } } catch (e) { /* аналітика ніколи не ламає сторінку */ } } function send(name, extra) { if (dnt) return; ctx.events.push({ e: name, t: Math.round((Date.now() - ctx.t0) / 1000) }); if (CFG.debug) console.log('[a3.track]', name, extra || ''); if (CFG.matomo && CFG.matomo.url) { // Matomo HTTP Tracking API — без завантаження matomo.js var p = new URLSearchParams({ idsite: CFG.matomo.siteId || 1, rec: 1, apiv: 1, url: location.origin + location.pathname, action_name: name === 'pageview' ? (document.title || ctx.path) : name, urlref: document.referrer || '', res: screen.width + 'x' + screen.height, lang: ctx.lang, rand: Math.random().toString(36).slice(2), send_image: 0 }); if (name !== 'pageview') { p.set('e_c', 'site'); p.set('e_a', name); if (extra) p.set('e_n', String(extra).slice(0, 80)); } Object.keys(ctx.utm).forEach(function (k) { p.set('utm_' + k, ctx.utm[k]); }); beacon(CFG.matomo.url.replace(/\/?$/, '/') + 'matomo.php?' + p.toString()); } if (CFG.endpoint) { var d = new URLSearchParams({ s: 'andrii.dk', e: name, p: ctx.path, r: ctx.ref, l: ctx.lang, d: ctx.device, n: extra ? String(extra).slice(0, 80) : '' }); Object.keys(ctx.utm).forEach(function (k) { d.set('utm_' + k, ctx.utm[k]); }); beacon(CFG.endpoint + (CFG.endpoint.indexOf('?') < 0 ? '?' : '&') + d.toString()); } } // ── що міряємо ───────────────────────────────────────────────────── send('pageview'); var marks = [25, 50, 75, 100], hit = {}; addEventListener('scroll', function () { var h = document.documentElement.scrollHeight - innerHeight; if (h <= 0) return; var pct = Math.min(100, Math.round(scrollY / h * 100)); if (pct > ctx.depth) ctx.depth = pct; marks.forEach(function (m) { if (pct >= m && !hit[m]) { hit[m] = 1; send('scroll', m + '%'); } }); }, { passive: true }); document.addEventListener('click', function (ev) { var el = ev.target.closest('[data-ev]'); if (el) return send(el.dataset.ev, el.dataset.evName || el.textContent.trim().slice(0, 60)); var a = ev.target.closest('a[href]'); if (!a) return; var href = a.getAttribute('href'); if (/^mailto:/.test(href)) send('mailto'); else if (/^https?:/.test(href) && a.hostname !== location.hostname) send('outbound', a.hostname); }, true); addEventListener('pagehide', function () { send('leave', ctx.depth + '% · ' + Math.round((Date.now() - ctx.t0) / 1000) + 'с'); }); // ── контекст для форм (працює навіть без приймача) ───────────────── A3.track = { event: send, context: function () { return { source: ctx.ref, utm: Object.keys(ctx.utm).length ? Object.keys(ctx.utm).map(function (k) { return k + '=' + ctx.utm[k]; }).join(' · ') : '—', journey: (ctx.journey || []).join(' → '), onPage: Math.round((Date.now() - ctx.t0) / 1000) + ' с', depth: ctx.depth + '%', device: ctx.device + ' · ' + ctx.screen, lang: ctx.lang }; } }; })(window.A3 = window.A3 || {}); ; /* a3.catalog — фільтрований каталог проєктів */ (function (A3) { 'use strict'; var D = window.A3DATA || {}, P = D.portfolio, T = D.t || {}; var tr = function (k, d) { return T[k] || d || k; }; if (!P) return; var $ = function (s) { return document.querySelector(s); }; var esc = function (s) { return String(s == null ? '' : s).replace(/[&<>"]/g, function (c) { return ({ '&': '&', '<': '<', '>': '>', '"': '"' })[c]; }); }; var doms = {}; P.domains.forEach(function (d) { doms[d.id] = d; }); var order = { prod: 0, building: 1, research: 2, archive: 3 }; var items = P.projects.slice().sort(function (a, b) { return (order[a.status] - order[b.status]) || (b.commits || 0) - (a.commits || 0) || a.name.localeCompare(b.name, D.lang || 'uk'); }); var state = { dom: null, st: null, q: '' }; /* ── фільтри ── */ function chip(label, color, pressed, on) { var b = document.createElement('button'); b.className = 'a3-chip'; b.type = 'button'; b.setAttribute('aria-pressed', pressed ? 'true' : 'false'); b.setAttribute('data-cur', tr('filterSearch')); if (color) { b.style.setProperty('--c', color); b.innerHTML = ''; } b.appendChild(document.createTextNode(label)); b.addEventListener('click', on); return b; } function buildFilters() { var dw = $('#fDomains'); dw.appendChild(chip(tr('filterAll'), null, true, function () { state.dom = null; sync(); })); P.domains.forEach(function (d) { var n = items.filter(function (p) { return p.domain === d.id; }).length; dw.appendChild(chip(d.label + ' · ' + n, d.color, false, function () { state.dom = state.dom === d.id ? null : d.id; sync(); })); }); var sw = $('#fStatus'); sw.appendChild(chip(tr('filterAnyState'), null, true, function () { state.st = null; sync(); })); Object.keys(P.statuses).forEach(function (k) { var s = P.statuses[k]; var n = items.filter(function (p) { return p.status === k; }).length; sw.appendChild(chip(s.label + ' · ' + n, s.dot, false, function () { state.st = state.st === k ? null : k; sync(); })); }); $('#fSearch').addEventListener('input', function (e) { state.q = e.target.value.trim().toLowerCase(); sync(); }); var lg = $('#a3Legend'); lg.innerHTML = P.domains.map(function (d) { return '' + esc(d.label) + ''; }).join('') + '' + tr('legendProd') + '   ' + tr('legendWip') + ''; } /* ── список ── */ function render() { $('#a3List').innerHTML = items.map(function (p, i) { var d = doms[p.domain] || {}, s = P.statuses[p.status] || {}; var n = String(i + 1).padStart(2, '0'); return '
' + '' + '
' + '

' + esc(p.full) + '

' + '
' + p.stack.map(function (t) { return '' + esc(t) + ''; }).join('') + '
' + (p.commits && !/коміт|commit|commits/i.test(p.proof || '') ? '
' + p.commits.toLocaleString(D.lang === 'da' ? 'da-DK' : D.lang === 'en' ? 'en-GB' : 'uk-UA') + ' ' + tr('commitsWord') + '
' : '') + (p.proof ? '
' + esc(p.proof) + '
' : '') + (p.url ? '' + esc(p.url.replace(/^https?:\/\//, '').replace(/\/$/, '')) + ' ↗' : '') + '
'; }).join(''); [].forEach.call(document.querySelectorAll('.a3-item'), function (item) { var btn = item.querySelector('button'), panel = item.querySelector('[data-panel]'); btn.addEventListener('click', function () { var open = item.classList.toggle('on'); btn.setAttribute('aria-expanded', open); panel.style.height = open ? panel.scrollHeight + 'px' : '0px'; if (open) history.replaceState(null, '', '#' + item.id); }); }); } function sync() { var shown = 0; [].forEach.call(document.querySelectorAll('.a3-item'), function (el) { var ok = (!state.dom || el.dataset.dom === state.dom) && (!state.st || el.dataset.st === state.st) && (!state.q || el.dataset.find.indexOf(state.q) > -1); el.hidden = !ok; if (ok) shown++; }); $('#fCount').innerHTML = tr('filterShown') + ' ' + shown + ' ' + tr('filterOf') + ' ' + items.length; $('#a3Empty').hidden = shown > 0; // підсвітка активних чипів [].forEach.call(document.querySelectorAll('#fDomains .a3-chip'), function (b, i) { b.setAttribute('aria-pressed', i === 0 ? (!state.dom) : (P.domains[i - 1].id === state.dom)); }); var keys = Object.keys(P.statuses); [].forEach.call(document.querySelectorAll('#fStatus .a3-chip'), function (b, i) { b.setAttribute('aria-pressed', i === 0 ? (!state.st) : (keys[i - 1] === state.st)); }); } function deepLink() { var id = location.hash.slice(1); if (!id) return; var el = document.getElementById(id); if (!el || !el.classList.contains('a3-item')) return; el.querySelector('button').click(); setTimeout(function () { el.scrollIntoView({ block: 'center' }); }, 120); } function scene() { var cv = $('#a3Gl'); if (!cv || A3.reduced) return; var mobile = innerWidth < 760; var clusters = P.domains.map(function (d, i) { var a = (i / P.domains.length) * Math.PI * 2, r = 0.62 + (i % 3) * 0.34; return [Math.cos(a) * r * 1.5, Math.sin(a) * r * 0.72, Math.sin(a * 2) * 0.45, 0.2 + (i % 3) * 0.05]; }); var palette = P.domains.map(function (d) { var h = d.color.slice(1); return [parseInt(h.slice(0, 2), 16) / 255, parseInt(h.slice(2, 4), 16) / 255, parseInt(h.slice(4, 6), 16) / 255]; }); var g = A3.gl(cv, { count: mobile ? 1800 : 4200, clusters: clusters, palette: palette }); if (!g) return; cv.classList.add('on'); g.start(); g.formation('constellation'); g.zoom(mobile ? .7 : .9); if (!A3.coarse) addEventListener('pointermove', function (e) { g.pointer((e.clientX / innerWidth - .5) * .45, -(e.clientY / innerHeight - .5) * .45); }, { passive: true }); document.addEventListener('visibilitychange', function () { document.hidden ? g.stop() : g.start(); }); } function init() { render(); buildFilters(); sync(); deepLink(); A3.motion(); A3.counters(); A3.cursor(); scene(); } if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init); else init(); })(window.A3 = window.A3 || {}); ;A3.langSwitch({"current": "uk", "order": ["uk", "en", "da"], "alt": {"uk": "/uk/proekty/", "en": "/projects/", "da": "/da/projekter/"}});