@ampath/esm-login-app 8.0.0-next.10

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 (202) hide show
  1. package/.editorconfig +8 -0
  2. package/.eslintignore +3 -0
  3. package/.eslintrc +76 -0
  4. package/.gitattributes +4 -0
  5. package/.prettierignore +9 -0
  6. package/.yarn/plugins/@yarnpkg/plugin-outdated.cjs +35 -0
  7. package/.yarn/versions/643d2b70.yml +21 -0
  8. package/README.md +4 -0
  9. package/__mocks__/config.mock.ts +26 -0
  10. package/__mocks__/locations.mock.ts +540 -0
  11. package/__mocks__/react-i18next.js +49 -0
  12. package/dist/1128.js +1 -0
  13. package/dist/1128.js.map +1 -0
  14. package/dist/1480.js +1 -0
  15. package/dist/1578.js +1 -0
  16. package/dist/1578.js.map +1 -0
  17. package/dist/1646.js +1 -0
  18. package/dist/1800.js +1 -0
  19. package/dist/1800.js.map +1 -0
  20. package/dist/1869.js +1 -0
  21. package/dist/1877.js +1 -0
  22. package/dist/2317.js +1 -0
  23. package/dist/2416.js +1 -0
  24. package/dist/2489.js +1 -0
  25. package/dist/2489.js.map +1 -0
  26. package/dist/282.js +1 -0
  27. package/dist/2881.js +1 -0
  28. package/dist/2997.js +1 -0
  29. package/dist/2997.js.map +1 -0
  30. package/dist/3219.js +1 -0
  31. package/dist/3219.js.map +1 -0
  32. package/dist/3378.js +1 -0
  33. package/dist/3963.js +1 -0
  34. package/dist/4106.js +1 -0
  35. package/dist/4111.js +1 -0
  36. package/dist/4169.js +1 -0
  37. package/dist/4169.js.map +1 -0
  38. package/dist/434.js +1 -0
  39. package/dist/4348.js +1 -0
  40. package/dist/4378.js +1 -0
  41. package/dist/4378.js.map +1 -0
  42. package/dist/4383.js +1 -0
  43. package/dist/4658.js +1 -0
  44. package/dist/4668.js +1 -0
  45. package/dist/4668.js.map +1 -0
  46. package/dist/4870.js +1 -0
  47. package/dist/4870.js.map +1 -0
  48. package/dist/4928.js +1 -0
  49. package/dist/4bc56e5b0b0e91da.png +0 -0
  50. package/dist/5098.js +1 -0
  51. package/dist/5098.js.map +1 -0
  52. package/dist/5117.js +1 -0
  53. package/dist/5132.js +1 -0
  54. package/dist/5145.js +1 -0
  55. package/dist/5503.js +1 -0
  56. package/dist/556.js +1 -0
  57. package/dist/5644.js +1 -0
  58. package/dist/5898.js +1 -0
  59. package/dist/5898.js.map +1 -0
  60. package/dist/5940.js +1 -0
  61. package/dist/5976.js +1 -0
  62. package/dist/5976.js.map +1 -0
  63. package/dist/6047.js +1 -0
  64. package/dist/6237.js +1 -0
  65. package/dist/6237.js.map +1 -0
  66. package/dist/6362.js +1 -0
  67. package/dist/6362.js.map +1 -0
  68. package/dist/6371.js +1 -0
  69. package/dist/6377.js +1 -0
  70. package/dist/6444.js +1 -0
  71. package/dist/647e55b5cedf5df2.png +0 -0
  72. package/dist/6508.js +1 -0
  73. package/dist/6724.js +1 -0
  74. package/dist/6904.js +1 -0
  75. package/dist/7045.js +1 -0
  76. package/dist/7144.js +43 -0
  77. package/dist/7144.js.map +1 -0
  78. package/dist/7175.js +1 -0
  79. package/dist/7182.js +1 -0
  80. package/dist/7251.js +1 -0
  81. package/dist/7251.js.map +1 -0
  82. package/dist/749.js +1 -0
  83. package/dist/749.js.map +1 -0
  84. package/dist/7742.js +1 -0
  85. package/dist/7912.js +1 -0
  86. package/dist/8358.js +1 -0
  87. package/dist/8359.js +1 -0
  88. package/dist/8695.js +1 -0
  89. package/dist/903.js +1 -0
  90. package/dist/9072.js +1 -0
  91. package/dist/9510.js +15 -0
  92. package/dist/9510.js.map +1 -0
  93. package/dist/9806.js +1 -0
  94. package/dist/a6792134b9df70c4.png +0 -0
  95. package/dist/acd6ab71c5f6bcb6.jpg +0 -0
  96. package/dist/d0bf081185f017f3.jpg +0 -0
  97. package/dist/d48e253df6a333a7.png +0 -0
  98. package/dist/esm-login-app.js +6 -0
  99. package/dist/esm-login-app.js.buildmanifest.json +1598 -0
  100. package/dist/esm-login-app.js.map +1 -0
  101. package/dist/main.js +6 -0
  102. package/dist/main.js.map +1 -0
  103. package/dist/routes.json +1 -0
  104. package/jest.config.js +20 -0
  105. package/package.json +111 -0
  106. package/prettier.config.js +8 -0
  107. package/rspack.config.js +1 -0
  108. package/src/assets/Taifa-Care.png +0 -0
  109. package/src/assets/ampath-logo.png +0 -0
  110. package/src/assets/dha.png +0 -0
  111. package/src/assets/gok.png +0 -0
  112. package/src/assets/medicine.jpg +0 -0
  113. package/src/assets/openmrs.jpg +0 -0
  114. package/src/change-location-link/change-location-link.extension.tsx +32 -0
  115. package/src/change-location-link/change-location-link.scss +17 -0
  116. package/src/change-location-link/change-location-link.test.tsx +36 -0
  117. package/src/change-password/change-password-link.extension.tsx +30 -0
  118. package/src/change-password/change-password-link.test.tsx +27 -0
  119. package/src/change-password/change-password-modal.scss +11 -0
  120. package/src/change-password/change-password.component.tsx +159 -0
  121. package/src/change-password/change-password.modal.tsx +175 -0
  122. package/src/change-password/change-password.resource.ts +12 -0
  123. package/src/change-password/change-password.scss +51 -0
  124. package/src/change-password/change-password.test.tsx +53 -0
  125. package/src/common/otp/otp.component.tsx +54 -0
  126. package/src/common/otp/otp.scss +13 -0
  127. package/src/common/resend-timer/resend-timer.component.tsx +56 -0
  128. package/src/common/resend-timer/resend-timer.scss +7 -0
  129. package/src/config-schema.ts +140 -0
  130. package/src/declarations.d.ts +5 -0
  131. package/src/footer.component.tsx +60 -0
  132. package/src/footer.scss +113 -0
  133. package/src/index.ts +27 -0
  134. package/src/loading/loading.component.tsx +11 -0
  135. package/src/loading/loading.scss +7 -0
  136. package/src/location-picker/location-picker-view.component.tsx +174 -0
  137. package/src/location-picker/location-picker.resource.ts +111 -0
  138. package/src/location-picker/location-picker.scss +94 -0
  139. package/src/location-picker/location-picker.test.tsx +341 -0
  140. package/src/login/login.component.tsx +350 -0
  141. package/src/login/login.scss +246 -0
  142. package/src/login/login.test.tsx +288 -0
  143. package/src/login.resource.ts +147 -0
  144. package/src/logo.component.tsx +22 -0
  145. package/src/logout/logout.extension.tsx +23 -0
  146. package/src/logout/logout.scss +12 -0
  147. package/src/otp/otp.component.tsx +108 -0
  148. package/src/otp/otp.module.scss +44 -0
  149. package/src/redirect-logout/logout.resource.ts +15 -0
  150. package/src/redirect-logout/redirect-logout.component.tsx +42 -0
  151. package/src/redirect-logout/redirect-logout.test.tsx +180 -0
  152. package/src/resources/otp.resource.ts +90 -0
  153. package/src/root.component.tsx +24 -0
  154. package/src/routes.json +63 -0
  155. package/src/setupTests.ts +15 -0
  156. package/src/test-helpers/render-with-router.tsx +17 -0
  157. package/src/types.ts +34 -0
  158. package/src/utils/get-base-url.ts +17 -0
  159. package/translations/am.json +41 -0
  160. package/translations/ar.json +41 -0
  161. package/translations/ar_SY.json +41 -0
  162. package/translations/bn.json +41 -0
  163. package/translations/cs.json +41 -0
  164. package/translations/de.json +41 -0
  165. package/translations/en.json +41 -0
  166. package/translations/en_US.json +41 -0
  167. package/translations/es.json +41 -0
  168. package/translations/es_MX.json +41 -0
  169. package/translations/fr.json +41 -0
  170. package/translations/he.json +41 -0
  171. package/translations/hi.json +41 -0
  172. package/translations/hi_IN.json +41 -0
  173. package/translations/id.json +41 -0
  174. package/translations/it.json +41 -0
  175. package/translations/ka.json +41 -0
  176. package/translations/km.json +41 -0
  177. package/translations/ku.json +41 -0
  178. package/translations/ky.json +41 -0
  179. package/translations/lg.json +41 -0
  180. package/translations/ne.json +41 -0
  181. package/translations/pl.json +41 -0
  182. package/translations/pt.json +41 -0
  183. package/translations/pt_BR.json +41 -0
  184. package/translations/qu.json +41 -0
  185. package/translations/ro_RO.json +41 -0
  186. package/translations/ru_RU.json +41 -0
  187. package/translations/si.json +41 -0
  188. package/translations/sq.json +41 -0
  189. package/translations/sw.json +41 -0
  190. package/translations/sw_KE.json +41 -0
  191. package/translations/tr.json +41 -0
  192. package/translations/tr_TR.json +41 -0
  193. package/translations/uk.json +41 -0
  194. package/translations/uz.json +41 -0
  195. package/translations/uz@Latn.json +41 -0
  196. package/translations/uz_UZ.json +41 -0
  197. package/translations/vi.json +41 -0
  198. package/translations/zh.json +41 -0
  199. package/translations/zh_CN.json +41 -0
  200. package/translations/zh_TW.json +41 -0
  201. package/tsconfig.json +25 -0
  202. package/yarnrc.yml +14 -0
