@pradip1995/segment-animation-shop-grid 0.1.2 → 0.1.4

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": "@pradip1995/segment-animation-shop-grid",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "license": "MIT",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -15,10 +15,6 @@
15
15
  ".": "./src/index.ts",
16
16
  "./manifest": "./src/manifest.ts"
17
17
  },
18
- "scripts": {
19
- "typecheck": "tsc --noEmit",
20
- "lint": "tsc --noEmit"
21
- },
22
18
  "peerDependencies": {
23
19
  "@pradip1995/commerce-core": "^4.0.0",
24
20
  "@pradip1995/medusa-connector": "^0.2.0",
@@ -28,9 +24,9 @@
28
24
  "next": ">=15"
29
25
  },
30
26
  "dependencies": {
31
- "@pradip1995/segment-primitives": "^0.4.0",
32
- "@pradip1995/segment-product-card": "^0.3.10",
33
- "@pradip1995/segment-tokens": "^0.3.7",
27
+ "@pradip1995/segment-primitives": "^0.4.11",
28
+ "@pradip1995/segment-product-card": "^0.3.26",
29
+ "@pradip1995/segment-tokens": "^0.3.14",
34
30
  "framer-motion": "^12.0.0"
35
31
  },
36
32
  "devDependencies": {
@@ -38,5 +34,9 @@
38
34
  "@types/react": "^19",
39
35
  "react": "19.0.3",
40
36
  "typescript": "^5.7.2"
37
+ },
38
+ "scripts": {
39
+ "typecheck": "tsc --noEmit",
40
+ "lint": "tsc --noEmit"
41
41
  }
42
42
  }
