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; +}