javascript

Мы запихнули LifeOS в Telegram Mini App: как сделать сложный UI на React + Framer Motion и не сойти…

  • среда, 4 февраля 2026 г. в 00:00:07
https://habr.com/ru/articles/992100/

Кто за этим стоит?

Нас двое, и этот проект — результат столкновения двух разных подходов к разработке:

  • Дмитрий Спирихин (Я) — System Architecture & Full-stack Development. Я притащил в проект логику из Unity, заставил всё работать на Rx JS и выстроил архитектуру так, чтобы она не рассыпалась под весом десятка модулей.

  • Демиан Авольстийный — Product Vision, Design & Vibe Coding. Демиан отвечал за тот самый «вайб», премиальный дизайн в стиле Apple и тактильность. Его задача была сделать так, чтобы приложением хотелось пользоваться не потому что «надо», а потому что это приятно.

В этой статье мы разберем, как геймдев-архитектура на RxJS помогла нам выстроить сложную систему состояний, почему Framer Motion — это спасение для тактильности, и как мы подружили ИИ с данными пользователя, чтобы он выдавал не «советы из интернета», а жесткие диагнозы на основе графиков.

Проблема «еще одного трекера»

На рынке тысяча трекеров привычек. Еще тысяча трекеров зала. И еще сотня приложений для медитаций. Проблема одна: они разрозненны. Данные о том, что я плохо спал, никак не влияют на план тренировок в другом приложении. А медитация живет в вакууме от уровня стресса.

Мы хотели создать LifeOS — единую систему управления собой, которая живет там, где я провожу 90% времени. В Telegram. Никаких впнов, ноушенов и тому подобное не надо.

Но была проблема: стандартные Telegram WebApps часто выглядят... как веб-сайты из 2010-го. Мне нужен был «вайб» Apple: премиальный, темный, с глубокими тенями и, главное, тактильный.

Так появился UltyMyLife. Вот как мы строили архитектуру дисциплины на React.

Интересный факт: Перед тем как релизнуть проект, мы поняли, что нужен полный редизайн, и в итоге потратили еще пару недель

Было

Стало


В статье разберём: 1) интерфейс, 2) хранение, 3) синхронизацию, 4) архитектуру на Rx JS, 5) ИИ, 6) визуал, 7) геймификацию 8) немного о бэкенде

1. Интерфейс: Тактильность против «Тапа»

Главная боль веб-приложений внутри «Телеги» — они ощущаются плоскими. Нет веса. Чтобы это исправить, мы сделали ставку на Framer Motion и сложные взаимодействия.

Привычки, которые хочется трогать

Обычный чекбокс — это скучно. Мы сделали карточки, которые нужно физически «свайпать». Это мелочь, но она дает дофаминовый отклик.

Вместо скучного списка дат в UltyMyLife мы используем календарь, который работает по принципу GitHub contribution graph.

Технический нюанс: Каждая ячейка динамически вычисляет процент выполнения привычек за день. Это превращает календарь в тепловую карту твоего прогресса.

// Логика расчета "прогресса" дня в HabitCalendar.js
const dayKey = formatDateKey(new Date(cellYear, cellMonth, day));
let percentNum = 0;

if (Object.keys(AppData.habitsByDate).includes(dayKey)) {
    const allHabitsOfCurrentDay = Array.from(Object.values(AppData.habitsByDate[dayKey]));
    // Вычисляем процент выполненных (v > 0) привычек
    percentNum = allHabitsOfCurrentDay.length > 0 
        ? Math.round((allHabitsOfCurrentDay.filter((v) => v > 0).length / allHabitsOfCurrentDay.length) * 100) 
        : 0;
}

// Рендерим ячейку с динамическим цветом
<div style={{
    ...styles(theme).cell,
    backgroundColor: getProgressColor(percentNum, theme), // Цвет зависит от %
    border: today === day ? `2px solid ${Colors.get('currentDateBorder', theme)}` : 'transparent',
}}>
    {day}
</div>

Для плавности переходов между месяцами мы использовали AnimatePresence. Это позволяет «пролистывать» календарь, сохраняя ощущение нативного iOS/Android приложения.

ScrollPicker: победа над системными инпутами

Стандартные <select> или <input type="time"> в WebApp выглядят инородно. На iOS вылетает системный барабан, на Android — модалка. Это мгновенно рушит иммерсивность.

Мы написали свой ScrollPicker. Главные вызовы здесь:

  1. Scroll Snapping: Чтобы пункты «прилипали» к центру.

  2. Initial Mount: Как мгновенно проскроллить к нужному значению при открытии, не пугая пользователя прыгающей анимацией.