@@ -1,6 +1,6 @@
1
1
  .animation-shop-grid {
2
2
  width: 100%;
3
- padding: 2.5rem 1.5rem 4rem 1.5rem;
3
+ padding: 0 1.5rem 4rem 1.5rem;
4
4
  background-color: var(--page-bg, #ffffff);
5
5
  color: var(--text-color, #111111);
6
6
  font-family: inherit;
@@ -9,7 +9,7 @@
9
9
 
10
10
  @media (min-width: 1024px) {
11
11
  .animation-shop-grid {
12
- padding: 3.5rem 3rem 6rem 3rem;
12
+ padding: 0 3rem 6rem 3rem;
13
13
  }
14
14
  }
15
15
 
@@ -9,6 +9,7 @@ import { loadMoreStoreProducts, type StoreLoadMoreFilters } from "./load-more-pr
9
9
  import "./animation-shop-grid.css"
10
10
 
11
11
  const PAGE_SIZE = 12
12
+ const EMPTY_PRODUCTS: HttpTypes.StoreProduct[] = []
12
13
 
13
14
  const SORT_OPTIONS = [
14
15
  { value: "created_at_desc", label: "Newest" },
@@ -119,6 +120,20 @@ function matchLabel(
119
120
  return hit?.name || hit?.title || undefined
120
121
  }
121
122
 
123
+ function formatSlugToTitle(slug?: string): string | undefined {
124
+ if (!slug) return undefined
125
+ if (slug.includes(",")) {
126
+ return slug
127
+ .split(",")
128
+ .map((s) => formatSlugToTitle(s))
129
+ .filter(Boolean)
130
+ .join(" & ")
131
+ }
132
+ return slug
133
+ .replace(/[-_]+/g, " ")
134
+ .replace(/\b\w/g, (char) => char.toUpperCase())
135
+ }
136
+
122
137
  const containerVariants = {
123
138
  hidden: { opacity: 0 },
124
139
  visible: {
@@ -135,7 +150,7 @@ const cardVariants = {
135
150
  visible: {
136
151
  opacity: 1,
137
152
  y: 0,
138
- transition: { duration: 0.4, ease: [0.16, 1, 0.3, 1] },
153
+ transition: { duration: 0.4, ease: [0.16, 1, 0.3, 1] as const },
139
154
  },
140
155
  }
141
156
 
@@ -226,7 +241,7 @@ export default function AnimationShopGrid(props: {
226
241
  props.maxPrice,
227
242
  ]
228
243
  )
229
- const initialProducts = props.products ?? []
244
+ const initialProducts = props.products ?? EMPTY_PRODUCTS
230
245
  const totalCount = props.productCount ?? initialProducts.length
231
246
 
232
247
  const [items, setItems] = useState(initialProducts)
@@ -241,12 +256,12 @@ export default function AnimationShopGrid(props: {
241
256
  const loadingRef = useRef(false)
242
257
 
243
258
  useEffect(() => {
244
- setItems(props.products ?? [])
259
+ setItems(props.products ?? EMPTY_PRODUCTS)
245
260
  setCurrentPage(initialPage)
246
261
  setHasMore(sortBy !== "bestsellers" && totalCount > initialPage * PAGE_SIZE)
247
262
  setLoadError(null)
248
263
  loadingRef.current = false
249
- }, [filterSignature, initialPage, sortBy, totalCount])
264
+ }, [filterSignature, initialPage, sortBy, totalCount, props.products])
250
265
 
251
266
  const loadNextPage = useCallback(() => {
252
267
  if (loadingRef.current || pending || !hasMore || sortBy === "bestsellers") return
@@ -297,12 +312,23 @@ export default function AnimationShopGrid(props: {
297
312
 
298
313
  const activeCategory = paramValue(props.category)
299
314
  const activeCollection = paramValue(props.collection)
315
+ const activeGender = paramValue(props.gender)
316
+ const activeMaterial = paramValue(props.material)
317
+ const activeColor = paramValue(props.color)
318
+ const activeType = paramValue(props.type)
319
+
300
320
  const categoryLabel =
301
321
  matchLabel(activeCategory, props.categories || []) ||
302
- (activeCategory ? "Category" : undefined)
322
+ formatSlugToTitle(activeCategory)
323
+
303
324
  const collectionLabel =
304
325
  matchLabel(activeCollection, props.collections || []) ||
305
- (activeCollection ? "Collection" : undefined)
326
+ formatSlugToTitle(activeCollection)
327
+
328
+ const genderLabel = formatSlugToTitle(activeGender)
329
+ const materialLabel = formatSlugToTitle(activeMaterial)
330
+ const colorLabel = formatSlugToTitle(activeColor)
331
+ const typeLabel = formatSlugToTitle(activeType)
306
332
 
307
333
  const appliedFilters: { key: string; label: string; href: string }[] = []
308
334
  if (props.q) {
@@ -327,90 +353,120 @@ export default function AnimationShopGrid(props: {
327
353
  })
328
354
  }
329
355
 
356
+ const displayTitle = useMemo(() => {
357
+ if (categoryLabel) return categoryLabel
358
+ if (collectionLabel) return collectionLabel
359
+ if (props.q) return `Search: ${props.q}`
360
+ if (genderLabel) return `${genderLabel}'s Collection`
361
+ if (materialLabel) return materialLabel
362
+ if (colorLabel) return colorLabel
363
+ if (typeLabel) return typeLabel
364
+ if (sortBy === "bestsellers") return "Bestsellers"
365
+ if (!props.title || props.title.toLowerCase() === "all beauty" || props.title.toLowerCase() === "all products") {
366
+ return "ALL PRODUCTS"
367
+ }
368
+ return props.title
369
+ }, [categoryLabel, collectionLabel, props.q, genderLabel, materialLabel, colorLabel, typeLabel, sortBy, props.title])
370
+
371
+ const displaySubtitle = useMemo(() => {
372
+ if (categoryLabel) return `Explore our curated selection of ${categoryLabel.toLowerCase()}.`
373
+ if (collectionLabel) return `Discover the latest items in our ${collectionLabel.toLowerCase()} collection.`
374
+ if (props.q) return `Product results matching "${props.q}".`
375
+ if (sortBy === "bestsellers") return "Our most popular and highly-rated pieces."
376
+ if (genderLabel || materialLabel || colorLabel) return "Handpicked items filtered to your taste."
377
+ return "The latest additions to the lineup."
378
+ }, [categoryLabel, collectionLabel, props.q, sortBy, genderLabel, materialLabel, colorLabel])
379
+
380
+ const [showFilters, setShowFilters] = useState(true)
381
+
382
+ useEffect(() => {
383
+ const sidebar = document.querySelector(".store-page__sidebar") as HTMLElement | null
384
+ const mainCol = document.querySelector(".store-page__main") as HTMLElement | null
385
+ if (sidebar) {
386
+ sidebar.style.display = showFilters ? "" : "none"
387
+ }
388
+ if (mainCol) {
389
+ if (!showFilters) {
390
+ mainCol.classList.add("w-full", "col-span-full")
391
+ } else {
392
+ mainCol.classList.remove("w-full", "col-span-full")
393
+ }
394
+ }
395
+ }, [showFilters])
396
+
330
397
  return (
331
- <section className="animation-shop-grid flex-1 min-w-0" aria-label="Product results">
398
+ <section className="animation-shop-grid flex-1 min-w-0 pt-0" aria-label="Product results">
399
+ {/* Simplified, Clean Hero Header */}
332
400
  <motion.header
333
- className="animation-shop-grid__header"
334
- initial={{ opacity: 0, y: -10 }}
401
+ className="w-full bg-[#FAF9F6] py-6 px-4 mb-4 flex flex-col justify-center items-center text-center border-b border-gray-100 rounded-sm"
402
+ initial={{ opacity: 0, y: -6 }}
335
403
  animate={{ opacity: 1, y: 0 }}
336
- transition={{ duration: 0.5, ease: [0.16, 1, 0.3, 1] }}
404
+ transition={{ duration: 0.4, ease: [0.16, 1, 0.3, 1] }}
337
405
  >
338
- <div className="animation-shop-grid__intro">
339
- <p className="animation-shop-grid__eyebrow">{eyebrow}</p>
340
- <h2 className="animation-shop-grid__title">{title}</h2>
341
- {props.q ? (
342
- <p className="animation-shop-grid__search">
343
- Results for &ldquo;{props.q}&rdquo;
344
- </p>
345
- ) : null}
346
- {totalCount > 0 ? (
347
- <p className="animation-shop-grid__count">
348
- Showing {showingCount} of {totalCount} {totalCount === 1 ? "product" : "products"}
349
- </p>
350
- ) : null}
351
- </div>
406
+ <nav aria-label="Breadcrumb" className="text-[11px] text-gray-400 font-sans tracking-wide mb-1.5 uppercase">
407
+ <LocalizedLink href="/" className="hover:text-black transition-colors" countryCode={countryCode}>
408
+ Home
409
+ </LocalizedLink>
410
+ <span className="mx-2 text-gray-300">/</span>
411
+ <span className="text-gray-900 font-semibold tracking-wider">{displayTitle}</span>
412
+ </nav>
352
413
 
353
- <div className="animation-shop-grid__sort" role="group" aria-label="Sort products">
354
- {SORT_OPTIONS.map((opt) => {
355
- const active = sortBy === opt.value
356
- return (
357
- <LocalizedLink
358
- key={opt.value}
359
- href={buildSortHref(storeSearchParams, opt.value)}
360
- className={`animation-shop-grid__sort-pill${active ? " animation-shop-grid__sort-pill--active" : ""}`}
361
- countryCode={countryCode}
362
- >
363
- {opt.label}
364
- </LocalizedLink>
365
- )
366
- })}
367
- </div>
414
+ <h1 className="text-xl sm:text-2xl font-normal tracking-widest text-[#111111] uppercase my-1">
415
+ {displayTitle}
416
+ </h1>
417
+
418
+ <p className="text-xs text-gray-500 font-normal tracking-wide max-w-sm m-0">
419
+ {displaySubtitle}
420
+ </p>
368
421
  </motion.header>
369
422
 
370
- <AnimatePresence>
371
- {appliedFilters.length > 0 ? (
372
- <motion.div
373
- className="animation-shop-grid__applied"
374
- role="group"
375
- aria-label="Applied filters"
376
- initial={{ opacity: 0, height: 0 }}
377
- animate={{ opacity: 1, height: "auto" }}
378
- exit={{ opacity: 0, height: 0 }}
423
+ {/* Filter Toolbar with Hide Filters Option */}
424
+ <div className="w-full flex flex-wrap items-center justify-between gap-3 mb-5 py-2.5 px-4 bg-white border border-gray-200/80 rounded-md shadow-2xs">
425
+ <div className="flex items-center gap-3">
426
+ <button
427
+ type="button"
428
+ onClick={() => setShowFilters(!showFilters)}
429
+ className="inline-flex items-center gap-2 text-xs font-semibold uppercase tracking-wider text-gray-800 hover:text-black transition-colors py-1 px-2.5 bg-gray-50 hover:bg-gray-100 border border-gray-200 rounded cursor-pointer"
379
430
  >
380
- <span className="animation-shop-grid__applied-label">Filters</span>
381
- <ul className="animation-shop-grid__applied-list">
431
+ <svg className="w-3.5 h-3.5 fill-none stroke-current stroke-[1.8]" viewBox="0 0 24 24">
432
+ <path strokeLinecap="round" strokeLinejoin="round" d="M10.5 6h9m-9 12h9m-15-6h15M3 6h3m-3 12h3m3-6h3" />
433
+ </svg>
434
+ <span>{showFilters ? "Hide Filters" : "Show Filters"}</span>
435
+ </button>
436
+
437
+ {appliedFilters.length > 0 ? (
438
+ <div className="hidden sm:flex items-center gap-2">
439
+ <span className="text-[11px] font-bold text-gray-400 uppercase tracking-wider">Active:</span>
382
440
  {appliedFilters.map((filter) => (
383
- <li key={filter.key}>
384
- <LocalizedLink
385
- href={filter.href}
386
- className="animation-shop-grid__applied-chip"
387
- countryCode={countryCode}
388
- aria-label={`Remove ${filter.label} filter`}
389
- >
390
- <span>{filter.label}</span>
391
- <span className="animation-shop-grid__applied-x" aria-hidden>
392
- ×
393
- </span>
394
- </LocalizedLink>
395
- </li>
441
+ <LocalizedLink
442
+ key={filter.key}
443
+ href={filter.href}
444
+ className="inline-flex items-center gap-1 text-xs font-medium bg-gray-100 hover:bg-gray-200 text-gray-900 px-2 py-0.5 rounded transition-colors"
445
+ countryCode={countryCode}
446
+ >
447
+ <span>{filter.label}</span>
448
+ <span className="text-gray-500 hover:text-black">×</span>
449
+ </LocalizedLink>
396
450
  ))}
397
- </ul>
451
+ </div>
452
+ ) : null}
453
+ </div>
454
+
455
+ <div className="flex items-center gap-3 ml-auto">
456
+ <span className="text-xs text-gray-500 font-medium tracking-wide">
457
+ {showingCount} of {totalCount} {totalCount === 1 ? "Product" : "Products"}
458
+ </span>
459
+ {appliedFilters.length > 0 ? (
398
460
  <LocalizedLink
399
461
  href={buildClearAllFiltersHref(storeSearchParams)}
400
- className="animation-shop-grid__applied-clear"
462
+ className="text-xs font-bold text-red-600 hover:text-red-700 uppercase tracking-wider underline ml-2"
401
463
  countryCode={countryCode}
402
464
  >
403
- Clear all
465
+ Clear All
404
466
  </LocalizedLink>
405
- </motion.div>
406
- ) : null}
407
- </AnimatePresence>
408
-
409
- {totalCount > 0 && sortBy !== "bestsellers" ? (
410
- <div className="animation-shop-grid__progress" aria-hidden>
411
- <span className="animation-shop-grid__progress-fill" style={{ width: `${progressPct}%` }} />
467
+ ) : null}
412
468
  </div>
413
- ) : null}
469
+ </div>
414
470
 
415
471
  {items.length === 0 ? (
416
472
  <motion.div
@@ -437,7 +493,7 @@ export default function AnimationShopGrid(props: {
437
493
  <ProductCard
438
494
  product={product}
439
495
  countryCode={countryCode}
440
- variant={props.productCardVariant ?? "motion"}
496
+ variant={props.productCardVariant ?? "shop"}
441
497
  />
442
498
  </motion.div>
443
499
  ))}
@@ -451,6 +507,46 @@ export default function AnimationShopGrid(props: {
451
507
  </div>
452
508
  ) : null}
453
509
 
510
+ {/* Managed Pagination: Only show page buttons if totalPages > 1 */}
511
+ {Math.ceil(totalCount / PAGE_SIZE) > 1 ? (
512
+ <div className="animation-shop-grid__pagination flex items-center justify-center gap-2 mt-8 mb-2">
513
+ {Array.from({ length: Math.ceil(totalCount / PAGE_SIZE) }, (_, i) => i + 1).map((pageNum) => (
514
+ <button
515
+ key={pageNum}
516
+ type="button"
517
+ onClick={() => {
518
+ if (pageNum > currentPage && hasMore) {
519
+ loadNextPage()
520
+ }
521
+ }}
522
+ className={`w-8 h-8 rounded-full text-xs font-semibold flex items-center justify-center transition-all ${
523
+ currentPage === pageNum
524
+ ? "bg-black text-white shadow-sm font-bold scale-105"
525
+ : "bg-gray-100 text-gray-600 hover:bg-gray-200 hover:text-black"
526
+ }`}
527
+ >
528
+ {pageNum}
529
+ </button>
530
+ ))}
531
+ {hasMore && (
532
+ <button
533
+ type="button"
534
+ onClick={loadNextPage}
535
+ className="w-8 h-8 rounded-full bg-black hover:bg-gray-800 text-white flex items-center justify-center transition-all hover:scale-105 shadow-sm ml-1"
536
+ aria-label="Next page"
537
+ >
538
+ <svg className="w-3.5 h-3.5 fill-current" viewBox="0 0 20 20">
539
+ <path
540
+ fillRule="evenodd"
541
+ d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z"
542
+ clipRule="evenodd"
543
+ />
544
+ </svg>
545
+ </button>
546
+ )}
547
+ </div>
548
+ ) : null}
549
+
454
550
  {loadError ? (
455
551
  <div className="animation-shop-grid__error">
456
552
  <p>{loadError}</p>
@@ -461,7 +557,7 @@ export default function AnimationShopGrid(props: {
461
557
  ) : null}
462
558
 
463
559
  {!hasMore && items.length > 0 ? (
464
- <p className="animation-shop-grid__end">
560
+ <p className="animation-shop-grid__end text-xs tracking-wider text-gray-500 uppercase font-medium mt-4">
465
561
  You&apos;ve viewed all {totalCount} {totalCount === 1 ? "product" : "products"}
466
562
  </p>
467
563
  ) : null}
package/src/manifest.ts CHANGED
@@ -3,7 +3,7 @@ import type { SegmentManifest } from "@pradip1995/plugin-sdk"
3
3
  const manifest: SegmentManifest = {
4
4
  id: "animation-shop-grid",
5
5
  type: "segment",
6
- version: "0.1.0",
6
+ version: "0.1.1",
7
7
  compatibleFramework: ["^1.0.0"],
8
8
  dataKey: "store",
9
9
  }