{"id":443242,"date":"2024-12-26T06:42:31","date_gmt":"2024-12-26T06:42:31","guid":{"rendered":"http:\/\/savepearlharbor.com\/?p=443242"},"modified":"-0001-11-30T00:00:00","modified_gmt":"-0001-11-29T21:00:00","slug":"","status":"publish","type":"post","link":"https:\/\/savepearlharbor.com\/?p=443242","title":{"rendered":"<span>\u0421\u043e\u0437\u0434\u0430\u0435\u043c Hamster Kombat \u043f\u043e\u0447\u0442\u0438 \u0441 \u043d\u0443\u043b\u044f. \u041f\u0440\u0430\u043a\u0442\u0438\u043a\u0430 \u043f\u043e Vue 3 \u0438 Telegram Mini Apps<\/span>"},"content":{"rendered":"<div><!--[--><!--]--><\/div>\n<div id=\"post-content-body\">\n<div>\n<div class=\"article-formatted-body article-formatted-body article-formatted-body_version-1\">\n<div xmlns=\"http:\/\/www.w3.org\/1999\/xhtml\">\n<div style=\"text-align:center;\"><img decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w780q1\/webt\/jy\/9i\/ml\/jy9imlzzajnk9qjswptwth81zks.jpeg\" data-src=\"https:\/\/habrastorage.org\/webt\/jy\/9i\/ml\/jy9imlzzajnk9qjswptwth81zks.jpeg\" data-blurred=\"true\"\/><\/div>\n<p> \u041f\u0440\u0438\u0432\u0435\u0442, \u0425\u0430\u0431\u0440! \u0412 \u044d\u0442\u043e\u0439 \u0441\u0442\u0430\u0442\u044c\u0435-\u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u0438 \u0432\u044b \u0443\u0437\u043d\u0430\u0435\u0442\u0435, \u043a\u0430\u043a \u0441 \u043d\u0443\u043b\u044f \u0441\u0434\u0435\u043b\u0430\u0442\u044c \u0441\u0432\u043e\u044e \u0441\u043e\u0431\u0441\u0442\u0432\u0435\u043d\u043d\u0443\u044e Telegram-\u0442\u0430\u043f\u0430\u043b\u043a\u0443 \u043d\u0430 \u0441\u043e\u0432\u0440\u0435\u043c\u0435\u043d\u043d\u043e\u043c \u0441\u0442\u0435\u043a\u0435. \u0412\u0430\u0436\u043d\u044b\u0439 \u0434\u0438\u0441\u043a\u043b\u0435\u0439\u043c\u0435\u0440: \u0442\u0430\u043f\u0430\u043b\u043a\u0430, \u043a\u043b\u0438\u043a\u0435\u0440 \u0438 \u043f\u0440\u043e\u0447\u0435\u0435 \u2014 \u044d\u0442\u043e \u0432\u0441\u0435\u0433\u043e \u043b\u0438\u0448\u044c \u0444\u043e\u0440\u043c\u0430. \u0426\u0435\u043b\u044c \u0441\u0442\u0430\u0442\u044c\u0438 \u2014 \u0434\u0430\u0442\u044c \u0432\u0441\u0435\u043e\u0431\u044a\u0435\u043c\u043b\u044e\u0449\u0438\u0439 \u043f\u0440\u0430\u043a\u0442\u0438\u043a\u0443\u043c \u043f\u043e \u0441\u043e\u0432\u0440\u0435\u043c\u0435\u043d\u043d\u043e\u043c\u0443 \u0441\u0442\u0435\u043a\u0443 \u0438 \u0434\u0435\u043f\u043b\u043e\u044e \u043f\u0440\u043e\u0435\u043a\u0442\u043e\u0432 <a href=\"https:\/\/selectel.ru\/services\/cloud\/?utm_source=habr.com&amp;utm_medium=referral&amp;utm_campaign=cloud_article_fstap_251224_content\">\u0432 \u043e\u0431\u043b\u0430\u043a\u043e<\/a>.<\/p>\n<p> \u0412\u043d\u0443\u0442\u0440\u0438 \u0441\u0442\u0430\u0442\u044c\u0438 \u2014 \u043f\u043e\u043b\u043d\u043e\u0446\u0435\u043d\u043d\u044b\u0439 Serverless-\u043f\u043e\u0434\u0445\u043e\u0434, \u0440\u0430\u0437\u0440\u0430\u0431\u043e\u0442\u043a\u0430 \u0431\u043e\u0442\u0430 \u043d\u0430 Node \u0438 \u043f\u043e\u043b\u043d\u044b\u0439 \u0446\u0438\u043a\u043b \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u044f FE-\u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f. \u0410 \u0435\u0449\u0435 \u043a\u043e\u043c\u043c\u0435\u043d\u0442\u0430\u0440\u0438\u0438 \u043f\u043e \u0430\u0440\u0445\u0438\u0442\u0435\u043a\u0442\u0443\u0440\u043d\u044b\u043c \u0438 \u0442\u0430\u043a\u0442\u0438\u0447\u0435\u0441\u043a\u0438\u043c \u0440\u0435\u0448\u0435\u043d\u0438\u044f\u043c, \u0447\u0442\u043e\u0431\u044b \u0432\u044b \u043f\u0440\u043e\u043a\u0430\u0447\u0430\u043b\u0438 \u0443\u0440\u043e\u0432\u0435\u043d\u044c \u043f\u0440\u043e\u0433\u0440\u0430\u043c\u043c\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f \u0438 \u043d\u0430\u0441\u043c\u043e\u0442\u0440\u0435\u043d\u043d\u043e\u0441\u0442\u0438. \u041f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0441\u0442\u0438 \u043f\u043e\u0434 \u043a\u0430\u0442\u043e\u043c!<a name=\"habracut\"><\/a><\/p>\n<p> <font color=\"#EB4247\"><\/p>\n<h2>\u0417\u0430\u0447\u0435\u043c \u0438\u0437\u0443\u0447\u0430\u0442\u044c<\/h2>\n<p><\/font><br \/> <b>\u0414\u043b\u044f \u043d\u0430\u0447\u0438\u043d\u0430\u044e\u0449\u0438\u0445.<\/b> \u0412\u044b \u0441\u043c\u043e\u0436\u0435\u0442\u0435 \u043e\u043a\u0443\u043d\u0443\u0442\u044c\u0441\u044f \u0432 \u043f\u043e\u043b\u043d\u044b\u0439 \u0446\u0438\u043a\u043b \u0440\u0430\u0437\u0440\u0430\u0431\u043e\u0442\u043a\u0438 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0439, \u0443\u0432\u0438\u0434\u0435\u0442\u044c \u043f\u043e\u0448\u0430\u0433\u043e\u0432\u043e\u0435 \u043f\u043e\u044f\u0432\u043b\u0435\u043d\u0438\u0435 \u0438 \u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u0435 \u0442\u0435\u0445\u043d\u043e\u043b\u043e\u0433\u0438\u0439.<\/p>\n<p> <b>\u0414\u043b\u044f \u0431\u043e\u043b\u0435\u0435 \u043e\u043f\u044b\u0442\u043d\u044b\u0445 (Junior+).<\/b> \u0412 \u0446\u0435\u043b\u043e\u043c, \u0442\u043e\u0442 \u0436\u0435 \u0441\u043c\u044b\u0441\u043b. \u0410 \u0442\u0430\u043a\u0436\u0435 \u0432\u044b \u0441\u043c\u043e\u0436\u0435\u0442\u0435 \u0438\u0437\u0443\u0447\u0438\u0442\u044c Vue, Telegram SDK, Supabase \u0438 Ubuntu setup.<\/p>\n<div style=\"text-align:center;\"><img decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/webt\/vf\/s-\/jd\/vfs-jdqaw4tehlnwz8qxojbok-8.png\" data-src=\"https:\/\/habrastorage.org\/webt\/vf\/s-\/jd\/vfs-jdqaw4tehlnwz8qxojbok-8.png\"\/><\/div>\n<p> <i>\u041a\u0430\u043a\u043e\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u0441\u044f \u043f\u043e \u0438\u0442\u043e\u0433\u0443 \u0441\u0442\u0430\u0442\u044c\u0438.<\/i><\/p>\n<p> \u041d\u0430 \u0432\u044b\u0445\u043e\u0434\u0435 \u0443 \u0432\u0430\u0441 \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u0441\u044f \u043d\u0430 98% production ready \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u0438 \u0448\u0430\u0431\u043b\u043e\u043d, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0432\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u0430\u0434\u0430\u043f\u0442\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u043f\u043e\u0434 \u0441\u0432\u043e\u044e \u0438\u0433\u0440\u0443, \u0431\u0438\u0437\u043d\u0435\u0441-\u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u0438\u043b\u0438 \u0441\u0435\u0440\u0432\u0438\u0441.<\/p>\n<blockquote><p><b>\u041f\u043e\u043b\u043d\u044b\u0439 \u0442\u0435\u0445\u043d\u043e\u043b\u043e\u0433\u0438\u0447\u0435\u0441\u043a\u0438\u0439 \u0441\u0442\u0435\u043a, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u043c\u044b \u0431\u0443\u0434\u0435\u043c \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c:<\/b> Vue 3, Supabase, Firebase Deploy, Pinia, Docker, <a href=\"https:\/\/selectel.ru\/services\/cloud\/servers\/vps-hosting\/?utm_source=habr.com&amp;utm_medium=referral&amp;utm_campaign=cloud_article_fstap_251224_content\">\u0432\u0438\u0440\u0442\u0443\u0430\u043b\u044c\u043d\u044b\u0435 \u0441\u0435\u0440\u0432\u0435\u0440\u044b Selectel<\/a>, Node.js, Telegram Mini Apps, Lodash, Telegraf.<\/p>\n<p> <b>\u0424\u0443\u043d\u043a\u0446\u0438\u043e\u043d\u0430\u043b, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u043c\u044b \u0441\u043e\u0437\u0434\u0430\u0434\u0438\u043c:<\/b> Single Page Application (SPA) \u0434\u043b\u044f Telegram \u043d\u0430 Vue 3, \u0440\u0435\u0444\u0435\u0440\u0430\u043b\u044c\u043d\u0430\u044f \u0441\u0438\u0441\u0442\u0435\u043c\u0430, \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 \u0437\u0430\u0434\u0430\u043d\u0438\u0439.<\/p><\/blockquote>\n<p> \u0421 \u0432\u0432\u0435\u0434\u0435\u043d\u0438\u0435\u043c \u0437\u0430\u043a\u043e\u043d\u0447\u0438\u043b\u0438 \u2014 \u0434\u0430\u0432\u0430\u0439\u0442\u0435 \u043f\u0440\u0438\u0441\u0442\u0443\u043f\u0430\u0442\u044c \u043a \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u044e!<\/p>\n<p> <a href=\"https:\/\/selectel.ru\/services\/cloud\/servers\/?utm_source=habr.com&amp;utm_medium=referral&amp;utm_campaign=cloud_article_fstap_251224_banner_001_ord\"><\/p>\n<div style=\"text-align:center;\"><img decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/webt\/di\/ei\/ks\/dieiksvcuar3umm3kjj24s37br8.png\" data-src=\"https:\/\/habrastorage.org\/webt\/di\/ei\/ks\/dieiksvcuar3umm3kjj24s37br8.png\"\/><\/div>\n<p><\/a><\/p>\n<p> <font color=\"#EB4247\"><\/p>\n<h2>\u041a\u0430\u0447\u0430\u0435\u043c \u0438\u0441\u0445\u043e\u0434\u043d\u044b\u0439 \u043a\u043e\u0434 \u0441\u043e \u0441\u0442\u0438\u043b\u044f\u043c\u0438<\/h2>\n<p><\/font><br \/> \u0414\u043b\u044f \u043d\u0430\u0447\u0430\u043b\u0430 \u043f\u043e\u0434\u0433\u043e\u0442\u043e\u0432\u0438\u043c \u0448\u0430\u0431\u043b\u043e\u043d \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u0430 \u0442\u0430\u043f\u0430\u043b\u043a\u0438: \u0437\u0430\u0433\u0440\u0443\u0437\u0438\u043c \u0441\u0442\u0438\u043b\u0438, \u0441\u043e\u0437\u0434\u0430\u0434\u0438\u043c \u0433\u043b\u0430\u0432\u043d\u0443\u044e \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0443 \u0438 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u043c \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u0435 \u0442\u0435\u043a\u0443\u0449\u0435\u0433\u043e \u0441\u0447\u0435\u0442\u0430. \u041f\u043e\u043b\u043d\u044b\u0439 \u043a\u043e\u0434 \u043f\u0440\u043e\u0435\u043a\u0442\u0430 \u0438\u0437 \u043f\u0440\u0435\u0434\u044b\u0434\u0443\u0449\u0435\u0433\u043e \u0432\u0438\u0434\u0435\u043e \u0440\u0430\u0441\u043f\u043e\u043b\u043e\u0436\u0435\u043d <a href=\"https:\/\/github.com\/vladilenm\/lizzard-clicker\/tree\/vue-init-template\">\u0432 \u0440\u0435\u043f\u043e\u0437\u0438\u0442\u043e\u0440\u0438\u0438 \u043d\u0430 GitHub<\/a>.<\/p>\n<p> \u041a\u043b\u043e\u043d\u0438\u0440\u0443\u0439\u0442\u0435 \u0438\u0437 \u0440\u0435\u043f\u043e\u0437\u0438\u0442\u043e\u0440\u0438\u044f \u0438\u0441\u0445\u043e\u0434\u043d\u044b\u0439 \u043a\u043e\u0434 \u0441\u043e \u0441\u0442\u0438\u043b\u044f\u043c\u0438 \u0432 \u0441\u0432\u043e\u044e \u043f\u0430\u043f\u043a\u0443, \u0437\u0430\u0442\u0435\u043c \u0432\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u0435 \u0434\u0432\u0435 \u043a\u043e\u043c\u0430\u043d\u0434\u044b \u0432 \u043a\u043e\u043d\u0441\u043e\u043b\u0438:<\/p>\n<pre><code class=\"plaintext\">\u200b\u200b\u200b\u200bnpm install \u200b\u200b\u200b\u200b \u200b\u200b\u200b\u200bnpm run dev\u200b\u200b\u200b\u200b <\/code><\/pre>\n<p> <font color=\"#EB4247\"><\/p>\n<h2>\u0414\u0435\u043b\u0430\u0435\u043c \u0433\u043b\u0430\u0432\u043d\u0443\u044e \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0443 \u0438 \u0441\u0447\u0435\u0442<\/h2>\n<p><\/font><br \/> \u0421\u043b\u0435\u0434\u0443\u044e\u0449\u0438\u043c \u0448\u0430\u0433\u043e\u043c \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0441\u043e\u0437\u0434\u0430\u0442\u044c \u0433\u043b\u0430\u0432\u043d\u0443\u044e \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0443 \u0438 \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u0435 \u0442\u0435\u043a\u0443\u0449\u0435\u0433\u043e \u0441\u0447\u0435\u0442\u0430. \u0414\u043b\u044f \u044d\u0442\u043e\u0433\u043e \u043f\u043e\u043d\u0430\u0434\u043e\u0431\u0438\u0442\u0441\u044f \u0432\u044b\u0447\u0438\u0441\u043b\u0438\u0442\u044c \u0442\u0435\u043a\u0443\u0449\u0438\u0439 \u0443\u0440\u043e\u0432\u0435\u043d\u044c \u0438\u0433\u0440\u044b \u0438 \u043d\u0430\u0431\u0440\u0430\u043d\u043d\u044b\u0439 \u0441\u0447\u0435\u0442, \u0430 \u0442\u0430\u043a\u0436\u0435 \u043d\u0430\u0439\u0442\u0438 \u0444\u043e\u0440\u043c\u0443\u043b\u0443 \u043f\u043e \u043f\u043e\u0434\u0441\u0447\u0435\u0442\u0443 \u0443\u0440\u043e\u0432\u043d\u044f.<\/p>\n<h3>\u0412\u044b\u0447\u0438\u0441\u043b\u0435\u043d\u0438\u0435 \u0443\u0440\u043e\u0432\u043d\u044f: store\/score.js<\/h3>\n<p> <\/p>\n<pre><code class=\"javascript\">\/\/ \u0431\u0430\u0437\u043e\u0432\u044b\u0439 \u0441\u0447\u0435\u0442 \u043f\u0435\u0440\u0432\u043e\u0433\u043e \u0443\u0440\u043e\u0432\u043d\u044f const baseLevelScore = 25  \/\/ \u0444\u043e\u0440\u043c\u0443\u043b\u0430 \u043f\u043e \u0432\u044b\u0447\u0438\u0441\u043b\u0435\u043d\u0438\u044e \u0443\u0440\u043e\u0432\u043d\u0435\u0439 \u0438 \u0441\u043a\u043e\u043b\u044c\u043a\u043e \u043d\u0443\u0436\u043d\u043e \u043e\u0447\u043a\u043e\u0432 \u0434\u043b\u044f \u043a\u0430\u0436\u0434\u043e\u0433\u043e const levels = new Array(15)   .fill(0)   .map((_, i) =&gt; baseLevelScore * Math.pow(2, i))  \/\/ \u0432\u044b\u0447\u0438\u0441\u043b\u044f\u0435\u043c \u0441\u0443\u043c\u043c\u0443, \u0441\u043a\u043e\u043b\u044c\u043a\u043e \u043d\u0443\u0436\u043d\u043e \u0441\u0443\u043c\u043c\u0430\u0440\u043d\u043e \u043e\u0447\u043a\u043e\u0432 \u043d\u0430 \u043a\u0430\u0436\u0434\u044b\u0439 \u0443\u0440\u043e\u0432\u0435\u043d\u044c const levelScores = levels.map((_, level) =&gt; {   let sum = 0   for (let [index, value] of levels.entries()) {     if (index &gt;= level) {       return sum + value     }     sum += value   }   return sum })  \/\/ \u0432\u044b\u0447\u0438\u0441\u043b\u044f\u0435\u043c \u0443\u0440\u043e\u0432\u0435\u043d\u044c \u0432 \u0437\u0430\u0432\u0438\u0441\u0438\u043c\u043e\u0441\u0442\u0438 \u043e\u0442 \u0442\u0435\u043a\u0443\u0449\u0435\u0433\u043e \u0441\u0447\u0435\u0442\u0430 function computeLevelByScore(score) {   for (let [index, value] of levelScores.entries()) {     if (score &lt;= value) {       return {         level: index,         value: levels[index],       }     }   } } <\/code><\/pre>\n<h3>\u041e\u0440\u0433\u0430\u043d\u0438\u0437\u0430\u0446\u0438\u044f store: store\/score.js<\/h3>\n<p> <\/p>\n<pre><code class=\"javascript\">export const useScoreStore = defineStore('score', {   state: () =&gt; ({     score: 0, \/\/ \u0431\u0430\u0437\u043e\u0432\u044b\u0439 \u0443\u0440\u043e\u0432\u0435\u043d\u044c, \u043f\u043e\u0441\u043b\u0435 \u0431\u0443\u0434\u0435\u043c \u043f\u043e\u043b\u0443\u0447\u0430\u0442\u044c \u043f\u043e API   }),   getters: {     level(state) {       return computeLevelByScore(state.score)     },     \/\/ \u044d\u0442\u043e\u0442 \u0441\u0447\u0435\u0442 \u043d\u0443\u0436\u0435\u043d \u0434\u043b\u044f \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u044f \u0442\u0435\u043a\u0443\u0449\u0435\u0433\u043e \u043f\u0440\u043e\u0433\u0440\u0435\u0441\u0441\u0430     currentScore(state) {        if (this.level.level === 0) {         return state.score       }       return state.score - levelScores[this.level.level - 1]     },   },   actions: {     add(score = 1) {       this.score += score     },     setScore(score) {       this.score = score     },   }, }) <\/code><\/pre>\n<h3>\u0413\u043b\u0430\u0432\u043d\u0430\u044f \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0430: HomeView.vue<\/h3>\n<p> <\/p>\n<pre><code class=\"javascript\">&lt;template&gt;   &lt;div class=\"game-container\"&gt;     &lt;ScoreProgress \/&gt;     &lt;div class=\"header\"&gt;       &lt;img src=\"..\/assets\/coin.png\" alt=\"coin\" \/&gt;       &lt;h2 class=\"score\" id=\"score\"&gt;{{ store.score }}&lt;\/h2&gt;     &lt;\/div&gt;     &lt;div class=\"circle\"&gt;       &lt;img @click=\"increment\" ref=\"img\" id=\"circle\" :src=\"imgSrc\" \/&gt;     &lt;\/div&gt;   &lt;\/div&gt; &lt;\/template&gt;  &lt;script setup&gt; import { ref, computed } from 'vue' import frog from '..\/assets\/frog.png' import lizzard from '..\/assets\/lizzard.png' import { useScoreStore } from '@\/stores\/score' import ScoreProgress from '@\/components\/ScoreProgress.vue'  \/\/ \u043f\u043e\u043b\u0443\u0447\u0430\u0435\u043c \u0442\u0435\u043a\u0443\u0449\u0438\u0439 \u0441\u0447\u0435\u0442 const store = useScoreStore()  \/\/ \u0435\u0441\u043b\u0438 \u0441\u0447\u0435\u0442 \u0431\u043e\u043b\u044c\u0448\u0435 25 \u043c\u0435\u043d\u044f\u0435\u043c \u043a\u0430\u0440\u0442\u0438\u043d\u043a\u0443. \u041f\u0435\u0440\u0432\u044b\u0439 \u0443\u0440\u043e\u0432\u0435\u043d\u044c const imgSrc = computed(() =&gt; (store.score &gt; 25 ? lizzard : frog)) const img = ref(null)  function increment(event) {   \/\/ \u043f\u0440\u0438 \u043a\u043b\u0438\u043a\u0435 \u0443\u0432\u0435\u043b\u0438\u0447\u0438\u0432\u0430\u0435\u043c \u0441\u0447\u0435\u0442   store.add(1)    \/\/ \u0434\u0430\u043b\u044c\u0448\u0435 \u043b\u043e\u0433\u0438\u043a\u0430 \u0430\u043d\u0438\u043c\u0430\u0446\u0438\u0438 \u0438\u0437 \u043f\u0440\u043e\u0448\u043b\u043e\u0433\u043e \u0440\u043e\u043b\u0438\u043a\u0430   \/\/ https:\/\/youtu.be\/vT-XwvcK2NI   const rect = event.target.getBoundingClientRect()    const offfsetX = event.clientX - rect.left - rect.width \/ 2   const offfsetY = event.clientY - rect.top - rect.height \/ 2    const DEG = 40    const tiltX = (offfsetY \/ rect.height) * DEG   const tiltY = (offfsetX \/ rect.width) * -DEG    img.value.style.setProperty('--tiltX', `${tiltX}deg`)   img.value.style.setProperty('--tiltY', `${tiltY}deg`)    setTimeout(() =&gt; {     img.value.style.setProperty('--tiltX', `0deg`)     img.value.style.setProperty('--tiltY', `0deg`)   }, 300)    const plusOne = document.createElement('div')   plusOne.classList.add('plus-one')   plusOne.textContent = '+1'   plusOne.style.left = `${event.clientX - rect.left}px`   plusOne.style.top = `${event.clientY - rect.top}px`    img.value.parentElement.appendChild(plusOne)    setTimeout(() =&gt; plusOne.remove(), 2000) } &lt;\/script&gt; <\/code><\/pre>\n<p> \u0418 \u0434\u043e\u0431\u0430\u0432\u043b\u044f\u0435\u043c \u0432\u0438\u0437\u0443\u0430\u043b\u044c\u043d\u043e\u0435 \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u0435 \u043f\u0440\u043e\u0433\u0440\u0435\u0441\u0441\u0430 \u0432 ScoreProgress.vue.<\/p>\n<h3>components\/ScoreProgress.vue<\/h3>\n<p> <\/p>\n<pre><code class=\"javascript\">&lt;template&gt;   &lt;div class=\"progress\"&gt;     &lt;h4 class=\"progress-level\"&gt;       &lt;span&gt;{{ store.currentScore }}\/{{ store.level.value }}&lt;\/span&gt;       &lt;span&gt;{{ store.level.level + 1 }}&lt;\/span&gt;     &lt;\/h4&gt;     &lt;div class=\"progress-container\"&gt;       &lt;div class=\"progress-value\" :style=\"{ width: progress + '%' }\"&gt;&lt;\/div&gt;     &lt;\/div&gt;   &lt;\/div&gt; &lt;\/template&gt;  &lt;script setup&gt; import { useScoreStore } from '@\/stores\/score' import { computed } from 'vue'    const store = useScoreStore()  const progress = computed(() =&gt; (100 * store.currentScore) \/ store.level.value) &lt;\/script&gt; <\/code><\/pre>\n<p> <font color=\"#EB4247\"><\/p>\n<h2>\u0421\u0442\u0440\u0430\u043d\u0438\u0446\u0430 \u0437\u0430\u0434\u0430\u0447 \u0438 \u0434\u0440\u0443\u0437\u0435\u0439-\u0440\u0435\u0444\u0435\u0440\u0430\u043b\u043e\u0432<\/h2>\n<p><\/font> <\/p>\n<h3>\u0414\u0435\u043f\u043b\u043e\u0438\u043c \u0444\u0440\u043e\u043d\u0442\u0435\u043d\u0434<\/h3>\n<p> \u041f\u0440\u0435\u0436\u0434\u0435 \u0437\u0430\u043b\u044c\u0435\u043c \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u043d\u0430 Firebase \u2014 \u0432 \u0434\u0430\u043d\u043d\u043e\u043c \u0441\u043b\u0443\u0447\u0430\u0435 \u043f\u043b\u0430\u0442\u0444\u043e\u0440\u043c\u0430 \u0432\u044b\u0441\u0442\u0443\u043f\u0438\u0442 \u0432 \u0440\u043e\u043b\u0438 \u0431\u0435\u0441\u043f\u043b\u0430\u0442\u043d\u043e\u0433\u043e \u0438 \u0443\u0434\u043e\u0431\u043d\u043e\u0433\u043e \u0445\u043e\u0441\u0442\u0438\u043d\u0433\u0430 \u0434\u043b\u044f \u0444\u0440\u043e\u043d\u0442\u0435\u043d\u0434\u0430.<\/p>\n<pre><code class=\"javascript\">firebase init firebase deploy <\/code><\/pre>\n<p> \u041f\u043e \u0438\u0442\u043e\u0433\u0443 \u043f\u043e\u043b\u0443\u0447\u0430\u0435\u043c url \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f.<\/p>\n<h3>\u0421\u043e\u0437\u0434\u0430\u0435\u043c \u0431\u043e\u0442\u0430 \u043d\u0430 Node.js<\/h3>\n<p> \u0411\u043e\u0442 \u043d\u0430\u043c \u043f\u043e\u043d\u0430\u0434\u043e\u0431\u0438\u0442\u0441\u044f \u0434\u043b\u044f \u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0438 \u0440\u0435\u0444\u0435\u0440\u0430\u043b\u044c\u043d\u043e\u0439 \u043f\u0440\u043e\u0433\u0440\u0430\u043c\u043c\u044b.<\/p>\n<p> 1. \u041f\u043e\u043b\u0443\u0447\u0430\u0435\u043c API-\u0442\u043e\u043a\u0435\u043d \u0432 \u0431\u043e\u0442\u0435 <a href=\"https:\/\/t.me\/BotFather\">@BotFather<\/a> \u0447\u0435\u0440\u0435\u0437 \u043a\u043e\u043c\u0430\u043d\u0434\u0443 \u200b\u200b\u200b\u200b\/newbot\u200b\u200b\u200b\u200b.<\/p>\n<p> 2. \u0414\u0430\u043b\u0435\u0435 \u0443\u0441\u0442\u0430\u043d\u0430\u0432\u043b\u0438\u0432\u0430\u0435\u043c telegraf \u0438 \u043e\u043f\u0438\u0441\u044b\u0432\u0430\u0435\u043c \u0431\u0430\u0437\u043e\u0432\u0443\u044e \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443 \u0431\u043e\u0442\u0430 \u0432 \u0444\u0430\u0439\u043b\u0435 bot\/app.js:<\/p>\n<pre><code class=\"javascript\">\/bot\/app.js import { Telegraf, Markup } from 'telegraf'  const token = 'YOUR TELEGRAM TOKEN' const webAppUrl = 'APP REMOTE URL'  const bot = new Telegraf(token)  bot.command('start', (ctx) =&gt; {   ctx.reply(     '\u041f\u0440\u0438\u0432\u0435\u0442! \u041d\u0430\u0436\u043c\u0438, \u0447\u0442\u043e\u0431 \u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u044c',     Markup.inlineKeyboard([       Markup.button.webApp(         '\u041e\u0442\u043a\u0440\u044b\u0442\u044c \u043c\u0438\u043d\u0438-\u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435',         `${webAppUrl}?ref=${ctx.payload}` \/\/ \u0417\u0434\u0435\u0441\u044c \u0432 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0435 ref \u043f\u0435\u0440\u0435\u0434\u0430\u0435\u043c \u0440\u0435\u0444\u0435\u0440\u0430\u043b\u0430 \u0432 \u043c\u0438\u043d\u0438-\u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435       ),     ])   ) })  bot.launch() <\/code><\/pre>\n<p> \u0414\u043b\u044f \u0437\u0430\u043f\u0443\u0441\u043a\u0430 \u0431\u043e\u0442\u0430 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c \u043a\u043e\u043c\u0430\u043d\u0434\u0443:<\/p>\n<pre><code class=\"plaintext\">node app <\/code><\/pre>\n<p> <\/p>\n<h3>\u0414\u0435\u043f\u043b\u043e\u0439 \u0431\u043e\u0442\u0430 \u043d\u0430 \u0441\u0435\u0440\u0432\u0435\u0440<\/h3>\n<p> \u0421\u0435\u0439\u0447\u0430\u0441 \u0431\u043e\u0442 \u0437\u0430\u043f\u0443\u0449\u0435\u043d \u043d\u0430 \u043a\u043e\u043c\u043f\u044c\u044e\u0442\u0435\u0440\u0435. \u041e\u0434\u043d\u0430\u043a\u043e \u0435\u0441\u043b\u0438 \u0432\u044b \u0445\u043e\u0442\u0438\u0442\u0435, \u0447\u0442\u043e\u0431\u044b \u043a\u043b\u0438\u043a\u0435\u0440 \u0431\u044b\u043b \u0434\u043e\u0441\u0442\u0443\u043f\u0435\u043d \u043a\u0440\u0443\u0433\u043b\u043e\u0441\u0443\u0442\u043e\u0447\u043d\u043e, \u043f\u0440\u0438\u0434\u0435\u0442\u0441\u044f \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0442\u044c \u0435\u0433\u043e \u0431\u0435\u0441\u043f\u0435\u0440\u0435\u0431\u043e\u0439\u043d\u0443\u044e \u0440\u0430\u0431\u043e\u0442\u0443 \u0438 \u043d\u0435\u043f\u0440\u0435\u0440\u044b\u0432\u043d\u043e\u0435 \u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u0435 \u0441 \u0438\u043d\u0442\u0435\u0440\u043d\u0435\u0442\u043e\u043c. \u041e\u043f\u0442\u0438\u043c\u0430\u043b\u044c\u043d\u044b\u0439 \u0432\u0430\u0440\u0438\u0430\u043d\u0442 \u2014 \u043f\u0435\u0440\u0435\u043d\u0435\u0441\u0442\u0438 \u0431\u043e\u0442\u0430 <a href=\"https:\/\/selectel.ru\/services\/cloud\/servers\/vps-hosting\/?utm_source=habr.com&amp;utm_medium=referral&amp;utm_campaign=cloud_article_fstap_251224_content\">\u0432 \u043e\u0431\u043b\u0430\u043a\u043e<\/a>.<\/p>\n<p> \u0411\u0443\u0434\u0435\u043c \u0434\u0435\u043f\u043b\u043e\u0438\u0442\u044c \u0431\u043e\u0442\u0430 \u0432 Docker \u2014 \u0434\u043b\u044f \u044d\u0442\u043e\u0433\u043e \u0434\u043e\u0431\u0430\u0432\u0438\u043c \u0434\u0432\u0430 \u0444\u0430\u0439\u043b\u0430:<\/p>\n<p> <b>Dockerfile<\/b> <\/p>\n<pre><code class=\"plaintext\">FROM node:16-alpine WORKDIR \/app COPY package*.json .\/ RUN npm ci COPY . . ENV PORT=3000 EXPOSE $PORT CMD [\"node\", \"app.js\"] <\/code><\/pre>\n<p> <b>Makefile<\/b> <\/p>\n<pre><code class=\"plaintext\">build:         docker build -t tgbot . run:         docker run -d -p 3000:3000 --name tgbot --rm tgbot <\/code><\/pre>\n<p> \u0414\u0430\u043b\u0435\u0435 \u0437\u0430\u043b\u044c\u0435\u043c \u0432\u0435\u0441\u044c \u043f\u0440\u043e\u0435\u043a\u0442 \u0432 GitHub-\u0440\u0435\u043f\u043e\u0437\u0438\u0442\u043e\u0440\u0438\u0439, \u0447\u0442\u043e\u0431\u044b \u043c\u043e\u0436\u043d\u043e \u0431\u044b\u043b\u043e \u043b\u0435\u0433\u043a\u043e \u043f\u0435\u0440\u0435\u043d\u0435\u0441\u0442\u0438 \u043f\u0440\u043e\u0435\u043a\u0442 \u043d\u0430 \u0441\u0435\u0440\u0432\u0435\u0440 \u2014 \u0431\u0435\u0437 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u044f FTP \u0438 scp. \u041f\u0435\u0440\u0435\u0439\u0434\u0435\u043c \u043a \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0435\u043c\u0443 \u044d\u0442\u0430\u043f\u0443.<\/p>\n<p> 1. \u041f\u0435\u0440\u0435\u0445\u043e\u0434\u0438\u043c \u0432 \u0440\u0430\u0437\u0434\u0435\u043b <b>\u041e\u0431\u043b\u0430\u0447\u043d\u0430\u044f \u043f\u043b\u0430\u0442\u0444\u043e\u0440\u043c\u0430<\/b> \u0432\u043d\u0443\u0442\u0440\u0438 <a href=\"https:\/\/my.selectel.ru\/\">\u043f\u0430\u043d\u0435\u043b\u0438 \u0443\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u044f<\/a>.<\/p>\n<div style=\"text-align:center;\"><img decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/webt\/il\/-w\/th\/il-wthfm2zbhx8fcegcdizwcajg.png\" data-src=\"https:\/\/habrastorage.org\/webt\/il\/-w\/th\/il-wthfm2zbhx8fcegcdizwcajg.png\"\/><\/div>\n<p> 2. \u0421\u043e\u0437\u0434\u0430\u0435\u043c \u0441\u0435\u0440\u0432\u0435\u0440. \u0414\u043b\u044f \u0440\u0430\u0431\u043e\u0442\u044b \u043d\u0430\u0448\u0435\u0433\u043e \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u043d\u0435 \u043d\u0443\u0436\u043d\u043e \u043c\u043d\u043e\u0433\u043e \u043c\u043e\u0449\u043d\u043e\u0441\u0442\u0435\u0439, \u043f\u043e\u044d\u0442\u043e\u043c\u0443 \u0434\u043e\u0441\u0442\u0430\u0442\u043e\u0447\u043d\u043e \u043e\u0434\u043d\u043e\u0433\u043e \u044f\u0434\u0440\u0430 vCPU \u0441 \u0434\u043e\u043b\u0435\u0439 20% \u0438 512 \u041c\u0411 \u043e\u043f\u0435\u0440\u0430\u0442\u0438\u0432\u043d\u043e\u0439 \u043f\u0430\u043c\u044f\u0442\u0438. \u0418 \u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e \u0434\u043e\u0431\u0430\u0432\u043b\u044f\u0435\u043c \u043f\u0443\u0431\u043b\u0438\u0447\u043d\u044b\u0439 IP-\u0430\u0434\u0440\u0435\u0441, \u0447\u0442\u043e\u0431\u044b \u043a \u0441\u0435\u0440\u0432\u0435\u0440\u0443 \u043c\u043e\u0436\u043d\u043e \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u0447\u0435\u0440\u0435\u0437 \u0438\u043d\u0442\u0435\u0440\u043d\u0435\u0442.<\/p>\n<p> 3. \u0410\u0432\u0442\u043e\u0440\u0438\u0437\u0443\u0435\u043c\u0441\u044f \u043d\u0430 \u0441\u0435\u0440\u0432\u0435\u0440\u0435 \u0447\u0435\u0440\u0435\u0437 \u043a\u043e\u043d\u0441\u043e\u043b\u044c \u043f\u043e\u0441\u0440\u0435\u0434\u0441\u0442\u0432\u043e\u043c \u043a\u043e\u043c\u0430\u043d\u0434\u044b ssh root@. \u041f\u0443\u0431\u043b\u0438\u0447\u043d\u044b\u0439 \u0430\u0434\u0440\u0435\u0441 \u0432\u0438\u0440\u0442\u0443\u0430\u043b\u044c\u043d\u043e\u0439 \u043c\u0430\u0448\u0438\u043d\u044b \u043c\u043e\u0436\u043d\u043e \u043f\u043e\u0441\u043c\u043e\u0442\u0440\u0435\u0442\u044c \u0432 \u0440\u0430\u0437\u0434\u0435\u043b\u0435 <b>\u041f\u043e\u0440\u0442\u044b<\/b>.<\/p>\n<div style=\"text-align:center;\"><img decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/webt\/33\/8z\/nh\/338znhq_tljcgsj0ue9ewss-rhy.png\" data-src=\"https:\/\/habrastorage.org\/webt\/33\/8z\/nh\/338znhq_tljcgsj0ue9ewss-rhy.png\"\/><\/div>\n<p> 4. \u041f\u043e\u0441\u043b\u0435 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u0441\u0435\u0440\u0432\u0435\u0440\u0443 \u043e\u0431\u043d\u043e\u0432\u043b\u044f\u0435\u043c \u0441\u0438\u0441\u0442\u0435\u043c\u0443 \u0438 \u0443\u0441\u0442\u0430\u043d\u0430\u0432\u043b\u0438\u0432\u0430\u0435\u043c Git:<\/p>\n<pre><code class=\"plaintext\">apt update apt install git <\/code><\/pre>\n<p> 5. \u0423\u0441\u0442\u0430\u043d\u0430\u0432\u043b\u0438\u0432\u0430\u0435\u043c Node.js \u2014 \u043f\u043e\u043b\u043d\u0430\u044f \u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0430 <a href=\"https:\/\/selectel.ru\/blog\/tutorials\/how-to-install-node-js-on-ubuntu-20-04\/\">\u0432 \u0410\u043a\u0430\u0434\u0435\u043c\u0438\u0438 Selectel<\/a>.<\/p>\n<pre><code class=\"plaintext\">curl -o- &lt;https:\/\/raw.githubusercontent.com\/nvm-sh\/nvm\/v0.39.1\/install.sh&gt; | bash source ~\/.bashrc  nvm install 20  nvm use 20  npm -v  node -v <\/code><\/pre>\n<p> 6. \u0423\u0441\u0442\u0430\u043d\u0430\u0432\u043b\u0438\u0432\u0430\u0435\u043c \u043d\u0430 \u0441\u0435\u0440\u0432\u0435\u0440\u0435 Docker \u2014 \u0434\u043b\u044f \u044d\u0442\u043e\u0433\u043e \u0442\u043e\u0436\u0435 \u043c\u043e\u0436\u043d\u043e \u0432\u043e\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c\u0441\u044f <a href=\"https:\/\/selectel.ru\/blog\/what-is-docker\/#vmashine\">\u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u0435\u0439<\/a>.<\/p>\n<p> 7. \u041a\u043b\u043e\u043d\u0438\u0440\u0443\u0435\u043c \u043a\u043e\u0434 \u0441 \u043d\u0430\u0448\u0435\u0433\u043e \u043f\u0440\u0435\u0434\u0432\u0430\u0440\u0438\u0442\u0435\u043b\u044c\u043d\u043e \u0441\u043e\u0437\u0434\u0430\u043d\u043d\u043e\u0433\u043e \u0440\u0435\u043f\u043e\u0437\u0438\u0442\u043e\u0440\u0438\u044f \u043d\u0430 GitHub:<\/p>\n<pre><code class=\"plaintext\">apt install git git clone REPO_URL <\/code><\/pre>\n<p> 8. \u0417\u0430\u043f\u0443\u0441\u043a\u0430\u0435\u043c \u043f\u0440\u043e\u0435\u043a\u0442:<\/p>\n<pre><code class=\"plaintext\">cd PROJECT_NAME make build make run <\/code><\/pre>\n<p> \u0413\u043e\u0442\u043e\u0432\u043e \u2014 \u0431\u043e\u0442 c Telegram Mini Apps \u0437\u0430\u043f\u0443\u0449\u0435\u043d.<\/p>\n<p> <font color=\"#EB4247\"><\/p>\n<h2>\u0420\u0430\u0437\u0440\u0430\u0431\u043e\u0442\u043a\u0430 \u0444\u0443\u043d\u043a\u0446\u0438\u043e\u043d\u0430\u043b\u0430<\/h2>\n<p><\/font> <\/p>\n<blockquote><p>\u0420\u0430\u0437\u0440\u0430\u0431\u043e\u0442\u043a\u0430 \u0444\u0443\u043d\u043a\u0446\u0438\u043e\u043d\u0430\u043b\u0430 \u0442\u0430\u043f\u0430\u043b\u043a\u0438 \u2014 \u043c\u043d\u043e\u0433\u043e\u0443\u0440\u043e\u0432\u043d\u0435\u0432\u044b\u0439 \u043f\u0440\u043e\u0446\u0435\u0441\u0441, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0441\u043b\u043e\u0436\u043d\u043e \u043f\u043e\u043b\u043d\u043e\u0441\u0442\u044c\u044e \u043e\u0442\u0440\u0430\u0437\u0438\u0442\u044c \u0432 \u0442\u0435\u043a\u0441\u0442\u043e\u0432\u043e\u043c \u0444\u043e\u0440\u043c\u0430\u0442\u0435. \u0415\u0441\u043b\u0438 \u0443 \u0432\u0430\u0441 \u043d\u0430 \u043a\u0430\u043a\u043e\u043c-\u0442\u043e \u0438\u0437 \u0448\u0430\u0433\u043e\u0432 \u0432\u043e\u0437\u043d\u0438\u043a\u043b\u0438 \u0432\u043e\u043f\u0440\u043e\u0441\u044b, \u043e\u0431\u0440\u0430\u0442\u0438\u0442\u0435\u0441\u044c <a href=\"https:\/\/youtu.be\/6H_UBijkwm8?si=3uBOwyiIFGPGQ2xI\">\u043a \u0432\u0438\u0434\u0435\u043e \u043d\u0430 YouTube<\/a> \u2014 \u0442\u0430\u043c \u0432\u0441\u0435 \u043f\u043e\u043a\u0430\u0437\u0430\u043d\u043e \u0438 \u0440\u0430\u0437\u0431\u0438\u0442\u043e \u043d\u0430 \u0442\u0430\u0439\u043c\u043a\u043e\u0434\u044b.<\/p><\/blockquote>\n<p> <\/p>\n<h3>\u0428\u0430\u0433 1. \u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u0431\u0430\u0437\u044b \u0434\u0430\u043d\u043d\u044b\u0445 \u043a \u043a\u043b\u0438\u0435\u043d\u0442\u0443<\/h3>\n<p> \u0412 \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0435 \u0431\u0430\u0437\u044b \u0434\u0430\u043d\u043d\u044b\u0445 \u0431\u0443\u0434\u0435\u043c \u0434\u043b\u044f \u0445\u0440\u0430\u043d\u0435\u043d\u0438\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u0441\u043a\u0438\u0445 \u0431\u0430\u043b\u043b\u043e\u0432 \u0431\u0443\u0434\u0435\u043c \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c Supabase. \u042d\u0442\u043e open source-\u0430\u043d\u0430\u043b\u043e\u0433 \u0440\u0435\u0448\u0435\u043d\u0438\u044f \u043e\u0442 Google.<\/p>\n<p> \u0418\u043d\u0438\u0446\u0438\u0430\u043b\u0438\u0437\u0438\u0440\u0443\u0435\u043c \u043f\u0440\u043e\u0435\u043a\u0442 \u0438 \u0441\u043e\u0437\u0434\u0430\u0435\u043c \u0434\u0432\u0435 \u0442\u0430\u0431\u043b\u0438\u0446\u044b: User (\u0441\u043f\u0438\u0441\u043e\u043a \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u0435\u0439) \u0438 Task (\u0441\u043f\u0438\u0441\u043e\u043a \u0437\u0430\u0434\u0430\u0447).<\/p>\n<div style=\"text-align:center;\"><img decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/webt\/tt\/bu\/4n\/ttbu4nb-86svvxo90-8ymwh8-p8.png\" data-src=\"https:\/\/habrastorage.org\/webt\/tt\/bu\/4n\/ttbu4nb-86svvxo90-8ymwh8-p8.png\"\/><\/div>\n<p> <i>\u0421\u043e\u0437\u0434\u0430\u043d\u0438\u0435 \u0442\u0430\u0431\u043b\u0438\u0446\u044b User \u0432 Supabase.<\/i><\/p>\n<div style=\"text-align:center;\"><img decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/webt\/9z\/ks\/ns\/9zksnsa7s6adtxj_vsw584eynvm.png\" data-src=\"https:\/\/habrastorage.org\/webt\/9z\/ks\/ns\/9zksnsa7s6adtxj_vsw584eynvm.png\"\/><\/div>\n<p> <i>\u0421\u043e\u0437\u0434\u0430\u043d\u0438\u0435 \u0442\u0430\u0431\u043b\u0438\u0446\u044b Task \u0432 Supabase.<\/i><\/p>\n<p> \u041d\u0430 \u0441\u0442\u043e\u0440\u043e\u043d\u0435 \u0438\u043d\u0438\u0446\u0438\u0430\u043b\u0438\u0437\u0438\u0440\u0443\u0435\u043c \u0434\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0439 \u0441\u0435\u0440\u0432\u0438\u0441 services\/supabase.js:<\/p>\n<pre><code class=\"javascript\">const SUPABASE_URL = 'https:\/\/yodsvoxtwmjearffyxhj.supabase.co' const SUPABASE_API_KEY = 'SECRET' import { createClient } from '@supabase\/supabase-js' const supabase = createClient(SUPABASE_URL, SUPABASE_API_KEY) export default supabase <\/code><\/pre>\n<p> <\/p>\n<h3>\u0428\u0430\u0433 2. \u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 Telegram-\u0431\u0438\u0431\u043b\u0438\u043e\u0442\u0435\u043a\u0438<\/h3>\n<p> \u0414\u043b\u044f \u0442\u043e\u0433\u043e, \u0447\u0442\u043e\u0431\u044b \u0441\u043e\u0435\u0434\u0438\u043d\u0438\u0442\u044c \u0432\u0435\u0431-\u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u0441 \u0444\u0443\u043d\u043a\u0446\u0438\u043e\u043d\u0430\u043b\u043e\u043c Telegram Mini Apps, \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u043c \u0431\u0438\u0431\u043b\u0438\u043e\u0442\u0435\u043a\u0443:<\/p>\n<pre><code class=\"plaintext\">\u200b\u200b\u200b\u200b&lt;script src=\"https:\/\/telegram.org\/js\/telegram-web-app.js\"&gt;&lt;\/script&gt; <\/code><\/pre>\n<p> \u0427\u0442\u043e\u0431\u044b \u0435\u0439 \u0431\u044b\u043b\u043e \u0443\u0434\u043e\u0431\u043d\u0435\u0439 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c\u0441\u044f, \u0434\u0435\u043b\u0430\u0435\u043c \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u044b\u0439 \u0445\u0443\u043a:<\/p>\n<pre><code class=\"javascript\">services\/telegram.js export function useTelegram() {   const tg = window.Telegram.WebApp   return { tg, user: tg.initDataUnsafe?.user } } <\/code><\/pre>\n<p> <\/p>\n<h3>\u0428\u0430\u0433 3. \u0421\u043e\u0437\u0434\u0430\u043d\u0438\u0435 API-\u0437\u0430\u043f\u0440\u043e\u0441\u043e\u0432 \u043a \u0431\u0430\u0437\u0435<\/h3>\n<p> \u041e\u043f\u0438\u0448\u0435\u043c \u0432\u0441\u0435 \u0437\u0430\u043f\u0440\u043e\u0441\u044b, \u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u0431\u0443\u0434\u0435\u0442 \u043e\u0442\u043f\u0440\u0430\u0432\u043b\u044f\u0442\u044c \u043d\u0430\u0448\u0435 \u0432\u0435\u0431 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u0434\u043b\u044f \u0432\u0437\u0430\u0438\u043c\u043e\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u044f \u0441 \u0431\u0430\u0437\u043e\u0439 \u0434\u0430\u043d\u043d\u044b\u0445.<\/p>\n<pre><code class=\"javascript\">api\/app.js import supabase from '@\/superbase' import { useTelegram } from '@\/telegram' import { useScoreStore } from '..\/stores\/score'  const { user } = useTelegram()  const MY_ID = user?.id ?? 'YOUR DEV ID' \/\/ \u0414\u043b\u044f \u0440\u0430\u0437\u0440\u0430\u0431\u043e\u0442\u043a\u0438. \u0412 \u0432\u0435\u0431\u0435 \u043d\u0435\u0442 user.id \u2014 \u043e\u043d \u043f\u043e\u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u0442\u043e\u043b\u044c\u043a\u043e \u0442\u043e\u0433\u0434\u0430, \u043a\u043e\u0433\u0434\u0430 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u0437\u0430\u043f\u0443\u0449\u0435\u043d\u043e \u0432 \u0440\u0430\u043c\u043a\u0430\u0445 Telegram  \/\/ \u0410\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f export async function getOrCreateUser() {   const potentialUser = await supabase     .from('users')     .select()     .eq('telegram', MY_ID)    \/\/ \u041f\u0440\u043e\u0432\u0435\u0440\u044f\u0435\u043c, \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u0435\u0442 \u043b\u0438 \u0443\u0436\u0435 \u0442\u0435\u043a\u0443\u0449\u0438\u0439 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c   if (potentialUser.data.length !== 0) {     return potentialUser.data[0]   }      \/\/ \u0415\u0441\u043b\u0438 \u043d\u0435\u0442, \u0442\u043e \u0441\u043e\u0437\u0434\u0430\u0435\u043c \u043d\u043e\u0432\u043e\u0433\u043e   const newUser = {     telegram: MY_ID,     friends: {},     tasks: {},     score: 0,   }    await supabase.from('users').insert(newUser)   return newUser }  \/\/ \u0414\u043e\u0431\u0430\u0432\u043b\u044f\u0435\u043c \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u0435 \u0441\u0447\u0435\u0442\u0430 \u0443 \u0442\u0435\u043a\u0443\u0449\u0435\u0433\u043e \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f export async function updateScore(score) {   await supabase.from('users').update({ score }).eq('telegram', MY_ID) }  \/\/ \u0417\u0430\u0432\u0435\u0440\u0448\u0430\u0435\u043c \u0437\u0430\u0434\u0430\u0447\u0443 \u0438 \u043d\u0430\u0447\u0438\u0441\u043b\u044f\u0435\u043c \u0431\u043e\u043d\u0443\u0441\u043d\u044b\u0435 \u0431\u0430\u043b\u043b\u044b \u0437\u0430 \u0435\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 export async function completeTask(user, task) {   await supabase     .from('users')     .update({ tasks: { ...user.tasks, [task.id]: true } })     .eq('telegram', MY_ID)    const score = useScoreStore()   const newScore = score.score + task.amount   await updateScore(newScore)   score.setScore(newScore) }  \/\/ \u0420\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u0443\u0435\u043c \u0440\u0435\u0444\u0435\u0440\u0430\u043b\u0430 export async function registerRef(userName, refId) { \/\/ \u041f\u043e\u043b\u0443\u0447\u0430\u0435\u043c \u0434\u0430\u043d\u043d\u044b\u0435 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u043f\u043e\u0434\u0435\u043b\u0438\u043b\u0441\u044f \u0441\u0432\u043e\u0435\u0439 \u0440\u0435\u0444\u0435\u0440\u0430\u043b\u044c\u043d\u043e\u0439 \u0441\u0441\u044b\u043b\u043a\u043e\u0439 const { data } = await supabase.from('users').select().eq('telegram', refId)    const refUser = data[0]    \/\/ \u0414\u043e\u0431\u0430\u0432\u043b\u044f\u0435\u043c \u043d\u0430\u0441 \u0432 \u0441\u043f\u0438\u0441\u043e\u043a \u0435\u0433\u043e \u0440\u0435\u0444\u0435\u0440\u0430\u043b\u043e\u0432, \u0430 \u0442\u0430\u043a\u0436\u0435 \u043d\u0430\u0447\u0438\u0441\u043b\u044f\u0435\u043c \u0431\u0430\u043b\u043b\u044b   await supabase     .from('users')     .update({       friends: { ...refUser.friends, [MY_ID]: userName },       score: refUser.score + 50,     })     .eq('telegram', refId) }  \/\/ \u041f\u043e\u043b\u0443\u0447\u0430\u0435\u043c \u0441\u043f\u0438\u0441\u043e\u043a \u0432\u0441\u0435\u0445 \u0437\u0430\u0434\u0430\u0447 (\u043e\u043d \u0441\u0442\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438\u0439) export async function getTasks() {   const { data } = await supabase.from('tasks').select('*')    return data } <\/code><\/pre>\n<h3>\u0428\u0430\u0433 4. \u0421\u043e\u0437\u0434\u0430\u043d\u0438\u0435 store \u0434\u043b\u044f \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f<\/h3>\n<p> \u0421\u043e\u0437\u0434\u0430\u0435\u043c \u0435\u0449\u0435 \u043e\u0434\u043d\u043e \u0445\u0440\u0430\u043d\u0438\u043b\u0438\u0449\u0435 \u0434\u0430\u043d\u043d\u044b\u0445 \u0434\u043b\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f \u0438 \u0437\u0430\u0434\u0430\u0447, \u0447\u0442\u043e\u0431\u044b \u0431\u044b\u043b\u043e \u0443\u0434\u043e\u0431\u043d\u0435\u0435 \u0441 \u043d\u0438\u043c\u0438 \u0440\u0430\u0431\u043e\u0442\u0430\u0442\u044c \u0438 \u043c\u043e\u0436\u043d\u043e \u0431\u044b\u043b\u043e \u0432\u044b\u043d\u0435\u0441\u0442\u0438 \u043b\u043e\u0433\u0438\u043a\u0443 \u0432 Pinia \u0438\u0437 \u0441\u0430\u043c\u0438\u0445 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u043e\u0432.<\/p>\n<pre><code class=\"javascript\">stores\/app.js import { defineStore } from 'pinia' import {   getOrCreateUser,   completeTask,   registerRef,   getTasks, } from '@\/api\/app' import { useScoreStore } from '.\/score' import { useTelegram } from '@\/services\/telegram'  const { user } = useTelegram()  export const useAppStore = defineStore('app', {   state: () =&gt; ({     user: {}, \/\/ \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e     tasks: [],   }),   actions: {     \/\/ \u0421 \u044d\u0442\u043e\u0433\u043e \u043c\u0435\u0442\u043e\u0434\u0430 \u043d\u0430\u0447\u0438\u043d\u0430\u0435\u0442\u0441\u044f \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u044f \u0438 \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f     async init(ref) {       \/\/ \u041f\u043e\u043b\u0443\u0447\u0430\u0435\u043c \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0435\u0433\u043e \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f \u043b\u0438\u0431\u043e \u0441\u043e\u0437\u0434\u0430\u0435\u043c \u043d\u043e\u0432\u043e\u0433\u043e       this.user = await getOrCreateUser()        const score = useScoreStore()       \/\/ \u0417\u0430\u0434\u0430\u0435\u043c \u0434\u0430\u043d\u043d\u044b\u0435, \u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u0431\u044b\u043b\u0438 \u0432 \u0431\u0430\u0437\u0435 \u0443 \u044d\u0442\u043e\u0433\u043e \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f       score.setScore(this.user.score)        \/\/ \u043f\u0440\u043e\u0432\u0435\u0440\u044f\u0435\u043c, \u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u043b\u0438 \u043e\u043d \u0440\u0435\u0444\u0435\u0440\u0430\u043b\u043e\u043c       if (ref &amp;&amp; +ref !== +this.user.telegram) {         await registerRef(user.first_name, ref)       }     },     \/\/ \u0412\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 \u0437\u0430\u0434\u0430\u0447\u0438     async completeTask(task) {       await completeTask(this.user, task)     },     \/\/ \u041f\u043e\u043b\u0443\u0447\u0430\u0435\u043c \u0441\u043f\u0438\u0441\u043e\u043a \u0432\u0441\u0435\u0445 \u0437\u0430\u0434\u0430\u0447     async fetchTasks() {       this.tasks = await getTasks()     },   }, }) <\/code><\/pre>\n<h3>\u0428\u0430\u0433 5. \u0417\u0430\u043f\u0443\u0441\u043a \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f<\/h3>\n<p> \u0414\u043e\u0431\u0430\u0432\u0438\u043c \u043a\u043e\u0434 \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0431\u0443\u0434\u0435\u0442 \u0438\u043d\u0438\u0446\u0438\u0430\u043b\u0438\u0437\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u0438 \u0440\u0430\u0437\u0432\u043e\u0440\u0430\u0447\u0438\u0432\u0430\u0442\u044c \u0435\u0433\u043e \u043d\u0430 \u0432\u0435\u0441\u044c \u044d\u043a\u0440\u0430\u043d, \u0441\u0447\u0438\u0442\u044b\u0432\u0430\u0442\u044c \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u0440\u0435\u0444\u0435\u0440\u0430\u043b\u0430 (\u0435\u0441\u043b\u0438 \u043d\u043e\u0432\u044b\u0439 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c \u0435\u0433\u043e \u0438\u043c\u0435\u0435\u0442) \u0438 \u0437\u0430\u0433\u0440\u0443\u0436\u0430\u0442\u044c \u0434\u0430\u043d\u043d\u044b\u0435 \u043e \u0442\u0435\u043a\u0443\u0449\u0435\u043c \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u0435 \u0438 \u0435\u0433\u043e \u0441\u0447\u0435\u0442\u0435.<\/p>\n<pre><code class=\"plaintext\">&lt;template&gt;   &lt;main class=\"game\" v-if=\"loaded\"&gt;     &lt;div class=\"page\"&gt;       &lt;RouterView \/&gt;     &lt;\/div&gt;     &lt;TheMenu \/&gt;   &lt;\/main&gt; &lt;\/template&gt;  &lt;script setup&gt; import { onMounted, ref } from 'vue' import { RouterView } from 'vue-router' import TheMenu from '.\/components\/TheMenu.vue' import { useAppStore } from '.\/stores\/app' import { useTelegram } from '.\/telegram'  const { tg } = useTelegram()  const app = useAppStore() const loaded = ref(false)  \/\/ \u041f\u043e\u043b\u0443\u0447\u0430\u0435\u043c \u0434\u0430\u043d\u043d\u044b\u0435, \u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u043d\u0430\u043c \u043f\u0440\u043e\u043a\u0438\u043d\u0443\u043b \u0431\u043e\u0442 \u043f\u043e \u0440\u0435\u0444\u0435\u0440\u0430\u043b\u0430\u043c const urlParams = new URLSearchParams(window.location.search)  \/\/ \u041f\u0435\u0440\u0435\u0434\u0430\u0435\u043c \u0440\u0435\u0444\u043a\u0443 \u0432 \u0438\u043d\u0438\u0446\u0438\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044e \u0438 \u043f\u043e\u043b\u0443\u0447\u0430\u0435\u043c \u0434\u0430\u043d\u043d\u044b\u0435 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f app.init(urlParams.get('ref')).then(() =&gt; {   loaded.value = true })  onMounted(() =&gt; {   \/\/ \u0421\u0438\u0433\u043d\u0430\u043b\u0438\u0437\u0438\u0440\u0443\u0435\u043c \u043e \u0442\u043e\u043c, \u0447\u0442\u043e \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u0433\u043e\u0442\u043e\u0432\u043e   tg.ready()   \/\/ \u0420\u0430\u0437\u0432\u043e\u0440\u0430\u0447\u0438\u0432\u0430\u0435\u043c \u043d\u0430 \u0432\u0435\u0441\u044c \u044d\u043a\u0440\u0430\u043d   tg.expand() }) &lt;\/script&gt; <\/code><\/pre>\n<h3>\u0428\u0430\u0433 6. \u0421\u043e\u0445\u0440\u0430\u043d\u0435\u043d\u0438\u0435 \u0441\u0447\u0435\u0442\u0430 \u0432 \u0431\u0430\u0437\u0435<\/h3>\n<p> \u0420\u0435\u0430\u043b\u0438\u0437\u0443\u0435\u043c \u0444\u0443\u043d\u043a\u0446\u0438\u043e\u043d\u0430\u043b, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0441\u043e\u0435\u0434\u0438\u043d\u0438\u0442 \u043b\u043e\u043a\u0430\u043b\u044c\u043d\u043e\u0435 \u0438\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u0435 \u0441\u0447\u0435\u0442\u0430 \u0441 \u0434\u0430\u043d\u043d\u044b\u043c\u0438 \u0432 \u0431\u0430\u0437\u0435 \u0434\u0430\u043d\u043d\u044b\u0445.<\/p>\n<p> \u0414\u043b\u044f \u0437\u0430\u0449\u0438\u0442\u044b \u043e\u0442 \u0441\u043f\u0430\u043c\u0430 \u0434\u043e\u0431\u0430\u0432\u0438\u043c debounce, \u0447\u0442\u043e\u0431 \u043d\u0435 \u0437\u0430\u0441\u043f\u0430\u043c\u0438\u0442\u044c \u0431\u0430\u0437\u0443 \u0437\u0430\u043f\u0440\u043e\u0441\u0430\u043c\u0438. \u0417\u0430\u0434\u0435\u0440\u0436\u043a\u0443 \u0432 500 \u043c\u0441. \u0415\u0441\u043b\u0438 \u043d\u0430 \u043a\u0430\u0436\u0434\u044b\u0439 \u043a\u043b\u0438\u043a \u043e\u0442\u043f\u0440\u0430\u0432\u043b\u044f\u0442\u044c \u0437\u0430\u043f\u0440\u043e\u0441, \u044d\u0442\u043e \u043d\u0435 \u0431\u0443\u0434\u0435\u0442 \u044d\u0444\u0444\u0435\u043a\u0442\u0438\u0432\u043d\u044b\u043c \u0440\u0435\u0448\u0435\u043d\u0438\u0435\u043c.<\/p>\n<pre><code class=\"plaintext\">npm i lodash.debounce <\/code><\/pre>\n<pre><code class=\"javascript\">@\/store\/score import { defineStore } from 'pinia' import debounce from 'lodash.debounce' import { updateScore } from '@\/api\/app'  const debouncedSave = debounce(updateScore, 500)  \/\/ =======   export const useScoreStore = defineStore('score', {   actions: {     add(score = 1) {       this.score += score       debouncedSave(this.score)     },     setScore(score) {       this.score = score     },   }, }) <\/code><\/pre>\n<h3>\u0428\u0430\u0433 7. \u0421\u043e\u0437\u0434\u0430\u043d\u0438\u0435 \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u044b \u0434\u0440\u0443\u0437\u0435\u0439<\/h3>\n<p> \u0414\u043e\u0431\u0430\u0432\u0438\u043c \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0443 \u0434\u0440\u0443\u0437\u0435\u0439, \u043d\u0430 \u043a\u043e\u0442\u043e\u0440\u043e\u0439 \u0431\u0443\u0434\u0435\u043c \u0437\u0430\u0433\u0440\u0443\u0436\u0430\u0442\u044c \u0441\u043f\u0438\u0441\u043e\u043a \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u0435\u0439, \u043f\u0440\u0438\u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u043d\u044b\u0445 \u043f\u043e \u0440\u0435\u0444\u0435\u0440\u0430\u043b\u044c\u043d\u043e\u0439 \u0441\u0441\u044b\u043b\u043a\u0435 \u0442\u0435\u043a\u0443\u0449\u0435\u0433\u043e. \u0422\u0430\u043a\u0436\u0435 \u043d\u0430 \u044d\u0442\u043e\u0439 \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0435 \u043c\u043e\u0436\u043d\u043e \u0431\u0443\u0434\u0435\u0442 \u0441\u043a\u043e\u043f\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0441\u0432\u043e\u044e \u0440\u0435\u0444\u0435\u0440\u0430\u043b\u044c\u043d\u0443\u044e \u0441\u0441\u044b\u043b\u043a\u0443.<\/p>\n<pre><code class=\"javascript\">views\/FriendView.vue &lt;template&gt;   &lt;div class=\"text-content\"&gt;     &lt;h1&gt;Your Friends&lt;\/h1&gt;      &lt;div class=\"center\"&gt;       &lt;button class=\"referal\" @click=\"copy\"&gt;{{ referalText }}&lt;\/button&gt;     &lt;\/div&gt;      &lt;h3 v-if=\"friends.length === 0\"&gt;No friends yet&lt;\/h3&gt;      &lt;ul class=\"list\"&gt;       &lt;li class=\"list-item\" v-for=\"friend in friends\" :key=\"friend.id\"&gt;         {{ friend.name }}         &lt;span class=\"list-btn done\"&gt; 50 &lt;\/span&gt;       &lt;\/li&gt;     &lt;\/ul&gt;   &lt;\/div&gt; &lt;\/template&gt;  &lt;script setup&gt; import { useTelegram } from '@\/telegram' import { useAppStore } from '@\/stores\/app' import { ref, computed } from 'vue'  const { user } = useTelegram() const app = useAppStore()  \/\/ \u0423\u0434\u043e\u0431\u043d\u044b\u0439 \u0444\u043e\u0440\u043c\u0430\u0442 \u0434\u043b\u044f \u0432\u044b\u0432\u043e\u0434\u0430 \u0432 \u0448\u0430\u0431\u043b\u043e\u043d (\u043e\u0431\u044a\u0435\u043a\u0442 -&gt; \u043c\u0430\u0441\u0441\u0438\u0432) const friends = computed(() =&gt; Object.keys(app.user.friends).map((id) =&gt; ({   id,   name: app.user.friends[id], })))  const referalText = ref('Your referal')  \/\/ \u041a\u043e\u043f\u0438\u0440\u0443\u0435\u043c \u0432 \u0431\u0443\u0444\u0435\u0440 \u0438 \u043c\u0435\u043d\u044f\u0435\u043c \u0442\u0435\u043a\u0441\u0442 function copy() {   navigator.clipboard.writeText(     'https:\/\/t.me\/YOUR_BOT_NAME_IN_TELEGRAM?start=' + user?.id   )   referalText.value = 'Copied!' } &lt;\/script&gt; <\/code><\/pre>\n<p> <\/p>\n<div style=\"text-align:center;\"><img decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/webt\/jv\/lc\/eh\/jvlceh9qxovkxyj8ju4vooagagg.png\" data-src=\"https:\/\/habrastorage.org\/webt\/jv\/lc\/eh\/jvlceh9qxovkxyj8ju4vooagagg.png\"\/><\/div>\n<p> <i>\u0421\u0442\u0440\u0430\u043d\u0438\u0446\u0430 \u0434\u0440\u0443\u0437\u0435\u0439, \u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442.<\/i><\/p>\n<h3>\u0428\u0430\u0433 8. \u0421\u043e\u0437\u0434\u0430\u043d\u0438\u0435 \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u044b \u0437\u0430\u0434\u0430\u0447<\/h3>\n<p> \u0414\u043e\u0431\u0430\u0432\u0438\u043c \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0443 \u0437\u0430\u0434\u0430\u0447, \u043d\u0430 \u043a\u043e\u0442\u043e\u0440\u043e\u0439 \u0431\u0443\u0434\u0435\u043c \u043f\u043e\u0434\u0433\u0440\u0443\u0436\u0430\u0442\u044c \u0441\u043f\u0438\u0441\u043e\u043a \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u043d\u044b\u0445 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u0435\u043c \u0437\u0430\u0434\u0430\u043d\u0438\u0439 \u0438 \u043a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u043d\u044b\u0445 \u043e\u0447\u043a\u043e\u0432, \u0430 \u0442\u0430\u043a\u0436\u0435 \u0435\u0449\u0435 \u043d\u0435\u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u043d\u044b\u0435 \u0437\u0430\u0434\u0430\u0447\u0438.<\/p>\n<pre><code class=\"javascript\">views\/TasksView.vue &lt;template&gt;   &lt;div class=\"text-content\"&gt;     &lt;h1&gt;Your tasks&lt;\/h1&gt;     &lt;p v-if=\"app.tasks.length === 0\"&gt;Loading tasks...&lt;\/p&gt;     &lt;ul class=\"list\"&gt;       &lt;li class=\"list-item\" v-for=\"task in app.tasks\" :key=\"task.id\"&gt;         {{ task.title }}          &lt;span&gt;           &lt;a             @click.prevent=\"openTask(task)\"             target=\"_blank\"             class=\"list-btn\"             :class=\"{ done: app.user?.tasks?.[task.id] }\"           &gt;             {{ task.done ? 'Done' : task.amount }}           &lt;\/a&gt;         &lt;\/span&gt;       &lt;\/li&gt;     &lt;\/ul&gt;   &lt;\/div&gt; &lt;\/template&gt;  &lt;script setup&gt; import { useTelegram } from '@\/telegram' import { onMounted } from 'vue' import { useAppStore } from '@\/stores\/app'  const app = useAppStore() const { tg } = useTelegram()  onMounted(() =&gt; {   \/\/ \u0415\u0441\u043b\u0438 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 \u0433\u043e\u0442\u043e\u0432, \u0437\u0430\u0433\u0440\u0443\u0436\u0430\u0435\u043c \u0435\u0433\u043e \u0441 \u0441\u0435\u0440\u0432\u0435\u0440\u0430   app.fetchTasks() })  function openTask(task) {   \/\/ \u0417\u0430\u043f\u0443\u0441\u043a\u0430\u0435\u043c \u0446\u0438\u043a\u043b \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u044f \u0437\u0430\u0434\u0430\u0447\u0438   app.completeTask(task)   if (task.url.includes('t.me')) {     \/\/ \u041e\u0442\u043a\u0440\u044b\u0432\u0430\u0435\u043c \u043a\u0430\u043a \u0432\u043d\u0443\u0442\u0440\u0435\u043d\u043d\u044e\u044e \u0441\u0441\u044b\u043b\u043a\u0443     tg.openTelegramLink(task.url)   } else {     \/\/ \u0418 \u043a\u0430\u043a \u0432\u043d\u0435\u0448\u043d\u044e\u044e     tg.openLink(task.url)   } } &lt;\/script&gt; <\/code><\/pre>\n<p> <\/p>\n<div style=\"text-align:center;\"><img decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/webt\/1x\/qx\/d7\/1xqxd7n7s0ffuv1kuysirvop-di.png\" data-src=\"https:\/\/habrastorage.org\/webt\/1x\/qx\/d7\/1xqxd7n7s0ffuv1kuysirvop-di.png\"\/><\/div>\n<p> <i>\u0421\u0442\u0440\u0430\u043d\u0438\u0446\u0430 \u0437\u0430\u0434\u0430\u0447, \u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442.<\/i><\/p>\n<p> <font color=\"#EB4247\"><\/p>\n<h2>\u0417\u0430\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435<\/h2>\n<p><\/font><br \/> \u0413\u043e\u0442\u043e\u0432\u043e! \u0423 \u043d\u0430\u0441 \u0435\u0441\u0442\u044c \u0437\u0430\u043f\u0443\u0449\u0435\u043d\u043d\u044b\u0439 \u0444\u0440\u043e\u043d\u0442\u0435\u043d\u0434, Telegram-\u0431\u043e\u0442 \u0438 \u0441\u0430\u043c\u0430 \u0438\u0433\u0440\u0430. \u0422\u0435\u043f\u0435\u0440\u044c \u0432\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c <a href=\"https:\/\/github.com\/vladilenm\/lizzard-clicker\/tree\/vue-init-template\">\u0448\u0430\u0431\u043b\u043e\u043d \u044d\u0442\u043e\u0433\u043e \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f<\/a> \u0434\u043b\u044f \u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0438 \u0441\u043e\u0431\u0441\u0442\u0432\u0435\u043d\u043d\u044b\u0445 \u043f\u0440\u043e\u0435\u043a\u0442\u043e\u0432.<\/div>\n<\/div>\n<\/div>\n<p><!----><!----><\/div>\n<p><!----><!----><br \/> \u0441\u0441\u044b\u043b\u043a\u0430 \u043d\u0430 \u043e\u0440\u0438\u0433\u0438\u043d\u0430\u043b \u0441\u0442\u0430\u0442\u044c\u0438 <a href=\"https:\/\/habr.com\/ru\/articles\/869618\/\"> https:\/\/habr.com\/ru\/articles\/869618\/<\/a><\/p>\n","protected":false},"excerpt":{"rendered":"<div><!--[--><!--]--><\/div>\n<div id=\"post-content-body\">\n<div>\n<div class=\"article-formatted-body article-formatted-body article-formatted-body_version-1\">\n<div xmlns=\"http:\/\/www.w3.org\/1999\/xhtml\">\n<div style=\"text-align:center;\"><img decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w780q1\/webt\/jy\/9i\/ml\/jy9imlzzajnk9qjswptwth81zks.jpeg\" data-src=\"https:\/\/habrastorage.org\/webt\/jy\/9i\/ml\/jy9imlzzajnk9qjswptwth81zks.jpeg\" data-blurred=\"true\"\/><\/div>\n<p> \u041f\u0440\u0438\u0432\u0435\u0442, \u0425\u0430\u0431\u0440! \u0412 \u044d\u0442\u043e\u0439 \u0441\u0442\u0430\u0442\u044c\u0435-\u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u0438 \u0432\u044b \u0443\u0437\u043d\u0430\u0435\u0442\u0435, \u043a\u0430\u043a \u0441 \u043d\u0443\u043b\u044f \u0441\u0434\u0435\u043b\u0430\u0442\u044c \u0441\u0432\u043e\u044e \u0441\u043e\u0431\u0441\u0442\u0432\u0435\u043d\u043d\u0443\u044e Telegram-\u0442\u0430\u043f\u0430\u043b\u043a\u0443 \u043d\u0430 \u0441\u043e\u0432\u0440\u0435\u043c\u0435\u043d\u043d\u043e\u043c \u0441\u0442\u0435\u043a\u0435. \u0412\u0430\u0436\u043d\u044b\u0439 \u0434\u0438\u0441\u043a\u043b\u0435\u0439\u043c\u0435\u0440: \u0442\u0430\u043f\u0430\u043b\u043a\u0430, \u043a\u043b\u0438\u043a\u0435\u0440 \u0438 \u043f\u0440\u043e\u0447\u0435\u0435 \u2014 \u044d\u0442\u043e \u0432\u0441\u0435\u0433\u043e \u043b\u0438\u0448\u044c \u0444\u043e\u0440\u043c\u0430. \u0426\u0435\u043b\u044c \u0441\u0442\u0430\u0442\u044c\u0438 \u2014 \u0434\u0430\u0442\u044c \u0432\u0441\u0435\u043e\u0431\u044a\u0435\u043c\u043b\u044e\u0449\u0438\u0439 \u043f\u0440\u0430\u043a\u0442\u0438\u043a\u0443\u043c \u043f\u043e \u0441\u043e\u0432\u0440\u0435\u043c\u0435\u043d\u043d\u043e\u043c\u0443 \u0441\u0442\u0435\u043a\u0443 \u0438 \u0434\u0435\u043f\u043b\u043e\u044e \u043f\u0440\u043e\u0435\u043a\u0442\u043e\u0432 <a href=\"https:\/\/selectel.ru\/services\/cloud\/?utm_source=habr.com&amp;utm_medium=referral&amp;utm_campaign=cloud_article_fstap_251224_content\">\u0432 \u043e\u0431\u043b\u0430\u043a\u043e<\/a>.<\/p>\n<p> \u0412\u043d\u0443\u0442\u0440\u0438 \u0441\u0442\u0430\u0442\u044c\u0438 \u2014 \u043f\u043e\u043b\u043d\u043e\u0446\u0435\u043d\u043d\u044b\u0439 Serverless-\u043f\u043e\u0434\u0445\u043e\u0434, \u0440\u0430\u0437\u0440\u0430\u0431\u043e\u0442\u043a\u0430 \u0431\u043e\u0442\u0430 \u043d\u0430 Node \u0438 \u043f\u043e\u043b\u043d\u044b\u0439 \u0446\u0438\u043a\u043b \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u044f FE-\u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f. \u0410 \u0435\u0449\u0435 \u043a\u043e\u043c\u043c\u0435\u043d\u0442\u0430\u0440\u0438\u0438 \u043f\u043e \u0430\u0440\u0445\u0438\u0442\u0435\u043a\u0442\u0443\u0440\u043d\u044b\u043c \u0438 \u0442\u0430\u043a\u0442\u0438\u0447\u0435\u0441\u043a\u0438\u043c \u0440\u0435\u0448\u0435\u043d\u0438\u044f\u043c, \u0447\u0442\u043e\u0431\u044b \u0432\u044b \u043f\u0440\u043e\u043a\u0430\u0447\u0430\u043b\u0438 \u0443\u0440\u043e\u0432\u0435\u043d\u044c \u043f\u0440\u043e\u0433\u0440\u0430\u043c\u043c\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f \u0438 \u043d\u0430\u0441\u043c\u043e\u0442\u0440\u0435\u043d\u043d\u043e\u0441\u0442\u0438. \u041f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0441\u0442\u0438 \u043f\u043e\u0434 \u043a\u0430\u0442\u043e\u043c!<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[],"tags":[],"class_list":["post-443242","post","type-post","status-publish","format-standard","hentry"],"_links":{"self":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts\/443242","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=443242"}],"version-history":[{"count":0,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts\/443242\/revisions"}],"wp:attachment":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=443242"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=443242"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=443242"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}