// Хитрость с моментальным скроллом при монтировании
useEffect(() => {
    if (scrollRef.current) {
        const selectedIndex = items.findIndex(item => item === value);
        if (selectedIndex !== -1) {
            // Сначала прыгаем мгновенно
            scrollRef.current.scrollTop = selectedIndex * ITEM_HEIGHT;
        }
        // И только после этого включаем smooth scrolling для пользователя
        requestAnimationFrame(() => {
            setIsLoaded(true); 
        });
    }
}, []);

Использование scrollSnapType: 'y mandatory' в CSS позволяет добиться того самого эффекта «барабана» минимальными усилиями, а динамический расчет scale и opacity в зависимости от isSelected создает эффект перспективы.

2. Данные: Почему LocalStorage — это ловушка

Многие начинающие разработчики Mini Apps наступают на одни и те же грабли: используют localStorage. В Telegram Mini App ваш сайт может быть открыт в системном WebView, который при очистке кэша или нехватке памяти может «забыть» всё, что вы там сохранили.

Для UltyMyLife мы выбрали IndexedDB (через легкую обертку idb). Это позволяет хранить мегабайты данных: историю тренировок за годы, кастомные иконки и логи сна.

Как мы готовим данные (Serialization)

Поскольку мы используем классы для бизнес-логики, нам нужно превращать их в плоский JSON для сохранения и восстанавливать обратно. Мы используем class-transformer. Это позволяет не просто грузить JSON, а сразу получать объекты с методами.

// Процесс "упаковки" данных перед сохранением
export function serializeData() {
  try {
    const newData = new Data(); 
    // Превращаем сложные классы в простые объекты
    const plainData = instanceToPlain(newData); 
    return JSON.stringify(plainData);
  } catch (error) {
    console.error('Serialization failed:', error);
    return null;
  }
}

Структура Training Log

Главный вызов — как хранить тренировки, чтобы быстро считать тоннаж (общий поднятый вес) и строить графики. Мы используем объект, где ключи — это даты в формате YYYY-MM-DD. Это дает доступ к тренировке за любой день за O(1).

{
  "2026-01-30": [
    {
      "programId": 0,
      "startTime": 1702213815432,
      "tonnage": 4500, // Считаем сразу при закрытии подхода
      "exercises": {
        "0": { // ID упражнения "Жим лежа"
          "sets": [
            { "type": 1, "reps": 10, "weight": 60, "time": 60000 },
            { "type": 1, "reps": 10, "weight": 60, "time": 60000 }
          ],
          "totalTonnage": 1200
        }
      }
    }
  ]
}

3. Синхронизация: Почему мы отказались от Telegram CloudStorage

Изначально мы планировали использовать window.Telegram.WebApp.CloudStorage. Но быстро столкнулись с реальностью:

  1. Лимиты: Облако Telegram не резиновое, а JSON с логами тренировок за год может весить прилично.

  2. Скорость и надежность: Нам нужен был полный контроль над процессом.

Решение: Компрессия через Pako и кастомный бэкенд

Чтобы бэкапы летали даже на медленном интернете, мы внедрили сжатие на клиенте. Мы используем библиотеку pako (zlib) для дефлейта строки и кодируем результат в Base64.

Результат: JSON на 100кб превращается в крошечную строку, которая улетает в облако мгновенно.

Cредний коэффициент сжатия c pako (zlib) около 70-80%. Это позволяет отправлять бэкапы даже при (Edge) где-нибудь в лесу.

Base64 для передачи через API Telegram без битых символов.

// Процесс сжатия данных перед отправкой
const dataString = JSON.stringify(dataToSave);

// Сжатие: превращаем строку в сжатый Uint8Array
const compressedData = pako.deflate(dataString);

// 2. Кодируем в Base64 для безопасной передачи
const base64Data = btoa(String.fromCharCode(...compressedData));

// Отправляем через наш NotificationsManager
const response = await NotificationsManager.sendMessage('backup', base64Data);

«Оборонительное» программирование: Логика восстановления

Самое сложное в бэкапах — не сохранить их, а восстановить. За время разработки структура данных менялась несколько раз. Наш cloudRestore — это швейцарский нож, который умеет:

  • Определять, сжаты ли данные или это «старый» JSON.

  • Распаковывать вложенные серверные ответы.

  • Чистить Base64 от лишних пробелов (привет, баги Safari)

