# Правила работы с Challenge Platform Этот документ содержит важные правила и паттерны для работы с кодовой базой проекта Challenge Platform. ## 🎯 Chakra UI - Специфика проекта ### TypeScript и Chakra UI Props Проект использует Chakra UI v2/v3 с особенностями типизации. **ВАЖНО:** #### ✅ Правильное подавление ошибок TypeScript: ```tsx // ❌ НЕПРАВИЛЬНО - комментарий на отдельной строке НЕ работает: {/* @ts-expect-error Chakra UI v2 uses isDisabled */} // ✅ ПРАВИЛЬНО - комментарий перед элементом + все props на одной строке: {/* @ts-expect-error Chakra UI v2 uses isDisabled */} // ✅ ПРАВИЛЬНО - inline комментарий после prop: ``` #### Частые props требующие подавления: - `isDisabled` (Button, Input) - `isLoading` (Button) - `isInvalid` (Input) - `spacing` (VStack, HStack) ### VStack и HStack ```tsx // ✅ ПРАВИЛЬНО: {/* @ts-expect-error Chakra UI v2 uses spacing */} {/* content */} ``` ## 💾 LocalStorage - Ключи и паттерны ### Константы для ключей **ВСЕГДА** используйте константы для ключей localStorage: ```tsx // ✅ ПРАВИЛЬНО: const WORKPLACE_NUMBER_KEY = 'challengeWorkplaceNumber' const USER_ID_KEY = 'challengeUserId' const USER_NICKNAME_KEY = 'challengeNickname' const SELECTED_CHAIN_KEY = 'challengeSelectedChainId' const SELECTED_TASK_KEY = 'challengeSelectedTaskId' // Префикс 'challenge' для всех ключей проекта ``` ### Синхронизация localStorage и Context Если добавляете новое поле в Context, которое должно сохраняться: 1. **Добавьте константу** для ключа 2. **Инициализируйте state** из localStorage: ```tsx const [field, setField] = useState(() => isBrowser() ? window.localStorage.getItem(FIELD_KEY) : null, ) ``` 3. **Сохраняйте в localStorage** при изменении (login) 4. **Очищайте из localStorage** при выходе (logout) 5. **Добавьте в интерфейс** ChallengeContextValue 6. **Добавьте в useMemo** (value и dependencies) ## 🔄 Context - Паттерн обновления ### Добавление нового поля в ChallengeContext Пример: добавление `workplaceNumber` **Шаг 1:** Интерфейс ```tsx interface ChallengeContextValue { // ... существующие поля workplaceNumber: string | null // ← добавить } ``` **Шаг 2:** Константа ```tsx const WORKPLACE_NUMBER_KEY = 'challengeWorkplaceNumber' ``` **Шаг 3:** State с инициализацией ```tsx const [workplaceNumber, setWorkplaceNumber] = useState(() => isBrowser() ? window.localStorage.getItem(WORKPLACE_NUMBER_KEY) : null, ) ``` **Шаг 4:** Обновление login ```tsx const login = useCallback( async (nicknameValue: string, workplaceNumberValue: string) => { // ... существующий код setWorkplaceNumber(workplaceNumberValue) if (isBrowser()) { // ... существующие setItem window.localStorage.setItem(WORKPLACE_NUMBER_KEY, workplaceNumberValue) } }, [authUser, refreshStatsById], ) ``` **Шаг 5:** Обновление logout ```tsx const logout = useCallback(() => { // ... существующие setNull setWorkplaceNumber(null) if (isBrowser()) { // ... существующие removeItem window.localStorage.removeItem(WORKPLACE_NUMBER_KEY) } }, []) ``` **Шаг 6:** Добавить в value ```tsx const value = useMemo( () => ({ // ... существующие поля workplaceNumber, // ← добавить }), [ // ... существующие зависимости workplaceNumber, // ← добавить в dependencies ], ) ``` ## 📝 Форматирование кода ### Отступы - **2 пробела** для отступов - Никаких табов - JSX атрибуты выравниваются по отступам родителя + 2 пробела ### Комментарии на русском ```tsx // ✅ ПРАВИЛЬНО: // Проверяем, есть ли сохранённый номер рабочего места useEffect(() => { const savedWorkplace = localStorage.getItem(WORKPLACE_NUMBER_KEY) // ... }, []) ``` ## 🔐 Паттерн двухшаговой формы Если нужна форма с несколькими шагами: ```tsx export const MultiStepForm = () => { const [step, setStep] = useState<'step1' | 'step2'>('step1') // Проверка сохранённых данных useEffect(() => { const savedData = localStorage.getItem(SAVED_DATA_KEY) if (savedData) { setData(savedData) setStep('step2') // Пропускаем первый шаг } }, []) const handleStep1Submit = (e: React.FormEvent) => { e.preventDefault() // Сохраняем СРАЗУ после валидации localStorage.setItem(SAVED_DATA_KEY, trimmedData) setStep('step2') } // Условный рендеринг if (step === 'step1') { return } return } ``` ### ⚠️ Важные моменты: 1. Сохраняйте данные **сразу** после валидации, не ждите финального submit 2. Проверяйте localStorage при монтировании компонента 3. Пропускайте шаги, если данные уже есть 4. Давайте возможность вернуться и изменить данные ## 🎨 Header - Паттерн отображения информации Порядок отображения в Header: ``` Место №{number} • {nickname} • {taskProgress} ``` Важные элементы отмечаем `fontWeight="medium"`: ```tsx Место №{workplaceNumber} ``` ## 🐛 Частые ошибки и решения ### ❌ Ошибка: "Property 'spacing' does not exist" **Решение:** Добавить `@ts-expect-error` комментарий перед VStack/HStack ### ❌ Ошибка: "Unused '@ts-expect-error' directive" **Причина:** Комментарий расположен неправильно **Решение:** Поместить комментарий **непосредственно** перед элементом с ошибкой, и все props элемента должны быть на одной строке или с inline комментариями ### ❌ Ошибка: Данные не сохраняются между сессиями **Проверить:** 1. Используется ли `localStorage.setItem`? 2. Инициализируется ли state из localStorage? 3. Очищается ли в logout? 4. Синхронизированы ли ключи (одинаковые константы)? ### ❌ Ошибка: Context не обновляется **Проверить:** 1. Добавлено ли новое поле в `useMemo` value? 2. Добавлено ли новое поле в dependencies массив `useMemo`? 3. Добавлено ли в интерфейс `ChallengeContextValue`? ## 📚 Структура проекта ``` src/ ├── components/ # Компоненты │ ├── LoginForm.tsx # Форма входа (может быть многошаговой) │ ├── Header.tsx # Шапка с информацией о пользователе │ └── ... ├── context/ # React Context │ └── ChallengeContext.tsx # Главный контекст приложения ├── pages/ # Страницы │ └── main/ └── ... ``` ## ✅ Чеклист перед коммитом - [ ] Нет linter errors (проверить `read_lints`) - [ ] TypeScript ошибки подавлены правильно - [ ] LocalStorage ключи используют константы - [ ] Новые поля Context добавлены во все нужные места - [ ] Комментарии на русском языке - [ ] Отступы - 2 пробела - [ ] Данные сохраняются и восстанавливаются из localStorage ## 🎯 Динамическая очередь проверки с прогресс-баром ### Симуляция очереди в API (stubs/api/index.js) Для лучшего UX очередь проверки показывает прогресс через прогресс-бар. **Паттерн реализации:** ```javascript // Таймауты: 100ms для быстрой работы в dev const timer = (time = 100) => (req, res, next) => setTimeout(next, time) // Храним состояние для каждого queueId const queueStates = {} router.get('/challenge/check-status/:queueId', (req, res) => { const queueId = req.params.queueId // Инициализируем с позицией 3 if (!queueStates[queueId]) { queueStates[queueId] = { position: 3, initialPosition: 3, // Для расчёта прогресса pollCount: 0 } } const state = queueStates[queueId] state.pollCount++ // Каждый запрос уменьшаем позицию (быстро) if (state.pollCount >= 1 && state.position > 0) { state.position-- state.pollCount = 0 } // Когда позиция 0 → возвращаем результат проверки if (state.position === 0 && state.pollCount >= 1) { delete queueStates[queueId] // Очищаем return res.json({ status: 'completed', submission: {...} }) } // Возвращаем текущую позицию + начальную для прогресса return res.json({ status: state.position > 0 ? 'waiting' : 'in_progress', position: state.position, initialPosition: state.initialPosition // ← Важно! }) }) ``` ### UI с прогресс-баром **Новый подход:** Вместо отображения номера в очереди показываем прогресс-бар. **Расчёт прогресса:** ```tsx const checkingProgress = (() => { if (!queueStatus) return 0 const initial = queueStatus.initialPosition || 3 const current = queueStatus.position || 0 if (queueStatus.status === 'in_progress') return 90 // Почти готово // От 0% до 80% по мере движения в очереди const progress = ((initial - current) / initial) * 80 return Math.max(10, progress) // Минимум 10% чтобы было видно })() ``` **Кастомный прогресс-бар с анимацией:** ```tsx {/* Анимированный блик */} ``` **Текст под прогресс-баром:** ```tsx {checkingProgress < 50 ? 'Ожидание в очереди' : checkingProgress < 90 ? 'Начинаем проверку' : 'Почти готово'} ``` ### ⚠️ Важно - **Таймауты в dev:** 100ms для быстрой работы. На prod бэкенд будет быстрее - **Прогресс:** 0-80% = движение в очереди, 80-90% = начало проверки, 90%+ = финализация - **Не показываем номер** в очереди - только прогресс-бар - **Текст:** простой "Проверяем решение..." вместо деталей - **Анимация shimmer:** добавляет ощущение процесса - Полинг: интервал 2 секунды (`PollingManager`) - **КРИТИЧНО:** При смене задания сбрасывайте все локальные состояния ### Сброс состояния при смене задания **ОБЯЗАТЕЛЬНО** добавляйте useEffect для сброса локальных состояний: ```tsx // Сбрасываем состояние при смене задания useEffect(() => { setLastResult(null) setPrevPosition(null) setPositionChanged(false) // ... любые другие локальные состояния }, [task.id]) // ← зависимость от task.id ``` **Почему это важно:** - Без сброса плашки успеха/ошибки от предыдущего задания остаются видимыми - Пользователь видит некорректную информацию - Состояние очереди "протекает" между заданиями ## 🎓 Философия проекта 1. **Удобство пользователя превыше всего** - сохраняем данные, не переспрашиваем 2. **Типобезопасность** - используем TypeScript, подавляем ошибки осознанно 3. **Консистентность** - следуем установленным паттернам 4. **Персистентность** - важные данные (номер места, выбор цепочки) сохраняются в localStorage 5. **Обратная связь** - пользователь всегда видит что происходит (позиция в очереди, прогресс проверки) --- **Версия:** 1.1 **Дата:** 13.12.2025 **Автор:** Claude (на основе опыта работы с проектом)