Розробка блоку "Недавно переглянуті" для E-Commerce
Блок недавно переглянутих товарів допомагає користувачу повернутися до вивчених позицій без пошуку по каталогу. Це один із найпростіших елементів персоналізації з мінімальною вартістю розробки та вимірюваним впливом на повернення до карточок товару. Розробка займає 1–2 робочих дня.
Зберігання історії переглядів
Для гостей історія зберігається в localStorage. Для авторизованих — за бажанням синхронізується з сервером:
// hooks/useRecentlyViewed.ts
const STORAGE_KEY = 'recently_viewed';
const MAX_ITEMS = 20;
export const useRecentlyViewed = () => {
const [items, setItems] = useState<number[]>(() => {
const stored = localStorage.getItem(STORAGE_KEY);
return stored ? JSON.parse(stored) : [];
});
const addProduct = useCallback((productId: number) => {
setItems(prev => {
const filtered = prev.filter(id => id !== productId);
const updated = [productId, ...filtered].slice(0, MAX_ITEMS);
localStorage.setItem(STORAGE_KEY, JSON.stringify(updated));
return updated;
});
}, []);
const clearHistory = useCallback(() => {
localStorage.removeItem(STORAGE_KEY);
setItems([]);
}, []);
return { productIds: items, addProduct, clearHistory };
};
На сторінці кожного товару — виклик addProduct(product.id):
// У компоненті сторінки товару
const { addProduct } = useRecentlyViewed();
useEffect(() => { addProduct(product.id); }, [product.id]);
Серверна синхронізація для авторизованих користувачів
Для авторизованих користувачів історія синхронізується з сервером — щоб зберігалася між пристроями:
// routes
Route::middleware('auth:sanctum')->post('/me/recently-viewed', [RecentlyViewedController::class, 'sync']);
Route::middleware('auth:sanctum')->get('/me/recently-viewed', [RecentlyViewedController::class, 'index']);
public function sync(Request $request): JsonResponse
{
$request->validate(['product_ids' => 'required|array|max:20', 'product_ids.*' => 'integer']);
$user = $request->user();
// Оновлюємо порядок: передана список — актуальний стан
$user->recentlyViewed()->sync(
collect($request->product_ids)->mapWithKeys(fn($id, $pos) => [
$id => ['position' => $pos, 'viewed_at' => now()]
])
);
return response()->json(['synced' => count($request->product_ids)]);
}
Синхронізація — при логіні (мерж localStorage з серверною історією) та при закритті вкладки (beforeunload + navigator.sendBeacon).
Завантаження даних про товари
Історія зберігає тільки product_id. Для відображення блоку потрібні дані — один запит до API:
const RecentlyViewedBlock = () => {
const { productIds } = useRecentlyViewed();
const visibleIds = productIds.slice(0, 8); // показуємо не більше 8
const { data: products } = useQuery({
queryKey: ['recently-viewed-products', visibleIds],
queryFn: () => api.get('/products/batch', { params: { ids: visibleIds.join(',') } }),
enabled: visibleIds.length > 0,
staleTime: 300_000,
});
if (!products?.length) return null;
// Зберігаємо порядок з історії
const ordered = visibleIds
.map(id => products.find((p: Product) => p.id === id))
.filter(Boolean);
return (
<section>
<div className="flex justify-between items-center mb-4">
<h2 className="text-lg font-semibold">Ви недавно переглядали</h2>
<button onClick={clearHistory} className="text-sm text-gray-400 hover:text-gray-600">
Очистити історію
</button>
</div>
<ProductCarousel products={ordered} />
</section>
);
};
Endpoint для batch-завантаження
public function batch(Request $request): JsonResponse
{
$request->validate(['ids' => 'required|string']);
$ids = array_filter(array_map('intval', explode(',', $request->ids)));
$ids = array_slice($ids, 0, 20); // лімітувати
$products = Product::whereIn('id', $ids)
->where('is_active', true)
->select(['id', 'name', 'slug', 'price', 'sale_price', 'rating_avg', 'rating_count'])
->with('thumbnail')
->get()
->keyBy('id');
// Повертаємо у порядку переданих id
$ordered = collect($ids)->map(fn($id) => $products->get($id))->filter()->values();
return response()->json(ProductCardResource::collection($ordered));
}
Де розміщувати блок
- Головна сторінка: для повернення користувачів — замість або поряд з популярними товарами
- Сторінка категорії: у нижній частині, після основної сітки товарів
- Сторінка товару: під блоком "Схожі товари"
- Кошик: в боковій панелі на десктопі
- Порожні результати пошуку: "Можливо, ви шукаєте щось з переглянутого?"
Виключення поточного товару
На сторінці товару X з блоку "Недавно переглянуті" виключається сам товар X — інакше він неминуче опиниться першим у списку:
const productIds = useRecentlyViewed().productIds.filter(id => id !== currentProductId);
Приватність
Кнопка "Очистити історію" дає користувачу контроль над даними. Термін зберігання в localStorage — не обмежений браузером, але можна додати TTL вручну:
const stored = localStorage.getItem(STORAGE_KEY);
if (stored) {
const { items, savedAt } = JSON.parse(stored);
const isExpired = Date.now() - savedAt > 30 * 24 * 60 * 60 * 1000; // 30 днів
if (isExpired) localStorage.removeItem(STORAGE_KEY);
}







