nexus-shared 1.1.18 → 1.1.20

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.
Files changed (74) hide show
  1. package/package.json +1 -1
  2. package/src/api-services/authentication-service.tsx +23 -0
  3. package/src/api-services/system-service.tsx +28 -315
  4. package/src/client.ts +2 -2
  5. package/src/components/documents/button.tsx +17 -11
  6. package/src/components/documents/icon-box.tsx +1 -1
  7. package/src/components/documents/tab-button.tsx +2 -2
  8. package/src/components/documents/tag.tsx +30 -0
  9. package/src/components/inputs/input-box.tsx +4 -2
  10. package/src/components/inputs/input-element.tsx +1 -1
  11. package/src/components/inputs/input-form.tsx +126 -14
  12. package/src/components/layouts/global-dialogbox.tsx +5 -9
  13. package/src/components/layouts/global-layout.tsx +3 -0
  14. package/src/components/layouts/layout-helpers.tsx +1 -4
  15. package/src/components/layouts/panels/user-panel.tsx +112 -0
  16. package/src/components/layouts/utility-menu.tsx +9 -29
  17. package/src/components/panels/theme-panel.tsx +2 -2
  18. package/src/helpers/browser-helpers.tsx +88 -3
  19. package/src/helpers/input-helpers.tsx +34 -4
  20. package/src/helpers/validation-helpers.tsx +302 -0
  21. package/src/index.ts +2 -18
  22. package/src/interface.ts +5 -1
  23. package/src/interfaces/auth-token-interfaces.tsx +6 -0
  24. package/src/interfaces/browser-interfaces.tsx +13 -0
  25. package/src/interfaces/button-interfaces.tsx +2 -2
  26. package/src/interfaces/http-interfaces.tsx +86 -8
  27. package/src/interfaces/icon-interfaces.tsx +3 -0
  28. package/src/interfaces/input-interfaces.tsx +61 -11
  29. package/src/interfaces/menu-interfaces.tsx +13 -0
  30. package/src/interfaces/message-interfaces.tsx +32 -0
  31. package/src/interfaces/storage-interfaces.tsx +2 -0
  32. package/src/interfaces/theme-interfaces.tsx +43 -129
  33. package/src/interfaces/type-interfaces.tsx +8 -2
  34. package/src/interfaces/user-interfaces.tsx +47 -0
  35. package/src/nexus.environments.tsx +41 -24
  36. package/src/proxy-api/proxy-backend.tsx +60 -0
  37. package/src/proxy-api/proxy-constants.tsx +28 -0
  38. package/src/proxy-api/proxy-helpers.tsx +91 -0
  39. package/src/proxy-api-client.tsx +1 -0
  40. package/src/proxy-api-server.tsx +2 -0
  41. package/src/services/http-client-services.tsx +26 -11
  42. package/src/services/http-services.tsx +42 -27
  43. package/src/services/loader-service.tsx +34 -17
  44. package/src/services/localstorage-service.tsx +81 -5
  45. package/src/services/message-services.tsx +383 -0
  46. package/src/services/theme-service.tsx +30 -16
  47. package/src/services/user-services.tsx +10 -0
  48. package/src/sso-config/auth-token-validation.ts +16 -0
  49. package/src/sso-config/callback-route.tsx +35 -0
  50. package/src/sso-config/cookie-encryption.ts +51 -0
  51. package/src/sso-config/cookie-helpers.tsx +102 -0
  52. package/src/sso-config/forgot-password-route.tsx +36 -0
  53. package/src/sso-config/oauth-callback-state.ts +61 -0
  54. package/src/sso-config/pkce-helpers.ts +9 -0
  55. package/src/sso-config/provider-complete-route.tsx +18 -0
  56. package/src/sso-config/redirect-context-actions.tsx +45 -0
  57. package/src/sso-config/redirect-context-helpers.tsx +159 -0
  58. package/src/sso-config/redirect-context-persist.tsx +18 -0
  59. package/src/sso-config/redirect-context-route.tsx +45 -0
  60. package/src/sso-config/refresh-route.tsx +23 -0
  61. package/src/sso-config/sign-in-route.tsx +74 -0
  62. package/src/sso-config/sign-out-route.tsx +57 -0
  63. package/src/sso-config/sign-up-route.tsx +57 -0
  64. package/src/sso-config/sso-backend-flow.ts +38 -0
  65. package/src/sso-config/sso-complete-route.tsx +50 -0
  66. package/src/sso-config/sso-config.ts +59 -0
  67. package/src/sso-config/sso-interfaces.tsx +99 -0
  68. package/src/sso-config/sso-response-helpers.tsx +20 -0
  69. package/src/sso-server.tsx +21 -1
  70. package/src/styles/nexus.dialog.css +3 -0
  71. package/src/styles/nexus.icon.css +1 -0
  72. package/src/styles/nexus.logic.css +34 -5
  73. package/src/interfaces/sso-interfaces.tsx +0 -52
  74. package/src/load-env.ts +0 -2
@@ -1,8 +1,13 @@
1
- 'use client';
1
+ "use client";
2
2
 
3
3
  import React, { useEffect } from "react";
4
- import { GetInputId } from "../../helpers/input-helpers";
5
- import { IForm, IFormCreateResults, IInputCreateResults } from "../../interfaces/input-interfaces";
4
+ import { HideLoader, ShowLoader, SubmitApi, WarningMessage } from "../../client";
5
+ import { GetInputId, SetResponseErrorMessage } from "../../helpers/input-helpers";
6
+ import { IsValidInputs } from "../../helpers/validation-helpers";
7
+ import { EActions, EAnimationClasses, EBackgrounds, EButtonTypes, EFlexPositions, EIcons, EIconTypes, ESizes, GetApiInformations, GetButton, UN, VoidCallbackHandler } from "../../interface";
8
+ import { DEFAULT_ROW_GAP, EInputTypes, GetForms, IApiForm, IForm, IFormCreateResults, IInputCreateResults, ISubmitForm, ISubmitFormCreateResults, OnFormChangeHandler, OnFormSubmitHandler } from "../../interfaces/input-interfaces";
9
+ import { GetFormButtonPosition } from "../../services/localstorage-service";
10
+ import { CreateButton } from "../documents/button";
6
11
  import { CreateInputs, DisposeInputs } from "./input";