@@ -0,0 +1,51 @@
1
+ @use '@carbon/layout';
2
+ @use '@openmrs/esm-styleguide/src/vars' as *;
3
+
4
+ .submitButton {
5
+ margin-top: layout.$spacing-06;
6
+ width: 18rem;
7
+
8
+ :global(.cds--inline-loading) {
9
+ min-height: layout.$spacing-05;
10
+ }
11
+
12
+ :global(.cds--inline-loading__text) {
13
+ font-size: unset;
14
+ }
15
+ }
16
+
17
+ .alignCenter {
18
+ display: flex;
19
+ text-align: center;
20
+ }
21
+
22
+ .panelItemContainer {
23
+ a {
24
+ @extend .alignCenter;
25
+ justify-content: space-between;
26
+ }
27
+
28
+ div {
29
+ @extend .alignCenter;
30
+ }
31
+ }
32
+
33
+ .container {
34
+ display: flex;
35
+ flex-wrap: wrap;
36
+ flex-direction: column;
37
+ justify-content: center;
38
+ align-items: center;
39
+ position: relative;
40
+ margin-top: layout.$spacing-08;
41
+ }
42
+
43
+ .changePasswordCard {
44
+ border-radius: 0;
45
+ border: 1px solid $ui-03;
46
+ background-color: $ui-02;
47
+ width: 23rem;
48
+ padding: layout.$spacing-08;
49
+ position: relative;
50
+ min-height: fit-content;
51
+ }
@@ -0,0 +1,53 @@
1
+ import React from 'react';
2
+ import userEvent from '@testing-library/user-event';
3
+ import { render, screen } from '@testing-library/react';
4
+ import { changeUserPassword } from './change-password.resource';
5
+ import ChangePasswordModal from './change-password.modal';
6
+
7
+ const mockClose = jest.fn();
8
+ const mockChangeUserPassword = jest.mocked(changeUserPassword);
9
+
10
+ jest.mock('./change-password.resource', () => ({
11
+ changeUserPassword: jest.fn().mockResolvedValue({}),
12
+ }));
13
+
14
+ describe('ChangePasswordModal', () => {
15
+ it('validates the form before submitting', async () => {
16
+ const user = userEvent.setup();
17
+
18
+ render(<ChangePasswordModal close={mockClose} />);
19
+
20
+ const submitButton = screen.getByRole('button', {
21
+ name: /change/i,
22
+ });
23
+
24
+ const oldPasswordInput = screen.getByLabelText(/old password/i);
25
+ const newPasswordInput = screen.getByLabelText(/^new password$/i);
26
+ const confirmPasswordInput = screen.getByLabelText(/confirm new password/i);
27
+
28
+ expect(screen.getByRole('heading', { name: /change password/i })).toBeInTheDocument();
29
+ expect(oldPasswordInput).toBeInTheDocument();
30
+ expect(newPasswordInput).toBeInTheDocument();
31
+ expect(confirmPasswordInput).toBeInTheDocument();
32
+
33
+ await user.click(submitButton);
34
+
35
+ expect(screen.getByText(/old password is required/i)).toBeInTheDocument();
36
+ expect(screen.getByText(/new password is required/i)).toBeInTheDocument();
37
+ expect(screen.getByText(/password confirmation is required/i)).toBeInTheDocument();
38
+
39
+ await user.type(oldPasswordInput, 'P@ssw0rd123!');
40
+ await user.type(newPasswordInput, 'N3wP@ssw0rd456!');
41
+ await user.type(confirmPasswordInput, 'N3wP@ssw0rd456');
42
+
43
+ expect(screen.getByText(/passwords do not match/i)).toBeInTheDocument();
44
+
45
+ await user.clear(confirmPasswordInput);
46
+ await user.type(confirmPasswordInput, 'N3wP@ssw0rd456!');
47
+
48
+ await user.click(submitButton);
49
+
50
+ expect(mockChangeUserPassword).toHaveBeenCalledTimes(1);
51
+ expect(mockChangeUserPassword).toHaveBeenCalledWith('P@ssw0rd123!', 'N3wP@ssw0rd456!');
52
+ });
53
+ });
@@ -0,0 +1,54 @@
1
+ import React, { useRef, useState } from 'react';
2
+
3
+ import styles from './otp.scss';
4
+
5
+ interface OtpInputProps {
6
+ length?: number;
7
+ onChange?: (otp: string) => void;
8
+ }
9
+
10
+ const OTPInput: React.FC<OtpInputProps> = ({ length = 5, onChange }) => {
11
+ const [otp, setOtp] = useState(Array(length).fill(''));
12
+ const inputRef = useRef<Array<HTMLInputElement | null>>([]);
13
+
14
+ const handleChange = (e: React.ChangeEvent<HTMLInputElement>, index: number) => {
15
+ const val = e.target.value;
16
+ if (!/^[0-9]?$/.test(val)) return;
17
+
18
+ const newOtp = [...otp];
19
+ newOtp[index] = val;
20
+
21
+ setOtp(newOtp);
22
+
23
+ onChange?.(newOtp.join(''));
24
+
25
+ if (val && index < length - 1) {
26
+ inputRef.current[index + 1].focus();
27
+ }
28
+ };
29
+
30
+ const handleKeyDown = (e, index) => {
31
+ if (e.key === 'Backspace' && otp[index] === '' && index > 0) {
32
+ inputRef.current[index - 1].focus();
33
+ }
34
+ };
35
+
36
+ return (
37
+ <div className={styles.container}>
38
+ {otp.map((digit, index) => (
39
+ <input
40
+ className={styles.input}
41
+ key={index}
42
+ type="text"
43
+ maxLength={1}
44
+ value={digit}
45
+ onChange={(e) => handleChange(e, index)}
46
+ onKeyDown={(e) => handleKeyDown(e, index)}
47
+ ref={(el) => (inputRef.current[index] = el)}
48
+ />
49
+ ))}
50
+ </div>
51
+ );
52
+ };
53
+
54
+ export default OTPInput;
@@ -0,0 +1,13 @@
1
+ .container {
2
+ display: flex;
3
+ gap: 10px;
4
+ }
5
+
6
+ .input {
7
+ width: 40px;
8
+ height: 40px;
9
+ font-size: 20px;
10
+ text-align: center;
11
+ border: 1px solid #ccc;
12
+ border-radius: 5px;
13
+ }
@@ -0,0 +1,56 @@
1
+ import React, { useEffect, useState } from 'react';
2
+ import styles from './resend-timer.scss';
3
+ import { getEmail, getOtp } from '../../resources/otp.resource';
4
+ import { useSession } from '@openmrs/esm-framework';
5
+
6
+ interface ResendTimerProps {
7
+ username: string;
8
+ password: string;
9
+ }
10
+
11
+ const ResendTimer: React.FC<ResendTimerProps> = ({ username, password }) => {
12
+ const RESEND_SECONDS = 30;
13
+ const [secondsLeft, setSecondsLeft] = useState(RESEND_SECONDS);
14
+ const session = useSession();
15
+
16
+ const uuid = session.user.person.uuid;
17
+
18
+ useEffect(() => {
19
+ if (secondsLeft === 0) return;
20
+
21
+ const timer = setInterval(() => {
22
+ setSecondsLeft((s) => s - 1);
23
+ }, 1000);
24
+ return () => clearInterval(timer);
25
+ }, [secondsLeft]);
26
+
27
+ const formatTime = (seconds: number) => {
28
+ const m = Math.floor(seconds / 60);
29
+ const s = seconds % 60;
30
+ return `${m}:${s.toString().padStart(2, '0')}`;
31
+ };
32
+
33
+ const handleResend = async () => {
34
+ const email = await getEmail(uuid, username, password);
35
+ await getOtp(username, password, email);
36
+ setSecondsLeft(RESEND_SECONDS);
37
+ };
38
+ return (
39
+ <>
40
+ <p>
41
+ Didn&apos;t get a code?
42
+ {secondsLeft > 0 ? (
43
+ <>
44
+ After <span className={styles.timeLeft}>{formatTime(secondsLeft)}</span> you can <span>Resend</span>
45
+ </>
46
+ ) : (
47
+ <a className={styles.link} onClick={handleResend}>
48
+ Resend
49
+ </a>
50
+ )}
51
+ </p>
52
+ </>
53
+ );
54
+ };
55
+
56
+ export default ResendTimer;
@@ -0,0 +1,7 @@
1
+ .link {
2
+ cursor: pointer;
3
+ }
4
+
5
+ .timeLeft {
6
+ color: #007acc;
7
+ }
@@ -0,0 +1,140 @@
1
+ import { validators, Type, validator } from '@openmrs/esm-framework';
2
+
3
+ export const configSchema = {
4
+ provider: {
5
+ type: {
6
+ _type: Type.String,
7
+ _default: 'basic',
8
+ _description:
9
+ "Selects the login mechanism to use. Choices are 'basic' and 'oauth2'. " +
10
+ "For 'oauth2' you'll also need to set the 'loginUrl'",
11
+ _validators: [validators.oneOf(['basic', 'oauth2'])],
12
+ },
13
+ loginUrl: {
14
+ _type: Type.String,
15
+ _default: '${openmrsSpaBase}/login',
16
+ _description: 'The URL to use to login. This is only needed if you are using OAuth2.',
17
+ _validators: [validators.isUrl],
18
+ },
19
+ logoutUrl: {
20
+ _type: Type.String,
21
+ _default: '${openmrsSpaBase}/logout',
22
+ _description: 'The URL to use to login. This is only needed if you are using OAuth2.',
23
+ _validators: [validators.isUrl],
24
+ },
25
+ },
26
+ chooseLocation: {
27
+ enabled: {
28
+ _type: Type.Boolean,
29
+ _default: true,
30
+ _description:
31
+ "Whether to show a 'Choose Location' screen after login. " +
32
+ "If true, the user will be taken to the URL set in the 'links.loginSuccess' config property after choosing a location.",
33
+ },
34
+ numberToShow: {
35
+ _type: Type.Number,
36
+ _default: 8,
37
+ _description: 'The number of locations displayed in the location picker.',
38
+ _validators: [validator((v: unknown) => typeof v === 'number' && v > 0, 'Must be greater than zero')],
39
+ },
40
+ locationsPerRequest: {
41
+ _type: Type.Number,
42
+ _default: 50,
43
+ _description: 'The number of results to fetch in each cycle of infinite scroll.',
44
+ _validators: [validator((v: unknown) => typeof v === 'number' && v > 0, 'Must be greater than zero')],
45
+ },
46
+ useLoginLocationTag: {
47
+ _type: Type.Boolean,
48
+ _default: true,
49
+ _description:
50
+ "Whether to display only locations with the 'Login Location' tag. If false, all locations are shown.",
51
+ },
52
+ },
53
+ links: {
54
+ loginSuccess: {
55
+ _type: Type.String,
56
+ _default: '${openmrsSpaBase}/home',
57
+ _description: 'The URL to redirect the user to after a successful login.',
58
+ _validators: [validators.isUrl],
59
+ },
60
+ },
61
+ logo: {
62
+ src: {
63
+ _type: Type.String,
64
+ _default: '',
65
+ _description:
66
+ 'The path or URL to the logo image. If set to an empty string, the default OpenMRS SVG sprite will be used.',
67
+ _validators: [validators.isUrl],
68
+ },
69
+ alt: {
70
+ _type: Type.String,
71
+ _default: 'Logo',
72
+ _description: 'The alternative text for the logo image, displayed when the image cannot be loaded or on hover.',
73
+ },
74
+ },
75
+ footer: {
76
+ additionalLogos: {
77
+ _type: Type.Array,
78
+ _elements: {
79
+ _type: Type.Object,
80
+ src: {
81
+ _type: Type.String,
82
+ _required: true,
83
+ _description: 'The source URL of the logo image',
84
+ _validators: [validators.isUrl],
85
+ },
86
+ alt: {
87
+ _type: Type.String,
88
+ _required: true,
89
+ _description: 'The alternative text for the logo image',
90
+ },
91
+ },
92
+ _default: [],
93
+ _description: 'An array of logos to be displayed in the footer next to the OpenMRS logo.',
94
+ },
95
+ },
96
+ showPasswordOnSeparateScreen: {
97
+ _type: Type.Boolean,
98
+ _default: false,
99
+ _description:
100
+ 'Whether to show the password field on a separate screen. If false, the password field will be shown on the same screen.',
101
+ },
102
+ subDomainUrl: {
103
+ _type: Type.String,
104
+ _default: '',
105
+ _description: 'The subdomain URL for the application.',
106
+ },
107
+ etlBaseUrl: {
108
+ _type: Type.String,
109
+ _default: '',
110
+ _description: 'The ETL base URL for the application.',
111
+ },
112
+ };
113
+
114
+ export interface ConfigSchema {
115
+ chooseLocation: {
116
+ enabled: boolean;
117
+ locationsPerRequest: number;
118
+ numberToShow: number;
119
+ useLoginLocationTag: boolean;
120
+ };
121
+ footer: {
122
+ additionalLogos: Array<{
123
+ alt: string;
124
+ src: string;
125
+ }>;
126
+ };
127
+ links: {
128
+ loginSuccess: string;
129
+ };
130
+ logo: {
131
+ alt: string;
132
+ src: string;
133
+ };
134
+ provider: {
135
+ loginUrl: string;
136
+ logoutUrl: string;
137
+ type: 'basic' | 'oauth2';
138
+ };
139
+ showPasswordOnSeparateScreen: boolean;
140
+ }
@@ -0,0 +1,5 @@
1
+ declare module '*.scss';
2
+ declare module '*.png';
3
+ declare module '*.jpg';
4
+ declare module '*.jpeg';
5
+ declare module '*.svg';
@@ -0,0 +1,60 @@
1
+ import React, { useCallback } from 'react';
2
+ import { Link, Tile } from '@carbon/react';
3
+ import { useTranslation } from 'react-i18next';
4
+ import { useConfig, ArrowRightIcon } from '@openmrs/esm-framework';
5
+ import { type ConfigSchema } from './config-schema';
6
+ import styles from './footer.scss';
7
+
8
+ interface Logo {
9
+ src: string;
10
+ alt?: string;
11
+ }
12
+
13
+ const Footer: React.FC = () => {
14
+ const { t } = useTranslation();
15
+ const config = useConfig<ConfigSchema>();
16
+ const logos: Logo[] = config.footer.additionalLogos || [];
17
+
18
+ const handleImageLoadError = useCallback((error: React.SyntheticEvent<HTMLImageElement, Event>) => {
19
+ console.error('Failed to load image', error);
20
+ }, []);
21
+
22
+ return (
23
+ <div className={styles.footer}>
24
+ <Tile className={styles.poweredByTile}>
25
+ <div className={styles.poweredByContainer}>
26
+ <span className={styles.poweredByText}>{t('builtWith', 'Built with')}</span>
27
+ <svg aria-label={t('openmrsLogo', 'OpenMRS Logo')} className={styles.poweredByLogo} role="img">
28
+ <use href="#omrs-logo-full-color"></use>
29
+ </svg>
30
+ <span className={`${styles.poweredByText} ${styles.poweredBySubtext}`}>
31
+ {t('poweredBySubtext', 'An open-source medical record system and global community')}
32
+ </span>
33
+ <Link
34
+ className={styles.learnMoreButton}
35
+ href="https://openmrs.org"
36
+ rel="noopener noreferrer"
37
+ renderIcon={() => <ArrowRightIcon size={16} aria-label="Arrow right icon" />}
38
+ target="_blank"
39
+ >
40
+ {t('learnMore', 'Learn more')}
41
+ </Link>
42
+ </div>
43
+ </Tile>
44
+
45
+ <div className={styles.logosContainer}>
46
+ {logos.map((logo) => (
47
+ <img
48
+ alt={logo.alt ? t(logo.alt) : t('footerlogo', 'Footer Logo')}
49
+ className={styles.poweredByLogo}
50
+ key={logo.src}
51
+ onError={handleImageLoadError}
52
+ src={logo.src}
53
+ />
54
+ ))}
55
+ </div>
56
+ </div>
57
+ );
58
+ };
59
+
60
+ export default Footer;
@@ -0,0 +1,113 @@
1
+ @use '@carbon/colors';
2
+ @use '@carbon/layout';
3
+
4
+ .footer {
5
+ display: flex;
6
+ justify-content: space-between;
7
+ align-items: center;
8
+ padding: layout.$spacing-05;
9
+ position: absolute;
10
+ bottom: 0;
11
+ flex-wrap: wrap;
12
+ gap: layout.$spacing-05;
13
+ width: 100%;
14
+ }
15
+
16
+ .logosContainer {
17
+ display: flex;
18
+ max-height: layout.$spacing-07;
19
+ justify-content: flex-end;
20
+ flex-direction: row;
21
+ gap: layout.$spacing-03;
22
+ filter: grayscale(100%);
23
+ opacity: 80%;
24
+ }
25
+
26
+ @media only screen and (max-width: 1024px) {
27
+ .footer {
28
+ flex-direction: row;
29
+ justify-content: center;
30
+ padding: layout.$spacing-05;
31
+ }
32
+
33
+ .poweredByTile {
34
+ padding: layout.$spacing-05 layout.$spacing-03;
35
+ font-size: layout.$spacing-04;
36
+ align-items: center;
37
+ justify-content: center;
38
+ }
39
+
40
+ .logosContainer {
41
+ justify-content: center;
42
+ gap: layout.$spacing-04;
43
+ }
44
+ }
45
+
46
+ @media only screen and (max-width: 480px) {
47
+ .footer {
48
+ flex-direction: column;
49
+ align-items: center;
50
+ justify-content: center;
51
+ gap: layout.$spacing-03;
52
+ padding: layout.$spacing-05;
53
+ }
54
+
55
+ .poweredByTile {
56
+ flex-direction: row;
57
+ align-items: center;
58
+ justify-content: center;
59
+ padding: 1.25rem layout.$spacing-05;
60
+ font-size: layout.$spacing-04;
61
+ height: auto;
62
+ max-width: 100%;
63
+ border-radius: layout.$spacing-04;
64
+ }
65
+
66
+ .poweredBySubtext {
67
+ display: none;
68
+ }
69
+ }
70
+
71
+ .poweredByTile {
72
+ display: flex;
73
+ text-align: left;
74
+ max-width: fit-content;
75
+ min-height: fit-content;
76
+ font-size: smaller;
77
+ background-color: colors.$white;
78
+ padding: layout.$spacing-03 layout.$spacing-05;
79
+ border: 1px solid colors.$gray-20;
80
+ border-radius: layout.$spacing-04;
81
+ flex-wrap: wrap;
82
+ }
83
+
84
+ .poweredByContainer {
85
+ display: flex;
86
+ height: layout.$spacing-06;
87
+ align-items: center;
88
+ gap: layout.$spacing-03;
89
+ }
90
+
91
+ .poweredByLogo {
92
+ height: layout.$spacing-07;
93
+ width: auto;
94
+ max-width: layout.$spacing-12;
95
+ border-collapse: collapse;
96
+ padding: 0;
97
+ object-fit: contain;
98
+ display: block;
99
+ flex-shrink: 0;
100
+ }
101
+
102
+ .poweredByLogo + .poweredByText {
103
+ margin-left: layout.$spacing-02;
104
+ }
105
+
106
+ .learnMoreButton {
107
+ display: flex;
108
+ align-items: center;
109
+
110
+ svg {
111
+ fill: colors.$blue-60;
112
+ }
113
+ }
package/src/index.ts ADDED
@@ -0,0 +1,27 @@
1
+ import { defineConfigSchema, getAsyncLifecycle, getSyncLifecycle } from '@openmrs/esm-framework';
2
+ import { configSchema } from './config-schema';
3
+ import changeLocationLinkComponent from './change-location-link/change-location-link.extension';
4
+ import changePasswordLinkComponent from './change-password/change-password-link.extension';
5
+ import locationPickerComponent from './location-picker/location-picker-view.component';
6
+ import logoutButtonComponent from './logout/logout.extension';
7
+ import rootComponent from './root.component';
8
+
9
+ export const moduleName = '@ampath/esm-login-app';
10
+
11
+ const options = {
12
+ featureName: 'login',
13
+ moduleName,
14
+ };
15
+
16
+ export const importTranslation = require.context('../translations', false, /.json$/, 'lazy');
17
+
18
+ export function startupApp() {
19
+ defineConfigSchema(moduleName, configSchema);
20
+ }
21
+
22
+ export const root = getSyncLifecycle(rootComponent, options);
23
+ export const locationPicker = getSyncLifecycle(locationPickerComponent, options);
24
+ export const logoutButton = getSyncLifecycle(logoutButtonComponent, options);
25
+ export const changeLocationLink = getSyncLifecycle(changeLocationLinkComponent, options);
26
+ export const changePasswordLink = getSyncLifecycle(changePasswordLinkComponent, options);
27
+ export const changePasswordModal = getAsyncLifecycle(() => import('./change-password/change-password.modal'), options);
@@ -0,0 +1,11 @@
1
+ import React from 'react';
2
+ import { Loading } from '@carbon/react';
3
+ import styles from './loading.scss';
4
+
5
+ const LoadingIcon: React.FC = () => (
6
+ <div className={styles['centerLoadingSVG']}>
7
+ <Loading description="Active loading indicator" role="progressbar" withOverlay={false} small />
8
+ </div>
9
+ );
10
+
11
+ export default LoadingIcon;
@@ -0,0 +1,7 @@
1
+ .centerLoadingSVG {
2
+ display: flex;
3
+ width: 100vw;
4
+ height: 100vh;
5
+ justify-content: center;
6
+ align-items: center;
7
+ }