@instockng/storefront-ui 1.0.10 → 1.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.
Files changed (98) hide show
  1. package/README.md +26 -0
  2. package/dist/components/Checkout.d.ts.map +1 -1
  3. package/dist/components/ShoppingCart.d.ts.map +1 -1
  4. package/dist/contexts/CartContext.d.ts.map +1 -1
  5. package/dist/index10.mjs +144 -141
  6. package/dist/index101.mjs +1 -1
  7. package/dist/index102.mjs +3 -3
  8. package/dist/index103.mjs +3 -3
  9. package/dist/index105.mjs +1 -1
  10. package/dist/index111.mjs +1 -1
  11. package/dist/index20.mjs +2 -2
  12. package/dist/index21.mjs +1 -1
  13. package/dist/index28.mjs +11 -11
  14. package/dist/index3.mjs +88 -78
  15. package/dist/index37.mjs +1 -1
  16. package/dist/index41.mjs +36 -23
  17. package/dist/index42.mjs +44 -36
  18. package/dist/index43.mjs +99 -44
  19. package/dist/index44.mjs +112 -99
  20. package/dist/index45.mjs +44 -80
  21. package/dist/index46.mjs +64 -53
  22. package/dist/index47.mjs +65 -48
  23. package/dist/index48.mjs +54 -73
  24. package/dist/index49.mjs +52 -63
  25. package/dist/index50.mjs +14 -70
  26. package/dist/index51.mjs +13 -14
  27. package/dist/index52.mjs +58 -13
  28. package/dist/index53.mjs +101 -34
  29. package/dist/index54.mjs +99 -95
  30. package/dist/index55.mjs +22 -132
  31. package/dist/index62.mjs +30 -231
  32. package/dist/index63.mjs +42 -5
  33. package/dist/index64.mjs +228 -127
  34. package/dist/index65.mjs +4 -66
  35. package/dist/index66.mjs +124 -77
  36. package/dist/index67.mjs +65 -26
  37. package/dist/index68.mjs +84 -6
  38. package/dist/index69.mjs +26 -72
  39. package/dist/index70.mjs +8 -3
  40. package/dist/index71.mjs +75 -2
  41. package/dist/index72.mjs +3 -82
  42. package/dist/index73.mjs +2 -54
  43. package/dist/index74.mjs +82 -5
  44. package/dist/index75.mjs +53 -4
  45. package/dist/index76.mjs +5 -178
  46. package/dist/index77.mjs +5 -53
  47. package/dist/index78.mjs +178 -68
  48. package/dist/index79.mjs +50 -31
  49. package/dist/index8.mjs +8 -7
  50. package/dist/index80.mjs +69 -43
  51. package/dist/index81.mjs +2 -2
  52. package/dist/index82.mjs +1 -1
  53. package/dist/index83.mjs +6 -6
  54. package/dist/index84.mjs +2 -2
  55. package/dist/index85.mjs +2 -2
  56. package/dist/index87.mjs +2 -2
  57. package/dist/index88.mjs +2 -2
  58. package/dist/index89.mjs +1 -1
  59. package/dist/index91.mjs +4 -4
  60. package/dist/index92.mjs +3 -3
  61. package/dist/index93.mjs +12 -30
  62. package/dist/index94.mjs +7 -11
  63. package/dist/index95.mjs +30 -3
  64. package/dist/index96.mjs +10 -3
  65. package/dist/index97.mjs +4 -13
  66. package/dist/index98.mjs +4 -7
  67. package/dist/index99.mjs +1 -1
  68. package/dist/styles.css +1 -0
  69. package/package.json +14 -13
  70. package/src/components/CartItem.stories.tsx +94 -0
  71. package/src/components/CartItem.tsx +141 -0
  72. package/src/components/Checkout.stories.tsx +380 -0
  73. package/src/components/Checkout.tsx +954 -0
  74. package/src/components/DiscountCodeInput.stories.tsx +76 -0
  75. package/src/components/DiscountCodeInput.tsx +162 -0
  76. package/src/components/OrderConfirmation.stories.tsx +142 -0
  77. package/src/components/OrderConfirmation.tsx +301 -0
  78. package/src/components/ProductCard.stories.tsx +112 -0
  79. package/src/components/ProductCard.tsx +195 -0
  80. package/src/components/ProductGrid.stories.tsx +137 -0
  81. package/src/components/ProductGrid.tsx +141 -0
  82. package/src/components/ShoppingCart.stories.tsx +459 -0
  83. package/src/components/ShoppingCart.tsx +263 -0
  84. package/src/components/ui/badge.tsx +37 -0
  85. package/src/components/ui/button.tsx +71 -0
  86. package/src/components/ui/card.tsx +79 -0
  87. package/src/components/ui/form-input.tsx +78 -0
  88. package/src/components/ui/form-select.tsx +73 -0
  89. package/src/components/ui/modal.tsx +181 -0
  90. package/src/contexts/CartContext.tsx +316 -0
  91. package/src/hooks/usePaystackPayment.ts +137 -0
  92. package/src/index.ts +51 -0
  93. package/src/lib/utils.ts +45 -0
  94. package/src/paystack.svg +67 -0
  95. package/src/providers/StorefrontProvider.tsx +70 -0
  96. package/src/styles.css +1 -0
  97. package/src/test-utils/MockCartProvider.tsx +424 -0
  98. package/src/vite-env.d.ts +12 -0