// Фрагмент "умного" восстановления (Defensive Loading)
try {
    const cleanBase64 = String(rawData).replace(/\s/g, ''); // Фикс InvalidCharacterError
    
    // Пытаемся разжать данные
    const binaryData = Uint8Array.from(atob(cleanBase64), c => c.charCodeAt(0));
    finalDataToLoad = pako.inflate(binaryData, { to: 'string' });
} catch (e) {
    console.warn("Сжатие не удалось. Пробуем загрузить сырые данные как фоллбек.");
    finalDataToLoad = typeof rawData === 'object' ? JSON.stringify(rawData) : rawData;
}

Этот подход гарантирует, что пользователь не потеряет свои данные при обновлении приложения.

Чтобы не превращать бэкенд в «черную дыру», мы используем PostgreSQL только для хранения сжатых состояний LifeOS. Мы не храним каждое действие отдельно — база видит только ID пользователя и его последний зашифрованный бэкап в виде BYTEA. Это упрощает масштабирование: нам не нужно делать сложные SQL-запросы к содержимому тренировок, мы просто отдаем «слепок» клиенту.

4. Реактивный «мозг» приложения: RxJS вместо Redux

Когда ваше приложение — это не просто три экрана, а сложная система (LifeOS) с кучей взаимосвязанных панелей, обычного useState становится мало. Мы внедрили RxJS, чтобы создать единую «шину событий» (Event Bus).

Почему RxJS?

В Mini App много событий, которые должны срабатывать «сквозь» компоненты:

  1. Вы выполнили привычку — нужно обновить прогресс в трех разных местах.

  2. Вы переключили страницу — нужно синхронно сменить набор кнопок в нижнем меню.

  3. Пришел ответ от сервера — нужно показать PopUpPanel, где бы пользователь ни находился.

Почему не Zustand?

Как Unity-разработчик, я привык к UniRx. Когда проект начал разрастаться, я понял, что мне не нужно просто «хранилище» — мне нужна «нервная система». RxJS позволяет обрабатывать взаимодействие пользователя как поток событий. Это дает тот самый игровой «game loop» эффект, когда интерфейс реагирует на изменения мгновенно и предсказуемо.

Шина событий и состояний (HabitsBus.js)

Мы используем BehaviorSubject для хранения состояний и Subject для мгновенных событий.

// Пример нашей шины состояний
export const theme$ = new BehaviorSubject('dark');
export const setPage$ = new BehaviorSubject('LoadPanel');
export const showPopUpPanel$ = new BehaviorSubject({show:false, header:'', isPositive:true});

// Функция-швейцарский нож для навигации
export const setPage = (page) => {
  lastPage$.next(setPage$.value);
  setPage$.next(page);
  
  // Автоматическая смена контекста нижнего меню
  if(page.startsWith('Habit')) bottomBtnPanel$.next('BtnsHabits');
  else if(page.startsWith('Training')) bottomBtnPanel$.next('BtnsTraining');
  // ... и так далее
}

Как это работает в компонентах

React-компоненты просто подписываются на нужные потоки. Это избавляет нас от «проп-дриллинга» (прокидывания данных через 10 уровней).

// Внутри любого компонента
useEffect(() => {
  const sub = theme$.subscribe(setThemeState);
  return () => sub.unsubscribe();
}, []);

Главный киллер-фича: setShowPopUpPanel

Вместо того чтобы в каждом компоненте держать стейт для уведомлений, мы просто вызываем одну функцию из любой точки кода — даже из логики сохранения данных, которая вообще не является React-компонентом.

export function setShowPopUpPanel(text, duration, isPositive) {
  showPopUpPanel$.next({show:true, header:text, isPositive});
  // Автоматическое скрытие через поток
  setTimeout(() => showPopUpPanel$.next({show:false, header:'', isPositive}), duration);
}

5. Интеллектуальный коуч: Когда данные начинают говорить

Все трекеры делают одно и то же: показывают цифры. «Ты сделал 4 из 7 привычек». «Спал 6.2 часа». Это не инсайт — это отчёт бухгалтера.

Мы пошли дальше: научили приложение думать. Не просто «ты мало спал», а «твои приседания упали на 15% после ночей с <6 часами сна — завтра отмени тяжёлую ногу».

Проблема №1: Данные разбросаны по 12 разным модулям

У нас есть:

  • habitsByDate — статусы привычек

  • trainingLog — тоннаж, упражнения, длительность

  • sleepingLog — сон с настроением и заметками

  • mentalLog — ментальные тренировки

  • breathingLog, meditationLog, hardeningLog — всё остальное

