@ews-admin/global-design-system 1.1.20 → 1.1.22
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/components/Input/Input.d.ts +8 -0
- package/dist/components/Input/Input.d.ts.map +1 -1
- package/dist/components/Modal/Modal.d.ts +5 -1
- package/dist/components/Modal/Modal.d.ts.map +1 -1
- package/dist/components/ProfileImageUpload/ProfileImageUpload.d.ts +47 -0
- package/dist/components/ProfileImageUpload/ProfileImageUpload.d.ts.map +1 -0
- package/dist/components/ProfileImageUpload/index.d.ts +3 -0
- package/dist/components/ProfileImageUpload/index.d.ts.map +1 -0
- package/dist/index.css +1 -1
- package/dist/index.d.ts +80 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.esm.css +1 -1
- package/dist/index.esm.js +161 -26
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +161 -24
- package/dist/index.js.map +1 -1
- package/dist/utils/index.d.ts +19 -0
- package/dist/utils/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/Input/Input.tsx +28 -2
- package/src/components/Modal/Modal.tsx +20 -1
- package/src/components/ProfileImageUpload/ProfileImageUpload.tsx +231 -0
- package/src/components/ProfileImageUpload/index.ts +2 -0
- package/src/index.ts +5 -0
- package/src/utils/index.ts +31 -0
package/dist/utils/index.d.ts
CHANGED
|
@@ -49,4 +49,23 @@ export declare const formatNumeric: (value: string) => string;
|
|
|
49
49
|
* @returns Boolean indicating if the phone number is valid
|
|
50
50
|
*/
|
|
51
51
|
export declare function isValidPhoneNumber(value: string): boolean;
|
|
52
|
+
/**
|
|
53
|
+
* Blood type enum matching backend BloodTypeEnum values
|
|
54
|
+
*/
|
|
55
|
+
export declare enum BloodType {
|
|
56
|
+
A_POSITIVE = "A+",
|
|
57
|
+
A_NEGATIVE = "A-",
|
|
58
|
+
B_POSITIVE = "B+",
|
|
59
|
+
B_NEGATIVE = "B-",
|
|
60
|
+
AB_POSITIVE = "AB+",
|
|
61
|
+
AB_NEGATIVE = "AB-",
|
|
62
|
+
O_POSITIVE = "O+",
|
|
63
|
+
O_NEGATIVE = "O-",
|
|
64
|
+
UNKNOWN = "UNKNOWN"
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Ordered list of all blood type values.
|
|
68
|
+
* Use this to build select/dropdown options.
|
|
69
|
+
*/
|
|
70
|
+
export declare const BLOOD_TYPES: BloodType[];
|
|
52
71
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAQ,KAAK,UAAU,EAAE,MAAM,MAAM,CAAC;AAE7C;;GAEG;AACH,eAAO,MAAM,QAAQ,QAAQ,CAAC;AAE9B;;;;GAIG;AACH,wBAAgB,EAAE,CAAC,GAAG,MAAM,EAAE,UAAU,EAAE,UAEzC;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,SAAW,GAAG,MAAM,CAK1E;AAED;;;;;GAKG;AACH,wBAAgB,UAAU,CACxB,IAAI,EAAE,IAAI,GAAG,MAAM,GAAG,MAAM,EAC5B,OAAO,CAAC,EAAE,IAAI,CAAC,qBAAqB,GACnC,MAAM,CAQR;AAED;;;;;GAKG;AACH,wBAAgB,QAAQ,CAAC,CAAC,SAAS,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,OAAO,EAChE,IAAI,EAAE,CAAC,EACP,IAAI,EAAE,MAAM,GACX,CAAC,GAAG,IAAI,EAAE,UAAU,CAAC,CAAC,CAAC,KAAK,IAAI,CAMlC;AAED;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,MAAM,SAAQ,GAAG,MAAM,CAEjD;AAED;;;;GAIG;AACH,eAAO,MAAM,aAAa,GAAI,OAAO,MAAM,KAAG,MAE7C,CAAC;AAEF;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAMzD"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAQ,KAAK,UAAU,EAAE,MAAM,MAAM,CAAC;AAE7C;;GAEG;AACH,eAAO,MAAM,QAAQ,QAAQ,CAAC;AAE9B;;;;GAIG;AACH,wBAAgB,EAAE,CAAC,GAAG,MAAM,EAAE,UAAU,EAAE,UAEzC;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,SAAW,GAAG,MAAM,CAK1E;AAED;;;;;GAKG;AACH,wBAAgB,UAAU,CACxB,IAAI,EAAE,IAAI,GAAG,MAAM,GAAG,MAAM,EAC5B,OAAO,CAAC,EAAE,IAAI,CAAC,qBAAqB,GACnC,MAAM,CAQR;AAED;;;;;GAKG;AACH,wBAAgB,QAAQ,CAAC,CAAC,SAAS,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,OAAO,EAChE,IAAI,EAAE,CAAC,EACP,IAAI,EAAE,MAAM,GACX,CAAC,GAAG,IAAI,EAAE,UAAU,CAAC,CAAC,CAAC,KAAK,IAAI,CAMlC;AAED;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,MAAM,SAAQ,GAAG,MAAM,CAEjD;AAED;;;;GAIG;AACH,eAAO,MAAM,aAAa,GAAI,OAAO,MAAM,KAAG,MAE7C,CAAC;AAEF;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAMzD;AAED;;GAEG;AACH,oBAAY,SAAS;IACnB,UAAU,OAAO;IACjB,UAAU,OAAO;IACjB,UAAU,OAAO;IACjB,UAAU,OAAO;IACjB,WAAW,QAAQ;IACnB,WAAW,QAAQ;IACnB,UAAU,OAAO;IACjB,UAAU,OAAO;IACjB,OAAO,YAAY;CACpB;AAED;;;GAGG;AACH,eAAO,MAAM,WAAW,EAAE,SAAS,EAUlC,CAAC"}
|
package/package.json
CHANGED
|
@@ -53,6 +53,14 @@ export interface InputProps
|
|
|
53
53
|
* Icon to display after the input
|
|
54
54
|
*/
|
|
55
55
|
rightIcon?: React.ReactNode;
|
|
56
|
+
/**
|
|
57
|
+
* Text or node to display as a left addon (e.g., currency "FCFA")
|
|
58
|
+
*/
|
|
59
|
+
leftAddon?: React.ReactNode;
|
|
60
|
+
/**
|
|
61
|
+
* Text or node to display as a right addon inside the input (e.g., currency "FCFA")
|
|
62
|
+
*/
|
|
63
|
+
rightAddon?: React.ReactNode;
|
|
56
64
|
/**
|
|
57
65
|
* Whether the input should take full width
|
|
58
66
|
*/
|
|
@@ -86,6 +94,8 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
|
|
86
94
|
showPasswordToggle = false,
|
|
87
95
|
required = false,
|
|
88
96
|
countryCodeSelect,
|
|
97
|
+
leftAddon,
|
|
98
|
+
rightAddon,
|
|
89
99
|
id,
|
|
90
100
|
type = "text",
|
|
91
101
|
...props
|
|
@@ -274,13 +284,20 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
|
|
274
284
|
</div>
|
|
275
285
|
</div>
|
|
276
286
|
)}
|
|
277
|
-
{leftIcon && !countryCodeSelect && (
|
|
287
|
+
{leftIcon && !countryCodeSelect && !leftAddon && (
|
|
278
288
|
<div className="flex absolute inset-y-0 left-0 items-center pl-3 pointer-events-none">
|
|
279
289
|
<span className={cn("text-ews-gray-400", iconSizes[size])}>
|
|
280
290
|
{leftIcon}
|
|
281
291
|
</span>
|
|
282
292
|
</div>
|
|
283
293
|
)}
|
|
294
|
+
{leftAddon && !countryCodeSelect && !leftIcon && (
|
|
295
|
+
<div className="flex absolute inset-y-0 left-0 items-center pointer-events-none overflow-hidden rounded-l-md">
|
|
296
|
+
<span className="flex items-center h-full px-3 text-sm font-medium whitespace-nowrap bg-ews-gray-50 border-r border-ews-gray-300 text-ews-gray-600">
|
|
297
|
+
{leftAddon}
|
|
298
|
+
</span>
|
|
299
|
+
</div>
|
|
300
|
+
)}
|
|
284
301
|
<input
|
|
285
302
|
id={inputId}
|
|
286
303
|
type={actualType}
|
|
@@ -289,13 +306,22 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
|
|
289
306
|
variants[actualVariant],
|
|
290
307
|
sizes[size],
|
|
291
308
|
countryCodeSelect && "pl-24",
|
|
292
|
-
leftIcon && !countryCodeSelect && "pl-10",
|
|
309
|
+
leftIcon && !countryCodeSelect && !leftAddon && "pl-10",
|
|
310
|
+
leftAddon && !countryCodeSelect && !leftIcon && "pl-16",
|
|
293
311
|
(rightIcon || shouldShowPasswordToggle) && "pr-10",
|
|
312
|
+
rightAddon && !rightIcon && !shouldShowPasswordToggle && "pr-16",
|
|
294
313
|
className
|
|
295
314
|
)}
|
|
296
315
|
ref={ref}
|
|
297
316
|
{...props}
|
|
298
317
|
/>
|
|
318
|
+
{rightAddon && !rightIcon && !shouldShowPasswordToggle && (
|
|
319
|
+
<div className="flex absolute inset-y-0 right-0 items-center pr-3 pointer-events-none">
|
|
320
|
+
<span className="text-sm font-medium text-ews-gray-500">
|
|
321
|
+
{rightAddon}
|
|
322
|
+
</span>
|
|
323
|
+
</div>
|
|
324
|
+
)}
|
|
299
325
|
{rightIcon && !shouldShowPasswordToggle && (
|
|
300
326
|
<div className="flex absolute inset-y-0 right-0 items-center pr-3 pointer-events-none">
|
|
301
327
|
<span className={cn("text-ews-gray-400", iconSizes[size])}>
|
|
@@ -36,6 +36,10 @@ export interface ModalProps {
|
|
|
36
36
|
* Modal variant
|
|
37
37
|
*/
|
|
38
38
|
variant?: "error" | "warning" | "confirmation" | "info";
|
|
39
|
+
/**
|
|
40
|
+
* Modal size/width
|
|
41
|
+
*/
|
|
42
|
+
size?: "sm" | "md" | "lg" | "xl" | "2xl" | "full";
|
|
39
43
|
/**
|
|
40
44
|
* Primary action button text
|
|
41
45
|
*/
|
|
@@ -80,6 +84,7 @@ const Modal = ({
|
|
|
80
84
|
title,
|
|
81
85
|
children,
|
|
82
86
|
variant = "info",
|
|
87
|
+
size = "md",
|
|
83
88
|
primaryAction,
|
|
84
89
|
secondaryAction,
|
|
85
90
|
onPrimaryAction,
|
|
@@ -147,6 +152,18 @@ const Modal = ({
|
|
|
147
152
|
|
|
148
153
|
const variantStyles = getVariantStyles();
|
|
149
154
|
|
|
155
|
+
const getSizeClasses = () => {
|
|
156
|
+
const sizeMap = {
|
|
157
|
+
sm: "max-w-sm",
|
|
158
|
+
md: "max-w-md",
|
|
159
|
+
lg: "max-w-lg",
|
|
160
|
+
xl: "max-w-xl",
|
|
161
|
+
"2xl": "max-w-2xl",
|
|
162
|
+
full: "max-w-full",
|
|
163
|
+
};
|
|
164
|
+
return sizeMap[size] || sizeMap.md;
|
|
165
|
+
};
|
|
166
|
+
|
|
150
167
|
const handleOverlayClick = (e: React.MouseEvent) => {
|
|
151
168
|
if (e.target === e.currentTarget && closeOnOverlayClick) {
|
|
152
169
|
onClose();
|
|
@@ -164,8 +181,10 @@ const Modal = ({
|
|
|
164
181
|
{/* Modal */}
|
|
165
182
|
<div
|
|
166
183
|
className={cn(
|
|
167
|
-
"relative
|
|
184
|
+
"relative w-full bg-white rounded-lg shadow-xl transition-all transform",
|
|
168
185
|
"duration-200 animate-in fade-in-0 zoom-in-95",
|
|
186
|
+
getSizeClasses(),
|
|
187
|
+
"mx-4",
|
|
169
188
|
className
|
|
170
189
|
)}
|
|
171
190
|
role="dialog"
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import { Pencil, Trash } from "lucide-react";
|
|
2
|
+
import React, { useEffect, useRef, useState } from "react";
|
|
3
|
+
|
|
4
|
+
import { cn } from "../../utils";
|
|
5
|
+
import { Modal } from "../Modal/Modal";
|
|
6
|
+
|
|
7
|
+
export interface ProfileImageUploadProps {
|
|
8
|
+
/** Current image URL */
|
|
9
|
+
imageUrl: string;
|
|
10
|
+
/** Alt text for accessibility */
|
|
11
|
+
altText: string;
|
|
12
|
+
/** Disable editing */
|
|
13
|
+
readOnly?: boolean;
|
|
14
|
+
/** Avatar diameter */
|
|
15
|
+
size?: "sm" | "md" | "lg";
|
|
16
|
+
/**
|
|
17
|
+
* Upload progress 0–100.
|
|
18
|
+
* Progress bar is hidden when 0; auto-hides 1 s after reaching 100.
|
|
19
|
+
*/
|
|
20
|
+
uploadProgress?: number;
|
|
21
|
+
/**
|
|
22
|
+
* When true the progress bar stays visible in an indeterminate (pulsing)
|
|
23
|
+
* state for phases with no measurable progress (pre-upload / post-upload).
|
|
24
|
+
* The bar auto-hides 1 s after both `isLoading` is false and progress reached 100.
|
|
25
|
+
*/
|
|
26
|
+
isLoading?: boolean;
|
|
27
|
+
/**
|
|
28
|
+
* Whether to show the delete button.
|
|
29
|
+
* Pass `false` when `imageUrl` is a default/fallback image.
|
|
30
|
+
*/
|
|
31
|
+
showDeleteButton?: boolean;
|
|
32
|
+
/** File input accept attribute (default: "image/*") */
|
|
33
|
+
accept?: string;
|
|
34
|
+
/** Max allowed file size in MB (default: 3) */
|
|
35
|
+
maxFileSizeMB?: number;
|
|
36
|
+
/** Called when a valid file is selected */
|
|
37
|
+
onFileSelect: (file: File) => void;
|
|
38
|
+
/** Called when the selected file exceeds `maxFileSizeMB` */
|
|
39
|
+
onFileSizeExceeded?: () => void;
|
|
40
|
+
/** Called when the user confirms deletion in the modal */
|
|
41
|
+
onDeleteConfirm: () => void;
|
|
42
|
+
/** Deletion confirmation modal — title text */
|
|
43
|
+
deleteConfirmTitle: string;
|
|
44
|
+
/** Deletion confirmation modal — body text */
|
|
45
|
+
deleteConfirmMessage: string;
|
|
46
|
+
/** Deletion confirmation modal — primary button label */
|
|
47
|
+
deleteConfirmLabel: string;
|
|
48
|
+
/** Deletion confirmation modal — secondary / cancel label */
|
|
49
|
+
cancelLabel: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const SIZE_CLASSES: Record<"sm" | "md" | "lg", string> = {
|
|
53
|
+
sm: "h-10 w-10",
|
|
54
|
+
md: "h-16 w-16",
|
|
55
|
+
lg: "h-32 w-32",
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const INDICATOR_CLASSES: Record<"sm" | "md" | "lg", string> = {
|
|
59
|
+
sm: "h-2 w-2",
|
|
60
|
+
md: "h-4 w-4",
|
|
61
|
+
lg: "h-8 w-8",
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
function UploadProgressBar({
|
|
65
|
+
progress,
|
|
66
|
+
isLoading,
|
|
67
|
+
}: {
|
|
68
|
+
progress: number;
|
|
69
|
+
isLoading?: boolean;
|
|
70
|
+
}) {
|
|
71
|
+
const [visible, setVisible] = useState(false);
|
|
72
|
+
|
|
73
|
+
useEffect(() => {
|
|
74
|
+
if (isLoading) {
|
|
75
|
+
setVisible(true);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
// isLoading just turned false
|
|
79
|
+
if (progress >= 100) {
|
|
80
|
+
setVisible(true);
|
|
81
|
+
const t = setTimeout(() => setVisible(false), 1000);
|
|
82
|
+
return () => clearTimeout(t);
|
|
83
|
+
}
|
|
84
|
+
// Legacy path (no isLoading prop): driven purely by progress
|
|
85
|
+
if (progress > 0 && progress < 100) {
|
|
86
|
+
setVisible(true);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
setVisible(false);
|
|
90
|
+
}, [progress, isLoading]);
|
|
91
|
+
|
|
92
|
+
if (!visible) return null;
|
|
93
|
+
|
|
94
|
+
const indeterminate = isLoading && progress === 0;
|
|
95
|
+
|
|
96
|
+
return (
|
|
97
|
+
<div className="mt-2 h-1.5 w-32 rounded-full bg-gray-200">
|
|
98
|
+
<div
|
|
99
|
+
className={cn(
|
|
100
|
+
"h-full rounded-full bg-ews-primary transition-all duration-300",
|
|
101
|
+
indeterminate && "animate-pulse"
|
|
102
|
+
)}
|
|
103
|
+
style={{ width: indeterminate ? "35%" : `${progress}%` }}
|
|
104
|
+
/>
|
|
105
|
+
</div>
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export const ProfileImageUpload: React.FC<ProfileImageUploadProps> = ({
|
|
110
|
+
imageUrl,
|
|
111
|
+
altText,
|
|
112
|
+
readOnly = false,
|
|
113
|
+
size = "lg",
|
|
114
|
+
uploadProgress = 0,
|
|
115
|
+
isLoading = false,
|
|
116
|
+
showDeleteButton = true,
|
|
117
|
+
accept = "image/*",
|
|
118
|
+
maxFileSizeMB = 3,
|
|
119
|
+
onFileSelect,
|
|
120
|
+
onFileSizeExceeded,
|
|
121
|
+
onDeleteConfirm,
|
|
122
|
+
deleteConfirmTitle,
|
|
123
|
+
deleteConfirmMessage,
|
|
124
|
+
deleteConfirmLabel,
|
|
125
|
+
cancelLabel,
|
|
126
|
+
}) => {
|
|
127
|
+
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
128
|
+
const [isHovered, setIsHovered] = useState(false);
|
|
129
|
+
const [showConfirm, setShowConfirm] = useState(false);
|
|
130
|
+
|
|
131
|
+
const handleEditClick = () => fileInputRef.current?.click();
|
|
132
|
+
|
|
133
|
+
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
134
|
+
const file = e.target.files?.[0];
|
|
135
|
+
if (!file) return;
|
|
136
|
+
|
|
137
|
+
if (file.size > maxFileSizeMB * 1024 * 1024) {
|
|
138
|
+
onFileSizeExceeded?.();
|
|
139
|
+
e.target.value = "";
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
onFileSelect(file);
|
|
144
|
+
e.target.value = ""; // allow re-selecting the same file
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
const handleConfirmDelete = () => {
|
|
148
|
+
setShowConfirm(false);
|
|
149
|
+
onDeleteConfirm();
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
return (
|
|
153
|
+
<>
|
|
154
|
+
<div className="inline-flex flex-col items-center">
|
|
155
|
+
<div
|
|
156
|
+
className="relative"
|
|
157
|
+
onMouseEnter={() => setIsHovered(true)}
|
|
158
|
+
onMouseLeave={() => setIsHovered(false)}
|
|
159
|
+
>
|
|
160
|
+
<img
|
|
161
|
+
src={imageUrl}
|
|
162
|
+
alt={altText}
|
|
163
|
+
className={cn(
|
|
164
|
+
"rounded-full border-4 border-white object-cover",
|
|
165
|
+
SIZE_CLASSES[size]
|
|
166
|
+
)}
|
|
167
|
+
/>
|
|
168
|
+
|
|
169
|
+
{/* Online status dot */}
|
|
170
|
+
<div
|
|
171
|
+
className={cn(
|
|
172
|
+
"absolute bottom-0 right-0 rounded-full border-4 border-white bg-green-400",
|
|
173
|
+
INDICATOR_CLASSES[size]
|
|
174
|
+
)}
|
|
175
|
+
/>
|
|
176
|
+
|
|
177
|
+
<input
|
|
178
|
+
type="file"
|
|
179
|
+
ref={fileInputRef}
|
|
180
|
+
accept={accept}
|
|
181
|
+
className="hidden"
|
|
182
|
+
onChange={handleFileChange}
|
|
183
|
+
/>
|
|
184
|
+
|
|
185
|
+
{isHovered && !readOnly && !isLoading && (
|
|
186
|
+
<>
|
|
187
|
+
{/* Edit / upload button */}
|
|
188
|
+
<button
|
|
189
|
+
type="button"
|
|
190
|
+
onClick={handleEditClick}
|
|
191
|
+
className="absolute left-0 top-0 flex h-8 w-8 cursor-pointer items-center justify-center rounded-full bg-ews-primary transition-colors hover:bg-ews-secondary"
|
|
192
|
+
aria-label="Edit profile image"
|
|
193
|
+
>
|
|
194
|
+
<Pencil className="h-4 w-4 text-white" />
|
|
195
|
+
</button>
|
|
196
|
+
|
|
197
|
+
{/* Delete button — only when not a fallback/default image */}
|
|
198
|
+
{showDeleteButton && (
|
|
199
|
+
<button
|
|
200
|
+
type="button"
|
|
201
|
+
onClick={() => setShowConfirm(true)}
|
|
202
|
+
className="absolute left-0 top-10 flex h-8 w-8 cursor-pointer items-center justify-center rounded-full bg-red-600 transition-colors hover:bg-red-700"
|
|
203
|
+
aria-label="Delete profile image"
|
|
204
|
+
>
|
|
205
|
+
<Trash className="h-4 w-4 text-white" />
|
|
206
|
+
</button>
|
|
207
|
+
)}
|
|
208
|
+
</>
|
|
209
|
+
)}
|
|
210
|
+
</div>
|
|
211
|
+
|
|
212
|
+
<UploadProgressBar progress={uploadProgress} isLoading={isLoading} />
|
|
213
|
+
</div>
|
|
214
|
+
|
|
215
|
+
{/* Delete confirmation */}
|
|
216
|
+
<Modal
|
|
217
|
+
isOpen={showConfirm}
|
|
218
|
+
onClose={() => setShowConfirm(false)}
|
|
219
|
+
title={deleteConfirmTitle}
|
|
220
|
+
variant="error"
|
|
221
|
+
size="sm"
|
|
222
|
+
primaryAction={deleteConfirmLabel}
|
|
223
|
+
secondaryAction={cancelLabel}
|
|
224
|
+
onPrimaryAction={handleConfirmDelete}
|
|
225
|
+
onSecondaryAction={() => setShowConfirm(false)}
|
|
226
|
+
>
|
|
227
|
+
<p>{deleteConfirmMessage}</p>
|
|
228
|
+
</Modal>
|
|
229
|
+
</>
|
|
230
|
+
);
|
|
231
|
+
};
|
package/src/index.ts
CHANGED
|
@@ -16,6 +16,9 @@ export { MultiSearchAutocomplete } from "./components/MultiSearchAutocomplete";
|
|
|
16
16
|
export { Modal } from "./components/Modal";
|
|
17
17
|
export type { ErrorField, ErrorObject, ModalProps } from "./components/Modal";
|
|
18
18
|
|
|
19
|
+
export { ProfileImageUpload } from "./components/ProfileImageUpload";
|
|
20
|
+
export type { ProfileImageUploadProps } from "./components/ProfileImageUpload";
|
|
21
|
+
|
|
19
22
|
export { DropdownMultiSelect } from "./components/DropdownMultiSelect";
|
|
20
23
|
export type { DropdownMultiSelectProps } from "./components/DropdownMultiSelect";
|
|
21
24
|
|
|
@@ -46,6 +49,8 @@ export type { IconProps, SimpleIconProps } from "./icons";
|
|
|
46
49
|
|
|
47
50
|
// Utils
|
|
48
51
|
export {
|
|
52
|
+
BLOOD_TYPES,
|
|
53
|
+
BloodType,
|
|
49
54
|
cn,
|
|
50
55
|
debounce,
|
|
51
56
|
formatCurrency,
|
package/src/utils/index.ts
CHANGED
|
@@ -94,3 +94,34 @@ export function isValidPhoneNumber(value: string): boolean {
|
|
|
94
94
|
const phoneRegex = /^(\+\d{1,17}|\d{1,17})$/;
|
|
95
95
|
return phoneRegex.test(trimmedValue);
|
|
96
96
|
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Blood type enum matching backend BloodTypeEnum values
|
|
100
|
+
*/
|
|
101
|
+
export enum BloodType {
|
|
102
|
+
A_POSITIVE = "A+",
|
|
103
|
+
A_NEGATIVE = "A-",
|
|
104
|
+
B_POSITIVE = "B+",
|
|
105
|
+
B_NEGATIVE = "B-",
|
|
106
|
+
AB_POSITIVE = "AB+",
|
|
107
|
+
AB_NEGATIVE = "AB-",
|
|
108
|
+
O_POSITIVE = "O+",
|
|
109
|
+
O_NEGATIVE = "O-",
|
|
110
|
+
UNKNOWN = "UNKNOWN",
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Ordered list of all blood type values.
|
|
115
|
+
* Use this to build select/dropdown options.
|
|
116
|
+
*/
|
|
117
|
+
export const BLOOD_TYPES: BloodType[] = [
|
|
118
|
+
BloodType.A_POSITIVE,
|
|
119
|
+
BloodType.A_NEGATIVE,
|
|
120
|
+
BloodType.B_POSITIVE,
|
|
121
|
+
BloodType.B_NEGATIVE,
|
|
122
|
+
BloodType.AB_POSITIVE,
|
|
123
|
+
BloodType.AB_NEGATIVE,
|
|
124
|
+
BloodType.O_POSITIVE,
|
|
125
|
+
BloodType.O_NEGATIVE,
|
|
126
|
+
BloodType.UNKNOWN,
|
|
127
|
+
];
|