@@ -0,0 +1,45 @@
1
+ import { type ClassValue, clsx } from "clsx"
2
+ import { twMerge } from "tailwind-merge"
3
+
4
+ export function cn(...inputs: ClassValue[]) {
5
+ return twMerge(clsx(inputs))
6
+ }
7
+
8
+ export function formatCurrency(amount: number): string {
9
+ return new Intl.NumberFormat('en-US', {
10
+ style: 'currency',
11
+ currency: 'NGN',
12
+ }).format(amount)
13
+ }
14
+
15
+ export function formatDate(date: string | Date): string {
16
+ const d = typeof date === 'string' ? new Date(date) : date
17
+ return d.toLocaleDateString('en-US', {
18
+ year: 'numeric',
19
+ month: 'long',
20
+ day: 'numeric',
21
+ })
22
+ }
23
+
24
+ export function formatDateTime(date: string | Date): string {
25
+ const d = typeof date === 'string' ? new Date(date) : date
26
+ return d.toLocaleString('en-US', {
27
+ year: 'numeric',
28
+ month: 'long',
29
+ day: 'numeric',
30
+ hour: '2-digit',
31
+ minute: '2-digit',
32
+ })
33
+ }
34
+
35
+ export function getStatusColor(status: string): string {
36
+ const colors: Record<string, string> = {
37
+ pending: 'bg-yellow-100 text-yellow-800',
38
+ shipped: 'bg-blue-100 text-blue-800',
39
+ delivered: 'bg-green-100 text-green-800',
40
+ cancelled: 'bg-red-100 text-red-800',
41
+ refunded: 'bg-purple-100 text-purple-800',
42
+ prospect: 'bg-orange-100 text-orange-800',
43
+ }
44
+ return colors[status] || 'bg-gray-100 text-gray-800'
45
+ }
@@ -0,0 +1,67 @@
1
+ <svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 280.6 50" style="enable-background:new 0 0 280.6 50;" xml:space="preserve">
2
+ <style type="text/css">
3
+ .st0{fill:#00C3F7;}
4
+ .st1{fill:#011B33;}
5
+ </style>
6
+ <g>
7
+ <g>
8
+ <path class="st0" d="M39.9,4.8H2.3C1.1,4.8,0,5.8,0,7.1v4.2c0,1.3,1.1,2.4,2.3,2.4h37.6c1.3,0,2.3-1.1,2.4-2.4V7.2
9
+ C42.3,5.8,41.2,4.8,39.9,4.8L39.9,4.8z M39.9,28.4H2.3c-0.6,0-1.2,0.3-1.7,0.7c-0.4,0.4-0.7,1-0.7,1.7V35c0,1.3,1.1,2.4,2.3,2.4
10
+ h37.6c1.3,0,2.3-1,2.4-2.4v-4.2C42.3,29.4,41.2,28.4,39.9,28.4L39.9,28.4z M23.5,40.1H2.3c-0.6,0-1.2,0.2-1.6,0.7
11
+ c-0.4,0.4-0.7,1-0.7,1.7v4.2c0,1.3,1.1,2.4,2.3,2.4h21.1c1.3,0,2.3-1.1,2.3-2.4v-4.3C25.8,41.2,24.8,40.1,23.5,40.1L23.5,40.1z
12
+ M42.3,16.6h-40c-0.6,0-1.2,0.2-1.6,0.7c-0.4,0.4-0.7,1-0.7,1.7v4.2c0,1.3,1.1,2.4,2.3,2.4h39.9c1.3,0,2.3-1.1,2.3-2.4v-4.2
13
+ C44.6,17.6,43.6,16.6,42.3,16.6L42.3,16.6z M42.3,16.6">
14
+ </path>
15
+ <path class="st1" d="M86,14.3c-1.2-1.2-2.5-2.1-4.1-2.8c-1.5-0.7-3.2-1-4.9-1c-1.6,0-3.2,0.3-4.7,1c-1,0.5-1.9,1.1-2.6,1.9v-0.7
16
+ c0-0.4-0.2-0.7-0.4-1c-0.3-0.3-0.6-0.5-1-0.5h-5.3c-0.4,0-0.8,0.2-1,0.5c-0.3,0.3-0.4,0.6-0.4,1v35c0,0.4,0.2,0.7,0.4,1
17
+ c0.3,0.3,0.6,0.4,1,0.4h5.3c0.4,0,0.7-0.1,1-0.4c0.3-0.3,0.4-0.6,0.4-1v-12c0.8,0.8,1.7,1.5,2.8,1.8c1.4,0.5,2.9,0.8,4.3,0.8
18
+ c1.7,0,3.4-0.3,4.9-1c1.6-0.6,3-1.6,4.1-2.8c1.2-1.3,2.2-2.8,2.8-4.4c0.7-1.8,1.1-3.8,1-5.7c0-2-0.3-3.9-1-5.8
19
+ C88.2,17.1,87.2,15.6,86,14.3L86,14.3z M81.3,27c-0.3,0.7-0.7,1.4-1.2,2c-1.1,1.2-2.6,1.8-4.2,1.8c-0.8,0-1.6-0.2-2.3-0.5
20
+ c-0.7-0.3-1.4-0.8-1.9-1.3c-0.5-0.6-1-1.3-1.3-2c-0.6-1.6-0.6-3.3,0-4.9c0.3-0.7,0.7-1.4,1.3-2c0.6-0.6,1.2-1,1.9-1.4
21
+ c0.7-0.3,1.5-0.5,2.3-0.5c0.8,0,1.6,0.2,2.4,0.5c0.7,0.3,1.3,0.8,1.9,1.3c0.5,0.6,0.9,1.2,1.2,2C81.9,23.6,81.9,25.4,81.3,27
22
+ L81.3,27z M118.5,11.3h-5.3c-0.4,0-0.7,0.2-1,0.4c-0.3,0.3-0.4,0.7-0.4,1.1v0.6c-0.7-0.8-1.5-1.4-2.4-1.8c-1.4-0.7-3-1-4.6-1
23
+ c-3.4,0-6.7,1.4-9.1,3.8c-1.2,1.3-2.2,2.8-2.9,4.4c-0.8,1.8-1.1,3.8-1.1,5.8c0,2,0.3,4,1.1,5.8c0.7,1.6,1.7,3.1,2.9,4.4
24
+ c2.4,2.4,5.6,3.8,9,3.8c1.6,0,3.2-0.3,4.6-1c0.9-0.5,1.8-1.1,2.4-1.8v0.7c0,0.4,0.2,0.7,0.4,1c0.3,0.2,0.6,0.4,1,0.4h5.3
25
+ c0.4,0,0.7-0.2,1-0.4c0.3-0.3,0.4-0.6,0.4-1V12.8c0-0.4-0.1-0.7-0.4-1C119.2,11.4,118.9,11.3,118.5,11.3L118.5,11.3z M111.3,26.9
26
+ c-0.3,0.7-0.7,1.4-1.2,2c-0.5,0.5-1.2,1-1.9,1.4c-1.5,0.7-3.2,0.7-4.7,0c-0.7-0.3-1.4-0.8-1.9-1.4c-0.5-0.6-1-1.3-1.2-2
27
+ c-0.6-1.6-0.6-3.3,0-4.9c0.3-0.7,0.7-1.4,1.2-2c0.5-0.6,1.2-1,1.9-1.4c1.5-0.7,3.2-0.7,4.7,0c0.7,0.3,1.3,0.8,1.9,1.3
28
+ c0.5,0.6,0.9,1.2,1.2,2C112,23.6,112,25.4,111.3,26.9L111.3,26.9z M171.2,23.8c-0.8-0.7-1.6-1.2-2.6-1.6c-1-0.4-2-0.7-3.1-0.9
29
+ l-4-0.8c-1-0.2-1.8-0.5-2.2-0.8c-0.3-0.2-0.5-0.6-0.5-1c0-0.4,0.2-0.8,0.8-1.1c0.7-0.4,1.5-0.6,2.2-0.5c1,0,2.1,0.2,3,0.6
30
+ c0.9,0.4,1.8,0.9,2.6,1.4c1.2,0.7,2.2,0.6,2.9-0.2l1.9-2.2c0.4-0.4,0.6-0.9,0.6-1.4c0-0.5-0.3-1.1-0.7-1.4
31
+ c-0.8-0.7-2.1-1.5-3.9-2.2c-1.7-0.7-3.9-1.1-6.5-1.1c-1.6,0-3.1,0.2-4.6,0.7c-1.3,0.4-2.5,1-3.5,1.8c-1,0.7-1.7,1.7-2.3,2.8
32
+ c-0.5,1.1-0.8,2.2-0.8,3.4c0,2.2,0.7,4,2,5.3c1.3,1.3,3,2.2,5.2,2.6l4.2,0.9c0.9,0.2,1.8,0.4,2.7,0.8c0.5,0.2,0.8,0.6,0.8,1.2
33
+ c0,0.5-0.2,0.9-0.8,1.3c-0.5,0.4-1.4,0.6-2.5,0.6c-1.1,0-2.3-0.2-3.3-0.7c-1-0.5-1.9-1.1-2.7-1.8c-0.4-0.3-0.8-0.5-1.2-0.7
34
+ c-0.5-0.1-1.1,0-1.7,0.5l-2.3,1.8c-0.6,0.5-1,1.3-0.8,2c0.1,0.8,0.8,1.5,1.9,2.4c2.9,2,6.4,3,9.9,2.9c1.6,0,3.3-0.2,4.8-0.7
35
+ c1.4-0.4,2.6-1,3.7-1.9c1-0.8,1.9-1.8,2.4-2.9c0.6-1.1,0.8-2.3,0.8-3.6c0-1.1-0.2-2.2-0.7-3.3C172.5,25.3,171.9,24.5,171.2,23.8
36
+ L171.2,23.8z M194.2,30.2c-0.2-0.4-0.7-0.7-1.2-0.8c-0.5,0-1,0.2-1.4,0.4c-0.6,0.4-1.4,0.7-2.2,0.7c-0.2,0-0.5,0-0.8-0.1
37
+ c-0.3,0-0.5-0.2-0.7-0.4c-0.2-0.2-0.4-0.5-0.6-0.8c-0.2-0.4-0.3-0.9-0.2-1.4v-9.6h6.9c0.4,0,0.8-0.2,1.1-0.5
38
+ c0.3-0.3,0.5-0.6,0.5-1v-4.1c0-0.4-0.2-0.8-0.5-1c-0.3-0.3-0.6-0.4-1-0.4h-6.9V4.7c0-0.4-0.1-0.8-0.4-1c-0.3-0.3-0.6-0.4-1-0.4
39
+ h-5.3c-0.4,0-0.8,0.1-1,0.4c-0.3,0.3-0.4,0.7-0.4,1v6.6h-3c-0.4,0-0.8,0.2-1,0.5c-0.3,0.3-0.4,0.6-0.4,1v4.1c0,0.4,0.1,0.7,0.4,1
40
+ c0.2,0.3,0.6,0.5,1,0.5h3v11.4c0,1.4,0.2,2.7,0.8,3.9c0.5,1,1.2,1.9,2,2.6c0.8,0.7,1.8,1.2,2.9,1.5c1.1,0.3,2.2,0.5,3.3,0.5
41
+ c1.5,0,2.9-0.2,4.3-0.7c1.3-0.4,2.5-1.2,3.4-2.1c0.6-0.6,0.7-1.6,0.2-2.3L194.2,30.2z M223.2,11.3h-5.3c-0.4,0-0.7,0.2-1,0.4
42
+ c-0.3,0.3-0.4,0.7-0.4,1.1v0.6c-0.7-0.8-1.5-1.4-2.4-1.8c-1.4-0.7-3-1-4.6-1c-3.4,0-6.6,1.4-9,3.8c-1.2,1.3-2.2,2.8-2.9,4.4
43
+ c-0.8,1.8-1.1,3.8-1.1,5.7c0,2,0.3,3.9,1.1,5.8c0.7,1.6,1.7,3.1,2.9,4.4c2.4,2.4,5.6,3.8,9,3.8c1.6,0,3.2-0.3,4.6-1
44
+ c0.9-0.5,1.8-1.1,2.4-1.8v0.7c0,0.4,0.1,0.7,0.4,1c0.3,0.3,0.6,0.4,1,0.4h5.3c0.8,0,1.4-0.6,1.4-1.4V12.8c0-0.4-0.1-0.7-0.4-1
45
+ C223.9,11.4,223.6,11.3,223.2,11.3L223.2,11.3z M216,26.9c-0.3,0.7-0.7,1.4-1.2,2c-0.6,0.5-1.2,1-1.9,1.4
46
+ c-0.7,0.3-1.6,0.5-2.4,0.5c-0.8,0-1.6-0.2-2.3-0.5c-0.7-0.3-1.4-0.8-1.9-1.4c-0.5-0.6-1-1.3-1.2-2c-0.6-1.6-0.6-3.3,0-4.9
47
+ c0.3-0.7,0.7-1.4,1.2-2c0.6-0.6,1.2-1,1.9-1.4c0.7-0.3,1.5-0.5,2.3-0.5c0.8,0,1.6,0.2,2.4,0.5c0.7,0.3,1.3,0.8,1.9,1.3
48
+ c0.5,0.6,1,1.2,1.2,2C216.7,23.6,216.7,25.4,216,26.9L216,26.9z M252.2,29.8l-3-2.3c-0.6-0.5-1.1-0.6-1.6-0.4
49
+ c-0.4,0.2-0.8,0.5-1.1,0.8c-0.7,0.8-1.4,1.5-2.3,2.1c-0.9,0.5-1.9,0.8-3,0.7c-1.2,0-2.3-0.3-3.3-1c-1-0.7-1.7-1.7-2.1-2.8
50
+ c-0.3-0.8-0.4-1.6-0.4-2.4c0-0.8,0.1-1.7,0.4-2.5c0.3-0.7,0.7-1.4,1.2-2c0.6-0.6,1.2-1,1.9-1.3c0.7-0.3,1.6-0.5,2.4-0.5
51
+ c1,0,2.1,0.2,2.9,0.7c0.9,0.6,1.6,1.3,2.3,2.1c0.3,0.3,0.7,0.6,1.1,0.8c0.5,0.2,1,0.1,1.6-0.4l3-2.3c0.4-0.3,0.7-0.6,0.8-1
52
+ c0.2-0.5,0.1-1-0.1-1.4c-1.2-1.8-2.8-3.3-4.7-4.4c-2-1.1-4.4-1.7-7.1-1.7c-1.9,0-3.8,0.4-5.5,1.1c-1.7,0.7-3.2,1.7-4.5,3
53
+ c-1.3,1.3-2.3,2.8-3,4.4c-1.4,3.5-1.4,7.5,0,11c0.7,1.7,1.7,3.2,3,4.4c2.7,2.6,6.2,4,10,4c2.7,0,5.1-0.6,7.1-1.7
54
+ c1.9-1.1,3.5-2.6,4.7-4.4c0.3-0.4,0.3-0.9,0.1-1.4C252.8,30.5,252.5,30.1,252.2,29.8L252.2,29.8z M280.3,35.3l-8.3-12.3l7.1-9.4
55
+ c0.3-0.4,0.4-1,0.3-1.5c-0.1-0.4-0.5-0.8-1.3-0.8h-5.6c-0.3,0-0.6,0.1-0.9,0.2c-0.4,0.2-0.6,0.4-0.8,0.8l-5.7,8h-1.4V1.4
56
+ c0-0.4-0.1-0.7-0.4-1c-0.3-0.3-0.6-0.4-1-0.4h-5.3c-0.4,0-0.7,0.1-1,0.4c-0.3,0.3-0.4,0.6-0.4,1v34.8c0,0.4,0.2,0.8,0.4,1
57
+ c0.3,0.3,0.6,0.4,1,0.4h5.3c0.4,0,0.7-0.2,1-0.4c0.3-0.3,0.4-0.6,0.4-1V27h1.5l6.2,9.6c0.4,0.7,1.1,1.1,1.8,1.1h5.9
58
+ c0.9,0,1.3-0.4,1.4-0.8C280.7,36.4,280.6,35.8,280.3,35.3L280.3,35.3z M148.5,11.3h-5.9c-0.4,0-0.9,0.1-1.2,0.5
59
+ c-0.3,0.3-0.5,0.6-0.5,1L136.5,29h-1.1l-4.7-16.3c-0.1-0.3-0.2-0.7-0.5-1c-0.3-0.3-0.7-0.5-1.1-0.5h-6.1c-0.8,0-1.3,0.3-1.5,0.8
60
+ c-0.2,0.5-0.2,1,0,1.4l7.5,22.9c0.1,0.3,0.3,0.7,0.6,0.9c0.3,0.3,0.7,0.4,1.1,0.4h3.2l-0.3,0.7l-0.7,2.1c-0.2,0.6-0.6,1.2-1.2,1.7
61
+ c-0.5,0.4-1.1,0.6-1.8,0.6c-0.5,0-1.1-0.1-1.6-0.3c-0.5-0.2-1-0.5-1.4-0.8c-0.4-0.3-0.9-0.4-1.3-0.4h-0.1c-0.6,0-1.1,0.3-1.3,0.8
62
+ l-1.9,2.8c-0.8,1.2-0.3,2,0.2,2.4c1,0.9,2.2,1.6,3.5,2.1c1.4,0.5,2.9,0.7,4.5,0.7c2.7,0,5-0.7,6.7-2.2c1.8-1.6,3.1-3.7,3.8-6
63
+ l8.7-28.3c0.2-0.5,0.2-1,0-1.5C149.7,11.6,149.3,11.3,148.5,11.3L148.5,11.3z M148.5,11.3">
64
+ </path>
65
+ </g>
66
+ </g>
67
+ </svg>
@@ -0,0 +1,70 @@
1
+ 'use client';
2
+
3
+ /**
4
+ * Storefront Provider
5
+ *
6
+ * All-in-one provider that wraps ApiClient and Cart providers.
7
+ * This simplifies setup - just wrap your app with this one provider.
8
+ *
9
+ * @example
10
+ * ```tsx
11
+ * // app/layout.tsx
12
+ * import { StorefrontProvider } from '@oms/storefront-ui';
13
+ *
14
+ * export default function RootLayout({ children }) {
15
+ * return (
16
+ * <html>
17
+ * <body>
18
+ * <StorefrontProvider brandSlug="my-brand">
19
+ * {children}
20
+ * </StorefrontProvider>
21
+ * </body>
22
+ * </html>
23
+ * );
24
+ * }
25
+ * ```
26
+ */
27
+
28
+ import { ReactNode } from 'react';
29
+ import { ApiClientProvider } from '@oms/api-client';
30
+ import { CartProvider, CartProviderProps } from '../contexts/CartContext';
31
+
32
+ export interface StorefrontProviderProps {
33
+ /** Your children components */
34
+ children: ReactNode;
35
+ /** Brand slug for cart operations */
36
+ brandSlug: string;
37
+ /** Optional: Override the default API URL (defaults to https://oms-api.instock.ng) */
38
+ apiUrl?: string;
39
+ /** Optional: Initial cart ID (overrides localStorage) */
40
+ initialCartId?: string;
41
+ /** Optional: Props to pass to the ShoppingCart component */
42
+ shoppingCartProps?: CartProviderProps['shoppingCartProps'];
43
+ }
44
+
45
+ /**
46
+ * All-in-one provider for @oms/storefront-ui
47
+ *
48
+ * Wraps your app with:
49
+ * - ApiClientProvider (for API configuration and React Query)
50
+ * - CartProvider (for cart state management)
51
+ */
52
+ export function StorefrontProvider({
53
+ children,
54
+ brandSlug,
55
+ apiUrl,
56
+ initialCartId,
57
+ shoppingCartProps,
58
+ }: StorefrontProviderProps) {
59
+ return (
60
+ <ApiClientProvider {...(apiUrl && { baseURL: apiUrl })}>
61
+ <CartProvider
62
+ brandSlug={brandSlug}
63
+ initialCartId={initialCartId}
64
+ shoppingCartProps={shoppingCartProps}
65
+ >
66
+ {children}
67
+ </CartProvider>
68
+ </ApiClientProvider>
69
+ );
70
+ }
package/src/styles.css ADDED
@@ -0,0 +1 @@
1
+ @import 'tailwindcss';
@@ -0,0 +1,424 @@
1
+ /**
2
+ * MockCartProvider for Storybook
3
+ *
4
+ * Provides fake cart data for UI prototyping without needing a real backend.
5
+ */
6
+
7
+ import { ReactNode, useState, useCallback } from 'react';
8
+ import { CartContext } from '../contexts/CartContext';
9
+ import { ShoppingCart } from '../components/ShoppingCart';
10
+ import type { Cart, PublicOrderResponse } from '@oms/api-client';
11
+ import type { ShoppingCartProps } from '../components/ShoppingCart';
12
+
13
+ // Use the proper type from the API client
14
+ type CheckoutResponse = PublicOrderResponse;
15
+
16
+ const mockCart: Cart = {
17
+ id: 'mock-cart-123',
18
+ expiresAt: new Date(Date.now() + 1000 * 60 * 60 * 24).toISOString(),
19
+ brand: {
20
+ id: 'mock-brand-456',
21
+ name: 'Demo Store',
22
+ slug: 'demo-store',
23
+ logoUrl: 'https://images.unsplash.com/photo-1599305445671-ac291c95aaa9?w=200&h=80&fit=crop',
24
+ siteUrl: 'https://demo-store.com',
25
+ domain: 'f',
26
+ createdAt: new Date().toISOString(),
27
+ updatedAt: new Date().toISOString(),
28
+ deletedAt: null,
29
+ },
30
+ customerPhone: null,
31
+ customerEmail: null,
32
+ customerFirstName: null,
33
+ customerLastName: null,
34
+ availablePaymentMethods: ['cod'],
35
+ deliveryZone: null,
36
+ recoveryAttempts: 0,
37
+ lastRecoveryAttemptAt: null,
38
+ recoveryDiscountCode: null,
39
+ items: [
40
+ {
41
+ id: 'item-1',
42
+ variant: {
43
+ id: 'variant-1',
44
+ productId: 'product-1',
45
+ name: 'Medium - Blue',
46
+ price: 29.99,
47
+ sku: 'TSHIRT-MD-BLUE',
48
+ thumbnailUrl: 'https://images.unsplash.com/photo-1521572163474-6864f9cf17ab?w=400',
49
+ trackInventory: true,
50
+ lowStockThreshold: 10,
51
+ isActive: true,
52
+ createdAt: new Date().toISOString(),
53
+ updatedAt: new Date().toISOString(),
54
+ deletedAt: null,
55
+ product: {
56
+ id: 'product-1',
57
+ name: 'Premium Cotton T-Shirt',
58
+ slug: 'premium-cotton-t-shirt',
59
+ brandId: 'mock-brand-456',
60
+ isActive: true,
61
+ description: 'Comfortable premium cotton t-shirt',
62
+ thumbnailUrl: 'https://images.unsplash.com/photo-1521572163474-6864f9cf17ab?w=400',
63
+ createdAt: new Date().toISOString(),
64
+ updatedAt: new Date().toISOString(),
65
+ deletedAt: null,
66
+ quantityDiscounts: null,
67
+ },
68
+ },
69
+ quantity: 2,
70
+ basePrice: 29.99,
71
+ discountPercent: 0,
72
+ finalPrice: 25.99,
73
+ subtotal: 51.98,
74
+ },
75
+ {
76
+ id: 'item-2',
77
+ variant: {
78
+ id: 'variant-2',
79
+ productId: 'product-2',
80
+ name: 'Large - Black',
81
+ price: 34.99,
82
+ sku: 'JEANS-LG-BLACK',
83
+ thumbnailUrl: 'https://images.unsplash.com/photo-1542272604-787c3835535d?w=400',
84
+ trackInventory: true,
85
+ lowStockThreshold: 10,
86
+ isActive: true,
87
+ createdAt: new Date().toISOString(),
88
+ updatedAt: new Date().toISOString(),
89
+ deletedAt: null,
90
+ product: {
91
+ id: 'product-2',
92
+ name: 'Slim Fit Jeans',
93
+ slug: 'slim-fit-jeans',
94
+ brandId: 'mock-brand-456',
95
+ isActive: true,
96
+ description: 'Modern slim fit jeans',
97
+ thumbnailUrl: 'https://images.unsplash.com/photo-1542272604-787c3835535d?w=400',
98
+ createdAt: new Date().toISOString(),
99
+ updatedAt: new Date().toISOString(),
100
+ deletedAt: null,
101
+ quantityDiscounts: null,
102
+ },
103
+ },
104
+ quantity: 1,
105
+ basePrice: 34.99,
106
+ discountPercent: 0,
107
+ finalPrice: 34.99,
108
+ subtotal: 34.99,
109
+ },
110
+ ],
111
+ pricing: {
112
+ subtotal: 94.97,
113
+ deliveryCharge: 0,
114
+ discount: null,
115
+ total: 94.97,
116
+ },
117
+ createdAt: new Date().toISOString(),
118
+ updatedAt: new Date().toISOString(),
119
+ convertedToOrderId: null,
120
+ wasRecovered: false,
121
+ recoveryUrl: 'https://demo-store.com?cartId=mock-cart-123',
122
+ };
123
+
124
+ interface MockCartProviderProps {
125
+ children: ReactNode;
126
+ cart?: Partial<Cart>;
127
+ isLoading?: boolean;
128
+ error?: Error | null;
129
+ /** Props to pass to the ShoppingCart component */
130
+ shoppingCartProps?: Omit<ShoppingCartProps, 'isOpen' | 'onClose'>;
131
+ /** Whether to render the ShoppingCart component (default: true) */
132
+ renderCart?: boolean;
133
+ }
134
+
135
+ export function MockCartProvider({
136
+ children,
137
+ cart: cartOverride,
138
+ isLoading = false,
139
+ error = null,
140
+ shoppingCartProps,
141
+ renderCart = true,
142
+ }: MockCartProviderProps) {
143
+ const [cart, setCart] = useState<Cart>({
144
+ ...mockCart,
145
+ ...cartOverride,
146
+ items: cartOverride?.items ?? mockCart.items,
147
+ pricing: {
148
+ ...mockCart.pricing,
149
+ ...cartOverride?.pricing,
150
+ },
151
+ brand: {
152
+ ...mockCart.brand,
153
+ ...cartOverride?.brand,
154
+ },
155
+ } as Cart);
156
+
157
+ const [isOpen, setIsOpen] = useState(false);
158
+ const [checkoutData, setCheckoutData] = useState<CheckoutResponse | undefined>(undefined);
159
+ const [isCheckoutSuccess, setIsCheckoutSuccess] = useState(false);
160
+ const [isCheckoutPending, setIsCheckoutPending] = useState(false);
161
+
162
+ const open = useCallback(() => {
163
+ setIsOpen(true);
164
+ }, []);
165
+
166
+ const close = useCallback(() => {
167
+ setIsOpen(false);
168
+ }, []);
169
+
170
+ // Mock mutations
171
+ const mockUpdateCartMutation = {
172
+ mutate: (data: any) => {
173
+ console.log('Mock updateCartMutation:', data);
174
+ setCart(prev => ({
175
+ ...prev,
176
+ ...data,
177
+ }));
178
+ },
179
+ mutateAsync: async (data: any) => {
180
+ console.log('Mock updateCartMutation:', data);
181
+ setCart(prev => ({
182
+ ...prev,
183
+ ...data,
184
+ }));
185
+ return cart;
186
+ },
187
+ isIdle: true,
188
+ isPending: false,
189
+ isError: false,
190
+ isSuccess: false,
191
+ data: undefined,
192
+ error: null,
193
+ reset: () => {},
194
+ status: 'idle' as const,
195
+ failureCount: 0,
196
+ failureReason: null,
197
+ isPaused: false,
198
+ variables: undefined,
199
+ submittedAt: 0,
200
+ context: undefined,
201
+ };
202
+
203
+ const createCheckoutResponse = useCallback((data: any): CheckoutResponse => {
204
+ return {
205
+ canConfirm: true,
206
+ confirmationMessage: '-',
207
+ id: `mock-order-${Date.now()}`,
208
+ brandId: cart.brand.id,
209
+ orderNumber: Math.floor(Math.random() * 10000),
210
+ firstName: data.firstName,
211
+ lastName: data.lastName,
212
+ phone: data.phone,
213
+ email: data.email || null,
214
+ address: data.address,
215
+ city: data.city,
216
+ deliveryZoneId: cart.deliveryZone?.id || 'mock-zone-1',
217
+ deliveryCharge: 1500,
218
+ estimatedDays: 3,
219
+ totalPrice: cart.pricing.total + 1500,
220
+ discountCodeId: null,
221
+ discountAmount: null,
222
+ paymentMethod: data.paymentMethod,
223
+ paystackReference: null,
224
+ status: 'pending' as const,
225
+ cancellationReason: null,
226
+ createdAt: new Date().toISOString(),
227
+ updatedAt: new Date().toISOString(),
228
+ deletedAt: null,
229
+ lastRecoveryAttemptAt: null,
230
+ prospectReason: null,
231
+ prospectSince: null,
232
+ recoveryAttempts: 0,
233
+ recoveryDiscountCodeId: null,
234
+ userActionToken: 'mock-token-123',
235
+ wasRecovered: false,
236
+ // Computed fields from formatOrderResponse
237
+ subtotal: cart.pricing.subtotal,
238
+ // Relations
239
+ brand: cart.brand,
240
+ deliveryZone: {
241
+ id: cart.deliveryZone?.id || 'mock-zone-1',
242
+ name: cart.deliveryZone?.name || 'Mock Zone',
243
+ brandId: cart.brand.id,
244
+ deliveryCost: 1500,
245
+ freeShippingThreshold: null,
246
+ allowCOD: true,
247
+ allowOnline: true,
248
+ waybillOnly: false,
249
+ estimatedDays: 3,
250
+ createdAt: new Date().toISOString(),
251
+ updatedAt: new Date().toISOString(),
252
+ deletedAt: null,
253
+ state: {
254
+ id: 'mock-state-1',
255
+ name: 'Mock State',
256
+ isActive: true,
257
+ createdAt: new Date().toISOString(),
258
+ updatedAt: new Date().toISOString(),
259
+ deletedAt: null,
260
+ },
261
+ isActive: true,
262
+ stateId: 'mock-state-1',
263
+ },
264
+ items: cart.items.map(item => ({
265
+ id: item.id,
266
+ orderId: `mock-order-${Date.now()}`,
267
+ variantId: item.variant.id,
268
+ warehouseId: 'mock-warehouse-1',
269
+ priceAtPurchase: item.finalPrice,
270
+ quantity: item.quantity,
271
+ variant: item.variant,
272
+ warehouse: {
273
+ id: 'mock-warehouse-1',
274
+ name: 'Main Warehouse',
275
+ isActive: true,
276
+ address: null,
277
+ city: null,
278
+ state: null,
279
+ createdAt: new Date().toISOString(),
280
+ updatedAt: new Date().toISOString(),
281
+ deletedAt: null,
282
+ },
283
+ })),
284
+ } satisfies CheckoutResponse;
285
+ }, [cart]);
286
+
287
+ const mockCheckoutMutation = {
288
+ mutate: (data: any) => {
289
+ console.log('Mock checkoutMutation.mutate:', data);
290
+ setIsCheckoutPending(true);
291
+ // Simulate async operation and update state
292
+ setTimeout(() => {
293
+ const response = createCheckoutResponse(data);
294
+ setCheckoutData(response);
295
+ setIsCheckoutSuccess(true);
296
+ setIsCheckoutPending(false);
297
+ }, 500);
298
+ },
299
+ mutateAsync: async (data: any) => {
300
+ console.log('Mock checkoutMutation.mutateAsync:', data);
301
+ setIsCheckoutPending(true);
302
+ // Simulate network delay
303
+ await new Promise(resolve => setTimeout(resolve, 500));
304
+ const response = createCheckoutResponse(data);
305
+ setCheckoutData(response);
306
+ setIsCheckoutSuccess(true);
307
+ setIsCheckoutPending(false);
308
+ return response;
309
+ },
310
+ isIdle: !isCheckoutSuccess && !checkoutData && !isCheckoutPending,
311
+ isPending: isCheckoutPending,
312
+ isError: false,
313
+ isSuccess: isCheckoutSuccess,
314
+ data: checkoutData,
315
+ error: null,
316
+ reset: () => {
317
+ setCheckoutData(undefined);
318
+ setIsCheckoutSuccess(false);
319
+ setIsCheckoutPending(false);
320
+ },
321
+ status: isCheckoutPending ? 'pending' as const : isCheckoutSuccess ? 'success' as const : 'idle' as const,
322
+ failureCount: 0,
323
+ failureReason: null,
324
+ isPaused: false,
325
+ variables: undefined,
326
+ submittedAt: 0,
327
+ context: undefined,
328
+ };
329
+
330
+ const contextValue = {
331
+ cart,
332
+ cartId: cart.id,
333
+ isLoading,
334
+ error,
335
+ updateCartMutation: mockUpdateCartMutation as any,
336
+ checkoutMutation: mockCheckoutMutation as any,
337
+
338
+ addItem: async (variantId: string, quantity: number) => {
339
+ console.log('Mock addItem:', { variantId, quantity });
340
+ },
341
+
342
+ updateItem: async (itemId: string, quantity: number) => {
343
+ console.log('Mock updateItem:', { itemId, quantity });
344
+ setCart(prev => ({
345
+ ...prev,
346
+ items: prev.items.map(item =>
347
+ item.id === itemId ? { ...item, quantity, subtotal: item.finalPrice * quantity } : item
348
+ ),
349
+ }));
350
+ },
351
+
352
+ removeItem: async (itemId: string) => {
353
+ console.log('Mock removeItem:', itemId);
354
+ setCart(prev => ({
355
+ ...prev,
356
+ items: prev.items.filter(item => item.id !== itemId),
357
+ }));
358
+ },
359
+
360
+ applyDiscount: async (code: string) => {
361
+ console.log('Mock applyDiscount:', code);
362
+ if (code === 'SAVE10') {
363
+ const discountAmount = Math.round(cart.pricing.subtotal * 0.1 * 100) / 100;
364
+ setCart(prev => ({
365
+ ...prev,
366
+ pricing: {
367
+ ...prev.pricing,
368
+ discount: {
369
+ code,
370
+ type: 'percentage',
371
+ value: 10,
372
+ amount: discountAmount,
373
+ description: '10% off your order',
374
+ },
375
+ total: prev.pricing.subtotal - discountAmount,
376
+ },
377
+ }));
378
+ } else {
379
+ throw new Error('Invalid discount code');
380
+ }
381
+ },
382
+
383
+ removeDiscount: async () => {
384
+ console.log('Mock removeDiscount');
385
+ setCart(prev => ({
386
+ ...prev,
387
+ pricing: {
388
+ ...prev.pricing,
389
+ discount: null,
390
+ total: prev.pricing.subtotal,
391
+ },
392
+ }));
393
+ },
394
+
395
+ clearCart: () => {
396
+ console.log('Mock clearCart');
397
+ setCart(prev => ({
398
+ ...prev,
399
+ items: [],
400
+ }));
401
+ },
402
+
403
+ refetch: () => {
404
+ console.log('Mock refetch');
405
+ },
406
+
407
+ isOpen,
408
+ open,
409
+ close,
410
+ };
411
+
412
+ return (
413
+ <CartContext.Provider value={contextValue}>
414
+ {children}
415
+ {renderCart && (
416
+ <ShoppingCart
417
+ isOpen={isOpen}
418
+ onClose={close}
419
+ {...shoppingCartProps}
420
+ />
421
+ )}
422
+ </CartContext.Provider>
423
+ );
424
+ }
@@ -0,0 +1,12 @@
1
+ /// <reference types="vite/client" />
2
+
3
+ declare module '*.svg' {
4
+ const content: string;
5
+ export default content;
6
+ }
7
+
8
+ declare module '*.svg?react' {
9
+ import * as React from 'react';
10
+ const ReactComponent: React.FunctionComponent<React.SVGProps<SVGSVGElement>>;
11
+ export default ReactComponent;
12
+ }