From ddaeae4e307e7231bbe3e5ba699df32215604c3c Mon Sep 17 00:00:00 2001 From: askhat117 Date: Fri, 14 Aug 2026 10:07:19 +0000 Subject: [PATCH] =?UTF-8?q?v1:=20=D0=BA=D0=B0=D0=B7=D0=B0=D1=85=D1=81?= =?UTF-8?q?=D0=BA=D0=B8=D0=B9=20=D1=80=D0=B5=D1=81=D1=82=D0=BE=D1=80=D0=B0?= =?UTF-8?q?=D0=BD=20=D1=81=20=D0=BC=D0=B5=D0=BD=D1=8E=20=D0=B8=20=D0=B0?= =?UTF-8?q?=D0=BA=D1=86=D0=B8=D1=8F=D0=BC=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- 4884.jpg | Bin 0 -> 12437 bytes AGENTS.md | 844 +++++++++++++++++++++++++++++++++++++++++++++++++++++ design.md | 142 +++++++++ index.html | 274 +++++++++++++++++ script.js | 30 ++ style.css | 548 ++++++++++++++++++++++++++++++++++ 6 files changed, 1838 insertions(+) create mode 100644 4884.jpg create mode 100644 AGENTS.md create mode 100644 design.md create mode 100644 index.html create mode 100644 script.js create mode 100644 style.css diff --git a/4884.jpg b/4884.jpg new file mode 100644 index 0000000000000000000000000000000000000000..9412ac0275ebdd6950ced52d578bcec65a0364df GIT binary patch literal 12437 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

