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.
- package/.env +2 -0
- package/components.json +21 -0
- package/dist/colors/anthracite-gray.webp +0 -0
- package/dist/colors/anthracite-gray_50x50.png +0 -0
- package/dist/colors/dark-wenge.webp +0 -0
- package/dist/colors/dark-wenge_50x50.png +0 -0
- package/dist/colors/indian-rosewood.webp +0 -0
- package/dist/colors/indian-rosewood_50x50.png +0 -0
- package/dist/colors/natural-wood.webp +0 -0
- package/dist/colors/natural-wood_50x50.png +0 -0
- package/dist/colors/redwood.webp +0 -0
- package/dist/colors/redwood_50x50.png +0 -0
- package/dist/colors/walnut.webp +0 -0
- package/dist/colors/walnut_50x50.png +0 -0
- package/dist/html2canvas.esm-BJ_egzt0.js +4802 -0
- package/dist/index-Dw5Zc1iD.js +33162 -0
- package/dist/index.es-Co1KNpGS.js +6681 -0
- package/dist/logo.png +0 -0
- package/dist/purify.es-CKpD2xIC.js +552 -0
- package/dist/sign-constructor.es.js +4 -0
- package/dist/sign-constructor.iife.js +171 -0
- package/dist/size-guide.webp +0 -0
- package/dist/size.webp +0 -0
- package/dist/templates/assets/modern/rectangle-black.webp +0 -0
- package/dist/templates/assets/modern/rectangle-white.webp +0 -0
- package/dist/templates/assets/modern/square-black.webp +0 -0
- package/dist/templates/assets/modern/square-white.webp +0 -0
- package/dist/templates/assets/wave.webp +0 -0
- package/dist/templates/jure.webp +0 -0
- package/dist/templates/modern.webp +0 -0
- package/dist/templates/sherwood.webp +0 -0
- package/dist/templates/wave.webp +0 -0
- package/eslint.config.js +23 -0
- package/index.html +13 -0
- package/modern-debug.svg +39 -0
- package/package.json +62 -0
- package/public/colors/anthracite-gray.webp +0 -0
- package/public/colors/anthracite-gray_50x50.png +0 -0
- package/public/colors/dark-wenge.webp +0 -0
- package/public/colors/dark-wenge_50x50.png +0 -0
- package/public/colors/indian-rosewood.webp +0 -0
- package/public/colors/indian-rosewood_50x50.png +0 -0
- package/public/colors/natural-wood.webp +0 -0
- package/public/colors/natural-wood_50x50.png +0 -0
- package/public/colors/redwood.webp +0 -0
- package/public/colors/redwood_50x50.png +0 -0
- package/public/colors/walnut.webp +0 -0
- package/public/colors/walnut_50x50.png +0 -0
- package/public/logo.png +0 -0
- package/public/size-guide.webp +0 -0
- package/public/size.webp +0 -0
- package/public/templates/assets/modern/rectangle-black.webp +0 -0
- package/public/templates/assets/modern/rectangle-white.webp +0 -0
- package/public/templates/assets/modern/square-black.webp +0 -0
- package/public/templates/assets/modern/square-white.webp +0 -0
- package/public/templates/assets/wave.webp +0 -0
- package/public/templates/jure.webp +0 -0
- package/public/templates/modern.webp +0 -0
- package/public/templates/sherwood.webp +0 -0
- package/public/templates/wave.webp +0 -0
- package/src/App.css +43 -0
- package/src/AppDemo2.tsx +257 -0
- package/src/components/cart-panel.tsx +170 -0
- package/src/components/cart-preview.tsx +356 -0
- package/src/components/cart-view.tsx +113 -0
- package/src/components/constructure-menu.tsx +37 -0
- package/src/components/header.tsx +214 -0
- package/src/components/heading.tsx +28 -0
- package/src/components/icons.tsx +54 -0
- package/src/components/import-file-modal.tsx +252 -0
- package/src/components/layers/grid-view.tsx +29 -0
- package/src/components/layers/image-layer.tsx +128 -0
- package/src/components/layers/layer-forms/material-form.tsx +53 -0
- package/src/components/layers/layer-forms/size-form.tsx +69 -0
- package/src/components/layers/layer-forms/template-form.tsx +39 -0
- package/src/components/layers/layer-forms/text-form.tsx +477 -0
- package/src/components/layers/layers-container.tsx +259 -0
- package/src/components/layers/text-layer.tsx +128 -0
- package/src/components/movable-item.tsx +228 -0
- package/src/components/preview.tsx +258 -0
- package/src/components/resize-button.tsx +83 -0
- package/src/components/size-guide-modal.tsx +47 -0
- package/src/components/size-guide.tsx +98 -0
- package/src/components/ui/button-group.tsx +83 -0
- package/src/components/ui/button.tsx +60 -0
- package/src/components/ui/checkbox.tsx +30 -0
- package/src/components/ui/dialog.tsx +151 -0
- package/src/components/ui/input-group.tsx +168 -0
- package/src/components/ui/input.tsx +21 -0
- package/src/components/ui/label.tsx +22 -0
- package/src/components/ui/popover.tsx +54 -0
- package/src/components/ui/progress.tsx +28 -0
- package/src/components/ui/radio-group.tsx +43 -0
- package/src/components/ui/scroll-area.tsx +56 -0
- package/src/components/ui/select.tsx +191 -0
- package/src/components/ui/separator.tsx +25 -0
- package/src/components/ui/sheet.tsx +141 -0
- package/src/components/ui/slider.tsx +61 -0
- package/src/components/ui/spinner.tsx +15 -0
- package/src/components/ui/textarea.tsx +18 -0
- package/src/components/ui/toggle-group.tsx +73 -0
- package/src/components/ui/toggle.tsx +45 -0
- package/src/components/ui/tooltip.tsx +67 -0
- package/src/fonts/BEBASNEUE-REGULAR.TTF +0 -0
- package/src/fonts/Braille-Regular.ttf +0 -0
- package/src/fonts/GOTHICB.TTF +0 -0
- package/src/hooks/use-mobile.ts +23 -0
- package/src/hooks/use-resize-constraints.ts +62 -0
- package/src/index.css +238 -0
- package/src/index.tsx +141 -0
- package/src/lib/cart-proposal-pdf.ts +350 -0
- package/src/lib/config-font.tsx +109 -0
- package/src/lib/config.ts +730 -0
- package/src/lib/pricing.ts +61 -0
- package/src/lib/type-checks.ts +47 -0
- package/src/lib/utils.ts +146 -0
- package/src/lib/widget-context.tsx +9 -0
- package/src/main.tsx +11 -0
- package/src/store/cart-store.ts +78 -0
- package/src/store/layers-store.ts +337 -0
- package/src/vite-env.d.ts +1 -0
- package/test/preview.html +37 -0
- package/tsconfig.app.json +33 -0
- package/tsconfig.json +13 -0
- package/tsconfig.node.json +25 -0
- 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
|
+
}
|