l-min-components 1.7.1507 → 1.7.1509
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/package.json +1 -1
- package/src/components/AppMainLayout/index.jsx +66 -19
- package/src/components/deactivated/index.jsx +22 -20
- package/src/components/deactivated/modal/subscription-expired-modal.jsx +0 -4
- package/src/components/errorPage/index.jsx +2 -2
- package/src/components/getErrorFeatures/errorWrapper.jsx +21 -6
- package/src/components/header/account-dropdown.jsx +13 -4
- package/src/components/header/index.jsx +4 -2
- package/src/components/header/languages.js +5 -5
- package/src/components/instructorAccountSwitcher/components/renew modal/index.jsx +2 -2
- package/src/components/instructorAccountSwitcher/index.jsx +8 -5
- package/src/hooks/adminSideMenuItems.jsx +10 -7
- package/src/hooks/useTranslation.jsx +129 -20
- package/src/utils/translation.js +171 -31
package/package.json
CHANGED
|
@@ -170,10 +170,10 @@ const AppMainLayout = ({ children }) => {
|
|
|
170
170
|
user,
|
|
171
171
|
userDetails,
|
|
172
172
|
handleGetFileStorageSummary,
|
|
173
|
-
getFileStorageSummaryData,
|
|
173
|
+
// getFileStorageSummaryData,
|
|
174
174
|
} = useHeader({ default: true });
|
|
175
175
|
|
|
176
|
-
//
|
|
176
|
+
// Get current default account and store in cookie (from api);
|
|
177
177
|
useEffect(() => {
|
|
178
178
|
if (getDefaultAccount?.data) {
|
|
179
179
|
const date = new Date();
|
|
@@ -197,6 +197,16 @@ const AppMainLayout = ({ children }) => {
|
|
|
197
197
|
|
|
198
198
|
localStorage.setItem("defaultLang", getDefaultAccount?.data?.language);
|
|
199
199
|
|
|
200
|
+
const { registration_step } = getDefaultAccount?.data;
|
|
201
|
+
// If registration step is "REGISTRATION_COMPLETED" - go to acct type selection page
|
|
202
|
+
if (registration_step === "REGISTRATION_COMPLETED") {
|
|
203
|
+
window.location.href = "/auth/account-type";
|
|
204
|
+
}
|
|
205
|
+
// If registration step is "REGISTRATION_PARTIALLY_COMPLETED" go to /register where the user can provide the missing fields.
|
|
206
|
+
else if (registration_step === "REGISTRATION_PARTIALLY_COMPLETED") {
|
|
207
|
+
window.location.href = "/auth/register";
|
|
208
|
+
}
|
|
209
|
+
|
|
200
210
|
// set default acct when you receive the default acct data
|
|
201
211
|
handleSetDefaultAccount(getDefaultAccount?.data?.id);
|
|
202
212
|
|
|
@@ -237,7 +247,6 @@ const AppMainLayout = ({ children }) => {
|
|
|
237
247
|
// Use setTimeout to ensure navigation happens after React Router is ready
|
|
238
248
|
setTimeout(() => {
|
|
239
249
|
const targetUrl = `/${defaultAccountType}`;
|
|
240
|
-
console.log(`Navigating to: ${targetUrl}`);
|
|
241
250
|
|
|
242
251
|
// Try using window.location.replace for staging compatibility
|
|
243
252
|
if (window.location.hostname.includes("staging")) {
|
|
@@ -245,7 +254,7 @@ const AppMainLayout = ({ children }) => {
|
|
|
245
254
|
} else {
|
|
246
255
|
window.location.href = targetUrl;
|
|
247
256
|
}
|
|
248
|
-
},
|
|
257
|
+
}, 1000);
|
|
249
258
|
}
|
|
250
259
|
}
|
|
251
260
|
}
|
|
@@ -311,16 +320,6 @@ const AppMainLayout = ({ children }) => {
|
|
|
311
320
|
handleGetDefaultAccount();
|
|
312
321
|
}, [defaultAcct]);
|
|
313
322
|
|
|
314
|
-
// Debug the API response
|
|
315
|
-
useEffect(() => {
|
|
316
|
-
console.log("getDefaultAccount state changed:", {
|
|
317
|
-
data: getDefaultAccount?.data,
|
|
318
|
-
loading: getDefaultAccount?.loading,
|
|
319
|
-
error: getDefaultAccount?.error,
|
|
320
|
-
response: getDefaultAccount?.response,
|
|
321
|
-
});
|
|
322
|
-
}, [getDefaultAccount]);
|
|
323
|
-
|
|
324
323
|
// setting current environment type
|
|
325
324
|
useEffect(() => {
|
|
326
325
|
if (window.location.hostname.includes("staging")) {
|
|
@@ -384,6 +383,50 @@ const AppMainLayout = ({ children }) => {
|
|
|
384
383
|
"Build the future with Learngual!",
|
|
385
384
|
"Choose our custom plans for more calls and better flexibility.",
|
|
386
385
|
"We know it's inconvenient. For better user accessibility, login with a desktop device.",
|
|
386
|
+
|
|
387
|
+
// Additional text from codebase analysis - ONLY actual strings found
|
|
388
|
+
"Student",
|
|
389
|
+
"Instructor account",
|
|
390
|
+
"Enterprise account",
|
|
391
|
+
"Student account",
|
|
392
|
+
"Dashboard",
|
|
393
|
+
"Courses",
|
|
394
|
+
"My courses",
|
|
395
|
+
"Manage courses",
|
|
396
|
+
"Manage report",
|
|
397
|
+
"Manage teams",
|
|
398
|
+
"Manage student",
|
|
399
|
+
"Messages",
|
|
400
|
+
"Announcements",
|
|
401
|
+
"File manager",
|
|
402
|
+
"Library",
|
|
403
|
+
"Add on",
|
|
404
|
+
"Contracts",
|
|
405
|
+
"Demo",
|
|
406
|
+
"Explore",
|
|
407
|
+
"Connect",
|
|
408
|
+
"Select affiliate",
|
|
409
|
+
"Search affiliates",
|
|
410
|
+
"Affiliates",
|
|
411
|
+
"GRACE PERIOD",
|
|
412
|
+
"ACTIVE",
|
|
413
|
+
"Oops!",
|
|
414
|
+
"Select plans",
|
|
415
|
+
"Renew subscription",
|
|
416
|
+
"Instructor",
|
|
417
|
+
"Learngual logo",
|
|
418
|
+
"profile",
|
|
419
|
+
"account photo",
|
|
420
|
+
"Select Plans",
|
|
421
|
+
"Search by Name / Email / Username",
|
|
422
|
+
"Text",
|
|
423
|
+
"add",
|
|
424
|
+
"Ready",
|
|
425
|
+
"play",
|
|
426
|
+
|
|
427
|
+
// expired instructor
|
|
428
|
+
"You haven’t been added to an enterprise account yet",
|
|
429
|
+
"This account will be active once an enterprise assigns you as an instructor.",
|
|
387
430
|
];
|
|
388
431
|
|
|
389
432
|
const {
|
|
@@ -578,6 +621,7 @@ const AppMainLayout = ({ children }) => {
|
|
|
578
621
|
onChange={(v) => setAffiliateAccount(v)}
|
|
579
622
|
setSearch={setAffilitateSearch}
|
|
580
623
|
defaultAffiliate={getDefaultAffiliateData?.data}
|
|
624
|
+
findText={findText}
|
|
581
625
|
/>
|
|
582
626
|
</>
|
|
583
627
|
)}
|
|
@@ -608,6 +652,7 @@ const AppMainLayout = ({ children }) => {
|
|
|
608
652
|
gracePeriod={gracePeriod}
|
|
609
653
|
stateType={planState === "GRACE PERIOD" ? 1 : 2}
|
|
610
654
|
planState={planState}
|
|
655
|
+
findText={findText}
|
|
611
656
|
/>
|
|
612
657
|
) : activeAccountType === "instructor" &&
|
|
613
658
|
!userPlanData?.loading &&
|
|
@@ -620,17 +665,19 @@ const AppMainLayout = ({ children }) => {
|
|
|
620
665
|
!getAllAffiliateData?.loading &&
|
|
621
666
|
!window.location.pathname.includes("notif") ? (
|
|
622
667
|
<div className="instructor_expired">
|
|
623
|
-
<h1>Dashboard</h1>
|
|
668
|
+
<h1>{findText("Dashboard")}</h1>
|
|
624
669
|
<div className="instructor_expired_body">
|
|
625
670
|
<div className="instructor_expired_center">
|
|
626
671
|
<img src={instructorImage} alt="" />
|
|
627
672
|
<h4>
|
|
628
|
-
|
|
629
|
-
|
|
673
|
+
{findText(
|
|
674
|
+
"You haven’t been added to an enterprise account yet"
|
|
675
|
+
)}
|
|
630
676
|
</h4>
|
|
631
677
|
<p>
|
|
632
|
-
|
|
633
|
-
|
|
678
|
+
{findText(
|
|
679
|
+
"This account will be active once an enterprise assigns you as an instructor."
|
|
680
|
+
)}
|
|
634
681
|
</p>
|
|
635
682
|
</div>
|
|
636
683
|
</div>
|
|
@@ -22,6 +22,7 @@ const GracePeriod = ({
|
|
|
22
22
|
handleCurrentSubscription,
|
|
23
23
|
gracePeriod,
|
|
24
24
|
stateType,
|
|
25
|
+
findText,
|
|
25
26
|
}) => {
|
|
26
27
|
console.log("🚀 ~ gracePeriod:", gracePeriod);
|
|
27
28
|
const location = useLocation();
|
|
@@ -80,7 +81,7 @@ const GracePeriod = ({
|
|
|
80
81
|
|
|
81
82
|
return (
|
|
82
83
|
<GracePeriodWrapper>
|
|
83
|
-
<h1>{pageTitle}</h1>
|
|
84
|
+
<h1>{findText(pageTitle)}</h1>
|
|
84
85
|
{stateType === 1 ? (
|
|
85
86
|
<div
|
|
86
87
|
className="grace_period_body"
|
|
@@ -94,26 +95,26 @@ const GracePeriod = ({
|
|
|
94
95
|
}}
|
|
95
96
|
>
|
|
96
97
|
<img src={warning} alt="" />
|
|
97
|
-
<h2>Your subscription to Learngual has expired</h2>
|
|
98
|
-
<p
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
98
|
+
<h2>{findText("Your subscription to Learngual has expired")}</h2>
|
|
99
|
+
<p
|
|
100
|
+
dangerouslySetInnerHTML={{
|
|
101
|
+
__html: findText(
|
|
102
|
+
"Your students and instructors will no longer {br1} be able to access your courses after {br2} {span}",
|
|
103
|
+
{
|
|
104
|
+
br1: `<br>`,
|
|
105
|
+
br2: `<br>`,
|
|
106
|
+
span: `<span style="fontWeight: 700; color: red;">${timeLeft}</span>`,
|
|
107
|
+
}
|
|
108
|
+
),
|
|
109
|
+
}}
|
|
110
|
+
></p>
|
|
110
111
|
<div
|
|
111
112
|
onClick={() => {
|
|
112
113
|
window.location.href = "/settings/payment";
|
|
113
114
|
}}
|
|
114
115
|
>
|
|
115
116
|
<Button
|
|
116
|
-
text="Renew subscription"
|
|
117
|
+
text={findText("Renew subscription")}
|
|
117
118
|
styles={{
|
|
118
119
|
height: "35px",
|
|
119
120
|
padding: "4px 20px",
|
|
@@ -125,10 +126,11 @@ const GracePeriod = ({
|
|
|
125
126
|
) : (
|
|
126
127
|
<>
|
|
127
128
|
<Expire>
|
|
128
|
-
<h4>Your subscription has expired</h4>
|
|
129
|
+
<h4>{findText("Your subscription has expired")}</h4>
|
|
129
130
|
<h5>
|
|
130
|
-
|
|
131
|
-
|
|
131
|
+
{findText(
|
|
132
|
+
"You don’t have an active plan, renew your subscription to gain full access to your account."
|
|
133
|
+
)}
|
|
132
134
|
</h5>
|
|
133
135
|
|
|
134
136
|
<div
|
|
@@ -137,7 +139,7 @@ const GracePeriod = ({
|
|
|
137
139
|
}}
|
|
138
140
|
>
|
|
139
141
|
<Button
|
|
140
|
-
text="Choose plan"
|
|
142
|
+
text={findText("Choose plan")}
|
|
141
143
|
styles={{
|
|
142
144
|
height: "35px",
|
|
143
145
|
padding: "4px 20px",
|
|
@@ -155,7 +157,7 @@ const GracePeriod = ({
|
|
|
155
157
|
}}
|
|
156
158
|
/>
|
|
157
159
|
<div className="content">
|
|
158
|
-
<p>Sorry, you can’t access your account</p>
|
|
160
|
+
<p>{findText("Sorry, you can’t access your account")}</p>
|
|
159
161
|
</div>
|
|
160
162
|
</div>
|
|
161
163
|
</>
|
|
@@ -75,10 +75,6 @@ const SubscriptionExpiredModal = ({
|
|
|
75
75
|
// };
|
|
76
76
|
// }, []);
|
|
77
77
|
|
|
78
|
-
const noSubError = () => {
|
|
79
|
-
toast.error("Error getting your subscription, please try again!");
|
|
80
|
-
};
|
|
81
|
-
|
|
82
78
|
const subId = getCurrentSubscriptionData?.data?.id;
|
|
83
79
|
|
|
84
80
|
const handleNavigateRenewal = () => {
|
|
@@ -22,8 +22,8 @@ const Index = () => {
|
|
|
22
22
|
<h3>Opss...</h3>
|
|
23
23
|
<h2>Page not found</h2>
|
|
24
24
|
<p>
|
|
25
|
-
Seems we can't find the page your looking for. Let's
|
|
26
|
-
|
|
25
|
+
Seems we can't find the page your looking for. Let's get you back
|
|
26
|
+
home.
|
|
27
27
|
</p>
|
|
28
28
|
<ButtonComponent
|
|
29
29
|
text="Go Back"
|
|
@@ -3,9 +3,10 @@ import styled from "styled-components";
|
|
|
3
3
|
|
|
4
4
|
import logo from "../../assets/images/logo.png";
|
|
5
5
|
import bg from "./images/bg-404.png";
|
|
6
|
-
import redirectURL from "../../utils/redirectURL";
|
|
7
6
|
import useCustomNavigate from "../../hooks/useCustomNavigate";
|
|
8
7
|
import getCookie from "../../utils/getCookie";
|
|
8
|
+
import useTranslation from "../../hooks/useTranslation";
|
|
9
|
+
import FullPageLoader from "../fullPageLoader";
|
|
9
10
|
|
|
10
11
|
const ErrorWrapper = ({
|
|
11
12
|
title = "Oops!",
|
|
@@ -31,7 +32,21 @@ const ErrorWrapper = ({
|
|
|
31
32
|
navigate(fallbackPath || "/", { reload: true });
|
|
32
33
|
}
|
|
33
34
|
};
|
|
34
|
-
|
|
35
|
+
|
|
36
|
+
const wordBank = [
|
|
37
|
+
"Opss...",
|
|
38
|
+
"Page not found",
|
|
39
|
+
"Seems we can't find the page your looking for. Let's get you back home.",
|
|
40
|
+
"Go Back",
|
|
41
|
+
"There was an error",
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
const { findText, translations, isTranslationsLoading } =
|
|
45
|
+
useTranslation(wordBank);
|
|
46
|
+
|
|
47
|
+
return isTranslationsLoading && Object.keys(translations)?.length === 0 ? (
|
|
48
|
+
<FullPageLoader hasBackground fixed={true} />
|
|
49
|
+
) : (
|
|
35
50
|
<Container>
|
|
36
51
|
<Navbar>
|
|
37
52
|
<a href="/">
|
|
@@ -40,13 +55,13 @@ const ErrorWrapper = ({
|
|
|
40
55
|
</Navbar>
|
|
41
56
|
<Content>
|
|
42
57
|
<LeftDiv>
|
|
43
|
-
<h1>{title}</h1>
|
|
58
|
+
<h1>{findText(title)}</h1>
|
|
44
59
|
<div>
|
|
45
|
-
{subTitle && <h2>{subTitle}</h2>}
|
|
46
|
-
{message && <p>{message}</p>}
|
|
60
|
+
{subTitle && <h2>{findText(subTitle)}</h2>}
|
|
61
|
+
{message && <p>{findText(message)}</p>}
|
|
47
62
|
</div>
|
|
48
63
|
|
|
49
|
-
<button onClick={handleNav}>{btnText}</button>
|
|
64
|
+
<button onClick={handleNav}>{findText(btnText)}</button>
|
|
50
65
|
</LeftDiv>
|
|
51
66
|
<ImageBox>
|
|
52
67
|
<img src={bgSrc || bg} alt="" />
|
|
@@ -183,7 +183,9 @@ const AccountDropdown = (props) => {
|
|
|
183
183
|
<>
|
|
184
184
|
{props?.instructorAccountData?.length > 0 && (
|
|
185
185
|
<div>
|
|
186
|
-
<h3 style={{ marginBottom: 10 }}>
|
|
186
|
+
<h3 style={{ marginBottom: 10 }}>
|
|
187
|
+
{props.findText("Instructor account")}
|
|
188
|
+
</h3>
|
|
187
189
|
{props?.instructorAccountData?.map((instructorItem, idx) => {
|
|
188
190
|
console.log("account>>", instructorItem);
|
|
189
191
|
return (
|
|
@@ -233,7 +235,9 @@ const AccountDropdown = (props) => {
|
|
|
233
235
|
)}
|
|
234
236
|
{props?.enterpriseAccountData?.length > 0 && (
|
|
235
237
|
<div>
|
|
236
|
-
<h3 style={{ marginBottom: 10 }}>
|
|
238
|
+
<h3 style={{ marginBottom: 10 }}>
|
|
239
|
+
{props?.findText("Enterprise account")}
|
|
240
|
+
</h3>
|
|
237
241
|
{props?.enterpriseAccountData?.map((enterpriseItem, idx) => (
|
|
238
242
|
<div
|
|
239
243
|
className={`account-info ${
|
|
@@ -279,7 +283,9 @@ const AccountDropdown = (props) => {
|
|
|
279
283
|
// dont show personal accounts for developer user
|
|
280
284
|
props?.personalAccountData?.length > 0 && (
|
|
281
285
|
<div>
|
|
282
|
-
<h3 style={{ marginBottom: 10 }}>
|
|
286
|
+
<h3 style={{ marginBottom: 10 }}>
|
|
287
|
+
{props?.findText("Student account")}
|
|
288
|
+
</h3>
|
|
283
289
|
{props?.personalAccountData?.map((personalItem, idx) => (
|
|
284
290
|
<div
|
|
285
291
|
className={`account-info ${
|
|
@@ -329,7 +335,10 @@ const AccountDropdown = (props) => {
|
|
|
329
335
|
<AccountDropdownFooter>
|
|
330
336
|
<button
|
|
331
337
|
onClick={() => {
|
|
332
|
-
window.location.
|
|
338
|
+
window.location.hostname?.includes("coming")
|
|
339
|
+
? (window.location.href =
|
|
340
|
+
"https://learngual.com/auth/account-type")
|
|
341
|
+
: (window.location.href = "/auth/account-type");
|
|
333
342
|
}}
|
|
334
343
|
style={{ cursor: "pointer" }}
|
|
335
344
|
>
|
|
@@ -552,8 +552,10 @@ const HeaderComponent = (props) => {
|
|
|
552
552
|
<h5 style={{ textTransform: "capitalize" }}>{accountName}</h5>
|
|
553
553
|
<h6 style={{ textTransform: "capitalize" }}>
|
|
554
554
|
{props?.selectedAccount?.type?.toLowerCase() === "personal"
|
|
555
|
-
? "Student"
|
|
556
|
-
: props?.
|
|
555
|
+
? props?.findtext("Student")
|
|
556
|
+
: props?.findtext(
|
|
557
|
+
props?.selectedAccount?.type?.toLowerCase()
|
|
558
|
+
)}
|
|
557
559
|
</h6>
|
|
558
560
|
</div>
|
|
559
561
|
<ArrowDownIcon width={16} height={10} />
|
|
@@ -7,11 +7,11 @@ export const languagesData = [
|
|
|
7
7
|
flag: usFlag,
|
|
8
8
|
slug: "en",
|
|
9
9
|
},
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
10
|
+
{
|
|
11
|
+
name: "Korean",
|
|
12
|
+
flag: koreanFlag,
|
|
13
|
+
slug: "ko",
|
|
14
|
+
},
|
|
15
15
|
];
|
|
16
16
|
|
|
17
17
|
export const selectedLanguageData = {
|
|
@@ -33,11 +33,11 @@ const RenewModal = ({
|
|
|
33
33
|
<BtnGroup>
|
|
34
34
|
<Button
|
|
35
35
|
type="secondary"
|
|
36
|
-
text={btn1Text || "Select
|
|
36
|
+
text={btn1Text || "Select plans"}
|
|
37
37
|
onClick={btn1onClick}
|
|
38
38
|
/>
|
|
39
39
|
<Button
|
|
40
|
-
text={btn2Text || "Renew
|
|
40
|
+
text={btn2Text || "Renew subscription"}
|
|
41
41
|
onClick={btn2onClick}
|
|
42
42
|
/>
|
|
43
43
|
</BtnGroup>
|
|
@@ -17,6 +17,7 @@ const InstructorAccountSwitcher = ({
|
|
|
17
17
|
affiliateList,
|
|
18
18
|
setSearch,
|
|
19
19
|
defaultAffiliate,
|
|
20
|
+
findText,
|
|
20
21
|
}) => {
|
|
21
22
|
// const [expiryFlow, setExpiryFlow] = useState();
|
|
22
23
|
const [switchValue, setSwitchValue] = useState("affiliates");
|
|
@@ -169,7 +170,7 @@ const InstructorAccountSwitcher = ({
|
|
|
169
170
|
className="placeholder"
|
|
170
171
|
onClick={() => setDropdown(!dropdown)}
|
|
171
172
|
>
|
|
172
|
-
<p>Select affiliate</p>
|
|
173
|
+
<p>{findText("Select affiliate")}</p>
|
|
173
174
|
<ArrowDown />
|
|
174
175
|
</div>
|
|
175
176
|
) : (
|
|
@@ -189,7 +190,7 @@ const InstructorAccountSwitcher = ({
|
|
|
189
190
|
<AffiliatesDropDownWrapper>
|
|
190
191
|
<div className="search_wrapper">
|
|
191
192
|
<Search
|
|
192
|
-
placeholder="Search
|
|
193
|
+
placeholder={findText("Search affiliates")}
|
|
193
194
|
onSubmit={setSearch}
|
|
194
195
|
/>
|
|
195
196
|
</div>
|
|
@@ -205,7 +206,9 @@ const InstructorAccountSwitcher = ({
|
|
|
205
206
|
<img src={item?.image} alt="" />
|
|
206
207
|
<p>{item?.name}</p>
|
|
207
208
|
</div>
|
|
208
|
-
{item.count ?
|
|
209
|
+
{item.count ? (
|
|
210
|
+
<span>{findText(item.count)}</span>
|
|
211
|
+
) : null}
|
|
209
212
|
</li>
|
|
210
213
|
))}
|
|
211
214
|
</ul>
|
|
@@ -222,7 +225,7 @@ const InstructorAccountSwitcher = ({
|
|
|
222
225
|
className={switchValue !== "affiliates" ? "active" : ""}
|
|
223
226
|
onClick={() => handleSwitch(1)}
|
|
224
227
|
>
|
|
225
|
-
<span>Personal</span>
|
|
228
|
+
<span>{findText("Personal")}</span>
|
|
226
229
|
<span className="circle"></span>
|
|
227
230
|
</button>
|
|
228
231
|
)}
|
|
@@ -230,7 +233,7 @@ const InstructorAccountSwitcher = ({
|
|
|
230
233
|
className={switchValue === "affiliates" ? "active" : ""}
|
|
231
234
|
onClick={() => handleSwitch(2)}
|
|
232
235
|
>
|
|
233
|
-
<span>Affiliates</span>
|
|
236
|
+
<span>{findText("Affiliates")}</span>
|
|
234
237
|
<span className="circle"></span>
|
|
235
238
|
</button>
|
|
236
239
|
</div>
|
|
@@ -23,7 +23,10 @@ import {
|
|
|
23
23
|
AccountsIconActive,
|
|
24
24
|
} from "../assets/adminSvg/accountsIcon";
|
|
25
25
|
import { ManagersIcon, ManagersIconActive } from "../assets/adminSvg/managers";
|
|
26
|
-
import {
|
|
26
|
+
import {
|
|
27
|
+
RequestsIcon,
|
|
28
|
+
RequestsIconActive,
|
|
29
|
+
} from "../assets/adminSvg/requestsIcon";
|
|
27
30
|
|
|
28
31
|
export const adminSideMenuOptions = [
|
|
29
32
|
{
|
|
@@ -139,25 +142,25 @@ export const adminSideMenuOptions = [
|
|
|
139
142
|
path: "/personal-account",
|
|
140
143
|
hasNotification: false,
|
|
141
144
|
notifications: null,
|
|
142
|
-
text: "Personal
|
|
145
|
+
text: "Personal account",
|
|
143
146
|
},
|
|
144
147
|
{
|
|
145
148
|
path: "/instructor-account",
|
|
146
149
|
hasNotification: false,
|
|
147
150
|
notifications: null,
|
|
148
|
-
text: "Instructor
|
|
151
|
+
text: "Instructor account",
|
|
149
152
|
},
|
|
150
153
|
{
|
|
151
154
|
path: "/enterprise-account",
|
|
152
155
|
hasNotification: false,
|
|
153
156
|
notifications: null,
|
|
154
|
-
text: "Enterprise
|
|
157
|
+
text: "Enterprise account",
|
|
155
158
|
},
|
|
156
159
|
{
|
|
157
160
|
path: "/developer-account",
|
|
158
161
|
hasNotification: false,
|
|
159
162
|
notifications: null,
|
|
160
|
-
text: "Developer
|
|
163
|
+
text: "Developer account",
|
|
161
164
|
},
|
|
162
165
|
],
|
|
163
166
|
},
|
|
@@ -220,7 +223,7 @@ export const adminSideMenuOptions = [
|
|
|
220
223
|
path: "/all-users",
|
|
221
224
|
icon: <UsersIcon />,
|
|
222
225
|
iconActive: <UsersIconActive />,
|
|
223
|
-
text: "All
|
|
226
|
+
text: "All users",
|
|
224
227
|
hasNotification: false,
|
|
225
228
|
hasDropdown: false,
|
|
226
229
|
notifications: null,
|
|
@@ -229,7 +232,7 @@ export const adminSideMenuOptions = [
|
|
|
229
232
|
],
|
|
230
233
|
},
|
|
231
234
|
{
|
|
232
|
-
optionType: "Dev.
|
|
235
|
+
optionType: "Dev. management",
|
|
233
236
|
routes: [
|
|
234
237
|
{
|
|
235
238
|
path: "/accounts",
|
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
import React, { useCallback, useContext, useEffect, useState } from "react";
|
|
2
2
|
import useAxios from "axios-hooks";
|
|
3
3
|
|
|
4
|
+
// Global cache for master translations
|
|
5
|
+
if (!window.translationCache) {
|
|
6
|
+
window.translationCache = {};
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const CACHE_DURATION = 60 * 60 * 1000; // 1 hour in milliseconds
|
|
10
|
+
const S3_BASE_URL =
|
|
11
|
+
"https://learngual-bucket.sfo3.digitaloceanspaces.com/media/media/";
|
|
12
|
+
|
|
4
13
|
const useTranslation = (initialSentences = []) => {
|
|
5
14
|
const value = localStorage?.getItem("defaultLang");
|
|
6
15
|
|
|
@@ -14,7 +23,7 @@ const useTranslation = (initialSentences = []) => {
|
|
|
14
23
|
}, [value]);
|
|
15
24
|
|
|
16
25
|
const [defaultLang, setDefaultLang] = useState(value ?? "en");
|
|
17
|
-
const [translations, setTranslations] = useState({}); // returned translation from
|
|
26
|
+
const [translations, setTranslations] = useState({}); // returned translation from S3
|
|
18
27
|
const [isTranslationsLoading, setIsTranslationsLoading] = useState(true);
|
|
19
28
|
|
|
20
29
|
const findText = useCallback(
|
|
@@ -24,7 +33,7 @@ const useTranslation = (initialSentences = []) => {
|
|
|
24
33
|
[translations]
|
|
25
34
|
);
|
|
26
35
|
|
|
27
|
-
// api request for sending data to backend
|
|
36
|
+
// api request for sending data to backend (fire-and-forget)
|
|
28
37
|
const [{ ...translateData }, translate] = useAxios(
|
|
29
38
|
{
|
|
30
39
|
method: "POST",
|
|
@@ -34,14 +43,17 @@ const useTranslation = (initialSentences = []) => {
|
|
|
34
43
|
}
|
|
35
44
|
);
|
|
36
45
|
|
|
37
|
-
|
|
46
|
+
/**
|
|
47
|
+
* Send translation request to backend (fire-and-forget)
|
|
48
|
+
*/
|
|
49
|
+
const sendTranslationRequestToBackend = async (language, words) => {
|
|
38
50
|
try {
|
|
39
|
-
await
|
|
51
|
+
// Fire-and-forget - don't await or use response
|
|
52
|
+
translate({
|
|
40
53
|
url: `/iam/v1/utils/translate/`,
|
|
41
54
|
data: {
|
|
42
55
|
language,
|
|
43
56
|
sentences: words,
|
|
44
|
-
// kwargs,
|
|
45
57
|
},
|
|
46
58
|
params: {
|
|
47
59
|
_account: "",
|
|
@@ -51,38 +63,135 @@ const useTranslation = (initialSentences = []) => {
|
|
|
51
63
|
},
|
|
52
64
|
});
|
|
53
65
|
} catch (err) {
|
|
54
|
-
console.
|
|
66
|
+
console.warn("Translation request failed:", err);
|
|
55
67
|
}
|
|
56
68
|
};
|
|
57
69
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
70
|
+
/**
|
|
71
|
+
* Fetch master translations from S3 with caching
|
|
72
|
+
*/
|
|
73
|
+
const getMasterTranslationsWithCache = async () => {
|
|
74
|
+
const now = Date.now();
|
|
75
|
+
const cached = window.translationCache.master;
|
|
61
76
|
|
|
62
|
-
|
|
63
|
-
if (
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
77
|
+
// Check if cache is valid and not expired
|
|
78
|
+
if (cached && cached.data && cached.expires > now) {
|
|
79
|
+
return cached.data;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
// Fetch from S3 - master JSON contains all languages
|
|
84
|
+
const response = await fetch(
|
|
85
|
+
`${S3_BASE_URL}qFpINMa05DrgUgzO0PEboReejximpq2r3VL2AmFZAwIq6fTN3A.json`
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
if (!response.ok) {
|
|
89
|
+
throw new Error(`Failed to fetch translations: ${response.status}`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const masterTranslations = await response.json();
|
|
93
|
+
|
|
94
|
+
console.log(masterTranslations, "MASTER");
|
|
95
|
+
|
|
96
|
+
// Cache in memory (single cache for all languages since JSON contains all)
|
|
97
|
+
window.translationCache.master = {
|
|
98
|
+
data: masterTranslations,
|
|
99
|
+
timestamp: now,
|
|
100
|
+
expires: now + CACHE_DURATION,
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
return masterTranslations;
|
|
104
|
+
} catch (error) {
|
|
105
|
+
console.error("Failed to fetch master translations:", error);
|
|
106
|
+
|
|
107
|
+
// Fallback to cached data if available (even if expired)
|
|
108
|
+
if (cached && cached.data) {
|
|
109
|
+
return cached.data;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
throw error;
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Extract relevant translations for specific texts
|
|
118
|
+
*/
|
|
119
|
+
const extractRelevantTranslations = (masterTranslations, words, language) => {
|
|
120
|
+
const relevantTranslations = {};
|
|
121
|
+
const languageCode = language.toUpperCase(); // ko -> KO, fr -> FR
|
|
122
|
+
|
|
123
|
+
words.forEach((word) => {
|
|
124
|
+
if (masterTranslations[word] && masterTranslations[word][languageCode]) {
|
|
125
|
+
const translation = masterTranslations[word][languageCode];
|
|
126
|
+
// Only use translation if it's not empty
|
|
127
|
+
relevantTranslations[word] = translation.trim() || word;
|
|
128
|
+
} else {
|
|
129
|
+
// Return original text if translation not found
|
|
130
|
+
relevantTranslations[word] = word;
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
return relevantTranslations;
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const handleTranslate = async (language, words) => {
|
|
138
|
+
if (!words || words.length === 0) {
|
|
139
|
+
setIsTranslationsLoading(false);
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
try {
|
|
144
|
+
// Step 1: Fire-and-forget translation request to backend
|
|
145
|
+
sendTranslationRequestToBackend(language, words);
|
|
146
|
+
|
|
147
|
+
// Step 2: Get master translations (cached or fresh from S3)
|
|
148
|
+
const masterTranslations = await getMasterTranslationsWithCache();
|
|
149
|
+
|
|
150
|
+
// Step 3: Extract only the translations we need
|
|
151
|
+
const relevantTranslations = extractRelevantTranslations(
|
|
152
|
+
masterTranslations,
|
|
153
|
+
words,
|
|
154
|
+
language
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
// Step 4: Update state
|
|
158
|
+
setTranslations((prevTranslations) => ({
|
|
159
|
+
...prevTranslations,
|
|
160
|
+
...relevantTranslations,
|
|
161
|
+
}));
|
|
162
|
+
|
|
163
|
+
setIsTranslationsLoading(false);
|
|
164
|
+
} catch (error) {
|
|
165
|
+
console.error("Failed to load translations:", error);
|
|
166
|
+
|
|
167
|
+
// Fallback: return original texts
|
|
168
|
+
const fallbackTranslations = {};
|
|
169
|
+
words.forEach((word) => {
|
|
170
|
+
fallbackTranslations[word] = word;
|
|
67
171
|
});
|
|
172
|
+
|
|
68
173
|
setTranslations((prevTranslations) => ({
|
|
69
174
|
...prevTranslations,
|
|
70
|
-
...
|
|
175
|
+
...fallbackTranslations,
|
|
71
176
|
}));
|
|
177
|
+
|
|
178
|
+
setIsTranslationsLoading(false);
|
|
72
179
|
}
|
|
73
|
-
}
|
|
180
|
+
};
|
|
74
181
|
|
|
75
182
|
useEffect(() => {
|
|
76
|
-
if (
|
|
183
|
+
if (initialSentences && initialSentences.length > 0) {
|
|
184
|
+
handleTranslate(defaultLang, initialSentences);
|
|
185
|
+
} else {
|
|
77
186
|
setIsTranslationsLoading(false);
|
|
78
187
|
}
|
|
79
|
-
}, [
|
|
80
|
-
console.log("translateData", translateData);
|
|
188
|
+
}, [defaultLang]);
|
|
81
189
|
|
|
190
|
+
// Remove the old useEffect that processed translateData.data since we don't use API response anymore
|
|
82
191
|
return {
|
|
83
192
|
defaultLang,
|
|
84
193
|
setDefaultLang,
|
|
85
|
-
translateData,
|
|
194
|
+
translateData, // Keep for backward compatibility (though not used anymore)
|
|
86
195
|
handleTranslate,
|
|
87
196
|
translations,
|
|
88
197
|
findText,
|
package/src/utils/translation.js
CHANGED
|
@@ -1,6 +1,142 @@
|
|
|
1
1
|
import { db } from "./db";
|
|
2
2
|
import axios from "./axiosConfig";
|
|
3
3
|
|
|
4
|
+
// Global cache for master translations
|
|
5
|
+
if (!window.translationCache) {
|
|
6
|
+
window.translationCache = {};
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const CACHE_DURATION = 60 * 60 * 1000; // 1 hour in milliseconds
|
|
10
|
+
const S3_BASE_URL =
|
|
11
|
+
"https://learngual-bucket.sfo3.digitaloceanspaces.com/media/media/";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Send translation request to backend (fire-and-forget)
|
|
15
|
+
*/
|
|
16
|
+
async function sendTranslationRequest(texts, kwargs, language) {
|
|
17
|
+
try {
|
|
18
|
+
axios({
|
|
19
|
+
url: "/iam/v1/utils/translate/",
|
|
20
|
+
method: "POST",
|
|
21
|
+
data: {
|
|
22
|
+
language,
|
|
23
|
+
sentences: texts,
|
|
24
|
+
kwargs,
|
|
25
|
+
},
|
|
26
|
+
params: {
|
|
27
|
+
_account: "",
|
|
28
|
+
},
|
|
29
|
+
authRequired: false,
|
|
30
|
+
});
|
|
31
|
+
} catch (error) {
|
|
32
|
+
// Fire-and-forget, ignore errors
|
|
33
|
+
console.warn("Translation request failed:", error);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Fetch master translations from S3 with caching
|
|
39
|
+
* Since the JSON contains all languages, we cache it once regardless of language
|
|
40
|
+
*/
|
|
41
|
+
async function getMasterTranslationsWithCache() {
|
|
42
|
+
const now = Date.now();
|
|
43
|
+
const cached = window.translationCache.master;
|
|
44
|
+
|
|
45
|
+
// Check if cache is valid and not expired
|
|
46
|
+
if (cached && cached.data && cached.expires > now) {
|
|
47
|
+
return cached.data;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
// Fetch from S3 - master JSON contains all languages
|
|
52
|
+
const response = await fetch(
|
|
53
|
+
`${S3_BASE_URL}qFpINMa05DrgUgzO0PEboReejximpq2r3VL2AmFZAwIq6fTN3A.json`
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
if (!response.ok) {
|
|
57
|
+
throw new Error(`Failed to fetch translations: ${response.status}`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const masterTranslations = await response.json();
|
|
61
|
+
|
|
62
|
+
// Cache in memory (single cache for all languages since JSON contains all)
|
|
63
|
+
window.translationCache.master = {
|
|
64
|
+
data: masterTranslations,
|
|
65
|
+
timestamp: now,
|
|
66
|
+
expires: now + CACHE_DURATION,
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
console.log(masterTranslations, "master");
|
|
70
|
+
|
|
71
|
+
return masterTranslations;
|
|
72
|
+
} catch (error) {
|
|
73
|
+
console.error("Failed to fetch master translations:", error);
|
|
74
|
+
|
|
75
|
+
// Fallback to cached data if available (even if expired)
|
|
76
|
+
if (cached && cached.data) {
|
|
77
|
+
return cached.data;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
throw error;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Extract relevant translations for specific texts
|
|
86
|
+
* Supports multiple languages: ko -> KO, fr -> FR, etc.
|
|
87
|
+
*/
|
|
88
|
+
function extractRelevantTranslations(masterTranslations, texts, language) {
|
|
89
|
+
const relevantTranslations = {};
|
|
90
|
+
const languageCode = language.toUpperCase(); // ko -> KO, fr -> FR
|
|
91
|
+
|
|
92
|
+
texts.forEach((text) => {
|
|
93
|
+
if (masterTranslations[text] && masterTranslations[text][languageCode]) {
|
|
94
|
+
const translation = masterTranslations[text][languageCode];
|
|
95
|
+
// Only use translation if it's not empty
|
|
96
|
+
relevantTranslations[text] = translation.trim() || text;
|
|
97
|
+
} else {
|
|
98
|
+
// Return original text if translation not found
|
|
99
|
+
relevantTranslations[text] = text;
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
return relevantTranslations;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Cache screen-specific translations in IndexedDB
|
|
108
|
+
*/
|
|
109
|
+
async function cacheScreenTranslations(translations, screenId, language) {
|
|
110
|
+
try {
|
|
111
|
+
const translationEntries = Object.keys(translations).map((key) => ({
|
|
112
|
+
key,
|
|
113
|
+
value: translations[key],
|
|
114
|
+
screenId,
|
|
115
|
+
updatedAt: new Date().toDateString(),
|
|
116
|
+
}));
|
|
117
|
+
|
|
118
|
+
await db[language].bulkPut(translationEntries);
|
|
119
|
+
} catch (error) {
|
|
120
|
+
console.error("Failed to cache translations:", error);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Get cached translations from IndexedDB as fallback
|
|
126
|
+
*/
|
|
127
|
+
async function getCachedTranslations(screenId, language) {
|
|
128
|
+
try {
|
|
129
|
+
const cached = await db[language].where({ screenId }).toArray();
|
|
130
|
+
return cached.reduce((acc, item) => {
|
|
131
|
+
acc[item.key] = item.value;
|
|
132
|
+
return acc;
|
|
133
|
+
}, {});
|
|
134
|
+
} catch (error) {
|
|
135
|
+
console.error("Failed to get cached translations:", error);
|
|
136
|
+
return {};
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
4
140
|
async function loadTranslations(language = "ko") {
|
|
5
141
|
await new Promise((resolve) => {
|
|
6
142
|
//check if loading else resolve
|
|
@@ -25,38 +161,42 @@ async function loadTranslations(language = "ko") {
|
|
|
25
161
|
*/
|
|
26
162
|
const kwargList = await db.kwarg.where({ screenId }).toArray();
|
|
27
163
|
const kwargs = kwargList.reduce((x, t) => ({ ...x, [t.key]: t.value }), {});
|
|
28
|
-
/**
|
|
29
|
-
* @type {{ data: {has_translated: boolean; result: string[]} }}
|
|
30
|
-
*/
|
|
31
|
-
const res = await axios({
|
|
32
|
-
url: "/iam/v1/utils/translate/",
|
|
33
|
-
method: "POST",
|
|
34
|
-
data: {
|
|
35
|
-
language,
|
|
36
|
-
sentences: texts,
|
|
37
|
-
kwargs,
|
|
38
|
-
},
|
|
39
|
-
params: {
|
|
40
|
-
_account: "",
|
|
41
|
-
},
|
|
42
|
-
authRequired: false,
|
|
43
|
-
});
|
|
44
|
-
// build an object using texts as key and results from request as value
|
|
45
|
-
if (!res.data) return {}; // somehow data was not found
|
|
46
|
-
const translations = texts.reduce(
|
|
47
|
-
(x, y, z) => ({ ...x, [y]: res.data.result[z] }),
|
|
48
|
-
{}
|
|
49
|
-
);
|
|
50
|
-
db[language].bulkPut(
|
|
51
|
-
Object.keys(translations).map((x, i) => ({
|
|
52
|
-
key: texts[i],
|
|
53
|
-
value: translations[x],
|
|
54
|
-
screenId,
|
|
55
|
-
updatedAt: new Date().toDateString(),
|
|
56
|
-
}))
|
|
57
|
-
);
|
|
58
164
|
|
|
59
|
-
|
|
165
|
+
// Step 1: Fire-and-forget translation request to backend
|
|
166
|
+
sendTranslationRequest(texts, kwargs, language);
|
|
167
|
+
|
|
168
|
+
try {
|
|
169
|
+
// Step 2: Get master translations (cached or fresh from S3)
|
|
170
|
+
const masterTranslations = await getMasterTranslationsWithCache();
|
|
171
|
+
|
|
172
|
+
// Step 3: Extract only the translations we need
|
|
173
|
+
const relevantTranslations = extractRelevantTranslations(
|
|
174
|
+
masterTranslations,
|
|
175
|
+
texts,
|
|
176
|
+
language
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
// Step 4: Cache the extracted translations in IndexedDB
|
|
180
|
+
await cacheScreenTranslations(relevantTranslations, screenId, language);
|
|
181
|
+
|
|
182
|
+
return relevantTranslations;
|
|
183
|
+
} catch (error) {
|
|
184
|
+
console.error("Failed to load translations:", error);
|
|
185
|
+
|
|
186
|
+
// Step 5: Fallback to cached translations from IndexedDB
|
|
187
|
+
const cachedTranslations = await getCachedTranslations(screenId, language);
|
|
188
|
+
|
|
189
|
+
// If we have cached translations, return them
|
|
190
|
+
if (Object.keys(cachedTranslations).length > 0) {
|
|
191
|
+
return cachedTranslations;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Last resort: return original texts
|
|
195
|
+
return texts.reduce((acc, text) => {
|
|
196
|
+
acc[text] = text;
|
|
197
|
+
return acc;
|
|
198
|
+
}, {});
|
|
199
|
+
}
|
|
60
200
|
}
|
|
61
201
|
|
|
62
202
|
export function extractBracedText(text) {
|