Разработка списка избранного (Wishlist) для интернет-магазина
Wishlist — список товаров, которые пользователь отметил для себя: «хочу купить позже», «слежу за ценой», «подарочный список». Это ненулевой по сложности функциональный блок: состояние кнопки синхронизировано глобально, список работает без авторизации через localStorage, а при входе мержится с серверным. Плюс уведомления о снижении цен, шаринг списка, сегментация в маркетинге.
Сценарии использования
Три основных сценария, каждый с разными техническими требованиями:
«Куплю потом» — быстрое сохранение для возврата. Не требует авторизации, достаточно localStorage.
«Слежу за ценой» — пользователь хочет уведомление при снижении цены. Требует email/push + авторизованный аккаунт.
«Подарочный список» (Gift Registry) — публичный или ссылочный список, которым делятся с другими. Требует серверное хранение + уникальный URL.
Хранение и синхронизация
Анонимный пользователь: список в localStorage. При загрузке страницы — инициализация store из localStorage.
Авторизованный пользователь: список в БД, localStorage как кеш. При авторизации — merge:
async function mergeWishlists(localItems: number[], userId: number) {
const serverItems = await api.getWishlist(userId);
const merged = [...new Set([...serverItems, ...localItems])];
await api.syncWishlist(userId, merged);
localStorage.removeItem('wishlist'); // очищаем local после merge
return merged;
}
Схема БД:
wishlists (
id, user_id, name, slug,
is_public BOOLEAN DEFAULT FALSE,
share_token VARCHAR(32), -- для ссылочного доступа
created_at
)
wishlist_items (
id, wishlist_id, product_id, variant_id,
note TEXT, -- личная заметка к товару
added_at TIMESTAMPTZ,
price_at_addition NUMERIC -- цена в момент добавления
)
Один пользователь может иметь несколько вишлистов (основной + «для спальни» + «подарочный»). Для большинства магазинов достаточно одного списка на пользователя — усложнять не стоит без явной потребности.
Кнопка «В избранное»
Иконка сердца (или закладки) на карточке товара в листинге и на странице товара. Два состояния: пустая / заполненная, с анимацией перехода.
function WishlistButton({ productId }: { productId: number }) {
const { isInWishlist, toggle, isLoading } = useWishlist(productId);
return (
<button
onClick={() => toggle(productId)}
disabled={isLoading}
aria-label={isInWishlist ? 'Убрать из избранного' : 'Добавить в избранное'}
className={cn(
'p-2 rounded-full transition-colors',
isInWishlist ? 'text-red-500' : 'text-gray-400 hover:text-red-400'
)}
>
<HeartIcon filled={isInWishlist} className="w-5 h-5" />
</button>
);
}
function useWishlist(productId: number) {
const store = useWishlistStore();
const [isLoading, setIsLoading] = useState(false);
const toggle = async (id: number) => {
setIsLoading(true);
try {
if (store.has(id)) {
store.remove(id);
if (isAuthenticated) await api.removeFromWishlist(id);
} else {
store.add(id);
if (isAuthenticated) await api.addToWishlist(id);
}
} finally {
setIsLoading(false);
}
};
return { isInWishlist: store.has(productId), toggle, isLoading };
}
Оптимистичный UI: обновляем состояние в store сразу (без ожидания API). Если запрос к серверу упал — откатываем через try/catch. Пользователь видит мгновенную реакцию.
Страница вишлиста
Список избранного — отдельная страница в личном кабинете (/account/wishlist) или публичная страница при шаринге (/wishlist/{slug}).
Интерфейс страницы:
- Грид товаров — те же карточки, что в каталоге, с кнопкой «Убрать»
- Фильтр: по наличию, по дате добавления, по снижению цены
- Сортировка: по дате добавления, по цене (возрастание/убывание), по изменению цены
- «Добавить всё в корзину» — batch-операция, добавляет доступные товары
- Статус наличия: «Нет в наличии» — визуально отмечается, но остаётся в списке
Для каждого товара показываем price_at_addition vs текущая цена — сразу видно, выросла или снизилась цена с момента добавления. Снижение цены — со значком «▼ −12%».
Уведомления о снижении цены
Пользователь может подписаться на уведомление об изменении цены товара в вишлисте:
price_alerts (
id, user_id, product_id,
threshold_type, -- 'any_drop' | 'percent_drop' | 'target_price'
threshold_value, -- для percent_drop: 10 (10%), для target_price: 2990
is_active BOOLEAN,
last_notified_at
)
Процесс отправки уведомлений (запускается через scheduler раз в час или при обновлении цены):
// Laravel Job: CheckPriceAlerts
foreach ($alerts as $alert) {
$currentPrice = $alert->product->price;
$shouldNotify = match ($alert->threshold_type) {
'any_drop' => $currentPrice < $alert->product->previous_price,
'percent_drop' => ($currentPrice / $alert->product->previous_price - 1) <= -$alert->threshold_value / 100,
'target_price' => $currentPrice <= $alert->threshold_value,
};
if ($shouldNotify && $alert->last_notified_at < now()->subDays(3)) {
Mail::to($alert->user)->queue(new PriceDropNotification($alert->product, $currentPrice));
$alert->update(['last_notified_at' => now()]);
}
}
Ограничение частоты уведомлений (last_notified_at < now()->subDays(3)) — чтобы не спамить при волатильных ценах.
Шаринг вишлиста
При включении «Поделиться списком» — генерируется share_token:
$wishlist->update([
'is_public' => true,
'share_token' => Str::random(32),
]);
return "/wishlist/{$wishlist->share_token}";
Публичная страница вишлиста: гость видит товары, может добавить любой в свою корзину, но не может редактировать список. Идеально для подарочных списков (дни рождения, свадьбы).
Кнопка «Скопировать ссылку» + кнопки «Поделиться в Telegram / WhatsApp» с предзаполненным текстом.
Интеграция с email-маркетингом
Wishlist — ценный сегмент для персонализированных рассылок:
- «В вашем избранном распродажа» — при старте акции проверяем пересечение товаров в вишлистах с распродажными товарами
- «Товар из вашего избранного заканчивается» — остаток < 3 шт.
- «Вы давно не заходили — вот что изменилось в вашем списке» — реактивационное письмо
Реализация: при событии (старт акции / изменение остатка) — асинхронная задача в очереди, которая собирает пользователей с этим товаром в вишлисте и отправляет персонализированные письма через ESP (Mailchimp, Sendpulse).
Счётчик на иконке вишлиста
В навигации — иконка сердца с бейджем (количество товаров в списке). Бейдж обновляется мгновенно через store, не через API-запрос при каждом рендере.
function WishlistNavIcon() {
const count = useWishlistStore(state => state.items.length);
return (
<div className="relative">
<HeartIcon className="w-6 h-6" />
{count > 0 && (
<span className="absolute -top-1 -right-1 bg-red-500 text-white
text-xs rounded-full w-4 h-4 flex items-center justify-center">
{count > 99 ? '99+' : count}
</span>
)}
</div>
);
}
SEO-аспекты
Личные вишлисты (/account/wishlist) — закрыты авторизацией, не индексируются. Публичные shared-вишлисты — noindex по умолчанию, если не реализована редакционная концепция «коллекций» с уникальным контентом.
Аналитика
- Какие товары чаще добавляют в избранное — сигнал популярности, альтернатива sales_count для новинок
- Wishlist-to-purchase conversion rate: % пользователей, купивших товар из вишлиста
- Среднее время от добавления в вишлист до покупки — помогает настроить тайминг email-триггеров
Сроки
- Базовый wishlist (localStorage, кнопка на карточке, страница списка без авторизации): 2–4 рабочих дня
- С серверным хранением, merge при авторизации, уведомлениями о цене: 1.5–2.5 недели
- Шаринг вишлиста, несколько списков, интеграция с email-маркетингом: 2.5–4 недели







