# Правила работы с 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 (на основе опыта работы с проектом)