bsign-customization-full 0.0.1

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 (126) hide show
  1. package/.env +2 -0
  2. package/components.json +21 -0
  3. package/dist/colors/anthracite-gray.webp +0 -0
  4. package/dist/colors/anthracite-gray_50x50.png +0 -0
  5. package/dist/colors/dark-wenge.webp +0 -0
  6. package/dist/colors/dark-wenge_50x50.png +0 -0
  7. package/dist/colors/indian-rosewood.webp +0 -0
  8. package/dist/colors/indian-rosewood_50x50.png +0 -0
  9. package/dist/colors/natural-wood.webp +0 -0
  10. package/dist/colors/natural-wood_50x50.png +0 -0
  11. package/dist/colors/redwood.webp +0 -0
  12. package/dist/colors/redwood_50x50.png +0 -0
  13. package/dist/colors/walnut.webp +0 -0
  14. package/dist/colors/walnut_50x50.png +0 -0
  15. package/dist/html2canvas.esm-BJ_egzt0.js +4802 -0
  16. package/dist/index-Dw5Zc1iD.js +33162 -0
  17. package/dist/index.es-Co1KNpGS.js +6681 -0
  18. package/dist/logo.png +0 -0
  19. package/dist/purify.es-CKpD2xIC.js +552 -0
  20. package/dist/sign-constructor.es.js +4 -0
  21. package/dist/sign-constructor.iife.js +171 -0
  22. package/dist/size-guide.webp +0 -0
  23. package/dist/size.webp +0 -0
  24. package/dist/templates/assets/modern/rectangle-black.webp +0 -0
  25. package/dist/templates/assets/modern/rectangle-white.webp +0 -0
  26. package/dist/templates/assets/modern/square-black.webp +0 -0
  27. package/dist/templates/assets/modern/square-white.webp +0 -0
  28. package/dist/templates/assets/wave.webp +0 -0
  29. package/dist/templates/jure.webp +0 -0
  30. package/dist/templates/modern.webp +0 -0
  31. package/dist/templates/sherwood.webp +0 -0
  32. package/dist/templates/wave.webp +0 -0
  33. package/eslint.config.js +23 -0
  34. package/index.html +13 -0
  35. package/modern-debug.svg +39 -0
  36. package/package.json +62 -0
  37. package/public/colors/anthracite-gray.webp +0 -0
  38. package/public/colors/anthracite-gray_50x50.png +0 -0
  39. package/public/colors/dark-wenge.webp +0 -0
  40. package/public/colors/dark-wenge_50x50.png +0 -0
  41. package/public/colors/indian-rosewood.webp +0 -0
  42. package/public/colors/indian-rosewood_50x50.png +0 -0
  43. package/public/colors/natural-wood.webp +0 -0
  44. package/public/colors/natural-wood_50x50.png +0 -0
  45. package/public/colors/redwood.webp +0 -0
  46. package/public/colors/redwood_50x50.png +0 -0
  47. package/public/colors/walnut.webp +0 -0
  48. package/public/colors/walnut_50x50.png +0 -0
  49. package/public/logo.png +0 -0
  50. package/public/size-guide.webp +0 -0
  51. package/public/size.webp +0 -0
  52. package/public/templates/assets/modern/rectangle-black.webp +0 -0
  53. package/public/templates/assets/modern/rectangle-white.webp +0 -0
  54. package/public/templates/assets/modern/square-black.webp +0 -0
  55. package/public/templates/assets/modern/square-white.webp +0 -0
  56. package/public/templates/assets/wave.webp +0 -0
  57. package/public/templates/jure.webp +0 -0
  58. package/public/templates/modern.webp +0 -0
  59. package/public/templates/sherwood.webp +0 -0
  60. package/public/templates/wave.webp +0 -0
  61. package/src/App.css +43 -0
  62. package/src/AppDemo2.tsx +257 -0
  63. package/src/components/cart-panel.tsx +170 -0
  64. package/src/components/cart-preview.tsx +356 -0
  65. package/src/components/cart-view.tsx +113 -0
  66. package/src/components/constructure-menu.tsx +37 -0
  67. package/src/components/header.tsx +214 -0
  68. package/src/components/heading.tsx +28 -0
  69. package/src/components/icons.tsx +54 -0
  70. package/src/components/import-file-modal.tsx +252 -0
  71. package/src/components/layers/grid-view.tsx +29 -0
  72. package/src/components/layers/image-layer.tsx +128 -0
  73. package/src/components/layers/layer-forms/material-form.tsx +53 -0
  74. package/src/components/layers/layer-forms/size-form.tsx +69 -0
  75. package/src/components/layers/layer-forms/template-form.tsx +39 -0
  76. package/src/components/layers/layer-forms/text-form.tsx +477 -0
  77. package/src/components/layers/layers-container.tsx +259 -0
  78. package/src/components/layers/text-layer.tsx +128 -0
  79. package/src/components/movable-item.tsx +228 -0
  80. package/src/components/preview.tsx +258 -0
  81. package/src/components/resize-button.tsx +83 -0
  82. package/src/components/size-guide-modal.tsx +47 -0
  83. package/src/components/size-guide.tsx +98 -0
  84. package/src/components/ui/button-group.tsx +83 -0
  85. package/src/components/ui/button.tsx +60 -0
  86. package/src/components/ui/checkbox.tsx +30 -0
  87. package/src/components/ui/dialog.tsx +151 -0
  88. package/src/components/ui/input-group.tsx +168 -0
  89. package/src/components/ui/input.tsx +21 -0
  90. package/src/components/ui/label.tsx +22 -0
  91. package/src/components/ui/popover.tsx +54 -0
  92. package/src/components/ui/progress.tsx +28 -0
  93. package/src/components/ui/radio-group.tsx +43 -0
  94. package/src/components/ui/scroll-area.tsx +56 -0
  95. package/src/components/ui/select.tsx +191 -0
  96. package/src/components/ui/separator.tsx +25 -0
  97. package/src/components/ui/sheet.tsx +141 -0
  98. package/src/components/ui/slider.tsx +61 -0
  99. package/src/components/ui/spinner.tsx +15 -0
  100. package/src/components/ui/textarea.tsx +18 -0
  101. package/src/components/ui/toggle-group.tsx +73 -0
  102. package/src/components/ui/toggle.tsx +45 -0
  103. package/src/components/ui/tooltip.tsx +67 -0
  104. package/src/fonts/BEBASNEUE-REGULAR.TTF +0 -0
  105. package/src/fonts/Braille-Regular.ttf +0 -0
  106. package/src/fonts/GOTHICB.TTF +0 -0
  107. package/src/hooks/use-mobile.ts +23 -0
  108. package/src/hooks/use-resize-constraints.ts +62 -0
  109. package/src/index.css +238 -0
  110. package/src/index.tsx +141 -0
  111. package/src/lib/cart-proposal-pdf.ts +350 -0
  112. package/src/lib/config-font.tsx +109 -0
  113. package/src/lib/config.ts +730 -0
  114. package/src/lib/pricing.ts +61 -0
  115. package/src/lib/type-checks.ts +47 -0
  116. package/src/lib/utils.ts +146 -0
  117. package/src/lib/widget-context.tsx +9 -0
  118. package/src/main.tsx +11 -0
  119. package/src/store/cart-store.ts +78 -0
  120. package/src/store/layers-store.ts +337 -0
  121. package/src/vite-env.d.ts +1 -0
  122. package/test/preview.html +37 -0
  123. package/tsconfig.app.json +33 -0
  124. package/tsconfig.json +13 -0
  125. package/tsconfig.node.json +25 -0
  126. package/vite.config.ts +38 -0
