Розроблення списку бажаного (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');
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,
is_active BOOLEAN,
last_notified_at
)
Процес відправлення сповіщень (запускається раз на годину чи при оновленні ціни):
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 шт.
- «Ви давно не заходили — ось що змінилось у вашому списку» — реактивационное письмо
Реалізація: при подієї (старт акції / зміна остатку) — асинхронна задача в черзі, яка збирає користувачів з цим товаром в вишлисте та відправляє персоналізовані листи.
Счітчик на іконці вишлиста
У навігації — іконка серця з бейджем (кількість товарів у списку). Бейдж оновлюється миттєво через 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 тижні







