i18n
parent
9be79b1481
commit
c40cba88d4
|
@ -25,6 +25,9 @@
|
|||
"deep-chat-react": "^2.0.1",
|
||||
"i18next": "^23.7.6",
|
||||
"i18next-browser-languagedetector": "^7.2.0",
|
||||
"i18next-http-backend": "^2.6.1",
|
||||
"marked": "^14.1.2",
|
||||
"marked-directive": "^1.0.7",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-i18next": "^13.5.0",
|
||||
|
@ -6043,6 +6046,15 @@
|
|||
"node": ">= 4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/attributes-parser": {
|
||||
"version": "2.2.3",
|
||||
"resolved": "https://registry.npmjs.org/attributes-parser/-/attributes-parser-2.2.3.tgz",
|
||||
"integrity": "sha512-zjOUWt95la8AdUO+kP1GBOonWrV5jy9NjJP+z9tva/DSA6FIzGKcN/gk3tdqQf/pOeB8dkyd3FCPrjhELMmrkg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"json-loose": "^1.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/autolinker": {
|
||||
"version": "3.16.2",
|
||||
"resolved": "https://registry.npmjs.org/autolinker/-/autolinker-3.16.2.tgz",
|
||||
|
@ -7209,6 +7221,15 @@
|
|||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/cross-fetch": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-4.0.0.tgz",
|
||||
"integrity": "sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"node-fetch": "^2.6.12"
|
||||
}
|
||||
},
|
||||
"node_modules/cross-spawn": {
|
||||
"version": "7.0.3",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
|
||||
|
@ -10569,6 +10590,15 @@
|
|||
"@babel/runtime": "^7.23.2"
|
||||
}
|
||||
},
|
||||
"node_modules/i18next-http-backend": {
|
||||
"version": "2.6.1",
|
||||
"resolved": "https://registry.npmjs.org/i18next-http-backend/-/i18next-http-backend-2.6.1.tgz",
|
||||
"integrity": "sha512-rCilMAnlEQNeKOZY1+x8wLM5IpYOj10guGvEpeC59tNjj6MMreLIjIW8D1RclhD3ifLwn6d/Y9HEM1RUE6DSog==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cross-fetch": "4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/iconv-lite": {
|
||||
"version": "0.6.3",
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
|
||||
|
@ -13621,6 +13651,15 @@
|
|||
"integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/json-loose": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/json-loose/-/json-loose-1.2.4.tgz",
|
||||
"integrity": "sha512-lwMWNC5pvVI33rhYWmAsmtICWE2IH7euDY/iIPeMFE5AuzAifYgqQrjqSMzwbrFV6MWPs41XD+CajElHI4cZMQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"moo": "^0.5.2"
|
||||
}
|
||||
},
|
||||
"node_modules/json-parse-even-better-errors": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
|
||||
|
@ -13978,6 +14017,30 @@
|
|||
"tmpl": "1.0.5"
|
||||
}
|
||||
},
|
||||
"node_modules/marked": {
|
||||
"version": "14.1.2",
|
||||
"resolved": "https://registry.npmjs.org/marked/-/marked-14.1.2.tgz",
|
||||
"integrity": "sha512-f3r0yqpz31VXiDB/wj9GaOB0a2PRLQl6vJmXiFrniNwjkKdvakqJRULhjFKJpxOchlCRiG5fcacoUZY5Xa6PEQ==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"marked": "bin/marked.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
}
|
||||
},
|
||||
"node_modules/marked-directive": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/marked-directive/-/marked-directive-1.0.7.tgz",
|
||||
"integrity": "sha512-2OilBJg5kSM0b7ijKlmIne8k1eA1YrNAWasw84fmfihgWuOQJFPmI5GzZd3DgM8PSquAKnYx87Qt5j/2Sw7JNA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"attributes-parser": "^2.2.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"marked": ">=7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/mdn-data": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.4.tgz",
|
||||
|
@ -14176,6 +14239,12 @@
|
|||
"mkdirp": "bin/cmd.js"
|
||||
}
|
||||
},
|
||||
"node_modules/moo": {
|
||||
"version": "0.5.2",
|
||||
"resolved": "https://registry.npmjs.org/moo/-/moo-0.5.2.tgz",
|
||||
"integrity": "sha512-iSAJLHYKnX41mKcJKjqvnAN9sf0LMDTXDEvFv+ffuRR9a1MIuXLjMNL6EsnDHSkKLTWNqQQ5uo61P4EbU4NU+Q==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||
|
@ -14261,6 +14330,48 @@
|
|||
"tslib": "^2.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/node-fetch": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
|
||||
"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"whatwg-url": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "4.x || >=6.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"encoding": "^0.1.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"encoding": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/node-fetch/node_modules/tr46": {
|
||||
"version": "0.0.3",
|
||||
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
|
||||
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/node-fetch/node_modules/webidl-conversions": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
|
||||
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
|
||||
"license": "BSD-2-Clause"
|
||||
},
|
||||
"node_modules/node-fetch/node_modules/whatwg-url": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
|
||||
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tr46": "~0.0.3",
|
||||
"webidl-conversions": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/node-forge": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz",
|
||||
|
|
|
@ -20,6 +20,9 @@
|
|||
"deep-chat-react": "^2.0.1",
|
||||
"i18next": "^23.7.6",
|
||||
"i18next-browser-languagedetector": "^7.2.0",
|
||||
"i18next-http-backend": "^2.6.1",
|
||||
"marked": "^14.1.2",
|
||||
"marked-directive": "^1.0.7",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-i18next": "^13.5.0",
|
||||
|
|
|
@ -0,0 +1,225 @@
|
|||
{
|
||||
"common": {
|
||||
"firstName": "Vorname",
|
||||
"lastName": "Nachname",
|
||||
"email": "E-Mail",
|
||||
"actions": "Aktionen",
|
||||
"bannerSubtitle": "VERWALTEN",
|
||||
"messageRequestFailed": "Anfrage fehlgeschlagen. Bitte versuchen Sie es erneut.",
|
||||
"change": "Ändern",
|
||||
"cancel": "Abbrechen",
|
||||
"delete": "Löschen",
|
||||
"role": "Rolle",
|
||||
"status": "Status",
|
||||
"password": "Passwort",
|
||||
"create": "Erstellen",
|
||||
"firstNameRules": {
|
||||
"required": "Bitte geben Sie einen Vornamen ein",
|
||||
"minLength": "Der Vorname muss mindestens {{minLength}} Zeichen lang sein",
|
||||
"maxLength": "Der Vorname darf höchstens {{maxLength}} Zeichen lang sein"
|
||||
},
|
||||
"lastNameRules": {
|
||||
"required": "Bitte geben Sie einen Nachnamen ein",
|
||||
"minLength": "Der Nachname muss mindestens {{minLength}} Zeichen lang sein",
|
||||
"maxLength": "Der Nachname darf höchstens {{maxLength}} Zeichen lang sein"
|
||||
},
|
||||
"emailRules": {
|
||||
"valid": "Bitte geben Sie eine gültige E-Mail-Adresse ein"
|
||||
},
|
||||
"passwordRules": {
|
||||
"required": "Bitte geben Sie ein Passwort ein",
|
||||
"minLength": "Das Passwort muss mindestens {{minLength}} Zeichen lang sein",
|
||||
"maxLength": "Das Passwort darf höchstens {{maxLength}} Zeichen lang sein"
|
||||
}
|
||||
},
|
||||
"sideMenu": {
|
||||
"overview": "ÜBERSICHT",
|
||||
"board": "Board",
|
||||
"lessons": "Lektionen",
|
||||
"organization": "ORGANISATION",
|
||||
"team": "Team",
|
||||
"roles": "Rollen",
|
||||
"settings": "Einstellungen",
|
||||
"whatsNew": "Was gibt's Neues",
|
||||
"suggestFeature": "Feature vorschlagen",
|
||||
"contactSupport": "Support kontaktieren"
|
||||
},
|
||||
"sideMenuEditor": {
|
||||
"lessonStatus": "Status",
|
||||
"lessonStatusPublished": "Veröffentlicht",
|
||||
"lessonStatusDraft": "Entwurf",
|
||||
"messageLessonStatusSuccessfullyUpdated": "Lektionsstatus erfolgreich aktualisiert"
|
||||
},
|
||||
"userProfile": {
|
||||
"bannerTitle": "Kontoeinstellungen",
|
||||
"middleCardTitle": "Profil",
|
||||
"messageProfileSuccessfullyUpdated": "Profil erfolgreich aktualisiert",
|
||||
"language": "Sprache",
|
||||
"languageEnglish": "Englisch",
|
||||
"languageGerman": "Deutsch",
|
||||
"personalInformation": "Persönliche Informationen"
|
||||
},
|
||||
"organizationSettings": {
|
||||
"bannerTitle": "Einstellungen",
|
||||
"generalCard": {
|
||||
"title": "Allgemein",
|
||||
"primaryColor": "Primärfarbe",
|
||||
"companyName": "Firmenname",
|
||||
"messageSettingsSuccessfullyUpdated": "Einstellungen erfolgreich aktualisiert"
|
||||
},
|
||||
"mediaCard": {
|
||||
"title": "Medien",
|
||||
"logo": "Logo",
|
||||
"messageLogoSuccessfullyUpdated": "Logo erfolgreich aktualisiert",
|
||||
"banner": "Banner",
|
||||
"messageBannerSuccessfullyUpdated": "Banner erfolgreich aktualisiert"
|
||||
},
|
||||
"subdomainCard": {
|
||||
"subdomain": "Subdomain",
|
||||
"middleCardTitle": "Subdomain",
|
||||
"subdomainAlreadyTaken": "Diese Subdomain ist bereits vergeben",
|
||||
"modalChangeSubdomain": {
|
||||
"title": "Subdomain ändern",
|
||||
"message": "Durch die Änderung Ihrer Subdomain wird Ihre Organisation unter der neuen Subdomain erreichbar sein. Beachten Sie, dass Sie ausgeloggt und zur neuen Subdomain weitergeleitet werden. Außerdem wird die alte Subdomain für andere Benutzer zur Registrierung freigegeben.",
|
||||
"messageSubdomainSuccessfullyUpdated": "Subdomain erfolgreich aktualisiert",
|
||||
"messageRedirect": "Sie werden zur neuen Subdomain weitergeleitet..."
|
||||
},
|
||||
"rules": {
|
||||
"required": "Bitte geben Sie eine Subdomain ein",
|
||||
"valid": "Bitte geben Sie eine gültige Subdomain ein",
|
||||
"minLength": "Die Subdomain muss mindestens {{minLength}} Zeichen lang sein",
|
||||
"maxLength": "Die Subdomain darf höchstens {{maxLength}} Zeichen lang sein"
|
||||
}
|
||||
}
|
||||
},
|
||||
"roles": {
|
||||
"bannerTitle": "Rollen",
|
||||
"roleNames": {
|
||||
"d0f0fa0d-3f3b-438b-a76f-7febeb8aab57": "Admin",
|
||||
"b7359e12-359e-423b-b39c-f0d4069adebc": "Editor",
|
||||
"a1f084ad-d501-4015-b326-4c5c46fd1c5e": "Betrachter"
|
||||
},
|
||||
"collapseTitleTeam": "Team",
|
||||
"collapseTitleRoles": "Rollen",
|
||||
"roles": [
|
||||
{
|
||||
"id": 1,
|
||||
"category": "Team",
|
||||
"title": "Neues Teammitglied einladen",
|
||||
"description": "Berechtigung, ein Mitglied einzuladen. Eine E-Mail wird an das neue Mitglied gesendet."
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"category": "Team",
|
||||
"title": "Teammitglied entfernen",
|
||||
"description": "Berechtigung, ein Mitglied zu entfernen."
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"category": "Roles",
|
||||
"title": "Neue Rolle erstellen",
|
||||
"description": "Berechtigung, ein Mitglied einzuladen. Eine E-Mail wird an das neue Mitglied gesendet."
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"category": "Roles",
|
||||
"title": "Rolle löschen",
|
||||
"description": "Berechtigung, ein Mitglied einzuladen. Eine E-Mail wird an das neue Mitglied gesendet."
|
||||
}
|
||||
]
|
||||
},
|
||||
"team": {
|
||||
"bannerTitle": "Team",
|
||||
"addMemberButton": "Neues Mitglied hinzufügen",
|
||||
"changeRole": "Rolle ändern",
|
||||
"popConfirmRoleChange": {
|
||||
"title": "Rolle ändern zu",
|
||||
"messageUpdatedRoleSuccessfully": "Rolle erfolgreich aktualisiert"
|
||||
},
|
||||
"popConfirmDeleteMember": {
|
||||
"title": "Löschung des Teammitglieds bestätigen",
|
||||
"messageDeletedMemberSuccessfully": "Mitglied erfolgreich gelöscht"
|
||||
}
|
||||
},
|
||||
"teamCreateUser": {
|
||||
"middleCardTitle": "Benutzer erstellen",
|
||||
"messageUserSuccessfullyCreated": "Benutzer erfolgreich erstellt",
|
||||
"buttonCreateUser": "Benutzer erstellen"
|
||||
},
|
||||
"lessons": {
|
||||
"bannerTitle": "Lektionen",
|
||||
"messageLessonSuccessfullyCreated": "Lektion erfolgreich erstellt",
|
||||
"lessonStatusDrafts": "Entwürfe",
|
||||
"searchPlaceholder": "Suchen..."
|
||||
},
|
||||
"lessonPage": {
|
||||
"finishLessonButton": "Lektion beenden",
|
||||
"questions": "Fragen"
|
||||
},
|
||||
"lessonQuestions": {
|
||||
"messageQuestionSuccessfullyCreated": "Frage erfolgreich erstellt",
|
||||
"askAQuestion": "Frage stellen",
|
||||
"typeSomething": "Etwas schreiben...",
|
||||
"submitButton": "Absenden",
|
||||
"ruleMessageRequired": "Bitte schreiben Sie eine Frage",
|
||||
"questions": "Fragen",
|
||||
"reply": "Antworten",
|
||||
"hide": "Ausblenden",
|
||||
"ruleReplyRequired": "Bitte schreiben Sie eine Antwort",
|
||||
"replyPlaceholder": "Antwort schreiben"
|
||||
},
|
||||
"lessonsComponents": {
|
||||
"categories": {
|
||||
"Common": "Allgemein",
|
||||
"Media": "Medien"
|
||||
},
|
||||
"commonComponents": {
|
||||
"Header": "Überschrift",
|
||||
"Text": "Text"
|
||||
},
|
||||
"mediaComponents": {
|
||||
"Image": "Bild",
|
||||
"YouTube": "YouTube",
|
||||
"Video": "Video"
|
||||
}
|
||||
},
|
||||
"lessonComponentsConverter": {
|
||||
"textPlaceholder": "Text hier eingeben...",
|
||||
"noImageProvided": "Kein Bild bereitgestellt",
|
||||
"gallery": "Galerie",
|
||||
"chooseImageFrom": "Bild auswählen aus",
|
||||
"chooseAnotherImageFrom": "Ein anderes Bild auswählen aus",
|
||||
"noVideoProvided": "Kein Video bereitgestellt",
|
||||
"videoId": "Video-ID",
|
||||
"video": "Video",
|
||||
"chooseVideoFrom": "Video auswählen aus",
|
||||
"chooseAnotherVideoFrom": "Ein anderes Video auswählen aus",
|
||||
"notImplemented": "Nicht implementiert",
|
||||
"unkownType": "Unbekannter Typ"
|
||||
},
|
||||
"header": {
|
||||
"accountSettings": "Profil",
|
||||
"logout": "Abmelden"
|
||||
},
|
||||
"headerBar": {
|
||||
"backButton": "Zurück"
|
||||
},
|
||||
"myEmpty": {
|
||||
"noData": "Keine Daten"
|
||||
},
|
||||
"myErrorResult": {
|
||||
"title": "Ein Fehler ist aufgetreten",
|
||||
"subTitle": "Bitte versuchen Sie es später erneut."
|
||||
},
|
||||
"contactSupport": {
|
||||
"bannerTitle": "Support",
|
||||
"middleCardTitle": "Support kontaktieren",
|
||||
"paragraph": "Haben Sie Fragen oder benötigen Sie Hilfe? Kontaktieren Sie uns über die untenstende Email-Adresse und wir werden uns so schnell wie möglich bei Ihnen melden."
|
||||
},
|
||||
"whatsNew": {
|
||||
"bannerTitle": "Was gibt's Neues"
|
||||
},
|
||||
"suggestFeature": {
|
||||
"bannerTitle": "Feature vorschlagen"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,225 @@
|
|||
{
|
||||
"common": {
|
||||
"firstName": "First Name",
|
||||
"lastName": "Last Name",
|
||||
"email": "Email",
|
||||
"actions": "Actions",
|
||||
"bannerSubtitle": "MANAGE",
|
||||
"messageRequestFailed": "Request failed. Please try again.",
|
||||
"change": "Change",
|
||||
"cancel": "Cancel",
|
||||
"delete": "Delete",
|
||||
"role": "Role",
|
||||
"status": "Status",
|
||||
"password": "Password",
|
||||
"create": "Create",
|
||||
"firstNameRules": {
|
||||
"required": "Please enter a first name",
|
||||
"minLength": "First name must be at least {{minLength}} characters long",
|
||||
"maxLength": "First name must be at most {{maxLength}} characters long"
|
||||
},
|
||||
"lastNameRules": {
|
||||
"required": "Please enter a last name",
|
||||
"minLength": "Last name must be at least {{minLength}} characters long",
|
||||
"maxLength": "Last name must be at most {{maxLength}} characters long"
|
||||
},
|
||||
"emailRules": {
|
||||
"valid": "Please enter a valid email"
|
||||
},
|
||||
"passwordRules": {
|
||||
"required": "Please enter a password",
|
||||
"minLength": "Password must be at least {{minLength}} characters long",
|
||||
"maxLength": "Password must be at most {{maxLength}} characters long"
|
||||
}
|
||||
},
|
||||
"sideMenu": {
|
||||
"overview": "OVERVIEW",
|
||||
"board": "Board",
|
||||
"lessons": "Lessons",
|
||||
"organization": "ORGANIZATION",
|
||||
"team": "Team",
|
||||
"roles": "Roles",
|
||||
"settings": "Settings",
|
||||
"whatsNew": "What's New",
|
||||
"suggestFeature": "Suggest a Feature",
|
||||
"contactSupport": "Contact Support"
|
||||
},
|
||||
"sideMenuEditor": {
|
||||
"lessonStatus": "Status",
|
||||
"lessonStatusPublished": "Published",
|
||||
"lessonStatusDraft": "Draft",
|
||||
"messageLessonStatusSuccessfullyUpdated": "Lesson status successfully updated"
|
||||
},
|
||||
"userProfile": {
|
||||
"bannerTitle": "Account Settings",
|
||||
"middleCardTitle": "Profile",
|
||||
"messageProfileSuccessfullyUpdated": "Profile successfully updated",
|
||||
"language": "Language",
|
||||
"languageEnglish": "English",
|
||||
"languageGerman": "German",
|
||||
"personalInformation": "Personal Information"
|
||||
},
|
||||
"organizationSettings": {
|
||||
"bannerTitle": "Settings",
|
||||
"generalCard": {
|
||||
"title": "General",
|
||||
"primaryColor": "Primary Color",
|
||||
"companyName": "Company Name",
|
||||
"messageSettingsSuccessfullyUpdated": "Settings successfully updated"
|
||||
},
|
||||
"mediaCard": {
|
||||
"title": "Media",
|
||||
"logo": "Logo",
|
||||
"messageLogoSuccessfullyUpdated": "Logo successfully updated",
|
||||
"banner": "Banner",
|
||||
"messageBannerSuccessfullyUpdated": "Banner successfully updated"
|
||||
},
|
||||
"subdomainCard": {
|
||||
"subdomain": "Subdomain",
|
||||
"middleCardTitle": "Subdomain",
|
||||
"subdomainAlreadyTaken": "This subdomain already taken",
|
||||
"modalChangeSubdomain": {
|
||||
"title": "Change Subdomain",
|
||||
"message": "Changing your subdomain will make your organization available at the new subdomain. Please note that you will be logged out and redirected to the new subdomain. Also the old subdomain will be available for registration by other users.",
|
||||
"messageSubdomainSuccessfullyUpdated": "Subdomain successfully updated",
|
||||
"messageRedirect": "You will be redirected to the new subdomain..."
|
||||
},
|
||||
"rules": {
|
||||
"required": "Please enter a subdomain",
|
||||
"valid": "Please enter a valid subdomain",
|
||||
"minLength": "Subdomain must be at least {{minLength}} characters long",
|
||||
"maxLength": "Subdomain must be at most {{maxLength}} characters long"
|
||||
}
|
||||
}
|
||||
},
|
||||
"roles": {
|
||||
"bannerTitle": "Roles",
|
||||
"roleNames": {
|
||||
"d0f0fa0d-3f3b-438b-a76f-7febeb8aab57": "Admin",
|
||||
"b7359e12-359e-423b-b39c-f0d4069adebc": "Editor",
|
||||
"a1f084ad-d501-4015-b326-4c5c46fd1c5e": "Viewer"
|
||||
},
|
||||
"collapseTitleTeam": "Team",
|
||||
"collapseTitleRoles": "Roles",
|
||||
"roles": [
|
||||
{
|
||||
"id": 1,
|
||||
"category": "Team",
|
||||
"title": "Invite new team member",
|
||||
"description": "Permission to invite a member. An email will be sent to the new member."
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"category": "Team",
|
||||
"title": "Remove team member",
|
||||
"description": "Permission to remove a member."
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"category": "Roles",
|
||||
"title": "Create new role",
|
||||
"description": "Permission to invite a member. An email will be sent to the new member."
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"category": "Roles",
|
||||
"title": "Delete role",
|
||||
"description": "Permission to invite a member. An email will be sent to the new member."
|
||||
}
|
||||
]
|
||||
},
|
||||
"team": {
|
||||
"bannerTitle": "Team",
|
||||
"addMemberButton": "Add new member",
|
||||
"changeRole": "Change Role",
|
||||
"popConfirmRoleChange": {
|
||||
"title": "Change role to",
|
||||
"messageUpdatedRoleSuccessfully": "Role successfully updated"
|
||||
},
|
||||
"popConfirmDeleteMember": {
|
||||
"title": "Confirm deletion of team member",
|
||||
"messageDeletedMemberSuccessfully": "Member successfully deleted"
|
||||
}
|
||||
},
|
||||
"teamCreateUser": {
|
||||
"middleCardTitle": "Create User",
|
||||
"messageUserSuccessfullyCreated": "User successfully created",
|
||||
"buttonCreateUser": "Create User"
|
||||
},
|
||||
"lessons": {
|
||||
"bannerTitle": "Lessons",
|
||||
"messageLessonSuccessfullyCreated": "Lesson successfully created",
|
||||
"lessonStatusDrafts": "Drafts",
|
||||
"searchPlaceholder": "Search..."
|
||||
},
|
||||
"lessonPage": {
|
||||
"finishLessonButton": "Finish lesson",
|
||||
"questions": "Questions"
|
||||
},
|
||||
"lessonQuestions": {
|
||||
"messageQuestionSuccessfullyCreated": "Question successfully created",
|
||||
"askAQuestion": "Ask a question",
|
||||
"typeSomething": "Type something...",
|
||||
"submitButton": "Submit",
|
||||
"ruleMessageRequired": "Please write a question",
|
||||
"questions": "Questions",
|
||||
"reply": "Reply",
|
||||
"hide": "Hide",
|
||||
"ruleReplyRequired": "Please write a reply",
|
||||
"replyPlaceholder": "Write a reply"
|
||||
},
|
||||
"lessonsComponents": {
|
||||
"categories": {
|
||||
"Common": "Common",
|
||||
"Media": "Media"
|
||||
},
|
||||
"commonComponents": {
|
||||
"Header": "Header",
|
||||
"Text": "Text"
|
||||
},
|
||||
"mediaComponents": {
|
||||
"Image": "Image",
|
||||
"YouTube": "YouTube",
|
||||
"Video": "Video"
|
||||
}
|
||||
},
|
||||
"lessonComponentsConverter": {
|
||||
"textPlaceholder": "Input text here...",
|
||||
"noImageProvided": "No image provided",
|
||||
"gallery": "Gallery",
|
||||
"chooseImageFrom": "Choose image from",
|
||||
"chooseAnotherImageFrom": "Choose another image from",
|
||||
"noVideoProvided": "No video provided",
|
||||
"videoId": "Video ID",
|
||||
"video": "Video",
|
||||
"chooseVideoFrom": "Choose video from",
|
||||
"chooseAnotherVideoFrom": "Choose another video from",
|
||||
"notImplemented": "Not implemented",
|
||||
"unkownType": "Unknown type"
|
||||
},
|
||||
"header": {
|
||||
"accountSettings": "Profile",
|
||||
"logout": "Logout"
|
||||
},
|
||||
"headerBar": {
|
||||
"backButton": "Back"
|
||||
},
|
||||
"myEmpty": {
|
||||
"noData": "No data"
|
||||
},
|
||||
"myErrorResult": {
|
||||
"title": "Something went wrong",
|
||||
"subTitle": "Please try again later."
|
||||
},
|
||||
"contactSupport": {
|
||||
"bannerTitle": "Support",
|
||||
"middleCardTitle": "Contact Support",
|
||||
"paragraph": "Do you have any questions or need help? Contact us via the email address below and we will get back to you as soon as possible."
|
||||
},
|
||||
"whatsNew": {
|
||||
"bannerTitle": "What's New"
|
||||
},
|
||||
"suggestFeature": {
|
||||
"bannerTitle": "Suggest a Feature"
|
||||
}
|
||||
}
|
|
@ -1,131 +1,176 @@
|
|||
import { Avatar, Dropdown, Flex } from 'antd';
|
||||
import { isSideMenuCollapsed, setIsSideMenuCollapsed } from '../SideMenu/sideMenuSlice';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { EditOutlined, EyeOutlined, LeftOutlined, LogoutOutlined, MenuFoldOutlined, MenuUnfoldOutlined, MoonOutlined, SunOutlined, UserOutlined } from '@ant-design/icons';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import { darkMode, setDarkMode, setUserAuthenticated } from '../../reducers/appSlice';
|
||||
import styles from './styles.module.css';
|
||||
import { Constants } from 'core/utils/utils';
|
||||
import webSocketService from 'core/services/websocketService';
|
||||
import { userProfilePictureUrl } from 'core/reducers/appSlice';
|
||||
import MyUserAvatar from 'shared/components/MyUserAvatar';
|
||||
import { Button, Dropdown, Flex } from "antd";
|
||||
import {
|
||||
isSideMenuCollapsed,
|
||||
setIsSideMenuCollapsed,
|
||||
} from "../SideMenu/sideMenuSlice";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import {
|
||||
EditOutlined,
|
||||
EyeOutlined,
|
||||
LeftOutlined,
|
||||
LogoutOutlined,
|
||||
MenuFoldOutlined,
|
||||
MenuUnfoldOutlined,
|
||||
MoonOutlined,
|
||||
SunOutlined,
|
||||
UserOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { darkMode, setDarkMode } from "../../reducers/appSlice";
|
||||
import styles from "./styles.module.css";
|
||||
import { Constants } from "core/utils/utils";
|
||||
import webSocketService from "core/services/websocketService";
|
||||
import { userProfilePictureUrl } from "core/reducers/appSlice";
|
||||
import MyUserAvatar from "shared/components/MyUserAvatar";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
type HeaderBarProps = {
|
||||
theme?: 'light' | 'dark';
|
||||
onView?: () => void;
|
||||
onEdit?: () => void;
|
||||
backTo?: string;
|
||||
theme?: "light" | "dark";
|
||||
onView?: () => void;
|
||||
onEdit?: () => void;
|
||||
backTo?: string;
|
||||
sticky?: boolean;
|
||||
};
|
||||
|
||||
export default function HeaderBar(props: HeaderBarProps = { theme: 'light' }) {
|
||||
const dispatch = useDispatch();
|
||||
export default function HeaderBar(props: HeaderBarProps = { theme: "light" }) {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const isCollpased = useSelector(isSideMenuCollapsed);
|
||||
const isDarkMode = useSelector(darkMode);
|
||||
const profilePictureUrl = useSelector(userProfilePictureUrl);
|
||||
const isCollpased = useSelector(isSideMenuCollapsed);
|
||||
const isDarkMode = useSelector(darkMode);
|
||||
const profilePictureUrl = useSelector(userProfilePictureUrl);
|
||||
|
||||
const navigate = useNavigate();
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<Flex
|
||||
justify="space-between"
|
||||
align="center"
|
||||
style={{
|
||||
paddingTop: 12,
|
||||
paddingLeft: 12,
|
||||
paddingRight: 12,
|
||||
}}
|
||||
return (
|
||||
<Flex
|
||||
justify="space-between"
|
||||
align="center"
|
||||
style={{
|
||||
paddingTop: 12,
|
||||
paddingLeft: 12,
|
||||
paddingRight: 12,
|
||||
position: props.sticky ? "sticky" : "relative",
|
||||
top: 0,
|
||||
zIndex: 999,
|
||||
}}
|
||||
>
|
||||
<Flex align="center" gap={16}>
|
||||
<div
|
||||
className={isDarkMode ? styles.containerDark : styles.containerLight}
|
||||
style={{ borderRadius: 28, padding: 4 }}
|
||||
>
|
||||
<Flex align="center" gap={16}>
|
||||
<div className={props.theme === 'light' ? styles.containerLight : styles.containerDark} style={{ borderRadius: 28, padding: 4 }}>
|
||||
{isCollpased ? (
|
||||
<div className={styles.iconContainer} onClick={() => dispatch(setIsSideMenuCollapsed(false))}>
|
||||
<MenuUnfoldOutlined className={styles.icon} />
|
||||
</div>
|
||||
) : (
|
||||
<div className={styles.iconContainer} onClick={() => dispatch(setIsSideMenuCollapsed(true))}>
|
||||
<MenuFoldOutlined className={styles.icon} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{props.backTo && (
|
||||
<Link to={props.backTo}>
|
||||
<Flex gap={4}>
|
||||
<LeftOutlined />
|
||||
<span>Back</span>
|
||||
</Flex>
|
||||
</Link>
|
||||
)}
|
||||
</Flex>
|
||||
|
||||
<Flex
|
||||
align="center"
|
||||
className={props.theme === 'light' ? styles.containerLight : styles.containerDark}
|
||||
style={{
|
||||
borderRadius: 28,
|
||||
paddingLeft: 6,
|
||||
paddingRight: 6,
|
||||
paddingTop: 4,
|
||||
paddingBottom: 4,
|
||||
}}
|
||||
gap={8}
|
||||
{isCollpased ? (
|
||||
<div
|
||||
className={styles.iconContainer}
|
||||
onClick={() => dispatch(setIsSideMenuCollapsed(false))}
|
||||
>
|
||||
{props.onView && (
|
||||
<div className={styles.iconContainer} onClick={props.onView}>
|
||||
<EyeOutlined className={styles.icon} />
|
||||
</div>
|
||||
)}
|
||||
<MenuUnfoldOutlined className={styles.icon} />
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
className={styles.iconContainer}
|
||||
onClick={() => dispatch(setIsSideMenuCollapsed(true))}
|
||||
>
|
||||
<MenuFoldOutlined className={styles.icon} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{props.onEdit && (
|
||||
<div className={styles.iconContainer} onClick={props.onEdit}>
|
||||
<EditOutlined className={styles.icon} />
|
||||
</div>
|
||||
)}
|
||||
{props.backTo && (
|
||||
<Button
|
||||
type="link"
|
||||
style={{ color: isDarkMode ? "#fff" : "#1e1e1e" }}
|
||||
onClick={() =>
|
||||
navigate(
|
||||
props.backTo
|
||||
? props.backTo
|
||||
: Constants.ROUTE_PATHS.LESSIONS.ROOT
|
||||
)
|
||||
}
|
||||
icon={<LeftOutlined />}
|
||||
>
|
||||
{t("headerBar.backButton")}
|
||||
</Button>
|
||||
)}
|
||||
</Flex>
|
||||
|
||||
{isDarkMode ? (
|
||||
<div className={styles.iconContainer} onClick={() => dispatch(setDarkMode(false))}>
|
||||
<SunOutlined className={styles.icon} />
|
||||
</div>
|
||||
) : (
|
||||
<div className={styles.iconContainer} onClick={() => dispatch(setDarkMode(true))}>
|
||||
<MoonOutlined className={styles.icon} />
|
||||
</div>
|
||||
)}
|
||||
<Dropdown
|
||||
overlayStyle={{ minWidth: 150 }}
|
||||
trigger={['click']}
|
||||
menu={{
|
||||
items: [
|
||||
{
|
||||
key: '1',
|
||||
label: 'Profile',
|
||||
icon: <UserOutlined />,
|
||||
onClick: () => navigate(Constants.ROUTE_PATHS.ACCOUNT_SETTINGS),
|
||||
},
|
||||
{
|
||||
key: '2',
|
||||
label: 'Logout',
|
||||
icon: <LogoutOutlined />,
|
||||
danger: true,
|
||||
onClick: () => {
|
||||
webSocketService.disconnect();
|
||||
window.localStorage.removeItem('session');
|
||||
window.location.href = '/';
|
||||
},
|
||||
},
|
||||
],
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<MyUserAvatar size={34} profilePictureUrl={profilePictureUrl ? profilePictureUrl : ''} />
|
||||
</div>
|
||||
</Dropdown>
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
<Flex
|
||||
align="center"
|
||||
className={isDarkMode ? styles.containerDark : styles.containerLight}
|
||||
style={{
|
||||
borderRadius: 28,
|
||||
paddingLeft: 6,
|
||||
paddingRight: 6,
|
||||
paddingTop: 4,
|
||||
paddingBottom: 4,
|
||||
}}
|
||||
gap={8}
|
||||
>
|
||||
{props.onView && (
|
||||
<div className={styles.iconContainer} onClick={props.onView}>
|
||||
<EyeOutlined className={styles.icon} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
/* return (
|
||||
{props.onEdit && (
|
||||
<div className={styles.iconContainer} onClick={props.onEdit}>
|
||||
<EditOutlined className={styles.icon} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isDarkMode ? (
|
||||
<div
|
||||
className={styles.iconContainer}
|
||||
onClick={() => dispatch(setDarkMode(false))}
|
||||
>
|
||||
<SunOutlined className={styles.icon} />
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
className={styles.iconContainer}
|
||||
onClick={() => dispatch(setDarkMode(true))}
|
||||
>
|
||||
<MoonOutlined className={styles.icon} />
|
||||
</div>
|
||||
)}
|
||||
<Dropdown
|
||||
overlayStyle={{ minWidth: 150 }}
|
||||
trigger={["click"]}
|
||||
menu={{
|
||||
items: [
|
||||
{
|
||||
key: "1",
|
||||
label: t("header.accountSettings"),
|
||||
icon: <UserOutlined />,
|
||||
onClick: () => navigate(Constants.ROUTE_PATHS.ACCOUNT_SETTINGS),
|
||||
},
|
||||
{
|
||||
key: "2",
|
||||
label: t("header.logout"),
|
||||
icon: <LogoutOutlined />,
|
||||
danger: true,
|
||||
onClick: () => {
|
||||
webSocketService.disconnect();
|
||||
window.localStorage.removeItem("session");
|
||||
window.location.href = "/";
|
||||
},
|
||||
},
|
||||
],
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<MyUserAvatar
|
||||
size={34}
|
||||
profilePictureUrl={profilePictureUrl ? profilePictureUrl : ""}
|
||||
/>
|
||||
</div>
|
||||
</Dropdown>
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
|
||||
/* return (
|
||||
<Header
|
||||
style={{
|
||||
position: "sticky",
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
}
|
||||
|
||||
.containerDark {
|
||||
background-color: rgba(0, 0, 0, 0.22);
|
||||
background-color: rgba(39, 39, 39, 0.4);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
|
|
|
@ -46,9 +46,11 @@ import webSocketService, {
|
|||
import { WebSocketSendMessagesCmds } from "core/utils/webSocket";
|
||||
import { useMessage } from "core/context/MessageContext";
|
||||
import styles from "./styles.module.css";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export function SideMenuContent() {
|
||||
const location = useLocation();
|
||||
const { t } = useTranslation();
|
||||
const [selectedKeys, setSelectedKeys] = useState("/");
|
||||
const [openKeys, setOpenKeys] = useState([""]);
|
||||
|
||||
|
@ -65,7 +67,7 @@ export function SideMenuContent() {
|
|||
|
||||
let overviewGroup: ItemType<MenuItemType> = {
|
||||
key: "overviewGroup",
|
||||
label: "OVERVIEW",
|
||||
label: t("sideMenu.overview"),
|
||||
type: "group",
|
||||
children: [],
|
||||
};
|
||||
|
@ -73,13 +75,13 @@ export function SideMenuContent() {
|
|||
if (overviewGroup.children) {
|
||||
overviewGroup.children.push({
|
||||
key: Constants.ROUTE_PATHS.BOARD,
|
||||
label: "Board",
|
||||
label: t("sideMenu.board"),
|
||||
icon: <FundProjectionScreenOutlined />,
|
||||
});
|
||||
|
||||
overviewGroup.children.push({
|
||||
key: Constants.ROUTE_PATHS.LESSIONS.ROOT,
|
||||
label: "Lessons",
|
||||
label: t("sideMenu.lessons"),
|
||||
icon: <SnippetsOutlined />,
|
||||
});
|
||||
}
|
||||
|
@ -90,7 +92,7 @@ export function SideMenuContent() {
|
|||
|
||||
let organizationGroup: ItemType<MenuItemType> = {
|
||||
key: "organizationGroup",
|
||||
label: "ORGANIZATION",
|
||||
label: t("sideMenu.organization"),
|
||||
type: "group",
|
||||
children: [],
|
||||
};
|
||||
|
@ -98,19 +100,19 @@ export function SideMenuContent() {
|
|||
if (organizationGroup.children) {
|
||||
organizationGroup.children.push({
|
||||
key: Constants.ROUTE_PATHS.ORGANIZATION_TEAM,
|
||||
label: "Team",
|
||||
label: t("sideMenu.team"),
|
||||
icon: <TeamOutlined />,
|
||||
});
|
||||
|
||||
organizationGroup.children.push({
|
||||
key: Constants.ROUTE_PATHS.ORGANIZATION_ROLES,
|
||||
label: "Roles",
|
||||
label: t("sideMenu.roles"),
|
||||
icon: <ControlOutlined />,
|
||||
});
|
||||
|
||||
organizationGroup.children.push({
|
||||
key: Constants.ROUTE_PATHS.ORGANIZATION_SETTINGS,
|
||||
label: "Settings",
|
||||
label: t("sideMenu.settings"),
|
||||
icon: <SettingOutlined />,
|
||||
});
|
||||
}
|
||||
|
@ -127,7 +129,7 @@ export function SideMenuContent() {
|
|||
|
||||
items.push({
|
||||
key: Constants.ROUTE_PATHS.WHATS_NEW,
|
||||
label: "What's New",
|
||||
label: t("sideMenu.whatsNew"),
|
||||
icon: <QuestionCircleOutlined />,
|
||||
});
|
||||
|
||||
|
@ -135,7 +137,7 @@ export function SideMenuContent() {
|
|||
|
||||
items.push({
|
||||
key: Constants.ROUTE_PATHS.SUGGEST_FEATURE,
|
||||
label: "Suggest a Feature",
|
||||
label: t("sideMenu.suggestFeature"),
|
||||
icon: <MessageOutlined />,
|
||||
});
|
||||
|
||||
|
@ -143,7 +145,7 @@ export function SideMenuContent() {
|
|||
|
||||
items.push({
|
||||
key: Constants.ROUTE_PATHS.CONTACT_SUPPORT,
|
||||
label: "Contact Support",
|
||||
label: t("sideMenu.contactSupport"),
|
||||
icon: <WalletOutlined />,
|
||||
});
|
||||
|
||||
|
@ -264,6 +266,7 @@ export function SideMenuContent() {
|
|||
|
||||
export function SideMenuEditorContent() {
|
||||
const location = useLocation();
|
||||
const { t } = useTranslation();
|
||||
const [isDragging, setIsDragging] = useState<String | null>(null);
|
||||
const { lessonId } = useParams();
|
||||
const { success, error } = useMessage();
|
||||
|
@ -275,6 +278,10 @@ export function SideMenuEditorContent() {
|
|||
|
||||
const [reqUpdateLessonState] = useUpdateLessonStateMutation();
|
||||
|
||||
const lessonsComponentsCategories = t("lessonsComponents.categories", {
|
||||
returnObjects: true,
|
||||
}) as { [key: string]: string };
|
||||
|
||||
useEffect(() => {
|
||||
// subscribe to the current page
|
||||
const pathname = location.pathname;
|
||||
|
@ -319,7 +326,7 @@ export function SideMenuEditorContent() {
|
|||
>
|
||||
{componentsGroups.map((group, i) => (
|
||||
<div key={i} style={{ paddingTop: 16 }}>
|
||||
<span>{group.category}</span>
|
||||
<span>{lessonsComponentsCategories[group.category]}</span>
|
||||
|
||||
<Flex
|
||||
gap={16}
|
||||
|
@ -359,29 +366,35 @@ export function SideMenuEditorContent() {
|
|||
|
||||
<div style={{ padding: 12 }}>
|
||||
<Form form={form} layout="vertical">
|
||||
<Form.Item label="Status" name="state" style={{ marginBottom: 0 }}>
|
||||
<Form.Item
|
||||
label={t("sideMenuEditor.lessonStatus")}
|
||||
name="state"
|
||||
style={{ marginBottom: 0 }}
|
||||
>
|
||||
<Select
|
||||
style={{ width: "100%" }}
|
||||
onChange={async (value) => {
|
||||
console.log("state changed", value, lessonId);
|
||||
|
||||
try {
|
||||
await reqUpdateLessonState({
|
||||
lessonId: currentLnId,
|
||||
newState: value,
|
||||
}).unwrap();
|
||||
|
||||
success("Lesson state updated successfully");
|
||||
success(
|
||||
t("sideMenuEditor.messageLessonStatusSuccessfullyUpdated")
|
||||
);
|
||||
} catch (err) {
|
||||
console.log("error", err);
|
||||
error("Failed to update lesson state");
|
||||
error(t("common.messageRequestFailed"));
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Select.Option value={LessonState.Published}>
|
||||
Published
|
||||
{t("sideMenuEditor.lessonStatusPublished")}
|
||||
</Select.Option>
|
||||
<Select.Option value={LessonState.Draft}>
|
||||
{t("sideMenuEditor.lessonStatusDraft")}
|
||||
</Select.Option>
|
||||
<Select.Option value={LessonState.Draft}>Draft</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
|
@ -423,6 +436,7 @@ export function DraggableCreateComponent({
|
|||
}
|
||||
|
||||
function CreateComponent({ component }: { component: Component }) {
|
||||
const { t } = useTranslation();
|
||||
const { error } = useMessage();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
|
@ -431,6 +445,14 @@ function CreateComponent({ component }: { component: Component }) {
|
|||
|
||||
const [reqAddLessonContent] = useAddLessonContentMutation();
|
||||
|
||||
const lessonsCommonComponents = t("lessonsComponents.commonComponents", {
|
||||
returnObjects: true,
|
||||
}) as { [key: string]: string };
|
||||
|
||||
const lessonsMediaComponents = t("lessonsComponents.mediaComponents", {
|
||||
returnObjects: true,
|
||||
}) as { [key: string]: string };
|
||||
|
||||
return (
|
||||
<Flex
|
||||
vertical
|
||||
|
@ -447,24 +469,32 @@ function CreateComponent({ component }: { component: Component }) {
|
|||
}}
|
||||
onClick={async () => {
|
||||
try {
|
||||
const defaultData =
|
||||
component.type < 2
|
||||
? lessonsCommonComponents[component.name] ||
|
||||
lessonsMediaComponents[component.name] ||
|
||||
component.defaultData ||
|
||||
""
|
||||
: component.defaultData || "";
|
||||
|
||||
const res = await reqAddLessonContent({
|
||||
lessonId: currentLnId,
|
||||
type: component.type,
|
||||
data: component.defaultData || "",
|
||||
data: defaultData,
|
||||
}).unwrap();
|
||||
|
||||
dispatch(
|
||||
addLessonContent({
|
||||
Id: res.Id,
|
||||
Type: component.type,
|
||||
Data: component.defaultData || "",
|
||||
Data: defaultData,
|
||||
Page: 1,
|
||||
Position: 1,
|
||||
})
|
||||
);
|
||||
} catch (err) {
|
||||
console.log("error", err);
|
||||
error("Failed to add content");
|
||||
error(t("common.messageRequestFailed"));
|
||||
}
|
||||
}}
|
||||
>
|
||||
|
@ -485,11 +515,13 @@ function CreateComponent({ component }: { component: Component }) {
|
|||
</div>
|
||||
) : null}
|
||||
<Typography.Text style={{ fontSize: 12 }}>
|
||||
{component.name}
|
||||
{
|
||||
{
|
||||
...lessonsCommonComponents,
|
||||
...lessonsMediaComponents,
|
||||
}[component.name]
|
||||
}
|
||||
</Typography.Text>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
//console.log("insert component", component.type);
|
||||
//dispatch(addLessonContent(component.type));
|
||||
|
|
|
@ -4,6 +4,7 @@ export interface Lesson {
|
|||
Title: string;
|
||||
ThumbnailUrl: string;
|
||||
CreatorUserId: string;
|
||||
QuestionsCount?: number;
|
||||
CreatedAt: string;
|
||||
}
|
||||
|
||||
|
@ -17,6 +18,7 @@ export interface LessonSettings {
|
|||
Title: string;
|
||||
ThumbnailUrl: string;
|
||||
State?: LessonState;
|
||||
QuestionsCount?: number;
|
||||
}
|
||||
|
||||
// used on lesson page and on the lesson editor
|
||||
|
|
|
@ -1,62 +1,112 @@
|
|||
import React from "react";
|
||||
import { FloatButton } from "antd";
|
||||
import { CommentOutlined } from "@ant-design/icons";
|
||||
import { DeepChat } from "deep-chat-react";
|
||||
import { getUserSessionFromLocalStorage } from "core/utils/utils";
|
||||
import React from 'react';
|
||||
import { FloatButton } from 'antd';
|
||||
import { CommentOutlined } from '@ant-design/icons';
|
||||
import { DeepChat } from 'deep-chat-react';
|
||||
import { getUserSessionFromLocalStorage } from 'core/utils/utils';
|
||||
|
||||
import { Marked, marked } from 'marked';
|
||||
import { createDirectives, presetDirectiveConfigs } from 'marked-directive';
|
||||
|
||||
function AiChat() {
|
||||
const [visible, setVisible] = React.useState(false);
|
||||
const [visible, setVisible] = React.useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
{visible ? (
|
||||
<div
|
||||
style={{
|
||||
position: "fixed",
|
||||
bottom: 100,
|
||||
right: 10,
|
||||
zIndex: 10000,
|
||||
maxWidth: "95vw",
|
||||
width: 500,
|
||||
height: 1000,
|
||||
maxHeight: "calc(100vh - 165px)",
|
||||
}}
|
||||
>
|
||||
<DeepChat
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
borderRadius: 10,
|
||||
boxShadow: "0 0 10px rgba(0,0,0,0.1)",
|
||||
bottom: 0,
|
||||
position: "absolute",
|
||||
}}
|
||||
history={[{ text: "Stell mir Fragen :)", role: "ai" }]}
|
||||
connect={{
|
||||
url: "/api/chat/v1/prompt/",
|
||||
method: "POST",
|
||||
headers: {
|
||||
"X-Authorization": getUserSessionFromLocalStorage() || "",
|
||||
},
|
||||
}}
|
||||
onMessage={async (message) => {
|
||||
console.log("onMessagee", message);
|
||||
}}
|
||||
></DeepChat>
|
||||
</div>
|
||||
) : null}
|
||||
return (
|
||||
<>
|
||||
{visible ? (
|
||||
<div
|
||||
style={{
|
||||
position: 'fixed',
|
||||
bottom: 100,
|
||||
right: 10,
|
||||
zIndex: 10000,
|
||||
maxWidth: '95vw',
|
||||
width: 500,
|
||||
height: 1000,
|
||||
maxHeight: 'calc(100vh - 165px)',
|
||||
}}
|
||||
>
|
||||
<DeepChat
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
borderRadius: 10,
|
||||
boxShadow: '0 0 10px rgba(0,0,0,0.1)',
|
||||
bottom: 0,
|
||||
position: 'absolute',
|
||||
}}
|
||||
avatars={true}
|
||||
history={[{ text: 'Stell mir Fragen :)', role: 'ai' }]}
|
||||
connect={{
|
||||
url: '/api/chat/v1/prompt/',
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-Authorization': getUserSessionFromLocalStorage() || '',
|
||||
},
|
||||
}}
|
||||
htmlClassUtilities={{
|
||||
['aiButtonSource']: {
|
||||
styles: {
|
||||
default: {
|
||||
backgroundColor: '#1890ff',
|
||||
color: 'white',
|
||||
padding: '5px 10px',
|
||||
borderRadius: '5px',
|
||||
textDecoration: 'none',
|
||||
margin: '4px',
|
||||
display: 'inline-block',
|
||||
transition: 'all 0.3s ease-in-out',
|
||||
},
|
||||
hover: {
|
||||
backgroundColor: '#40a9ff',
|
||||
transform: 'scale(1.05)',
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
responseInterceptor={async (response: any) => {
|
||||
let _response = { ...response };
|
||||
|
||||
<FloatButton
|
||||
icon={<CommentOutlined />}
|
||||
type="primary"
|
||||
onClick={() => console.log("onClick")}
|
||||
style={{ zIndex: 10000 }}
|
||||
onClickCapture={() => {
|
||||
setVisible(!visible);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
let text = response.text;
|
||||
|
||||
delete _response.text;
|
||||
|
||||
// Regular expression to match pattern %URL%x%URL%
|
||||
//const re = /%URL%(.*?)%URL%/g;
|
||||
// Replace pattern with an HTML anchor tag
|
||||
//text = text.replace(re, '@@[Mehr Infos]{href="$1"}');
|
||||
|
||||
_response.html = new Marked()
|
||||
.use(
|
||||
createDirectives([
|
||||
...presetDirectiveConfigs,
|
||||
{
|
||||
level: 'block',
|
||||
marker: 'µ',
|
||||
renderer(token) {
|
||||
return `<a class="aiButtonSource" ${token.attrs?.toString()}>${token.text}</a>`;
|
||||
},
|
||||
},
|
||||
])
|
||||
)
|
||||
.parse(text);
|
||||
|
||||
return _response;
|
||||
}}
|
||||
></DeepChat>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<FloatButton
|
||||
icon={<CommentOutlined />}
|
||||
type="primary"
|
||||
onClick={() => console.log('onClick')}
|
||||
style={{ zIndex: 10000 }}
|
||||
onClickCapture={() => {
|
||||
setVisible(!visible);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default AiChat;
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
.aiButtonSource {
|
||||
background-color: #35f;
|
||||
color: #fff;
|
||||
padding: 10px 20px;
|
||||
margin: 0 0;
|
||||
display: inline-block;
|
||||
border-radius: 8px;
|
||||
text-decoration: none;
|
||||
|
||||
cursor: pointer;
|
||||
|
||||
transition: all 0.3s ease 0s;
|
||||
}
|
||||
|
||||
.aiButtonSource:hover {
|
||||
background-color: #35f;
|
||||
color: #fff;
|
||||
border: 2px solid #35f;
|
||||
}
|
|
@ -1,16 +1,22 @@
|
|||
import { Descriptions, Typography } from "antd";
|
||||
import MyMiddleCard from "shared/components/MyMiddleCard";
|
||||
import MyBanner from "shared/components/MyBanner";
|
||||
import HeaderBar from "core/components/Header";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export default function ContactSupport() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<>
|
||||
<MyBanner title="Contact Support" />
|
||||
<MyBanner
|
||||
title={t("contactSupport.bannerTitle")}
|
||||
headerBar={<HeaderBar />}
|
||||
/>
|
||||
|
||||
<MyMiddleCard title="Support">
|
||||
<MyMiddleCard title={t("contactSupport.middleCardTitle")}>
|
||||
<Typography.Paragraph>
|
||||
If you have any questions or need help, please contact us at the
|
||||
following e-mail address:
|
||||
{t("contactSupport.paragraph")}
|
||||
</Typography.Paragraph>
|
||||
<Descriptions>
|
||||
<Descriptions.Item label="E-Mail">
|
||||
|
|
|
@ -21,6 +21,7 @@ import {
|
|||
setLessonPageCurrentLessonId,
|
||||
} from "./lessonPageSlice";
|
||||
import MyCenteredSpin from "shared/components/MyCenteredSpin";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const LessonContents: React.FC = () => {
|
||||
const dispatch = useDispatch();
|
||||
|
@ -65,6 +66,8 @@ const LessonContents: React.FC = () => {
|
|||
};
|
||||
|
||||
export default function LessonPage() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
|
||||
|
@ -74,6 +77,7 @@ export default function LessonPage() {
|
|||
theme="light"
|
||||
backTo={Constants.ROUTE_PATHS.LESSIONS.ROOT}
|
||||
onEdit={() => navigate(`${location.pathname}/editor`)}
|
||||
sticky
|
||||
/>
|
||||
|
||||
<MyMiddleCard
|
||||
|
@ -86,8 +90,12 @@ export default function LessonPage() {
|
|||
<LessonContents />
|
||||
|
||||
<Flex justify="right">
|
||||
<Button type="primary" icon={<CheckOutlined />}>
|
||||
Finish lesson
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<CheckOutlined />}
|
||||
onClick={() => navigate(Constants.ROUTE_PATHS.LESSIONS.ROOT)}
|
||||
>
|
||||
{t("lessonPage.finishLessonButton")}
|
||||
</Button>
|
||||
</Flex>
|
||||
</MyMiddleCard>
|
||||
|
|
|
@ -1,76 +1,90 @@
|
|||
import { closestCenter, closestCorners, DndContext, DragEndEvent, DragOverlay, MeasuringStrategy, rectIntersection, useDroppable } from '@dnd-kit/core';
|
||||
import { verticalListSortingStrategy, SortableContext, rectSwappingStrategy, rectSortingStrategy } from '@dnd-kit/sortable';
|
||||
import SortableEditorItem from './SortableEditorItem';
|
||||
import { store } from 'core/store/store';
|
||||
|
||||
import { restrictToVerticalAxis, restrictToWindowEdges, snapCenterToCursor } from '@dnd-kit/modifiers';
|
||||
import { currentLessonId, onDragHandler } from './lessonPageEditorSlice';
|
||||
import { LessonContent } from 'core/types/lesson';
|
||||
import { useUpdateLessonContentPositionMutation } from 'core/services/lessons';
|
||||
import { useSelector } from 'react-redux';
|
||||
import React from 'react';
|
||||
import { Typography } from 'antd';
|
||||
import { HolderOutlined } from '@ant-design/icons';
|
||||
import {
|
||||
closestCorners,
|
||||
DndContext,
|
||||
DragEndEvent,
|
||||
DragOverlay,
|
||||
useDroppable,
|
||||
} from "@dnd-kit/core";
|
||||
import {
|
||||
verticalListSortingStrategy,
|
||||
SortableContext,
|
||||
} from "@dnd-kit/sortable";
|
||||
import SortableEditorItem from "./SortableEditorItem";
|
||||
import { store } from "core/store/store";
|
||||
import { snapCenterToCursor } from "@dnd-kit/modifiers";
|
||||
import { currentLessonId, onDragHandler } from "./lessonPageEditorSlice";
|
||||
import { LessonContent } from "core/types/lesson";
|
||||
import { useUpdateLessonContentPositionMutation } from "core/services/lessons";
|
||||
import { useSelector } from "react-redux";
|
||||
import React from "react";
|
||||
import { HolderOutlined } from "@ant-design/icons";
|
||||
|
||||
const Droppable = ({ items }: { items: LessonContent[] }) => {
|
||||
const droppableID = 'editorComponentArea';
|
||||
const { setNodeRef } = useDroppable({ id: droppableID });
|
||||
const currentLnId = useSelector(currentLessonId);
|
||||
const droppableID = "editorComponentArea";
|
||||
const { setNodeRef } = useDroppable({ id: droppableID });
|
||||
const currentLnId = useSelector(currentLessonId);
|
||||
|
||||
const [reqUpdateLessonContentPosition] = useUpdateLessonContentPositionMutation();
|
||||
const [reqUpdateLessonContentPosition] =
|
||||
useUpdateLessonContentPositionMutation();
|
||||
|
||||
const [isDragging, setIsDragging] = React.useState(false);
|
||||
const [isDragging, setIsDragging] = React.useState(false);
|
||||
|
||||
const itemIDs = items.map((item) => item.Id);
|
||||
const itemIDs = items.map((item) => item.Id);
|
||||
|
||||
const handleDragEnd = (event: DragEndEvent) => {
|
||||
console.log('drag end', event);
|
||||
setIsDragging(false);
|
||||
const handleDragEnd = (event: DragEndEvent) => {
|
||||
console.log("drag end", event);
|
||||
setIsDragging(false);
|
||||
|
||||
if (!event.over) return;
|
||||
if (!event.over) return;
|
||||
|
||||
const activeId = event.active.id;
|
||||
const overId = event.over.id;
|
||||
const activeId = event.active.id;
|
||||
const overId = event.over.id;
|
||||
|
||||
if (activeId === overId) return;
|
||||
if (activeId === overId) return;
|
||||
|
||||
let oldIndex = itemIDs.findIndex((item) => item === activeId);
|
||||
let newIndex = itemIDs.findIndex((item) => item === overId);
|
||||
let oldIndex = itemIDs.findIndex((item) => item === activeId);
|
||||
let newIndex = itemIDs.findIndex((item) => item === overId);
|
||||
|
||||
// store.dispatch(onDragHandler({ activeId, overId }));
|
||||
// store.dispatch(onDragHandler({ activeId, overId }));
|
||||
|
||||
store.dispatch(onDragHandler({ oldIndex, newIndex }));
|
||||
store.dispatch(onDragHandler({ oldIndex, newIndex }));
|
||||
|
||||
try {
|
||||
reqUpdateLessonContentPosition({
|
||||
lessonId: currentLnId,
|
||||
contentId: activeId,
|
||||
newPosition: newIndex + 1,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
};
|
||||
try {
|
||||
reqUpdateLessonContentPosition({
|
||||
lessonId: currentLnId,
|
||||
contentId: activeId,
|
||||
newPosition: newIndex + 1,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<DndContext
|
||||
modifiers={[snapCenterToCursor]}
|
||||
collisionDetection={closestCorners}
|
||||
onDragStart={() => {
|
||||
setIsDragging(true);
|
||||
}}
|
||||
onDragEnd={handleDragEnd}
|
||||
>
|
||||
<SortableContext id={droppableID} items={itemIDs} strategy={verticalListSortingStrategy}>
|
||||
<div ref={setNodeRef}>
|
||||
{items.map((item) => (
|
||||
<SortableEditorItem key={`${droppableID}_${item.Id}`} item={item} />
|
||||
))}
|
||||
</div>
|
||||
</SortableContext>
|
||||
<DragOverlay>{isDragging ? <HolderOutlined style={{ cursor: 'grabbing' }} /> : null}</DragOverlay>
|
||||
</DndContext>
|
||||
);
|
||||
return (
|
||||
<DndContext
|
||||
modifiers={[snapCenterToCursor]}
|
||||
collisionDetection={closestCorners}
|
||||
onDragStart={() => {
|
||||
setIsDragging(true);
|
||||
}}
|
||||
onDragEnd={handleDragEnd}
|
||||
>
|
||||
<SortableContext
|
||||
id={droppableID}
|
||||
items={itemIDs}
|
||||
strategy={verticalListSortingStrategy}
|
||||
>
|
||||
<div ref={setNodeRef}>
|
||||
{items.map((item) => (
|
||||
<SortableEditorItem key={`${droppableID}_${item.Id}`} item={item} />
|
||||
))}
|
||||
</div>
|
||||
</SortableContext>
|
||||
<DragOverlay>
|
||||
{isDragging ? <HolderOutlined style={{ cursor: "grabbing" }} /> : null}
|
||||
</DragOverlay>
|
||||
</DndContext>
|
||||
);
|
||||
};
|
||||
/*
|
||||
function handleDragEnd(event: DragEndEvent) {
|
||||
|
|
|
@ -19,9 +19,10 @@ import "./styles.module.css";
|
|||
import { Converter } from "../converter";
|
||||
import { useDeleteLessonContentMutation } from "core/services/lessons";
|
||||
|
||||
/*
|
||||
const animateLayoutChanges = (args: any) =>
|
||||
args.isSorting || args.wasDragging ? defaultAnimateLayoutChanges(args) : true;
|
||||
|
||||
*/
|
||||
const SortableEditorItem = (props: { item: LessonContent }) => {
|
||||
const lnContent = props.item;
|
||||
const {
|
||||
|
|
|
@ -52,6 +52,12 @@ const PreviewCard: React.FC = () => {
|
|||
dispatch(setLessonThumbnailUrl(data.ThumbnailUrl));
|
||||
}, [data]);
|
||||
|
||||
useEffect(() => {
|
||||
addWebSocketReconnectListener(refetch);
|
||||
|
||||
return () => removeWebSocketReconnectListener(refetch);
|
||||
}, []);
|
||||
|
||||
if (error) return <MyErrorResult />;
|
||||
|
||||
return (
|
||||
|
@ -150,6 +156,7 @@ export default function LessonPageEditor() {
|
|||
)
|
||||
)
|
||||
}
|
||||
sticky
|
||||
/>
|
||||
|
||||
<Flex justify="center" style={{ paddingTop: 24 }}>
|
||||
|
|
|
@ -27,8 +27,11 @@ import {
|
|||
} from "core/services/websocketService";
|
||||
import { useCachedUser } from "core/services/cachedUser";
|
||||
import MyUserAvatar from "shared/components/MyUserAvatar";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const CreateQuestionForm: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { lessonId } = useParams();
|
||||
const [form] = useForm();
|
||||
|
||||
|
@ -42,11 +45,11 @@ const CreateQuestionForm: React.FC = () => {
|
|||
message: form.getFieldValue("message"),
|
||||
}).unwrap();
|
||||
|
||||
success("Question created successfully");
|
||||
success(t("lessonQuestions.messageQuestionSuccessfullyCreated"));
|
||||
form.resetFields();
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
error("Failed to create question");
|
||||
error(t("common.messageRequestFailed"));
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -58,12 +61,14 @@ const CreateQuestionForm: React.FC = () => {
|
|||
requiredMark={false}
|
||||
>
|
||||
<Form.Item
|
||||
label="Ask a question"
|
||||
label={t("lessonQuestions.askAQuestion")}
|
||||
name="message"
|
||||
rules={[{ required: true, message: "Please write a question" }]}
|
||||
rules={[
|
||||
{ required: true, message: t("lessonQuestions.ruleMessageRequired") },
|
||||
]}
|
||||
>
|
||||
<Input.TextArea
|
||||
placeholder={"Type something"}
|
||||
placeholder={t("lessonQuestions.typeSomething")}
|
||||
autoSize={{ minRows: 2 }}
|
||||
maxLength={5000}
|
||||
/>
|
||||
|
@ -71,7 +76,7 @@ const CreateQuestionForm: React.FC = () => {
|
|||
|
||||
<Form.Item>
|
||||
<Button type="primary" htmlType="submit" loading={isLoading}>
|
||||
Submit
|
||||
{t("lessonQuestions.submitButton")}
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
|
@ -79,6 +84,8 @@ const CreateQuestionForm: React.FC = () => {
|
|||
};
|
||||
|
||||
export default function Questions() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const { lessonId } = useParams();
|
||||
|
||||
|
@ -108,7 +115,9 @@ export default function Questions() {
|
|||
return (
|
||||
<Flex justify="center">
|
||||
<Flex style={{ width: 800, maxWidth: 800 * 0.9 }} vertical>
|
||||
<Typography.Title level={3}>Questions</Typography.Title>
|
||||
<Typography.Title level={3}>
|
||||
{t("lessonQuestions.questions")}
|
||||
</Typography.Title>
|
||||
|
||||
<CreateQuestionForm />
|
||||
|
||||
|
@ -152,6 +161,7 @@ export function QuestionItem({
|
|||
replies: LessonQuestion[];
|
||||
likedQuestions: string[];
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const [showReplies, setShowReplies] = useState(1);
|
||||
const { success, error } = useMessage();
|
||||
|
||||
|
@ -167,12 +177,12 @@ export function QuestionItem({
|
|||
message: message,
|
||||
}).unwrap();
|
||||
|
||||
success("Question created successfully");
|
||||
success(t("lessonQuestions.messageReplySuccessfullyCreated"));
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
error("Failed to create question");
|
||||
error(t("common.messageRequestFailed"));
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
}
|
||||
}
|
||||
|
@ -185,7 +195,7 @@ export function QuestionItem({
|
|||
}).unwrap();
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
error("Failed to like");
|
||||
error(t("lessonQuestions.messageRequestFailed"));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -197,7 +207,7 @@ export function QuestionItem({
|
|||
}).unwrap();
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
error("Failed to dislike");
|
||||
error(t("lessonQuestions.messageRequestFailed"));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -258,6 +268,8 @@ export function QuestionUIRaw({
|
|||
}) {
|
||||
//const [hasLiked, setHasLiked] = useState(false);
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [replyFormVisible, setReplyFormVisible] = useState(false);
|
||||
const [replyMessage, setReplyMessage] = useState<null | string>(null);
|
||||
const [isSendingReply, setIsSendingReply] = useState(false);
|
||||
|
@ -324,7 +336,9 @@ export function QuestionUIRaw({
|
|||
}, 100);
|
||||
}}
|
||||
>
|
||||
{replyFormVisible ? "Hide" : "Reply"}
|
||||
{replyFormVisible
|
||||
? t("lessonQuestions.hide")
|
||||
: t("lessonQuestions.reply")}
|
||||
</Button>
|
||||
</Flex>
|
||||
{replyFormVisible ? (
|
||||
|
@ -344,12 +358,17 @@ export function QuestionUIRaw({
|
|||
>
|
||||
<Form.Item
|
||||
name="reply"
|
||||
rules={[{ required: true, message: "Please write a reply" }]}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t("lessonQuestions.ruleReplyRequired"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input.TextArea
|
||||
ref={inputRef}
|
||||
value={replyMessage ? replyMessage : userAt}
|
||||
placeholder="Write a reply"
|
||||
placeholder={t("lessonQuestions.ruleReplyRequired")}
|
||||
onChange={(e) => setReplyMessage(e.target.value)}
|
||||
autoSize={{ minRows: 2 }}
|
||||
maxLength={5000}
|
||||
|
@ -361,7 +380,7 @@ export function QuestionUIRaw({
|
|||
loading={isSendingReply}
|
||||
htmlType="submit"
|
||||
>
|
||||
Reply
|
||||
{t("lessonQuestions.reply")}
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
|
|
|
@ -1,380 +1,410 @@
|
|||
import { LessonContent } from 'core/types/lesson';
|
||||
import { getTypeByName } from './components';
|
||||
import { Button, Input, Typography, Flex } from 'antd';
|
||||
import { useUpdateLessonContentMutation } from 'core/services/lessons';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { currentLessonId } from './LessonPageEditor/lessonPageEditorSlice';
|
||||
import { useRef } from 'react';
|
||||
import MyUpload from 'shared/components/MyUpload';
|
||||
import { Constants } from 'core/utils/utils';
|
||||
import { MediaPlayer, MediaProvider } from '@vidstack/react';
|
||||
import { defaultLayoutIcons, DefaultVideoLayout } from '@vidstack/react/player/layouts/default';
|
||||
import '@vidstack/react/player/styles/default/theme.css';
|
||||
import '@vidstack/react/player/styles/default/layouts/video.css';
|
||||
import { darkMode } from 'core/reducers/appSlice';
|
||||
import { LessonContent } from "core/types/lesson";
|
||||
import { getTypeByName } from "./components";
|
||||
import { Button, Input, Typography, Flex } from "antd";
|
||||
import { useUpdateLessonContentMutation } from "core/services/lessons";
|
||||
import { useSelector } from "react-redux";
|
||||
import { currentLessonId } from "./LessonPageEditor/lessonPageEditorSlice";
|
||||
import { useRef } from "react";
|
||||
import MyUpload from "shared/components/MyUpload";
|
||||
import { Constants } from "core/utils/utils";
|
||||
import { MediaPlayer, MediaProvider } from "@vidstack/react";
|
||||
import {
|
||||
defaultLayoutIcons,
|
||||
DefaultVideoLayout,
|
||||
} from "@vidstack/react/player/layouts/default";
|
||||
import "@vidstack/react/player/styles/default/theme.css";
|
||||
import "@vidstack/react/player/styles/default/layouts/video.css";
|
||||
import { darkMode } from "core/reducers/appSlice";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const extractVideoId = (url: string) => {
|
||||
// regex to extract video id from youtube url
|
||||
const regex = /(?:https?:\/\/)?(?:www\.)?youtube\.com\/(?:watch\?v=|embed\/|v\/|.+\?v=|.+\/|.+v=)?([^"&?\/\s]{11})/;
|
||||
const match = url.match(regex);
|
||||
return match ? match[1] : url;
|
||||
// regex to extract video id from youtube url
|
||||
const regex =
|
||||
/(?:https?:\/\/)?(?:www\.)?youtube\.com\/(?:watch\?v=|embed\/|v\/|.+\?v=|.+\/|.+v=)?([^"&?\/\s]{11})/;
|
||||
const match = url.match(regex);
|
||||
return match ? match[1] : url;
|
||||
};
|
||||
|
||||
export function Converter({ mode, lessonContent, onEdit }: { mode: 'view' | 'edititable'; lessonContent: LessonContent; onEdit?: (newData: string) => void }) {
|
||||
const lessonId = useSelector(currentLessonId);
|
||||
export function Converter({
|
||||
mode,
|
||||
lessonContent,
|
||||
onEdit,
|
||||
}: {
|
||||
mode: "view" | "edititable";
|
||||
lessonContent: LessonContent;
|
||||
onEdit?: (newData: string) => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const debounceRef = useRef<null | NodeJS.Timeout>(null);
|
||||
|
||||
const isDarkMode = useSelector(darkMode);
|
||||
const lessonId = useSelector(currentLessonId);
|
||||
const isDarkMode = useSelector(darkMode);
|
||||
|
||||
const [reqUpdateLessonContent] = useUpdateLessonContentMutation();
|
||||
const [reqUpdateLessonContent] = useUpdateLessonContentMutation();
|
||||
|
||||
const debounceRef = useRef<null | NodeJS.Timeout>(null);
|
||||
switch (lessonContent.Type) {
|
||||
case getTypeByName("Header"):
|
||||
if (mode === "view") {
|
||||
return (
|
||||
<div
|
||||
style={{ fontWeight: "bold", fontSize: 24, wordBreak: "break-all" }}
|
||||
>
|
||||
{lessonContent.Data}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
switch (lessonContent.Type) {
|
||||
case getTypeByName('Header'):
|
||||
if (mode === 'view') {
|
||||
return <div style={{ fontWeight: 'bold', fontSize: 24, wordBreak: 'break-all' }}>{lessonContent.Data}</div>;
|
||||
return (
|
||||
<Typography.Title
|
||||
editable={{
|
||||
triggerType: "text" as any,
|
||||
onChange: (event) => {
|
||||
onEdit?.(event);
|
||||
|
||||
try {
|
||||
reqUpdateLessonContent({
|
||||
lessonId: lessonId,
|
||||
contentId: lessonContent.Id,
|
||||
data: event,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
},
|
||||
}}
|
||||
level={1}
|
||||
style={{
|
||||
margin: 0,
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
{lessonContent.Data}
|
||||
</Typography.Title>
|
||||
);
|
||||
case getTypeByName("Text"):
|
||||
if (mode === "view") {
|
||||
const formattedText = lessonContent.Data.split("\n").map(
|
||||
(line, index) => <li key={index}>{line || "\u00A0"}</li>
|
||||
);
|
||||
|
||||
return (
|
||||
<ul
|
||||
style={{
|
||||
fontSize: 16,
|
||||
wordBreak: "break-all",
|
||||
padding: 0,
|
||||
margin: 0,
|
||||
listStyleType: "none",
|
||||
}}
|
||||
>
|
||||
{formattedText}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Input.TextArea
|
||||
autoSize
|
||||
variant="borderless"
|
||||
placeholder={t("lessonComponentsConverter.textPlaceholder")}
|
||||
style={{ width: "100%", padding: 0, paddingTop: 4 }}
|
||||
value={lessonContent.Data}
|
||||
onChange={(event) => {
|
||||
onEdit?.(event.target.value);
|
||||
|
||||
if (debounceRef.current) {
|
||||
clearTimeout(debounceRef.current);
|
||||
}
|
||||
|
||||
return (
|
||||
<Typography.Title
|
||||
editable={{
|
||||
triggerType: 'text' as any,
|
||||
onChange: (event) => {
|
||||
onEdit?.(event);
|
||||
debounceRef.current = setTimeout(() => {
|
||||
try {
|
||||
reqUpdateLessonContent({
|
||||
lessonId: lessonId,
|
||||
contentId: lessonContent.Id,
|
||||
data: event.target.value,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
}, 1000);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
case getTypeByName("Image"):
|
||||
if (mode === "view" && lessonContent.Data === "") {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: "relative",
|
||||
height: 120,
|
||||
width: "100%",
|
||||
backgroundColor: isDarkMode ? "#222" : "#EBEBEB",
|
||||
marginTop: 4,
|
||||
borderRadius: 4,
|
||||
marginRight: 8,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
transform: "translate(-50%, -50%)",
|
||||
}}
|
||||
>
|
||||
{t("lessonComponentsConverter.noImageProvided")}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
reqUpdateLessonContent({
|
||||
lessonId: lessonId,
|
||||
contentId: lessonContent.Id,
|
||||
data: event,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
},
|
||||
}}
|
||||
level={1}
|
||||
style={{
|
||||
margin: 0,
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
{lessonContent.Data}
|
||||
</Typography.Title>
|
||||
);
|
||||
case getTypeByName('Text'):
|
||||
if (mode === 'view') {
|
||||
const formattedText = lessonContent.Data.split('\n').map((line, index) => <li key={index}>{line || '\u00A0'}</li>);
|
||||
const GalleryUpload = () => {
|
||||
return (
|
||||
<MyUpload
|
||||
action={`/lessons/${lessonId}/contents/${lessonContent.Id}/file/image`}
|
||||
onChange={(info) => {
|
||||
if (info.file.status === "done") {
|
||||
onEdit?.(info.file.response.Data);
|
||||
}
|
||||
}}
|
||||
imgCropProps={{
|
||||
aspect: 5 / 4,
|
||||
children: <></>,
|
||||
}}
|
||||
>
|
||||
<Button type="link">
|
||||
{t("lessonComponentsConverter.gallery")}
|
||||
</Button>
|
||||
</MyUpload>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<ul
|
||||
style={{
|
||||
fontSize: 16,
|
||||
wordBreak: 'break-all',
|
||||
padding: 0,
|
||||
margin: 0,
|
||||
listStyleType: 'none',
|
||||
}}
|
||||
>
|
||||
{formattedText}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
if (lessonContent.Data === "") {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: "relative",
|
||||
height: 120,
|
||||
width: "100%",
|
||||
backgroundColor: isDarkMode ? "#222" : "#EBEBEB",
|
||||
borderRadius: 4,
|
||||
margin: "12px 12px 12px 0",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
position: "absolute",
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
transform: "translate(-50%, -50%)",
|
||||
}}
|
||||
>
|
||||
<span>{t("lessonComponentsConverter.chooseImageFrom")}</span>
|
||||
|
||||
return (
|
||||
<Input.TextArea
|
||||
autoSize
|
||||
variant="borderless"
|
||||
placeholder="Input text here"
|
||||
style={{ width: '100%', padding: 0, paddingTop: 4 }}
|
||||
value={lessonContent.Data}
|
||||
onChange={(event) => {
|
||||
console.log('edit');
|
||||
<GalleryUpload />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
onEdit?.(event.target.value);
|
||||
return (
|
||||
<Flex vertical style={{ width: "100%" }}>
|
||||
<img
|
||||
src={`${Constants.STATIC_CONTENT_ADDRESS}/${lessonContent.Data}`}
|
||||
alt="img"
|
||||
style={{ width: "100%" }}
|
||||
/>
|
||||
|
||||
if (debounceRef.current) {
|
||||
clearTimeout(debounceRef.current);
|
||||
}
|
||||
{mode === "edititable" && (
|
||||
<Flex
|
||||
style={{
|
||||
width: "100%",
|
||||
backgroundColor: isDarkMode ? "#222" : "#EBEBEB",
|
||||
marginTop: 4,
|
||||
borderRadius: 4,
|
||||
}}
|
||||
justify="center"
|
||||
>
|
||||
<div>
|
||||
<span>
|
||||
{t("lessonComponentsConverter.chooseAnotherImageFrom")}
|
||||
</span>
|
||||
<GalleryUpload />
|
||||
</div>
|
||||
</Flex>
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
case getTypeByName("YouTube"):
|
||||
const videoId = extractVideoId(lessonContent.Data);
|
||||
|
||||
debounceRef.current = setTimeout(() => {
|
||||
try {
|
||||
reqUpdateLessonContent({
|
||||
lessonId: lessonId,
|
||||
contentId: lessonContent.Id,
|
||||
data: event.target.value,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
}, 1000);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
case getTypeByName('Image'):
|
||||
console.log('image', lessonContent.Data);
|
||||
return (
|
||||
<Flex vertical style={{ width: "100%", paddingBottom: 12 }}>
|
||||
<iframe
|
||||
width="100%"
|
||||
height={mode === "view" ? 422 : 390}
|
||||
style={{ border: 0 }}
|
||||
src={`https://www.youtube.com/embed/${videoId}`}
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
||||
></iframe>
|
||||
|
||||
if (mode === 'view' && lessonContent.Data === '') {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: 'relative',
|
||||
height: 120,
|
||||
width: '100%',
|
||||
backgroundColor: isDarkMode ? '#222' : '#EBEBEB',
|
||||
marginTop: 4,
|
||||
borderRadius: 4,
|
||||
marginRight: 8,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
}}
|
||||
>
|
||||
No image provided
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
{mode === "edititable" && (
|
||||
<>
|
||||
<Typography.Text>
|
||||
{t("lessonComponentsConverter.videoId")}
|
||||
</Typography.Text>
|
||||
<Input
|
||||
placeholder="https://www.youtube.com/watch?v=Robzc9p1l50 or Robzc9p1l50"
|
||||
value={lessonContent.Data}
|
||||
onChange={(event) => {
|
||||
onEdit?.(event.target.value);
|
||||
|
||||
const GalleryUpload = () => {
|
||||
return (
|
||||
<MyUpload
|
||||
action={`/lessons/${lessonId}/contents/${lessonContent.Id}/file/image`}
|
||||
onChange={(info) => {
|
||||
if (info.file.status === 'done') {
|
||||
console.log('done');
|
||||
onEdit?.(info.file.response.Data);
|
||||
}
|
||||
}}
|
||||
imgCropProps={{
|
||||
aspect: 5 / 4,
|
||||
children: <></>,
|
||||
}}
|
||||
>
|
||||
<Button type="link">Gallery</Button>
|
||||
</MyUpload>
|
||||
);
|
||||
};
|
||||
if (debounceRef.current) clearTimeout(debounceRef.current);
|
||||
|
||||
if (lessonContent.Data === '') {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: 'relative',
|
||||
height: 120,
|
||||
width: '100%',
|
||||
backgroundColor: isDarkMode ? '#222' : '#EBEBEB',
|
||||
borderRadius: 4,
|
||||
margin: '12px 12px 12px 0',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
position: 'absolute',
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
}}
|
||||
>
|
||||
<span>Choose image from</span>
|
||||
if (event.target.value === "") return;
|
||||
|
||||
<GalleryUpload />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
debounceRef.current = setTimeout(() => {
|
||||
try {
|
||||
reqUpdateLessonContent({
|
||||
lessonId: lessonId,
|
||||
contentId: lessonContent.Id,
|
||||
data: extractVideoId(event.target.value),
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
}, 1000);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
case getTypeByName("Video"):
|
||||
if (mode === "view" && lessonContent.Data === "") {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: "relative",
|
||||
height: 120,
|
||||
width: "100%",
|
||||
backgroundColor: isDarkMode ? "#222" : "#EBEBEB",
|
||||
marginTop: 4,
|
||||
borderRadius: 4,
|
||||
marginRight: 8,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
transform: "translate(-50%, -50%)",
|
||||
}}
|
||||
>
|
||||
{t("lessonComponentsConverter.noVideoProvided")}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Flex vertical style={{ width: '100%' }}>
|
||||
<img src={`${Constants.STATIC_CONTENT_ADDRESS}/${lessonContent.Data}`} alt="img" style={{ width: '100%' }} />
|
||||
const VideoUpload = () => {
|
||||
return (
|
||||
<MyUpload
|
||||
action={`/lessons/${lessonId}/contents/${lessonContent.Id}/file/video`}
|
||||
onChange={(info) => {
|
||||
if (info.file.status === "done") {
|
||||
onEdit?.(info.file.response.Data);
|
||||
}
|
||||
}}
|
||||
accept={Constants.ACCEPTED_VIDEO_FILE_TYPES}
|
||||
>
|
||||
<Button type="link">{t("lessonComponentsConverter.video")}</Button>
|
||||
</MyUpload>
|
||||
);
|
||||
};
|
||||
|
||||
{mode === 'edititable' && (
|
||||
<Flex
|
||||
style={{
|
||||
width: '100%',
|
||||
backgroundColor: isDarkMode ? '#222' : '#EBEBEB',
|
||||
marginTop: 4,
|
||||
borderRadius: 4,
|
||||
}}
|
||||
justify="center"
|
||||
>
|
||||
<div>
|
||||
<span>Choose another image from</span>
|
||||
<GalleryUpload />
|
||||
</div>
|
||||
</Flex>
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
case getTypeByName('YouTube'):
|
||||
const videoId = extractVideoId(lessonContent.Data);
|
||||
if (lessonContent.Data === "") {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: "relative",
|
||||
height: 120,
|
||||
width: "100%",
|
||||
backgroundColor: isDarkMode ? "#222" : "#EBEBEB",
|
||||
borderRadius: 4,
|
||||
margin: "12px 12px 12px 0",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
position: "absolute",
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
transform: "translate(-50%, -50%)",
|
||||
}}
|
||||
>
|
||||
<span>{t("lessonComponentsConverter.chooseVideoFrom")}</span>
|
||||
|
||||
console.log('videoId', videoId);
|
||||
<VideoUpload />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Flex vertical style={{ width: '100%', paddingBottom: 12 }}>
|
||||
<iframe
|
||||
width="100%"
|
||||
height={mode === 'view' ? 422 : 390}
|
||||
style={{ border: 0 }}
|
||||
src={`https://www.youtube.com/embed/${videoId}`}
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
||||
></iframe>
|
||||
return (
|
||||
<Flex vertical style={{ width: "100%" }}>
|
||||
<MediaPlayer
|
||||
load="idle"
|
||||
src={`${Constants.STATIC_CONTENT_ADDRESS}/${lessonContent.Data}`}
|
||||
>
|
||||
<MediaProvider />
|
||||
<DefaultVideoLayout icons={defaultLayoutIcons} />
|
||||
</MediaPlayer>
|
||||
|
||||
{mode === 'edititable' && (
|
||||
<>
|
||||
<Typography.Text>Video ID</Typography.Text>
|
||||
<Input
|
||||
placeholder="https://www.youtube.com/watch?v=Robzc9p1l50 or Robzc9p1l50"
|
||||
value={lessonContent.Data}
|
||||
onChange={(event) => {
|
||||
console.warn('edit', event.target.value, videoId);
|
||||
{mode === "edititable" && (
|
||||
<Flex
|
||||
style={{
|
||||
width: "100%",
|
||||
backgroundColor: isDarkMode ? "#222" : "#EBEBEB",
|
||||
margin: "4px 0",
|
||||
borderRadius: 4,
|
||||
height: 48,
|
||||
}}
|
||||
justify="center"
|
||||
align="center"
|
||||
>
|
||||
<div>
|
||||
<span>
|
||||
{t("lessonComponentsConverter.chooseAnotherVideoFrom")}
|
||||
</span>
|
||||
<VideoUpload />
|
||||
</div>
|
||||
</Flex>
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
|
||||
onEdit?.(event.target.value);
|
||||
case getTypeByName("Iframe"):
|
||||
return (
|
||||
<div style={{ fontSize: 14, width: "100%" }}>
|
||||
{t("lessonComponentsConverter.notImplemented")}
|
||||
</div>
|
||||
);
|
||||
case getTypeByName("Banner"):
|
||||
return (
|
||||
<div style={{ fontSize: 14, width: "100%" }}>
|
||||
{t("lessonComponentsConverter.notImplemented")}
|
||||
</div>
|
||||
);
|
||||
|
||||
if (debounceRef.current) {
|
||||
clearTimeout(debounceRef.current);
|
||||
}
|
||||
|
||||
if (event.target.value === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
debounceRef.current = setTimeout(() => {
|
||||
try {
|
||||
reqUpdateLessonContent({
|
||||
lessonId: lessonId,
|
||||
contentId: lessonContent.Id,
|
||||
data: extractVideoId(event.target.value),
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
}, 1000);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
case getTypeByName('Video'):
|
||||
if (mode === 'view' && lessonContent.Data === '') {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: 'relative',
|
||||
height: 120,
|
||||
width: '100%',
|
||||
backgroundColor: isDarkMode ? '#222' : '#EBEBEB',
|
||||
marginTop: 4,
|
||||
borderRadius: 4,
|
||||
marginRight: 8,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
}}
|
||||
>
|
||||
No video provided
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const VideoUpload = () => {
|
||||
return (
|
||||
<MyUpload
|
||||
action={`/lessons/${lessonId}/contents/${lessonContent.Id}/file/video`}
|
||||
onChange={(info) => {
|
||||
if (info.file.status === 'done') {
|
||||
console.log('done');
|
||||
onEdit?.(info.file.response.Data);
|
||||
}
|
||||
}}
|
||||
accept={Constants.ACCEPTED_VIDEO_FILE_TYPES}
|
||||
>
|
||||
<Button type="link">Video</Button>
|
||||
</MyUpload>
|
||||
);
|
||||
};
|
||||
|
||||
if (lessonContent.Data === '') {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: 'relative',
|
||||
height: 120,
|
||||
width: '100%',
|
||||
backgroundColor: isDarkMode ? '#222' : '#EBEBEB',
|
||||
borderRadius: 4,
|
||||
margin: '12px 12px 12px 0',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
position: 'absolute',
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
}}
|
||||
>
|
||||
<span>Choose video from</span>
|
||||
|
||||
<VideoUpload />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Flex vertical style={{ width: '100%' }}>
|
||||
<MediaPlayer load="idle" src={`${Constants.STATIC_CONTENT_ADDRESS}/${lessonContent.Data}`}>
|
||||
<MediaProvider />
|
||||
<DefaultVideoLayout icons={defaultLayoutIcons} />
|
||||
</MediaPlayer>
|
||||
|
||||
{mode === 'edititable' && (
|
||||
<Flex
|
||||
style={{
|
||||
width: '100%',
|
||||
backgroundColor: isDarkMode ? '#222' : '#EBEBEB',
|
||||
margin: '4px 0',
|
||||
borderRadius: 4,
|
||||
height: 48,
|
||||
}}
|
||||
justify="center"
|
||||
align="center"
|
||||
>
|
||||
<div>
|
||||
<span>Choose another video from</span>
|
||||
<VideoUpload />
|
||||
</div>
|
||||
</Flex>
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
|
||||
case getTypeByName('Iframe'):
|
||||
return <div style={{ fontSize: 14, width: '100%' }}>Not implemented</div>;
|
||||
case getTypeByName('Banner'):
|
||||
return <div style={{ fontSize: 14, width: '100%' }}>Not implemented</div>;
|
||||
|
||||
default:
|
||||
return <div>Unknown type</div>;
|
||||
}
|
||||
default:
|
||||
return <div>{t("lessonComponentsConverter.unkownType")}</div>;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,15 +16,23 @@ import LessonPreviewCard from "shared/components/MyLessonPreviewCard";
|
|||
import { useMessage } from "core/context/MessageContext";
|
||||
import { useEffect } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { lessons, setLessons } from "./lessonsSlice";
|
||||
import {
|
||||
lessons,
|
||||
searchFilter,
|
||||
setLessons,
|
||||
setSearchFilter,
|
||||
} from "./lessonsSlice";
|
||||
import {
|
||||
addWebSocketReconnectListener,
|
||||
removeWebSocketReconnectListener,
|
||||
} from "core/services/websocketService";
|
||||
import MyCenteredSpin from "shared/components/MyCenteredSpin";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const CreateLessonButton: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [createLesson, { isLoading }] = useCreateLessonMutation();
|
||||
const { success, error } = useMessage();
|
||||
|
||||
|
@ -33,7 +41,7 @@ const CreateLessonButton: React.FC = () => {
|
|||
const res = await createLesson({}).unwrap();
|
||||
|
||||
if (res && res.Id) {
|
||||
success("Lesson created successfully ");
|
||||
success(t("lessons.messageLessonSuccessfullyCreated"));
|
||||
|
||||
navigate(
|
||||
Constants.ROUTE_PATHS.LESSIONS.PAGE_EDITOR.replace(
|
||||
|
@ -44,7 +52,7 @@ const CreateLessonButton: React.FC = () => {
|
|||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
error("Failed to create lesson");
|
||||
error(t("common.messageRequestFailed"));
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -54,14 +62,16 @@ const CreateLessonButton: React.FC = () => {
|
|||
onClick={handleCreateLesson}
|
||||
loading={isLoading}
|
||||
>
|
||||
Create
|
||||
{t("common.create")}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
const LessonList: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useDispatch();
|
||||
const dataLessons = useSelector(lessons);
|
||||
const dataSearchFilter = useSelector(searchFilter);
|
||||
|
||||
const { data, error, isLoading, refetch } = useGetLessonsQuery(undefined, {
|
||||
refetchOnMountOrArgChange: true,
|
||||
|
@ -84,8 +94,12 @@ const LessonList: React.FC = () => {
|
|||
|
||||
if (!dataLessons || dataLessons.length === 0) return <MyEmpty />;
|
||||
|
||||
const publishedItems = dataLessons.filter((item) => item.State === 1);
|
||||
const unpublishedItems = dataLessons.filter((item) => item.State === 2);
|
||||
const filteredItems = dataLessons.filter((item) =>
|
||||
item.Title.toLowerCase().includes(dataSearchFilter.toLowerCase())
|
||||
);
|
||||
|
||||
const publishedItems = filteredItems.filter((item) => item.State === 1);
|
||||
const unpublishedItems = filteredItems.filter((item) => item.State === 2);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -100,6 +114,7 @@ const LessonList: React.FC = () => {
|
|||
lessonSettings={{
|
||||
Title: item.Title,
|
||||
ThumbnailUrl: item.ThumbnailUrl,
|
||||
QuestionsCount: item.QuestionsCount,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
|
@ -109,7 +124,7 @@ const LessonList: React.FC = () => {
|
|||
{unpublishedItems.length > 0 && (
|
||||
<>
|
||||
<Divider orientation="left" style={{ marginBottom: 0 }}>
|
||||
Unpublished
|
||||
{t("lessons.lessonStatusDrafts")}
|
||||
</Divider>
|
||||
|
||||
{unpublishedItems.map((item, index) => (
|
||||
|
@ -121,6 +136,7 @@ const LessonList: React.FC = () => {
|
|||
lessonSettings={{
|
||||
Title: item.Title,
|
||||
ThumbnailUrl: item.ThumbnailUrl,
|
||||
QuestionsCount: item.QuestionsCount,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
|
@ -131,20 +147,29 @@ const LessonList: React.FC = () => {
|
|||
};
|
||||
|
||||
export default function Lessons() {
|
||||
const onSearch: SearchProps["onSearch"] = (value, _e, info) =>
|
||||
console.log(info?.source, value);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
dispatch(setSearchFilter(""));
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<MyBanner title="Lessons" headerBar={<HeaderBar />} />
|
||||
<MyBanner title={t("lessons.bannerTitle")} headerBar={<HeaderBar />} />
|
||||
|
||||
<MyContainer>
|
||||
<Flex justify="right" gap={16} style={{ paddingBottom: 16 }}>
|
||||
<CreateLessonButton />
|
||||
|
||||
<Search
|
||||
placeholder="Search..."
|
||||
onSearch={onSearch}
|
||||
placeholder={t("lessons.searchPlaceholder")}
|
||||
onChange={(e) => {
|
||||
dispatch(setSearchFilter(e.target.value));
|
||||
}}
|
||||
style={{ width: 300 }}
|
||||
allowClear
|
||||
/>
|
||||
|
|
|
@ -10,6 +10,7 @@ export const lessonsSlice = createSlice({
|
|||
name: "lessons",
|
||||
initialState: {
|
||||
lessons: [] as Lesson[],
|
||||
searchFilter: "",
|
||||
},
|
||||
reducers: {
|
||||
setLessons: (state, action) => {
|
||||
|
@ -45,9 +46,13 @@ export const lessonsSlice = createSlice({
|
|||
lesson.State = action.payload.State;
|
||||
}
|
||||
},
|
||||
setSearchFilter: (state, action) => {
|
||||
state.searchFilter = action.payload;
|
||||
},
|
||||
},
|
||||
selectors: {
|
||||
lessons: (state) => state.lessons,
|
||||
searchFilter: (state) => state.searchFilter,
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -57,6 +62,7 @@ export const {
|
|||
updateLessonPreviewTitle,
|
||||
updateLessonPreviewThumbnail,
|
||||
updateLessonState,
|
||||
setSearchFilter,
|
||||
} = lessonsSlice.actions;
|
||||
|
||||
export const { lessons } = lessonsSlice.selectors;
|
||||
export const { lessons, searchFilter } = lessonsSlice.selectors;
|
||||
|
|
|
@ -12,8 +12,12 @@ import {
|
|||
addWebSocketReconnectListener,
|
||||
removeWebSocketReconnectListener,
|
||||
} from "core/services/websocketService";
|
||||
import MyUserAvatar from "shared/components/MyUserAvatar";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export default function Roles() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { data, error, isLoading, refetch } = useGetRolesQuery(undefined, {
|
||||
refetchOnMountOrArgChange: true,
|
||||
});
|
||||
|
@ -26,7 +30,11 @@ export default function Roles() {
|
|||
|
||||
return (
|
||||
<>
|
||||
<MyBanner title="Roles" subtitle="MANAGE" headerBar={<HeaderBar />} />
|
||||
<MyBanner
|
||||
title={t("roles.bannerTitle")}
|
||||
subtitle={t("common.bannerSubtitle")}
|
||||
headerBar={<HeaderBar />}
|
||||
/>
|
||||
|
||||
<MyContainer
|
||||
style={{
|
||||
|
@ -61,6 +69,7 @@ interface Permission {
|
|||
}
|
||||
|
||||
// test data
|
||||
|
||||
const tmpI18nObj = [
|
||||
{
|
||||
id: 1,
|
||||
|
@ -91,17 +100,28 @@ const tmpI18nObj = [
|
|||
},
|
||||
];
|
||||
|
||||
/*
|
||||
export const tmpRoleNames = {
|
||||
"d0f0fa0d-3f3b-438b-a76f-7febeb8aab57": "Admin",
|
||||
"b7359e12-359e-423b-b39c-f0d4069adebc": "Moderator",
|
||||
"a1f084ad-d501-4015-b326-4c5c46fd1c5e": "User",
|
||||
} as any;
|
||||
} as any; */
|
||||
|
||||
function RoleComponent({ role }: { role: Role }) {
|
||||
const teamPermissions = tmpI18nObj.filter(
|
||||
const { t } = useTranslation();
|
||||
|
||||
const tmpI18nRoles = t("roles.roles", {
|
||||
returnObjects: true,
|
||||
}) as Permission[];
|
||||
|
||||
const tmpRoleNames = t("roles.roleNames", {
|
||||
returnObjects: true,
|
||||
}) as any;
|
||||
|
||||
const teamPermissions = tmpI18nRoles.filter(
|
||||
(permission) => permission.category === "Team"
|
||||
);
|
||||
const rolePermissions = tmpI18nObj.filter(
|
||||
const rolePermissions = tmpI18nRoles.filter(
|
||||
(permission) => permission.category === "Roles"
|
||||
);
|
||||
|
||||
|
@ -130,7 +150,7 @@ function RoleComponent({ role }: { role: Role }) {
|
|||
items={[
|
||||
{
|
||||
key: "1",
|
||||
label: "Team",
|
||||
label: t("roles.collapseTitleTeam"),
|
||||
children: (
|
||||
<>
|
||||
{teamPermissions.map((permission, index) => (
|
||||
|
@ -141,7 +161,7 @@ function RoleComponent({ role }: { role: Role }) {
|
|||
},
|
||||
{
|
||||
key: "2",
|
||||
label: "Roles",
|
||||
label: t("roles.collapseTitleRoles"),
|
||||
children: (
|
||||
<>
|
||||
{rolePermissions.map((permission, index) => (
|
||||
|
@ -168,9 +188,13 @@ function RoleComponent({ role }: { role: Role }) {
|
|||
title={`${user.FirstName} ${user.LastName}`}
|
||||
placement="top"
|
||||
>
|
||||
<Avatar style={{ backgroundColor: "#f56a00" }}>
|
||||
{user.FirstName[0]}
|
||||
</Avatar>
|
||||
<div>
|
||||
<MyUserAvatar
|
||||
profilePictureUrl={user.ProfilePictureUrl}
|
||||
firstName={user.FirstName}
|
||||
size={32}
|
||||
/>
|
||||
</div>
|
||||
</Tooltip>
|
||||
))}
|
||||
</Avatar.Group>
|
||||
|
|
|
@ -30,6 +30,7 @@ import {
|
|||
addWebSocketReconnectListener,
|
||||
removeWebSocketReconnectListener,
|
||||
} from "core/services/websocketService";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
type GeneralFieldType = {
|
||||
primaryColor: string | AggregationColor;
|
||||
|
@ -37,6 +38,8 @@ type GeneralFieldType = {
|
|||
};
|
||||
|
||||
export default function Settings() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { data, error, isLoading, refetch } = useGetOrganizationSettingsQuery(
|
||||
undefined,
|
||||
{
|
||||
|
@ -54,7 +57,11 @@ export default function Settings() {
|
|||
|
||||
return (
|
||||
<>
|
||||
<MyBanner title="Settings" subtitle="MANAGE" headerBar={<HeaderBar />} />
|
||||
<MyBanner
|
||||
title={t("organizationSettings.bannerTitle")}
|
||||
subtitle={t("common.bannerSubtitle")}
|
||||
headerBar={<HeaderBar />}
|
||||
/>
|
||||
|
||||
<GeneralCard data={data} isLoading={isLoading} />
|
||||
|
||||
|
@ -69,6 +76,7 @@ function GeneralCard({
|
|||
data,
|
||||
isLoading,
|
||||
}: { data?: OrganizationSettings; isLoading?: boolean } = {}) {
|
||||
const { t } = useTranslation();
|
||||
const [form] = useForm<GeneralFieldType>();
|
||||
const { success, error: errorMessage } = useMessage();
|
||||
|
||||
|
@ -93,10 +101,12 @@ function GeneralCard({
|
|||
|
||||
currentPrimaryColor.current = hexColor;
|
||||
|
||||
success("Settings updated successfully!");
|
||||
success(
|
||||
t("organizationSettings.generalCard.messageSettingsSuccessfullyUpdated")
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
errorMessage("Failed to update settings!");
|
||||
errorMessage(t("common.messageRequestFailed"));
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -129,7 +139,7 @@ function GeneralCard({
|
|||
padding: 16,
|
||||
},
|
||||
}}
|
||||
title="General"
|
||||
title={t("organizationSettings.generalCard.title")}
|
||||
loading={isLoading}
|
||||
extra={
|
||||
<Button
|
||||
|
@ -145,7 +155,7 @@ function GeneralCard({
|
|||
<Flex wrap gap={12}>
|
||||
<Form.Item<GeneralFieldType>
|
||||
name="primaryColor"
|
||||
label="Primary color"
|
||||
label={t("organizationSettings.generalCard.primaryColor")}
|
||||
>
|
||||
<ColorPicker
|
||||
size="small"
|
||||
|
@ -162,7 +172,10 @@ function GeneralCard({
|
|||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item<GeneralFieldType> name="companyName" label="Company name">
|
||||
<Form.Item<GeneralFieldType>
|
||||
name="companyName"
|
||||
label={t("organizationSettings.generalCard.companyName")}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
</Flex>
|
||||
|
@ -174,6 +187,7 @@ function GeneralCard({
|
|||
function MediaCard({
|
||||
isLoading,
|
||||
}: { data?: OrganizationSettings; isLoading?: boolean } = {}) {
|
||||
const { t } = useTranslation();
|
||||
const { success } = useMessage();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
|
@ -182,15 +196,22 @@ function MediaCard({
|
|||
|
||||
return (
|
||||
<Form layout="vertical">
|
||||
<MyMiddleCard title="Media" loading={isLoading}>
|
||||
<Form.Item label="Logo">
|
||||
<MyMiddleCard
|
||||
title={t("organizationSettings.mediaCard.title")}
|
||||
loading={isLoading}
|
||||
>
|
||||
<Form.Item label={t("organizationSettings.mediaCard.logo")}>
|
||||
<MyUpload
|
||||
action="/organization/file/logo"
|
||||
onChange={(info) => {
|
||||
if (info.file.status === "done" && info.file.response.Data) {
|
||||
dispatch(setLogoUrl(info.file.response.Data));
|
||||
|
||||
success("Logo updated successfully!");
|
||||
success(
|
||||
t(
|
||||
"organizationSettings.mediaCard.messageLogoSuccessfullyUpdated"
|
||||
)
|
||||
);
|
||||
}
|
||||
}}
|
||||
imgCropProps={{
|
||||
|
@ -202,7 +223,7 @@ function MediaCard({
|
|||
src={`${Constants.STATIC_CONTENT_ADDRESS}/${
|
||||
appLogoUrl || Constants.DEMO_LOGO_URL
|
||||
}`}
|
||||
alt="Company Logo"
|
||||
alt={t("organizationSettings.mediaCard.logo")}
|
||||
style={{
|
||||
width: 128,
|
||||
maxHeight: 128,
|
||||
|
@ -214,14 +235,18 @@ function MediaCard({
|
|||
</MyUpload>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="Banner">
|
||||
<Form.Item label={t("organizationSettings.mediaCard.banner")}>
|
||||
<MyUpload
|
||||
action="/organization/file/banner"
|
||||
onChange={(info) => {
|
||||
if (info.file.status === "done" && info.file.response.Data) {
|
||||
dispatch(setBannerUrl(info.file.response.Data));
|
||||
|
||||
success("Banner updated successfully!");
|
||||
success(
|
||||
t(
|
||||
"organizationSettings.mediaCard.messageBannerSuccessfullyUpdated"
|
||||
)
|
||||
);
|
||||
}
|
||||
}}
|
||||
imgCropProps={{
|
||||
|
@ -233,7 +258,7 @@ function MediaCard({
|
|||
src={`${Constants.STATIC_CONTENT_ADDRESS}/${
|
||||
appBannerUrl || Constants.DEMO_BANNER_URL
|
||||
}`}
|
||||
alt="Banner"
|
||||
alt={t("organizationSettings.mediaCard.banner")}
|
||||
style={{
|
||||
width: "100%",
|
||||
height: 228,
|
||||
|
@ -256,6 +281,7 @@ function SubdomainCard({
|
|||
data,
|
||||
isLoading,
|
||||
}: { data?: OrganizationSettings; isLoading?: boolean } = {}) {
|
||||
const { t } = useTranslation();
|
||||
const [form] = useForm();
|
||||
const { success, info } = useMessage();
|
||||
|
||||
|
@ -283,12 +309,16 @@ function SubdomainCard({
|
|||
const { data } = await reqIsSubdomainAvailable(value);
|
||||
|
||||
if (!data.Available) {
|
||||
return Promise.reject("This subdomain is already taken!");
|
||||
return Promise.reject(
|
||||
t("organizationSettings.subdomainCard.subdomainAlreadyTaken")
|
||||
);
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
} catch (error) {
|
||||
return Promise.reject("This subdomain is already taken!");
|
||||
return Promise.reject(
|
||||
t("organizationSettings.subdomainCard.subdomainAlreadyTaken")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -306,17 +336,27 @@ function SubdomainCard({
|
|||
return (
|
||||
<>
|
||||
<Modal
|
||||
title="Change Subdomain"
|
||||
title={t(
|
||||
"organizationSettings.subdomainCard.modalChangeSubdomain.title"
|
||||
)}
|
||||
open={isModalOpen}
|
||||
centered
|
||||
onCancel={() => setIsModalOpen(false)}
|
||||
okText="Change"
|
||||
okText={t("common.change")}
|
||||
onOk={async () => {
|
||||
try {
|
||||
await reqUpdateSubdomain(form.getFieldValue("subdomain"));
|
||||
|
||||
success("Subdomain updated successfully!");
|
||||
info("You will be redirected to the new subdomain!");
|
||||
success(
|
||||
t(
|
||||
"organizationSettings.subdomainCard.modalChangeSubdomain.messageSubdomainSuccessfullyUpdated"
|
||||
)
|
||||
);
|
||||
info(
|
||||
t(
|
||||
"organizationSettings.subdomainCard.modalChangeSubdomain.messageRedirect"
|
||||
)
|
||||
);
|
||||
/*
|
||||
window.location.href = `https://${form.getFieldValue(
|
||||
"subdomain"
|
||||
|
@ -327,16 +367,13 @@ function SubdomainCard({
|
|||
}}
|
||||
>
|
||||
<p>
|
||||
Changing your subdomain will make your organization available at the
|
||||
new subdomain. Please note that you will be logged out and redirected
|
||||
to the new subdomain. Also the old subdomain will be available for
|
||||
registration by other users.
|
||||
{t("organizationSettings.subdomainCard.modalChangeSubdomain.message")}
|
||||
</p>
|
||||
</Modal>
|
||||
|
||||
<Form form={form} layout="vertical" requiredMark={false}>
|
||||
<MyMiddleCard
|
||||
title="Subdomain"
|
||||
title={t("organizationSettings.subdomainCard.middleCardTitle")}
|
||||
loading={isLoading}
|
||||
extra={
|
||||
<Button
|
||||
|
@ -348,9 +385,7 @@ function SubdomainCard({
|
|||
form
|
||||
.validateFields()
|
||||
.then(() => setIsModalOpen(true))
|
||||
.catch(() => {
|
||||
console.error("Validation failed!");
|
||||
});
|
||||
.catch(() => {});
|
||||
}}
|
||||
/>
|
||||
}
|
||||
|
@ -358,25 +393,37 @@ function SubdomainCard({
|
|||
<Flex>
|
||||
<Form.Item
|
||||
name="subdomain"
|
||||
label="Subdomain"
|
||||
label={t("organizationSettings.subdomainCard.subdomain")}
|
||||
hasFeedback
|
||||
validateDebounce={300}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: "Please input your subdomain!",
|
||||
message: t(
|
||||
"organizationSettings.subdomainCard.rules.required"
|
||||
),
|
||||
},
|
||||
{
|
||||
pattern: subdomainPattern,
|
||||
message: "Please enter a valid subdomain!",
|
||||
message: t("organizationSettings.subdomainCard.rules.valid"),
|
||||
},
|
||||
{
|
||||
min: Constants.GLOBALS.MIN_SUBDOMAIN_LENGTH,
|
||||
message: `Subdomain must be at least ${Constants.GLOBALS.MIN_SUBDOMAIN_LENGTH} characters!`,
|
||||
message: t(
|
||||
"organizationSettings.subdomainCard.rules.minLength",
|
||||
{
|
||||
min: Constants.GLOBALS.MIN_SUBDOMAIN_LENGTH,
|
||||
}
|
||||
),
|
||||
},
|
||||
{
|
||||
max: Constants.GLOBALS.MAX_SUBDOMAIN_LENGTH,
|
||||
message: "Subdomain is too long!",
|
||||
message: t(
|
||||
"organizationSettings.subdomainCard.rules.maxLength",
|
||||
{
|
||||
max: Constants.GLOBALS.MAX_SUBDOMAIN_LENGTH,
|
||||
}
|
||||
),
|
||||
},
|
||||
{
|
||||
required: true,
|
||||
|
@ -384,7 +431,11 @@ function SubdomainCard({
|
|||
},
|
||||
]}
|
||||
>
|
||||
<Input addonBefore="https://" addonAfter=". xx.de" />
|
||||
<Input
|
||||
addonBefore="https://"
|
||||
addonAfter=". xx.de"
|
||||
maxLength={Constants.GLOBALS.MAX_SUBDOMAIN_LENGTH}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Flex>
|
||||
</MyMiddleCard>
|
||||
|
|
|
@ -1,7 +1,16 @@
|
|||
import HeaderBar from "core/components/Header";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import MyBanner from "shared/components/MyBanner";
|
||||
|
||||
export default function SuggestFeature() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<>
|
||||
<h1>SuggestFeature</h1>
|
||||
<MyBanner
|
||||
title={t("suggestFeature.bannerTitle")}
|
||||
headerBar={<HeaderBar />}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ import HeaderBar from "core/components/Header";
|
|||
import { useMessage } from "core/context/MessageContext";
|
||||
import { useCreateTeamMemberMutation } from "core/services/organization";
|
||||
import { Constants, EncodeStringToBase64 } from "core/utils/utils";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import MyMiddleCard from "shared/components/MyMiddleCard";
|
||||
|
||||
|
@ -17,11 +18,17 @@ type FieldType = {
|
|||
};
|
||||
|
||||
export default function TeamCreateUser() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const navigate = useNavigate();
|
||||
const { success } = useMessage();
|
||||
|
||||
const [reqCreateTeamMember, { isLoading }] = useCreateTeamMemberMutation();
|
||||
|
||||
const tmpRoleNames = t("roles.roleNames", {
|
||||
returnObjects: true,
|
||||
}) as any;
|
||||
|
||||
return (
|
||||
<>
|
||||
<HeaderBar
|
||||
|
@ -29,7 +36,7 @@ export default function TeamCreateUser() {
|
|||
backTo={Constants.ROUTE_PATHS.ORGANIZATION_TEAM}
|
||||
/>
|
||||
|
||||
<MyMiddleCard title="Create User">
|
||||
<MyMiddleCard title={t("teamCreateUser.middleCardTitle")}>
|
||||
<Form
|
||||
layout="vertical"
|
||||
requiredMark={false}
|
||||
|
@ -48,7 +55,7 @@ export default function TeamCreateUser() {
|
|||
password: EncodeStringToBase64(values.password),
|
||||
}).unwrap();
|
||||
|
||||
success("User created successfully!");
|
||||
success(t("teamCreateUser.messageUserSuccessfullyCreated"));
|
||||
|
||||
navigate(Constants.ROUTE_PATHS.ORGANIZATION_TEAM);
|
||||
} catch (error) {
|
||||
|
@ -57,74 +64,90 @@ export default function TeamCreateUser() {
|
|||
}}
|
||||
>
|
||||
<Form.Item<FieldType>
|
||||
label="First Name"
|
||||
label={t("common.firstName")}
|
||||
name="firstName"
|
||||
rules={[
|
||||
{ required: true, message: "Please input first name!" },
|
||||
{ required: true, message: t("common.firstNameRules.required") },
|
||||
{
|
||||
min: Constants.GLOBALS.MIN_FIRST_NAME_LENGTH,
|
||||
message: `First name must be at least ${Constants.GLOBALS.MIN_FIRST_NAME_LENGTH} characters long!`,
|
||||
message: t("common.firstNameRules.minLength", {
|
||||
minLength: Constants.GLOBALS.MIN_FIRST_NAME_LENGTH,
|
||||
}),
|
||||
},
|
||||
{
|
||||
max: Constants.GLOBALS.MAX_FIRST_NAME_LENGTH,
|
||||
message: `First name must be at most ${Constants.GLOBALS.MAX_FIRST_NAME_LENGTH} characters long!`,
|
||||
message: t("common.firstNameRules.maxLength", {
|
||||
maxLength: Constants.GLOBALS.MAX_FIRST_NAME_LENGTH,
|
||||
}),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input
|
||||
placeholder="First Name"
|
||||
placeholder={t("common.firstName")}
|
||||
maxLength={Constants.GLOBALS.MAX_FIRST_NAME_LENGTH}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item<FieldType>
|
||||
label="Last Name"
|
||||
label={t("common.lastName")}
|
||||
name="lastName"
|
||||
rules={[
|
||||
{ required: true, message: "Please input last name!" },
|
||||
{ required: true, message: t("common.lastNameRules.required") },
|
||||
{
|
||||
min: Constants.GLOBALS.MIN_LAST_NAME_LENGTH,
|
||||
message: `Last name must be at least ${Constants.GLOBALS.MIN_LAST_NAME_LENGTH} characters long!`,
|
||||
message: t("common.lastNameRules.minLength", {
|
||||
minLength: Constants.GLOBALS.MIN_LAST_NAME_LENGTH,
|
||||
}),
|
||||
},
|
||||
{
|
||||
max: Constants.GLOBALS.MAX_LAST_NAME_LENGTH,
|
||||
message: `Last name must be at most ${Constants.GLOBALS.MAX_LAST_NAME_LENGTH} characters long!`,
|
||||
message: t("common.lastNameRules.maxLength", {
|
||||
maxLength: Constants.GLOBALS.MAX_LAST_NAME_LENGTH,
|
||||
}),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input
|
||||
placeholder="Last Name"
|
||||
placeholder={t("common.lastName")}
|
||||
maxLength={Constants.GLOBALS.MAX_LAST_NAME_LENGTH}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item<FieldType>
|
||||
label="Email"
|
||||
label={t("common.email")}
|
||||
name="email"
|
||||
rules={[
|
||||
{ required: true, message: "Please input email!", type: "email" },
|
||||
{
|
||||
required: true,
|
||||
message: t("common.emailRules.valid"),
|
||||
type: "email",
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input placeholder="Email" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item<FieldType>
|
||||
label="Password"
|
||||
label={t("common.password")}
|
||||
name="password"
|
||||
rules={[
|
||||
{ required: true, message: "Please input password!" },
|
||||
{ required: true, message: t("common.passwordRules.required") },
|
||||
{
|
||||
min: Constants.GLOBALS.MIN_PASSWORD_LENGTH,
|
||||
message: `Password must be at least ${Constants.GLOBALS.MIN_PASSWORD_LENGTH} characters long!`,
|
||||
message: t("common.passwordRules.minLength", {
|
||||
minLength: Constants.GLOBALS.MIN_PASSWORD_LENGTH,
|
||||
}),
|
||||
},
|
||||
{
|
||||
max: Constants.GLOBALS.MAX_PASSWORD_LENGTH,
|
||||
message: `Password must be at most ${Constants.GLOBALS.MAX_PASSWORD_LENGTH} characters long!`,
|
||||
message: t("common.passwordRules.maxLength", {
|
||||
maxLength: Constants.GLOBALS.MAX_PASSWORD_LENGTH,
|
||||
}),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input.Password
|
||||
placeholder="Password"
|
||||
placeholder={t("common.password")}
|
||||
maxLength={Constants.GLOBALS.MAX_PASSWORD_LENGTH}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
@ -132,13 +155,13 @@ export default function TeamCreateUser() {
|
|||
<Form.Item name="roleId" label="Role">
|
||||
<Select>
|
||||
<Select.Option value="d0f0fa0d-3f3b-438b-a76f-7febeb8aab57">
|
||||
Admin
|
||||
{tmpRoleNames["d0f0fa0d-3f3b-438b-a76f-7febeb8aab57"]}
|
||||
</Select.Option>
|
||||
<Select.Option value="b7359e12-359e-423b-b39c-f0d4069adebc">
|
||||
Moderator
|
||||
{tmpRoleNames["b7359e12-359e-423b-b39c-f0d4069adebc"]}
|
||||
</Select.Option>
|
||||
<Select.Option value="a1f084ad-d501-4015-b326-4c5c46fd1c5e">
|
||||
User
|
||||
{tmpRoleNames["a1f084ad-d501-4015-b326-4c5c46fd1c5e"]}
|
||||
</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
@ -150,7 +173,7 @@ export default function TeamCreateUser() {
|
|||
htmlType="submit"
|
||||
loading={isLoading}
|
||||
>
|
||||
Create User
|
||||
{t("teamCreateUser.buttonCreateUser")}
|
||||
</Button>
|
||||
</Flex>
|
||||
</Form>
|
||||
|
|
|
@ -15,15 +15,16 @@ import MyErrorResult from "shared/components/MyResult";
|
|||
import { useEffect, useState } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { setTeamMembers, teamMembers } from "./teamSlice";
|
||||
import { tmpRoleNames } from "features/Roles";
|
||||
import {
|
||||
addWebSocketReconnectListener,
|
||||
removeWebSocketReconnectListener,
|
||||
} from "core/services/websocketService";
|
||||
import { useMessage } from "core/context/MessageContext";
|
||||
import MyUserAvatar from "shared/components/MyUserAvatar";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const TeamList: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useDispatch();
|
||||
const { success, error: errorMessage } = useMessage();
|
||||
|
||||
|
@ -41,41 +42,45 @@ const TeamList: React.FC = () => {
|
|||
const [reqDeleteTeamMember, { isLoading: loadingDeleteTeamMember }] =
|
||||
useDeleteTeamMemberMutation();
|
||||
|
||||
const tmpRoleNames = t("roles.roleNames", {
|
||||
returnObjects: true,
|
||||
}) as any;
|
||||
|
||||
const getTableContent = () => {
|
||||
let items = [
|
||||
{
|
||||
title: "First name",
|
||||
title: t("common.firstName"),
|
||||
dataIndex: "firstName",
|
||||
key: "firstName",
|
||||
},
|
||||
{
|
||||
title: "Last name",
|
||||
title: t("common.lastName"),
|
||||
dataIndex: "lastName",
|
||||
key: "lastName",
|
||||
},
|
||||
{
|
||||
title: "Email",
|
||||
title: t("common.email"),
|
||||
dataIndex: "email",
|
||||
key: "email",
|
||||
},
|
||||
{
|
||||
title: "Role",
|
||||
title: t("common.role"),
|
||||
dataIndex: "role",
|
||||
key: "role",
|
||||
},
|
||||
{
|
||||
title: "Status",
|
||||
title: t("common.status"),
|
||||
dataIndex: "status",
|
||||
key: "status",
|
||||
},
|
||||
{
|
||||
title: "Actions",
|
||||
title: t("common.actions"),
|
||||
dataIndex: "actions",
|
||||
key: "actions",
|
||||
render: (_: any, record: any) => (
|
||||
<Space size="middle">
|
||||
<Popconfirm
|
||||
title="Change role to"
|
||||
title={t("team.popConfirmRoleChange.title")}
|
||||
onConfirm={async () => {
|
||||
try {
|
||||
await reqUpdateTeamMemberRole({
|
||||
|
@ -83,13 +88,18 @@ const TeamList: React.FC = () => {
|
|||
roleId: selectedRoleId,
|
||||
}).unwrap();
|
||||
|
||||
success("Role updated successfully");
|
||||
success(
|
||||
t(
|
||||
"team.popConfirmRoleChange.messageUpdatedRoleSuccessfully"
|
||||
)
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
errorMessage("Error updating role");
|
||||
errorMessage(t("common.messageRequestFailed"));
|
||||
}
|
||||
}}
|
||||
okButtonProps={{ loading: loadingUpdateTeamMemberRole }}
|
||||
cancelText={t("common.cancel")}
|
||||
description={
|
||||
<Select
|
||||
style={{ width: 150 }}
|
||||
|
@ -109,27 +119,32 @@ const TeamList: React.FC = () => {
|
|||
type="link"
|
||||
onClick={() => setSelectedRoleId(record.role)}
|
||||
>
|
||||
Change role
|
||||
{t("team.changeRole")}
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
|
||||
<Popconfirm
|
||||
title="Confirm deletion of team member"
|
||||
title={t("team.popConfirmDeleteMember.title")}
|
||||
okButtonProps={{
|
||||
loading: loadingDeleteTeamMember,
|
||||
}}
|
||||
cancelText={t("common.cancel")}
|
||||
onConfirm={async () => {
|
||||
try {
|
||||
await reqDeleteTeamMember(record.key).unwrap();
|
||||
|
||||
success("Team member deleted successfully");
|
||||
success(
|
||||
t(
|
||||
"team.popConfirmDeleteMember.messageDeletedMemberSuccessfully"
|
||||
)
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
errorMessage("Error deleting team member");
|
||||
errorMessage(t("common.messageRequestFailed"));
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Button type="link">Delete</Button>
|
||||
<Button type="link">{t("common.delete")}</Button>
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
),
|
||||
|
@ -199,9 +214,15 @@ const TeamList: React.FC = () => {
|
|||
};
|
||||
|
||||
export default function Team() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<>
|
||||
<MyBanner title="Team" subtitle="MANAGE" headerBar={<HeaderBar />} />
|
||||
<MyBanner
|
||||
title={t("team.bannerTitle")}
|
||||
subtitle={t("common.bannerSubtitle")}
|
||||
headerBar={<HeaderBar />}
|
||||
/>
|
||||
|
||||
<MyContainer
|
||||
style={{
|
||||
|
@ -212,7 +233,9 @@ export default function Team() {
|
|||
>
|
||||
<Flex justify="end">
|
||||
<Link to={Constants.ROUTE_PATHS.ORGANIZATION_TEAM_CREATE_USER}>
|
||||
<Button icon={<UserAddOutlined />}>Invite new member</Button>
|
||||
<Button icon={<UserAddOutlined />}>
|
||||
{t("team.addMemberButton")}
|
||||
</Button>
|
||||
</Link>
|
||||
</Flex>
|
||||
|
||||
|
|
|
@ -1,15 +1,14 @@
|
|||
import {
|
||||
Avatar,
|
||||
Card,
|
||||
Descriptions,
|
||||
Flex,
|
||||
Form,
|
||||
Input,
|
||||
Select,
|
||||
Typography,
|
||||
} from "antd";
|
||||
import HeaderBar from "../../core/components/Header";
|
||||
import MyBanner from "../../shared/components/MyBanner";
|
||||
import { Constants } from "core/utils/utils";
|
||||
import MyMiddleCard from "shared/components/MyMiddleCard";
|
||||
import Meta from "antd/es/card/Meta";
|
||||
import { useGetUserProfileQuery } from "core/services/userProfile";
|
||||
|
@ -31,15 +30,16 @@ import {
|
|||
setProfilePictureUrl,
|
||||
setRoleId,
|
||||
} from "./userProfileSlice";
|
||||
import { tmpRoleNames } from "features/Roles";
|
||||
import MyErrorResult from "shared/components/MyResult";
|
||||
import MyUpload from "shared/components/MyUpload";
|
||||
import { useMessage } from "core/context/MessageContext";
|
||||
import MyUserAvatar from "shared/components/MyUserAvatar";
|
||||
import { setUserProfilePictureUrl } from "core/reducers/appSlice";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export default function UserProfile({ isAdmin }: { isAdmin?: boolean }) {
|
||||
const dispatch = useDispatch();
|
||||
const { t, i18n } = useTranslation();
|
||||
const { success } = useMessage();
|
||||
|
||||
const dataProfilePictureUrl = useSelector(profilePictureUrl);
|
||||
|
@ -55,6 +55,10 @@ export default function UserProfile({ isAdmin }: { isAdmin?: boolean }) {
|
|||
}
|
||||
);
|
||||
|
||||
const tmpRoleNames = t("roles.roleNames", {
|
||||
returnObjects: true,
|
||||
}) as any;
|
||||
|
||||
function AdminWrapper({ children }: { children: React.ReactNode }) {
|
||||
if (!isAdmin) {
|
||||
return <>{children}</>;
|
||||
|
@ -98,12 +102,15 @@ export default function UserProfile({ isAdmin }: { isAdmin?: boolean }) {
|
|||
return (
|
||||
<>
|
||||
<MyBanner
|
||||
title="Account Settings"
|
||||
subtitle="MANAGE"
|
||||
title={t("userProfile.bannerTitle")}
|
||||
subtitle={t("common.bannerSubtitle")}
|
||||
headerBar={<HeaderBar />}
|
||||
/>
|
||||
|
||||
<MyMiddleCard title="My Profile" loading={isLoading}>
|
||||
<MyMiddleCard
|
||||
title={t("userProfile.middleCardTitle")}
|
||||
loading={isLoading}
|
||||
>
|
||||
{error ? (
|
||||
<MyErrorResult />
|
||||
) : (
|
||||
|
@ -115,31 +122,60 @@ export default function UserProfile({ isAdmin }: { isAdmin?: boolean }) {
|
|||
},
|
||||
}}
|
||||
>
|
||||
<Meta
|
||||
avatar={
|
||||
<MyUpload
|
||||
action={`/user/profile/picture`}
|
||||
onChange={(info) => {
|
||||
if (info.file.status === "done") {
|
||||
success("Profile picture updated successfully");
|
||||
<Form
|
||||
layout="vertical"
|
||||
initialValues={{
|
||||
language: i18n.language,
|
||||
}}
|
||||
>
|
||||
<Flex justify="space-between" align="center" wrap gap={24}>
|
||||
<Meta
|
||||
avatar={
|
||||
<MyUpload
|
||||
action={`/user/profile/picture`}
|
||||
onChange={(info) => {
|
||||
if (info.file.status === "done") {
|
||||
success(
|
||||
t("userProfile.messageProfileSuccessfullyUpdated")
|
||||
);
|
||||
|
||||
dispatch(setProfilePictureUrl(info.file.response.Data));
|
||||
dispatch(
|
||||
setUserProfilePictureUrl(info.file.response.Data)
|
||||
);
|
||||
}
|
||||
}}
|
||||
imgCropProps={{
|
||||
aspect: 1 / 1,
|
||||
children: <></>,
|
||||
}}
|
||||
>
|
||||
<MyUserAvatar profilePictureUrl={dataProfilePictureUrl} />
|
||||
</MyUpload>
|
||||
}
|
||||
title={`${dataFirstName} ${dataLastName}`}
|
||||
description={tmpRoleNames[dataRoleId]}
|
||||
/>
|
||||
dispatch(
|
||||
setProfilePictureUrl(info.file.response.Data)
|
||||
);
|
||||
dispatch(
|
||||
setUserProfilePictureUrl(info.file.response.Data)
|
||||
);
|
||||
}
|
||||
}}
|
||||
imgCropProps={{
|
||||
aspect: 1 / 1,
|
||||
children: <></>,
|
||||
}}
|
||||
>
|
||||
<MyUserAvatar
|
||||
profilePictureUrl={dataProfilePictureUrl}
|
||||
/>
|
||||
</MyUpload>
|
||||
}
|
||||
title={`${dataFirstName} ${dataLastName}`}
|
||||
description={tmpRoleNames[dataRoleId]}
|
||||
/>
|
||||
|
||||
<Form.Item label={t("userProfile.language")} name="language">
|
||||
<Select
|
||||
onChange={(value) => i18n.changeLanguage(value)}
|
||||
style={{ width: 120 }}
|
||||
>
|
||||
<Select.Option value="en">
|
||||
{t("userProfile.languageEnglish")}
|
||||
</Select.Option>
|
||||
<Select.Option value="de">
|
||||
{t("userProfile.languageGerman")}
|
||||
</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Flex>
|
||||
</Form>
|
||||
</Card>
|
||||
|
||||
<Card
|
||||
|
@ -151,26 +187,26 @@ export default function UserProfile({ isAdmin }: { isAdmin?: boolean }) {
|
|||
>
|
||||
<AdminWrapper>
|
||||
<Descriptions
|
||||
title="Personal Information"
|
||||
title={t("userProfile.personalInformation")}
|
||||
layout="vertical"
|
||||
items={[
|
||||
{
|
||||
key: "1",
|
||||
label: "First name",
|
||||
label: t("common.firstName"),
|
||||
children: (
|
||||
<TextItem value={dataFirstName} name="firstName" />
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "2",
|
||||
label: "Last name",
|
||||
label: t("common.lastName"),
|
||||
children: (
|
||||
<TextItem value={dataLastName} name="lastName" />
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "3",
|
||||
label: "Email",
|
||||
label: t("common.email"),
|
||||
children: <TextItem value={dataEmail} name="email" />,
|
||||
},
|
||||
]}
|
||||
|
|
|
@ -1,7 +1,13 @@
|
|||
import HeaderBar from "core/components/Header";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import MyBanner from "shared/components/MyBanner";
|
||||
|
||||
export default function WhatsNew() {
|
||||
return (
|
||||
<>
|
||||
<h1>WhatsNew</h1>
|
||||
</>
|
||||
);
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<>
|
||||
<MyBanner title={t("whatsNew.bannerTitle")} headerBar={<HeaderBar />} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
import i18n from "i18next";
|
||||
import { initReactI18next } from "react-i18next";
|
||||
import Backend from "i18next-http-backend";
|
||||
import LanguageDetector from "i18next-browser-languagedetector";
|
||||
|
||||
i18n
|
||||
.use(initReactI18next) // passes i18n down to react-i18next
|
||||
.use(LanguageDetector)
|
||||
.use(Backend)
|
||||
.init({
|
||||
supportedLngs: ["en", "de"],
|
||||
fallbackLng: "de",
|
||||
interpolation: {
|
||||
escapeValue: false, // react already safes from xss
|
||||
},
|
||||
});
|
||||
|
||||
export default i18n;
|
|
@ -6,6 +6,7 @@ import { Provider } from "react-redux";
|
|||
import { store } from "./core/store/store";
|
||||
import { Suspense } from "react";
|
||||
import MyCenteredSpin from "./shared/components/MyCenteredSpin";
|
||||
import "./i18n";
|
||||
// import reportWebVitals from './reportWebVitals';
|
||||
|
||||
const root = ReactDOM.createRoot(
|
||||
|
|
|
@ -1,5 +1,18 @@
|
|||
import { Empty } from "antd";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export default function MyEmpty() {
|
||||
return <Empty />
|
||||
}
|
||||
interface MyEmptyProps extends React.ComponentProps<typeof Empty> {
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
const MyEmpty: React.FC<MyEmptyProps> = ({ children, ...props }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Empty {...props} description={t("myEmpty.noData")}>
|
||||
{children}
|
||||
</Empty>
|
||||
);
|
||||
};
|
||||
|
||||
export default MyEmpty;
|
||||
|
|
|
@ -5,6 +5,7 @@ import { Link } from "react-router-dom";
|
|||
import MyUpload from "shared/components/MyUpload";
|
||||
import styles from "./styles.module.css";
|
||||
import { LessonSettings } from "core/types/lesson";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export default function MyLessonPreviewCard({
|
||||
mode = "view",
|
||||
|
@ -21,6 +22,8 @@ export default function MyLessonPreviewCard({
|
|||
onEditTitle?: (newTitle: string) => void;
|
||||
onThumbnailChanged?: () => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const LinkWrapper = ({ children }: { children: React.ReactNode }) => {
|
||||
if (mode === "editable") return <>{children}</>;
|
||||
|
||||
|
@ -74,7 +77,8 @@ export default function MyLessonPreviewCard({
|
|||
{mode === "view" ? (
|
||||
<div>
|
||||
<div className={styles.title}>{lessonSettings.Title}</div>
|
||||
<CommentOutlined /> 12 comments
|
||||
<CommentOutlined /> {lessonSettings.QuestionsCount}{" "}
|
||||
{t("lessonPage.questions")}
|
||||
</div>
|
||||
) : (
|
||||
<Typography.Title
|
||||
|
|
|
@ -1,11 +1,14 @@
|
|||
import { Result } from "antd";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export default function MyErrorResult() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Result
|
||||
status="error"
|
||||
title="Something went wrong"
|
||||
subTitle="Please try again later."
|
||||
title={t("myErrorResult.title")}
|
||||
subTitle={t("myErrorResult.subTitle")}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,34 +1,55 @@
|
|||
import { Avatar } from 'antd';
|
||||
import { Constants } from 'core/utils/utils';
|
||||
import { UserOutlined } from '@ant-design/icons';
|
||||
import { primaryColor } from 'core/reducers/appSlice';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { Avatar } from "antd";
|
||||
import { Constants } from "core/utils/utils";
|
||||
import { UserOutlined } from "@ant-design/icons";
|
||||
import { primaryColor } from "core/reducers/appSlice";
|
||||
import { useSelector } from "react-redux";
|
||||
|
||||
interface MyUserAvatarProps {
|
||||
size?: number;
|
||||
firstName?: string;
|
||||
profilePictureUrl: string;
|
||||
disableCursorPointer?: boolean;
|
||||
size?: number;
|
||||
firstName?: string;
|
||||
profilePictureUrl: string;
|
||||
disableCursorPointer?: boolean;
|
||||
}
|
||||
|
||||
const MyUserAvatar: React.FC<MyUserAvatarProps> = ({ size = 56, firstName, profilePictureUrl, disableCursorPointer }) => {
|
||||
const appPrimaryColor = useSelector(primaryColor);
|
||||
const MyUserAvatar: React.FC<MyUserAvatarProps> = ({
|
||||
size = 56,
|
||||
firstName,
|
||||
profilePictureUrl,
|
||||
disableCursorPointer,
|
||||
}) => {
|
||||
const appPrimaryColor = useSelector(primaryColor);
|
||||
|
||||
const defaultStyle = disableCursorPointer === undefined ? { cursor: 'pointer' } : {};
|
||||
const defaultStyle =
|
||||
disableCursorPointer === undefined ? { cursor: "pointer" } : {};
|
||||
|
||||
const isProfilePictureEmpty = profilePictureUrl === '';
|
||||
const avatarContent = isProfilePictureEmpty && firstName !== undefined ? firstName.charAt(0) : undefined;
|
||||
const iconContent = isProfilePictureEmpty && firstName === undefined ? <UserOutlined /> : undefined;
|
||||
const avatarSrc = isProfilePictureEmpty ? undefined : `${Constants.STATIC_CONTENT_ADDRESS}/${profilePictureUrl}`;
|
||||
const avatarStyle = isProfilePictureEmpty ? { ...defaultStyle, backgroundColor: `#${appPrimaryColor}` } : defaultStyle;
|
||||
const isProfilePictureEmpty = profilePictureUrl === "";
|
||||
const avatarContent =
|
||||
isProfilePictureEmpty && firstName !== undefined
|
||||
? firstName.charAt(0)
|
||||
: undefined;
|
||||
const iconContent =
|
||||
isProfilePictureEmpty && firstName === undefined ? (
|
||||
<UserOutlined />
|
||||
) : undefined;
|
||||
const avatarSrc = isProfilePictureEmpty
|
||||
? undefined
|
||||
: `${Constants.STATIC_CONTENT_ADDRESS}/${profilePictureUrl}`;
|
||||
const avatarStyle = isProfilePictureEmpty
|
||||
? { ...defaultStyle, backgroundColor: `#${appPrimaryColor}` }
|
||||
: defaultStyle;
|
||||
|
||||
return (
|
||||
<div style={{ userSelect: 'none' }}>
|
||||
<Avatar size={size} style={avatarStyle} src={avatarSrc} icon={iconContent}>
|
||||
{avatarContent}
|
||||
</Avatar>
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div style={{ userSelect: "none" }}>
|
||||
<Avatar
|
||||
size={size}
|
||||
style={avatarStyle}
|
||||
src={avatarSrc}
|
||||
icon={iconContent}
|
||||
>
|
||||
{avatarContent}
|
||||
</Avatar>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MyUserAvatar;
|
||||
|
|
Loading…
Reference in New Issue