@@ -0,0 +1,170 @@
1
+ import { ChevronDown, ChevronUp, Edit, Trash, X } from "lucide-react";
2
+ import { useCartStore } from "../store/cart-store";
3
+ import { Button } from "./ui/button";
4
+ import { INITIAL_STATE, useLayersStore, type TLayersOption } from "../store/layers-store";
5
+ import type { LayrProps, TLayer } from "./preview";
6
+ import { createTemplateLayers, getDefaultTemplateOptions } from "../lib/config";
7
+ import { ButtonGroup } from "./ui/button-group";
8
+ import { useIsMobile } from "../hooks/use-mobile";
9
+ import { useState } from "react";
10
+
11
+ export default function CartPanel({ setConstructureSelectedItem }: {
12
+ setConstructureSelectedItem: (item: string) => void;
13
+ }) {
14
+ const { cart, init: setCart } = useCartStore()
15
+ const { init: setSign, setCartId, editOptions, clearAll, cartId } = useLayersStore();
16
+ const isMobile = useIsMobile();
17
+ const [mobileCartOpen, setMobileCartOpen] = useState(false);
18
+
19
+ const handleRemoveFromCart = (id: number) => {
20
+ const filtered = cart.filter(item => item.id !== id);
21
+ setCart(filtered);
22
+ if (filtered.length <= 0) {
23
+ const defaults = getDefaultTemplateOptions();
24
+ clearAll({
25
+ ...INITIAL_STATE,
26
+ layers: createTemplateLayers({
27
+ templateId: defaults.selectedTemplateId,
28
+ shapeId: defaults.selectedShapeId,
29
+ materialId: defaults.selectedMaterialId,
30
+ preferredSizeId: defaults.selectedSize.id,
31
+ }),
32
+ });
33
+ setCartId(undefined);
34
+ }
35
+ }
36
+
37
+ const handleChangeCurrentItem = (id: number, layers: TLayer<LayrProps>[], itemOptions: TLayersOption) => {
38
+ clearAll();
39
+ setSign(layers);
40
+ editOptions(itemOptions)
41
+ setCartId(id);
42
+ setConstructureSelectedItem('text')
43
+ }
44
+
45
+ return cart.length > 0 ? (
46
+ <>
47
+ {isMobile ? (
48
+ <>
49
+ {!mobileCartOpen && (
50
+ <div className="fixed right-4 bottom-[calc(10rem+env(safe-area-inset-bottom))] z-[130]">
51
+ <Button
52
+ variant="default"
53
+ size="sm"
54
+ className="rounded-full px-4 shadow-lg"
55
+ onClick={() => setMobileCartOpen(true)}
56
+ >
57
+ <ChevronUp className="size-4.5" />
58
+ {`Added signs (${cart.length})`}
59
+ </Button>
60
+ </div>
61
+ )}
62
+
63
+ {mobileCartOpen && (
64
+ <>
65
+ <button
66
+ type="button"
67
+ className="fixed inset-0 z-[120] bg-black/30"
68
+ onClick={() => setMobileCartOpen(false)}
69
+ aria-label="Close added signs panel"
70
+ />
71
+ <div className="fixed inset-x-0 bottom-0 z-[130] border-t border-[#D6D6D6] bg-white px-3 pt-2 pb-[calc(0.5rem+env(safe-area-inset-bottom))] shadow-[0_-12px_30px_rgba(0,0,0,0.2)]">
72
+ <div className="mb-2 flex items-center justify-between">
73
+ <p className="text-sm font-semibold">{`Added signs (${cart.length})`}</p>
74
+ <div className="flex items-center gap-1">
75
+ <Button
76
+ variant="ghost"
77
+ size="icon-sm"
78
+ className="rounded-full"
79
+ onClick={() => setMobileCartOpen(false)}
80
+ >
81
+ <ChevronDown className="size-4" />
82
+ </Button>
83
+ <Button
84
+ variant="ghost"
85
+ size="icon-sm"
86
+ className="rounded-full"
87
+ onClick={() => setMobileCartOpen(false)}
88
+ >
89
+ <X className="size-4" />
90
+ </Button>
91
+ </div>
92
+ </div>
93
+
94
+ <div className="no-scrollbar flex w-full gap-2 overflow-x-auto pb-1">
95
+ {cart.map(item => (
96
+ <div key={item.id} className={`relative shrink-0 flex flex-col gap-0.5 border px-3 py-2 ${item.id === cartId ? 'border-[#107E12]' : 'border-[#D6D6D6]'}`}>
97
+
98
+ {item.qty > 1 && <p className="text-xs bottom-0 right-0 absolute px-[7px]">{item.qty}</p>}
99
+
100
+ <img
101
+ src={item.img}
102
+ alt="Cart item"
103
+ className="h-16 w-16 rounded-md object-cover"
104
+ />
105
+ <div className="absolute top-0 right-0">
106
+ <ButtonGroup>
107
+ <Button variant={"ghost"} size={"icon-sm"} onClick={() => handleChangeCurrentItem(item.id, item.sign, item.options)}><Edit /></Button>
108
+ <Button
109
+ variant={"ghost"}
110
+ size={"icon-sm"}
111
+ className="hover:bg-red-700 rounded-r-none group"
112
+ onClick={() => handleRemoveFromCart(item.id)}
113
+ >
114
+ <Trash className="group-hover:text-white transition" />
115
+ </Button>
116
+ </ButtonGroup>
117
+ </div>
118
+ </div>
119
+ ))}
120
+ </div>
121
+ </div>
122
+ </>
123
+ )}
124
+ </>
125
+ ) : (
126
+ <div className="no-scrollbar flex w-full overflow-x-auto border-t border-[#D6D6D6] p-3 md:h-full md:min-h-0 md:w-auto md:self-stretch md:flex-col md:items-center md:overflow-y-auto md:overflow-x-hidden md:border-t-0 md:p-0">
127
+ {cart.map(item => (
128
+ <div key={item.id} className={`relative shrink-0 flex flex-col gap-0.5 border border-r-0 px-3 py-2 md:px-6 md:py-[14px] ${item.id === cartId ? 'border-[#107E12]' : 'border-[#D6D6D6]'}`}>
129
+ {/* {item.id === cartId && <p className="text-xs bottom-0 left-0 absolute px-[2px]">You're editing</p>} */}
130
+ <p className="text-sm bottom-0 right-0 absolute px-[7px] w-full flex justify-between">
131
+ <span className="text-[#8F8F8F]">Quantuty</span>
132
+
133
+ <span className="font-semibold">{item.qty}</span>
134
+ </p>
135
+
136
+ <img
137
+ src={item.img}
138
+ alt="Cart item"
139
+ className="h-16 w-16 rounded-md object-contain md:h-[85px] md:w-[85px]"
140
+ />
141
+ <div className="absolute top-0 right-0">
142
+ <ButtonGroup>
143
+ <Button variant={"ghost"} size={"icon-sm"} onClick={() => handleChangeCurrentItem(item.id, item.sign, item.options)}><Edit /></Button>
144
+ <Button
145
+ variant={"ghost"}
146
+ size={"icon-sm"}
147
+ className="hover:bg-red-700 rounded-r-none group"
148
+ onClick={() => handleRemoveFromCart(item.id)}
149
+ >
150
+ <Trash className="group-hover:text-white transition" />
151
+ </Button>
152
+ </ButtonGroup>
153
+ </div>
154
+ </div>
155
+ ))}
156
+ </div>
157
+ )}
158
+
159
+ <style>{`
160
+ .no-scrollbar::-webkit-scrollbar {
161
+ display: none;
162
+ }
163
+ .no-scrollbar {
164
+ -ms-overflow-style: none; /* IE and Edge */
165
+ scrollbar-width: none; /* Firefox */
166
+ }
167
+ `}</style>
168
+ </>
169
+ ) : null
170
+ }
@@ -0,0 +1,356 @@
1
+ import { useMemo, useState, type ReactNode } from "react"
2
+ import axios from "axios"
3
+ import { Download, Edit3, Minus, Plus, ShoppingCart, Trash2, X } from "lucide-react"
4
+
5
+ import {
6
+ createTemplateLayers,
7
+ getDefaultTemplateOptions,
8
+ getTemplateById,
9
+ getTemplateMaterial,
10
+ prodApiUrl,
11
+ } from "../lib/config"
12
+ import { CURRENCY_SYMBOL, formatPrice, getCartSummary, getUnitPrice } from "../lib/pricing"
13
+ import { INITIAL_STATE, useLayersStore } from "../store/layers-store"
14
+ import { useCartStore, type TCartItem } from "../store/cart-store"
15
+ import type { LayerTextProps } from "./layers/text-layer"
16
+ import type { TLayer } from "./preview"
17
+ import { Button } from "./ui/button"
18
+ import { downloadCartProposalPdf } from "../lib/cart-proposal-pdf"
19
+ import {
20
+ Dialog,
21
+ DialogClose,
22
+ DialogContent,
23
+ DialogDescription,
24
+ DialogHeader,
25
+ DialogTitle,
26
+ DialogTrigger,
27
+ } from "./ui/dialog"
28
+ import { Progress } from "./ui/progress"
29
+ import { ScrollArea } from "./ui/scroll-area"
30
+ import { Spinner } from "./ui/spinner"
31
+
32
+ const apiUrl = typeof import.meta.env.VITE_API_URL === "string" && import.meta.env.VITE_API_URL.trim() !== "" ? import.meta.env.VITE_API_URL : prodApiUrl
33
+
34
+ const normalizeQty = (qty: number): number => {
35
+ return Number.isFinite(qty) && qty > 0 ? Math.floor(qty) : 1
36
+ }
37
+
38
+ const formatSizeNumber = (value: number): string => {
39
+ return Number.isInteger(value) ? String(value) : value.toFixed(1).replace(/\.0$/, "")
40
+ }
41
+
42
+ const formatLinePrice = (value: number): string => {
43
+ return `${CURRENCY_SYMBOL}${Number.isInteger(value) ? value.toFixed(0) : value.toFixed(2)}`
44
+ }
45
+
46
+ const getColorLabel = (item: TCartItem): string => {
47
+ const template = getTemplateById(item.options.selectedTemplateId)
48
+ const material = getTemplateMaterial({
49
+ template,
50
+ materialId: item.options.selectedMaterialId,
51
+ })
52
+
53
+ return material.label
54
+ }
55
+
56
+ const getPersonalizationLines = (item: TCartItem): string[] => {
57
+ const textLayers = item.sign.filter((layer) => layer.type === "text" && layer.subtype !== "braille") as TLayer<LayerTextProps>[]
58
+ const lines = textLayers
59
+ .map((layer) => layer.props.text.trim())
60
+ .filter(Boolean)
61
+ return lines.length > 0 ? lines : ["-"]
62
+ }
63
+
64
+
65
+ const hasAdaFont = (item: TCartItem): boolean => {
66
+ const textLayers = item.sign.filter((layer) => layer.type === "text" && layer.subtype !== "braille") as TLayer<LayerTextProps>[]
67
+ return textLayers.some((layer) => Boolean(layer.props.braille))
68
+ }
69
+
70
+ export default function CartPreview({ children }: {
71
+ children: ReactNode
72
+ }) {
73
+ const { cart, init: setCart, update: updateCart } = useCartStore()
74
+ const { init: setSign, setCartId, editOptions, clearAll, cartId } = useLayersStore()
75
+
76
+ const [open, setOpen] = useState(false)
77
+ const [loading, setLoading] = useState<number | null>(null)
78
+ const [pdfLoading, setPdfLoading] = useState(false)
79
+
80
+ const loadingProgress = loading !== null && cart.length ? (loading / cart.length) * 100 : 0
81
+ const { totalQty, totalAmount } = useMemo(() => getCartSummary(cart), [cart])
82
+
83
+ const handleQtyChange = (id: number, nextQty: number) => {
84
+ updateCart(id, { qty: normalizeQty(nextQty) })
85
+ }
86
+
87
+ const handleEditItem = (item: TCartItem) => {
88
+ clearAll()
89
+ setSign(item.sign)
90
+ editOptions(item.options)
91
+ setCartId(item.id)
92
+ setOpen(false)
93
+ }
94
+
95
+ const handleRemoveItem = (id: number) => {
96
+ const filtered = cart.filter((item) => item.id !== id)
97
+ setCart(filtered)
98
+
99
+ if (filtered.length === 0) {
100
+ const defaults = getDefaultTemplateOptions()
101
+ clearAll({
102
+ ...INITIAL_STATE,
103
+ layers: createTemplateLayers({
104
+ templateId: defaults.selectedTemplateId,
105
+ shapeId: defaults.selectedShapeId,
106
+ materialId: defaults.selectedMaterialId,
107
+ preferredSizeId: defaults.selectedSize.id,
108
+ }),
109
+ })
110
+ setCartId(undefined)
111
+ return
112
+ }
113
+
114
+ if (cartId === id) {
115
+ const removedIndex = cart.findIndex((item) => item.id === id)
116
+ const fallbackIndex = removedIndex <= 0 ? 0 : removedIndex - 1
117
+ const fallbackItem = filtered[fallbackIndex] ?? filtered[0]
118
+
119
+ clearAll()
120
+ setSign(fallbackItem.sign)
121
+ editOptions(fallbackItem.options)
122
+ setCartId(fallbackItem.id)
123
+ }
124
+ }
125
+
126
+ const handleConfirm = async () => {
127
+ if (cart.length === 0) return
128
+
129
+ setLoading(0)
130
+ try {
131
+ for (const [index, item] of cart.entries()) {
132
+ const textLayers = item.sign.filter((layer) => layer.type === "text" && layer.subtype !== "braille") as TLayer<LayerTextProps>[]
133
+ const title = textLayers.map((layer) => layer.props.text).join(" ")
134
+
135
+ const body = {
136
+ title,
137
+ desc: JSON.stringify(item.sign),
138
+ image: item.img,
139
+ }
140
+
141
+ const { data } = await axios.post<{ id: number }>(apiUrl, body)
142
+
143
+ try {
144
+ await axios.post("/cart/add.json", {
145
+ items: [
146
+ {
147
+ id: data.id,
148
+ quantity: normalizeQty(item.qty),
149
+ },
150
+ ],
151
+ })
152
+ } catch (e) {
153
+ console.error(e)
154
+ }
155
+
156
+ setLoading(index + 1)
157
+ }
158
+
159
+ window.location.href = "/cart"
160
+ } catch (e) {
161
+ console.error(e)
162
+ } finally {
163
+ setLoading(null)
164
+ }
165
+ }
166
+
167
+ const handleDownloadPdf = async () => {
168
+ if (cart.length === 0 || pdfLoading || loading !== null) return
169
+
170
+ setPdfLoading(true)
171
+ try {
172
+ await downloadCartProposalPdf(cart)
173
+ } catch (error) {
174
+ console.error(error)
175
+ } finally {
176
+ setPdfLoading(false)
177
+ }
178
+ }
179
+
180
+ return (
181
+ <Dialog open={open} onOpenChange={setOpen}>
182
+ <DialogTrigger className="w-full">{children}</DialogTrigger>
183
+ <DialogContent
184
+ showCloseButton={false}
185
+ className="h-[min(95vh,980px)] w-[min(1240px,96vw)] max-w-[96vw] gap-0 overflow-hidden rounded-[34px] border border-[#D8D8D8] p-0 sm:max-w-[1240px]"
186
+ >
187
+ <DialogHeader className="border-b border-[#D6D6D6] bg-white px-5 py-4 sm:px-8 sm:py-6">
188
+ <div className="flex items-center justify-between gap-4">
189
+ <DialogTitle className="text-[24px] font-semibold leading-[1.15] text-[#1C1D1D]">Are you absolutely sure?</DialogTitle>
190
+ <DialogClose asChild>
191
+ <button
192
+ type="button"
193
+ className="inline-flex cursor-pointer items-center gap-2 rounded-lg px-1 py-1 text-[16px] font-medium text-[#1C1D1D] hover:opacity-70"
194
+ aria-label="Close cart preview"
195
+ >
196
+ <X className="size-6 sm:size-7" />
197
+ <span>Close</span>
198
+ </button>
199
+ </DialogClose>
200
+ </div>
201
+ <DialogDescription className="sr-only">Review your signs before checkout.</DialogDescription>
202
+ </DialogHeader>
203
+
204
+ <ScrollArea className="min-h-0 flex-1 bg-[#F7F7F7] md:h-[70vh] lg:h-[75vh]">
205
+ <div className="space-y-5 px-5 py-5 sm:px-8 sm:py-6">
206
+ {cart.length === 0 ? (
207
+ <div className="rounded-[28px] border border-[#D6D6D6] bg-white px-6 py-10 text-center text-xl text-[#8F8F8F] sm:text-2xl">
208
+ Your cart is empty.
209
+ </div>
210
+ ) : (
211
+ cart.map((item, index) => {
212
+ const qty = normalizeQty(item.qty)
213
+ const lineTotal = getUnitPrice(item.options.selectedSize, qty) * qty
214
+ const colorLabel = getColorLabel(item)
215
+ const templateLabel = getTemplateById(item.options.selectedTemplateId).name
216
+ const personalizationLines = getPersonalizationLines(item)
217
+ const adaText = hasAdaFont(item) ? "Yes" : "No"
218
+ const width = formatSizeNumber(item.options.selectedSize.inchs.w)
219
+ const height = formatSizeNumber(item.options.selectedSize.inchs.h)
220
+
221
+ return (
222
+ <article
223
+ key={item.id}
224
+ className="rounded-[28px] border border-[#CFCFCF] bg-[#F4F4F4] p-4 sm:rounded-[34px] sm:p-7"
225
+ >
226
+ <div className="flex flex-col gap-5 md:flex-row md:gap-8">
227
+ <div className="mx-auto w-full max-w-[280px] shrink-0 md:mx-0">
228
+ <img
229
+ src={item.img}
230
+ alt={`Sign ${index + 1}`}
231
+ className="h-auto max-h-[280px] w-full rounded-xl bg-white object-contain"
232
+ />
233
+ </div>
234
+
235
+ <div className="flex min-w-0 flex-1 flex-col">
236
+ <div className="flex items-start justify-between gap-4">
237
+ <h3 className="text-[24px] font-semibold leading-none text-[#1C1D1D]">Sign {index + 1}</h3>
238
+
239
+ <div className="flex items-center gap-1">
240
+ <button
241
+ type="button"
242
+ className="inline-flex size-10 cursor-pointer items-center justify-center rounded-lg text-[#1C1D1D] transition hover:bg-[#EAEAEA]"
243
+ aria-label={`Edit sign ${index + 1}`}
244
+ onClick={() => handleEditItem(item)}
245
+ >
246
+ <Edit3 className="size-6" />
247
+ </button>
248
+ <button
249
+ type="button"
250
+ className="inline-flex size-10 cursor-pointer items-center justify-center rounded-lg text-[#F01818] transition hover:bg-[#FFE9E9]"
251
+ aria-label={`Delete sign ${index + 1}`}
252
+ onClick={() => handleRemoveItem(item.id)}
253
+ >
254
+ <Trash2 className="size-6" />
255
+ </button>
256
+ </div>
257
+ </div>
258
+
259
+ <div className="mt-2 space-y-1 text-[16px] leading-[1.35] text-[#1C1D1D]">
260
+ <p>
261
+ <span className="font-semibold">Size & Color:</span>{" "}
262
+ {`${width}" x ${height}" - ${colorLabel} - ${templateLabel}`}
263
+ </p>
264
+ <div className="flex items-start gap-2">
265
+ <span className="shrink-0 font-semibold">Personalization:</span>
266
+ <div className="flex min-w-0 flex-col">
267
+ {personalizationLines.map((line, lineIndex) => (
268
+ <span key={`${item.id}-personalization-${lineIndex}`} className="break-words">
269
+ {line}
270
+ </span>
271
+ ))}
272
+ </div>
273
+ </div>
274
+ <p>
275
+ <span className="font-semibold">Braille Font:</span> {adaText}
276
+ </p>
277
+ </div>
278
+
279
+ <div className="mt-4 flex flex-wrap items-end justify-between gap-4">
280
+ <div className="inline-flex items-center rounded-2xl bg-[#EAEAEA] ">
281
+ <button
282
+ type="button"
283
+ className="inline-flex size-12 cursor-pointer items-center justify-center rounded-xl text-[#1C1D1D] hover:bg-[#DEDEDE]"
284
+ aria-label={`Decrease quantity of sign ${index + 1}`}
285
+ onClick={() => handleQtyChange(item.id, qty - 1)}
286
+ >
287
+ <Minus className="size-3" />
288
+ </button>
289
+ <span className="min-w-10 px-1 text-center text-[14px] font-medium text-[#1C1D1D]">{qty}</span>
290
+ <button
291
+ type="button"
292
+ className="inline-flex size-12 cursor-pointer items-center justify-center rounded-xl text-[#1C1D1D] hover:bg-[#DEDEDE]"
293
+ aria-label={`Increase quantity of sign ${index + 1}`}
294
+ onClick={() => handleQtyChange(item.id, qty + 1)}
295
+ >
296
+ <Plus className="size-3" />
297
+ </button>
298
+ </div>
299
+
300
+ <p className="text-[24px] font-bold leading-none text-[#1C1D1D]">{formatLinePrice(lineTotal)}</p>
301
+ </div>
302
+ </div>
303
+ </div>
304
+ </article>
305
+ )
306
+ })
307
+ )}
308
+ </div>
309
+ </ScrollArea>
310
+
311
+ <div className="border-t border-[#D6D6D6] bg-white px-5 pt-4 pb-[calc(1rem+env(safe-area-inset-bottom))] sm:px-8 sm:pt-5 sm:pb-6">
312
+ {loading !== null && (
313
+ <div className="mb-3 space-y-1.5">
314
+ <p className="text-sm text-[#667085]">Just a second, we are creating products in Shopify...</p>
315
+ <Progress value={loadingProgress} className="w-full" />
316
+ </div>
317
+ )}
318
+
319
+ <div className="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
320
+ <div className="flex flex-wrap items-baseline gap-x-10 gap-y-2 text-[20px]">
321
+ <p className="text-[#8F8F8F]">
322
+ <span>Total quantity:</span>
323
+ <span className="ml-3 font-semibold text-[#1C1D1D]">{totalQty}</span>
324
+ </p>
325
+ <p className="text-[#8F8F8F]">
326
+ <span>Total sum:</span>
327
+ <span className="ml-3 font-semibold text-[#1C1D1D]">{formatPrice(totalAmount)}</span>
328
+ </p>
329
+ </div>
330
+
331
+ <div className="flex w-full flex-col gap-3 sm:flex-row lg:w-auto">
332
+ <Button
333
+ variant="outline"
334
+ className="h-[64px] w-full rounded-2xl border-[#D0D5DD] px-8 text-[16px] font-semibold text-[#1C1D1D] hover:bg-[#F7F7F7] sm:w-auto sm:min-w-[230px]"
335
+ disabled={cart.length === 0 || loading !== null || pdfLoading}
336
+ onClick={() => handleDownloadPdf()}
337
+ >
338
+ {pdfLoading ? <Spinner /> : <Download className="size-6" />}
339
+ {pdfLoading ? "Generating PDF..." : "Download PDF"}
340
+ </Button>
341
+
342
+ <Button
343
+ className="h-[64px] w-full rounded-2xl bg-[#101010] px-8 text-[16px] font-semibold text-white hover:bg-[#101010]/90 sm:w-auto sm:min-w-[230px] lg:min-w-[260px]"
344
+ disabled={cart.length === 0 || loading !== null || pdfLoading}
345
+ onClick={() => handleConfirm()}
346
+ >
347
+ {loading !== null ? <Spinner /> : <ShoppingCart className="size-7" />}
348
+ {loading !== null ? "Processing..." : "Checkout"}
349
+ </Button>
350
+ </div>
351
+ </div>
352
+ </div>
353
+ </DialogContent>
354
+ </Dialog>
355
+ )
356
+ }
@@ -0,0 +1,113 @@
1
+ import { Edit } from "lucide-react";
2
+ import { Button } from "./ui/button";
3
+ import { Input } from "./ui/input";
4
+ import { useCartStore } from "../store/cart-store";
5
+ import { useLayersStore, type TLayersOption } from "../store/layers-store";
6
+ import type { LayrProps, TLayer } from "./preview";
7
+ import { useEffect } from "react";
8
+
9
+ export default function CartView({ setConstructureSelectedItem }: { setConstructureSelectedItem: (value: string) => void }) {
10
+ const { layers, init: setSign, preview, setCartId, cartId: currentItemEditId, options, editOptions, clearAll } = useLayersStore();
11
+ const { cart, add, update, init: setCart } = useCartStore()
12
+
13
+ // console.log(cart.map(item => ({bg: item.sign[0].props})));
14
+
15
+ const handleRemoveFromCart = (id: number) => {
16
+ const filtered = cart.filter(item => item.id !== id);
17
+ setCart(filtered);
18
+ }
19
+
20
+ const handleChangeCurrentItem = (id: number, layers: TLayer<LayrProps>[], itemOptions: TLayersOption) => {
21
+ clearAll();
22
+ setSign(layers);
23
+ editOptions(itemOptions)
24
+ setCartId(id);
25
+ setConstructureSelectedItem('template')
26
+ }
27
+
28
+ const handleAddToCart = (id?: number) => {
29
+ if (id) {
30
+ const item = cart.find(item => item.id === id);
31
+ if (item) {
32
+ const itemId = add(item.sign, item.img, item.options);
33
+ setSign(item.sign)
34
+ setCartId(itemId.id);
35
+ }
36
+ return;
37
+ }
38
+ if (preview) {
39
+ const itemId = add(layers, preview, options);
40
+ setCartId(itemId.id);
41
+ }
42
+ }
43
+
44
+ useEffect(() => {
45
+ if (cart.length === 0) {
46
+ setConstructureSelectedItem('template')
47
+ }
48
+ }, [cart])
49
+
50
+ return (
51
+ <div className="flex gap-2 items-center flex-wrap py-4">
52
+ {cart.map(({ sign, id, qty, img, options }) => {
53
+
54
+ const textLayers = sign.filter(layer => layer.type === 'text' && layer.subtype !== 'braille') as Array<{ props: { text: string } }>;
55
+ const text = textLayers.map(text => text.props.text);
56
+ const itemName = text.join(' ');
57
+
58
+ return (
59
+ <div key={id} className={`border-1 ${id === currentItemEditId ? "border-black/50" : "border-black/20"} p-1 flex flex-col items-center gap-4 group`}>
60
+ <div className="w-24 h-24 relative group-hover:bg-black/30 group-hover:bg-blend-multiply cursor-pointer" style={{
61
+ backgroundImage: `url(${img})`,
62
+ backgroundRepeat: 'no-repeat',
63
+ backgroundPosition: 'center top',
64
+ backgroundSize: 'cover'
65
+ }} onClick={() => handleChangeCurrentItem(id, sign, options)}>
66
+ <div className="hidden group-hover:block absolute text-white top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2">
67
+ {id === currentItemEditId ? (
68
+ <p className="text-xs">being edited</p>
69
+ ) : (
70
+ <Edit />
71
+ )}
72
+ </div>
73
+
74
+ {/* <img src={img} className="w-16 h-16" /> */}
75
+ </div>
76
+ <div className="space-y-1 relative">
77
+
78
+ <div className="text-sm absolute top-0 right-0 border flex">
79
+ {/* <span className="text-center w-full">{index + 1}</span> */}
80
+ <Button onClick={() => handleChangeCurrentItem(id, sign, options)} disabled={id === currentItemEditId} className="cursor-pointer active:scale-90" variant={"outline"} size={"icon-sm"}><Edit /></Button>
81
+ </div>
82
+
83
+ <p className="">{itemName.length > 40 ? itemName.slice(0, 40) + '...' : itemName} </p>
84
+ {/* <span className="text-xs">{id}</span> */}
85
+
86
+ <div className="flex items-end justify-between gap-2">
87
+ <div className="space-y-1 flex gap-0.5">
88
+ {/* <Label htmlFor={`quantity_${id}`}>Quantity</Label> */}
89
+ <Button variant={"outline"} size={"icon"} onClick={() => update(id, {}, (prev) => ({ qty: prev.qty !== 1 ? prev.qty - 1 : prev.qty }))}>-</Button>
90
+ <Input
91
+ value={qty}
92
+ type="number"
93
+ name={`quantity_${id}`}
94
+ id={`quantity_${id}`}
95
+ min={1}
96
+ onChange={e => update(id, { qty: Number(e.target.value) })}
97
+ className="max-w-16 text-center"
98
+ />
99
+ <Button variant={"outline"} size={"icon"} onClick={() => update(id, {}, (prev) => ({ qty: prev.qty + 1 }))}>+</Button>
100
+ </div>
101
+
102
+ <div className="space-x-1">
103
+ <span className="text-xs text-black/50 hover:underline underline-offset-2 cursor-pointer" onClick={() => handleAddToCart(id)}>dublicate</span>
104
+ <span className="text-xs text-black/50 hover:underline underline-offset-2 cursor-pointer hover:text-red-600" onClick={() => handleRemoveFromCart(id)}>delete</span>
105
+ </div>
106
+ </div>
107
+ </div>
108
+ </div>
109
+ )
110
+ })}
111
+ </div>
112
+ )
113
+ }