Files
challenge-pl/claude.md

421 lines
15 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Правила работы с Challenge Platform
Этот документ содержит важные правила и паттерны для работы с кодовой базой проекта Challenge Platform.
## 🎯 Chakra UI - Специфика проекта
### TypeScript и Chakra UI Props
Проект использует Chakra UI v2/v3 с особенностями типизации. **ВАЖНО:**
#### ✅ Правильное подавление ошибок TypeScript:
```tsx
// ❌ НЕПРАВИЛЬНО - комментарий на отдельной строке НЕ работает:
{/* @ts-expect-error Chakra UI v2 uses isDisabled */}
<Button
isDisabled={someCondition}
>
Кнопка
</Button>
// ✅ ПРАВИЛЬНО - комментарий перед элементом + все props на одной строке:
{/* @ts-expect-error Chakra UI v2 uses isDisabled */}
<Button isDisabled={someCondition}>Кнопка</Button>
// ✅ ПРАВИЛЬНО - inline комментарий после prop:
<Input
value={value}
// @ts-expect-error Chakra UI v2 uses isInvalid
isInvalid={!!error}
/>
```
#### Частые props требующие подавления:
- `isDisabled` (Button, Input)
- `isLoading` (Button)
- `isInvalid` (Input)
- `spacing` (VStack, HStack)
### VStack и HStack
```tsx
// ✅ ПРАВИЛЬНО:
{/* @ts-expect-error Chakra UI v2 uses spacing */}
<VStack spacing={4} align="stretch">
{/* content */}
</VStack>
```
## 💾 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<string | null>(() =>
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<string | null>(() =>
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<ChallengeContextValue>(
() => ({
// ... существующие поля
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 <Step1Form />
}
return <Step2Form />
}
```
### ⚠️ Важные моменты:
1. Сохраняйте данные **сразу** после валидации, не ждите финального submit
2. Проверяйте localStorage при монтировании компонента
3. Пропускайте шаги, если данные уже есть
4. Давайте возможность вернуться и изменить данные
## 🎨 Header - Паттерн отображения информации
Порядок отображения в Header:
```
Место №{number} • {nickname} • {taskProgress}
```
Важные элементы отмечаем `fontWeight="medium"`:
```tsx
<Text fontSize="sm" color="gray.600" fontWeight="medium">
Место №{workplaceNumber}
</Text>
```
## 🐛 Частые ошибки и решения
### ❌ Ошибка: "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
<Box bg="blue.100" borderRadius="md" h="24px" overflow="hidden">
<Box
bg="blue.500"
h="full"
w={`${checkingProgress}%`}
transition="width 0.5s ease"
position="relative"
>
{/* Анимированный блик */}
<Box
position="absolute"
bgGradient="linear(to-r, transparent, blue.400, transparent)"
animation="shimmer 2s infinite"
css={{
'@keyframes shimmer': {
'0%': { transform: 'translateX(-100%)' },
'100%': { transform: 'translateX(100%)' }
}
}}
/>
</Box>
</Box>
```
**Текст под прогресс-баром:**
```tsx
<Text fontSize="xs" color="blue.600" textAlign="center" mt={2}>
{checkingProgress < 50 ? 'Ожидание в очереди' :
checkingProgress < 90 ? 'Начинаем проверку' :
'Почти готово'}
</Text>
```
### ⚠️ Важно
- **Таймауты в 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 (на основе опыта работы с проектом)