From c40cba88d4e467d9e1c1919eb5bf9acaf2ae3759 Mon Sep 17 00:00:00 2001 From: alex Date: Sun, 15 Sep 2024 00:25:54 +0200 Subject: [PATCH] i18n --- package-lock.json | 111 +++ package.json | 3 + public/locales/de/translation.json | 225 ++++++ public/locales/en/translation.json | 225 ++++++ src/core/components/Header/index.tsx | 277 ++++--- src/core/components/Header/styles.module.css | 2 +- src/core/components/SideMenu/index.tsx | 82 +- src/core/types/lesson.ts | 2 + src/features/AiChat/index.tsx | 160 ++-- src/features/AiChat/style.module.css | 19 + src/features/ContactSupport/index.tsx | 14 +- src/features/Lessons/LessonPage/index.tsx | 12 +- .../Lessons/LessonPageEditor/Droppable.tsx | 132 ++-- .../LessonPageEditor/SortableEditorItem.tsx | 3 +- .../Lessons/LessonPageEditor/index.tsx | 7 + src/features/Lessons/Questions/index.tsx | 49 +- src/features/Lessons/converter.tsx | 734 +++++++++--------- src/features/Lessons/index.tsx | 49 +- src/features/Lessons/lessonsSlice.ts | 8 +- src/features/Roles/index.tsx | 42 +- src/features/Settings/index.tsx | 117 ++- src/features/SuggestFeature/index.tsx | 11 +- src/features/Team/CreateUser/index.tsx | 69 +- src/features/Team/index.tsx | 57 +- src/features/UserProfile/index.tsx | 104 ++- src/features/WhatsNew/index.tsx | 16 +- src/i18n.ts | 18 + src/index.tsx | 1 + src/shared/components/MyEmpty/index.tsx | 19 +- .../components/MyLessonPreviewCard/index.tsx | 6 +- src/shared/components/MyResult/index.tsx | 7 +- src/shared/components/MyUserAvatar/index.tsx | 69 +- 32 files changed, 1855 insertions(+), 795 deletions(-) create mode 100644 public/locales/de/translation.json create mode 100644 public/locales/en/translation.json create mode 100644 src/features/AiChat/style.module.css create mode 100644 src/i18n.ts diff --git a/package-lock.json b/package-lock.json index bb9e555..ba28672 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 1b686f5..910949b 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/public/locales/de/translation.json b/public/locales/de/translation.json new file mode 100644 index 0000000..17dae31 --- /dev/null +++ b/public/locales/de/translation.json @@ -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" + } +} diff --git a/public/locales/en/translation.json b/public/locales/en/translation.json new file mode 100644 index 0000000..a227a1c --- /dev/null +++ b/public/locales/en/translation.json @@ -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" + } +} diff --git a/src/core/components/Header/index.tsx b/src/core/components/Header/index.tsx index 01efbad..27a58a2 100644 --- a/src/core/components/Header/index.tsx +++ b/src/core/components/Header/index.tsx @@ -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 ( - + +
- -
- {isCollpased ? ( -
dispatch(setIsSideMenuCollapsed(false))}> - -
- ) : ( -
dispatch(setIsSideMenuCollapsed(true))}> - -
- )} -
- - {props.backTo && ( - - - - Back - - - )} -
- - dispatch(setIsSideMenuCollapsed(false))} > - {props.onView && ( -
- -
- )} + +
+ ) : ( +
dispatch(setIsSideMenuCollapsed(true))} + > + +
+ )} + - {props.onEdit && ( -
- -
- )} + {props.backTo && ( + + )} +
- {isDarkMode ? ( -
dispatch(setDarkMode(false))}> - -
- ) : ( -
dispatch(setDarkMode(true))}> - -
- )} - , - onClick: () => navigate(Constants.ROUTE_PATHS.ACCOUNT_SETTINGS), - }, - { - key: '2', - label: 'Logout', - icon: , - danger: true, - onClick: () => { - webSocketService.disconnect(); - window.localStorage.removeItem('session'); - window.location.href = '/'; - }, - }, - ], - }} - > -
- -
-
-
- - ); + + {props.onView && ( +
+ +
+ )} - /* return ( + {props.onEdit && ( +
+ +
+ )} + + {isDarkMode ? ( +
dispatch(setDarkMode(false))} + > + +
+ ) : ( +
dispatch(setDarkMode(true))} + > + +
+ )} + , + onClick: () => navigate(Constants.ROUTE_PATHS.ACCOUNT_SETTINGS), + }, + { + key: "2", + label: t("header.logout"), + icon: , + danger: true, + onClick: () => { + webSocketService.disconnect(); + window.localStorage.removeItem("session"); + window.location.href = "/"; + }, + }, + ], + }} + > +
+ +
+
+
+ + ); + + /* return (
= { 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: , }); overviewGroup.children.push({ key: Constants.ROUTE_PATHS.LESSIONS.ROOT, - label: "Lessons", + label: t("sideMenu.lessons"), icon: , }); } @@ -90,7 +92,7 @@ export function SideMenuContent() { let organizationGroup: ItemType = { 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: , }); organizationGroup.children.push({ key: Constants.ROUTE_PATHS.ORGANIZATION_ROLES, - label: "Roles", + label: t("sideMenu.roles"), icon: , }); organizationGroup.children.push({ key: Constants.ROUTE_PATHS.ORGANIZATION_SETTINGS, - label: "Settings", + label: t("sideMenu.settings"), icon: , }); } @@ -127,7 +129,7 @@ export function SideMenuContent() { items.push({ key: Constants.ROUTE_PATHS.WHATS_NEW, - label: "What's New", + label: t("sideMenu.whatsNew"), icon: , }); @@ -135,7 +137,7 @@ export function SideMenuContent() { items.push({ key: Constants.ROUTE_PATHS.SUGGEST_FEATURE, - label: "Suggest a Feature", + label: t("sideMenu.suggestFeature"), icon: , }); @@ -143,7 +145,7 @@ export function SideMenuContent() { items.push({ key: Constants.ROUTE_PATHS.CONTACT_SUPPORT, - label: "Contact Support", + label: t("sideMenu.contactSupport"), icon: , }); @@ -264,6 +266,7 @@ export function SideMenuContent() { export function SideMenuEditorContent() { const location = useLocation(); + const { t } = useTranslation(); const [isDragging, setIsDragging] = useState(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) => (
- {group.category} + {lessonsComponentsCategories[group.category]}
- + @@ -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 ( { 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 }) {
) : null} - {component.name} + { + { + ...lessonsCommonComponents, + ...lessonsMediaComponents, + }[component.name] + } ); } - -//console.log("insert component", component.type); -//dispatch(addLessonContent(component.type)); diff --git a/src/core/types/lesson.ts b/src/core/types/lesson.ts index 4fdee43..daa3e4d 100644 --- a/src/core/types/lesson.ts +++ b/src/core/types/lesson.ts @@ -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 diff --git a/src/features/AiChat/index.tsx b/src/features/AiChat/index.tsx index 21a6a76..a0e72e2 100644 --- a/src/features/AiChat/index.tsx +++ b/src/features/AiChat/index.tsx @@ -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 ? ( -
- { - console.log("onMessagee", message); - }} - > -
- ) : null} + return ( + <> + {visible ? ( +
+ { + let _response = { ...response }; - } - 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 `${token.text}`; + }, + }, + ]) + ) + .parse(text); + + return _response; + }} + > +
+ ) : null} + + } + type="primary" + onClick={() => console.log('onClick')} + style={{ zIndex: 10000 }} + onClickCapture={() => { + setVisible(!visible); + }} + /> + + ); } export default AiChat; diff --git a/src/features/AiChat/style.module.css b/src/features/AiChat/style.module.css new file mode 100644 index 0000000..efed1c1 --- /dev/null +++ b/src/features/AiChat/style.module.css @@ -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; +} diff --git a/src/features/ContactSupport/index.tsx b/src/features/ContactSupport/index.tsx index 09fd718..1db419f 100644 --- a/src/features/ContactSupport/index.tsx +++ b/src/features/ContactSupport/index.tsx @@ -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 ( <> - + } + /> - + - If you have any questions or need help, please contact us at the - following e-mail address: + {t("contactSupport.paragraph")} diff --git a/src/features/Lessons/LessonPage/index.tsx b/src/features/Lessons/LessonPage/index.tsx index 6dea7f6..01b89a8 100644 --- a/src/features/Lessons/LessonPage/index.tsx +++ b/src/features/Lessons/LessonPage/index.tsx @@ -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 /> - diff --git a/src/features/Lessons/LessonPageEditor/Droppable.tsx b/src/features/Lessons/LessonPageEditor/Droppable.tsx index 86890cd..506b043 100644 --- a/src/features/Lessons/LessonPageEditor/Droppable.tsx +++ b/src/features/Lessons/LessonPageEditor/Droppable.tsx @@ -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 ( - { - setIsDragging(true); - }} - onDragEnd={handleDragEnd} - > - -
- {items.map((item) => ( - - ))} -
-
- {isDragging ? : null} -
- ); + return ( + { + setIsDragging(true); + }} + onDragEnd={handleDragEnd} + > + +
+ {items.map((item) => ( + + ))} +
+
+ + {isDragging ? : null} + +
+ ); }; /* function handleDragEnd(event: DragEndEvent) { diff --git a/src/features/Lessons/LessonPageEditor/SortableEditorItem.tsx b/src/features/Lessons/LessonPageEditor/SortableEditorItem.tsx index b8e6c48..5597c53 100644 --- a/src/features/Lessons/LessonPageEditor/SortableEditorItem.tsx +++ b/src/features/Lessons/LessonPageEditor/SortableEditorItem.tsx @@ -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 { diff --git a/src/features/Lessons/LessonPageEditor/index.tsx b/src/features/Lessons/LessonPageEditor/index.tsx index b12a587..48ec02a 100644 --- a/src/features/Lessons/LessonPageEditor/index.tsx +++ b/src/features/Lessons/LessonPageEditor/index.tsx @@ -52,6 +52,12 @@ const PreviewCard: React.FC = () => { dispatch(setLessonThumbnailUrl(data.ThumbnailUrl)); }, [data]); + useEffect(() => { + addWebSocketReconnectListener(refetch); + + return () => removeWebSocketReconnectListener(refetch); + }, []); + if (error) return ; return ( @@ -150,6 +156,7 @@ export default function LessonPageEditor() { ) ) } + sticky /> diff --git a/src/features/Lessons/Questions/index.tsx b/src/features/Lessons/Questions/index.tsx index 54b5e11..b60702f 100644 --- a/src/features/Lessons/Questions/index.tsx +++ b/src/features/Lessons/Questions/index.tsx @@ -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} > @@ -71,7 +76,7 @@ const CreateQuestionForm: React.FC = () => { @@ -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 ( - Questions + + {t("lessonQuestions.questions")} + @@ -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); const [isSendingReply, setIsSendingReply] = useState(false); @@ -324,7 +336,9 @@ export function QuestionUIRaw({ }, 100); }} > - {replyFormVisible ? "Hide" : "Reply"} + {replyFormVisible + ? t("lessonQuestions.hide") + : t("lessonQuestions.reply")} {replyFormVisible ? ( @@ -344,12 +358,17 @@ export function QuestionUIRaw({ > setReplyMessage(e.target.value)} autoSize={{ minRows: 2 }} maxLength={5000} @@ -361,7 +380,7 @@ export function QuestionUIRaw({ loading={isSendingReply} htmlType="submit" > - Reply + {t("lessonQuestions.reply")} diff --git a/src/features/Lessons/converter.tsx b/src/features/Lessons/converter.tsx index ebe5b1f..3ac82a6 100644 --- a/src/features/Lessons/converter.tsx +++ b/src/features/Lessons/converter.tsx @@ -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); - const isDarkMode = useSelector(darkMode); + const lessonId = useSelector(currentLessonId); + const isDarkMode = useSelector(darkMode); - const [reqUpdateLessonContent] = useUpdateLessonContentMutation(); + const [reqUpdateLessonContent] = useUpdateLessonContentMutation(); - const debounceRef = useRef(null); + switch (lessonContent.Type) { + case getTypeByName("Header"): + if (mode === "view") { + return ( +
+ {lessonContent.Data} +
+ ); + } - switch (lessonContent.Type) { - case getTypeByName('Header'): - if (mode === 'view') { - return
{lessonContent.Data}
; + return ( + { + 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} + + ); + case getTypeByName("Text"): + if (mode === "view") { + const formattedText = lessonContent.Data.split("\n").map( + (line, index) =>
  • {line || "\u00A0"}
  • + ); + + return ( +
      + {formattedText} +
    + ); + } + + return ( + { + onEdit?.(event.target.value); + + if (debounceRef.current) { + clearTimeout(debounceRef.current); } - return ( - { - 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 ( +
    +
    + {t("lessonComponentsConverter.noImageProvided")} +
    +
    + ); + } - try { - reqUpdateLessonContent({ - lessonId: lessonId, - contentId: lessonContent.Id, - data: event, - }); - } catch (err) { - console.error(err); - } - }, - }} - level={1} - style={{ - margin: 0, - width: '100%', - }} - > - {lessonContent.Data} -
    - ); - case getTypeByName('Text'): - if (mode === 'view') { - const formattedText = lessonContent.Data.split('\n').map((line, index) =>
  • {line || '\u00A0'}
  • ); + const GalleryUpload = () => { + return ( + { + if (info.file.status === "done") { + onEdit?.(info.file.response.Data); + } + }} + imgCropProps={{ + aspect: 5 / 4, + children: <>, + }} + > + + + ); + }; - return ( -
      - {formattedText} -
    - ); - } + if (lessonContent.Data === "") { + return ( +
    +
    + {t("lessonComponentsConverter.chooseImageFrom")} - return ( - { - console.log('edit'); + +
    +
    + ); + } - onEdit?.(event.target.value); + return ( + + img - if (debounceRef.current) { - clearTimeout(debounceRef.current); - } + {mode === "edititable" && ( + +
    + + {t("lessonComponentsConverter.chooseAnotherImageFrom")} + + +
    +
    + )} +
    + ); + 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 ( + + - if (mode === 'view' && lessonContent.Data === '') { - return ( -
    -
    - No image provided -
    -
    - ); - } + {mode === "edititable" && ( + <> + + {t("lessonComponentsConverter.videoId")} + + { + onEdit?.(event.target.value); - const GalleryUpload = () => { - return ( - { - if (info.file.status === 'done') { - console.log('done'); - onEdit?.(info.file.response.Data); - } - }} - imgCropProps={{ - aspect: 5 / 4, - children: <>, - }} - > - - - ); - }; + if (debounceRef.current) clearTimeout(debounceRef.current); - if (lessonContent.Data === '') { - return ( -
    -
    - Choose image from + 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); + }} + /> + + )} +
    + ); + case getTypeByName("Video"): + if (mode === "view" && lessonContent.Data === "") { + return ( +
    +
    + {t("lessonComponentsConverter.noVideoProvided")} +
    +
    + ); + } - return ( - - img + const VideoUpload = () => { + return ( + { + if (info.file.status === "done") { + onEdit?.(info.file.response.Data); + } + }} + accept={Constants.ACCEPTED_VIDEO_FILE_TYPES} + > + + + ); + }; - {mode === 'edititable' && ( - -
    - Choose another image from - -
    -
    - )} -
    - ); - case getTypeByName('YouTube'): - const videoId = extractVideoId(lessonContent.Data); + if (lessonContent.Data === "") { + return ( +
    +
    + {t("lessonComponentsConverter.chooseVideoFrom")} - console.log('videoId', videoId); + +
    +
    + ); + } - return ( - - + return ( + + + + + - {mode === 'edititable' && ( - <> - Video ID - { - console.warn('edit', event.target.value, videoId); + {mode === "edititable" && ( + +
    + + {t("lessonComponentsConverter.chooseAnotherVideoFrom")} + + +
    +
    + )} +
    + ); - onEdit?.(event.target.value); + case getTypeByName("Iframe"): + return ( +
    + {t("lessonComponentsConverter.notImplemented")} +
    + ); + case getTypeByName("Banner"): + return ( +
    + {t("lessonComponentsConverter.notImplemented")} +
    + ); - 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); - }} - /> - - )} -
    - ); - case getTypeByName('Video'): - if (mode === 'view' && lessonContent.Data === '') { - return ( -
    -
    - No video provided -
    -
    - ); - } - - const VideoUpload = () => { - return ( - { - if (info.file.status === 'done') { - console.log('done'); - onEdit?.(info.file.response.Data); - } - }} - accept={Constants.ACCEPTED_VIDEO_FILE_TYPES} - > - - - ); - }; - - if (lessonContent.Data === '') { - return ( -
    -
    - Choose video from - - -
    -
    - ); - } - - return ( - - - - - - - {mode === 'edititable' && ( - -
    - Choose another video from - -
    -
    - )} -
    - ); - - case getTypeByName('Iframe'): - return
    Not implemented
    ; - case getTypeByName('Banner'): - return
    Not implemented
    ; - - default: - return
    Unknown type
    ; - } + default: + return
    {t("lessonComponentsConverter.unkownType")}
    ; + } } diff --git a/src/features/Lessons/index.tsx b/src/features/Lessons/index.tsx index f82fefd..f8a670e 100644 --- a/src/features/Lessons/index.tsx +++ b/src/features/Lessons/index.tsx @@ -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")} ); }; 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 ; - 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 && ( <> - Unpublished + {t("lessons.lessonStatusDrafts")} {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 ( <> - } /> + } /> { + dispatch(setSearchFilter(e.target.value)); + }} style={{ width: 300 }} allowClear /> diff --git a/src/features/Lessons/lessonsSlice.ts b/src/features/Lessons/lessonsSlice.ts index a25438b..bfcb141 100644 --- a/src/features/Lessons/lessonsSlice.ts +++ b/src/features/Lessons/lessonsSlice.ts @@ -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; diff --git a/src/features/Roles/index.tsx b/src/features/Roles/index.tsx index c04aae1..1c632e4 100644 --- a/src/features/Roles/index.tsx +++ b/src/features/Roles/index.tsx @@ -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 ( <> - } /> + } + /> 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" > - - {user.FirstName[0]} - +
    + +
    ))} diff --git a/src/features/Settings/index.tsx b/src/features/Settings/index.tsx index 4a214ad..5998453 100644 --- a/src/features/Settings/index.tsx +++ b/src/features/Settings/index.tsx @@ -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 ( <> - } /> + } + /> @@ -69,6 +76,7 @@ function GeneralCard({ data, isLoading, }: { data?: OrganizationSettings; isLoading?: boolean } = {}) { + const { t } = useTranslation(); const [form] = useForm(); 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={
    diff --git a/src/features/Team/index.tsx b/src/features/Team/index.tsx index de5180b..623b75a 100644 --- a/src/features/Team/index.tsx +++ b/src/features/Team/index.tsx @@ -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) => ( { 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={ i18n.changeLanguage(value)} + style={{ width: 120 }} + > + + {t("userProfile.languageEnglish")} + + + {t("userProfile.languageGerman")} + + +
    +
    + ), }, { key: "2", - label: "Last name", + label: t("common.lastName"), children: ( ), }, { key: "3", - label: "Email", + label: t("common.email"), children: , }, ]} diff --git a/src/features/WhatsNew/index.tsx b/src/features/WhatsNew/index.tsx index be07f20..3b837b4 100644 --- a/src/features/WhatsNew/index.tsx +++ b/src/features/WhatsNew/index.tsx @@ -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 ( - <> -

    WhatsNew

    - - ); + const { t } = useTranslation(); + + return ( + <> + } /> + + ); } diff --git a/src/i18n.ts b/src/i18n.ts new file mode 100644 index 0000000..2ff1b74 --- /dev/null +++ b/src/i18n.ts @@ -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; diff --git a/src/index.tsx b/src/index.tsx index e0aa384..8f25a18 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -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( diff --git a/src/shared/components/MyEmpty/index.tsx b/src/shared/components/MyEmpty/index.tsx index 567730b..4db8933 100644 --- a/src/shared/components/MyEmpty/index.tsx +++ b/src/shared/components/MyEmpty/index.tsx @@ -1,5 +1,18 @@ import { Empty } from "antd"; +import { useTranslation } from "react-i18next"; -export default function MyEmpty() { - return -} \ No newline at end of file +interface MyEmptyProps extends React.ComponentProps { + children?: React.ReactNode; +} + +const MyEmpty: React.FC = ({ children, ...props }) => { + const { t } = useTranslation(); + + return ( + + {children} + + ); +}; + +export default MyEmpty; diff --git a/src/shared/components/MyLessonPreviewCard/index.tsx b/src/shared/components/MyLessonPreviewCard/index.tsx index 4adc689..8687259 100644 --- a/src/shared/components/MyLessonPreviewCard/index.tsx +++ b/src/shared/components/MyLessonPreviewCard/index.tsx @@ -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" ? (
    {lessonSettings.Title}
    - 12 comments + {lessonSettings.QuestionsCount}{" "} + {t("lessonPage.questions")}
    ) : ( ); } diff --git a/src/shared/components/MyUserAvatar/index.tsx b/src/shared/components/MyUserAvatar/index.tsx index 070971b..413b0bc 100644 --- a/src/shared/components/MyUserAvatar/index.tsx +++ b/src/shared/components/MyUserAvatar/index.tsx @@ -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 = ({ size = 56, firstName, profilePictureUrl, disableCursorPointer }) => { - const appPrimaryColor = useSelector(primaryColor); +const MyUserAvatar: React.FC = ({ + 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 ? : 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 ? ( + + ) : undefined; + const avatarSrc = isProfilePictureEmpty + ? undefined + : `${Constants.STATIC_CONTENT_ADDRESS}/${profilePictureUrl}`; + const avatarStyle = isProfilePictureEmpty + ? { ...defaultStyle, backgroundColor: `#${appPrimaryColor}` } + : defaultStyle; - return ( -
    - - {avatarContent} - -
    - ); + return ( +
    + + {avatarContent} + +
    + ); }; export default MyUserAvatar;