15 KiB
Правила работы с Challenge Platform
Этот документ содержит важные правила и паттерны для работы с кодовой базой проекта Challenge Platform.
🎯 Chakra UI - Специфика проекта
TypeScript и Chakra UI Props
Проект использует Chakra UI v2/v3 с особенностями типизации. ВАЖНО:
✅ Правильное подавление ошибок TypeScript:
// ❌ НЕПРАВИЛЬНО - комментарий на отдельной строке НЕ работает:
{/* @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
// ✅ ПРАВИЛЬНО:
{/* @ts-expect-error Chakra UI v2 uses spacing */}
<VStack spacing={4} align="stretch">
{/* content */}
</VStack>
💾 LocalStorage - Ключи и паттерны
Константы для ключей
ВСЕГДА используйте константы для ключей localStorage:
// ✅ ПРАВИЛЬНО:
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, которое должно сохраняться:
- Добавьте константу для ключа
- Инициализируйте state из localStorage:
const [field, setField] = useState<string | null>(() => isBrowser() ? window.localStorage.getItem(FIELD_KEY) : null, ) - Сохраняйте в localStorage при изменении (login)
- Очищайте из localStorage при выходе (logout)
- Добавьте в интерфейс ChallengeContextValue
- Добавьте в useMemo (value и dependencies)
🔄 Context - Паттерн обновления
Добавление нового поля в ChallengeContext
Пример: добавление workplaceNumber
Шаг 1: Интерфейс
interface ChallengeContextValue {
// ... существующие поля
workplaceNumber: string | null // ← добавить
}
Шаг 2: Константа
const WORKPLACE_NUMBER_KEY = 'challengeWorkplaceNumber'
Шаг 3: State с инициализацией
const [workplaceNumber, setWorkplaceNumber] = useState<string | null>(() =>
isBrowser() ? window.localStorage.getItem(WORKPLACE_NUMBER_KEY) : null,
)
Шаг 4: Обновление login
const login = useCallback(
async (nicknameValue: string, workplaceNumberValue: string) => {
// ... существующий код
setWorkplaceNumber(workplaceNumberValue)
if (isBrowser()) {
// ... существующие setItem
window.localStorage.setItem(WORKPLACE_NUMBER_KEY, workplaceNumberValue)
}
},
[authUser, refreshStatsById],
)
Шаг 5: Обновление logout
const logout = useCallback(() => {
// ... существующие setNull
setWorkplaceNumber(null)
if (isBrowser()) {
// ... существующие removeItem
window.localStorage.removeItem(WORKPLACE_NUMBER_KEY)
}
}, [])
Шаг 6: Добавить в value
const value = useMemo<ChallengeContextValue>(
() => ({
// ... существующие поля
workplaceNumber, // ← добавить
}),
[
// ... существующие зависимости
workplaceNumber, // ← добавить в dependencies
],
)
📝 Форматирование кода
Отступы
- 2 пробела для отступов
- Никаких табов
- JSX атрибуты выравниваются по отступам родителя + 2 пробела
Комментарии на русском
// ✅ ПРАВИЛЬНО:
// Проверяем, есть ли сохранённый номер рабочего места
useEffect(() => {
const savedWorkplace = localStorage.getItem(WORKPLACE_NUMBER_KEY)
// ...
}, [])
🔐 Паттерн двухшаговой формы
Если нужна форма с несколькими шагами:
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 />
}
⚠️ Важные моменты:
- Сохраняйте данные сразу после валидации, не ждите финального submit
- Проверяйте localStorage при монтировании компонента
- Пропускайте шаги, если данные уже есть
- Давайте возможность вернуться и изменить данные
🎨 Header - Паттерн отображения информации
Порядок отображения в Header:
Место №{number} • {nickname} • {taskProgress}
Важные элементы отмечаем fontWeight="medium":
<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 комментариями
❌ Ошибка: Данные не сохраняются между сессиями
Проверить:
- Используется ли
localStorage.setItem? - Инициализируется ли state из localStorage?
- Очищается ли в logout?
- Синхронизированы ли ключи (одинаковые константы)?
❌ Ошибка: Context не обновляется
Проверить:
- Добавлено ли новое поле в
useMemovalue? - Добавлено ли новое поле в dependencies массив
useMemo? - Добавлено ли в интерфейс
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 очередь проверки показывает прогресс через прогресс-бар.
Паттерн реализации:
// Таймауты: 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 с прогресс-баром
Новый подход: Вместо отображения номера в очереди показываем прогресс-бар.
Расчёт прогресса:
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% чтобы было видно
})()
Кастомный прогресс-бар с анимацией:
<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>
Текст под прогресс-баром:
<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 для сброса локальных состояний:
// Сбрасываем состояние при смене задания
useEffect(() => {
setLastResult(null)
setPrevPosition(null)
setPositionChanged(false)
// ... любые другие локальные состояния
}, [task.id]) // ← зависимость от task.id
Почему это важно:
- Без сброса плашки успеха/ошибки от предыдущего задания остаются видимыми
- Пользователь видит некорректную информацию
- Состояние очереди "протекает" между заданиями
🎓 Философия проекта
- Удобство пользователя превыше всего - сохраняем данные, не переспрашиваем
- Типобезопасность - используем TypeScript, подавляем ошибки осознанно
- Консистентность - следуем установленным паттернам
- Персистентность - важные данные (номер места, выбор цепочки) сохраняются в localStorage
- Обратная связь - пользователь всегда видит что происходит (позиция в очереди, прогресс проверки)
Версия: 1.1
Дата: 13.12.2025
Автор: Claude (на основе опыта работы с проектом)