Каталог онлайн-курсов на HTML + Vanilla JS + SCSS (BEM).
npm install
npm run build:css
npm run devОткроется локальный сервер. Для продакшена CSS собирается командой npm run build.
src/
assets/ # фото инструкторов, SVG-декор
scss/ # стили (BEM + CSS-переменные)
js/
data/ # курсы, константы категорий
utils/ # фильтрация, debounce, изображения
catalog/ # CourseCatalog + render
main.js
index.html
css/main.css # сборка из SCSS
- Поиск по названию (живой, debounce 150 ms)
- Фильтрация по категориям со счётчиками
- Load more / Show less
- Fluid-адаптив 1920 → 320 px без жёстких брейкпоинтов
- Безопасный рендер через
<template>+textContent
Проект настроен под Vercel:
npm run build # sass → css/main.cssvercel.json: build-команда + output из корня репозитория.
Это не "обязательная часть ТЗ", а мой взгляд на продукт и инженерию на несколько шагов вперёд - как бы я развивал проект, если бы он перестал быть тестовым и стал частью реального продукта.
Сейчас структура js/ и scss/blocks/ оправдана: одна секция, один доменный модуль (CourseCatalog). Если страниц станет 30, плоские папки превратятся в «свалку».
Как бы я перестроил:
src/
pages/
courses/ # всё, что относится только к этой странице
index.html
courses.js
courses.scss
shared/
ui/ # кнопки, инпуты, badge - переиспользуемые блоки
lib/ # debounce, dom helpers
tokens/ # CSS-переменные / design tokens
features/
catalog/ # бизнес-логика каталога (фильтр, поиск, пагинация)
Принцип: page → features → shared. Страница собирается из фич, фичи не знают о вёрстке соседних страниц.
Альтернатива BEM на масштабе: для изолированных блоков BEM остаётся отличным выбором (как сейчас). При росте UI-kit я бы добавил CSS Modules. BEM не «устарел» - он просто решает другую задачу: предсказуемые имена без runtime. На 30 страницах важнее границы модулей, чем смена методологии ради смены.
Направление | Зачем
Сортировка (цена ↑↓, название) | Базовый UX каталога
URL-состояние (?category=marketing&q=design) | Шаринг, SEO, «назад» в браузере
Skeleton / loading states | Если данные с API - не моргать пустым гридом
Виртуализация списка | 1000+ курсов без лагов (сейчас DOM-кеш уже задел)
i18n | EN/RU без дублирования разметки
- Show less - в реальном продукте часто заменяют на бесконечный скролл или классическую пагинацию; «схлопывание» редко нужно пользователю.
- Дублирование путей к ассетам в
images.js- один маппинг + fallback, валидация при сборке.
Вёрстка - не конец. У каталога есть метрики: CTR карточки, конверсия в просмотр курса, время до первого клика, bounce при пустом поиске.
Гипотезы, которые я бы проверил:
- Load more vs пагинация 1-2-3 - load more часто выигрывает по engagement, но проигрывает по SEO; нужны данные.
- Поиск без debounce vs 150 ms - на мобилке мгновенный фильтр может «дёргать» экран; A/B на
input lagиtask completion. - Счётчик на фильтре (
Marketing⁴) - повышает доверие или перегружает? Метрика: клики по категориям vs All. - Карточка: цена крупнее автора - гипотеза: цена - главный триггер; тест через два варианта
.course-card__footer. - Пустая выдача - сейчас текст «No courses found»; альтернатива: «Попробуйте Marketing» + быстрые фильтры → метрика
recovery rate.
Процесс: гипотеза → событие в аналитике → сплит 50/50 → минимум 1–2 недели / N тысяч сессий → решение по доверительному интервалу, не «мне кажется».
| Сейчас | На проде |
|---|---|
Статический courses.js |
REST/GraphQL + кеш |
| Vanilla JS | Можно остаться (мало UI) или Vue/React только для интерактива |
| SCSS вручную | Style Dictionary из Figma tokens |
| Нет CI | GitHub Action: build:css + Playwright smoke на каждый PR |
Тестовое закрывает ТЗ: pixel-perfect вёрстка (старался прям по pixel-perfect делать, но из за того, что верстка резиновая(для адаптива) - в некоторых моментах могут быть расхождения), fluid, поиск, фильтры, чистый BEM, vanilla JS.
Следующий уровень - не «переписать на React», а выстроить процесс: модульная структура, design tokens, AI-пайплайн Figma→код, метрики и A/B. Так вёрстка перестаёт быть разовой задачей и становится частью продуктовой системы.
- Outline:
p.courses__label→h1→h2в карточке aria-liveна пустой выдаче,aria-busyна гриде,role="tablist"на фильтрахdestroy()для снятия слушателей при необходимости