@b3dotfun/sdk 0.0.65-test.3 → 0.0.65-test.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/anyspend/react/components/AnySpend.js +1 -1
- package/dist/cjs/anyspend/react/components/common/CryptoPaymentMethod.js +1 -1
- package/dist/cjs/anyspend/react/components/common/PanelOnrampPayment.js +1 -1
- package/dist/cjs/global-account/react/components/AvatarEditor/AvatarEditor.d.ts +1 -0
- package/dist/cjs/global-account/react/components/AvatarEditor/AvatarEditor.js +71 -4
- package/dist/cjs/global-account/react/components/B3DynamicModal.js +1 -1
- package/dist/cjs/global-account/react/components/ManageAccount/HomeActions.js +1 -1
- package/dist/cjs/global-account/react/components/ManageAccount/ProfileSection.js +1 -1
- package/dist/cjs/global-account/react/components/ManageAccount/SettingsContent.js +1 -1
- package/dist/cjs/global-account/react/components/ModalHeader/ModalHeader.d.ts +2 -1
- package/dist/cjs/global-account/react/components/ModalHeader/ModalHeader.js +2 -2
- package/dist/cjs/global-account/react/components/ui/drawer.js +1 -1
- package/dist/esm/anyspend/react/components/AnySpend.js +1 -1
- package/dist/esm/anyspend/react/components/common/CryptoPaymentMethod.js +1 -1
- package/dist/esm/anyspend/react/components/common/PanelOnrampPayment.js +1 -1
- package/dist/esm/global-account/react/components/AvatarEditor/AvatarEditor.d.ts +1 -0
- package/dist/esm/global-account/react/components/AvatarEditor/AvatarEditor.js +72 -5
- package/dist/esm/global-account/react/components/B3DynamicModal.js +1 -1
- package/dist/esm/global-account/react/components/ManageAccount/HomeActions.js +1 -1
- package/dist/esm/global-account/react/components/ManageAccount/ProfileSection.js +1 -1
- package/dist/esm/global-account/react/components/ManageAccount/SettingsContent.js +1 -1
- package/dist/esm/global-account/react/components/ModalHeader/ModalHeader.d.ts +2 -1
- package/dist/esm/global-account/react/components/ModalHeader/ModalHeader.js +2 -2
- package/dist/esm/global-account/react/components/ui/drawer.js +1 -1
- package/dist/styles/index.css +1 -1
- package/dist/types/global-account/react/components/AvatarEditor/AvatarEditor.d.ts +1 -0
- package/dist/types/global-account/react/components/ModalHeader/ModalHeader.d.ts +2 -1
- package/package.json +2 -1
- package/src/anyspend/react/components/AnySpend.tsx +1 -1
- package/src/anyspend/react/components/common/CryptoPaymentMethod.tsx +1 -1
- package/src/anyspend/react/components/common/PanelOnrampPayment.tsx +1 -1
- package/src/global-account/react/components/AvatarEditor/AvatarEditor.tsx +123 -6
- package/src/global-account/react/components/B3DynamicModal.tsx +1 -1
- package/src/global-account/react/components/ManageAccount/HomeActions.tsx +1 -1
- package/src/global-account/react/components/ManageAccount/ProfileSection.tsx +6 -1
- package/src/global-account/react/components/ManageAccount/SettingsContent.tsx +1 -1
- package/src/global-account/react/components/ModalHeader/ModalHeader.tsx +16 -8
- package/src/global-account/react/components/ui/drawer.tsx +1 -1
|
@@ -7,7 +7,10 @@ import { cn } from "@b3dotfun/sdk/shared/utils/cn";
|
|
|
7
7
|
import { debugB3React } from "@b3dotfun/sdk/shared/utils/debug";
|
|
8
8
|
import { client } from "@b3dotfun/sdk/shared/utils/thirdweb";
|
|
9
9
|
import { Loader2, Upload, X } from "lucide-react";
|
|
10
|
-
import { useRef, useState } from "react";
|
|
10
|
+
import { useCallback, useRef, useState } from "react";
|
|
11
|
+
import type { Area } from "react-easy-crop";
|
|
12
|
+
import Cropper from "react-easy-crop";
|
|
13
|
+
import "react-easy-crop/react-easy-crop.css";
|
|
11
14
|
import { toast } from "sonner";
|
|
12
15
|
import { useActiveAccount } from "thirdweb/react";
|
|
13
16
|
import { upload } from "thirdweb/storage";
|
|
@@ -17,6 +20,16 @@ import ModalHeader from "../ModalHeader/ModalHeader";
|
|
|
17
20
|
|
|
18
21
|
const debug = debugB3React("AvatarEditor");
|
|
19
22
|
|
|
23
|
+
// Helper function to create an image element from a URL
|
|
24
|
+
const createImage = (url: string): Promise<HTMLImageElement> =>
|
|
25
|
+
new Promise((resolve, reject) => {
|
|
26
|
+
const image = new Image();
|
|
27
|
+
image.addEventListener("load", () => resolve(image));
|
|
28
|
+
image.addEventListener("error", error => reject(error));
|
|
29
|
+
image.setAttribute("crossOrigin", "anonymous");
|
|
30
|
+
image.src = url;
|
|
31
|
+
});
|
|
32
|
+
|
|
20
33
|
interface AvatarEditorProps {
|
|
21
34
|
onSetAvatar?: () => void;
|
|
22
35
|
className?: string;
|
|
@@ -33,6 +46,9 @@ export function AvatarEditor({ onSetAvatar, className }: AvatarEditorProps) {
|
|
|
33
46
|
const [previewUrl, setPreviewUrl] = useState<string | null>(null);
|
|
34
47
|
const [isSaving, setIsSaving] = useState(false);
|
|
35
48
|
const [isDragging, setIsDragging] = useState(false);
|
|
49
|
+
const [crop, setCrop] = useState({ x: 0, y: 0 });
|
|
50
|
+
const [zoom, setZoom] = useState(1);
|
|
51
|
+
const [croppedAreaPixels, setCroppedAreaPixels] = useState<Area | null>(null);
|
|
36
52
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
37
53
|
const { setUser, user, partnerId } = useB3();
|
|
38
54
|
const setB3ModalContentType = useModalStore(state => state.setB3ModalContentType);
|
|
@@ -50,6 +66,48 @@ export function AvatarEditor({ onSetAvatar, className }: AvatarEditorProps) {
|
|
|
50
66
|
const currentAvatar = validateImageUrl(rawCurrentAvatar);
|
|
51
67
|
const safePreviewUrl = validateImageUrl(previewUrl);
|
|
52
68
|
|
|
69
|
+
const onCropComplete = useCallback((_croppedArea: Area, croppedAreaPixels: Area) => {
|
|
70
|
+
setCroppedAreaPixels(croppedAreaPixels);
|
|
71
|
+
}, []);
|
|
72
|
+
|
|
73
|
+
const createCroppedImage = async (imageSrc: string, pixelCrop: Area): Promise<Blob> => {
|
|
74
|
+
const image = await createImage(imageSrc);
|
|
75
|
+
const canvas = document.createElement("canvas");
|
|
76
|
+
const ctx = canvas.getContext("2d");
|
|
77
|
+
|
|
78
|
+
if (!ctx) {
|
|
79
|
+
throw new Error("Failed to get canvas context");
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Set canvas size to the crop area
|
|
83
|
+
canvas.width = pixelCrop.width;
|
|
84
|
+
canvas.height = pixelCrop.height;
|
|
85
|
+
|
|
86
|
+
// Draw the cropped image
|
|
87
|
+
ctx.drawImage(
|
|
88
|
+
image,
|
|
89
|
+
pixelCrop.x,
|
|
90
|
+
pixelCrop.y,
|
|
91
|
+
pixelCrop.width,
|
|
92
|
+
pixelCrop.height,
|
|
93
|
+
0,
|
|
94
|
+
0,
|
|
95
|
+
pixelCrop.width,
|
|
96
|
+
pixelCrop.height,
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
// Return as blob
|
|
100
|
+
return new Promise((resolve, reject) => {
|
|
101
|
+
canvas.toBlob(blob => {
|
|
102
|
+
if (!blob) {
|
|
103
|
+
reject(new Error("Canvas is empty"));
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
resolve(blob);
|
|
107
|
+
}, "image/jpeg");
|
|
108
|
+
});
|
|
109
|
+
};
|
|
110
|
+
|
|
53
111
|
const handleFileSelect = (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
54
112
|
const file = event.target.files?.[0];
|
|
55
113
|
if (file) {
|
|
@@ -87,6 +145,10 @@ export function AvatarEditor({ onSetAvatar, className }: AvatarEditorProps) {
|
|
|
87
145
|
if (fileInputRef.current) {
|
|
88
146
|
fileInputRef.current.value = "";
|
|
89
147
|
}
|
|
148
|
+
// Reset crop state
|
|
149
|
+
setCrop({ x: 0, y: 0 });
|
|
150
|
+
setZoom(1);
|
|
151
|
+
setCroppedAreaPixels(null);
|
|
90
152
|
};
|
|
91
153
|
|
|
92
154
|
const handleSaveChanges = async () => {
|
|
@@ -99,8 +161,20 @@ export function AvatarEditor({ onSetAvatar, className }: AvatarEditorProps) {
|
|
|
99
161
|
try {
|
|
100
162
|
let fileToUpload: File | null = null;
|
|
101
163
|
|
|
102
|
-
// If user uploaded a new file
|
|
103
|
-
if (selectedFile) {
|
|
164
|
+
// If user uploaded a new file and cropped it
|
|
165
|
+
if (selectedFile && previewUrl && croppedAreaPixels) {
|
|
166
|
+
try {
|
|
167
|
+
const croppedBlob = await createCroppedImage(previewUrl, croppedAreaPixels);
|
|
168
|
+
const extension = selectedFile.name.split(".").pop() || "jpg";
|
|
169
|
+
fileToUpload = new File([croppedBlob], `avatar-cropped.${extension}`, { type: "image/jpeg" });
|
|
170
|
+
} catch (error) {
|
|
171
|
+
debug("Error cropping image:", error);
|
|
172
|
+
toast.error("Failed to crop image. Please try again.");
|
|
173
|
+
setIsSaving(false);
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
} else if (selectedFile) {
|
|
177
|
+
// Fallback if no crop was made
|
|
104
178
|
fileToUpload = selectedFile;
|
|
105
179
|
} else if (selectedProfileType && selectedAvatar) {
|
|
106
180
|
// User selected from existing profile avatars
|
|
@@ -319,7 +393,7 @@ export function AvatarEditor({ onSetAvatar, className }: AvatarEditorProps) {
|
|
|
319
393
|
{/* Upload Image Button */}
|
|
320
394
|
<button
|
|
321
395
|
onClick={handleUploadImageClick}
|
|
322
|
-
className="font-inter
|
|
396
|
+
className="font-inter mb-6 flex w-full items-center justify-center gap-2 rounded-xl border border-[#e4e4e7] bg-white px-4 py-3 text-sm font-semibold text-[#18181b] shadow-sm transition-colors hover:bg-[#f4f4f5]"
|
|
323
397
|
>
|
|
324
398
|
<Upload className="h-4 w-4" />
|
|
325
399
|
Upload image
|
|
@@ -419,13 +493,56 @@ export function AvatarEditor({ onSetAvatar, className }: AvatarEditorProps) {
|
|
|
419
493
|
</div>
|
|
420
494
|
) : (
|
|
421
495
|
<div className="mb-6 w-full">
|
|
422
|
-
<div className="aspect-square w-full overflow-hidden rounded-xl bg-[#f4f4f5]">
|
|
496
|
+
<div className="relative aspect-square w-full overflow-hidden rounded-xl bg-[#f4f4f5]">
|
|
423
497
|
{safePreviewUrl ? (
|
|
424
|
-
|
|
498
|
+
<>
|
|
499
|
+
<Cropper
|
|
500
|
+
image={safePreviewUrl}
|
|
501
|
+
crop={crop}
|
|
502
|
+
zoom={zoom}
|
|
503
|
+
aspect={1}
|
|
504
|
+
onCropChange={setCrop}
|
|
505
|
+
onCropComplete={onCropComplete}
|
|
506
|
+
onZoomChange={setZoom}
|
|
507
|
+
cropShape="rect"
|
|
508
|
+
showGrid={false}
|
|
509
|
+
style={{
|
|
510
|
+
containerStyle: {
|
|
511
|
+
width: "100%",
|
|
512
|
+
height: "100%",
|
|
513
|
+
backgroundColor: "#f4f4f5",
|
|
514
|
+
},
|
|
515
|
+
cropAreaStyle: {
|
|
516
|
+
border: "2px solid #3368ef",
|
|
517
|
+
borderRadius: "0px",
|
|
518
|
+
},
|
|
519
|
+
}}
|
|
520
|
+
/>
|
|
521
|
+
<button
|
|
522
|
+
onClick={handleRemovePreview}
|
|
523
|
+
className="absolute right-4 top-4 z-10 flex h-8 w-8 items-center justify-center rounded-full bg-[#51525c] text-white transition-colors hover:bg-[#71717a]"
|
|
524
|
+
>
|
|
525
|
+
<X className="h-4 w-4" />
|
|
526
|
+
</button>
|
|
527
|
+
</>
|
|
425
528
|
) : (
|
|
426
529
|
<div className="bg-b3-primary-wash h-full w-full" />
|
|
427
530
|
)}
|
|
428
531
|
</div>
|
|
532
|
+
{safePreviewUrl && (
|
|
533
|
+
<div className="mt-4 flex items-center gap-3">
|
|
534
|
+
<label className="flex-shrink-0 text-sm font-semibold text-[#475467]">Zoom</label>
|
|
535
|
+
<input
|
|
536
|
+
type="range"
|
|
537
|
+
min={1}
|
|
538
|
+
max={3}
|
|
539
|
+
step={0.1}
|
|
540
|
+
value={zoom}
|
|
541
|
+
onChange={e => setZoom(Number(e.target.value))}
|
|
542
|
+
className="flex-1 accent-[#3368ef]"
|
|
543
|
+
/>
|
|
544
|
+
</div>
|
|
545
|
+
)}
|
|
429
546
|
</div>
|
|
430
547
|
)}
|
|
431
548
|
</>
|
|
@@ -186,7 +186,7 @@ export function B3DynamicModal() {
|
|
|
186
186
|
<ModalDescription className="sr-only hidden">{contentType?.type || "Modal Body"}</ModalDescription>
|
|
187
187
|
|
|
188
188
|
<div className={cn("no-scrollbar max-h-[90dvh] overflow-auto sm:max-h-[80dvh]")}>
|
|
189
|
-
{
|
|
189
|
+
{!hideCloseButton && (
|
|
190
190
|
<button
|
|
191
191
|
onClick={navigateBack}
|
|
192
192
|
className="flex items-center gap-2 px-6 py-4 text-gray-600 transition-colors hover:text-gray-900 dark:text-gray-400 dark:hover:text-white"
|
|
@@ -41,7 +41,7 @@ const HomeActionButton = ({
|
|
|
41
41
|
return (
|
|
42
42
|
<Button
|
|
43
43
|
className={cn(
|
|
44
|
-
"border-b3-line hover:border-b3-primary-blue
|
|
44
|
+
"border-b3-line hover:border-b3-primary-blue flex h-[84px] w-full flex-col items-center justify-center gap-2 rounded-2xl border-[1.5px] bg-[#FAFAFA] shadow-[0_0_0_1px_rgba(10,13,18,0.18)_inset,0_-2px_0_0_rgba(10,13,18,0.05)_inset,0_1px_2px_0_rgba(10,13,18,0.05)] hover:bg-[#FAFAFA]",
|
|
45
45
|
customClass,
|
|
46
46
|
)}
|
|
47
47
|
onClick={onClick}
|
|
@@ -49,7 +49,12 @@ const ProfileSection = () => {
|
|
|
49
49
|
<div className="flex items-center justify-between px-5 py-6">
|
|
50
50
|
<div className="global-account-profile flex items-center gap-4">
|
|
51
51
|
<div className="global-account-profile-avatar relative">
|
|
52
|
-
<IPFSMediaRenderer
|
|
52
|
+
<IPFSMediaRenderer
|
|
53
|
+
src={avatarSrc}
|
|
54
|
+
alt="Profile Avatar"
|
|
55
|
+
className="border-b3-line border-1 bg-b3-primary-wash size-14 rounded-full border"
|
|
56
|
+
/>
|
|
57
|
+
|
|
53
58
|
<button
|
|
54
59
|
onClick={handleEditAvatar}
|
|
55
60
|
className="border-b3-background hover:bg-b3-grey/80 absolute -bottom-1 -right-1 flex size-6 items-center justify-center rounded-full border-4 bg-[#a0a0ab] transition-colors"
|
|
@@ -50,7 +50,7 @@ const SettingsContent = ({
|
|
|
50
50
|
|
|
51
51
|
return (
|
|
52
52
|
<div className="flex h-[470px] flex-col">
|
|
53
|
-
<ModalHeader title="Settings" />
|
|
53
|
+
<ModalHeader showBackButton={false} showCloseButton={false} title="Settings" />
|
|
54
54
|
|
|
55
55
|
{/* Profile Section */}
|
|
56
56
|
<div className="p-5">
|
|
@@ -3,6 +3,7 @@ import { ChevronDown, X } from "lucide-react";
|
|
|
3
3
|
import { useModalStore } from "../../stores";
|
|
4
4
|
|
|
5
5
|
const ModalHeader = ({
|
|
6
|
+
showBackButton = true,
|
|
6
7
|
handleBack,
|
|
7
8
|
handleClose,
|
|
8
9
|
title,
|
|
@@ -11,6 +12,7 @@ const ModalHeader = ({
|
|
|
11
12
|
className,
|
|
12
13
|
showBackWord = false,
|
|
13
14
|
}: {
|
|
15
|
+
showBackButton?: boolean;
|
|
14
16
|
handleBack?: () => void;
|
|
15
17
|
handleClose?: () => void;
|
|
16
18
|
title: string;
|
|
@@ -26,21 +28,27 @@ const ModalHeader = ({
|
|
|
26
28
|
<div
|
|
27
29
|
className={cn("flex h-16 items-center justify-between border-b border-[#e4e4e7] bg-white px-5 py-3", className)}
|
|
28
30
|
>
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
31
|
+
{showBackButton ? (
|
|
32
|
+
<button
|
|
33
|
+
onClick={handleBack || navigateBack}
|
|
34
|
+
className="flex h-6 w-6 items-center justify-center transition-opacity hover:opacity-70"
|
|
35
|
+
>
|
|
36
|
+
<ChevronDown className="h-6 w-6 rotate-90 text-[#51525c]" />
|
|
37
|
+
{showBackWord && <span className="text-sm font-medium">Back</span>}
|
|
38
|
+
</button>
|
|
39
|
+
) : (
|
|
40
|
+
<div className="w-2" />
|
|
41
|
+
)}
|
|
36
42
|
<p className="font-inter text-lg font-semibold leading-7 text-[#18181b]">{title}</p>
|
|
37
|
-
{showCloseButton
|
|
43
|
+
{showCloseButton ? (
|
|
38
44
|
<button
|
|
39
45
|
onClick={handleClose || (() => setB3ModalOpen(false))}
|
|
40
46
|
className="flex h-6 w-6 items-center justify-center transition-opacity hover:opacity-70"
|
|
41
47
|
>
|
|
42
48
|
<X className="h-6 w-6 text-[#51525c]" />
|
|
43
49
|
</button>
|
|
50
|
+
) : (
|
|
51
|
+
<div className="w-2" />
|
|
44
52
|
)}
|
|
45
53
|
{children}
|
|
46
54
|
</div>
|
|
@@ -35,7 +35,7 @@ const DrawerContent = React.forwardRef<
|
|
|
35
35
|
<DrawerPrimitive.Content
|
|
36
36
|
ref={ref}
|
|
37
37
|
className={cn(
|
|
38
|
-
"bg-b3-react-background fixed inset-x-0 bottom-0 z-50 mt-24 flex h-auto flex-col rounded-t-[10px] border py-6",
|
|
38
|
+
"bg-b3-react-background fixed inset-x-0 bottom-0 z-50 mt-24 flex h-auto flex-col rounded-t-[10px] border py-6 pt-5",
|
|
39
39
|
className,
|
|
40
40
|
)}
|
|
41
41
|
{...props}
|