diff --git a/Makefile b/Makefile
index 45f87d1d6..7b10a10af 100644
--- a/Makefile
+++ b/Makefile
@@ -30,4 +30,9 @@ trivy:
docker build -f ./backend/Dockerfile -t retrospected/backend:trivy ./backend
docker build -f ./frontend/Dockerfile -t retrospected/frontend:trivy ./frontend
trivy retrospected/backend:trivy
- trivy retrospected/frontend:trivy
\ No newline at end of file
+ trivy retrospected/frontend:trivy
+
+translate:
+ crowdin push sources
+ crowdin pre-translate --method=mt --engine-id=316468 -l=fr -l=nl -l=ar -l=de -l=it -l=ja -l=uk
+ crowdin download
\ No newline at end of file
diff --git a/backend/src/common/payloads.ts b/backend/src/common/payloads.ts
index a0041f009..38ef010ac 100644
--- a/backend/src/common/payloads.ts
+++ b/backend/src/common/payloads.ts
@@ -80,3 +80,7 @@ export interface DeleteAccountPayload {
export interface ChatMessagePayload {
content: string;
}
+
+export interface ChangeUserNamePayload {
+ name: string;
+}
diff --git a/backend/src/db/actions/users.ts b/backend/src/db/actions/users.ts
index e09745aa7..81df929d4 100644
--- a/backend/src/db/actions/users.ts
+++ b/backend/src/db/actions/users.ts
@@ -205,7 +205,7 @@ export async function registerUser(
identity.password = registration.password || null;
identity.emailVerification = registration.emailVerification || null;
- user.name = registration.name;
+ user.name = user.name || registration.name;
user.slackUserId = registration.slackUserId || null;
user.slackTeamId = registration.slackTeamId || null;
user.photo = registration.photo || user.photo;
diff --git a/backend/src/index.ts b/backend/src/index.ts
index 53242f121..94419a4fb 100644
--- a/backend/src/index.ts
+++ b/backend/src/index.ts
@@ -37,6 +37,7 @@ import {
CreateSessionPayload,
SelfHostedCheckPayload,
DeleteAccountPayload,
+ ChangeUserNamePayload,
} from './common';
import registerPasswordUser from './auth/register/register-user';
import { sendVerificationEmail, sendResetPassword } from './email/emailSender';
@@ -294,6 +295,24 @@ db().then(() => {
}
});
+ app.post('/api/me/username', async (req, res) => {
+ const user = await getUserViewFromRequest(req);
+ if (!user) {
+ return res.status(401).send('Please login');
+ }
+ const payload = req.body as ChangeUserNamePayload;
+ const success = await updateUser(user.id, { name: payload.name });
+ if (success) {
+ const updated = await getUserView(user.identityId);
+ if (updated) {
+ return res.send(updated.toJson());
+ }
+ }
+ return res
+ .status(500)
+ .send('Something went wrong while updating the user name');
+ });
+
app.delete('/api/me', heavyLoadLimiter, async (req, res) => {
const user = await getUserViewFromRequest(req);
if (user) {
diff --git a/frontend/src/Layout.tsx b/frontend/src/Layout.tsx
index caa38e8c5..6f9fac7eb 100644
--- a/frontend/src/Layout.tsx
+++ b/frontend/src/Layout.tsx
@@ -177,11 +177,12 @@ function App() {
) : null}
-
+
-
- {t('Main.helpUkraine')}
-
+ {t('Main.helpUkraine')}
@@ -244,19 +245,16 @@ const Spacer = styled.div`
flex: 1;
`;
-const HelpUkraine = styled.div`
+const HelpUkraine = styled.a`
display: flex;
align-items: center;
justify-content: center;
margin: 0 20px;
- a {
- font-style: unset;
- text-decoration: unset;
- font-size: 1.2rem;
- font-weight: 100;
- color: #0057b7;
- }
- border: 1px solid #0057b7;
+ font-style: unset;
+ text-decoration: unset;
+ font-size: 1.2rem;
+ font-weight: 100;
+ color: #0057b7;
border-radius: 5px;
padding: 10px;
backdrop-filter: blur(10px);
diff --git a/frontend/src/common/payloads.ts b/frontend/src/common/payloads.ts
index a0041f009..38ef010ac 100644
--- a/frontend/src/common/payloads.ts
+++ b/frontend/src/common/payloads.ts
@@ -80,3 +80,7 @@ export interface DeleteAccountPayload {
export interface ChatMessagePayload {
content: string;
}
+
+export interface ChangeUserNamePayload {
+ name: string;
+}
diff --git a/frontend/src/common/types.ts b/frontend/src/common/types.ts
index 6812799f3..a21cab1b8 100644
--- a/frontend/src/common/types.ts
+++ b/frontend/src/common/types.ts
@@ -130,7 +130,7 @@ export interface FullUser extends User {
username: string | null;
accountType: AccountType;
photo: string | null;
- language: string;
+ language: string | null;
email: string | null;
canDeleteSession: boolean;
stripeId: string | null;
diff --git a/frontend/src/translations/LanguageProvider.tsx b/frontend/src/translations/LanguageProvider.tsx
index 35574a41f..5ef780076 100644
--- a/frontend/src/translations/LanguageProvider.tsx
+++ b/frontend/src/translations/LanguageProvider.tsx
@@ -8,7 +8,7 @@ export default function LanguageProvider({ children }: PropsWithChildren<{}>) {
useEffect(() => {
if (user) {
- i18n.changeLanguage(user.language);
+ i18n.changeLanguage(user.language || 'en-GB');
}
}, [user, i18n]);
diff --git a/frontend/src/translations/locales/ar-SA.json b/frontend/src/translations/locales/ar-SA.json
index 819ecfb6f..eef728766 100644
--- a/frontend/src/translations/locales/ar-SA.json
+++ b/frontend/src/translations/locales/ar-SA.json
@@ -266,6 +266,7 @@
"cancelButton": "لا شكراً"
},
"AccountPage": {
+ "noEmptyNameError": "لا يمكنك اختيار اسم عرض فارغ. الرجاء المحاولة مرة أخرى.",
"anonymousError": "الحسابات المجهولة المصدر لا يمكن الوصول إلى ملفها الشخصي (لأنها لا تملك ملفاً).",
"details": {
"header": "التفاصيل الخاصة بك",
diff --git a/frontend/src/translations/locales/de-DE.json b/frontend/src/translations/locales/de-DE.json
index baabe0011..c1a6a2266 100644
--- a/frontend/src/translations/locales/de-DE.json
+++ b/frontend/src/translations/locales/de-DE.json
@@ -266,6 +266,7 @@
"cancelButton": "Nein danke"
},
"AccountPage": {
+ "noEmptyNameError": "Sie können keinen leeren Anzeigenamen auswählen. Bitte versuchen Sie es erneut.",
"anonymousError": "Anonyme Konten können keinen Zugriff auf ihr Profil haben (weil sie kein Profil haben).",
"details": {
"header": "Ihre Details",
diff --git a/frontend/src/translations/locales/en-GB.json b/frontend/src/translations/locales/en-GB.json
index 2726e3218..18e2c2b34 100644
--- a/frontend/src/translations/locales/en-GB.json
+++ b/frontend/src/translations/locales/en-GB.json
@@ -266,6 +266,7 @@
"cancelButton": "No thanks"
},
"AccountPage": {
+ "noEmptyNameError": "You cannot choose an empty display name. Please try again.",
"anonymousError": "Anonymous accounts cannot have access to their profile (because they don't have one).",
"details": {
"header": "Your Details",
diff --git a/frontend/src/translations/locales/es-ES.json b/frontend/src/translations/locales/es-ES.json
index 4b9605d8b..2672b06ba 100644
--- a/frontend/src/translations/locales/es-ES.json
+++ b/frontend/src/translations/locales/es-ES.json
@@ -266,6 +266,7 @@
"cancelButton": "No gracias"
},
"AccountPage": {
+ "noEmptyNameError": "You cannot choose an empty display name. Please try again.",
"anonymousError": "Las cuentas anónimas no pueden tener acceso a su perfil (porque no tienen una).",
"details": {
"header": "Tus detalles",
diff --git a/frontend/src/translations/locales/fr-FR.json b/frontend/src/translations/locales/fr-FR.json
index 6829cee14..c6a76e25b 100644
--- a/frontend/src/translations/locales/fr-FR.json
+++ b/frontend/src/translations/locales/fr-FR.json
@@ -12,7 +12,7 @@
},
"Main": {
"hint": "Vous pouvez inviter d'autres participants en leur envoyant l'URL de cette page",
- "helpUkraine": "Aidez l'Ukraine!",
+ "helpUkraine": "Aidez l'Ukraine !",
"loading": "Chargement en cours...",
"unlicenced": {
"title": "Retrospected est sans licence"
@@ -30,7 +30,7 @@
"votes_one": "vote",
"votes_other": "votes",
"actions_one": "action",
- "actions_other": "action"
+ "actions_other": "actions"
},
"Column": {
"createGroupTooltip": "Créer un groupe"
@@ -202,11 +202,11 @@
},
"Register": {
"header": "S'enregistrer",
- "info": "Enregistrez un nouveau compte Retrospected.",
+ "info": "Enregistrez un nouveau compte Retrospected !",
"registerButton": "Créer un compte",
"errorAlreadyRegistered": "Désolé, cet email est déjà enregistré",
"errorGeneral": "Une erreur s'est produite lors de la tentative de création de votre compte.",
- "messageSuccess": "Merci! Vous devriez recevoir un e-mail sous peu pour valider votre compte.",
+ "messageSuccess": "Merci ! Vous devriez recevoir un e-mail sous peu pour valider votre compte.",
"errorInvalidEmail": "Merci d'entrer un email valide"
},
"ValidateAccount": {
@@ -237,7 +237,7 @@
"inviteButton": "Inviter",
"dialog": {
"title": "Invitez des participants à votre retrospective",
- "text": "Pour inviter des participants à votre session retrospected, envoyez leur le lien suivant",
+ "text": "Pour inviter des participants à votre session Retrospected, envoyez leur le lien suivant",
"copyButton": "Copier"
}
},
@@ -253,20 +253,21 @@
},
"DeleteSession": {
"header": "Supprimer \"{{name}}\" ?",
- "firstLine": "Effacer une session est irreversible. Tout les posts, groupes, votes et la session elle-même vont être effacés. Les données ne peuvent être récupérée.",
- "secondLine": "Êtes-vous certain(e) de vouloir effaçer cette session et son contenu ?",
+ "firstLine": "Effacer une session est irréversible. Tous les posts, groupes, votes et la session elle-même vont être effacés. Les données ne peuvent être récupérée.",
+ "secondLine": "Êtes-vous certain(e) de vouloir effacer cette session et son contenu ?",
"yesImSure": "Oui, j'en suis sûr",
"cancel": "Non, je me suis trompé(e)"
},
"RevealCards": {
"buttonLabel": "Révéler",
"dialogTitle": "Révéler tous les posts",
- "dialogContent": "Cela va révéler (déflouter) tout les posts. L'opération n'est pas reversible.",
+ "dialogContent": "Cela va révéler (déflouter) tous les posts. L'opération n'est pas réversible.",
"confirmButton": "Révéler",
"cancelButton": "Non merci"
},
"AccountPage": {
- "anonymousError": "Les comptes anonymes ne peuvent avoir accès à leur profile (puisque ils n'en ont pas).",
+ "noEmptyNameError": "Vous ne pouvez pas choisir un nom d'affichage vide. Veuillez réessayer.",
+ "anonymousError": "Les comptes anonymes ne peuvent avoir accès à leur profil (puisqu'ils n'en ont pas).",
"details": {
"header": "Vos Coordonnées",
"username": "Nom d'utilisateur",
@@ -283,13 +284,13 @@
"header": "Votre Abonnement",
"manageButton": "Gérer mon abonnement",
"membersEditor": {
- "title": "Votre Equipe",
- "limitReached": "Vous avez atteint le nombre maximum de membres ({{limit}}) permis par votre abonnement. Vous pouvez passer à l'abonnement Company pour un nombre de collaborateur illimité.",
- "info": "Ajouter des addresses emails ci-dessous pour donner un accès Pro à vos collaborateurs (dans la limite de {{limit}} collaborateurs). Appuyez sur Entrée après chaque email."
+ "title": "Votre Équipe",
+ "limitReached": "Vous avez atteint le nombre maximum de membres ({{limit}}) permis par votre abonnement. Vous pouvez passer à l'abonnement Unlimited pour un nombre de collaborateur illimité.",
+ "info": "Ajouter des adresses emails ci-dessous pour donner un accès Pro à vos collaborateurs (dans la limite de {{limit}} collaborateurs). Appuyez sur Entrée après chaque email."
}
},
"trial": {
- "header": "Votre Periode d'Essai",
+ "header": "Votre Période d'Essai",
"yourTrialWillExpireIn": "Votre période d'essai va se terminer dans {{date}}.",
"subscribe": "S'abonner"
},
@@ -301,7 +302,7 @@
"confirm": {
"title": "Êtes-vous absolument certain(e) ?",
"description": "Cette opération n'est pas réversible !",
- "confirmation": "Oui, je veux effaçer toutes mes données",
+ "confirmation": "Oui, je veux effacer toutes mes données",
"cancellation": "J'ai changé d'avis !"
},
"subheader": "Choisir quoi effacer",
@@ -309,18 +310,18 @@
"recommended": "Recommandé",
"deleteSessions": {
"main": "Effacer les sessions (les rétrospectives) que vous avez créées ?",
- "selected": "Vos sessions, and toutes les données associées (incluant les posts et votes d'autres personnes) seront éffacées de manière permanente et irréversible.",
- "unselected": "Vos sessions seront conservées, et leur auteur deviendra un auteur anonyme."
+ "selected": "Vos sessions, and toutes les données associées (incluant les posts et votes d'autres personnes) seront effacées de manière permanente et irréversible.",
+ "unselected": "Vos sessions seront conservées et leur auteur deviendra un auteur anonyme."
},
"deletePosts": {
- "main": "Effacer les posts que vous avez écris ?",
- "selected": "Vos posts, dans n'importe quelle session, ainsi que les votes associés, seront effacés de manière permanente et irreversible.",
+ "main": "Effacer les posts que vous avez écrit ?",
+ "selected": "Vos posts, dans n'importe quelle session, ainsi que les votes associés, seront effacés de manière permanente et irréversible.",
"unselected": "Vos posts seront conservés, mais leur auteur deviendra un compte anonyme."
},
"deleteVotes": {
"main": "Effacer vos votes ?",
"selected": "Vos votes, dans n'importe quelle session, seront effacés.",
- "unselected": "Vos votes seront conservés, et deviendront anonymes."
+ "unselected": "Vos votes seront conservés et deviendront anonymes."
},
"deleteAccountButton": "SUPPRIMER VOTRE COMPTE",
"cancelButton": "Annuler"
@@ -332,7 +333,7 @@
"alertAlreadySubscribed": "Vous avez déjà un abonnement, vous n'avez peut-être donc pas besoin d'un abonnement supplémentaire.",
"currency": {
"title": "Devise",
- "description": "Choisissez une devise de facturation.",
+ "description": "Choisissez une devise de facturation",
"warning": "Votre compte est déjà en {{currency}}, vous ne pouvez donc plus en changer."
},
"plan": {
diff --git a/frontend/src/translations/locales/hu-HU.json b/frontend/src/translations/locales/hu-HU.json
index 9b2c51117..08e3ca101 100644
--- a/frontend/src/translations/locales/hu-HU.json
+++ b/frontend/src/translations/locales/hu-HU.json
@@ -266,6 +266,7 @@
"cancelButton": "Nem köszönöm"
},
"AccountPage": {
+ "noEmptyNameError": "You cannot choose an empty display name. Please try again.",
"anonymousError": "Az anonim fiókok nem férhetnek hozzá a profiljukhoz (mert nincs ilyenük).",
"details": {
"header": "Az adataid",
diff --git a/frontend/src/translations/locales/it-IT.json b/frontend/src/translations/locales/it-IT.json
index 6f14224e7..b77ef4bf0 100644
--- a/frontend/src/translations/locales/it-IT.json
+++ b/frontend/src/translations/locales/it-IT.json
@@ -266,6 +266,7 @@
"cancelButton": "No grazie"
},
"AccountPage": {
+ "noEmptyNameError": "Non puoi scegliere un nome di visualizzazione vuoto. Per favore riprova.",
"anonymousError": "Gli account anonimi non possono avere accesso al loro profilo (perché non ne hanno uno).",
"details": {
"header": "I Tuoi Dettagli",
diff --git a/frontend/src/translations/locales/ja-JP.json b/frontend/src/translations/locales/ja-JP.json
index cb3c5166d..0dea814ba 100644
--- a/frontend/src/translations/locales/ja-JP.json
+++ b/frontend/src/translations/locales/ja-JP.json
@@ -266,6 +266,7 @@
"cancelButton": "いいえ結構です"
},
"AccountPage": {
+ "noEmptyNameError": "空の表示名を選択することはできません。もう一度やり直してください。",
"anonymousError": "匿名のアカウントはプロフィールへのアクセス権を持つことはできません(そのアカウントがないので)",
"details": {
"header": "詳細",
diff --git a/frontend/src/translations/locales/nl-NL.json b/frontend/src/translations/locales/nl-NL.json
index a4e59adaf..bd94aa814 100644
--- a/frontend/src/translations/locales/nl-NL.json
+++ b/frontend/src/translations/locales/nl-NL.json
@@ -266,6 +266,7 @@
"cancelButton": "Nee, bedankt"
},
"AccountPage": {
+ "noEmptyNameError": "U kunt geen lege weergavenaam kiezen. Probeer het opnieuw.",
"anonymousError": "Anonieme accounts kunnen geen toegang hebben tot hun profiel (omdat ze geen account hebben).",
"details": {
"header": "Uw gegevens",
diff --git a/frontend/src/translations/locales/pl-PL.json b/frontend/src/translations/locales/pl-PL.json
index 76902895e..872cc0841 100644
--- a/frontend/src/translations/locales/pl-PL.json
+++ b/frontend/src/translations/locales/pl-PL.json
@@ -266,6 +266,7 @@
"cancelButton": "Nie, dziękuję"
},
"AccountPage": {
+ "noEmptyNameError": "You cannot choose an empty display name. Please try again.",
"anonymousError": "Anonimowe konta nie mogą mieć dostępu do swojego profilu (ponieważ nie mają nich).",
"details": {
"header": "Twoje dane",
diff --git a/frontend/src/translations/locales/pt-BR.json b/frontend/src/translations/locales/pt-BR.json
index f95849424..067d354ba 100644
--- a/frontend/src/translations/locales/pt-BR.json
+++ b/frontend/src/translations/locales/pt-BR.json
@@ -266,6 +266,7 @@
"cancelButton": "Não, obrigado."
},
"AccountPage": {
+ "noEmptyNameError": "You cannot choose an empty display name. Please try again.",
"anonymousError": "Contas anônimas não podem ter acesso ao seu perfil (porque não têm uma).",
"details": {
"header": "Suas informações",
diff --git a/frontend/src/translations/locales/pt-PT.json b/frontend/src/translations/locales/pt-PT.json
index 6cf9bfbdc..63b047eb5 100644
--- a/frontend/src/translations/locales/pt-PT.json
+++ b/frontend/src/translations/locales/pt-PT.json
@@ -266,6 +266,7 @@
"cancelButton": "Não, obrigado."
},
"AccountPage": {
+ "noEmptyNameError": "You cannot choose an empty display name. Please try again.",
"anonymousError": "Contas anônimas não podem ter acesso ao seu perfil (porque não têm uma).",
"details": {
"header": "Suas informações",
diff --git a/frontend/src/translations/locales/uk-UA.json b/frontend/src/translations/locales/uk-UA.json
index e2107653e..c7cfadcbe 100644
--- a/frontend/src/translations/locales/uk-UA.json
+++ b/frontend/src/translations/locales/uk-UA.json
@@ -266,6 +266,7 @@
"cancelButton": "Ні, дякую"
},
"AccountPage": {
+ "noEmptyNameError": "Не можна вибрати пусте ім'я дисплея. Будь ласка, спробуйте ще раз.",
"anonymousError": "Анонімні акаунти не можуть мати доступу до свого профілю (адже вони не мають).",
"details": {
"header": "Ваші дані",
diff --git a/frontend/src/translations/locales/zh-CN.json b/frontend/src/translations/locales/zh-CN.json
index 5323fa2ea..575ac94bd 100644
--- a/frontend/src/translations/locales/zh-CN.json
+++ b/frontend/src/translations/locales/zh-CN.json
@@ -266,6 +266,7 @@
"cancelButton": "不要谢谢。"
},
"AccountPage": {
+ "noEmptyNameError": "You cannot choose an empty display name. Please try again.",
"anonymousError": "匿名帐户不能访问他们的个人资料 (因为他们没有一个)。",
"details": {
"header": "您的详细信息",
diff --git a/frontend/src/translations/locales/zh-TW.json b/frontend/src/translations/locales/zh-TW.json
index 497e53212..0b3409d47 100644
--- a/frontend/src/translations/locales/zh-TW.json
+++ b/frontend/src/translations/locales/zh-TW.json
@@ -266,6 +266,7 @@
"cancelButton": "不,謝謝"
},
"AccountPage": {
+ "noEmptyNameError": "You cannot choose an empty display name. Please try again.",
"anonymousError": "匿名帳戶無法訪問他們的個人資料(因為他們沒有個人資料)。",
"details": {
"header": "你的資料",
diff --git a/frontend/src/views/account/AccountPage.tsx b/frontend/src/views/account/AccountPage.tsx
index e4422e396..e1412124b 100644
--- a/frontend/src/views/account/AccountPage.tsx
+++ b/frontend/src/views/account/AccountPage.tsx
@@ -14,17 +14,39 @@ import TrialPrompt from '../home/TrialPrompt';
import useFormatDate from '../../hooks/useFormatDate';
import { DeleteModal } from './delete/DeleteModal';
import useModal from '../../hooks/useModal';
+import EditableLabel from 'components/EditableLabel';
+import { useCallback, useContext } from 'react';
+import { updateUserName } from './api';
+import UserContext from 'auth/Context';
+import { useSnackbar } from 'notistack';
function AccountPage() {
const url = usePortalUrl();
const user = useUser();
+ const { setUser } = useContext(UserContext);
const isTrial = useIsTrial();
const formatDistanceToNow = useFormatDate();
const navigate = useNavigate();
const { t } = useTranslation();
+ const { enqueueSnackbar } = useSnackbar();
const [deleteModalOpen, handleDeleteModalOpen, handleDeleteModalClose] =
useModal();
+ const handleEditName = useCallback(
+ async (name: string) => {
+ const trimmed = name.trim();
+ if (!trimmed.length) {
+ enqueueSnackbar(t('AccountPage.noEmptyNameError'), {
+ variant: 'warning',
+ });
+ } else {
+ const updatedUser = await updateUserName(name);
+ setUser(updatedUser);
+ }
+ },
+ [setUser, enqueueSnackbar, t]
+ );
+
const ownsThePlan =
user &&
!!user.ownSubscriptionsId &&
@@ -47,8 +69,9 @@ function AccountPage() {
- {user.name}
+
+
@@ -159,6 +182,9 @@ function AccountPage() {
}
const Name = styled.h1`
+ display: flex;
+ align-items: center;
+ gap: 10px;
font-weight: 100;
font-size: 3em;
@media screen and (max-width: 500px) {
diff --git a/frontend/src/views/account/api.ts b/frontend/src/views/account/api.ts
index 40fabfc9a..5e5cc24bf 100644
--- a/frontend/src/views/account/api.ts
+++ b/frontend/src/views/account/api.ts
@@ -1,5 +1,5 @@
-import { Quota } from 'common';
-import { fetchGet, fetchPatch } from '../../api/fetch';
+import { ChangeUserNamePayload, FullUser, Quota } from 'common';
+import { fetchGet, fetchPatch, fetchPostGet } from '../../api/fetch';
export async function getPortalUrl(): Promise {
const response = await fetchGet<{ url: string } | null>(
@@ -20,3 +20,12 @@ export async function getQuota(): Promise {
export async function updateMembers(members: string[]): Promise {
await fetchPatch(`/api/stripe/members`, members);
}
+
+export async function updateUserName(name: string): Promise {
+ const updated = await fetchPostGet(
+ `/api/me/username`,
+ null,
+ { name }
+ );
+ return updated;
+}