Чтобы ИИ нашёл корреляции, нужно собрать всё в единый контекст. Но не просто склеить JSON — а структурировать так, чтобы языковая модель поняла причинно-следственные связи.

// ❌ Плохо: "2024-01-15: {sleep: 5.2h, squat: 100kg}"
// ✅ Отлично: "2024-01-15: Sleep=5.2h (Mood=2/5) → Squat max dropped 15kg vs prev session"

Мы строим промпт как медицинскую карту спортсмена:

SLEEP_AND_RECOVERY (last 7 days):
  2024-01-22: Sleep=4.8h, Mood=2/5, Note="Late work"
  2024-01-23: Sleep=7.1h, Mood=5/5, Note="Early bedtime"

GYM_PERFORMANCE (last 7 days):
  DATE: 2024-01-22 | Program: Legs | Duration: 42 min
    - Squat: MaxWeight=80kg, TotalReps=24, Vol=1920
  DATE: 2024-01-23 | Program: Upper | Duration: 38 min
    - Bench Press: MaxWeight=65kg, TotalReps=30, Vol=1950

Как мы ходим к LLM

Mini App никогда не общается с LLM напрямую. Клиент собирает данные, формирует текстовый «досье» и шлёт обычный HTTPS‑запрос на наш бэкенд. На сервере лежит тонкий слой над LLM‑API: он добавляет системный промпт, проставляет жёсткие правила выхода (структура, формат, лимиты длины), кэширует ответы и, при необходимости, может переключить модель или провайдера. Клиенту прилетает уже готовый текст инсайта, который мы просто рендерим в UI и сохраняем в локальный лог.

Проблема №2: Даты в UTC vs локальное время пользователя

Классическая ловушка: пользователь тренируется в 23:00, данные уходят как 2024-01-22T20:00:00Z, а в логах сна за 2024-01-22 — пусто. Потому что сон начался в 00:30 и записался как 2024-01-23.

Мы решили это на уровне формирования промпта:

const getLocalISODate = (dateObj) => {
  const offset = dateObj.getTimezoneOffset() * 60000;
  const localDate = new Date(dateObj.getTime() - offset);
  return localDate.toISOString().split('T')[0]; // "2024-01-22"
};

Теперь все модули говорят на одном языке — локальном времени пользователя. Корреляции перестали ломаться.

Проблема №3: ИИ пишет «улучши сон» вместо конкретики

Первые версии выдавали водянистые советы: «Постарайтесь спать больше». Это бесполезно.

Мы вбили в промпт жёсткие правила форматирования:

НЕ пиши "улучши сон", пиши "сдвинь отбой на 20 минут раньше".
НЕ пиши "тренируйся жестче", пиши "добавь 1 подход в отстающем упражнении".

Результат:

  Проблема: 3 пропуска привычек по вторникам. Причина — поздний ужин после работы.
  Действия:
  1) Заведи будильник "ужин" на 19:00 (сейчас ешь в 21:30)
  2) Замени вечерний скролл на 5 мин дыхания (4-7-8)
  3) В понедельник вечером приготовь форму для вторника

Это уже не совет — это микро-задача, которую можно выполнить прямо сейчас.

Технический нюанс: Кэширование инсайтов

Запрос к ИИ — дорого и медленно. Мы внедрили простое кэширование на 5 минут:

const insightCache = {
  data: null,
  timestamp: 0,
  CACHE_DURATION: 5 * 60 * 1000
};

if (Date.now() - insightCache.timestamp < insightCache.CACHE_DURATION) {
  return insightCache.data; // Мгновенный ответ при повторном открытии экрана
}

Плюс фолбэк на клиентский анализ, если ИИ недоступен:

// Быстрый клиентский инсайт без сети
const hasSleepCorrelation = last7Days.some(date => {
  const sleep = sleeping[date]?.duration < 21600000; // <6 часов
  const weakTraining = trainings[date]?.some(s => s.totalTonnage < avgTonnage * 0.85);
  return sleep && weakTraining;
});

Итог: Данные → Диагноз → Действие

Мы перестали быть «ещё одним трекером». Теперь приложение:

  1. Собирает данные из 7+ модулей в единый контекст

  2. Находит корреляции (сон → сила, стресс → пропуски)

  3. Выдаёт микро-задачи вместо абстрактных советов

Это и есть LifeOS — не сборник метрик, а система, которая понимает пользователя и говорит ему то, что нужно услышать. Жёстко. Конкретно. Без воды.

