@ecomiq/storefront 2.0.11 → 2.0.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ecomiq/storefront",
3
- "version": "2.0.11",
3
+ "version": "2.0.12",
4
4
  "description": "Micro framework SSR de Ecomiq para storefronts. Páginas, datos y deploy sin configurar nada.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -0,0 +1,9 @@
1
+ export {
2
+ listRecentlyViewed,
3
+ trackRecentlyViewed,
4
+ toRecentlyViewedItem,
5
+ useRecentlyViewed,
6
+ useTrackRecentlyViewed,
7
+ type RecentlyViewedItem,
8
+ type TrackableProduct,
9
+ } from './recently-viewed'
@@ -0,0 +1,125 @@
1
+ import { useCallback, useEffect, useState } from 'react'
2
+ import { z } from 'zod'
3
+ import { useSettings } from '../providers/ecomiq'
4
+
5
+ const RecentlyViewedItemSchema = z.object({
6
+ id: z.string().min(1),
7
+ name: z.string().min(1),
8
+ slug: z.string().min(1),
9
+ imageUrl: z.string(),
10
+ price: z.number().nonnegative(),
11
+ compareAtPrice: z.number().nonnegative().nullable(),
12
+ })
13
+
14
+ const RecentlyViewedSessionSchema = z.object({
15
+ version: z.literal(1),
16
+ storeId: z.string().min(1),
17
+ items: z.array(RecentlyViewedItemSchema).max(12),
18
+ })
19
+
20
+ export type RecentlyViewedItem = z.infer<typeof RecentlyViewedItemSchema>
21
+
22
+ const MAX_ITEMS = 8
23
+
24
+ function storageKey(storeId: string) {
25
+ return `ecomiq:recently-viewed:v1:${storeId}`
26
+ }
27
+
28
+ function readSession(storeId: string) {
29
+ if (typeof localStorage === 'undefined') return undefined
30
+ try {
31
+ const raw = localStorage.getItem(storageKey(storeId))
32
+ if (!raw) return undefined
33
+ const parsed = RecentlyViewedSessionSchema.safeParse(JSON.parse(raw))
34
+ return parsed.success && parsed.data.storeId === storeId ? parsed.data : undefined
35
+ } catch {
36
+ return undefined
37
+ }
38
+ }
39
+
40
+ /** Lectura síncrona (tests / casos sin React). */
41
+ export function listRecentlyViewed(storeId: string): RecentlyViewedItem[] {
42
+ return readSession(storeId)?.items ?? []
43
+ }
44
+
45
+ /** Persistencia síncrona: el item queda al frente y se deduplica por id. */
46
+ export function trackRecentlyViewed(
47
+ storeId: string,
48
+ item: RecentlyViewedItem,
49
+ ): RecentlyViewedItem[] {
50
+ const current = listRecentlyViewed(storeId).filter((entry) => entry.id !== item.id)
51
+ const items = [item, ...current].slice(0, MAX_ITEMS)
52
+ localStorage.setItem(
53
+ storageKey(storeId),
54
+ JSON.stringify({
55
+ version: 1,
56
+ storeId,
57
+ items,
58
+ } satisfies z.infer<typeof RecentlyViewedSessionSchema>),
59
+ )
60
+ return items
61
+ }
62
+
63
+ export type TrackableProduct = {
64
+ id: string | number
65
+ name: string
66
+ slug: string
67
+ imageUrl?: string | null
68
+ images?: Array<{ url: string }>
69
+ price: number
70
+ compareAtPrice: number | null
71
+ }
72
+
73
+ export function toRecentlyViewedItem(product: TrackableProduct): RecentlyViewedItem {
74
+ return {
75
+ id: String(product.id),
76
+ name: product.name,
77
+ slug: product.slug,
78
+ imageUrl: product.imageUrl || product.images?.[0]?.url || '',
79
+ price: product.price,
80
+ compareAtPrice: product.compareAtPrice,
81
+ }
82
+ }
83
+
84
+ /**
85
+ * Lista reactiva de productos vistos recientemente de la tienda actual.
86
+ *
87
+ * ```tsx
88
+ * const { items, track } = useRecentlyViewed()
89
+ * ```
90
+ */
91
+ export function useRecentlyViewed() {
92
+ const { store } = useSettings()
93
+ const [items, setItems] = useState<RecentlyViewedItem[]>([])
94
+
95
+ useEffect(() => {
96
+ setItems(listRecentlyViewed(store.id))
97
+ }, [store.id])
98
+
99
+ const track = useCallback(
100
+ (item: RecentlyViewedItem) => {
101
+ const next = trackRecentlyViewed(store.id, item)
102
+ setItems(next)
103
+ return next
104
+ },
105
+ [store.id],
106
+ )
107
+
108
+ return { items, track, storeId: store.id }
109
+ }
110
+
111
+ /**
112
+ * Registra un producto en “visto recientemente” al montar / cambiar de PDP.
113
+ *
114
+ * ```tsx
115
+ * useTrackRecentlyViewed(product)
116
+ * ```
117
+ */
118
+ export function useTrackRecentlyViewed(product: TrackableProduct) {
119
+ const { store } = useSettings()
120
+ const item = toRecentlyViewedItem(product)
121
+
122
+ useEffect(() => {
123
+ trackRecentlyViewed(store.id, item)
124
+ }, [store.id, item.id, item.name, item.slug, item.imageUrl, item.price, item.compareAtPrice])
125
+ }
package/src/index.ts CHANGED
@@ -166,6 +166,15 @@ export {
166
166
  type StorefrontAuthClientInstance,
167
167
  type StorefrontSession,
168
168
  } from './providers'
169
+ export {
170
+ listRecentlyViewed,
171
+ trackRecentlyViewed,
172
+ toRecentlyViewedItem,
173
+ useRecentlyViewed,
174
+ useTrackRecentlyViewed,
175
+ type RecentlyViewedItem,
176
+ type TrackableProduct,
177
+ } from './hooks'
169
178
  export { createStorefrontAuth } from './auth/create-storefront-auth'
170
179
  export { ecomiqOAuthPassword } from './auth/ecomiq-oauth-password-plugin'
171
180
  export { ecomiqOAuthPasswordClient } from './auth/ecomiq-oauth-password-client'