7
12
 
8
13
  const forceChangeDefaultValue = (inputResults: IInputCreateResults[], newDefaultValue: any): boolean => {
@@ -16,37 +21,144 @@ const forceChangeDefaultValue = (inputResults: IInputCreateResults[], newDefault
16
21
  }
17
22
  return isChanged;
18
23
  };
19
- export const CreateForm = (param: IForm, rowGap?: number): IFormCreateResults | null => {
20
- const { inputs, onChanged, index, isTabularForm, parentIdentifier } = param;
24
+ export const CreateForm = (param: IForm, parentIdentifier: HTMLElement | string | UN, rowGap?: number, onFormRendered?: VoidCallbackHandler): IFormCreateResults | null => {
25
+ const { inputs, onChanged, index, isTabularForm, defaultValue } = param;
21
26
  const parentElement = typeof parentIdentifier === "string" ? document.getElementById(parentIdentifier) : parentIdentifier;
22
27
 
23
28
  if (!parentElement || inputs.length === 0 || !onChanged) {
24
29
  //TODO: WarningMessage("Invalid Inputs", "Please provide valid inputs, onChanged function and parent element.");
25
30
  return null;
26
31
  }
32
+ const rowElement = document.createElement("div");
33
+ if (parentElement) parentElement.appendChild(rowElement);
27
34
 
28
- const parent = document.createElement("div");
29
- parent.className = `row g${rowGap ?? 3} sb fg`;
30
- parent.style.paddingTop = "var(--IEM)";
31
- if (parentElement) parentElement.appendChild(parent);
35
+ rowElement.className = `row g${rowGap ?? DEFAULT_ROW_GAP} sb fg`;
36
+ rowElement.style.paddingTop = "var(--IEM)";
32
37
 
33
- const inputResults = CreateInputs(inputs, onChanged, index, isTabularForm, parent, param.defaultValue);
38
+ const inputResults = CreateInputs(inputs, onChanged, index, isTabularForm, rowElement, defaultValue);
39
+
40
+ onFormRendered?.(inputResults);
34
41
 
35
42
  return {
36
43
  inputResults: inputResults,
37
- formElement: parent,
44
+ formElement: rowElement,
38
45
  dispose: (invokeOnChanged?: boolean) => {
39
46
  DisposeInputs(inputResults, invokeOnChanged);
40
- parent.remove();
47
+ rowElement.remove();
41
48
  },
42
49
  forceChangeDefaultValue: (newDefaultValue: any) => forceChangeDefaultValue(inputResults, newDefaultValue),
43
50
  } as IFormCreateResults;
44
51
  };
45
52
 
46
- export const Form = ({ param, rowGap }: { param: IForm; rowGap?: number }): React.ReactNode => {
53
+ export const Form = ({ param, parentIdentifier, rowGap, onFormRendered }: { param: IForm; parentIdentifier: HTMLElement | string | UN; rowGap?: number; onFormRendered?: VoidCallbackHandler }): React.ReactNode => {
47
54
  useEffect(() => {
48
- const formResults = CreateForm(param, rowGap);
55
+ const formResults = CreateForm(param, parentIdentifier, rowGap, onFormRendered);
49
56
  return () => formResults?.dispose();
50
57
  }, [param, rowGap]);
51
58
  return <React.Fragment></React.Fragment>;
52
59
  };
60
+
61
+ export const CreateSubmitForm = (param: ISubmitForm, parentIdentifier: HTMLElement | string | UN, rowGap?: number, buttonPosition?: EFlexPositions, formAction?: EActions | UN, onFormRendered?: VoidCallbackHandler): ISubmitFormCreateResults | null => {
62
+ const parentElement = typeof parentIdentifier === "string" ? document.getElementById(parentIdentifier) : parentIdentifier;
63
+ buttonPosition = buttonPosition ?? GetFormButtonPosition();
64
+
65
+ if (!parentElement || !param.inputs || param.inputs.length === 0 || !param.onSubmit) return null;
66
+
67
+ let formValue: any | UN;
68
+ let defaultValue: any | UN = GetApiInformations.IS_API(param.defaultValue) ? null : param.defaultValue;
69
+ const onChangedHandler: OnFormChangeHandler = (currentValue, isInitilizing, isForcedChanged, isDisposed, currentKey, index, type, dataSource) => {
70
+ formValue = currentValue;
71
+ if (param.onChanged) param.onChanged(currentValue, isInitilizing, isForcedChanged, isDisposed, currentKey, index, type, dataSource);
72
+ };
73
+ const onFormSubmit = async (event: Event) => {
74
+ event.preventDefault();
75
+ const isValidForm = IsValidInputs(formResults?.inputResults, formValue, true, true);
76
+ const isValid = param.overrideValidation ? await param.overrideValidation(formValue, isValidForm, formResults, formAction) : isValidForm;
77
+
78
+ param.onSubmit({ ...formValue }, isValidForm, event, formResults!, defaultValue);
79
+ if (!isValid) WarningMessage("Invalid Inputs", "Please fill all required fields correctly.");
80
+ };
81
+ const formParameters = GetForms.FORM(param.inputs, onChangedHandler, defaultValue);
82
+ const formElement = document.createElement("form");
83
+ parentElement.appendChild(formElement);
84
+ const formResults = CreateForm(formParameters, formElement, rowGap, onFormRendered);
85
+ if (!formResults) return null;
86
+ formResults.formElement.classList.add("h100", "fg");
87
+
88
+ const submitButton = param.inputs.find((input) => input.inputType === EInputTypes.SubmitButton);
89
+ if (!submitButton) {
90
+ const buttonStyle: React.CSSProperties = {
91
+ width: "fit-content",
92
+ };
93
+ const DEFAULT_BUTTON_CLASS = "col- jc r";
94
+ rowGap = rowGap ?? DEFAULT_ROW_GAP;
95
+ const PADDING = rowGap * 2;
96
+
97
+ const buttonParam = GetButton("Submit", null, null, EIcons.SAVE, null, null, EBackgrounds.PrimaryColor, DEFAULT_BUTTON_CLASS, null, EIconTypes.OUTLINED, null, 0, null, EButtonTypes.SUBMIT);
98
+ const buttonContainer = document.createElement("div");
99
+ buttonContainer.className = `df ac pt1 pb5 ${buttonPosition} bb pr${PADDING} pl${PADDING}`;
100
+ buttonContainer.style.minHeight = "calc(var(--IH) * 1.5)";
101
+
102
+ formElement.appendChild(buttonContainer);
103
+ CreateButton(buttonParam, buttonContainer, buttonStyle, false);
104
+
105
+ if (param.onCancelClick) {
106
+ const buttonParam = GetButton("Cancel", null, null, EIcons.CLOSE, param.onCancelClick, null, EBackgrounds.ErrorColor, DEFAULT_BUTTON_CLASS + ` ml${PADDING}`, null, EIconTypes.OUTLINED, null, 0, null, EButtonTypes.BUTTON);
107
+ CreateButton(buttonParam, buttonContainer, buttonStyle, false);
108
+ }
109
+ }
110
+
111
+ formElement.className = `df ddc w100 h100 oh fg pr`;
112
+ formElement.addEventListener("submit", onFormSubmit);
113
+
114
+ const onDispose = () => {
115
+ formResults?.dispose();
116
+ formElement.remove();
117
+ };
118
+
119
+ return {
120
+ formElement: formElement,
121
+ dispose: () => onDispose(),
122
+ formObject: formResults,
123
+ };
124
+ };
125
+
126
+ export const SubmitForm = ({ param, parentIdentifier, rowGap, buttonPosition, formAction, onFormRendered }: { param: ISubmitForm; parentIdentifier: HTMLElement | string | UN; rowGap?: number; buttonPosition?: EFlexPositions; formAction?: EActions | UN; onFormRendered?: VoidCallbackHandler }): React.ReactNode => {
127
+ useEffect(() => {
128
+ const submitForm = CreateSubmitForm(param, parentIdentifier, rowGap, buttonPosition, formAction, onFormRendered);
129
+ return () => submitForm?.dispose();
130
+ }, [param, parentIdentifier, rowGap, buttonPosition, formAction]);
131
+ return <React.Fragment></React.Fragment>;
132
+ };
133
+
134
+ export const CreateApiForm = (param: IApiForm, parentIdentifier: HTMLElement | string | UN, rowGap?: number, buttonPosition?: EFlexPositions, formAction?: EActions | UN, overrideLoaderParend?: HTMLElement | string | UN, successMessage?: boolean | UN, errorMessage?: boolean | UN, onFormRendered?: VoidCallbackHandler): ISubmitFormCreateResults | null => {
135
+ let isLoading = false;
136
+ const onFormSubmit: OnFormSubmitHandler = async (currentValue, isValidForm, event, form, defaultValue) => {
137
+ if (isLoading) return;
138
+ isLoading = true;
139
+
140
+ if (param.onSubmit) param.onSubmit(currentValue, isValidForm, event, form, defaultValue);
141
+ if (!isValidForm) return;
142
+
143
+ const loaderParent = overrideLoaderParend ?? submitForm?.formElement ?? parentIdentifier;
144
+ ShowLoader(loaderParent, EAnimationClasses.SPIN, true, ESizes.Normal, null, null, null, "r");
145
+ const apiResponse = await SubmitApi(param.api, currentValue, successMessage, errorMessage);
146
+ if (!apiResponse.isSuccess) SetResponseErrorMessage(apiResponse, submitForm?.formObject.inputResults);
147
+ HideLoader(loaderParent);
148
+ param.onApiSubmit(currentValue, apiResponse, submitForm!, defaultValue);
149
+ isLoading = false;
150
+ };
151
+ const submitFormParam = GetForms.SUBMITFORM(param?.inputs, onFormSubmit, param?.defaultValue, param?.overrideValidation, param?.onChanged, param?.onCancelClick);
152
+ const submitForm = CreateSubmitForm(submitFormParam, parentIdentifier, rowGap, buttonPosition, formAction, onFormRendered);
153
+ if (!submitForm) return null;
154
+
155
+ return submitForm;
156
+ };
157
+
158
+ export const ApiForm = ({ param, parentIdentifier, rowGap, buttonPosition, formAction, overrideLoaderParend, successMessage, errorMessage, onFormRendered }: { param: IApiForm; parentIdentifier: HTMLElement | string | UN; rowGap?: number; buttonPosition?: EFlexPositions; formAction?: EActions | UN; overrideLoaderParend?: HTMLElement | string | UN; successMessage?: boolean | UN; errorMessage?: boolean | UN; onFormRendered?: VoidCallbackHandler }): React.ReactNode => {
159
+ useEffect(() => {
160
+ const apiForm = CreateApiForm(param, parentIdentifier, rowGap, buttonPosition, formAction, overrideLoaderParend, successMessage, errorMessage, onFormRendered);
161
+ return () => apiForm?.dispose();
162
+ }, [param, parentIdentifier, rowGap, buttonPosition, formAction, overrideLoaderParend, successMessage, errorMessage]);
163
+ return <React.Fragment></React.Fragment>;
164
+ };
@@ -1,4 +1,4 @@
1
- 'use client';
1
+ "use client";
2
2
 
3
3
  import React, { useEffect } from "react";
4
4
  import { ConvertRemToPixels, IsKeyPressed } from "../../helpers/browser-helpers";
@@ -17,7 +17,7 @@ export const DEFAULT_DIALOGBOX_HOVER_EFFECT_DELAY = 500;
17
17
  interface IDialogRegistryEntry {
18
18
  triggerElement: HTMLElement;
19
19
  affectedElement: HTMLElement;
20
- iggnoreChildClick?: boolean;
20
+ iggnoreChildClick?: boolean | UN;
21
21
  close: (isInitial: boolean) => void;
22
22
  }
23
23
 
@@ -32,12 +32,7 @@ let globalDocumentClickTimeout: ReturnType<typeof setTimeout>;
32
32
  const shouldKeepDialogOpen = (entry: IDialogRegistryEntry, path: EventTarget[], target: HTMLElement) => {
33
33
  if (entry.iggnoreChildClick === false) return target === entry.triggerElement;
34
34
 
35
- return (
36
- path.includes(entry.triggerElement) ||
37
- path.includes(entry.affectedElement) ||
38
- isClickInsideOpenNestedDialog(entry.triggerElement, path) ||
39
- entry.triggerElement.contains(target)
40
- );
35
+ return path.includes(entry.triggerElement) || path.includes(entry.affectedElement) || isClickInsideOpenNestedDialog(entry.triggerElement, path) || entry.triggerElement.contains(target);
41
36
  };
42
37
 
43
38
  const isClickInsideOpenNestedDialog = (parentTrigger: HTMLElement, path: EventTarget[]) => {
@@ -169,7 +164,7 @@ export const GlobalDialogbox = ({ children, hoverEffectDelay, position, buttonId
169
164
  return <ul id={GetDialogId(buttonIdentity)}>{isVisible && children}</ul>;
170
165
  };
171
166
 
172
- export const InitializeDialogbox = (triggerElement: HTMLElement, affectedElement: HTMLElement, hoverEffectDelay?: number | boolean, defaultHeight?: string, defaultPadding?: number, defaultWidth?: number, background?: EBackgrounds, onVisibilityChanged?: (isVisible: boolean) => void, onDisposed?: () => void, iggnoreChildClick?: boolean, position?: EPositions, defaultClass?: string, preferTopWhenEnoughSpace?: boolean, targetInput?: HTMLTextAreaElement | HTMLInputElement | UN, isAutoHideInInitialization?: boolean): IDialogboxResults | null => {
167
+ export const InitializeDialogbox = (triggerElement: HTMLElement, affectedElement: HTMLElement, hoverEffectDelay?: number | boolean | UN, defaultHeight?: string | UN, defaultPadding?: number | UN, defaultWidth?: number | UN, background?: EBackgrounds | UN, onVisibilityChanged?: (isVisible: boolean) => void | UN, onDisposed?: () => void | UN, iggnoreChildClick?: boolean | UN, position?: EPositions | UN, defaultClass?: string | UN, preferTopWhenEnoughSpace?: boolean | UN, targetInput?: HTMLTextAreaElement | HTMLInputElement | UN, isAutoHideInInitialization?: boolean | UN): IDialogboxResults | null => {
173
168
  if (!triggerElement || !affectedElement) return null;
174
169
  defaultHeight = defaultHeight ?? "auto";
175
170
  defaultPadding = defaultPadding ?? 5;
@@ -331,6 +326,7 @@ export const InitializeDialogbox = (triggerElement: HTMLElement, affectedElement
331
326
  unregisterDialog(triggerElement);
332
327
  onDisposed?.();
333
328
  };
329
+ // @ts-ignore
334
330
  let visibilityTimeout: NodeJS.Timeout;
335
331
  const visibility = (visible: boolean, isInitial: boolean) => {
336
332
  const isVisible = visible;
@@ -6,6 +6,7 @@ import { IGlobalLayout, ROOT_LOADER_IDENTITY } from "../../interfaces/layout-int
6
6
  import { FILES } from "../../nexus.environments";
7
7
  import { LayoutHelpers } from "./layout-helpers";
8
8
 
9
+ import { GetSystemThemesAsync } from "../../api-services/system-service";
9
10
  import "./../../styles/nexus.animation.css";
10
11
  import "./../../styles/nexus.core.css";
11
12
  import "./../../styles/nexus.dialog.css";
@@ -18,6 +19,8 @@ import "./../../styles/nexus.utility.css";
18
19
  const inter = Inter({ weight: ["300", "400", "500", "600", "700", "800"], subsets: ["latin"] });
19
20
 
20
21
  export const GlobalLayout = async ({ children, globalLayout }: Readonly<{ children: React.ReactNode; globalLayout: IGlobalLayout }>) => {
22
+ const systemThemes = await GetSystemThemesAsync();
23
+ globalLayout.systemThemes = systemThemes;
21
24
  return (
22
25
  <html lang="en">
23
26
  <head>
@@ -3,15 +3,12 @@
3
3
  import { useEffect } from "react";
4
4
  import { RemoveElement } from "../../helpers/browser-helpers";
5
5
  import { IGlobalLayout, ROOT_LOADER_IDENTITY } from "../../interfaces/layout-interfaces";
6
- import { DEFAULT_THEME_INFORMATIONS } from "../../interfaces/theme-interfaces";
7
6
  import { O } from "../../nexus-client";
8
7
  import { InitializeStyles } from "../../services/theme-service";
9
8
 
10
9
  export const LayoutHelpers = ({ globalLayout }: Readonly<{ globalLayout: IGlobalLayout }>) => {
11
10
  useEffect(() => {
12
- globalLayout.systemThemes = DEFAULT_THEME_INFORMATIONS;
13
- O.systemThemes = DEFAULT_THEME_INFORMATIONS;
14
-
11
+ O.systemThemes = globalLayout.systemThemes;
15
12
  const timeout = setTimeout(() => {
16
13
  clearTimeout(timeout);
17
14
  RemoveElement(ROOT_LOADER_IDENTITY, 500);
@@ -0,0 +1,112 @@
1
+ import Image from "next/image";
2
+ import React, { JSX, useEffect, useMemo, useState } from "react";
3
+ import { GetProfileDetails } from "../../../api-services/authentication-service";
4
+ import { BuildImageUrl, GetAuthPageURL, GetSignOutPageURL } from "../../../helpers/browser-helpers";
5
+ import { BUTTON_IDENTITY, ClickHandler, EBackgrounds, EButtonTypes, EFileSizes, EIcons, EMenuVisibility, EOpacity, EPositions, ESizes, GetButton, GetUtilityMenuCode, IButton, ICurrentUser, IGlobalLayout, IPanelMenu, IsAdmin_SuperAdmin, UN } from "../../../interface";
6
+ import { FILES } from "../../../nexus.environments";
7
+ import { InputLoader, ShowLoader } from "../../../services/loader-service";
8
+ import { SetCurrentUser } from "../../../services/user-services";
9
+ import { Buttons } from "../../documents/button";
10
+ import { Iconbox } from "../../documents/icon-box";
11
+ import { GetUserRoleTag } from "../../documents/tag";
12
+ import { GlobalDialogbox } from "../global-dialogbox";
13
+
14
+ const USER_PROFILE_ID = GetUtilityMenuCode("user-profile");
15
+ export const DEFAULT_USER_PROFILE_PICTURE_CLASS = "profile-user";
16
+ const USER_PROFILE_BUTTON_ID = "user-profile-menu-tab";
17
+
18
+ export const ProfilePanel = ({ globalLayout, userPanels, boxBackground, hasAccessToken }: Readonly<{ globalLayout: IGlobalLayout; userPanels: IPanelMenu[]; boxBackground?: EBackgrounds; hasAccessToken?: boolean | UN }>): JSX.Element => {
19
+ const [isLoading, setIsLoading] = useState(hasAccessToken === true);
20
+ const [currentUser, setCurrentUser] = useState<ICurrentUser | null>(null);
21
+ const groupPanels = useMemo(() => {
22
+ // TODO: Implement menu visibility logic
23
+ const isAuthenticated = currentUser && currentUser.username && hasAccessToken === true;
24
+ const visibilityFlag = isAuthenticated ? EMenuVisibility.AuthenticatedOnly : EMenuVisibility.GuestOnly;
25
+ const visibleMenus = userPanels.filter((x) => (x.menuVisibilities & visibilityFlag) !== 0);
26
+ return groupByGroupOrder(visibleMenus);
27
+ }, [userPanels, currentUser]);
28
+ const fetchProfileDetails = async () => {
29
+ setIsLoading(true);
30
+ InputLoader(USER_PROFILE_ID, true);
31
+ const response = await GetProfileDetails(true);
32
+ setCurrentUser(response);
33
+ SetCurrentUser(response);
34
+ setIsLoading(false);
35
+ InputLoader(USER_PROFILE_ID, false);
36
+ };
37
+ useEffect(() => {
38
+ if (hasAccessToken) fetchProfileDetails();
39
+ }, [hasAccessToken]);
40
+ return (
41
+ <div id={USER_PROFILE_ID} className={`pr ${ESizes.Small} ib ${boxBackground} h p2 r`}>
42
+ <Image src={FILES.ICONS.USER} alt="logo" width={74} height={74} className="img" />
43
+ {!isLoading && (
44
+ <GlobalDialogbox hoverEffectDelay={false} defaultPadding={0} position={EPositions.Left} buttonIdentity={USER_PROFILE_ID} iggnoreChildClick={false} defaultWidth={25}>
45
+ <React.Fragment>
46
+ <div className="bb h100 oh df ddc pl5">
47
+ <div style={{ display: currentUser ? "flex" : "none" }} className="df ac pr5 pt5">
48
+ <div className="ib xl oh item pr">
49
+ {currentUser && <Iconbox onClicked={() => onEditProfileClicked(currentUser)} iconOpacity={EOpacity.NONE} iconDefaultClass="xl" defaultClass="xl pa ho1 bg950 rr" background={EBackgrounds.None} icon={EIcons.CAMERA} />}
50
+ <img src={BuildImageUrl(currentUser?.profilePicture, EFileSizes.SIZE_128, FILES.ICONS.USER)} alt="profile-picture" className={`${DEFAULT_USER_PROFILE_PICTURE_CLASS} img ba2 rr`} />
51
+ </div>
52
+ <div className="ml5 w100">
53
+ <div className="df ac">
54
+ <h5 className="fw500 bg1 o09">{currentUser?.fullname}</h5>
55
+ </div>
56
+ <div className="df ac mt2">
57
+ <p className="bg1 mr5 o06">{currentUser?.emailAddress ?? `@${currentUser?.username}`}</p>
58
+ {IsAdmin_SuperAdmin(currentUser?.roleName) && GetUserRoleTag(currentUser?.roleName, true, "")}
59
+ </div>
60
+ </div>
61
+ </div>
62
+
63
+ <div className="sb sm h100 mt5 mb5 pr5">
64
+ {groupPanels.map((group, index) => (
65
+ <Buttons key={index} padding={0} pt={index !== 0 ? 5 : 0} buttons={group} />
66
+ ))}
67
+ </div>
68
+ </div>
69
+ </React.Fragment>
70
+ </GlobalDialogbox>
71
+ )}
72
+ </div>
73
+ );
74
+ };
75
+
76
+ const onEditProfileClicked = (currentUser: ICurrentUser) => {
77
+ // TODO: Implement edit profile functionality
78
+ };
79
+
80
+ function groupByGroupOrder(menuArray: IPanelMenu[]): IButton[][] {
81
+ const grouped: Record<number, IButton[]> = {};
82
+ menuArray.forEach((menu) => {
83
+ if (!grouped[menu.groupOrder]) {
84
+ grouped[menu.groupOrder] = [];
85
+ }
86
+ grouped[menu.groupOrder].push(GetButton(menu.menuName, menu.menuCode, menu.menuUrl, menu.menuIcon, onClickedPanelMenu, menu, null, null, menu.displayOrder, null, null, 12, 0, EButtonTypes.BUTTON));
87
+ });
88
+
89
+ const groupedArray = Object.keys(grouped)
90
+ .sort((a, b) => Number(a) - Number(b)) // Sort groups by groupOrder
91
+ .map((groupKey) => grouped[Number(groupKey)].sort((a, b) => (a.displayOrder ?? 0) - (b.displayOrder ?? 0)));
92
+
93
+ return groupedArray;
94
+ }
95
+
96
+ const onClickedPanelMenu: ClickHandler = async (event, element, data: IPanelMenu) => {
97
+ switch (data.menuCode) {
98
+ case BUTTON_IDENTITY(false).SIGN_IN_BUTTON: {
99
+ ShowLoader();
100
+ const url = GetAuthPageURL();
101
+ return (window.location.href = url);
102
+ }
103
+ case BUTTON_IDENTITY(false).SIGN_OUT_BUTTON: {
104
+ ShowLoader();
105
+ SetCurrentUser(null);
106
+ return (window.location.href = GetSignOutPageURL());
107
+ }
108
+ default: {
109
+ break;
110
+ }
111
+ }
112
+ };
@@ -1,56 +1,36 @@
1
- import React, { JSX } from "react";
1
+ import { UN } from "nexus-shared";
2
2
  import { EIconTypes } from "../../interfaces/icon-interfaces";
3
- import { EPositions, ESizes, IGlobalLayout } from "../../interfaces/layout-interfaces";
4
- import { GetUtilityMenuCode, IPanelMenu, IUtilityMenu, UTILITIES_BUTTON_PREFIX } from "../../interfaces/menu-interfaces";
3
+ import { EOpacity, EPositions, ESizes, IGlobalLayout } from "../../interfaces/layout-interfaces";
4
+ import { BUTTON_IDENTITY, GetUtilityMenuCode, IPanelMenu, IUtilityMenu } from "../../interfaces/menu-interfaces";
5
5
  import { EBackgrounds } from "../../interfaces/theme-interfaces";
6
6
  import { Iconbox } from "../documents/icon-box";
7
7
  import { PageTitle } from "../documents/page-title";
8
8
  import { ThemePanel } from "../panels/theme-panel";
9
9
  import { GlobalDialogbox } from "./global-dialogbox";
10
+ import { ProfilePanel } from "./panels/user-panel";
10
11
 
11
- export const BUTTON_IDENTITY = (isneedPrefix: boolean) => {
12
- const _ = isneedPrefix ? UTILITIES_BUTTON_PREFIX : "";
13
- return {
14
- PROFILE_BOX: `${_}profile`,
15
- SEARCH_BOX: `${_}root-search`,
16
- FAVORITE_BOX: `${_}favorite-menu`,
17
- CARD_BOX: `${_}cart-menu`,
18
- THEME_BOX: `${_}switch-theme`,
19
- MEGA_BOX: `${_}mega-menu`,
20
- SIGN_IN_BUTTON: `${_}sign-in`,
21
- SIGN_OUT_BUTTON: `${_}sign-out`,
22
- };
23
- };
24
- export const UtilityMenu = ({ utilityMenus, userPanels, column, paddingRight, globalLayout, iconBoxBackground }: Readonly<{ utilityMenus: IUtilityMenu[]; userPanels: IPanelMenu[]; column?: number; paddingRight?: number; globalLayout: IGlobalLayout; iconBoxBackground: EBackgrounds }>) => {
12
+ export const UtilityMenu = ({ utilityMenus, userPanels, column, paddingRight, globalLayout, iconBoxBackground, hasAccessToken }: Readonly<{ utilityMenus: IUtilityMenu[]; userPanels: IPanelMenu[]; column?: number; paddingRight?: number; globalLayout: IGlobalLayout; iconBoxBackground: EBackgrounds; hasAccessToken?: boolean | UN }>) => {
25
13
  return (
26
- <div style={{ paddingRight: paddingRight ?? 0, zIndex: 3 }} className={`utilities df fr ac jr h100p col-${column ?? 12}`}>
14
+ <div style={{ paddingRight: paddingRight ?? 0 }} className={`utilities df fr ac jr h100p col-${column ?? 12}`}>
27
15
  {utilityMenus.map((utilityMenu, index) => (
28
16
  <UtilityIcon key={index} utilityMenu={utilityMenu} boxBackground={iconBoxBackground} globalLayout={globalLayout} />
29
17
  ))}
30
- <ProfilePanel globalLayout={globalLayout} boxBackground={iconBoxBackground} userPanels={userPanels} />
18
+ <ProfilePanel globalLayout={globalLayout} boxBackground={iconBoxBackground} userPanels={userPanels} hasAccessToken={hasAccessToken} />
31
19
  </div>
32
20
  );
33
21
  };
34
22
 
35
23
  const UtilityIcon = ({ utilityMenu, globalLayout, boxBackground }: Readonly<{ utilityMenu: IUtilityMenu; globalLayout: IGlobalLayout; boxBackground?: EBackgrounds }>) => {
36
24
  return (
37
- <Iconbox id={GetUtilityMenuCode(utilityMenu.menuCode)} icon={utilityMenu.menuIcon} iconType={EIconTypes.FILLED} iconSize={ESizes.Small} boxSize={ESizes.Small} background={boxBackground} defaultClass="mr2">
25
+ <Iconbox id={GetUtilityMenuCode(utilityMenu.menuCode)} icon={utilityMenu.menuIcon} iconType={EIconTypes.FILLED} iconSize={ESizes.Small} boxSize={ESizes.Small} background={boxBackground} iconOpacity={EOpacity.O07} defaultClass="ml2">
38
26
  <UtilityDialogBox utilityMenu={utilityMenu} globalLayout={globalLayout} />
39
27
  </Iconbox>
40
28
  );
41
29
  };
42
30
 
43
- export const ProfilePanel = ({ globalLayout, userPanels, boxBackground }: Readonly<{ globalLayout: IGlobalLayout; userPanels: IPanelMenu[]; boxBackground?: EBackgrounds }>): JSX.Element => {
44
- return (
45
- <div className={`pr ${ESizes.Small} ib ${boxBackground} h p2 r`}>
46
- <h6>BA</h6>
47
- </div>
48
- );
49
- };
50
-
51
31
  const UtilityDialogBox = ({ utilityMenu, globalLayout }: Readonly<{ utilityMenu: IUtilityMenu; globalLayout: IGlobalLayout }>) => {
52
32
  return (
53
- <GlobalDialogbox hoverEffectDelay={true} defaultPadding={5} position={EPositions.Left} buttonIdentity={GetUtilityMenuCode(utilityMenu.menuCode)} defaultWidth={30}>
33
+ <GlobalDialogbox hoverEffectDelay={false} defaultPadding={5} position={EPositions.Left} buttonIdentity={GetUtilityMenuCode(utilityMenu.menuCode)} defaultWidth={30}>
54
34
  <div>
55
35
  <PageTitle marginBottom={5} borderBottom={1} title={utilityMenu.menuName} />
56
36
  <UtilityPanel utilityMenu={utilityMenu} globalLayout={globalLayout} />
@@ -1,4 +1,4 @@
1
- 'use client';
1
+ "use client";
2
2
 
3
3
  import React, { useEffect } from "react";
4
4
  import { GetButton, IButton } from "../../interfaces/button-interfaces";
@@ -30,7 +30,7 @@ const RenderSwitchTheme = (baseElement: HTMLElement) => {
30
30
  baseElement.appendChild(p);
31
31
  return;
32
32
  }
33
- const themeButtons: IButton[] = themes.map((theme, index) => GetButton(theme.displayName, theme.systemName, null, theme.icon, onClicked, theme, null, null, theme.displayOrder, EIconTypes.FILLED, null));
33
+ const themeButtons: IButton[] = themes.map((theme, index) => GetButton(theme.displayName, theme.systemName, null, theme.iconName, onClicked, theme, null, null, theme.displayOrder, EIconTypes.OUTLINED, null));
34
34
  const activeButton = themeButtons.find((button) => button.id === currentTheme);
35
35
  CreateTabButtons(themeButtons, baseElement, activeButton, onClicked);
36
36
 
@@ -1,12 +1,13 @@
1
+ import { BuildProxyApiUrl, EModules, ESigninRequestMode, UN } from "../interface";
1
2
  import { EAnimationClasses } from "../interfaces/layout-interfaces";
2
- import { NEXUS_INFO } from "../nexus.environments";
3
- import { EKeyModifier, EKeys } from "./../interfaces/browser-interfaces";
3
+ import { FILES, NEXUS_INFO, URLS } from "../nexus.environments";
4
+ import { EFileSizes, EKeyModifier, EKeys } from "./../interfaces/browser-interfaces";
4
5
  import { HasFlag } from "./bitwise-helpers";
5
6
  import { Debounce } from "./utility-helpers";
6
7
 
7
8
  export const IsBrowser = (): boolean => typeof window !== "undefined" && typeof document !== "undefined";
8
9
 
9
- export const RemoveElement = async (elementIdentity: string | HTMLElement, timeout?: number, animation?: EAnimationClasses): Promise<boolean> => {
10
+ export const RemoveElement = async (elementIdentity: string | HTMLElement, timeout?: number | UN, animation?: EAnimationClasses | UN): Promise<boolean> => {
10
11
  return new Promise(async (resolve) => {
11
12
  if (!IsBrowser() || !elementIdentity) return resolve(false);
12
13
  else if (typeof elementIdentity === "string") {
@@ -69,3 +70,87 @@ export const FindChildElements = (parent: HTMLElement, childSelector: string, sh
69
70
  return children.length > 0 ? [children[0]] : [];
70
71
  }
71
72
  };
73
+ export const PxToRem = (px: number): number => {
74
+ const fontSize = parseFloat(getComputedStyle(document.documentElement).fontSize) || 16;
75
+ return Math.round((px / fontSize) * 1000) / 1000;
76
+ };
77
+
78
+ export const ConvertColorToRGBA = (color: string, alpha: number = 0.1): string => {
79
+ if (!color) return "rgba(0, 0, 0, 0.1)";
80
+
81
+ let finalColor = color.trim();
82
+ if (finalColor.startsWith("var(")) finalColor = finalColor.replace(/^var\(\s*/, "").replace(/\s*\)$/, "");
83
+ // CSS Variable Support
84
+ if (finalColor.startsWith("--")) finalColor = getComputedStyle(document.documentElement).getPropertyValue(finalColor).trim();
85
+
86
+ // Create temporary element for browser auto parsing
87
+ const temp = document.createElement("div");
88
+ temp.style.color = finalColor;
89
+
90
+ document.body.appendChild(temp);
91
+
92
+ const computed = getComputedStyle(temp).color;
93
+
94
+ document.body.removeChild(temp);
95
+
96
+ // computed => rgb(...) or rgba(...)
97
+ const match = computed.match(/rgba?\(\s*(\d+)[,\s]+(\d+)[,\s]+(\d+)(?:[,\s\/]+([\d.]+))?\s*\)/i);
98
+
99
+ if (!match) return "rgba(0, 0, 0, 1)";
100
+
101
+ const r = parseInt(match[1], 10);
102
+ const g = parseInt(match[2], 10);
103
+ const b = parseInt(match[3], 10);
104
+
105
+ let a = match[4] !== undefined ? parseFloat(match[4]) : 1;
106
+
107
+ // Override alpha if provided
108
+ if (alpha) a = alpha;
109
+
110
+ return `rgba(${r}, ${g}, ${b}, ${a})`;
111
+ };
112
+ export function FindParentElement<ELEMENT extends Element>(element: Element | null | undefined, selector: string): ELEMENT | null {
113
+ if (!element || !selector || !element.matches) return null;
114
+
115
+ if (element.matches(selector)) return element as ELEMENT;
116
+
117
+ return FindParentElement<ELEMENT>(element.parentElement, selector);
118
+ }
119
+ export const GetCurrentFullURL = () => window?.location?.href;
120
+ export const GetCurrentBaseURL = () => window?.location?.origin;
121
+ export const BuildImageUrl = (viewToken?: string | UN, size?: EFileSizes | UN, fallback?: string | UN, isPublic: boolean = false) => {
122
+ if (!viewToken) return fallback ?? FILES.ICONS.NO_IMAGE;
123
+ if (!size) size = EFileSizes.SIZE_256;
124
+ if (isPublic) return BuildProxyApiUrl(EModules.Drive, `Public/Image/${size}/${viewToken}`);
125
+ else return BuildProxyApiUrl(EModules.Drive, `Private/Image/${size}/${viewToken}`);
126
+ };
127
+
128
+ const resolveReturnPath = (redirectURL?: string | UN): string => {
129
+ if (redirectURL) {
130
+ try {
131
+ const url = new URL(redirectURL, GetCurrentBaseURL());
132
+ return url.pathname + url.search || "/";
133
+ } catch {
134
+ /* fall through to current location */
135
+ }
136
+ }
137
+ if (!IsBrowser()) return "/";
138
+ return window.location.pathname + window.location.search || "/";
139
+ };
140
+
141
+ export const GetAuthPageURL = (redirectURL?: string | UN, signinMode?: ESigninRequestMode | UN) => {
142
+ const setupUrl = new URL(URLS.GLOBAL_SIGNIN);
143
+ setupUrl.searchParams.set("redirect_uri", `${GetCurrentBaseURL()}/api/auth/callback`);
144
+ setupUrl.searchParams.set("state", resolveReturnPath(redirectURL));
145
+ setupUrl.searchParams.set("signin_mode", signinMode ?? ESigninRequestMode.DEFAULT);
146
+ return setupUrl.toString();
147
+ };
148
+
149
+ /** Clears the session via /api/auth/sign-out; non-admin apps return to the current page (with query params). */
150
+ export const GetSignOutPageURL = (returnPath?: string | UN) => {
151
+ const signOutUrl = new URL(`${GetCurrentBaseURL()}/api/auth/sign-out`);
152
+ if (!NEXUS_INFO.IS_ADMIN_MODULE) {
153
+ signOutUrl.searchParams.set("returnUrl", resolveReturnPath(returnPath));
154
+ }
155
+ return signOutUrl.toString();
156
+ };
@@ -1,8 +1,10 @@
1
- import { DEFAULT_ICON_ELEMENT_NAME } from "../interface";
1
+ import { DEFAULT_ICON_ELEMENT_NAME, IErrorRespose } from "../interface";
2
2
  import { EValueChangeTypes, IInputCreateResults, IInputRootParameters } from "../interfaces/input-interfaces";
3
3
  import { ESizes } from "../interfaces/layout-interfaces";
4
4
  import { UN } from "../interfaces/type-interfaces";
5
+ import { AddRemoveErrorFromInput } from "./validation-helpers";
5
6
 
7
+ export const INPUT_ICON_BOX_CLASS = "input-icon-box";
6
8
  export const GetParentKey = (parentKey?: string | null) => {
7
9
  if (parentKey) return parentKey.split("|")[0];
8
10
  return undefined;
@@ -23,22 +25,50 @@ export const ForceSetValue = (input?: IInputCreateResults | null, value?: any |
23
25
  clearTimeout(timeoutValue);
24
26
  }, timeout ?? 0);
25
27
  };
28
+
29
+ export const ApplyPasswordInputs = (inputResults: IInputCreateResults[] | UN, selectors: string[] | UN) => {
30
+ if (!inputResults || inputResults.length === 0 || !selectors || selectors.length === 0) return;
31
+ for (const selector of selectors) {
32
+ const input = inputResults.find((x) => x.input.id === selector);
33
+ if (input) ApplyPasswordInput(input.rootElement.querySelector(`div.ib.${INPUT_ICON_BOX_CLASS}`), input.inputElement);
34
+ }
35
+ };
36
+
26
37
  export const ApplyPasswordInput = (iconbox: HTMLElement | null, inputElement: HTMLElement | null) => {
27
38
  const input = inputElement as HTMLInputElement | undefined;
28
39
  if (!input || !iconbox) return;
40
+
41
+ if (!iconbox.classList.contains("h")) iconbox.classList.add("h");
42
+
29
43
  const icon = iconbox.querySelector(DEFAULT_ICON_ELEMENT_NAME) as HTMLElement | null;
30
- if (icon) icon.setAttribute("name", "eye");
44
+ if (!icon) return;
31
45
  const onMouseLeave = () => {
32
46
  iconbox.removeEventListener("mouseleave", onMouseLeave);
33
47
  iconbox.removeEventListener("click", onClicked);
34
48
  input.type = "password";
35
49
  if (icon) icon.setAttribute("name", "key");
36
50
  };
51
+ const onMouseEnter = () => {
52
+ icon.setAttribute("name", "eye");
53
+ iconbox.addEventListener("mouseleave", onMouseLeave);
54
+ iconbox.addEventListener("click", onClicked);
55
+ };
37
56
  const onClicked = () => {
38
57
  const isPassword = input.type === "password";
39
58
  input.type = isPassword ? "text" : "password";
40
59
  if (icon) icon.setAttribute("name", isPassword ? "eye-off" : "eye");
41
60
  };
42
- iconbox.addEventListener("mouseleave", onMouseLeave);
43
- iconbox.addEventListener("click", onClicked);
61
+ iconbox.addEventListener("mouseenter", onMouseEnter);
62
+ };
63
+
64
+ export const SetResponseErrorMessage = (apiResponse: IErrorRespose, inputResults: IInputCreateResults[] | UN) => {
65
+ if (!apiResponse || !inputResults || inputResults.length === 0 || !apiResponse.inputName) return;
66
+
67
+ const inputKeys = apiResponse.inputName.toLowerCase().split("|");
68
+ let isFirstElement = true;
69
+ for (const inputKey of inputKeys) {
70
+ const input = inputResults.find((x) => GetInputId(x.input).toLowerCase() === inputKey);
71
+ if (input) AddRemoveErrorFromInput(input.input, input.inputElement, true, apiResponse.message, isFirstElement);
72
+ isFirstElement = false;
73
+ }
44
74
  };