Files
challenge-pl/claude.md

15 KiB
Raw Blame History

Правила работы с 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, которое должно сохраняться:

  1. Добавьте константу для ключа
  2. Инициализируйте state из localStorage:
    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: Интерфейс

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 />
}

⚠️ Важные моменты:

  1. Сохраняйте данные сразу после валидации, не ждите финального submit
  2. Проверяйте localStorage при монтировании компонента
  3. Пропускайте шаги, если данные уже есть
  4. Давайте возможность вернуться и изменить данные

🎨 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 комментариями

Ошибка: Данные не сохраняются между сессиями

Проверить:

  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 очередь проверки показывает прогресс через прогресс-бар.

Паттерн реализации:

// Таймауты: 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

Почему это важно:

  • Без сброса плашки успеха/ошибки от предыдущего задания остаются видимыми
  • Пользователь видит некорректную информацию
  • Состояние очереди "протекает" между заданиями

🎓 Философия проекта

  1. Удобство пользователя превыше всего - сохраняем данные, не переспрашиваем
  2. Типобезопасность - используем TypeScript, подавляем ошибки осознанно
  3. Консистентность - следуем установленным паттернам
  4. Персистентность - важные данные (номер места, выбор цепочки) сохраняются в localStorage
  5. Обратная связь - пользователь всегда видит что происходит (позиция в очереди, прогресс проверки)

Версия: 1.1
Дата: 13.12.2025
Автор: Claude (на основе опыта работы с проектом)