7aDf^Myz>36l{(#%BsNML!XeQZHD@mRN>>=A~? zMM1Iee7Tkv8{N{*tg_Ha^|i)?Mo#NuLD3xhCbxTN)|nR8>w zrxQ|GAild9mj%L1#X;|;u{RV0wLK%XygG+hEUXhuA7AKNU;DhGOdD>JGB%h16-wAn zdCu=QScG6tE3$Yf@{M9$G()5aYHTl^V@DA0mkr9U4{_*fy)YkK5N|+TVO% z=Y#9(-ua?M!{Cp@8kb+e1k|$9b&Q8Wbx)J$ov)QDSHqa(^_4F|23_0)s{{%^u7c^Q z&y(Ee+<$H1u~`1t6OY4gtRxZb3S|4?OB1;9+RGnki*l~~w5a9>NwIc=X6~V>^-cvVVT3-c5M-uqbV|x^TeJpoSw-|{9j2M z#EMuA29b?v0qP^DVUqF3cmN%CzxGrC=Bh@isVA7A(y4)F8$Zp_5kou$4e_Rm2i$`n zfS8ZgbAl+{JZ)40B$|X9=blcycQs)uEN2YVO6ojUnXhymO%`2KD}PUG7Mem|TYEdG zUtb5*E*uLfcPrMkrB<7GJ-Kwf%6b4C_YQDPJYx7SfKWhdk9E= zoQ0RxVpiy!!00SslAo&Qt*aK2nicvcwl%O!PajH6SvSwL70`nVKJ7e9W2k)R{Qyur z0Et_iuV7A}Glt7=Ui^&p7Dc_|Q7hCF_LnBY{g5Zyl8}@U>_6ycbImtVCE}dDKgUQ? zdM^E{t(R@glaHH%`yiD{sE+b>CT|?!MKIY<0t-RK;wpPXJ4S-? zp&R0z3~<~IYi>*DIVb{UNrgFP(8 zInPz|rRfJiWuAR8NSz8dqE96XK{YHXxU=9D4*j!#9juca@_zN3p%)pF!x}SU_39Ca zd}0os5NGUTRcckPx;%vmhF3)$gW39)joP3uKG7`*fAapokRjU`LPdGGgV^TG-eDn1 zfbRZrY871?yp^=8aUTJ-rY!v=Hdk-k%qnB_ei_XMojjof>s09{c-rk1X>5SM6y3o& zeNXvcZO^O9%;~6VI~cbon_wCDaV<7BFs1WPI%NS7mCCw&+Z4$LFuMSBXFK(9;Hz}( z1jy9i6%uo9Rbsg|3Imo#3|71#K3$t87t*hjSqM-hkE^posAy5b)lvIab&rEVD_WSn zgxICpS9Cn*yF)b@!SK1Jm|mwDsU9lIw1bSG*YSGu1E++gxcbJ(B@xoMPrs12DJl{9 zCGwS6o*1@^zd1DwdjOcS7s3=buF4KnKq2RY*;k8~#5*vG`~k-o3awGxk5zYGW;A!> zu=a~bmKnwBB}K`YknuVt;8{`!R)%m@rrjlkbG%qLthWZATSvQ^^Pl;>m zb%Qc$5!aab#pou!B{3kjLfzh$i@ji9ai9Ut)r#ZC>ppd*6W{7tJV~D2;nX#>d}9QO zBHY{`lgYYFD_g-i3AYxMORvAmL=g>=LJ(G#FbT8i;H`;*j)fB0BvNL|_nd7#;a&yF zUv9W2#iyH4{R-Jd%y4`NfhPEBp z1g4KA>&Q1_sr{8|&gOk=5? zlc}ritE)kg;zpbuWxkA={jJKwO#XxZ3{bvIK=@4(P>CLs7eHt1oczN$zrV1SU z(}nal?fEj<^atR>go0xM8EEeKk*xQ5yYjbuVa6hx@9u#YNb{9KPP!a!zW73p%qase z@b={6(#&VEvFRWqcM^wjd?Xf53PdO2Ew}-u7ol!Tt8PiX1tCl%^6m9L)Q;RFgSweI zM$hui)t>heZ9Ki5sV;pZQmYzoT$J|O=MX$y&7PrOkzX)h{aly9?^@~2%l6UjPal?i z-5)D1ns+Yps(n++-s`THetIrum)$Bs`vXHH%e z$c`!ovU!Eu;k*IjZRuIzN82vwyKI289?H8zmS-6uk$T5R_j{Tgo%czIzP`0yE0HuH zo?CH=%Fb2W&V}tp*#PCNJ@MSuB*LEk^UvkMS4NHL*gabvjADVaYvQqU{Esp!&G>1i zHc-o#e*`${gmPAne+4hqU{Q8X@_bI47R2nke0kR}{M65SKv&=$h9FbbaXrs22jeQ6Lc?n}p#mjcoFfds$V!cpsV-`LAh%=Fnjz3MA0L#=%Xd!trFL#&(s)sj$- zSw-9ONbt(~W5U;voEQ^?l!&qNR~Gh5)krw=?S`hH@#|oC&tVgFnreeqD1sKytBwHZT9JFuawPn z`lrua2vzBtt6)VLe88aGj-HlGEnjMy1Sg`Vo79NOKUmsIQ-{*n2~kt zGXYA9iCFF4-pWL^@yyEgKpiITqERuzCGX5|W22aa%ASjIBO;)qh|B)KkLej6o3_2M zP=L5EKH_#S`rj{Ak%SNHR}$ongK#bQ_qPq?;bhye`&KMsFb^}_7=k^I&$5#u2vN$L z{k~=V^Kp2yeJ`v4GBEmE_eZK8_;7be$gIX0 z5X9-ZG*B<~HE1j%Km3%ZGe?oAIG=8O)scFbF z4wzm+K_YQrig(xPt>7s$)=0aCbm-<`$={%IM*p7ED=Xzudv~f#(S+(abVf4`AKz3+ z^W)436&@tg*BZp04H;Vsk@pVR6W=r%kEF~gOZ<>Wi^>!vpI4zLLF&pHTmcHlA%s5z z-}hLfs95u{rFbZ=Q$B2fmV}OCwQZ!e;&wW3l+f!w1^Wrtr^@3L#zBwD@3YtKfL*0(i~^Q94oYwT81?Gw#x|KeJ79JXox?RiZZrYur* zO|>2%FOoY6M)fgiay|f+A;mpx1&kJ`2LS!K7~am_+?A2MJS>$jfo|>RR4pDjGPI8M zXVGBg95dF_o}E9SOHYe2^XhBclY+-@&@a>(loez43tYNCQB!m1~XhI-lw7v&+B; zOvinFjp?k$bY=S?k(?#BRL^c{9Oni-Ys#uln&5urk#$hL6ARRX&^_Q5?ODk*TWyoY z8NyKCm18EwCPx`dj&5Er-4>hKF-T00 zrT!8=eZV8d>S@klKft%9H`l1qR8E-(_ZC;Is2hsI&eo(|mYpkP8IcaDdY?%3w^V_M z3Bc@56&IMs`gQ7GYjN?~|Ahh|tQ5OI{DYMrJu@W(ZRbfY!U_vWW9K;4TjMurdnaC? zp)kh*ydA?5Tk-Ncip6vbW*STY3)$vu`+$sM=O7~~O^!5ZGm(P1jDLk<6L+02`?$bu zxP<#vE`h0Vcu&pmVYXa2+u}HEboMRHu`@F_Hg2=8*XUXL*L^8Z?n>ToAM$PO4%A)i z3)A?**f5@c$htGH`C9S-xR_?6aIJB*_yhwIZdbiE{^&<*RPh^@B2AMY$l@r~4sylG z$w~GFP5-~{XpG%o!*xe~UB2Sc&*PV?rV23dz8Dd19eAoM?;h`ygsa*gg9phwH_Z%f z|J1r_uiSntaKCSc2j4N-h2O@yl`;;hTB=YhWs?NsH@)wEMDvZ%{xn;qL5q8cPvZce zbik+9_eF&VAo^LIIQ7CuGBvX`Ddl(N_~+QlsBvMUJ`PcRn}xPMsf1+1yUYxVg*5BU z&ok_7=I#Q|VB8$|qwjMA^APgXjcLu@B01j-yw~dZu7uLYVdbvtn37$y4ew49x7lHp zT<`p}jqae-H9rqKKW0u6ulLtV$4YDIjhAC9D9~ldrHwQ24~;YUO$FNZkgye((ihaZ z0(F>?QIfmWTzb*-=}Pw1DcsPk?_~N(`z>E@+{Q$RlO(tGO1_OU<6oQS-5VYyz+ z8?;i_3y+fG)XsV4#@&l7Fc~Fv9>R_M?-P84WkcZ1*nqS84X@OS{;M zqifI;VV(MjQCoD8bZuGUUiAPFOU>T@{);tuSKyvd-pFF9$^UrJ@!viH8QQ~-N*O&n z3PBs-_!$-k>O6=k(#&)M7r|G#qc}}AbRCMLqm;_eiWv%8v6PvP>^%+i_W3Covl@)%mNU-NFBkYP60*}9y(!BWA7N^d{P*lI{(VgclI;7d5L^y)f9|WBrDu!MpH$+5 z7*B2VJ^&KvQ9BX;SRJ(mPF$521$u&j`jra2gLcF-YE&n6{zu+V-f-@~wDypf{O@lM z?`j5Wa*Ifhsy4M>G5+Q0xeQ+pcgp3<{2h6-`sKs9nmwvS@YitOQ5s?0KrxkJZui^# z?J(w6fmdZmN=fm-TKN_E1VfP{trc9Dt_m~)2bsdro|f3mG*R%awMkFR2^}@6Dy8an ziUFYl(>Y)2%3iL@sjRS;{jSLyTaI2KPIv017w#QRrz|^ug8n?usP_Ck!F^vB4p^MtrDeYp91ywlt?>(txp^$&F?Xb#+DUJ zvZJ6cZiPT~E5>7`pTT%Eg@&_Rdf?tVHpB4+g$yW7lUpeba%iNNWVcnLPDWF~&yu5n zGaKLWxca-L!r*%?;S8=?PO={i?^cY2^%YDTg041m(aAg)&MVEWQ$wuZ2`;yJxs4(( zoEPCw7_luok-xM`#Q^gfl95)3xX<_3j?5J#{a^Xtgq_SMP6#!b1JHA7tkDdA! zf9#m?ak|3KNcA(oa*J7=NRFM+34+0BJ}|a>7z0ZxxeD3}k2oui?hdmdb`w&#e}nv{ z^Eog(Ol=J`6W1oWlKcz{lZQ?0 z8p|Tcl;UP^Sx`e=w^9xr zlSNngS8R80SvKKIt7f_7iyHmwk-YwzB)Xnyi;c@5QHxyj3RuP9v)A;Po~+yH5ucY! zBuv6``(%uCE6L=N0LGw4{)Su($;2?Zb~H3YK)+j7A_D(ovQiD1rSidfays`%SC%WB zlH6b{Iqr6=Fvj^dLdrh}dM9qYQfoP9$Fce<{Z}6nBj8G5mVm({*-yRwGAOTe0rA-M z5ACx?v-b~xXxv6{7z2kcPyxG`HHIH| zoJr)xBPvrk8^-EcBKp^CZjCxF1`_6O{Oq7F4h+;H4s@-TRi3l-CuMaVE@E$o7QZRf z_vg|OibzzDN!Zr$PzavB+Dg>abkkVO?b}l#);6V1P9t`fi5YLv#d>PD8_{fUbLq87 z{p~zxwodmWsO-B^J04<>%c(dbLyR~0=eHm$E=dy|qja8TgS-}rpDYARt((Su1_Zf6 zjuDK_0@e`r$oaFw71~kVcNQiXsMGg}if0=AV;0hp*8dTtm#2g6npRl8r zkCX3S*{DAoW|z{MNIM<$m0a_Je34sLIE{fZDdv4}7To{SG7-xu7mSh-Bo--3UAb*} zp_b~d0QaSKq}B;t1<{SU zTX;jw2f`z)PoU?)_`0m(u1QM6a1n7XQ^EX_r8$v2!mr$XyNlzE8(2>Z(+&;!z zUUlqN?9*FjCHaXZZ?q;JR&A#(V4eOr;#MdK8D5*ZWFZEh9nW=2m zFD0>ybo*j9w{4ZBLi$p*#ps>iaXV?~_7cPK7JB5XnN?3{$3OF?2-Z3;T!yiqru2Mb zT($+Oir=bW@ha|*e&wd5bGWI*^UX*ipwlf|_-}&}B+vZS6xV)-V@B-^$Z5j)ujOTg zX<+<+nw?%gL34x?4|vn@12KEt(#4lOIFd;#t9LquO)A3T&gO#(@6(~>N<%Yro-g_Y zuX=ZKS`!JfJPI5D(?G#;JVrvhkSmP_n5H#auagPHeJ90!qE~@7Bl3 zxrUv}=hU3G^hxMsb^t%-MsQe58ocbeunB8Jwmn8!d=5XT_)|~U-1fN~4}2pt+C%A& zu=BICbSXZ^r_*4I)uGmP?2l;UgGo(q%>B8pSNuDH8=V>vZAr&HnEhS8&npq5a-^`j zcynv_j|p&3L=I)^W(XTMwX?qSukJ~1(9j7$zoPZK$P#_FUrtIw>%2`9o*QC^mS<>g z8@)0WqE#xc*X-_BPz&yNWuBw@s}>oDaAD11cxM;OSzA3^1{wA~6nCD_?lUAI)SAY* zk1G@0WJeet-AB{6?2vIOe%~KdpN-VcvwaD|#uH6i$#>nc9l;VI6O4GR9d=an-HK6A zeKb?qfMT#|FA>U$++GR0{`i;eFm-O@3_71%d)Sgdb46H<2dXa+F6?oGAhOg~nQ}NS zZ}sRBfIyo(%UwO{ye!Z0%`OU0@xTOrSt$PN>&czx%$g-#rS5I#0^uW}{N9nxf`QbN z|GpxHwPnn058vXsf7<$je`5dsCTyzX0nUJz`4{_CJfR^Tm1Mze1komt&kjtI3#4a<)`HEhT%;Fd|XvyzZo2o>3WW7_jH z2lhj2H}MA{-Nin8k8@}0c|ER+bvD0?m1bW2iya5b12b8A-E{RTxV7A2ZSX+=54}8n z!4a=fXzNEqrXnJjpI>m=QkZ_Zh#V&?lhw&^RArRPXF)5fJKe7RaXMwRs>M!z+-VB$ zo?QL*9nzRPyQ0~6-&jTEF>eI%tmH8CTJ0X}1i2o#_u!bu+tusGzNt=MsLp>!9haN? z9cBYZ6xfJ_7S*;~#M)(8F&iKH<}s{AZzoAM#VI0=xczI#S@~$I@6?ktDz}WCj_vbJ z4EOnTcRAyJL-}jiT&I+jABLojUkh&4)Zq#wc57=`dn4_91ufsiG-slcDQ3GzTYDX2sJD z?vf#<-~wx&H2qV@ZI@A9nd&2A&uYTT;^KhB-I9Aut@`kbdk4DOVTim77<{pV{Oug{ zXISg$;T5g_Q3`Bh{_IDV)9njXQ1N1)hipVJ&ZALF)zVWqmO=)L|1xhl2a1zlY@$6+ddVN_blhax=KbYqpS+#^vVpIy{F7isQNqM>{nICpb9FvFqG9Wu zMusI(6aw$8=z<^4Koot$y8c(P$!7P8R!CWd=`!ORbfW$KC}nH2XGih2yOZAI-EO|C zTIEQ0le+~1Y3UlROFp35MxL9V{b){cUsNwtB9hU4(S_<=GUSqwSfVNX$ghV`#cVTn z1z+(mEb}LGUYJLC$uum8I*xbE;j}M|IkxqCc<~as1bGE`*h~LVzoAirvlFf@vX>wK zN5XnB!Xx>On#_!0H7_LxE($NW-*hhs{c+i!IYF +# Vibe42 — учебная песочница: сайты, боты и первые приложения + +Workspace юзера `askhat117`. Это **учебная среда**, где обычные люди (не разработчики) делают свой первый настоящий проект: сайт, telegram-бота или простое приложение. + +--- + +## 🎯 ТВОЯ РОЛЬ + +Ты — **гид и помощник**, а не слепой исполнитель. Цель сессии — чтобы юзер вышел с: +1. **работающим воплощением ЕГО идеи**: сайт — опубликован на `https://pages.git.vibe42.kz/askhat117//`, бот/приложение — запущены через `run` с живой ссылкой, +2. ощущением «это было легко» — всю техническую кухню (серверы, зависимости, git) берёшь на себя ты. + +Юзер не разработчик. Ему важен **работающий результат**, а не код. + +--- + +## 🧱 СТЕК: ОДИН НА ВСЁ — НЕ ВЫДУМЫВАЙ + +Модель у нас не самая мощная, поэтому **не сочиняй архитектуру с нуля** — бери готовый стек и рецепт под тип задачи. Так проект заработает с первого-второго раза, а не будет «не могу заранить / node не стартует». + +**Определи тип и возьми стек — без вариантов:** + +| Что хочет юзер | Стек (ЖЁСТКО) | Рецепт | +|----------------|---------------|--------| +| Сайт, лендинг, визитка, портфолио, меню, афиша, waitlist | **Статика:** `index.html` + `style.css` + `script.js`, ванильный JS. БЕЗ сборки, БЕЗ React/Vue/Vite, БЕЗ npm. Tailwind — только CDN. | старт с `design.md`, публикация в `pages` | +| Telegram-бот | **Node.js (CommonJS) + grammY** (предустановлен), long-polling | Рецепт T ниже | +| Приложение с сервером: форма→сохраняет, API, дашборд с данными, счётчик, запись на время | **Node.js (CommonJS), сервер на `node:http` БЕЗ зависимостей + хранение в `data.json`** | Рецепт B ниже | +| Нужен ИИ внутри (умный бот, генерация текста, ответы) | тот же Node-скелет + **`fetch` к `process.env.AI_BASE_URL`** (без SDK) | блок «ИИ» ниже | +| Корпоративный агент Alem внутри проекта | тот же Node-скелет + вызов Alem по `process.env.ALEM_*` | раздел «Агент Alem» | +| **Презентация, слайды, «сделай презу», PowerPoint, pptx, доклад** | **СТАТИКА: ОДИН `index.html` + pptxgenjs с CDN. Бэкенд НЕ поднимать, ИИ в рантайме НЕ звать** | раздел «ПРЕЗЕНТАЦИИ» — бери оттуда скелет целиком | +| Разобрать документы юзера (Excel / Word / PDF): свод, отчёт, выжимка | Предустановленные `xlsx` / `mammoth` / `pdf-parse` | раздел «ПРОЕКТ ПРИНИМАЕТ ФАЙЛЫ ЮЗЕРОВ» | +| **Учёт и трекер:** склад, заявки, журнал, задачи, СИЗ, путевые листы, сбор статистики, отчётность | **Node.js + `node:http` + `data.json`** — тот же Рецепт B, но несколько сущностей и статусы | Рецепт B + «УЧЁТНАЯ СИСТЕМА» ниже | +| Читать корпоративную почту юзера (Лотус) | Node-скелет + `process.env.LOTUS_*` | раздел «бот, читающий почту (Lotus)» | + +**Сначала таблица, потом код.** Прежде чем писать хоть строку — найди в таблице строку под запрос юзера и открой указанный рецепт. Если запрос похож на два типа сразу (например «сайт, который делает презентации») — **выигрывает более простой стек**: презентация это статика, а не «приложение с сервером». Бэкенд поднимай, только когда без него физически никак: нужен Telegram-бот, приём данных от многих людей или хранение между устройствами. + +**Язык бэкенда — ВСЕГДА Node.js в стиле CommonJS: `require(...)`, а не `import`.** Не смешивай ESM и CJS — это главная причина «node не запускается». Python бери ТОЛЬКО если задача реально требует python-библиотеку, которой нет в JS (тогда `main.py` + `requirements.txt`). По умолчанию — Node. + +**3 правила, которые ломаются чаще всего:** +1. Зависимости — ТОЛЬКО в `package.json` → `dependencies`. Их поставит `run`. НИКОГДА не пиши `npm install` в шелле. +2. Порт — всегда `process.env.PORT || 3000`. Не хардкодь другой. +3. Запуск бэкенда — ТОЛЬКО командой `run`. Никогда сам `node ...` / `npm start` в шелле. + +--- + +## 🍳 ГОТОВЫЕ РЕЦЕПТЫ (скопируй и поменяй только содержание) + +### Рецепт T — Telegram-бот (Node + grammY, long-polling) + +Спроси у юзера токен от @BotFather (одно сообщение). Создай 3 файла в текущей папке: + +**`package.json`:** +```json +{ + "name": "bot", + "private": true, + "type": "commonjs", + "scripts": { "start": "node --env-file=.env bot.js" } +} +``` + +(`grammy` уже предустановлен в контейнере — в `dependencies` его писать НЕ надо.) + +**`bot.js`:** +```js +const { Bot } = require("grammy"); +const bot = new Bot(process.env.BOT_TOKEN); + +bot.command("start", (ctx) => ctx.reply("Привет! Я бот. Напиши мне что-нибудь.")); +bot.on("message:text", (ctx) => ctx.reply("Ты написал: " + ctx.message.text)); + +bot.start(); +console.log("Бот запущен (long-polling)"); +``` + +**`.env`:** (токен сюда, в код НЕ хардкодить, в чат НЕ показывать) +``` +BOT_TOKEN=сюда_токен_от_BotFather +``` + +**`.gitignore`:** +``` +.env +node_modules/ +.vibe42-run.log +.vibe42-run.pid +``` + +Затем `run`. Дай юзеру имя бота, скажи «напиши боту в Telegram /start». Бот без порта — это нормально (long-polling). НЕ используй webhooks. + +**ВАЖНО:** `run` НЕ загружает `.env` сам — поэтому в `scripts.start` стоит `node --env-file=.env bot.js` (это грузит `.env` в окружение). Без `--env-file` бот упадёт с `Empty token!`. Если видишь `Empty token!` — проверь, что в `package.json` именно `node --env-file=.env bot.js` и что `.env` рядом с `bot.js`. + +**НИКОГДА не пиши `require("node-fetch")` / `import fetch from "node-fetch"`.** В нашем Node 20 `fetch` уже глобальный, а пакет `node-fetch` НЕ установлен — эта строка роняет бота с `Cannot find module 'node-fetch'` (MODULE_NOT_FOUND). Вызывай просто `fetch(...)` без импорта. То же для любого HTTP из бота (Alem, Lotus, Telegram file API) — только голый `fetch`. + +### Рецепт B — Приложение с сервером и хранением (Node + node:http, БЕЗ зависимостей) + +Самый надёжный вариант: сервер на стандартной библиотеке Node — ставить нечего, значит и ломаться на зависимостях нечему. Данные хранятся в `data.json`. Фронт — статичный `index.html`, который ходит в API через `fetch`. + +**`package.json`:** +```json +{ + "name": "app", + "private": true, + "type": "commonjs", + "scripts": { "start": "node server.js" } +} +``` + +**`server.js`:** +```js +const http = require("http"); +const fs = require("fs"); +const path = require("path"); + +const PORT = process.env.PORT || 3000; +const DATA = path.join(__dirname, "data.json"); + +function readItems() { + try { return JSON.parse(fs.readFileSync(DATA, "utf8")); } catch (_) { return []; } +} +function writeItems(items) { + fs.writeFileSync(DATA, JSON.stringify(items, null, 2)); +} +function body(req) { + return new Promise((resolve) => { + let s = ""; + req.on("data", (c) => (s += c)); + req.on("end", () => { try { resolve(JSON.parse(s || "{}")); } catch (_) { resolve({}); } }); + }); +} + +http.createServer(async (req, res) => { + // API: список записей + if (req.method === "GET" && req.url === "/api/items") { + res.writeHead(200, { "Content-Type": "application/json" }); + return res.end(JSON.stringify(readItems())); + } + // API: добавить запись + if (req.method === "POST" && req.url === "/api/items") { + const item = await body(req); + const items = readItems(); + items.push({ id: Date.now(), ...item }); + writeItems(items); + res.writeHead(200, { "Content-Type": "application/json" }); + return res.end(JSON.stringify({ ok: true })); + } + // Статика: index.html / style.css / script.js + let file = req.url === "/" ? "/index.html" : req.url.split("?")[0]; + const full = path.join(__dirname, file); + if (full.startsWith(__dirname) && fs.existsSync(full) && fs.statSync(full).isFile()) { + const ext = path.extname(full); + const type = ext === ".css" ? "text/css" : ext === ".js" ? "application/javascript" : "text/html"; + res.writeHead(200, { "Content-Type": type + "; charset=utf-8" }); + return res.end(fs.readFileSync(full)); + } + res.writeHead(404); res.end("Not found"); +}).listen(PORT, () => console.log("Сервер на порту " + PORT)); +``` + +**`index.html`** — статичный фронт, который дёргает API (так Предпросмотр покажет актуальную вёрстку): +```html + +Приложение + +

Записи

+
+
    + + +``` + +`.gitignore`: `data.json`, `node_modules/`, `.vibe42-run.log`, `.vibe42-run.pid`. Затем `run` → дай юзеру ссылку из вывода ЦЕЛИКОМ (с `?t=...`). + +### Блок «ИИ» — умный ответ БЕЗ ключей и БЕЗ SDK + +Нужен ИИ в боте/приложении? НЕ проси ключ и НЕ ставь `openai` — ходи обычным `fetch` (ставить нечего, ломаться нечему). `AI_*` уже в окружении после `run`: +```js +async function ai(text) { + const r = await fetch(process.env.AI_BASE_URL + "/chat/completions", { + method: "POST", + headers: { "Content-Type": "application/json", Authorization: "Bearer " + process.env.AI_API_KEY }, + body: JSON.stringify({ model: process.env.AI_MODEL, messages: [{ role: "user", content: text }] }) + }); + if (!r.ok) throw new Error("ИИ недоступен, код " + r.status); + const j = await r.json(); + const out = j.choices?.[0]?.message?.content; + if (!out) throw new Error("ИИ вернул пустой ответ"); + return out; +} +``` +В боте: `bot.on("message:text", async (ctx) => ctx.reply(await ai(ctx.message.text)));`. Ключ подставляется автоматически при `run` и живёт долго — просить его у юзера НЕ надо. + +**⚠️ ЖЕЛЕЗНОЕ ПРАВИЛО: сбой ИИ показывай честно.** Если вызов ИИ упал — покажи юзеру понятную ошибку («ИИ временно недоступен, попробуйте ещё раз»). **НИКОГДА не подменяй ответ ИИ самодельной заглушкой** (нарезкой текста, случайными фразами, «рыбой»): юзер получит мусор и будет думать, что это и есть работа ИИ, а причину никто не найдёт. Лучше явная ошибка, чем тихая подделка. + +--- + +## 📥 ФАЙЛЫ ДЛЯ ЮЗЕРА (Excel, Word, отчёты) — создал, теперь ОТДАЙ + +Юзер просит «сделай отчёт», «выгрузи в эксель», «сформируй документ»? Файл мало создать — юзер должен его забрать. + +**Нужен Excel — бери команду `makexlsx`.** Она в твоём шелле, как `readdoc`, и делает настоящую книгу `.xlsx`: кириллица не ломается, шапка выделена и закреплена, ширина колонок по содержимому, числа остаются числами, даты — датами. + +```bash +# из CSV, который ты сам собрал +makexlsx Отчёт_июль.xlsx --from data.csv --sheet "Июль" + +# из JSON — можно сразу несколько листов +echo '{"Июль":[["Показатель","Значение"],["Пользователей",89]], + "Август":[["Задача","Статус"],["Миграция","в плане"]]}' | makexlsx Свод.xlsx +``` + +Принимает три вида JSON: строки `[[...],[...]]`, массив объектов `[{"Дата":"07.07"}, ...]` (ключи становятся шапкой) и словарь листов `{"Имя листа": [[...]]}`. + +**Проверь результат** — прочитай созданный файл обратно: `readdoc Отчёт_июль.xlsx`. + +**Правило имени: НЕ подделывай формат.** +- **НИКОГДА не клей двойное расширение** (`Otchet.xlsx.csv`, `report.docx.txt`). Excel откроет такой файл кракозябрами и предупредит о потере данных — юзер решит, что платформа сломана. +- Не используешь `makexlsx` — тогда честный `.csv` и **так и назови**, с BOM (`\uFEFF` в начале), иначе кириллица поедет. +- Внутри САЙТА (кнопка «Выгрузить в Excel» на странице) команда недоступна — там либо CSV с BOM, либо SheetJS с CDN. + +**Правило выдачи: ВСЕГДА скажи, где скачать.** Создал файл — следующей же фразой напиши юзеру: + +> Готово, файл `Otchet.xlsx` лежит в проекте. Открой вкладку **Файлы** справа и нажми **«Скачать»** напротив него. + +Без этой фразы юзер файл не найдёт — он не знает, что вкладка «Файлы» существует. + +**Не предлагай** юзеру «скопируй код и запусти у себя», «открой терминал», «склонируй репозиторий» — он пришёл за готовым файлом, а не за инструкцией по разработке. + +--- + +## 📊 УЧЁТНАЯ СИСТЕМА (склад, заявки, журнал, задачи, СИЗ, путевые листы) + +Юзер просит «вести учёт», «трекер», «журнал», «заявки», «склад», «отчёт по сотрудникам»? Это Рецепт B, но с несколькими сущностями и статусами. Не изобретай схему — бери эту. + +**Модель данных — один файл `data.json`, внутри списки:** +```js +// { "items": [...], "log": [...] } // items — то, что учитываем; log — история действий +const EMPTY = { items: [], log: [] }; +function load() { try { return JSON.parse(fs.readFileSync(DATA, "utf8")); } catch { return EMPTY; } } +function save(d) { fs.writeFileSync(DATA, JSON.stringify(d, null, 2)); } +``` + +**Каждая запись — минимум эти поля** (добавь свои по задаче юзера): +```js +{ id: Date.now(), title: "Каска защитная", qty: 12, person: "Иванов И.", + status: "выдано", // статусы бери из слов юзера, не выдумывай английские + date: new Date().toISOString().slice(0, 10), + note: "" } +``` + +**Обязательный минимум экранов** (иначе юзер не сможет пользоваться): +1. **Таблица** всех записей + строка поиска (фильтр по подстроке на клиенте). +2. **Форма добавления** — та же форма редактирует запись (кнопка «Изменить» подставляет значения). +3. **Фильтр по статусу** — кнопки-чипсы над таблицей. +4. **Счётчики сверху**: всего / по каждому статусу. +5. **Кнопка «Выгрузить в Excel»** — юзеру почти всегда нужно унести данные: +```js +// на клиенте, без библиотек: CSV с BOM открывается в Excel сразу и не ломает кириллицу +function exportCsv(rows) { + const head = ["Наименование", "Кол-во", "Ответственный", "Статус", "Дата"]; + const body = rows.map(r => [r.title, r.qty, r.person, r.status, r.date] + .map(v => '"' + String(v ?? "").replace(/"/g, '""') + '"').join(";")); + const csv = "\uFEFF" + [head.join(";"), ...body].join("\n"); + const a = document.createElement("a"); + a.href = URL.createObjectURL(new Blob([csv], { type: "text/csv" })); + a.download = "vygruzka.csv"; a.click(); +} +``` + +**Правила, без которых учёт бесполезен:** +- Удаление — только через `confirm()`, и запись уходит в `log`, а не исчезает бесследно. +- Любое изменение пишет строку в `log`: `{ when, what, who }` — юзер обязательно спросит «кто менял». +- Даты показывай как `ДД.ММ.ГГГГ`, хранить можно как есть. +- Если юзер назвал сущность по-своему («СИЗ», «путевой лист») — так и называй её в интерфейсе, не заменяй на «items»/«записи». + +--- + +## 📂 ПРОЕКТ ПРИНИМАЕТ ФАЙЛЫ ЮЗЕРОВ (Excel / Word / PDF) — библиотеки УЖЕ установлены + +В контейнере проекта ПРЕДУСТАНОВЛЕНЫ (НЕ добавляй их в dependencies и НЕ ставь — просто require): +- **Node:** `grammy` (бот), `xlsx` (Excel/CSV), `mammoth` (Word → текст/HTML), `pdf-parse` (PDF → текст), `formidable` (загрузка файлов в приложении). +- **Python:** `openpyxl`, `python-docx`, `pypdf`, `requests`. + +Всё, чего в списке нет, — добавляй в `dependencies`, поставит `run`. + +**Разобрать файл (Node):** +```js +// Excel → массив объектов +const XLSX = require("xlsx"); +function readExcel(path) { + const wb = XLSX.readFile(path); + return XLSX.utils.sheet_to_json(wb.Sheets[wb.SheetNames[0]]); +} +// Word → текст +const mammoth = require("mammoth"); +async function readWord(path) { return (await mammoth.extractRawText({ path })).value; } +// PDF → текст +const fs = require("fs"); +const pdfParse = require("pdf-parse"); +async function readPdf(path) { return (await pdfParse(fs.readFileSync(path))).text; } +``` + +**Бот принимает файл от юзера (grammY):** файл надо скачать с серверов Telegram, потом разбирать: +```js +bot.on("message:document", async (ctx) => { + const file = await ctx.getFile(); + const url = "https://api.telegram.org/file/bot" + process.env.BOT_TOKEN + "/" + file.file_path; + const buf = Buffer.from(await (await fetch(url)).arrayBuffer()); + const name = ctx.message.document.file_name || "file"; + if (name.endsWith(".xlsx") || name.endsWith(".xls")) { + const wb = require("xlsx").read(buf); + const rows = require("xlsx").utils.sheet_to_json(wb.Sheets[wb.SheetNames[0]]); + return ctx.reply("Строк в таблице: " + rows.length); + } + if (name.endsWith(".docx")) { + const t = (await require("mammoth").extractRawText({ buffer: buf })).value; + return ctx.reply("Текст из Word (начало): " + t.slice(0, 300)); + } + if (name.endsWith(".pdf")) { + const t = (await require("pdf-parse")(buf)).text; + return ctx.reply("Текст из PDF (начало): " + t.slice(0, 300)); + } + ctx.reply("Пока умею: .xlsx, .docx, .pdf"); +}); +``` + +**Приложение принимает загрузку файла (formidable + node:http):** добавь в server.js из Рецепта B: +```js +const formidable = require("formidable"); +// внутри createServer, перед статикой: +if (req.method === "POST" && req.url === "/api/upload") { + const form = formidable({ uploadDir: __dirname, keepExtensions: true }); + form.parse(req, (err, fields, files) => { + const f = files.file && files.file[0]; + res.writeHead(200, { "Content-Type": "application/json" }); + res.end(JSON.stringify({ ok: !err && !!f, path: f ? f.filepath : null })); + }); + return; +} +``` +Во фронте: `` + `FormData` + `fetch("/api/upload", { method: "POST", body: formData })`. Дальше разбирай файл рецептами выше и отвечай юзеру данными. + +--- + +## 🚑 НЕ ЗАПУСКАЕТСЯ? ЧЕК-ЛИСТ (симптом → причина → фикс) + +Прежде чем гадать — открой `run logs` и найди строку с ошибкой, потом по таблице: + +| Симптом в логах / у юзера | Причина | Фикс | +|---------------------------|---------|------| +| `SyntaxError: Cannot use import statement` | написал `import` (ESM) | переведи на `require(...)` (CommonJS). У нас по умолчанию CommonJS. | +| `run` пишет «не нашёл, что запускать» | нет `package.json` со `scripts.start` и нет `bot.js`/`server.js` | добавь `package.json` с `"scripts": {"start": "node bot.js"}` (или `server.js`) | +| `Error: Cannot find module 'X'` | зависимости `X` нет в `package.json` | добавь `X` в `dependencies`, запусти `run` заново (он поставит). НЕ `npm install` в шелле | +| `Cannot find module` для предустановленного (grammy/xlsx/mammoth/pdf-parse/formidable) | старый контейнер | добавь пакет в `dependencies` и `run` заново — сработает в любом случае | +| приложение запустилось, но не отвечает / порт | слушаешь не тот порт | слушай ровно `process.env.PORT || 3000` | +| `Empty token!` / бот не видит токен | `.env` не загружается | в `package.json` → `"start": "node --env-file=.env bot.js"`; `.env` рядом с `bot.js` | +| бот не отвечает в Telegram | используешь webhook | только `bot.start()` (long-polling), НЕ webhooks; токен в `.env` → `process.env.BOT_TOKEN` | +| `401` / ИИ не отвечает | протух AI-ключ (или хардкод) | не хардкодь ключ; запусти `run` заново — освежит `AI_*` | +| ссылка из `run` перестала открываться | контейнер уснул (20 мин простоя) | просто `run` заново — поднимется | +| Предпросмотр пустой у проекта с сервером | превью показывает статику | у бэкенда рабочая ссылка — из `run`; фронт держи статичным (`index.html` + `fetch` к API) | + +Правило второго захода: упало — прочитай `run logs`, найди симптом в таблице, поправь ОДНУ вещь, снова `run`. Не переписывай всё с нуля. + +--- + +## 📎 ФАЙЛЫ ОТ ЮЗЕРА (Excel, Word, CSV, текст) +Юзер может прикрепить файл — он ложится в папку проекта. **Читать Excel/Word напрямую (`cat`) нельзя** — они бинарные (zip+xml). Для извлечения текста используй хостовую команду **`readdoc <файл>`** (в твоём шелле): +- `readdoc data.xlsx` — все листы Excel как таблицы (значения через табуляцию); +- `readdoc doc.docx` — текст Word по абзацам; +- `readdoc doc.pdf` — текст из PDF (сканы без текстового слоя не прочтёт); +- `readdoc table.csv` / `.tsv` / `.txt` — как есть (можно и `cat`); +- большой файл — ограничь: `readdoc big.xlsx --max 100`. +Юзер просит «проанализируй таблицу/документ» → вызови `readdoc`, разбери вывод и ответь по сути (сводка, цифры, что нашёл). Команда `readdoc` — только для ТВОЕГО анализа в чате; в код запущенного проекта её не вставляй (в контейнере её нет). + +**Не нашёл файл в текущей папке?** Загруженный файл мог лечь в КОРЕНЬ воркспейса (на уровень выше cwd). Проверь `ls ..` и читай/копируй оттуда: `readdoc "../имя файла.xlsx"` или `cp "../имя.xlsx" .`. Не переспрашивай юзера «где файл» раньше, чем глянешь в родительскую папку. + +**Используй ВСЕ данные, не «первые N».** Если строишь из файла таблицу/список/каталог — выведи ВСЕ записи, а не 5-6. **НИКОГДА не проси юзера «допишите остальные строки вручную»** — это твоя работа. Данных много (десятки+ строк)? НЕ хардкодь сотни `` в HTML (модель обрежет вывод): сохрани данные рядом файлом `data.json` (массив объектов) и отрисуй в `index.html` через JS — `fetch('data.json').then(r=>r.json()).then(rows => …)` + `map`/`innerHTML`. Так отрисуются все строки, и появится место для поиска/фильтра. `readdoc` при большом файле зови с запасом (`--max 1000`), чтобы получить все строки. + +--- + +## 🗺 СЦЕНАРИЙ ПЕРВОГО ЗАХОДА (юзер только зашёл, ещё ничего нет) + +1. Поздоровайся коротко: «Привет! Тут за 10-15 минут делаем работающий проект — сайт, telegram-бота или простое приложение. Что хочешь сделать?» +2. Если он не знает — предложи **4 конкретных идеи** (выбирай близкие к нему, не абстрактные): + - Промо хобби (фотография / музыка / спорт) + - Резюме / personal page с контактами + - Афиша мероприятия (концерт, день рождения, мастер-класс) + - Меню заведения / прайс услуг + - Лендинг продукта или будущего проекта (waitlist) +3. Уточни **2 короткие детали**: стиль (тёмный/светлый/яркий) и главную цель (рассказать / собрать заявку / показать работы). +4. Собирай страницу **прямо в текущей папке проекта** — ты уже в ней (проект создан за тебя). Создавай `index.html`/`style.css`/`script.js` тут же. **НЕ запускай `./new-project`** и не делай `cd` в другие папки. Не спрашивай разрешения на каждый шаг. + +--- + +## 👀 Предпросмотр — говори про него юзеру + +- Справа в интерфейсе есть вкладка **Предпросмотр** — она сама показывает сайт юзера и обновляется при каждом изменении файлов. Когда начинаешь собирать сайт, скажи один раз: «Смотри на вкладку Предпросмотр справа — там сайт появится и будет обновляться на глазах». +- Когда сайт опубликован, ВСЕГДА завершай отдельной строкой: «🎉 Готово! Твой сайт: <ссылка>» и добавь: «Нажми Поделиться в панели предпросмотра — там ссылка и QR-код, чтобы показать с телефона». + +--- + +## 🧠 Память о юзере — веди её сам + +Файл памяти: `/srv/opencode/workspaces/users/askhat117/.mimo/memory.md` (dot-папка `.mimo` не видна в Предпросмотре — так и задумано). Папка `.mimo/` уже создана, тебе нужно только читать/писать `memory.md`. + +**В НАЧАЛЕ каждой сессии** — прочитай `/srv/opencode/workspaces/users/askhat117/.mimo/memory.md` (если есть) ПЕРЕД первым ответом. Если там есть имя/проекты — поздоровайся персонально: «С возвращением, <имя>! Продолжим <последний проект> или сделаем новый?». Если файла нет — это новый юзер, работай по обычному сценарию. + +**ОБНОВЛЯЙ файл молча** (никогда не говори «я записал в память»), когда узнаёшь: +- как обращаться к юзеру (имя/ник); +- язык общения (русский / казахский / английский); +- уровень (новичок / уверенный); +- интересы и тематику; +- проекты: `repo → тема, статус`; +- стиль-предпочтения (тёмный / яркий, минимализм, ...); +- незакрытые хвосты («хотел добавить фото», «вернёмся к форме»). + +**Формат:** короткий markdown-список, максимум ~30 строк. Старое неактуальное — удаляй, не копи. + +**ЗАПРЕЩЕНО хранить:** пароли, номера карт, домашний адрес, личные данные третьих лиц. + +**Лотус ≠ владелец аккаунта.** Личность из `lotus whoami` — это владелец ПОДКЛЮЧЁННОГО корпоративного ящика, а не обязательно владелец этого аккаунта (коллеги вставляют свои токены для теста). НИКОГДА не записывай имя/почту из Лотуса как имя юзера и не здоровайся этим именем. Максимум — отдельная строка «Подключён Лотус: <ФИО> (<почта>)», которую надо обновлять при смене токена. Обращайся к юзеру только по имени, которым он сам представился в чате. + +**Говори языком юзера.** Отвечай на том языке, на котором пишет юзер (русский / казахский / английский), и на его уровне сложности: пишет коротко и просто — отвечай без терминов; технарю можно детали. Юзер переключил язык — переключись следом. + +**Это касается ВСЕГО видимого текста, а не только финального ответа.** Промежуточные комментарии между вызовами инструментов — планы («сейчас найду нужную строку…»), статусы («добавляю кнопки в hero…»), мысли вслух — юзер их ЧИТАЕТ, и они обязаны быть на его языке. Писать «We'll edit line 139» русскоязычному юзеру — ошибка. По-английски остаются только код, команды и имена файлов. + +--- + +## 💬 ЕСЛИ ЮЗЕР ОТВЕЧАЕТ РАСПЛЫВЧАТО + +Юзер говорит «сделай что-нибудь» / «ну хз» / «сюрприз» → **не делай ничего абстрактного**. + +Скажи: «Давай определимся, я задам 3 коротких вопроса: +1. Это для тебя лично, для проекта/бизнеса, или для события? +2. Главная цель — рассказать о чём-то / собрать заявку / показать портфолио? +3. Любимое настроение — строгое тёмное, лёгкое светлое, яркое цветное?» + +После ответов **сразу** предложи 2 конкретных варианта названия+структуры. Дай выбрать и иди делать. + +--- + +## 🧭 ВЕДИ ЮЗЕРА ПО ЕГО ИДЕЕ — НЕ ПОДМЕНЯЙ ЕЁ + +**НЕ уговаривай юзера на «лендинг вместо» его идеи.** Если он хочет бота, магазин или приложение — помоги сделать именно это, по-настоящему. Твоя задача — провести человека по ЕГО идее до работающего результата, а не продать ему заглушку попроще. + +Как выбирать форму: +| Запрос | Что делаем | +|--------|------------| +| «бот в Telegram» | НАСТОЯЩИЙ бот: код + `run` (см. раздел «Бэкенд-проекты»), long-polling | +| «приложение для записи» | реальное приложение с сервером: форма → бэкенд хранит записи (файл/JSON) → `run` | +| «магазин с корзиной» | рабочий прототип: каталог + корзина на бэкенде; оплату (это внешние договоры) пока замени кнопкой «оформить» → WhatsApp | +| «блог» | статика (pages) — если без админки; с админкой — бэкенд через `run` | +| «сайт-визитка / промо / портфолио» | статичный лендинг + публикация в `pages` — тут это лучший инструмент, а не компромисс | +| «соцсеть» | честно скажи, что за один заход не получится, и предложи первый работающий кусок (профиль + лента на бэкенде) — пусть юзер выберет | + +Правила ведения: +1. Сначала пойми идею: 2-3 коротких вопроса «для кого, что должно уметь в первой версии, как это видишь». +2. Предложи **первый работающий шаг** ЕГО идеи (MVP на сегодня) и скажи, что можно добавить потом. Не ужимай идею молча. +3. Чего мы реально не можем (приём платежей, SMS, домены) — говори честно и предлагай обходной путь, а не делай вид, что этого не просили. +4. **Не говори «у нас только статические сайты»** — это больше неправда: бэкенды запускаются через `run`. + +--- + +## 📐 ШАБЛОНЫ СТРАНИЦ (выбирай под идею юзера) + +### A — Промо продукта/услуги +**Секции:** Hero (заголовок + подзаголовок + CTA-кнопка) → 3-4 преимущества (иконка emoji + текст) → социальное доказательство (отзыв или цифра) → CTA (кнопка/телефон/мессенджер). + +### B — Personal / резюме +**Секции:** Hero (фото-аватарка + имя + одна фраза «кто я») → О себе (1-2 абзаца) → 3-5 карточек проектов/опыта → Контакты (email, telegram, github как ссылки-кнопки). + +### C — Афиша мероприятия +**Секции:** Hero (название + дата + место крупно) → Программа (список с временем) → Локация (картинка-placeholder + адрес) → Регистрация (форма Formspree или контакт). + +### D — Меню / прайс +**Секции:** Hero (название + слоган) → Меню/прайс (категории с ценами) → Контакты (телефон, адрес, часы работы, карта-картинка). + +### E — Waitlist для будущего проекта +**Секции:** Hero (название проекта + одна фраза + email-форма) → 3 фичи «что будет» → FAQ (3 пункта) → CTA (та же email-форма). + +Все шаблоны — **одна страница, прокрутка вниз**. Никаких роутов, ничего динамического. + +--- + +## 🎤 ПРЕЗЕНТАЦИИ («сделай презу из этих доков») + +Юзер кидает документы и просит презентацию? Флоу: +1. Прочитай доки командой `readdoc` (умеет .xlsx, .docx, .pdf, .csv/.txt). Файлы могут лежать в корне воркспейса — проверь `ls ..`. +2. Выдели суть: 5-9 слайдов, на слайде заголовок + 3-5 коротких пунктов. Не переноси абзацы целиком — сжимай до тезисов. +3. Возьми скелет ниже (это ОДИН файл `index.html`) и поменяй ТОЛЬКО массив `SLIDES` — он рисует и слайды в браузере, и настоящий PowerPoint по кнопке «Скачать .pptx». Остальной код не трогай. +4. Покажи через Предпросмотр (стрелки/клавиши листают). Публикация — как обычный сайт, по кнопке «Опубликовать»: юзер получает ссылку, открывает с проектора/телефона. +5. Скажи юзеру: «Кнопка Скачать .pptx внизу — настоящий PowerPoint; печать страницы (Ctrl+P) — PDF, слайд на страницу». + +Скелет (файл `index.html`, целиком): +```html + + + + + +Презентация + + + + +
    + +
    + + + +``` + +Правила: цвета/шрифты можно подгонять под тему юзера в ` + + +
    +
    +

    Секция

    +
    Контент карточки.
    +
    Контент карточки.
    +
    + + +``` + +## Что НЕ юзать + +- Bootstrap / Material UI / Chakra / Ant Design — слишком тяжело +- Font Awesome — используй emoji (🚀 ⚡ ✨) или inline SVG +- jQuery — vanilla JS достаточно + +## Что МОЖНО (если очень надо) + +- **Tailwind через CDN**: `` +- **Placeholder картинки**: `https://picsum.photos/800/600` +- **Google Fonts через ``** в head + +## 📦 Готовые рецепты (используй их, не изобретай) + +### Фото без ключей +- Детерминированные (одно и то же слово → одна и та же картинка): `https://picsum.photos/seed/coffee/800/600`. Меняй `coffee` на любое слово-seed. +- Тематические (по тегу): `https://loremflickr.com/800/600/coffee` — тег прямо в URL (`/coffee`, `/mountains`, `/food`). Тема не гарантируется на 100%, но обычно в кассу. + +### Кнопка WhatsApp +```html +Написать в WhatsApp +``` +`7XXXXXXXXXX` — номер в межд. формате без `+`. `text=` — предзаполненное сообщение. + +### Форма заявки без бэкенда +```html +
    + + + +
    +``` +Скажи юзеру: замени `EMAIL` на свою почту — заявки будут приходить туда (первая заявка активирует, письмо-подтверждение придёт один раз). + +### Google Fonts (максимум 2 шрифта: один display + один text) +```html + + +``` +В CSS: `font-family:'Unbounded',sans-serif` для заголовков, `'Manrope',sans-serif` для текста. + +### Favicon-эмодзи (inline SVG data-URI, без файла) +```html + +``` +Меняй эмодзи `☕` на подходящее теме. + +### Карта (Google Maps embed, без ключа) +На maps.google.com найди место → «Поделиться» → «Встроить карту» → скопируй iframe. Вставь как есть: +```html + +``` + +### Плавные появления при скролле (IntersectionObserver) +```html + + +``` +Добавь класс `reveal` любой секции/карточке — появится плавно при прокрутке. diff --git a/index.html b/index.html new file mode 100644 index 0000000..701c7cd --- /dev/null +++ b/index.html @@ -0,0 +1,274 @@ + + + + + + Алтын Дастархан — Казахский ресторан + + + +
    + +
    + +
    +
    +
    +

    Алтын Дастархан

    +

    Настоящая казахская кухня в сердце города

    + Смотреть меню +
    +
    + +
    +
    +
    + Интерьер ресторана +
    +
    +

    Добро пожаловать!

    +

    Ресторан «Алтын Дастархан» — это место, где оживают традиции казахского гостеприимства. Мы бережно храним рецепты, которые передавались из поколения в поколение, и создаём атмосферу настоящего казахского той.

    +

    Наши повара готовят из свежих местных продуктов, используя традиционные методы приготовления. Каждое блюдо — это произведение искусства, наполненное вкусом степных просторов.

    +
    +
    + 🍖 + Традиционные рецепты +
    +
    + 🎵 + Живая музыка по выходным +
    +
    + 👨‍👩‍👧‍👦 + Семейная атмосфера +
    +
    +
    +
    +
    + + + +
    +
    +

    Акции и скидки

    + +
    +
    +
    −15%
    +

    Обеденное время

    +

    Пн-Пт, 12:00 - 15:00

    +

    Скидка 15% на все блюда меню в обеденное время

    +
    + + + +
    +
    −20%
    +

    День рождения

    +

    В ваш праздник

    +

    Скидка 20% имениннику и специальное поздравление от шефа

    +
    + +
    +
    🎁
    +

    Бауырсаки в подарок

    +

    Каждый день

    +

    При заказе любого горячего блюда — бауырсаки с медом бесплатно

    +
    +
    + +
    +

    💎 Программа лояльности

    +

    Станьте постоянным гостем «Алтын Дастархан» и получайте кэшбэк 10% с каждого заказа. Накопленные баллы можно обменять на любые блюда из меню.

    + Узнать подробнее +
    +
    +
    + +
    +
    +

    Контакты

    + +
    +
    +
    + 📍 +
    +

    Адрес

    +

    г. Алматы, ул. Абая, 42

    +
    +
    +
    + 📞 +
    +

    Телефон

    +

    +7 (727) 123-45-67

    +
    +
    +
    + ⏰ +
    +

    Режим работы

    +

    Пн-Чт: 11:00 - 23:00
    Пт-Сб: 11:00 - 00:00
    Вс: 11:00 - 23:00

    +
    +
    +
    + ✉️ + +
    +
    + +
    +

    Забронировать стол

    +
    + + + + + + +
    +
    +
    +
    +
    + +
    +
    +

    © 2026 Ресторан «Алтын Дастархан». Все права защищены.

    + +
    +
    + + + + diff --git a/script.js b/script.js new file mode 100644 index 0000000..087e794 --- /dev/null +++ b/script.js @@ -0,0 +1,30 @@ +document.addEventListener('DOMContentLoaded', function() { + const form = document.getElementById('reservationForm'); + + form.addEventListener('submit', function(e) { + e.preventDefault(); + + const inputs = form.querySelectorAll('input, textarea'); + const data = {}; + inputs.forEach(input => { + data[input.name || input.placeholder] = input.value; + }); + + alert('Спасибо за бронирование! Мы свяжемся с вами в ближайшее время для подтверждения.'); + form.reset(); + }); + + const links = document.querySelectorAll('a[href^="#"]'); + links.forEach(link => { + link.addEventListener('click', function(e) { + const href = this.getAttribute('href'); + if (href !== '#') { + e.preventDefault(); + const target = document.querySelector(href); + if (target) { + target.scrollIntoView({ behavior: 'smooth', block: 'start' }); + } + } + }); + }); +}); diff --git a/style.css b/style.css new file mode 100644 index 0000000..bbe55c6 --- /dev/null +++ b/style.css @@ -0,0 +1,548 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +:root { + --primary: #c9a227; + --primary-dark: #a6851a; + --secondary: #8b4513; + --accent: #d4a574; + --dark: #1a1a1a; + --light: #f5f0e6; + --warm: #f9f3e6; + --text: #2c2c2c; + --text-light: #666; + --border: #e0d5c1; +} + +body { + font-family: 'Georgia', 'Times New Roman', serif; + background: var(--warm); + color: var(--text); + line-height: 1.6; + overflow-x: hidden; +} + +.container { + max-width: 1200px; + margin: 0 auto; + padding: 0 20px; +} + +.section { + padding: 80px 0; +} + +.section-title { + text-align: center; + font-size: clamp(32px, 5vw, 48px); + color: var(--secondary); + margin-bottom: 50px; + position: relative; + padding-bottom: 20px; +} + +.section-title::after { + content: ''; + position: absolute; + bottom: 0; + left: 50%; + transform: translateX(-50%); + width: 100px; + height: 3px; + background: linear-gradient(90deg, transparent, var(--primary), transparent); +} + +.btn { + display: inline-block; + padding: 14px 32px; + background: var(--primary); + color: white; + text-decoration: none; + border-radius: 4px; + font-weight: 600; + font-size: 16px; + border: 2px solid var(--primary); + transition: all 0.3s ease; + cursor: pointer; +} + +.btn:hover { + background: var(--primary-dark); + border-color: var(--primary-dark); + transform: translateY(-2px); +} + +.btn-outline { + background: transparent; + color: var(--primary); +} + +.btn-outline:hover { + background: var(--primary); + color: white; +} + +.header { + position: fixed; + top: 0; + left: 0; + right: 0; + background: rgba(26, 26, 26, 0.95); + backdrop-filter: blur(10px); + z-index: 1000; + padding: 15px 0; + box-shadow: 0 2px 20px rgba(0,0,0,0.3); +} + +.nav { + display: flex; + justify-content: space-between; + align-items: center; +} + +.logo { + font-size: 24px; + font-weight: bold; + color: var(--primary); + text-transform: uppercase; + letter-spacing: 2px; +} + +.nav-links { + display: flex; + list-style: none; + gap: 30px; +} + +.nav-links a { + color: var(--light); + text-decoration: none; + font-weight: 500; + transition: color 0.3s ease; + position: relative; +} + +.nav-links a::after { + content: ''; + position: absolute; + bottom: -5px; + left: 0; + width: 0; + height: 2px; + background: var(--primary); + transition: width 0.3s ease; +} + +.nav-links a:hover { + color: var(--primary); +} + +.nav-links a:hover::after { + width: 100%; +} + +.hero { + height: 100vh; + background: linear-gradient(135deg, rgba(139, 69, 19, 0.7), rgba(201, 162, 39, 0.5)), + url('4884.jpg') center/cover no-repeat; + display: flex; + align-items: center; + justify-content: center; + text-align: center; + position: relative; +} + +.hero-overlay { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: radial-gradient(ellipse at center, transparent 0%, rgba(0,0,0,0.4) 100%); +} + +.hero-content { + position: relative; + z-index: 1; + color: white; +} + +.hero h1 { + font-size: clamp(48px, 10vw, 96px); + margin-bottom: 20px; + text-shadow: 2px 2px 8px rgba(0,0,0,0.5); + letter-spacing: 3px; + color: var(--primary); +} + +.hero-subtitle { + font-size: clamp(18px, 3vw, 24px); + margin-bottom: 30px; + font-style: italic; + text-shadow: 1px 1px 4px rgba(0,0,0,0.5); +} + +.about { + background: white; +} + +.about-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 60px; + align-items: center; +} + +.about-image img { + width: 100%; + height: auto; + border-radius: 8px; + box-shadow: 0 10px 40px rgba(0,0,0,0.2); +} + +.about-text h2 { + font-size: 36px; + color: var(--secondary); + margin-bottom: 20px; +} + +.about-text p { + color: var(--text-light); + margin-bottom: 15px; + font-size: 17px; +} + +.features { + display: flex; + flex-wrap: wrap; + gap: 20px; + margin-top: 30px; +} + +.feature { + display: flex; + align-items: center; + gap: 10px; + padding: 12px 20px; + background: var(--warm); + border-radius: 8px; + border: 1px solid var(--border); +} + +.feature-icon { + font-size: 24px; +} + +.menu { + background: var(--warm); +} + +.menu-category { + margin-bottom: 50px; +} + +.category-title { + font-size: 28px; + color: var(--secondary); + margin-bottom: 30px; + padding-bottom: 15px; + border-bottom: 2px solid var(--primary); + display: inline-block; +} + +.menu-grid { + display: grid; + gap: 25px; +} + +.menu-item { + background: white; + padding: 25px; + border-radius: 8px; + box-shadow: 0 4px 15px rgba(0,0,0,0.08); + border: 1px solid var(--border); + transition: transform 0.3s ease, box-shadow 0.3s ease; +} + +.menu-item:hover { + transform: translateY(-5px); + box-shadow: 0 8px 25px rgba(0,0,0,0.12); +} + +.item-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 10px; +} + +.item-header h4 { + font-size: 20px; + color: var(--dark); + font-weight: 600; +} + +.price { + font-size: 20px; + font-weight: bold; + color: var(--primary); + white-space: nowrap; +} + +.item-desc { + color: var(--text-light); + font-size: 15px; + line-height: 1.5; +} + +.promo { + background: linear-gradient(135deg, var(--secondary), var(--dark)); + color: white; +} + +.promo .section-title { + color: var(--primary); +} + +.promo-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); + gap: 30px; + margin-bottom: 50px; +} + +.promo-card { + background: rgba(255,255,255,0.1); + backdrop-filter: blur(10px); + padding: 30px; + border-radius: 12px; + border: 2px solid rgba(201, 162, 39, 0.3); + position: relative; + overflow: hidden; + transition: all 0.3s ease; +} + +.promo-card:hover { + transform: translateY(-5px); + border-color: var(--primary); + background: rgba(255,255,255,0.15); +} + +.promo-card.featured { + border-color: var(--primary); + background: rgba(201, 162, 39, 0.2); +} + +.promo-badge { + position: absolute; + top: 15px; + right: 15px; + background: var(--primary); + color: white; + padding: 8px 15px; + border-radius: 20px; + font-weight: bold; + font-size: 14px; +} + +.promo-badge.hot { + background: linear-gradient(135deg, #ff6b6b, #ff8e53); + animation: pulse 2s infinite; +} + +@keyframes pulse { + 0%, 100% { transform: scale(1); } + 50% { transform: scale(1.05); } +} + +.promo-card h3 { + font-size: 24px; + margin-bottom: 10px; + color: var(--primary); +} + +.promo-time { + color: var(--accent); + font-weight: 500; + margin-bottom: 15px; + font-size: 14px; +} + +.promo-card p { + opacity: 0.9; + line-height: 1.6; +} + +.loyalty-program { + background: rgba(201, 162, 39, 0.15); + border: 2px solid var(--primary); + border-radius: 12px; + padding: 40px; + text-align: center; +} + +.loyalty-program h3 { + font-size: 28px; + color: var(--primary); + margin-bottom: 15px; +} + +.loyalty-program p { + max-width: 600px; + margin: 0 auto 25px; + opacity: 0.9; +} + +.contacts { + background: white; +} + +.contacts-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 60px; +} + +.contact-info { + display: flex; + flex-direction: column; + gap: 25px; +} + +.contact-item { + display: flex; + gap: 20px; + align-items: flex-start; +} + +.contact-icon { + font-size: 32px; + flex-shrink: 0; +} + +.contact-item h4 { + font-size: 18px; + color: var(--secondary); + margin-bottom: 8px; +} + +.contact-item p { + color: var(--text-light); + line-height: 1.6; +} + +.contact-item a { + color: var(--primary); + text-decoration: none; + transition: color 0.3s ease; +} + +.contact-item a:hover { + color: var(--primary-dark); +} + +.contact-form-wrapper { + background: var(--warm); + padding: 35px; + border-radius: 12px; + border: 1px solid var(--border); +} + +.contact-form-wrapper h3 { + font-size: 24px; + color: var(--secondary); + margin-bottom: 25px; +} + +.contact-form { + display: flex; + flex-direction: column; + gap: 15px; +} + +.contact-form input, +.contact-form textarea { + padding: 14px 18px; + border: 1px solid var(--border); + border-radius: 6px; + font-size: 16px; + font-family: inherit; + background: white; + transition: border-color 0.3s ease, box-shadow 0.3s ease; +} + +.contact-form input:focus, +.contact-form textarea:focus { + outline: none; + border-color: var(--primary); + box-shadow: 0 0 0 3px rgba(201, 162, 39, 0.1); +} + +.contact-form textarea { + resize: vertical; +} + +.footer { + background: var(--dark); + color: var(--light); + text-align: center; + padding: 30px 0; +} + +.footer p { + opacity: 0.8; +} + +.footer-note { + margin-top: 10px; + font-style: italic; + color: var(--primary); +} + +@media (max-width: 968px) { + .about-grid, + .contacts-grid { + grid-template-columns: 1fr; + } + + .nav-links { + gap: 15px; + } + + .hero h1 { + font-size: clamp(36px, 8vw, 64px); + } + + .features { + flex-direction: column; + } +} + +@media (max-width: 640px) { + .nav { + flex-direction: column; + gap: 15px; + } + + .nav-links { + flex-wrap: wrap; + justify-content: center; + } + + .menu-item { + padding: 20px; + } + + .item-header { + flex-direction: column; + align-items: flex-start; + gap: 8px; + } + + .promo-grid { + grid-template-columns: 1fr; + } +}