6. Glassmorphism: Как сделать «стекло» в вебе без разводов

Вы когда-нибудь замечали, почему 99% веб-приложений выглядят как плоские картонки? Потому что настоящий glassmorphism в браузере — это ад.

Вот что мы хотели:

  • Полупрозрачный фон с размытием

  • Свечение по краям

  • Градиенты внутри

  • Тень, которая не сливается с фоном

Вот что получают большинство:

  • Белый прямоугольник

  • Размытие, которое лагает

  • Эффекты, которые не работают в мобильном браузере

Когда Демиан принес макеты с глубокими тенями и размытием, мой внутренний Unity-разработчик закричал "Draw calls!", но мой внутренний full-stack понял — придется извращаться с CSS, чтобы сохранить этот вайб

Проблема №1: backdrop-filter — ненадёжный костыль

CSS свойство backdrop-filter: blur() — это магия. И одновременно проклятие.

Что оно делает: Размывает всё, что находится позади элемента.

Почему это больно:

  • Не работает в Firefox без флага

  • В некоторых мобильных браузерах просто игнорируется

  • В Telegram WebView на некоторых устройствах даёт артефакты

Наше решение — двойной слой защиты:

const glassOverlay = (theme) => ({
    position: 'absolute',
    top: 0, left: 0, right: 0, bottom: 0,
    backgroundColor: Colors.get('bottomPanel', theme), // Полупрозрачный фон
    opacity: 0.85,                                      // Главный трюк
    backdropFilter: 'blur(15px)',                       // Размытие
    WebkitBackdropFilter: 'blur(15px)',                 // Safari fix
    border: `1px solid ${Colors.get('border', theme)}`, // Контур для глубины
    borderRadius: '25px',
    zIndex: -1,
});

Обратите внимание на opacity: 0.85. Это не для красоты — это фолбэк. Если backdrop-filter не сработает, пользователь всё равно увидит полупрозрачную панель вместо белого квадрата.

Проблема №2: Вложенность и производительность

Каждый backdrop-filter — это отдельный рендер-пасс. Чем больше таких элементов на экране, тем сильнее лагает анимация.

Мы применили glassmorphism только там, где он критичен:

  • Нижняя панель навигации

  • Модальные окна

  • Карты привычек

Всё остальное — простые градиенты и тени. Это сэкономило ~40% кадров в секунду на слабых устройствах.

Проблема №3: Темные и светлые темы

Glassmorphism в светлой теме — это кошмар. Размытие белого фона даёт серую кашу, которая сливается с интерфейсом.

Мы пошли на радикальное решение: в светлой теме эффект ослаблен.

// Colors.js
glassPanel: {
    dark: 'rgba(30, 30, 35, 0.85)',
    light: 'rgba(245, 245, 250, 0.92)', // Меньше прозрачности
},
shadow: {
    dark: 'rgba(0, 0, 0, 0.4)',
    light: 'rgba(0, 0, 0, 0.08)', // Почти невидимая тень
}

В светлой теме стекло становится почти непрозрачным, но сохраняет градиенты и контур. Это компромисс, но он работает.

Трюк №1: Градиент внутри стекла

Чистое размытие — скучно. Мы добавили внутренний градиент для глубины:

background: linear-gradient(
    180deg,
    rgba(255, 255, 255, 0.1) 0%,
    rgba(255, 255, 255, 0.03) 100%
);

В тёмной теме это создаёт эффект «подсветки сверху», как у настоящего стекла под светом.

Трюк №2: Контур с градиентом

Обычная рамка — это примитив. Мы используем box-shadow для имитации светодиодной подсветки:

boxShadow: `
    0 8px 20px ${Colors.get('shadow', theme)},      /* Основная тень */
    inset 0 0 15px rgba(255, 255, 255, 0.1)          /* Внутреннее свечение */
`

Это создаёт ощущение, что панель «парит» над интерфейсом.

Трюк №3: Анимация прозрачности

Когда модальное окно появляется, мы анимируем не только opacity, но и backdrop-filter:

<motion.div
    initial={{ opacity: 0, backdropFilter: 'blur(0px)' }}
    animate={{ opacity: 1, backdropFilter: 'blur(15px)' }}
    transition={{ duration: 0.3 }}
>

8. Бэкенд: Node.js, Express и PostgreSQL в роли «Слепого Хранителя»

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

Мы выбрали стек Node.js + Express в связке с PostgreSQL. Но мы используем их не совсем стандартно для веба.

Архитектура «Слепого Хранителя»

