@ampath/esm-login-app 8.0.0-next.5 → 8.0.0-next.6

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/routes.json CHANGED
@@ -1 +1 @@
1
- {"$schema":"https://json.openmrs.org/routes.schema.json","backendDependencies":{"webservices.rest":">=2.2.0"},"pages":[{"component":"root","route":"login","online":true,"offline":true},{"component":"root","route":"logout","online":true,"offline":true},{"component":"root","route":"change-password","online":true,"offline":true}],"extensions":[{"name":"location-picker","slot":"location-picker","component":"locationPicker","online":true,"offline":true},{"name":"logout-button","slot":"user-panel-bottom-slot","component":"logoutButton","online":true,"offline":true},{"name":"password-changer","slot":"user-panel-slot","component":"changePasswordLink","online":true,"offline":true},{"name":"location-changer","slot":"top-nav-info-slot","component":"changeLocationLink","online":true,"offline":true,"order":1}],"modals":[{"name":"change-password-modal","component":"changePasswordModal"}],"version":"8.0.0-next.5"}
1
+ {"$schema":"https://json.openmrs.org/routes.schema.json","backendDependencies":{"webservices.rest":">=2.2.0"},"pages":[{"component":"root","route":"login","online":true,"offline":true},{"component":"root","route":"logout","online":true,"offline":true},{"component":"root","route":"change-password","online":true,"offline":true}],"extensions":[{"name":"location-picker","slot":"location-picker","component":"locationPicker","online":true,"offline":true},{"name":"logout-button","slot":"user-panel-bottom-slot","component":"logoutButton","online":true,"offline":true},{"name":"password-changer","slot":"user-panel-slot","component":"changePasswordLink","online":true,"offline":true},{"name":"location-changer","slot":"top-nav-info-slot","component":"changeLocationLink","online":true,"offline":true,"order":1}],"modals":[{"name":"change-password-modal","component":"changePasswordModal"}],"version":"8.0.0-next.6"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ampath/esm-login-app",
3
- "version": "8.0.0-next.5",
3
+ "version": "8.0.0-next.6",
4
4
  "license": "MPL-2.0",
5
5
  "description": "The login microfrontend for the OpenMRS SPA",
6
6
  "browser": "dist/esm-login-app.js",
Binary file
@@ -1,6 +1,7 @@
1
1
  import React, { useEffect, useState } from 'react';
2
2
  import styles from './resend-timer.scss';
3
3
  import { getOtp } from '../../resources/otp.resource';
4
+ import { useSession } from '@openmrs/esm-framework';
4
5
 
5
6
  interface ResendTimerProps {
6
7
  username: string;
@@ -10,6 +11,9 @@ interface ResendTimerProps {
10
11
  const ResendTimer: React.FC<ResendTimerProps> = ({ username, password }) => {
11
12
  const RESEND_SECONDS = 30;
12
13
  const [secondsLeft, setSecondsLeft] = useState(RESEND_SECONDS);
14
+ const session = useSession();
15
+
16
+ const uuid = session.user.person.uuid;
13
17
 
14
18
  useEffect(() => {
15
19
  if (secondsLeft === 0) return;
@@ -27,7 +31,7 @@ const ResendTimer: React.FC<ResendTimerProps> = ({ username, password }) => {
27
31
  };
28
32
 
29
33
  const handleResend = async () => {
30
- await getOtp(username, password);
34
+ await getOtp(username, password, uuid);
31
35
  setSecondsLeft(RESEND_SECONDS);
32
36
  };
33
37
  return (
@@ -1,27 +1,25 @@
1
- import { validators, Type, validator } from "@openmrs/esm-framework";
1
+ import { validators, Type, validator } from '@openmrs/esm-framework';
2
2
 
3
3
  export const configSchema = {
4
4
  provider: {
5
5
  type: {
6
6
  _type: Type.String,
7
- _default: "basic",
7
+ _default: 'basic',
8
8
  _description:
9
9
  "Selects the login mechanism to use. Choices are 'basic' and 'oauth2'. " +
10
10
  "For 'oauth2' you'll also need to set the 'loginUrl'",
11
- _validators: [validators.oneOf(["basic", "oauth2"])],
11
+ _validators: [validators.oneOf(['basic', 'oauth2'])],
12
12
  },
13
13
  loginUrl: {
14
14
  _type: Type.String,
15
- _default: "${openmrsSpaBase}/login",
16
- _description:
17
- "The URL to use to login. This is only needed if you are using OAuth2.",
15
+ _default: '${openmrsSpaBase}/login',
16
+ _description: 'The URL to use to login. This is only needed if you are using OAuth2.',
18
17
  _validators: [validators.isUrl],
19
18
  },
20
19
  logoutUrl: {
21
20
  _type: Type.String,
22
- _default: "${openmrsSpaBase}/logout",
23
- _description:
24
- "The URL to use to login. This is only needed if you are using OAuth2.",
21
+ _default: '${openmrsSpaBase}/logout',
22
+ _description: 'The URL to use to login. This is only needed if you are using OAuth2.',
25
23
  _validators: [validators.isUrl],
26
24
  },
27
25
  },
@@ -36,25 +34,14 @@ export const configSchema = {
36
34
  numberToShow: {
37
35
  _type: Type.Number,
38
36
  _default: 8,
39
- _description: "The number of locations displayed in the location picker.",
40
- _validators: [
41
- validator(
42
- (v: unknown) => typeof v === "number" && v > 0,
43
- "Must be greater than zero"
44
- ),
45
- ],
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')],
46
39
  },
47
40
  locationsPerRequest: {
48
41
  _type: Type.Number,
49
42
  _default: 50,
50
- _description:
51
- "The number of results to fetch in each cycle of infinite scroll.",
52
- _validators: [
53
- validator(
54
- (v: unknown) => typeof v === "number" && v > 0,
55
- "Must be greater than zero"
56
- ),
57
- ],
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')],
58
45
  },
59
46
  useLoginLocationTag: {
60
47
  _type: Type.Boolean,
@@ -66,24 +53,23 @@ export const configSchema = {
66
53
  links: {
67
54
  loginSuccess: {
68
55
  _type: Type.String,
69
- _default: "${openmrsSpaBase}/home",
70
- _description: "The URL to redirect the user to after a successful login.",
56
+ _default: '${openmrsSpaBase}/home',
57
+ _description: 'The URL to redirect the user to after a successful login.',
71
58
  _validators: [validators.isUrl],
72
59
  },
73
60
  },
74
61
  logo: {
75
62
  src: {
76
63
  _type: Type.String,
77
- _default: "",
64
+ _default: '',
78
65
  _description:
79
- "The path or URL to the logo image. If set to an empty string, the default OpenMRS SVG sprite will be used.",
66
+ 'The path or URL to the logo image. If set to an empty string, the default OpenMRS SVG sprite will be used.',
80
67
  _validators: [validators.isUrl],
81
68
  },
82
69
  alt: {
83
70
  _type: Type.String,
84
- _default: "Logo",
85
- _description:
86
- "The alternative text for the logo image, displayed when the image cannot be loaded or on hover.",
71
+ _default: 'Logo',
72
+ _description: 'The alternative text for the logo image, displayed when the image cannot be loaded or on hover.',
87
73
  },
88
74
  },
89
75
  footer: {
@@ -94,25 +80,24 @@ export const configSchema = {
94
80
  src: {
95
81
  _type: Type.String,
96
82
  _required: true,
97
- _description: "The source URL of the logo image",
83
+ _description: 'The source URL of the logo image',
98
84
  _validators: [validators.isUrl],
99
85
  },
100
86
  alt: {
101
87
  _type: Type.String,
102
88
  _required: true,
103
- _description: "The alternative text for the logo image",
89
+ _description: 'The alternative text for the logo image',
104
90
  },
105
91
  },
106
92
  _default: [],
107
- _description:
108
- "An array of logos to be displayed in the footer next to the OpenMRS logo.",
93
+ _description: 'An array of logos to be displayed in the footer next to the OpenMRS logo.',
109
94
  },
110
95
  },
111
96
  showPasswordOnSeparateScreen: {
112
97
  _type: Type.Boolean,
113
98
  _default: false,
114
99
  _description:
115
- "Whether to show the password field on a separate screen. If false, the password field will be shown on the same screen.",
100
+ 'Whether to show the password field on a separate screen. If false, the password field will be shown on the same screen.',
116
101
  },
117
102
  };
118
103
 
@@ -139,7 +124,7 @@ export interface ConfigSchema {
139
124
  provider: {
140
125
  loginUrl: string;
141
126
  logoutUrl: string;
142
- type: "basic" | "oauth2";
127
+ type: 'basic' | 'oauth2';
143
128
  };
144
129
  showPasswordOnSeparateScreen: boolean;
145
130
  }
@@ -1 +1,5 @@
1
1
  declare module '*.scss';
2
+ declare module '*.png';
3
+ declare module '*.jpg';
4
+ declare module '*.jpeg';
5
+ declare module '*.svg';
@@ -13,11 +13,10 @@ import {
13
13
  } from '@openmrs/esm-framework';
14
14
  import { type ConfigSchema } from '../config-schema';
15
15
  import Logo from '../logo.component';
16
- import Footer from '../footer.component';
17
16
  import styles from './login.scss';
18
- import { getOtp } from '../resources/otp.resource';
17
+ import { getEmail, getOtp } from '../resources/otp.resource';
19
18
  import { getOtpEnabledStatus } from '../utils/get-base-url';
20
- import { boolean } from 'zod';
19
+ import image from '../assets/medicine.jpg';
21
20
 
22
21
  export interface LoginReferrer {
23
22
  referrer?: string;
@@ -103,7 +102,7 @@ const Login: React.FC = () => {
103
102
  const session = sessionStore.session;
104
103
  const authenticated = sessionStore?.session?.authenticated;
105
104
 
106
- if (isOtpEnabled === true) {
105
+ if (isOtpEnabled !== true) {
107
106
  if (authenticated) {
108
107
  if (session.sessionLocation) {
109
108
  let to = loginLinks?.loginSuccess || '/home';
@@ -114,7 +113,15 @@ const Login: React.FC = () => {
114
113
  to = location.state.referrer;
115
114
  }
116
115
  }
117
- await getOtp(username, password);
116
+ const uuid = session.user.person.uuid;
117
+ try {
118
+ const email = await getEmail(uuid, username, password);
119
+ await getOtp(username, password, email);
120
+ } catch (err: any) {
121
+ setErrorMessage(err.message);
122
+ return;
123
+ }
124
+
118
125
  navigate('otp', {
119
126
  state: {
120
127
  username,
@@ -123,7 +130,13 @@ const Login: React.FC = () => {
123
130
  },
124
131
  });
125
132
  } else if (!session.sessionLocation) {
126
- await getOtp(username, password);
133
+ const uuid = session.user.person.uuid;
134
+ try {
135
+ await getOtp(username, password, uuid);
136
+ } catch (err: any) {
137
+ setErrorMessage(err.message);
138
+ return;
139
+ }
127
140
  navigate('otp', {
128
141
  state: {
129
142
  username,
@@ -197,117 +210,124 @@ const Login: React.FC = () => {
197
210
 
198
211
  if (!loginProvider || loginProvider.type === 'basic') {
199
212
  return (
200
- <div className={styles.container}>
201
- <Tile className={styles.loginCard}>
202
- {errorMessage && (
203
- <div className={styles.errorMessage}>
204
- <InlineNotification
205
- kind="error"
206
- subtitle={t(errorMessage)}
207
- title={getCoreTranslation('error')}
208
- onClick={() => setErrorMessage('')}
209
- />
210
- </div>
211
- )}
212
- <div className={styles.center}>
213
- <Logo t={t} />
214
- </div>
215
- <form onSubmit={handleSubmit}>
216
- <div className={styles.inputGroup}>
217
- <TextInput
218
- id="username"
219
- type="text"
220
- name="username"
221
- autoComplete="username"
222
- labelText={t('username', 'Username')}
223
- value={username}
224
- onChange={changeUsername}
225
- ref={usernameInputRef}
226
- required
227
- autoFocus
228
- />
229
- {showPasswordOnSeparateScreen ? (
230
- <>
231
- <div className={showPasswordField ? undefined : styles.hiddenPasswordField}>
232
- <PasswordInput
233
- id="password"
234
- labelText={t('password', 'Password')}
235
- name="password"
236
- autoComplete="current-password"
237
- onChange={changePassword}
238
- ref={passwordInputRef}
239
- required
240
- value={password}
241
- showPasswordLabel={t('showPassword', 'Show password')}
242
- invalidText={t('validValueRequired', 'A valid value is required')}
243
- aria-hidden={!showPasswordField}
244
- tabIndex={showPasswordField ? 0 : -1}
245
- />
246
- </div>
247
- {showPasswordField ? (
248
- <Button
249
- type="submit"
250
- className={styles.continueButton}
251
- renderIcon={(props) => <ArrowRightIcon size={24} {...props} />}
252
- iconDescription={t('loginButtonIconDescription', 'Log in button')}
253
- disabled={!isLoginEnabled || isLoggingIn}
254
- >
255
- {isLoggingIn ? (
256
- <InlineLoading className={styles.loader} description={t('loggingIn', 'Logging in') + '...'} />
213
+ <>
214
+ <div className={styles.wrapperContainer}>
215
+ <div className={styles.container}>
216
+ <Tile className={styles.loginCard}>
217
+ {errorMessage && (
218
+ <div className={styles.errorMessage}>
219
+ <InlineNotification
220
+ kind="error"
221
+ subtitle={errorMessage}
222
+ title={getCoreTranslation('error')}
223
+ onClick={() => setErrorMessage('')}
224
+ />
225
+ </div>
226
+ )}
227
+ <div className={styles.center}>
228
+ <Logo t={t} />
229
+ </div>
230
+ <form onSubmit={handleSubmit}>
231
+ <div className={styles.inputGroup}>
232
+ <TextInput
233
+ id="username"
234
+ type="text"
235
+ name="username"
236
+ autoComplete="username"
237
+ labelText={t('username', 'Username')}
238
+ value={username}
239
+ onChange={changeUsername}
240
+ ref={usernameInputRef}
241
+ required
242
+ autoFocus
243
+ />
244
+ {showPasswordOnSeparateScreen ? (
245
+ <>
246
+ <div className={showPasswordField ? undefined : styles.hiddenPasswordField}>
247
+ <PasswordInput
248
+ id="password"
249
+ labelText={t('password', 'Password')}
250
+ name="password"
251
+ autoComplete="current-password"
252
+ onChange={changePassword}
253
+ ref={passwordInputRef}
254
+ required
255
+ value={password}
256
+ showPasswordLabel={t('showPassword', 'Show password')}
257
+ invalidText={t('validValueRequired', 'A valid value is required')}
258
+ aria-hidden={!showPasswordField}
259
+ tabIndex={showPasswordField ? 0 : -1}
260
+ />
261
+ </div>
262
+ {showPasswordField ? (
263
+ <Button
264
+ type="submit"
265
+ className={styles.continueButton}
266
+ renderIcon={(props) => <ArrowRightIcon size={24} {...props} />}
267
+ iconDescription={t('loginButtonIconDescription', 'Log in button')}
268
+ disabled={!isLoginEnabled || isLoggingIn}
269
+ >
270
+ {isLoggingIn ? (
271
+ <InlineLoading
272
+ className={styles.loader}
273
+ description={t('loggingIn', 'Logging in') + '...'}
274
+ />
275
+ ) : (
276
+ t('login', 'Log in')
277
+ )}
278
+ </Button>
257
279
  ) : (
258
- t('login', 'Log in')
280
+ <Button
281
+ type="submit"
282
+ className={styles.continueButton}
283
+ renderIcon={(props) => <ArrowRightIcon size={24} {...props} />}
284
+ iconDescription="Continue to password"
285
+ onClick={(evt) => {
286
+ evt.preventDefault();
287
+ continueLogin();
288
+ }}
289
+ disabled={!isLoginEnabled}
290
+ >
291
+ {t('continue', 'Continue')}
292
+ </Button>
259
293
  )}
260
- </Button>
294
+ </>
261
295
  ) : (
262
- <Button
263
- type="submit"
264
- className={styles.continueButton}
265
- renderIcon={(props) => <ArrowRightIcon size={24} {...props} />}
266
- iconDescription="Continue to password"
267
- onClick={(evt) => {
268
- evt.preventDefault();
269
- continueLogin();
270
- }}
271
- disabled={!isLoginEnabled}
272
- >
273
- {t('continue', 'Continue')}
274
- </Button>
296
+ <>
297
+ <PasswordInput
298
+ id="password"
299
+ labelText={t('password', 'Password')}
300
+ name="password"
301
+ autoComplete="current-password"
302
+ onChange={changePassword}
303
+ ref={passwordInputRef}
304
+ required
305
+ value={password}
306
+ showPasswordLabel={t('showPassword', 'Show password')}
307
+ invalidText={t('validValueRequired', 'A valid value is required')}
308
+ />
309
+ <Button
310
+ type="submit"
311
+ className={styles.continueButton}
312
+ renderIcon={(props) => <ArrowRightIcon size={24} {...props} />}
313
+ iconDescription="Log in"
314
+ disabled={!isLoginEnabled || isLoggingIn}
315
+ >
316
+ {isLoggingIn ? (
317
+ <InlineLoading className={styles.loader} description={t('loggingIn', 'Logging in') + '...'} />
318
+ ) : (
319
+ t('login', 'Log in')
320
+ )}
321
+ </Button>
322
+ </>
275
323
  )}
276
- </>
277
- ) : (
278
- <>
279
- <PasswordInput
280
- id="password"
281
- labelText={t('password', 'Password')}
282
- name="password"
283
- autoComplete="current-password"
284
- onChange={changePassword}
285
- ref={passwordInputRef}
286
- required
287
- value={password}
288
- showPasswordLabel={t('showPassword', 'Show password')}
289
- invalidText={t('validValueRequired', 'A valid value is required')}
290
- />
291
- <Button
292
- type="submit"
293
- className={styles.continueButton}
294
- renderIcon={(props) => <ArrowRightIcon size={24} {...props} />}
295
- iconDescription="Log in"
296
- disabled={!isLoginEnabled || isLoggingIn}
297
- >
298
- {isLoggingIn ? (
299
- <InlineLoading className={styles.loader} description={t('loggingIn', 'Logging in') + '...'} />
300
- ) : (
301
- t('login', 'Log in')
302
- )}
303
- </Button>
304
- </>
305
- )}
306
- </div>
307
- </form>
308
- </Tile>
309
- <Footer />
310
- </div>
324
+ </div>
325
+ </form>
326
+ </Tile>
327
+ </div>
328
+ <img className={styles.image} src={image} alt="TAIFA CARE" />
329
+ </div>
330
+ </>
311
331
  );
312
332
  }
313
333
  return null;
@@ -34,10 +34,11 @@
34
34
 
35
35
  .container {
36
36
  display: flex;
37
- flex-direction: column;
38
- justify-content: center;
37
+ flex-direction: row;
38
+ justify-content: flex-start;
39
39
  align-items: center;
40
40
  position: relative;
41
+ margin-left: 10rem;
41
42
  }
42
43
 
43
44
  .center {
@@ -165,3 +166,16 @@
165
166
  position: absolute;
166
167
  pointer-events: none;
167
168
  }
169
+
170
+ .wrapperContainer {
171
+ display: flex;
172
+ flex-direction: row;
173
+ gap: 10rem;
174
+ }
175
+
176
+ .image {
177
+ width: 50rem;
178
+ height: 55rem;
179
+ max-height: 60rem;
180
+ object-fit: cover;
181
+ }
@@ -1,18 +1,19 @@
1
- import React, { useState } from "react";
2
- import { Button, InlineNotification, Loading } from "@carbon/react";
3
- import { useLocation, useNavigate } from "react-router-dom";
4
- import { useTranslation } from "react-i18next";
1
+ import React, { useState } from 'react';
2
+ import { Button, InlineNotification, Loading } from '@carbon/react';
3
+ import { useLocation, useNavigate } from 'react-router-dom';
4
+ import { useTranslation } from 'react-i18next';
5
5
 
6
- import styles from "./otp.scss";
7
- import OTPInput from "../common/otp/otp.component";
8
- import ResendTimer from "../common/resend-timer/resend-timer.component";
9
- import Footer from "../footer.component";
10
- import Logo from "../logo.component";
11
- import { verifyOtp } from "../resources/otp.resource";
12
- import { refetchCurrentUser } from "@openmrs/esm-framework";
6
+ import styles from './otp.module.scss';
7
+ import OTPInput from '../common/otp/otp.component';
8
+ import ResendTimer from '../common/resend-timer/resend-timer.component';
9
+ import Logo from '../logo.component';
10
+ import { verifyOtp } from '../resources/otp.resource';
11
+ import { refetchCurrentUser } from '@openmrs/esm-framework';
12
+
13
+ import image from '../assets/medicine.jpg';
13
14
 
14
15
  const OtpComponent: React.FC = () => {
15
- const [otpValue, setOtpValue] = useState("");
16
+ const [otpValue, setOtpValue] = useState('');
16
17
  const [isLoading, setIsLoading] = useState(false);
17
18
  const [error, setError] = useState<string | null>(null);
18
19
  const navigate = useNavigate();
@@ -36,13 +37,13 @@ const OtpComponent: React.FC = () => {
36
37
  const session = sessionStore.session;
37
38
 
38
39
  if (!session.sessionLocation) {
39
- navigate("/login/location");
40
+ navigate('/login/location');
40
41
  return;
41
42
  }
42
43
 
43
- let to = "/home";
44
+ let to = '/home';
44
45
  if (location.state?.referrer) {
45
- to = location.state.referrer.startsWith("/")
46
+ to = location.state.referrer.startsWith('/')
46
47
  ? `\${openmrsSpaBase}${location.state.referrer}`
47
48
  : location.state.referrer;
48
49
  }
@@ -52,23 +53,25 @@ const OtpComponent: React.FC = () => {
52
53
  setError(res.data.message);
53
54
  }
54
55
  } catch (error) {
55
- setError(
56
- error?.message ||
57
- error?.attributes?.error ||
58
- "Invalid OTP or credentials"
59
- );
56
+ setError(error?.message || error?.attributes?.error || 'Invalid OTP or credentials');
60
57
  } finally {
61
58
  setIsLoading(false);
62
59
  }
63
60
  };
64
61
 
65
62
  const handleCancel = () => {
66
- navigate(-1);
63
+ const fallback = 'login';
64
+ if (window.history.length > 1) {
65
+ navigate(-1);
66
+ } else {
67
+ navigate(fallback, { replace: true });
68
+ }
67
69
  };
70
+
68
71
  return (
69
72
  <>
70
73
  <div className={styles.wrapperContainer}>
71
- <div>
74
+ <div className={styles.leftSide}>
72
75
  <div className={styles.logo}>
73
76
  <Logo t={t} />
74
77
  </div>
@@ -88,15 +91,15 @@ const OtpComponent: React.FC = () => {
88
91
  />
89
92
  )}
90
93
  <Button className={styles.button} onClick={handleVerify}>
91
- {isLoading ? <Loading /> : "Verify"}
94
+ {isLoading ? <Loading /> : 'Verify'}
92
95
  </Button>
93
96
  <Button className={styles.button} onClick={handleCancel}>
94
97
  Cancel
95
98
  </Button>
96
99
  <ResendTimer username={username} password={password} />
97
- <Footer />
98
100
  </div>
99
101
  </div>
102
+ <img className={styles.image} src={image} alt="TAIFA CARE" />
100
103
  </div>
101
104
  </>
102
105
  );
@@ -8,6 +8,10 @@
8
8
  gap: 1rem;
9
9
  margin-left: 5rem;
10
10
  }
11
+ .leftSide {
12
+ margin-top: 10rem;
13
+ margin-left: 10rem;
14
+ }
11
15
 
12
16
  .button {
13
17
  width: 15rem;
@@ -27,20 +31,14 @@
27
31
 
28
32
  .wrapperContainer {
29
33
  display: flex;
30
- gap: 5rem;
31
- }
32
-
33
- .rightSide {
34
- display: flex;
35
- justify-content: center;
36
- align-items: center;
37
- width: 50rem;
34
+ flex-direction: row;
35
+ gap: 10rem;
38
36
  }
39
37
 
40
38
  .image {
41
- width: 60rem;
42
- height: 40rem;
43
- border-radius: 5px;
44
- // max-height: 40rem;
45
- // object-fit: contain;
39
+ width: 54rem;
40
+ height: 55rem;
41
+ max-height: 60rem;
42
+ object-fit: cover;
43
+ flex-shrink: 0;
46
44
  }