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,258 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from "react";
|
|
2
|
+
import { useIsMobile } from "../hooks/use-mobile";
|
|
3
|
+
import { useLayersStore } from "../store/layers-store";
|
|
4
|
+
import type { LayerImageProps } from "./layers/image-layer";
|
|
5
|
+
import ImageLayer from "./layers/image-layer";
|
|
6
|
+
import type { LayerTextProps } from "./layers/text-layer";
|
|
7
|
+
import TextLayer from "./layers/text-layer";
|
|
8
|
+
import { type TMovableItemProps } from "./movable-item"
|
|
9
|
+
import * as htmlToImage from "html-to-image";
|
|
10
|
+
import { isBackgroundLayer } from "../lib/type-checks";
|
|
11
|
+
import { useCartStore } from "../store/cart-store";
|
|
12
|
+
import GridView from "./layers/grid-view";
|
|
13
|
+
import { getDefaultTemplateOptions, getSizeLimitScale, getTemplateTextFont } from "../lib/config";
|
|
14
|
+
|
|
15
|
+
const PREVIEW_PIXEL_RATIO = 1;
|
|
16
|
+
const CART_IMAGE_PIXEL_RATIO = 3;
|
|
17
|
+
const CSS_INCH_IN_PX = 96;
|
|
18
|
+
const PREVIEW_CONTENT_MARGIN_PX = 80;
|
|
19
|
+
const MOBILE_REFERENCE_SIZE = getDefaultTemplateOptions().selectedSize.inchs;
|
|
20
|
+
|
|
21
|
+
export type LayrProps = LayerImageProps | LayerTextProps | { imageSrc: string } & { hidden?: boolean };
|
|
22
|
+
|
|
23
|
+
export type TLayer<T> = {
|
|
24
|
+
type: 'image' | 'text' | 'background'
|
|
25
|
+
subtype?: 'braille' | 'icon'
|
|
26
|
+
props: T & TMovableItemProps & { hidden?: boolean };
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
// interface PreviewProps {
|
|
30
|
+
// background?: {
|
|
31
|
+
// aspectRatio?: string
|
|
32
|
+
// }
|
|
33
|
+
// }
|
|
34
|
+
|
|
35
|
+
export default function Preview() {
|
|
36
|
+
const { layers, setPreview, cartId, options, change, textCenteringRequest } = useLayersStore();
|
|
37
|
+
const { update: updateCart } = useCartStore();
|
|
38
|
+
const isMobile = useIsMobile();
|
|
39
|
+
const canvasRef = useRef<HTMLDivElement | null>(null);
|
|
40
|
+
const lastAutoCenteredTemplateIdRef = useRef<string | null>(null);
|
|
41
|
+
|
|
42
|
+
const [backgroundLayer, setBackground] = useState<string>();
|
|
43
|
+
|
|
44
|
+
const containerRef = useRef<HTMLDivElement>(null)
|
|
45
|
+
const selectedSize = options.selectedSize.inchs;
|
|
46
|
+
const templateTextFont = getTemplateTextFont(options.selectedTemplateId);
|
|
47
|
+
const referenceWidthPx = MOBILE_REFERENCE_SIZE.w * CSS_INCH_IN_PX + PREVIEW_CONTENT_MARGIN_PX;
|
|
48
|
+
const referenceHeightPx = MOBILE_REFERENCE_SIZE.h * CSS_INCH_IN_PX + PREVIEW_CONTENT_MARGIN_PX;
|
|
49
|
+
const currentWidthPx = selectedSize.w * CSS_INCH_IN_PX + PREVIEW_CONTENT_MARGIN_PX;
|
|
50
|
+
const currentHeightPx = selectedSize.h * CSS_INCH_IN_PX + PREVIEW_CONTENT_MARGIN_PX;
|
|
51
|
+
const mobilePreviewScale = isMobile
|
|
52
|
+
? Math.min(referenceWidthPx / currentWidthPx, referenceHeightPx / currentHeightPx)
|
|
53
|
+
: 1;
|
|
54
|
+
const previewSizeLimitScale = getSizeLimitScale(options.selectedSize);
|
|
55
|
+
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
const layerBackground = layers.find(layer => layer.type === 'background');
|
|
58
|
+
if (layerBackground && isBackgroundLayer(layerBackground) && layerBackground.props.imageSrc !== backgroundLayer)
|
|
59
|
+
setBackground(layerBackground.props.imageSrc)
|
|
60
|
+
|
|
61
|
+
const timer = setTimeout(() => {
|
|
62
|
+
generateImage({ pixelRatio: PREVIEW_PIXEL_RATIO, updatePreviewState: true });
|
|
63
|
+
}, 100);
|
|
64
|
+
|
|
65
|
+
return () => clearTimeout(timer);
|
|
66
|
+
}, [options, layers[0], layers[1], layers[2], layers[3], layers[4], layers[5]])
|
|
67
|
+
|
|
68
|
+
useEffect(() => {
|
|
69
|
+
const timer = setTimeout(() => {
|
|
70
|
+
refreshCart()
|
|
71
|
+
}, 100);
|
|
72
|
+
return () => clearTimeout(timer);
|
|
73
|
+
}, [cartId, options, layers[0], layers[1], layers[2], layers[3], layers[4], layers[5]])
|
|
74
|
+
|
|
75
|
+
const generateImage = async ({
|
|
76
|
+
pixelRatio = PREVIEW_PIXEL_RATIO,
|
|
77
|
+
updatePreviewState = true,
|
|
78
|
+
}: {
|
|
79
|
+
pixelRatio?: number;
|
|
80
|
+
updatePreviewState?: boolean;
|
|
81
|
+
} = {}) => {
|
|
82
|
+
if (!canvasRef.current) return undefined;
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
const previewUrl = await htmlToImage.toPng(canvasRef.current, {
|
|
86
|
+
cacheBust: true,
|
|
87
|
+
pixelRatio,
|
|
88
|
+
skipFonts: false,
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
if (updatePreviewState) {
|
|
92
|
+
setPreview(previewUrl);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return previewUrl;
|
|
96
|
+
} catch (error) {
|
|
97
|
+
console.error(error);
|
|
98
|
+
return undefined;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const refreshCart = async () => {
|
|
103
|
+
if (cartId) {
|
|
104
|
+
const img = await generateImage({
|
|
105
|
+
pixelRatio: CART_IMAGE_PIXEL_RATIO,
|
|
106
|
+
updatePreviewState: false,
|
|
107
|
+
});
|
|
108
|
+
if (img) {
|
|
109
|
+
updateCart(cartId, {
|
|
110
|
+
img,
|
|
111
|
+
sign: layers,
|
|
112
|
+
options
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const centerTextBlock = (): boolean => {
|
|
119
|
+
const textLayers = layers
|
|
120
|
+
.map((layer, index) => ({ layer, index }))
|
|
121
|
+
.filter(({ layer }) => layer.type === "text" && layer.subtype !== "braille");
|
|
122
|
+
|
|
123
|
+
if (!textLayers.length) return false;
|
|
124
|
+
const parent = containerRef.current?.offsetParent as HTMLElement;
|
|
125
|
+
if (!parent) return false;
|
|
126
|
+
|
|
127
|
+
const centerX = parent.clientWidth / 2;
|
|
128
|
+
const centerY = parent.clientHeight / 2;
|
|
129
|
+
|
|
130
|
+
const positionedTextLayers = textLayers.filter(({ layer }) =>
|
|
131
|
+
typeof layer.props.coordinates?.x === "number" && typeof layer.props.coordinates?.y === "number"
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
if (!positionedTextLayers.length) return false;
|
|
135
|
+
|
|
136
|
+
const textXCoordinates = positionedTextLayers.map(({ layer }) => layer.props.coordinates.x as number);
|
|
137
|
+
const textYCoordinates = positionedTextLayers.map(({ layer }) => layer.props.coordinates.y as number);
|
|
138
|
+
|
|
139
|
+
const currentTextCenterX = (Math.min(...textXCoordinates) + Math.max(...textXCoordinates)) / 2;
|
|
140
|
+
const currentTextCenterY = (Math.min(...textYCoordinates) + Math.max(...textYCoordinates)) / 2;
|
|
141
|
+
|
|
142
|
+
const deltaX = centerX - currentTextCenterX;
|
|
143
|
+
const deltaY = centerY - currentTextCenterY;
|
|
144
|
+
|
|
145
|
+
if (Math.abs(deltaX) < 0.01 && Math.abs(deltaY) < 0.01) return true;
|
|
146
|
+
|
|
147
|
+
positionedTextLayers.forEach(({ layer, index }) => {
|
|
148
|
+
change(index, {
|
|
149
|
+
coordinates: {
|
|
150
|
+
x: (layer.props.coordinates.x as number) + deltaX,
|
|
151
|
+
y: (layer.props.coordinates.y as number) + deltaY,
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
const brailleLayers = layers
|
|
157
|
+
.map((layer, index) => ({ layer, index }))
|
|
158
|
+
.filter(({ layer }) => layer.subtype === "braille" &&
|
|
159
|
+
typeof layer.props.coordinates?.x === "number" &&
|
|
160
|
+
typeof layer.props.coordinates?.y === "number"
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
brailleLayers.forEach(({ layer, index }) => {
|
|
164
|
+
change(index, {
|
|
165
|
+
coordinates: {
|
|
166
|
+
x: (layer.props.coordinates.x as number) + deltaX,
|
|
167
|
+
y: (layer.props.coordinates.y as number) + deltaY,
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
return true;
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
// Automatic centering on startup and when template changes.
|
|
175
|
+
useEffect(() => {
|
|
176
|
+
if (!backgroundLayer) return;
|
|
177
|
+
if (lastAutoCenteredTemplateIdRef.current === options.selectedTemplateId) return;
|
|
178
|
+
|
|
179
|
+
const isCentered = centerTextBlock();
|
|
180
|
+
if (isCentered) {
|
|
181
|
+
lastAutoCenteredTemplateIdRef.current = options.selectedTemplateId;
|
|
182
|
+
}
|
|
183
|
+
}, [backgroundLayer, layers, options.selectedTemplateId]);
|
|
184
|
+
|
|
185
|
+
// Manual centering by "Center text" button.
|
|
186
|
+
useEffect(() => {
|
|
187
|
+
if (textCenteringRequest === 0) return;
|
|
188
|
+
centerTextBlock();
|
|
189
|
+
}, [textCenteringRequest])
|
|
190
|
+
|
|
191
|
+
return (
|
|
192
|
+
<>
|
|
193
|
+
{(backgroundLayer) && <div>
|
|
194
|
+
<div
|
|
195
|
+
className={isMobile ? "overflow-hidden" : undefined}
|
|
196
|
+
style={isMobile ? {
|
|
197
|
+
width: `${referenceWidthPx}px`,
|
|
198
|
+
height: `${referenceHeightPx}px`,
|
|
199
|
+
} : undefined}
|
|
200
|
+
>
|
|
201
|
+
<div
|
|
202
|
+
className={isMobile ? "h-fit w-fit origin-top-left" : undefined}
|
|
203
|
+
style={isMobile ? { transform: `scale(${mobilePreviewScale})` } : undefined}
|
|
204
|
+
>
|
|
205
|
+
<div
|
|
206
|
+
className="relative overflow-hidden origin-top transition-transform duration-500 ease-in-out"
|
|
207
|
+
ref={canvasRef}
|
|
208
|
+
style={{ transform: `scale(${previewSizeLimitScale})` }}
|
|
209
|
+
>
|
|
210
|
+
|
|
211
|
+
<div className="m-10" ref={containerRef}>
|
|
212
|
+
{/* Layer 1 - Main Background */}
|
|
213
|
+
<div
|
|
214
|
+
className="bg-center bg-no-repeat relative pointer-events-none transition-[width,height] duration-500 ease-in-out will-change-[width,height]"
|
|
215
|
+
style={{
|
|
216
|
+
backgroundImage: `url(${backgroundLayer})`,
|
|
217
|
+
backgroundColor: backgroundLayer ? "transparent" : "#f3f4f6",
|
|
218
|
+
backgroundSize: "100% 100%", // Avoid side clipping for rectangular signs when source aspect ratio differs.
|
|
219
|
+
width: `${options.selectedSize.inchs.w}in`,
|
|
220
|
+
height: `${options.selectedSize.inchs.h}in`,
|
|
221
|
+
}}
|
|
222
|
+
>
|
|
223
|
+
{options.gridCm && <div className="absolute inset-0 z-50 pointer-events-none">
|
|
224
|
+
<GridView h={options.selectedSize.cm.h} w={options.selectedSize.cm.w} color="color-mix(in oklab, var(--color-red-600) 50%, transparent)" />
|
|
225
|
+
</div>}
|
|
226
|
+
{options.gridInch && <div className="absolute inset-0 z-50 pointer-events-none">
|
|
227
|
+
<GridView h={options.selectedSize.inchs.h} w={options.selectedSize.inchs.w} color="color-mix(in oklab, var(--color-purple-600) 50%, transparent)" width={2} />
|
|
228
|
+
</div>}
|
|
229
|
+
</div>
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
{layers && layers.map((layer, index) => !layer.props.hidden ? (
|
|
233
|
+
<>
|
|
234
|
+
{layer.subtype === 'icon' && (
|
|
235
|
+
<ImageLayer key={`layer_${index}`} layerIndex={index} />
|
|
236
|
+
)}
|
|
237
|
+
|
|
238
|
+
{layer.type === 'image' && (
|
|
239
|
+
<ImageLayer key={`layer_${index}`} layerIndex={index} />
|
|
240
|
+
)}
|
|
241
|
+
|
|
242
|
+
{(layer.type === 'text' && layer.subtype !== 'braille') && (
|
|
243
|
+
<TextLayer key={`layer_${index}`} layerIndex={index} font={templateTextFont} />
|
|
244
|
+
)}
|
|
245
|
+
|
|
246
|
+
{layer.subtype === 'braille' && (
|
|
247
|
+
<TextLayer key={`layer_${index}`} layerIndex={index} font='font-braille' />
|
|
248
|
+
)}
|
|
249
|
+
</>
|
|
250
|
+
) : null)}
|
|
251
|
+
</div>
|
|
252
|
+
</div>
|
|
253
|
+
</div>
|
|
254
|
+
</div>
|
|
255
|
+
</div>}
|
|
256
|
+
</>
|
|
257
|
+
)
|
|
258
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
2
|
+
import { cn } from "../lib/utils";
|
|
3
|
+
import { useCallback, useState } from "react";
|
|
4
|
+
|
|
5
|
+
export type ResizeProps = {
|
|
6
|
+
isMouseMoveLeftDown: boolean,
|
|
7
|
+
isMouseMoveRightUp: boolean,
|
|
8
|
+
isMouseMoveLeftUp: boolean,
|
|
9
|
+
isMouseMoveRightDown: boolean,
|
|
10
|
+
isMouseMoveLeft: boolean,
|
|
11
|
+
isMouseMoveRight: boolean,
|
|
12
|
+
isMouseMoveUp: boolean,
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const resizableItemVariants = cva(
|
|
16
|
+
"absolute bg-blue-500 hover:bg-red-500 rounded-full h-[14px] w-[14px] translate-y-0",
|
|
17
|
+
{
|
|
18
|
+
variants: {
|
|
19
|
+
position: {
|
|
20
|
+
leftTop: "-top-[10px] -left-[10px]",
|
|
21
|
+
leftCenter: "bottom-1/2 -left-[10px]",
|
|
22
|
+
leftBottom: "-bottom-[10px] -left-[10px]",
|
|
23
|
+
rightTop: "-top-[10px] -right-[10px]",
|
|
24
|
+
rightCenter: "bottom-1/2 -right-[10px]",
|
|
25
|
+
rightBottom: "-bottom-[10px] -right-[10px]",
|
|
26
|
+
bottomCenter: "-bottom-[10px] left-1/2",
|
|
27
|
+
topCenter: "-top-[10px] left-1/2",
|
|
28
|
+
},
|
|
29
|
+
cursor: {
|
|
30
|
+
'nwse-resize': 'cursor-nwse-resize',
|
|
31
|
+
'nesw-resize': 'cursor-nesw-resize',
|
|
32
|
+
'e-resize': 'cursor-e-resize',
|
|
33
|
+
'ns-resize': 'cursor-ns-resize'
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
export default function ResizeButton(props: VariantProps<typeof resizableItemVariants> & {
|
|
40
|
+
onUpdate: (move: ResizeProps) => void;
|
|
41
|
+
}) {
|
|
42
|
+
const [isDragging, setIsDragging] = useState(false)
|
|
43
|
+
const [dragStart, setDragStart] = useState({ x: 0, y: 0 })
|
|
44
|
+
|
|
45
|
+
const handleMouseDown = useCallback(
|
|
46
|
+
(e: React.MouseEvent) => {
|
|
47
|
+
e.preventDefault()
|
|
48
|
+
setIsDragging(true)
|
|
49
|
+
setDragStart({ x: e.clientX, y: e.clientY })
|
|
50
|
+
}, [],
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
const handleMouseMove = useCallback(
|
|
54
|
+
(e: React.MouseEvent) => {
|
|
55
|
+
if (!isDragging) return
|
|
56
|
+
|
|
57
|
+
const isMouseMoveLeftDown = e.clientX < dragStart.x && e.clientY > dragStart.y
|
|
58
|
+
const isMouseMoveRightUp = e.clientX > dragStart.x && e.clientY < dragStart.y
|
|
59
|
+
const isMouseMoveLeftUp = e.clientX < dragStart.x && e.clientY < dragStart.y
|
|
60
|
+
const isMouseMoveRightDown = e.clientX > dragStart.x && e.clientY > dragStart.y
|
|
61
|
+
|
|
62
|
+
const isMouseMoveLeft = e.clientX < dragStart.x
|
|
63
|
+
const isMouseMoveRight = e.clientX > dragStart.x
|
|
64
|
+
const isMouseMoveUp = e.clientY < dragStart.y
|
|
65
|
+
|
|
66
|
+
props.onUpdate({ isMouseMoveLeftDown, isMouseMoveRightUp, isMouseMoveLeftUp, isMouseMoveRightDown, isMouseMoveLeft, isMouseMoveRight, isMouseMoveUp })
|
|
67
|
+
},
|
|
68
|
+
[isDragging],
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
const handleMouseUp = useCallback(() => {
|
|
72
|
+
setIsDragging(false)
|
|
73
|
+
}, [])
|
|
74
|
+
|
|
75
|
+
return (
|
|
76
|
+
<div
|
|
77
|
+
onMouseMove={handleMouseMove}
|
|
78
|
+
onMouseUp={handleMouseUp}
|
|
79
|
+
onMouseDown={handleMouseDown}
|
|
80
|
+
className={cn(resizableItemVariants(props))}
|
|
81
|
+
>{isDragging && 'Dragging'}</div>
|
|
82
|
+
)
|
|
83
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Dialog,
|
|
3
|
+
DialogPortal,
|
|
4
|
+
DialogOverlay,
|
|
5
|
+
} from "./ui/dialog"
|
|
6
|
+
import * as DialogPrimitive from "@radix-ui/react-dialog"
|
|
7
|
+
import { cn } from "../lib/utils"
|
|
8
|
+
import { X } from "lucide-react"
|
|
9
|
+
|
|
10
|
+
interface ImportFileModalProps {
|
|
11
|
+
open: boolean
|
|
12
|
+
onOpenChange: (open: boolean) => void
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function SizeGuideModal({
|
|
16
|
+
open,
|
|
17
|
+
onOpenChange,
|
|
18
|
+
}: ImportFileModalProps) {
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
22
|
+
<DialogPortal>
|
|
23
|
+
<DialogOverlay className="bg-black/40" />
|
|
24
|
+
<DialogPrimitive.Content
|
|
25
|
+
className={cn(
|
|
26
|
+
"fixed left-1/2 top-1/2 z-50 max-h-[90vh] w-[calc(100%-1rem)] max-w-[1600px] overflow-auto bg-background -translate-x-1/2 -translate-y-1/2 sm:w-[calc(100%-2rem)]",
|
|
27
|
+
"rounded-2xl",
|
|
28
|
+
"duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out",
|
|
29
|
+
"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
|
30
|
+
"data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95",
|
|
31
|
+
"data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%]",
|
|
32
|
+
"data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%]",
|
|
33
|
+
"focus:outline-none"
|
|
34
|
+
)}
|
|
35
|
+
>
|
|
36
|
+
<div className="w-full px-4 pt-2 flex justify-end">
|
|
37
|
+
<DialogPrimitive.Close className="flex items-center gap-2 text-sm font-medium text-foreground hover:text-foreground/80 transition-colors">
|
|
38
|
+
<X className="h-4 w-4" />
|
|
39
|
+
Close
|
|
40
|
+
</DialogPrimitive.Close>
|
|
41
|
+
</div>
|
|
42
|
+
<img src="/size-guide.webp" className="w-full h-auto rounded-2xl object-contain" />
|
|
43
|
+
</DialogPrimitive.Content>
|
|
44
|
+
</DialogPortal>
|
|
45
|
+
</Dialog>
|
|
46
|
+
)
|
|
47
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { Percent, X } from "lucide-react"
|
|
2
|
+
import { Button } from "./ui/button"
|
|
3
|
+
import { useState } from "react"
|
|
4
|
+
import { useIsMobile } from "../hooks/use-mobile"
|
|
5
|
+
|
|
6
|
+
export type TGuide = {
|
|
7
|
+
price: string;
|
|
8
|
+
qty: number;
|
|
9
|
+
discount?: number;
|
|
10
|
+
badgeColor?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function Guide({ items, currencySymbol = "$" }: {
|
|
14
|
+
items: TGuide[];
|
|
15
|
+
currencySymbol?: string;
|
|
16
|
+
}) {
|
|
17
|
+
return (
|
|
18
|
+
<div className="quantity-discount">
|
|
19
|
+
<div className="qty-discount--list">
|
|
20
|
+
|
|
21
|
+
{items.map(({ price, qty, discount, badgeColor }) => (
|
|
22
|
+
<div className="qty-discount--item" key={qty}>
|
|
23
|
+
<p className="qty-discount--item-qty">{qty} Item</p>
|
|
24
|
+
<p className="qty-discount--item-price">
|
|
25
|
+
{currencySymbol}<span id="main-product-price">
|
|
26
|
+
{price}
|
|
27
|
+
</span>
|
|
28
|
+
</p>
|
|
29
|
+
<p className="qty-discount--item-hm">each</p>
|
|
30
|
+
|
|
31
|
+
{discount && (
|
|
32
|
+
<span
|
|
33
|
+
className="qty-discount--item-badge"
|
|
34
|
+
style={{
|
|
35
|
+
background: badgeColor ?? "#FFBE00",
|
|
36
|
+
}}
|
|
37
|
+
>
|
|
38
|
+
SAVE {discount}%
|
|
39
|
+
</span>
|
|
40
|
+
)}
|
|
41
|
+
</div>
|
|
42
|
+
))}
|
|
43
|
+
|
|
44
|
+
</div>
|
|
45
|
+
|
|
46
|
+
<p className="qty-discount--info">
|
|
47
|
+
Special pricing is available for B2B clients and Property Managers. Contact us to learn more.
|
|
48
|
+
</p>
|
|
49
|
+
</div>
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export default function PriceGuide({ guide, currencySymbol = "$" }: {
|
|
54
|
+
guide: TGuide[];
|
|
55
|
+
currencySymbol?: string;
|
|
56
|
+
}) {
|
|
57
|
+
const [open, setOpen] = useState(false)
|
|
58
|
+
const isMobile = useIsMobile()
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<div className="relative">
|
|
62
|
+
<Button
|
|
63
|
+
variant={"green"}
|
|
64
|
+
size={"lg"}
|
|
65
|
+
onClick={() => setOpen(prev => !prev)}
|
|
66
|
+
className={`${open ? "bg-[#107E12] text-white" : ""} hidden md:flex`}
|
|
67
|
+
>
|
|
68
|
+
<Percent className="size-4.5" />
|
|
69
|
+
Price guide
|
|
70
|
+
</Button>
|
|
71
|
+
|
|
72
|
+
<span className="flex md:hidden items-center gap-1.5 text-[#107E12] underline underline-offset-4" onClick={() => setOpen(prev => !prev)}>
|
|
73
|
+
<Percent className="size-3.5" />
|
|
74
|
+
Price guide
|
|
75
|
+
</span>
|
|
76
|
+
|
|
77
|
+
{open && (
|
|
78
|
+
<>
|
|
79
|
+
{isMobile && (
|
|
80
|
+
<button
|
|
81
|
+
type="button"
|
|
82
|
+
className="fixed inset-0 z-40 bg-black/30"
|
|
83
|
+
onClick={() => setOpen(false)}
|
|
84
|
+
aria-label="Close price guide"
|
|
85
|
+
/>
|
|
86
|
+
)}
|
|
87
|
+
<div className={`border border-[#E6E6E6] rounded-xl bg-white p-4 z-50 ${isMobile ? "fixed inset-x-3 bottom-3 max-h-[75vh] overflow-auto" : "absolute -top-53 max-w-[600px]"}`}>
|
|
88
|
+
<div className="flex items-start justify-between">
|
|
89
|
+
<h6 className="mb-4 text-[18px] font-semibold">Price guide</h6>
|
|
90
|
+
<X className="size-4 cursor-pointer" onClick={() => setOpen(false)} />
|
|
91
|
+
</div>
|
|
92
|
+
<Guide items={guide} currencySymbol={currencySymbol} />
|
|
93
|
+
</div>
|
|
94
|
+
</>
|
|
95
|
+
)}
|
|
96
|
+
</div>
|
|
97
|
+
)
|
|
98
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { Slot } from "@radix-ui/react-slot"
|
|
2
|
+
import { cva, type VariantProps } from "class-variance-authority"
|
|
3
|
+
import { cn } from "../../lib/utils"
|
|
4
|
+
import { Separator } from "./separator"
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
const buttonGroupVariants = cva(
|
|
8
|
+
"flex w-fit items-stretch [&>*]:focus-visible:z-10 [&>*]:focus-visible:relative [&>[data-slot=select-trigger]:not([class*='w-'])]:w-fit [&>input]:flex-1 has-[select[aria-hidden=true]:last-child]:[&>[data-slot=select-trigger]:last-of-type]:rounded-r-md has-[>[data-slot=button-group]]:gap-2",
|
|
9
|
+
{
|
|
10
|
+
variants: {
|
|
11
|
+
orientation: {
|
|
12
|
+
horizontal:
|
|
13
|
+
"[&>*:not(:first-child)]:rounded-l-none [&>*:not(:first-child)]:border-l-0 [&>*:not(:last-child)]:rounded-r-none",
|
|
14
|
+
vertical:
|
|
15
|
+
"flex-col [&>*:not(:first-child)]:rounded-t-none [&>*:not(:first-child)]:border-t-0 [&>*:not(:last-child)]:rounded-b-none",
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
defaultVariants: {
|
|
19
|
+
orientation: "horizontal",
|
|
20
|
+
},
|
|
21
|
+
}
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
function ButtonGroup({
|
|
25
|
+
className,
|
|
26
|
+
orientation,
|
|
27
|
+
...props
|
|
28
|
+
}: React.ComponentProps<"div"> & VariantProps<typeof buttonGroupVariants>) {
|
|
29
|
+
return (
|
|
30
|
+
<div
|
|
31
|
+
role="group"
|
|
32
|
+
data-slot="button-group"
|
|
33
|
+
data-orientation={orientation}
|
|
34
|
+
className={cn(buttonGroupVariants({ orientation }), className)}
|
|
35
|
+
{...props}
|
|
36
|
+
/>
|
|
37
|
+
)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function ButtonGroupText({
|
|
41
|
+
className,
|
|
42
|
+
asChild = false,
|
|
43
|
+
...props
|
|
44
|
+
}: React.ComponentProps<"div"> & {
|
|
45
|
+
asChild?: boolean
|
|
46
|
+
}) {
|
|
47
|
+
const Comp = asChild ? Slot : "div"
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<Comp
|
|
51
|
+
className={cn(
|
|
52
|
+
"bg-muted flex items-center gap-2 rounded-md border px-4 text-sm font-medium shadow-xs [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4",
|
|
53
|
+
className
|
|
54
|
+
)}
|
|
55
|
+
{...props}
|
|
56
|
+
/>
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function ButtonGroupSeparator({
|
|
61
|
+
className,
|
|
62
|
+
orientation = "vertical",
|
|
63
|
+
...props
|
|
64
|
+
}: React.ComponentProps<typeof Separator>) {
|
|
65
|
+
return (
|
|
66
|
+
<Separator
|
|
67
|
+
data-slot="button-group-separator"
|
|
68
|
+
orientation={orientation}
|
|
69
|
+
className={cn(
|
|
70
|
+
"bg-input relative !m-0 self-stretch data-[orientation=vertical]:h-auto",
|
|
71
|
+
className
|
|
72
|
+
)}
|
|
73
|
+
{...props}
|
|
74
|
+
/>
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export {
|
|
79
|
+
ButtonGroup,
|
|
80
|
+
ButtonGroupSeparator,
|
|
81
|
+
ButtonGroupText,
|
|
82
|
+
buttonGroupVariants,
|
|
83
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import { Slot } from "@radix-ui/react-slot"
|
|
3
|
+
import { cva, type VariantProps } from "class-variance-authority"
|
|
4
|
+
import { cn } from "../../lib/utils"
|
|
5
|
+
|
|
6
|
+
const buttonVariants = cva(
|
|
7
|
+
"cursor-pointer inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-xl text-[16px] font-semibold transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
|
8
|
+
{
|
|
9
|
+
variants: {
|
|
10
|
+
variant: {
|
|
11
|
+
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
|
12
|
+
destructive:
|
|
13
|
+
"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
|
14
|
+
outline:
|
|
15
|
+
"border bg-background border-black hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
|
|
16
|
+
secondary:
|
|
17
|
+
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
|
18
|
+
ghost:
|
|
19
|
+
"bg-[#F2F2F2] hover:text-accent-foreground dark:hover:bg-accent/50",
|
|
20
|
+
link: "text-primary underline-offset-4 hover:underline",
|
|
21
|
+
green: "text-[#107E12] bg-white border border-[#107E12]",
|
|
22
|
+
},
|
|
23
|
+
size: {
|
|
24
|
+
default: "h-9 px-4 py-2 has-[>svg]:px-3",
|
|
25
|
+
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
|
|
26
|
+
lg: "h-[56px] rounded-md px-6 has-[>svg]:px-4",
|
|
27
|
+
icon: "size-9",
|
|
28
|
+
"icon-sm": "size-8",
|
|
29
|
+
"icon-lg": "size-10",
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
defaultVariants: {
|
|
33
|
+
variant: "default",
|
|
34
|
+
size: "default",
|
|
35
|
+
},
|
|
36
|
+
}
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
function Button({
|
|
40
|
+
className,
|
|
41
|
+
variant,
|
|
42
|
+
size,
|
|
43
|
+
asChild = false,
|
|
44
|
+
...props
|
|
45
|
+
}: React.ComponentProps<"button"> &
|
|
46
|
+
VariantProps<typeof buttonVariants> & {
|
|
47
|
+
asChild?: boolean
|
|
48
|
+
}) {
|
|
49
|
+
const Comp = asChild ? Slot : "button"
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<Comp
|
|
53
|
+
data-slot="button"
|
|
54
|
+
className={cn(buttonVariants({ variant, size, className }))}
|
|
55
|
+
{...props}
|
|
56
|
+
/>
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export { Button, buttonVariants }
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
|
|
3
|
+
import { CheckIcon } from "lucide-react"
|
|
4
|
+
import { cn } from "../../lib/utils"
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
function Checkbox({
|
|
8
|
+
className,
|
|
9
|
+
...props
|
|
10
|
+
}: React.ComponentProps<typeof CheckboxPrimitive.Root>) {
|
|
11
|
+
return (
|
|
12
|
+
<CheckboxPrimitive.Root
|
|
13
|
+
data-slot="checkbox"
|
|
14
|
+
className={cn(
|
|
15
|
+
"peer border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
|
|
16
|
+
className
|
|
17
|
+
)}
|
|
18
|
+
{...props}
|
|
19
|
+
>
|
|
20
|
+
<CheckboxPrimitive.Indicator
|
|
21
|
+
data-slot="checkbox-indicator"
|
|
22
|
+
className="flex items-center justify-center text-current transition-none"
|
|
23
|
+
>
|
|
24
|
+
<CheckIcon className="size-3.5" />
|
|
25
|
+
</CheckboxPrimitive.Indicator>
|
|
26
|
+
</CheckboxPrimitive.Root>
|
|
27
|
+
)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export { Checkbox }
|