@ews-admin/global-design-system 1.1.21 → 1.1.23
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/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 +107 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.esm.css +1 -1
- package/dist/index.esm.js +202 -24
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +203 -22
- package/dist/index.js.map +1 -1
- package/dist/utils/env-config.d.ts +32 -0
- package/dist/utils/env-config.d.ts.map +1 -0
- package/dist/utils/index.d.ts +21 -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/ProfileImageUpload/ProfileImageUpload.tsx +231 -0
- package/src/components/ProfileImageUpload/index.ts +2 -0
- package/src/index.ts +7 -0
- package/src/utils/env-config.ts +82 -0
- package/src/utils/index.ts +38 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export type Environment = "local" | "dev" | "qa" | "prod";
|
|
2
|
+
export interface EnvConfigOverrides {
|
|
3
|
+
bffUrl?: string;
|
|
4
|
+
loginBffUrl?: string;
|
|
5
|
+
appUrl?: string;
|
|
6
|
+
loginUrl?: string;
|
|
7
|
+
dashboardUrl?: string;
|
|
8
|
+
adminUrl?: string;
|
|
9
|
+
}
|
|
10
|
+
export interface EnvConfig {
|
|
11
|
+
env: Environment;
|
|
12
|
+
bffUrl: string;
|
|
13
|
+
loginBffUrl: string;
|
|
14
|
+
appUrl: string;
|
|
15
|
+
loginUrl: string;
|
|
16
|
+
dashboardUrl: string;
|
|
17
|
+
adminUrl: string;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Creates an environment configuration with all derived URLs.
|
|
21
|
+
*
|
|
22
|
+
* For deployed environments (dev, qa, prod), only the environment name is needed
|
|
23
|
+
* — all URLs follow a predictable convention.
|
|
24
|
+
*
|
|
25
|
+
* For local development, sensible defaults are provided but can be overridden
|
|
26
|
+
* (e.g. to point BFF URL at a mock server).
|
|
27
|
+
*
|
|
28
|
+
* @param env - The environment name (VITE_ENV value)
|
|
29
|
+
* @param overrides - Optional URL overrides (e.g. for local mock servers)
|
|
30
|
+
*/
|
|
31
|
+
export declare function createEnvConfig(env: string, overrides?: EnvConfigOverrides): EnvConfig;
|
|
32
|
+
//# sourceMappingURL=env-config.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"env-config.d.ts","sourceRoot":"","sources":["../../src/utils/env-config.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,WAAW,GAAG,OAAO,GAAG,KAAK,GAAG,IAAI,GAAG,MAAM,CAAC;AAE1D,MAAM,WAAW,kBAAkB;IACjC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,SAAS;IACxB,GAAG,EAAE,WAAW,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAsCD;;;;;;;;;;;GAWG;AACH,wBAAgB,eAAe,CAC7B,GAAG,EAAE,MAAM,EACX,SAAS,CAAC,EAAE,kBAAkB,GAC7B,SAAS,CASX"}
|
package/dist/utils/index.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { type ClassValue } from "clsx";
|
|
2
|
+
export { createEnvConfig } from "./env-config";
|
|
3
|
+
export type { EnvConfig, EnvConfigOverrides, Environment, } from "./env-config";
|
|
2
4
|
/**
|
|
3
5
|
* Default currency for price formatting
|
|
4
6
|
*/
|
|
@@ -49,4 +51,23 @@ export declare const formatNumeric: (value: string) => string;
|
|
|
49
51
|
* @returns Boolean indicating if the phone number is valid
|
|
50
52
|
*/
|
|
51
53
|
export declare function isValidPhoneNumber(value: string): boolean;
|
|
54
|
+
/**
|
|
55
|
+
* Blood type enum matching backend BloodTypeEnum values
|
|
56
|
+
*/
|
|
57
|
+
export declare enum BloodType {
|
|
58
|
+
A_POSITIVE = "A+",
|
|
59
|
+
A_NEGATIVE = "A-",
|
|
60
|
+
B_POSITIVE = "B+",
|
|
61
|
+
B_NEGATIVE = "B-",
|
|
62
|
+
AB_POSITIVE = "AB+",
|
|
63
|
+
AB_NEGATIVE = "AB-",
|
|
64
|
+
O_POSITIVE = "O+",
|
|
65
|
+
O_NEGATIVE = "O-",
|
|
66
|
+
UNKNOWN = "UNKNOWN"
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Ordered list of all blood type values.
|
|
70
|
+
* Use this to build select/dropdown options.
|
|
71
|
+
*/
|
|
72
|
+
export declare const BLOOD_TYPES: BloodType[];
|
|
52
73
|
//# 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,OAAO,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAC/C,YAAY,EACV,SAAS,EACT,kBAAkB,EAClB,WAAW,GACZ,MAAM,cAAc,CAAC;AAEtB;;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])}>
|
|
@@ -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,7 +49,10 @@ export type { IconProps, SimpleIconProps } from "./icons";
|
|
|
46
49
|
|
|
47
50
|
// Utils
|
|
48
51
|
export {
|
|
52
|
+
BLOOD_TYPES,
|
|
53
|
+
BloodType,
|
|
49
54
|
cn,
|
|
55
|
+
createEnvConfig,
|
|
50
56
|
debounce,
|
|
51
57
|
formatCurrency,
|
|
52
58
|
formatDate,
|
|
@@ -54,6 +60,7 @@ export {
|
|
|
54
60
|
generateId,
|
|
55
61
|
isValidPhoneNumber,
|
|
56
62
|
} from "./utils";
|
|
63
|
+
export type { EnvConfig, EnvConfigOverrides, Environment } from "./utils";
|
|
57
64
|
|
|
58
65
|
// Hooks
|
|
59
66
|
export { useDebounce, useDebouncedCallback, useSelectField } from "./hooks";
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
export type Environment = "local" | "dev" | "qa" | "prod";
|
|
2
|
+
|
|
3
|
+
export interface EnvConfigOverrides {
|
|
4
|
+
bffUrl?: string;
|
|
5
|
+
loginBffUrl?: string;
|
|
6
|
+
appUrl?: string;
|
|
7
|
+
loginUrl?: string;
|
|
8
|
+
dashboardUrl?: string;
|
|
9
|
+
adminUrl?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface EnvConfig {
|
|
13
|
+
env: Environment;
|
|
14
|
+
bffUrl: string;
|
|
15
|
+
loginBffUrl: string;
|
|
16
|
+
appUrl: string;
|
|
17
|
+
loginUrl: string;
|
|
18
|
+
dashboardUrl: string;
|
|
19
|
+
adminUrl: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const LOCAL_PORTS = {
|
|
23
|
+
bff: 8082,
|
|
24
|
+
loginBff: 8080,
|
|
25
|
+
app: 3000,
|
|
26
|
+
login: 3001,
|
|
27
|
+
dashboard: 3002,
|
|
28
|
+
admin: 3008,
|
|
29
|
+
} as const;
|
|
30
|
+
|
|
31
|
+
function buildUrls(environment: Environment): Omit<EnvConfig, "env"> {
|
|
32
|
+
if (environment === "local") {
|
|
33
|
+
const base = "http://local";
|
|
34
|
+
const domain = "medecine360local.com";
|
|
35
|
+
return {
|
|
36
|
+
bffUrl: `${base}.api.${domain}:${LOCAL_PORTS.bff}/bff/api/v1`,
|
|
37
|
+
loginBffUrl: `${base}.api.${domain}:${LOCAL_PORTS.loginBff}/login-bff/api/v1`,
|
|
38
|
+
appUrl: `${base}.app.${domain}:${LOCAL_PORTS.app}`,
|
|
39
|
+
loginUrl: `${base}.login.${domain}:${LOCAL_PORTS.login}`,
|
|
40
|
+
dashboardUrl: `${base}.dashboard.${domain}:${LOCAL_PORTS.dashboard}`,
|
|
41
|
+
adminUrl: `${base}.admin.${domain}:${LOCAL_PORTS.admin}`,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const prefix = environment === "prod" ? "" : `${environment}.`;
|
|
46
|
+
const domain = "medecine360.com";
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
bffUrl: `https://${prefix}api.${domain}/bff/api/v1`,
|
|
50
|
+
loginBffUrl: `https://${prefix}api.${domain}/login-bff/api/v1`,
|
|
51
|
+
appUrl: `https://${prefix}app.${domain}`,
|
|
52
|
+
loginUrl: `https://${prefix}login.${domain}`,
|
|
53
|
+
dashboardUrl: `https://${prefix}dashboard.${domain}`,
|
|
54
|
+
adminUrl: `https://${prefix}admin.${domain}`,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Creates an environment configuration with all derived URLs.
|
|
60
|
+
*
|
|
61
|
+
* For deployed environments (dev, qa, prod), only the environment name is needed
|
|
62
|
+
* — all URLs follow a predictable convention.
|
|
63
|
+
*
|
|
64
|
+
* For local development, sensible defaults are provided but can be overridden
|
|
65
|
+
* (e.g. to point BFF URL at a mock server).
|
|
66
|
+
*
|
|
67
|
+
* @param env - The environment name (VITE_ENV value)
|
|
68
|
+
* @param overrides - Optional URL overrides (e.g. for local mock servers)
|
|
69
|
+
*/
|
|
70
|
+
export function createEnvConfig(
|
|
71
|
+
env: string,
|
|
72
|
+
overrides?: EnvConfigOverrides
|
|
73
|
+
): EnvConfig {
|
|
74
|
+
const environment = (env || "local") as Environment;
|
|
75
|
+
const derived = buildUrls(environment);
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
env: environment,
|
|
79
|
+
...derived,
|
|
80
|
+
...overrides,
|
|
81
|
+
};
|
|
82
|
+
}
|
package/src/utils/index.ts
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import { clsx, type ClassValue } from "clsx";
|
|
2
2
|
|
|
3
|
+
export { createEnvConfig } from "./env-config";
|
|
4
|
+
export type {
|
|
5
|
+
EnvConfig,
|
|
6
|
+
EnvConfigOverrides,
|
|
7
|
+
Environment,
|
|
8
|
+
} from "./env-config";
|
|
9
|
+
|
|
3
10
|
/**
|
|
4
11
|
* Default currency for price formatting
|
|
5
12
|
*/
|
|
@@ -94,3 +101,34 @@ export function isValidPhoneNumber(value: string): boolean {
|
|
|
94
101
|
const phoneRegex = /^(\+\d{1,17}|\d{1,17})$/;
|
|
95
102
|
return phoneRegex.test(trimmedValue);
|
|
96
103
|
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Blood type enum matching backend BloodTypeEnum values
|
|
107
|
+
*/
|
|
108
|
+
export enum BloodType {
|
|
109
|
+
A_POSITIVE = "A+",
|
|
110
|
+
A_NEGATIVE = "A-",
|
|
111
|
+
B_POSITIVE = "B+",
|
|
112
|
+
B_NEGATIVE = "B-",
|
|
113
|
+
AB_POSITIVE = "AB+",
|
|
114
|
+
AB_NEGATIVE = "AB-",
|
|
115
|
+
O_POSITIVE = "O+",
|
|
116
|
+
O_NEGATIVE = "O-",
|
|
117
|
+
UNKNOWN = "UNKNOWN",
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Ordered list of all blood type values.
|
|
122
|
+
* Use this to build select/dropdown options.
|
|
123
|
+
*/
|
|
124
|
+
export const BLOOD_TYPES: BloodType[] = [
|
|
125
|
+
BloodType.A_POSITIVE,
|
|
126
|
+
BloodType.A_NEGATIVE,
|
|
127
|
+
BloodType.B_POSITIVE,
|
|
128
|
+
BloodType.B_NEGATIVE,
|
|
129
|
+
BloodType.AB_POSITIVE,
|
|
130
|
+
BloodType.AB_NEGATIVE,
|
|
131
|
+
BloodType.O_POSITIVE,
|
|
132
|
+
BloodType.O_NEGATIVE,
|
|
133
|
+
BloodType.UNKNOWN,
|
|
134
|
+
];
|