Приватность — наш главный приоритет. Мы не хотели, чтобы наш сервер «знал» о пользователе слишком много.

  • PostgreSQL как хранилище слепков: Вместо того чтобы создавать сотни таблиц под каждый чих пользователя (привычки, подходы, сны), мы храним данные в виде бинарных слепков (BYTEA).

  • Логика: Клиент сжимает данные через Pako, шифрует их и отправляет на сервер. PostgreSQL просто хранит последний актуальный «снимок» состояния.

  • Плюс: Это дает невероятную скорость синхронизации. Серверу не нужно парсить сложный JSON, он просто отдает байты, а клиентская часть на RxJS мгновенно разворачивает их в живое состояние приложения.

AI Proxy: Оркестрация моделей

Бэкенд также выступает в роли умного шлюза для нашего ИИ-коуча:

  • Маршрутизация: Сервер решает, отправить запрос на Groq (Llama 3) для мгновенного ответа или на GPT-4o-mini для глубокого анализа.

  • Безопасность ключей: Все API-ключи живут на сервере, клиент никогда не видит их напрямую.

  • Кэширование: Если пользователь запрашивает инсайт дважды за 5 минут, Express отдает результат из кэша, не тратя токены и время на повторный запрос к LLM.

Итог: Почему это того стоит

Glassmorphism — это не просто «красиво». Это тактильность:

  • Пользователь видит, что панель «над» контентом

  • Размытие создаёт ощущение глубины

  • Полупрозрачность связывает интерфейс с фоном

В плоском дизайне всё выглядит как на одном слое. В нашем приложении пользователь чувствует иерархию: что важно, что вторично, куда сейчас сфокусировано внимание.

И да, мы потратили три дня на отладку этого эффекта. Но когда пользователь впервые открывает приложение и говорит «вау, это выглядит как нативное» — оно того стоит.

7. Геймификация: Как превратить дисциплину в игру (без потери серьёзности)

Все говорят про геймификацию. Почти никто не делает её правильно.

Обычный подход:

  • «+10 очков за привычку»

  • «Уровень 5 из 100»

  • «Медалька за 7 дней подряд»

Это не геймификация. Это календарик с цифрами.

Мы построили систему, которая работает по законам игровой психологии — но без детских наград. Это не «Клуб Винкс», это система мотивации для взрослых.

Проблема №1: Опыт должен быть заслуженным

В большинстве приложений опыт даётся за всё подряд. Открыл приложение — +5 XP. Нажал кнопку — +3 XP. Через неделю пользователь 50-го уровня, но ничего не сделал.

Логика простая: Чем сложнее действие, тем больше опыта. Пользователь не может «накликать» уровень — нужно работать.

TotalXP = (T \cdot 50) + (M \cdot 30) + (S \cdot 20) + (H \cdot 10)

Где веса распределены по сложности (Тренировка — 50, Привычка — 10).

Уровни (Level Up) рассчитываются по экспоненте, как в RPG: каждый следующий уровень требует на 15% больше опыта.

Итог: Геймификация ≠ игра

Мы не добавили мультяшных персонажей и звуков «победы». Мы добавили:

  1. Систему опыта с весами действий

  2. Визуальную иерархию через цвета и ранги

  3. Соревнование через лидерборды с фильтрами

  4. Постоянный фидбек через анимации и значки

Это не игра. Это система мотивации, которая использует законы игровой психологии для серьёзных целей: дисциплины, здоровья, роста.

Пользователь не играет в «трекер». Он прокачивает себя — и видит этот прогресс каждый день.

Итог: Что получилось?

Мы создали не «бота в Telegram». Мы создали операционную систему для жизни, которая живёт там, где пользователь проводит 90% времени — в мессенджере. Без установки. Без разрешений. Без трения.

Ощущения: Как от дорогого iOS-приложения, но без установки из App Store.

Код проекта — это постоянная борьба за 60 FPS на анимациях и борьба с особенностями веб-вью (скролл, сейф-зоны). Но результат того стоит.

Если вам интересен код конкретных компонентов (например, того самого свайпа привычек или кастомного пикера) — пишите в комменты, разберем отдельно.

Что под капотом

Слой

Технология

Frontend

React + Framer Motion

State

RxJS (The "Unity" Way)

Storage

IndexedDB + Pako

Design

Apple-style Glassmorphism

AI

Groq + Llama 3

Backend

Node.js / Express

Database

PostgreSQL

Localization

Russian & English

Скриншоты

если интересно, потрогать результат можно тут.