Compare commits
No commits in common. "9c49159411b5cc5e7cec59daa974c44a342cf4e3" and "24ac1dca369820ad41ef5aa0879778329d469e41" have entirely different histories.
9c49159411
...
24ac1dca36
23
AGENTS.md
23
AGENTS.md
@ -1,4 +1,4 @@
|
|||||||
<!-- vibe42-agents-version: v43-apps-publish-2026-09-15 -->
|
<!-- vibe42-agents-version: v39-skills-2026-09-11 -->
|
||||||
# Vibe42 — учебная песочница: сайты, боты и первые приложения
|
# Vibe42 — учебная песочница: сайты, боты и первые приложения
|
||||||
|
|
||||||
Workspace юзера `tore`. Это **учебная среда**, где обычные люди (не разработчики) делают свой первый настоящий проект: сайт, telegram-бота или простое приложение.
|
Workspace юзера `tore`. Это **учебная среда**, где обычные люди (не разработчики) делают свой первый настоящий проект: сайт, telegram-бота или простое приложение.
|
||||||
@ -21,15 +21,14 @@ Workspace юзера `tore`. Это **учебная среда**, где обы
|
|||||||
|
|
||||||
| Что хочет юзер | Стек (ЖЁСТКО) | Навык / рецепт — загрузить ПЕРЕД работой |
|
| Что хочет юзер | Стек (ЖЁСТКО) | Навык / рецепт — загрузить ПЕРЕД работой |
|
||||||
|----------------|---------------|--------|
|
|----------------|---------------|--------|
|
||||||
| Сайт, лендинг, визитка, портфолио, меню, афиша, waitlist (форма на нём — только если ответы никуда сохранять не надо; иначе строка «Простое приложение с сервером») | **Статика:** в проекте УЖЕ лежит стартовый `index.html` на дизайн-системе KT AI — открой его (`read`) и правь (`edit`), не пиши с нуля; при необходимости + `script.js`. Ванильный JS, БЕЗ сборки, БЕЗ React/Vue/Vite, БЕЗ npm. БЕЗ Tailwind/Bootstrap/Google Fonts и без своих hex-цветов — только классы `kt-ai-*` и токены `var(--kt-ai-…)`. | навык `landing-templates` (шаблоны секций A–E) + `design.md` → правка готового `index.html`, публикация в `pages` |
|
| Сайт, лендинг, визитка, портфолио, меню, афиша, waitlist | **Статика:** в проекте УЖЕ лежит стартовый `index.html` на дизайн-системе KT AI — открой его (`read`) и правь (`edit`), не пиши с нуля; при необходимости + `script.js`. Ванильный JS, БЕЗ сборки, БЕЗ React/Vue/Vite, БЕЗ npm. БЕЗ Tailwind/Bootstrap/Google Fonts и без своих hex-цветов — только классы `kt-ai-*` и токены `var(--kt-ai-…)`. | навык `landing-templates` (шаблоны секций A–E) + `design.md` → правка готового `index.html`, публикация в `pages` |
|
||||||
| Telegram-бот | **Node.js (CommonJS) + grammY** (предустановлен), long-polling | навык `backend-run` → Рецепт T |
|
| Telegram-бот | **Node.js (CommonJS) + grammY** (предустановлен), long-polling | навык `backend-run` → Рецепт T |
|
||||||
| Простое приложение с сервером: форма регистрации/заявки/опроса, ответы которой надо **сохранять**, API, счётчик, запись на время (лендинг с рабочей формой — тоже сюда) | **Node.js (CommonJS), сервер на `node:http` БЕЗ зависимостей + хранение в `data.json`** | навыки `kt-design-system` (архетип экрана) → `backend-run` → Рецепт B |
|
| Приложение с сервером: форма→сохраняет, API, дашборд с данными, счётчик, запись на время | **Node.js (CommonJS), сервер на `node:http` БЕЗ зависимостей + хранение в `data.json`** | навык `backend-run` → Рецепт B |
|
||||||
| **Полноценное приложение:** кабинет, панель, CRM-lite, несколько экранов, живые данные — юзер явно просит «приложение/систему», а не страницу | **Express + Vite/React (фронт собирается в `dist/`, сервер его раздаёт с одного порта) + `data.json`.** Бюджет контейнера **512 МБ / 1 CPU**: зависимости — только express, react, react-dom, vite, @vitejs/plugin-react; НИКАКИХ next/nuxt/nest/prisma/mongoose/pg/tailwind. Сборку и установку делает `run` (scripts.build + scripts.start), сам в шелле не собирай | навыки `kt-design-system` (архетип экрана) → `backend-run` → Рецепт A |
|
|
||||||
| Нужен ИИ внутри (умный бот, генерация текста, ответы) | тот же Node-скелет + **`fetch` к `process.env.AI_BASE_URL`** (без SDK) | навык `backend-run` → блок «ИИ» |
|
| Нужен ИИ внутри (умный бот, генерация текста, ответы) | тот же Node-скелет + **`fetch` к `process.env.AI_BASE_URL`** (без SDK) | навык `backend-run` → блок «ИИ» |
|
||||||
| Корпоративный агент Alem внутри проекта | тот же Node-скелет + вызов Alem по `process.env.ALEM_*` | навык `backend-run` → раздел «Агент Alem» |
|
| Корпоративный агент Alem внутри проекта | тот же Node-скелет + вызов Alem по `process.env.ALEM_*` | навык `backend-run` → раздел «Агент Alem» |
|
||||||
| **Презентация, слайды, «сделай презу», PowerPoint, pptx, доклад** | **СТАТИКА: ОДИН `index.html` + pptxgenjs с CDN. Бэкенд НЕ поднимать, ИИ в рантайме НЕ звать** | навык `presentations` — бери оттуда скелет целиком |
|
| **Презентация, слайды, «сделай презу», PowerPoint, pptx, доклад** | **СТАТИКА: ОДИН `index.html` + pptxgenjs с CDN. Бэкенд НЕ поднимать, ИИ в рантайме НЕ звать** | навык `presentations` — бери оттуда скелет целиком |
|
||||||
| Разобрать документы юзера (Excel / Word / PDF): свод, отчёт, выжимка | Предустановленные `xlsx` / `mammoth` / `pdf-parse` | навык `office-files` |
|
| Разобрать документы юзера (Excel / Word / PDF): свод, отчёт, выжимка | Предустановленные `xlsx` / `mammoth` / `pdf-parse` | навык `office-files` |
|
||||||
| **Учёт и трекер:** склад, заявки, журнал, задачи, СИЗ, путевые листы, сбор статистики, отчётность | **Node.js + `node:http` + `data.json`** — тот же Рецепт B, но несколько сущностей и статусы | навыки `kt-design-system` → `accounting-system` → `backend-run` (Рецепт B) |
|
| **Учёт и трекер:** склад, заявки, журнал, задачи, СИЗ, путевые листы, сбор статистики, отчётность | **Node.js + `node:http` + `data.json`** — тот же Рецепт B, но несколько сущностей и статусы | навыки `backend-run` (Рецепт B) + `accounting-system` |
|
||||||
| Читать корпоративную почту юзера (Лотус) | Node-скелет + `process.env.LOTUS_*` | навык `backend-run` → «бот, читающий почту (Lotus)» |
|
| Читать корпоративную почту юзера (Лотус) | Node-скелет + `process.env.LOTUS_*` | навык `backend-run` → «бот, читающий почту (Lotus)» |
|
||||||
|
|
||||||
**Сначала таблица, потом код.** Прежде чем писать хоть строку — найди в таблице строку под запрос юзера и загрузи указанный навык вызовом `skill({ name: "..." })`: рецепты и скелеты лежат там, а не здесь. Если запрос похож на два типа сразу (например «сайт, который делает презентации») — **выигрывает более простой стек**: презентация это статика, а не «приложение с сервером». Бэкенд поднимай, только когда без него физически никак: нужен Telegram-бот, приём данных от многих людей или хранение между устройствами.
|
**Сначала таблица, потом код.** Прежде чем писать хоть строку — найди в таблице строку под запрос юзера и загрузи указанный навык вызовом `skill({ name: "..." })`: рецепты и скелеты лежат там, а не здесь. Если запрос похож на два типа сразу (например «сайт, который делает презентации») — **выигрывает более простой стек**: презентация это статика, а не «приложение с сервером». Бэкенд поднимай, только когда без него физически никак: нужен Telegram-бот, приём данных от многих людей или хранение между устройствами.
|
||||||
@ -40,7 +39,6 @@ Workspace юзера `tore`. Это **учебная среда**, где обы
|
|||||||
1. Зависимости — ТОЛЬКО в `package.json` → `dependencies`. Их поставит `run`. НИКОГДА не пиши `npm install` в шелле.
|
1. Зависимости — ТОЛЬКО в `package.json` → `dependencies`. Их поставит `run`. НИКОГДА не пиши `npm install` в шелле.
|
||||||
2. Порт — всегда `process.env.PORT || 3000`. Не хардкодь другой.
|
2. Порт — всегда `process.env.PORT || 3000`. Не хардкодь другой.
|
||||||
3. Запуск бэкенда — ТОЛЬКО командой `run`. Никогда сам `node ...` / `npm start` в шелле.
|
3. Запуск бэкенда — ТОЛЬКО командой `run`. Никогда сам `node ...` / `npm start` в шелле.
|
||||||
4. **Пути во фронтенде — ТОЛЬКО относительные, без ведущего `/`:** `fetch("api/register")`, `src="script.js"`, `href="list.html"`. Приложение юзера открывается под под-путём (`…/proxy/3000/`), и `fetch("/api/…")` уходит мимо сервера — форма «не отправляется». В сервере наоборот сравнивай `url` с `/api/...` как обычно.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
## 🍳 Готовые рецепты кода → навык `backend-run`
|
## 🍳 Готовые рецепты кода → навык `backend-run`
|
||||||
@ -102,7 +100,7 @@ Workspace юзера `tore`. Это **учебная среда**, где обы
|
|||||||
|
|
||||||
Файл памяти: `/srv/opencode/workspaces/users/tore/.mimo/memory.md` (dot-папка `.mimo` не видна в Предпросмотре — так и задумано). Папка `.mimo/` уже создана, тебе нужно только читать/писать `memory.md`.
|
Файл памяти: `/srv/opencode/workspaces/users/tore/.mimo/memory.md` (dot-папка `.mimo` не видна в Предпросмотре — так и задумано). Папка `.mimo/` уже создана, тебе нужно только читать/писать `memory.md`.
|
||||||
|
|
||||||
**В НАЧАЛЕ каждой сессии** — прочитай `/srv/opencode/workspaces/users/tore/.mimo/memory.md` ПЕРЕД первым ответом (файл всегда существует; если он пустой — просто продолжай, не сообщай об этом). Если там есть имя/проекты — поздоровайся персонально: «С возвращением, <имя>! Продолжим <последний проект> или сделаем новый?». Если файла нет — это новый юзер, работай по обычному сценарию.
|
**В НАЧАЛЕ каждой сессии** — прочитай `/srv/opencode/workspaces/users/tore/.mimo/memory.md` (если есть) ПЕРЕД первым ответом. Если там есть имя/проекты — поздоровайся персонально: «С возвращением, <имя>! Продолжим <последний проект> или сделаем новый?». Если файла нет — это новый юзер, работай по обычному сценарию.
|
||||||
|
|
||||||
**ОБНОВЛЯЙ файл молча** (никогда не говори «я записал в память»), когда узнаёшь:
|
**ОБНОВЛЯЙ файл молча** (никогда не говори «я записал в память»), когда узнаёшь:
|
||||||
- как обращаться к юзеру (имя/ник);
|
- как обращаться к юзеру (имя/ник);
|
||||||
@ -187,8 +185,6 @@ Workspace юзера `tore`. Это **учебная среда**, где обы
|
|||||||
---
|
---
|
||||||
## 🚀 ПУБЛИКАЦИЯ В ИНТЕРНЕТ — ТОЛЬКО ПО КНОПКЕ «Опубликовать»
|
## 🚀 ПУБЛИКАЦИЯ В ИНТЕРНЕТ — ТОЛЬКО ПО КНОПКЕ «Опубликовать»
|
||||||
|
|
||||||
**Проект с сервером (server.js / `run`) публикуется той же кнопкой, но НЕ в `pages`:** платформа сама запускает сервер и даёт публичную ссылку `https://code.vibe42.kz/apps/tore/<repo>/` (открывается без входа, формы и `api/…` работают, уснувшее приложение просыпается по первому заходу). Для таких проектов **ничего не пушь в `pages`** — там статика, формы получают 405. Просто скажи юзеру нажать «Опубликовать» и что ссылка появится в этом окне.
|
|
||||||
|
|
||||||
**Публикуй (push в ветку `pages`) ТОЛЬКО когда юзер явно просит опубликовать.** Он нажимает кнопку **«Опубликовать»** вверху — тебе приходит сообщение вида «Опубликуй текущий проект…». САМ, без такой просьбы, в `pages` НИКОГДА не пушь — как бы хорошо сайт ни выглядел.
|
**Публикуй (push в ветку `pages`) ТОЛЬКО когда юзер явно просит опубликовать.** Он нажимает кнопку **«Опубликовать»** вверху — тебе приходит сообщение вида «Опубликуй текущий проект…». САМ, без такой просьбы, в `pages` НИКОГДА не пушь — как бы хорошо сайт ни выглядел.
|
||||||
|
|
||||||
Когда юзер попросил опубликовать:
|
Когда юзер попросил опубликовать:
|
||||||
@ -277,23 +273,22 @@ git push origin HEAD:pages
|
|||||||
- **Pages 404** → запушь ветку `pages` снова: `git push origin HEAD:pages -f`
|
- **Pages 404** → запушь ветку `pages` снова: `git push origin HEAD:pages -f`
|
||||||
|
|
||||||
### ⚠️ Проекты с бэкендом (server.js / run) и статика — НЕ путай юзера
|
### ⚠️ Проекты с бэкендом (server.js / run) и статика — НЕ путай юзера
|
||||||
Публикация в Pages — статика, `server.js` там НЕ работает. Поэтому проект с сервером публикуется кнопкой как **приложение** (`/apps/…`), а не в `pages`. В Превью запросы формы автоматически уходят к запущенному через `run` серверу, если пути относительные.
|
Вкладка **Превью** и публикация в Pages показывают ТОЛЬКО статические файлы — server.js там НЕ работает: формы method=POST и запросы к твоему серверу будут мертвы. Поэтому:
|
||||||
- Прежде чем переделывать работающий статический index.html на серверный рендер (форма POST, шаблоны из server.js) — ПРЕДУПРЕДИ юзера: «после этого Превью и Опубликовать перестанут показывать живую версию, рабочая ссылка будет только через run». Меняй только после его согласия.
|
- Прежде чем переделывать работающий статический index.html на серверный рендер (форма POST, шаблоны из server.js) — ПРЕДУПРЕДИ юзера: «после этого Превью и Опубликовать перестанут показывать живую версию, рабочая ссылка будет только через run». Меняй только после его согласия.
|
||||||
- Если проект уже с бэкендом: держи index.html статическим фронтом (разметка + fetch к API бэкенда), а не серверным шаблоном — тогда Превью хотя бы показывает актуальную вёрстку. Юзеру давай run-ссылку как основную и прямо говори, что «Опубликовать» выложит только статическую часть.
|
- Если проект уже с бэкендом: держи index.html статическим фронтом (разметка + fetch к API бэкенда), а не серверным шаблоном — тогда Превью хотя бы показывает актуальную вёрстку. Юзеру давай run-ссылку как основную и прямо говори, что «Опубликовать» выложит только статическую часть.
|
||||||
- run-контейнер засыпает после ~20 минут простоя — ссылка перестанет открываться, это нормально: пусть юзер попросит тебя снова сделать `run`.
|
- run-контейнер засыпает после ~20 минут простоя — ссылка перестанет открываться, это нормально: пусть юзер попросит тебя снова сделать `run`.
|
||||||
- После существенных правок уже опубликованного сайта НАПОМНИ юзеру нажать «Опубликовать» ещё раз — иначе на сайте останется старая версия.
|
- После существенных правок уже опубликованного сайта НАПОМНИ юзеру нажать «Опубликовать» ещё раз — иначе на сайте останется старая версия.
|
||||||
- **Не дёргай Gitea API типа `/repos/.../pages`, `/settings/pages`, `/deploy_keys`** — их нет
|
- **Не дёргай Gitea API типа `/repos/.../pages`, `/settings/pages`, `/deploy_keys`** — их нет
|
||||||
- **Не пытайся «настроить Pages через UI Gitea»** — Pages у нас работают только через push в ветку `pages`
|
- **Не пытайся «настроить Pages через UI Gitea»** — Pages у нас работают только через push в ветку `pages`
|
||||||
- **Публикация — только кнопкой «Опубликовать» в интерфейсе; API git-хостинга (Gitea/GitLab Pages) не вызывай** — публикацией управляет Pages-шлюз платформы, из проекта достаточно push в ветку `pages`
|
|
||||||
- Запуталось — сделай новый чистый проект через `./new-project NAME-v2`, перенеси туда работающий index.html
|
- Запуталось — сделай новый чистый проект через `./new-project NAME-v2`, перенеси туда работающий index.html
|
||||||
|
|
||||||
---
|
---
|
||||||
## ❌ Чего НЕ делать НИКОГДА
|
## ❌ Чего НЕ делать НИКОГДА
|
||||||
|
|
||||||
- ❌ `git init` в workspace root
|
- ❌ `git init` в workspace root
|
||||||
- ❌ `npm install` руками в шелле и тяжёлые зависимости (mongoose/pg/prisma/next/nuxt/nest) — контейнер юзера 512 МБ
|
- ❌ `npm install` с прод-зависимостями (express/mongoose/pg/prisma/next/nuxt)
|
||||||
- ❌ Поднимать бэкенд там, где хватает статики (лендинг, визитка, презентация) — сверься с таблицей стека
|
- ❌ Поднимать бэкенд там, где хватает статики (лендинг, визитка, презентация) — сверься с таблицей стека
|
||||||
- ❌ Ставить тяжёлые фреймворки (nest / next / nuxt / django) — сервер на `node:http`, а для полноценного приложения (Рецепт A) — express
|
- ❌ Ставить прод-фреймворки (express / nest / next / django) — сервер только на `node:http`
|
||||||
- ❌ Использовать `gh` CLI или GitHub API
|
- ❌ Использовать `gh` CLI или GitHub API
|
||||||
- ❌ Вызывать Gitea Pages-API (его нет)
|
- ❌ Вызывать Gitea Pages-API (его нет)
|
||||||
- ❌ Долгое отлаживание Pages — почти всегда решение «push HEAD:pages»
|
- ❌ Долгое отлаживание Pages — почти всегда решение «push HEAD:pages»
|
||||||
@ -309,7 +304,7 @@ git push origin HEAD:pages
|
|||||||
---
|
---
|
||||||
## 🎨 design.md + дизайн-система KT AI
|
## 🎨 design.md + дизайн-система KT AI
|
||||||
|
|
||||||
Рядом лежит `design.md` — **прочитай его перед первой строкой вёрстки**. `design.md` — про сайты и лендинги. Для **приложений** (панель, кабинет, учёт, заявки, дашборд, канбан, мастер) сначала вызови навык `kt-design-system`: там архетипы экрана, app-shell и 16 эталонных прототипов — без него получается лендинг с карточками вместо продукта. В нём каркас `index.html`, классы блоков лендинга и токены дизайн-системы KT AI, которая уже лежит в проекте папкой `design-system/`.
|
Рядом лежит `design.md` — **прочитай его перед первой строкой вёрстки**. В нём каркас `index.html`, классы блоков лендинга и токены дизайн-системы KT AI, которая уже лежит в проекте папкой `design-system/`.
|
||||||
|
|
||||||
**Порядок для сайта/страницы — ровно такой:** 1) `read design.md`, 2) `read index.html` — стартовый каркас на ДС уже лежит в проекте, 3) `edit index.html` под задачу юзера. Отдельный `style.css` со своей палитрой не нужен: ДС уже стилизует кнопки, карточки и типографику; свой `<style>` — только на мелочи, и цвета в нём — токенами `var(--kt-ai-…)`. Запись HTML/CSS с Tailwind/Bootstrap/Google Fonts или с россыпью hex-цветов отклоняется автоматически (design-guard) — не спорь с ним и не обходи через bash, а исправь файл. Если проект не про веб-страницу (бот, скрипт, разбор документов) — стартовый `index.html` просто удали.
|
**Порядок для сайта/страницы — ровно такой:** 1) `read design.md`, 2) `read index.html` — стартовый каркас на ДС уже лежит в проекте, 3) `edit index.html` под задачу юзера. Отдельный `style.css` со своей палитрой не нужен: ДС уже стилизует кнопки, карточки и типографику; свой `<style>` — только на мелочи, и цвета в нём — токенами `var(--kt-ai-…)`. Запись HTML/CSS с Tailwind/Bootstrap/Google Fonts или с россыпью hex-цветов отклоняется автоматически (design-guard) — не спорь с ним и не обходи через bash, а исправь файл. Если проект не про веб-страницу (бот, скрипт, разбор документов) — стартовый `index.html` просто удали.
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
<!-- vibe42-design-version: v5-ktds-apps-2026-09-15 -->
|
<!-- vibe42-design-version: v4-ds-scaffold-2026-09-11 -->
|
||||||
# Дизайн-система KT AI — по ней собирается весь UI
|
# Дизайн-система KT AI — по ней собирается весь UI
|
||||||
|
|
||||||
В папке проекта лежит `design-system/` — это **дизайн-система KT AI**
|
В папке проекта лежит `design-system/` — это **дизайн-система KT AI**
|
||||||
@ -152,10 +152,6 @@ cp -r /srv/opencode/design-system/kt-ai design-system
|
|||||||
|
|
||||||
## Не лендинг, а приложение (дашборд, таблица, очередь заявок, чат)
|
## Не лендинг, а приложение (дашборд, таблица, очередь заявок, чат)
|
||||||
|
|
||||||
> Для приложений есть отдельный слой дизайн-системы: архетипы экрана, app-shell, KPI, таблицы, drawer и 16 эталонных прототипов.
|
|
||||||
> Вызови навык `skill({ name: "kt-design-system" })` — он важнее этого раздела.
|
|
||||||
|
|
||||||
|
|
||||||
Тогда экран собирается **не вёрсткой руками, а конфигом**:
|
Тогда экран собирается **не вёрсткой руками, а конфигом**:
|
||||||
|
|
||||||
1. Возьми `design-system/templates/kt-ai-app-shell.html` — самодостаточный HTML-каркас приложения
|
1. Возьми `design-system/templates/kt-ai-app-shell.html` — самодостаточный HTML-каркас приложения
|
||||||
|
|||||||
163
index.html
163
index.html
@ -41,7 +41,30 @@
|
|||||||
color: var(--kt-ai-fg-muted);
|
color: var(--kt-ai-fg-muted);
|
||||||
margin-top: var(--kt-ai-space-2);
|
margin-top: var(--kt-ai-space-2);
|
||||||
}
|
}
|
||||||
|
.registrations-list {
|
||||||
|
margin-top: var(--kt-ai-space-8);
|
||||||
|
}
|
||||||
|
.reg-item {
|
||||||
|
background: var(--kt-ai-card-bg);
|
||||||
|
border: 1px solid var(--kt-ai-card-border);
|
||||||
|
border-radius: var(--kt-ai-radius-xl);
|
||||||
|
padding: var(--kt-ai-space-5);
|
||||||
|
margin-bottom: var(--kt-ai-space-4);
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr auto;
|
||||||
|
gap: var(--kt-ai-space-3);
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.reg-name {
|
||||||
|
font-weight: var(--kt-ai-font-semibold);
|
||||||
|
}
|
||||||
|
.reg-meta {
|
||||||
|
color: var(--kt-ai-fg-muted);
|
||||||
|
font-size: var(--kt-ai-text-sm);
|
||||||
|
}
|
||||||
|
.track-badge {
|
||||||
|
justify-self: end;
|
||||||
|
}
|
||||||
.success-message {
|
.success-message {
|
||||||
background: var(--kt-ai-bg-soft);
|
background: var(--kt-ai-bg-soft);
|
||||||
border: 2px solid var(--kt-ai-primary);
|
border: 2px solid var(--kt-ai-primary);
|
||||||
@ -50,34 +73,17 @@
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
margin-top: var(--kt-ai-space-8);
|
margin-top: var(--kt-ai-space-8);
|
||||||
}
|
}
|
||||||
@media (max-width: 768px) {
|
.empty-state {
|
||||||
.kt-ai-wrap {
|
text-align: center;
|
||||||
padding-left: var(--kt-ai-space-4);
|
padding: var(--kt-ai-space-12);
|
||||||
padding-right: var(--kt-ai-space-4);
|
color: var(--kt-ai-fg-muted);
|
||||||
}
|
}
|
||||||
.hero-gradient {
|
.filter-bar {
|
||||||
padding: var(--kt-ai-space-6);
|
display: flex;
|
||||||
}
|
gap: var(--kt-ai-space-4);
|
||||||
.stats-grid {
|
margin-bottom: var(--kt-ai-space-6);
|
||||||
grid-template-columns: repeat(2, 1fr);
|
flex-wrap: wrap;
|
||||||
gap: var(--kt-ai-space-4);
|
align-items: center;
|
||||||
}
|
|
||||||
.kt-ai-grid {
|
|
||||||
grid-template-columns: 1fr !important;
|
|
||||||
}
|
|
||||||
.kt-ai-hero-actions {
|
|
||||||
flex-direction: column;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
.kt-ai-hero-actions .kt-ai-btn {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
.kt-ai-pagehead-in {
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
.kt-ai-pagehead-in .kt-ai-btn {
|
|
||||||
margin-left: var(--kt-ai-space-2);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
@ -305,7 +311,48 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<section id="participants" class="kt-ai-section" data-divider="true">
|
||||||
|
<div class="kt-ai-wrap" style="max-width:900px">
|
||||||
|
<div class="kt-ai-section-head">
|
||||||
|
<h2>Зарегистрированные участники</h2>
|
||||||
|
<p>Посмотри, кто уже присоединился к хакатону</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="filter-bar">
|
||||||
|
<select id="filter-track" class="kt-ai-select" data-size="sm">
|
||||||
|
<option value="all">Все направления</option>
|
||||||
|
<option value="AI / Machine Learning">AI / Machine Learning</option>
|
||||||
|
<option value="IoT & Smart Devices">IoT & Smart Devices</option>
|
||||||
|
<option value="Cybersecurity">Cybersecurity</option>
|
||||||
|
<option value="Big Data & Analytics">Big Data & Analytics</option>
|
||||||
|
<option value="Cloud & DevOps">Cloud & DevOps</option>
|
||||||
|
<option value="Telecom Solutions">Telecom Solutions</option>
|
||||||
|
</select>
|
||||||
|
<select id="filter-experience" class="kt-ai-select" data-size="sm">
|
||||||
|
<option value="all">Весь опыт</option>
|
||||||
|
<option value="Junior (до 1 года)">Junior</option>
|
||||||
|
<option value="Middle (1-3 года)">Middle</option>
|
||||||
|
<option value="Senior (3+ лет)">Senior</option>
|
||||||
|
<option value="Student">Студент</option>
|
||||||
|
</select>
|
||||||
|
<button id="clear-filters" class="kt-ai-btn" data-variant="ghost" data-size="sm">Сбросить фильтры</button>
|
||||||
|
<span class="kt-ai-spacer"></span>
|
||||||
|
<span id="count-display" class="kt-ai-chip">0 участников</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="registrations-list" class="registrations-list">
|
||||||
|
<!-- Сюда JavaScript вставит список -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="empty-state" class="empty-state" style="display:none">
|
||||||
|
<svg width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="var(--kt-ai-fg-faint)" stroke-width="1.5" style="margin-bottom:var(--kt-ai-space-4)">
|
||||||
|
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/>
|
||||||
|
</svg>
|
||||||
|
<p>Пока нет участников с такими фильтрами</p>
|
||||||
|
<p style="font-size:var(--kt-ai-text-sm);margin-top:var(--kt-ai-space-2)">Стань первым!</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<footer class="kt-ai-wrap kt-ai-pagefoot">
|
<footer class="kt-ai-wrap kt-ai-pagefoot">
|
||||||
<span>© 2026 Казахтелеком. КТ Опен Хакатон</span>
|
<span>© 2026 Казахтелеком. КТ Опен Хакатон</span>
|
||||||
@ -348,7 +395,46 @@ function saveRegistration(reg) {
|
|||||||
return regs;
|
return regs;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// === Рендеринг списка участников ===
|
||||||
|
function renderRegistrations(filters = { track: 'all', experience: 'all' }) {
|
||||||
|
const regs = getRegistrations();
|
||||||
|
const container = document.getElementById('registrations-list');
|
||||||
|
const emptyState = document.getElementById('empty-state');
|
||||||
|
const countDisplay = document.getElementById('count-display');
|
||||||
|
|
||||||
|
let filtered = regs.filter(r => {
|
||||||
|
const trackMatch = filters.track === 'all' || r.track === filters.track;
|
||||||
|
const expMatch = filters.experience === 'all' || r.experience === filters.experience;
|
||||||
|
return trackMatch && expMatch;
|
||||||
|
});
|
||||||
|
|
||||||
|
countDisplay.textContent = filtered.length + (filtered.length === 1 ? ' участник' : ' участников');
|
||||||
|
|
||||||
|
if (filtered.length === 0) {
|
||||||
|
container.style.display = 'none';
|
||||||
|
emptyState.style.display = 'block';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
container.style.display = 'block';
|
||||||
|
emptyState.style.display = 'none';
|
||||||
|
|
||||||
|
container.innerHTML = filtered.map(r => `
|
||||||
|
<div class="reg-item">
|
||||||
|
<div>
|
||||||
|
<div class="reg-name">${escapeHtml(r.fullname)}</div>
|
||||||
|
<div class="reg-meta">
|
||||||
|
${r.team ? 'Команда: ' + escapeHtml(r.team) + ' • ' : ''}
|
||||||
|
${r.experience || 'Опыт не указан'}
|
||||||
|
</div>
|
||||||
|
<div class="reg-meta" style="margin-top:var(--kt-ai-space-1)">
|
||||||
|
Зарегистрирован: ${formatDate(r.registeredAt)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span class="kt-ai-chip track-badge">${escapeHtml(r.track)}</span>
|
||||||
|
</div>
|
||||||
|
`).join('');
|
||||||
|
}
|
||||||
|
|
||||||
// === Обновление статистики ===
|
// === Обновление статистики ===
|
||||||
function updateStats() {
|
function updateStats() {
|
||||||
@ -417,10 +503,29 @@ document.getElementById('new-registration').addEventListener('click', function()
|
|||||||
document.getElementById('registration-form').scrollIntoView({ behavior: 'smooth' });
|
document.getElementById('registration-form').scrollIntoView({ behavior: 'smooth' });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
document.getElementById('filter-track').addEventListener('change', function() {
|
||||||
|
renderRegistrations({
|
||||||
|
track: this.value,
|
||||||
|
experience: document.getElementById('filter-experience').value
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('filter-experience').addEventListener('change', function() {
|
||||||
|
renderRegistrations({
|
||||||
|
track: document.getElementById('filter-track').value,
|
||||||
|
experience: this.value
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('clear-filters').addEventListener('click', function() {
|
||||||
|
document.getElementById('filter-track').value = 'all';
|
||||||
|
document.getElementById('filter-experience').value = 'all';
|
||||||
|
renderRegistrations({ track: 'all', experience: 'all' });
|
||||||
|
});
|
||||||
|
|
||||||
// === Запуск при загрузке ===
|
// === Запуск при загрузке ===
|
||||||
updateStats();
|
updateStats();
|
||||||
|
renderRegistrations();
|
||||||
|
|
||||||
// Плавный скролл для якорных ссылок
|
// Плавный скролл для якорных ссылок
|
||||||
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
|
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user