feat(web): added image cropping to avatar uploads (#185)

* feat(web): added image cropping to avatar uploads

* chore: add translations

* refactor: move react crop css import to avatar component

* refactor: remove duplicate interface

---------

Co-authored-by: Henry <henry_ball@hotmail.co.uk>
This commit is contained in:
Morfixx
2025-09-19 22:44:20 +03:00
committed by GitHub
parent cea5cf84c8
commit 7ccd2ac1e0
19 changed files with 359 additions and 92 deletions

View File

@@ -30,6 +30,7 @@ checksums:
added%20checklist%20item%20%3C0%3E%7B0%7D%3C%2F0%3E/singular: bdd202da20b1fffbec21792c5453f90c
added%20label%20%3C0%3E%7B0%7D%3C%2F0%3E/singular: b32be052b3d57de0c9120fa7f9fc86ee
Adding%20a%20new%20member%20will%20cost%20an%20additional%20%7Bprice%7D%20(%7BbillingType%7D)%20per%20seat./singular: 12e88573028306110fbc15ef1e714892
Adjust%20the%20square%20crop%20to%20fit%20your%20avatar./singular: a4df26bbce6f14c6962fac1324db00a8
Admin%20roles/singular: 32a5d78073b9bb9a246773afba8831df
All%20systems%20operational/singular: ee943a4046b09e6334cceeea9fda2bfc
Already%20have%20an%20account%3F%20%3C0%3E%3C1%3ESign%20in%3C%2F1%3E%3C%2F0%3E/singular: 2959fd276248208b65cb27ed46b20135
@@ -119,6 +120,7 @@ checksums:
Create%20workspace/singular: 2e6718e79964ea5ce22d76c2189c77ca
created%20the%20card/singular: 605475f5aaeb4dbccbf7c4eb9107b43d
Critical/singular: eb327cd411b50aee954f8d1d215d003a
Crop%20your%20avatar/singular: eb25e2d5972ec36a0b40481c8136ab15
Current%20password%20is%20required/singular: 72536bca9598680027f2be8ce80ac280
Custom%20domain/singular: b09e7a9c187b7163b4a6cfc78042fe42
Custom%20URLs%20require%20upgrading%20to%20a%20Pro%20plan/singular: f7275e3b473b8f7b39dab6b37eb26fea

View File

@@ -59,6 +59,7 @@
"react-dom": "catalog:react18",
"react-hook-form": "^7.51.1",
"react-icons": "^5.5.0",
"react-image-crop": "^11.0.10",
"react-lottie-player": "^1.5.5",
"react-markdown": "^10.1.0",
"superjson": "2.2.1",

View File

@@ -140,6 +140,10 @@ msgstr "hat Label <0>{0}</0> hinzugefügt"
msgid "Adding a new member will cost an additional {price} ({billingType}) per seat."
msgstr "Das Hinzufügen eines neuen Mitglieds kostet zusätzlich {price} ({billingType}) pro Platz."
#: src/views/settings/components/Avatar.tsx:278
msgid "Adjust the square crop to fit your avatar."
msgstr "Passe den quadratischen Zuschnitt an deinen Avatar an."
#: src/views/home/components/Pricing.tsx:55
msgid "Admin roles"
msgstr "Administratorrollen"
@@ -338,6 +342,7 @@ msgstr "Fehlerbericht"
#: src/views/card/components/DeleteChecklistConfirmation.tsx:63
#: src/views/card/components/DeleteCommentConfirmation.tsx:76
#: src/views/members/components/DeleteMemberConfirmation.tsx:55
#: src/views/settings/components/Avatar.tsx:309
#: src/views/settings/components/ChangePasswordConfirmation.tsx:168
#: src/views/settings/components/DeleteAccountConfirmation.tsx:88
#: src/views/settings/components/DeleteWorkspaceConfirmation.tsx:98
@@ -527,6 +532,10 @@ msgstr "hat die Karte erstellt"
msgid "Critical"
msgstr "Kritisch"
#: src/views/settings/components/Avatar.tsx:275
msgid "Crop your avatar"
msgstr "Schneide deinen Avatar zu"
#: src/views/settings/components/ChangePasswordConfirmation.tsx:19
msgid "Current password is required"
msgstr "Aktuelles Passwort ist erforderlich"
@@ -749,7 +758,7 @@ msgstr "Fehler beim Einladen des Mitglieds"
msgid "Error updating display name"
msgstr "Fehler beim Aktualisieren des Anzeigenamens"
#: src/views/settings/components/Avatar.tsx:39
#: src/views/settings/components/Avatar.tsx:80
msgid "Error updating profile image"
msgstr "Fehler beim Aktualisieren des Profilbilds"
@@ -774,8 +783,8 @@ msgstr "Fehler beim Upgrade des Abonnements"
msgid "Error upgrading to Pro"
msgstr "Fehler beim Upgrade auf Pro"
#: src/views/settings/components/Avatar.tsx:56
#: src/views/settings/components/Avatar.tsx:97
#: src/views/settings/components/Avatar.tsx:94
#: src/views/settings/components/Avatar.tsx:221
msgid "Error uploading profile image"
msgstr "Fehler beim Hochladen des Profilbilds"
@@ -1321,7 +1330,7 @@ msgstr "Bitte gib einen gültigen Namen ein"
msgid "Please enter a valid password"
msgstr "Bitte gib ein gültiges Passwort ein"
#: src/views/settings/components/Avatar.tsx:57
#: src/views/settings/components/Avatar.tsx:95
msgid "Please select a file to upload."
msgstr "Bitte wähle eine Datei zum Hochladen aus."
@@ -1352,8 +1361,8 @@ msgstr "Bitte wähle eine Datei zum Hochladen aus."
#: src/views/members/components/DeleteMemberConfirmation.tsx:28
#: src/views/members/components/InviteMemberForm.tsx:78
#: src/views/members/components/InviteMemberForm.tsx:131
#: src/views/settings/components/Avatar.tsx:40
#: src/views/settings/components/Avatar.tsx:98
#: src/views/settings/components/Avatar.tsx:81
#: src/views/settings/components/Avatar.tsx:222
#: src/views/settings/components/DeleteAccountConfirmation.tsx:40
#: src/views/settings/components/DeleteWorkspaceConfirmation.tsx:46
#: src/views/settings/components/UpdateDisplayNameForm.tsx:55
@@ -1391,7 +1400,7 @@ msgstr "Pro-Plan"
msgid "Pro Plan ∞"
msgstr "Pro-Plan ∞"
#: src/views/settings/components/Avatar.tsx:26
#: src/views/settings/components/Avatar.tsx:67
msgid "Profile image updated"
msgstr "Profilbild aktualisiert"
@@ -1492,6 +1501,7 @@ msgid "Run on your own infrastructure"
msgstr "Auf eigener Infrastruktur betreiben"
#: src/views/card/components/Comment.tsx:165
#: src/views/settings/components/Avatar.tsx:312
msgid "Save"
msgstr "Speichern"
@@ -2118,7 +2128,7 @@ msgstr "Dein Anzeigename wurde aktualisiert."
msgid "Your password has been changed."
msgstr "Ihr Passwort wurde geändert."
#: src/views/settings/components/Avatar.tsx:27
#: src/views/settings/components/Avatar.tsx:68
msgid "Your profile image has been updated."
msgstr "Dein Profilbild wurde aktualisiert."

File diff suppressed because one or more lines are too long

View File

@@ -160,6 +160,10 @@ msgstr "added label <0>{0}</0>"
msgid "Adding a new member will cost an additional {price} ({billingType}) per seat."
msgstr "Adding a new member will cost an additional {price} ({billingType}) per seat."
#: src/views/settings/components/Avatar.tsx:278
msgid "Adjust the square crop to fit your avatar."
msgstr "Adjust the square crop to fit your avatar."
#: src/views/home/components/Pricing.tsx:55
msgid "Admin roles"
msgstr "Admin roles"
@@ -387,6 +391,7 @@ msgstr "Bug Report"
#: src/views/card/components/DeleteChecklistConfirmation.tsx:63
#: src/views/card/components/DeleteCommentConfirmation.tsx:76
#: src/views/members/components/DeleteMemberConfirmation.tsx:55
#: src/views/settings/components/Avatar.tsx:309
#: src/views/settings/components/ChangePasswordConfirmation.tsx:168
#: src/views/settings/components/DeleteAccountConfirmation.tsx:88
#: src/views/settings/components/DeleteWorkspaceConfirmation.tsx:98
@@ -592,6 +597,10 @@ msgstr "created the card"
msgid "Critical"
msgstr "Critical"
#: src/views/settings/components/Avatar.tsx:275
msgid "Crop your avatar"
msgstr "Crop your avatar"
#: src/views/settings/components/ChangePasswordConfirmation.tsx:19
msgid "Current password is required"
msgstr "Current password is required"
@@ -822,7 +831,7 @@ msgstr "Error inviting member"
msgid "Error updating display name"
msgstr "Error updating display name"
#: src/views/settings/components/Avatar.tsx:39
#: src/views/settings/components/Avatar.tsx:80
msgid "Error updating profile image"
msgstr "Error updating profile image"
@@ -847,8 +856,8 @@ msgstr "Error upgrading subscription"
msgid "Error upgrading to Pro"
msgstr "Error upgrading to Pro"
#: src/views/settings/components/Avatar.tsx:56
#: src/views/settings/components/Avatar.tsx:97
#: src/views/settings/components/Avatar.tsx:94
#: src/views/settings/components/Avatar.tsx:221
msgid "Error uploading profile image"
msgstr "Error uploading profile image"
@@ -1411,7 +1420,7 @@ msgstr "Please enter a valid name"
msgid "Please enter a valid password"
msgstr "Please enter a valid password"
#: src/views/settings/components/Avatar.tsx:57
#: src/views/settings/components/Avatar.tsx:95
msgid "Please select a file to upload."
msgstr "Please select a file to upload."
@@ -1442,8 +1451,8 @@ msgstr "Please select a file to upload."
#: src/views/members/components/DeleteMemberConfirmation.tsx:28
#: src/views/members/components/InviteMemberForm.tsx:78
#: src/views/members/components/InviteMemberForm.tsx:131
#: src/views/settings/components/Avatar.tsx:40
#: src/views/settings/components/Avatar.tsx:98
#: src/views/settings/components/Avatar.tsx:81
#: src/views/settings/components/Avatar.tsx:222
#: src/views/settings/components/DeleteAccountConfirmation.tsx:40
#: src/views/settings/components/DeleteWorkspaceConfirmation.tsx:46
#: src/views/settings/components/UpdateDisplayNameForm.tsx:55
@@ -1481,7 +1490,7 @@ msgstr "Pro Plan"
msgid "Pro Plan ∞"
msgstr "Pro Plan ∞"
#: src/views/settings/components/Avatar.tsx:26
#: src/views/settings/components/Avatar.tsx:67
msgid "Profile image updated"
msgstr "Profile image updated"
@@ -1590,6 +1599,7 @@ msgid "Run on your own infrastructure"
msgstr "Run on your own infrastructure"
#: src/views/card/components/Comment.tsx:165
#: src/views/settings/components/Avatar.tsx:312
msgid "Save"
msgstr "Save"
@@ -2240,7 +2250,7 @@ msgstr "Your display name has been updated."
msgid "Your password has been changed."
msgstr "Your password has been changed."
#: src/views/settings/components/Avatar.tsx:27
#: src/views/settings/components/Avatar.tsx:68
msgid "Your profile image has been updated."
msgstr "Your profile image has been updated."

File diff suppressed because one or more lines are too long

View File

@@ -140,6 +140,10 @@ msgstr "añadió la etiqueta <0>{0}</0>"
msgid "Adding a new member will cost an additional {price} ({billingType}) per seat."
msgstr "Añadir un nuevo miembro costará {price} adicionales ({billingType}) por asiento."
#: src/views/settings/components/Avatar.tsx:278
msgid "Adjust the square crop to fit your avatar."
msgstr "Ajusta el recorte cuadrado para que se adapte a tu avatar."
#: src/views/home/components/Pricing.tsx:55
msgid "Admin roles"
msgstr "Roles de administrador"
@@ -338,6 +342,7 @@ msgstr "Informe de error"
#: src/views/card/components/DeleteChecklistConfirmation.tsx:63
#: src/views/card/components/DeleteCommentConfirmation.tsx:76
#: src/views/members/components/DeleteMemberConfirmation.tsx:55
#: src/views/settings/components/Avatar.tsx:309
#: src/views/settings/components/ChangePasswordConfirmation.tsx:168
#: src/views/settings/components/DeleteAccountConfirmation.tsx:88
#: src/views/settings/components/DeleteWorkspaceConfirmation.tsx:98
@@ -527,6 +532,10 @@ msgstr "creó la tarjeta"
msgid "Critical"
msgstr "Crítico"
#: src/views/settings/components/Avatar.tsx:275
msgid "Crop your avatar"
msgstr "Recorta tu avatar"
#: src/views/settings/components/ChangePasswordConfirmation.tsx:19
msgid "Current password is required"
msgstr "Se requiere la contraseña actual"
@@ -749,7 +758,7 @@ msgstr "Error al invitar al miembro"
msgid "Error updating display name"
msgstr "Error al actualizar el nombre visible"
#: src/views/settings/components/Avatar.tsx:39
#: src/views/settings/components/Avatar.tsx:80
msgid "Error updating profile image"
msgstr "Error al actualizar la imagen de perfil"
@@ -774,8 +783,8 @@ msgstr "Error al actualizar la suscripción"
msgid "Error upgrading to Pro"
msgstr "Error al actualizar a Pro"
#: src/views/settings/components/Avatar.tsx:56
#: src/views/settings/components/Avatar.tsx:97
#: src/views/settings/components/Avatar.tsx:94
#: src/views/settings/components/Avatar.tsx:221
msgid "Error uploading profile image"
msgstr "Error al subir la imagen de perfil"
@@ -1321,7 +1330,7 @@ msgstr "Por favor, introduce un nombre válido"
msgid "Please enter a valid password"
msgstr "Por favor, introduce una contraseña válida"
#: src/views/settings/components/Avatar.tsx:57
#: src/views/settings/components/Avatar.tsx:95
msgid "Please select a file to upload."
msgstr "Por favor selecciona un archivo para subir."
@@ -1352,8 +1361,8 @@ msgstr "Por favor selecciona un archivo para subir."
#: src/views/members/components/DeleteMemberConfirmation.tsx:28
#: src/views/members/components/InviteMemberForm.tsx:78
#: src/views/members/components/InviteMemberForm.tsx:131
#: src/views/settings/components/Avatar.tsx:40
#: src/views/settings/components/Avatar.tsx:98
#: src/views/settings/components/Avatar.tsx:81
#: src/views/settings/components/Avatar.tsx:222
#: src/views/settings/components/DeleteAccountConfirmation.tsx:40
#: src/views/settings/components/DeleteWorkspaceConfirmation.tsx:46
#: src/views/settings/components/UpdateDisplayNameForm.tsx:55
@@ -1391,7 +1400,7 @@ msgstr "Plan Pro"
msgid "Pro Plan ∞"
msgstr "Plan Pro ∞"
#: src/views/settings/components/Avatar.tsx:26
#: src/views/settings/components/Avatar.tsx:67
msgid "Profile image updated"
msgstr "Imagen de perfil actualizada"
@@ -1492,6 +1501,7 @@ msgid "Run on your own infrastructure"
msgstr "Ejecuta en tu propia infraestructura"
#: src/views/card/components/Comment.tsx:165
#: src/views/settings/components/Avatar.tsx:312
msgid "Save"
msgstr "Guardar"
@@ -2118,7 +2128,7 @@ msgstr "Tu nombre de visualización ha sido actualizado."
msgid "Your password has been changed."
msgstr "Tu contraseña ha sido cambiada."
#: src/views/settings/components/Avatar.tsx:27
#: src/views/settings/components/Avatar.tsx:68
msgid "Your profile image has been updated."
msgstr "Tu imagen de perfil ha sido actualizada."

File diff suppressed because one or more lines are too long

View File

@@ -140,6 +140,10 @@ msgstr "a ajouté l'étiquette <0>{0}</0>"
msgid "Adding a new member will cost an additional {price} ({billingType}) per seat."
msgstr "L'ajout d'un nouveau membre coûtera {price} supplémentaires ({billingType}) par siège."
#: src/views/settings/components/Avatar.tsx:278
msgid "Adjust the square crop to fit your avatar."
msgstr "Ajustez le recadrage carré pour adapter votre avatar."
#: src/views/home/components/Pricing.tsx:55
msgid "Admin roles"
msgstr "Rôles d'administrateur"
@@ -338,6 +342,7 @@ msgstr "Rapport de bug"
#: src/views/card/components/DeleteChecklistConfirmation.tsx:63
#: src/views/card/components/DeleteCommentConfirmation.tsx:76
#: src/views/members/components/DeleteMemberConfirmation.tsx:55
#: src/views/settings/components/Avatar.tsx:309
#: src/views/settings/components/ChangePasswordConfirmation.tsx:168
#: src/views/settings/components/DeleteAccountConfirmation.tsx:88
#: src/views/settings/components/DeleteWorkspaceConfirmation.tsx:98
@@ -527,6 +532,10 @@ msgstr "a créé la carte"
msgid "Critical"
msgstr "Critique"
#: src/views/settings/components/Avatar.tsx:275
msgid "Crop your avatar"
msgstr "Recadrez votre avatar"
#: src/views/settings/components/ChangePasswordConfirmation.tsx:19
msgid "Current password is required"
msgstr "Le mot de passe actuel est requis"
@@ -749,7 +758,7 @@ msgstr "Erreur lors de l'invitation du membre"
msgid "Error updating display name"
msgstr "Erreur lors de la mise à jour du nom d'affichage"
#: src/views/settings/components/Avatar.tsx:39
#: src/views/settings/components/Avatar.tsx:80
msgid "Error updating profile image"
msgstr "Erreur lors de la mise à jour de l'image de profil"
@@ -774,8 +783,8 @@ msgstr "Erreur lors de la mise à niveau de l'abonnement"
msgid "Error upgrading to Pro"
msgstr "Erreur lors de la mise à niveau vers Pro"
#: src/views/settings/components/Avatar.tsx:56
#: src/views/settings/components/Avatar.tsx:97
#: src/views/settings/components/Avatar.tsx:94
#: src/views/settings/components/Avatar.tsx:221
msgid "Error uploading profile image"
msgstr "Erreur lors du téléchargement de l'image de profil"
@@ -1321,7 +1330,7 @@ msgstr "Veuillez saisir un nom valide"
msgid "Please enter a valid password"
msgstr "Veuillez saisir un mot de passe valide"
#: src/views/settings/components/Avatar.tsx:57
#: src/views/settings/components/Avatar.tsx:95
msgid "Please select a file to upload."
msgstr "Veuillez sélectionner un fichier à télécharger."
@@ -1352,8 +1361,8 @@ msgstr "Veuillez sélectionner un fichier à télécharger."
#: src/views/members/components/DeleteMemberConfirmation.tsx:28
#: src/views/members/components/InviteMemberForm.tsx:78
#: src/views/members/components/InviteMemberForm.tsx:131
#: src/views/settings/components/Avatar.tsx:40
#: src/views/settings/components/Avatar.tsx:98
#: src/views/settings/components/Avatar.tsx:81
#: src/views/settings/components/Avatar.tsx:222
#: src/views/settings/components/DeleteAccountConfirmation.tsx:40
#: src/views/settings/components/DeleteWorkspaceConfirmation.tsx:46
#: src/views/settings/components/UpdateDisplayNameForm.tsx:55
@@ -1391,7 +1400,7 @@ msgstr "Plan Pro"
msgid "Pro Plan ∞"
msgstr "Plan Pro ∞"
#: src/views/settings/components/Avatar.tsx:26
#: src/views/settings/components/Avatar.tsx:67
msgid "Profile image updated"
msgstr "Image de profil mise à jour"
@@ -1492,6 +1501,7 @@ msgid "Run on your own infrastructure"
msgstr "Exécutez sur votre propre infrastructure"
#: src/views/card/components/Comment.tsx:165
#: src/views/settings/components/Avatar.tsx:312
msgid "Save"
msgstr "Enregistrer"
@@ -2118,7 +2128,7 @@ msgstr "Votre nom d'affichage a été mis à jour."
msgid "Your password has been changed."
msgstr "Votre mot de passe a été modifié."
#: src/views/settings/components/Avatar.tsx:27
#: src/views/settings/components/Avatar.tsx:68
msgid "Your profile image has been updated."
msgstr "Votre image de profil a été mise à jour."

File diff suppressed because one or more lines are too long

View File

@@ -140,6 +140,10 @@ msgstr "ha aggiunto l'etichetta <0>{0}</0>"
msgid "Adding a new member will cost an additional {price} ({billingType}) per seat."
msgstr "L'aggiunta di un nuovo membro costerà un supplemento di {price} ({billingType}) per posto."
#: src/views/settings/components/Avatar.tsx:278
msgid "Adjust the square crop to fit your avatar."
msgstr "Regola il ritaglio quadrato per adattarlo al tuo avatar."
#: src/views/home/components/Pricing.tsx:55
msgid "Admin roles"
msgstr "Ruoli amministratore"
@@ -338,6 +342,7 @@ msgstr "Segnalazione bug"
#: src/views/card/components/DeleteChecklistConfirmation.tsx:63
#: src/views/card/components/DeleteCommentConfirmation.tsx:76
#: src/views/members/components/DeleteMemberConfirmation.tsx:55
#: src/views/settings/components/Avatar.tsx:309
#: src/views/settings/components/ChangePasswordConfirmation.tsx:168
#: src/views/settings/components/DeleteAccountConfirmation.tsx:88
#: src/views/settings/components/DeleteWorkspaceConfirmation.tsx:98
@@ -527,6 +532,10 @@ msgstr "ha creato la carta"
msgid "Critical"
msgstr "Critico"
#: src/views/settings/components/Avatar.tsx:275
msgid "Crop your avatar"
msgstr "Ritaglia il tuo avatar"
#: src/views/settings/components/ChangePasswordConfirmation.tsx:19
msgid "Current password is required"
msgstr "La password attuale è obbligatoria"
@@ -749,7 +758,7 @@ msgstr "Errore durante l'invito del membro"
msgid "Error updating display name"
msgstr "Errore durante l'aggiornamento del nome visualizzato"
#: src/views/settings/components/Avatar.tsx:39
#: src/views/settings/components/Avatar.tsx:80
msgid "Error updating profile image"
msgstr "Errore durante l'aggiornamento dell'immagine del profilo"
@@ -774,8 +783,8 @@ msgstr "Errore nell'aggiornamento dell'abbonamento"
msgid "Error upgrading to Pro"
msgstr "Errore durante l'aggiornamento a Pro"
#: src/views/settings/components/Avatar.tsx:56
#: src/views/settings/components/Avatar.tsx:97
#: src/views/settings/components/Avatar.tsx:94
#: src/views/settings/components/Avatar.tsx:221
msgid "Error uploading profile image"
msgstr "Errore durante il caricamento dell'immagine del profilo"
@@ -1321,7 +1330,7 @@ msgstr "Inserisci un nome valido"
msgid "Please enter a valid password"
msgstr "Inserisci una password valida"
#: src/views/settings/components/Avatar.tsx:57
#: src/views/settings/components/Avatar.tsx:95
msgid "Please select a file to upload."
msgstr "Seleziona un file da caricare."
@@ -1352,8 +1361,8 @@ msgstr "Seleziona un file da caricare."
#: src/views/members/components/DeleteMemberConfirmation.tsx:28
#: src/views/members/components/InviteMemberForm.tsx:78
#: src/views/members/components/InviteMemberForm.tsx:131
#: src/views/settings/components/Avatar.tsx:40
#: src/views/settings/components/Avatar.tsx:98
#: src/views/settings/components/Avatar.tsx:81
#: src/views/settings/components/Avatar.tsx:222
#: src/views/settings/components/DeleteAccountConfirmation.tsx:40
#: src/views/settings/components/DeleteWorkspaceConfirmation.tsx:46
#: src/views/settings/components/UpdateDisplayNameForm.tsx:55
@@ -1391,7 +1400,7 @@ msgstr "Piano Pro"
msgid "Pro Plan ∞"
msgstr "Piano Pro ∞"
#: src/views/settings/components/Avatar.tsx:26
#: src/views/settings/components/Avatar.tsx:67
msgid "Profile image updated"
msgstr "Immagine del profilo aggiornata"
@@ -1492,6 +1501,7 @@ msgid "Run on your own infrastructure"
msgstr "Esegui sulla tua infrastruttura"
#: src/views/card/components/Comment.tsx:165
#: src/views/settings/components/Avatar.tsx:312
msgid "Save"
msgstr "Salva"
@@ -2118,7 +2128,7 @@ msgstr "Il tuo nome visualizzato è stato aggiornato."
msgid "Your password has been changed."
msgstr "La tua password è stata modificata."
#: src/views/settings/components/Avatar.tsx:27
#: src/views/settings/components/Avatar.tsx:68
msgid "Your profile image has been updated."
msgstr "La tua immagine del profilo è stata aggiornata."

File diff suppressed because one or more lines are too long

View File

@@ -140,6 +140,10 @@ msgstr "heeft label <0>{0}</0> toegevoegd"
msgid "Adding a new member will cost an additional {price} ({billingType}) per seat."
msgstr "Het toevoegen van een nieuw lid kost een extra {price} ({billingType}) per plaats."
#: src/views/settings/components/Avatar.tsx:278
msgid "Adjust the square crop to fit your avatar."
msgstr "Pas de vierkante uitsnede aan zodat je avatar goed past."
#: src/views/home/components/Pricing.tsx:55
msgid "Admin roles"
msgstr "Beheerdersrollen"
@@ -338,6 +342,7 @@ msgstr "Bugrapport"
#: src/views/card/components/DeleteChecklistConfirmation.tsx:63
#: src/views/card/components/DeleteCommentConfirmation.tsx:76
#: src/views/members/components/DeleteMemberConfirmation.tsx:55
#: src/views/settings/components/Avatar.tsx:309
#: src/views/settings/components/ChangePasswordConfirmation.tsx:168
#: src/views/settings/components/DeleteAccountConfirmation.tsx:88
#: src/views/settings/components/DeleteWorkspaceConfirmation.tsx:98
@@ -527,6 +532,10 @@ msgstr "heeft de kaart aangemaakt"
msgid "Critical"
msgstr "Kritiek"
#: src/views/settings/components/Avatar.tsx:275
msgid "Crop your avatar"
msgstr "Snijd je avatar bij"
#: src/views/settings/components/ChangePasswordConfirmation.tsx:19
msgid "Current password is required"
msgstr "Huidig wachtwoord is vereist"
@@ -749,7 +758,7 @@ msgstr "Fout bij het uitnodigen van lid"
msgid "Error updating display name"
msgstr "Fout bij bijwerken weergavenaam"
#: src/views/settings/components/Avatar.tsx:39
#: src/views/settings/components/Avatar.tsx:80
msgid "Error updating profile image"
msgstr "Fout bij het bijwerken van profielafbeelding"
@@ -774,8 +783,8 @@ msgstr "Fout bij het upgraden van abonnement"
msgid "Error upgrading to Pro"
msgstr "Fout bij upgraden naar Pro"
#: src/views/settings/components/Avatar.tsx:56
#: src/views/settings/components/Avatar.tsx:97
#: src/views/settings/components/Avatar.tsx:94
#: src/views/settings/components/Avatar.tsx:221
msgid "Error uploading profile image"
msgstr "Fout bij het uploaden van profielafbeelding"
@@ -1321,7 +1330,7 @@ msgstr "Voer een geldige naam in"
msgid "Please enter a valid password"
msgstr "Voer een geldig wachtwoord in"
#: src/views/settings/components/Avatar.tsx:57
#: src/views/settings/components/Avatar.tsx:95
msgid "Please select a file to upload."
msgstr "Selecteer een bestand om te uploaden."
@@ -1352,8 +1361,8 @@ msgstr "Selecteer een bestand om te uploaden."
#: src/views/members/components/DeleteMemberConfirmation.tsx:28
#: src/views/members/components/InviteMemberForm.tsx:78
#: src/views/members/components/InviteMemberForm.tsx:131
#: src/views/settings/components/Avatar.tsx:40
#: src/views/settings/components/Avatar.tsx:98
#: src/views/settings/components/Avatar.tsx:81
#: src/views/settings/components/Avatar.tsx:222
#: src/views/settings/components/DeleteAccountConfirmation.tsx:40
#: src/views/settings/components/DeleteWorkspaceConfirmation.tsx:46
#: src/views/settings/components/UpdateDisplayNameForm.tsx:55
@@ -1391,7 +1400,7 @@ msgstr "Pro Plan"
msgid "Pro Plan ∞"
msgstr "Pro Plan ∞"
#: src/views/settings/components/Avatar.tsx:26
#: src/views/settings/components/Avatar.tsx:67
msgid "Profile image updated"
msgstr "Profielafbeelding bijgewerkt"
@@ -1492,6 +1501,7 @@ msgid "Run on your own infrastructure"
msgstr "Draai op je eigen infrastructuur"
#: src/views/card/components/Comment.tsx:165
#: src/views/settings/components/Avatar.tsx:312
msgid "Save"
msgstr "Opslaan"
@@ -2118,7 +2128,7 @@ msgstr "Je weergavenaam is bijgewerkt."
msgid "Your password has been changed."
msgstr "Je wachtwoord is gewijzigd."
#: src/views/settings/components/Avatar.tsx:27
#: src/views/settings/components/Avatar.tsx:68
msgid "Your profile image has been updated."
msgstr "Je profielafbeelding is bijgewerkt."

File diff suppressed because one or more lines are too long

View File

@@ -140,6 +140,10 @@ msgstr "добавлена метка <0>{0}</0>"
msgid "Adding a new member will cost an additional {price} ({billingType}) per seat."
msgstr "Добавление нового участника будет стоить дополнительно {price} ({billingType}) за место."
#: src/views/settings/components/Avatar.tsx:278
msgid "Adjust the square crop to fit your avatar."
msgstr "Настройте квадратное кадрирование, чтобы оно подходило для вашего аватара."
#: src/views/home/components/Pricing.tsx:55
msgid "Admin roles"
msgstr "Роли администратора"
@@ -338,6 +342,7 @@ msgstr "Отчет об ошибке"
#: src/views/card/components/DeleteChecklistConfirmation.tsx:63
#: src/views/card/components/DeleteCommentConfirmation.tsx:76
#: src/views/members/components/DeleteMemberConfirmation.tsx:55
#: src/views/settings/components/Avatar.tsx:309
#: src/views/settings/components/ChangePasswordConfirmation.tsx:168
#: src/views/settings/components/DeleteAccountConfirmation.tsx:88
#: src/views/settings/components/DeleteWorkspaceConfirmation.tsx:98
@@ -527,6 +532,10 @@ msgstr "создал(а) карточку"
msgid "Critical"
msgstr "Критический"
#: src/views/settings/components/Avatar.tsx:275
msgid "Crop your avatar"
msgstr "Обрежьте ваш аватар"
#: src/views/settings/components/ChangePasswordConfirmation.tsx:19
msgid "Current password is required"
msgstr "Требуется текущий пароль"
@@ -749,7 +758,7 @@ msgstr "Ошибка при приглашении участника"
msgid "Error updating display name"
msgstr "Ошибка при обновлении отображаемого имени"
#: src/views/settings/components/Avatar.tsx:39
#: src/views/settings/components/Avatar.tsx:80
msgid "Error updating profile image"
msgstr "Ошибка при обновлении изображения профиля"
@@ -774,8 +783,8 @@ msgstr "Ошибка при обновлении подписки"
msgid "Error upgrading to Pro"
msgstr "Ошибка при обновлении до Pro"
#: src/views/settings/components/Avatar.tsx:56
#: src/views/settings/components/Avatar.tsx:97
#: src/views/settings/components/Avatar.tsx:94
#: src/views/settings/components/Avatar.tsx:221
msgid "Error uploading profile image"
msgstr "Ошибка загрузки изображения профиля"
@@ -1280,6 +1289,10 @@ msgstr "Пароль должен содержать не менее 8 симв
msgid "Passwords do not match"
msgstr "Пароли не совпадают"
#: src/views/members/index.tsx:134
msgid "Paused"
msgstr "Приостановлено"
#: src/views/home/components/Pricing.tsx:102
msgid "Payment frequency"
msgstr "Частота платежей"
@@ -1317,7 +1330,7 @@ msgstr "Пожалуйста, введите действительное имя
msgid "Please enter a valid password"
msgstr "Пожалуйста, введите действительный пароль"
#: src/views/settings/components/Avatar.tsx:57
#: src/views/settings/components/Avatar.tsx:95
msgid "Please select a file to upload."
msgstr "Пожалуйста, выберите файл для загрузки."
@@ -1348,8 +1361,8 @@ msgstr "Пожалуйста, выберите файл для загрузки.
#: src/views/members/components/DeleteMemberConfirmation.tsx:28
#: src/views/members/components/InviteMemberForm.tsx:78
#: src/views/members/components/InviteMemberForm.tsx:131
#: src/views/settings/components/Avatar.tsx:40
#: src/views/settings/components/Avatar.tsx:98
#: src/views/settings/components/Avatar.tsx:81
#: src/views/settings/components/Avatar.tsx:222
#: src/views/settings/components/DeleteAccountConfirmation.tsx:40
#: src/views/settings/components/DeleteWorkspaceConfirmation.tsx:46
#: src/views/settings/components/UpdateDisplayNameForm.tsx:55
@@ -1387,7 +1400,7 @@ msgstr "Профессиональный план"
msgid "Pro Plan ∞"
msgstr "Профессиональный план ∞"
#: src/views/settings/components/Avatar.tsx:26
#: src/views/settings/components/Avatar.tsx:67
msgid "Profile image updated"
msgstr "Изображение профиля обновлено"
@@ -1488,6 +1501,7 @@ msgid "Run on your own infrastructure"
msgstr "Запустите на своей инфраструктуре"
#: src/views/card/components/Comment.tsx:165
#: src/views/settings/components/Avatar.tsx:312
msgid "Save"
msgstr "Сохранить"
@@ -2114,7 +2128,7 @@ msgstr "Ваше отображаемое имя было обновлено."
msgid "Your password has been changed."
msgstr "Ваш пароль был изменён."
#: src/views/settings/components/Avatar.tsx:27
#: src/views/settings/components/Avatar.tsx:68
msgid "Your profile image has been updated."
msgstr "Ваше изображение профиля было обновлено."

File diff suppressed because one or more lines are too long

View File

@@ -7,6 +7,7 @@ import type { ReactElement, ReactNode } from "react";
import { Plus_Jakarta_Sans } from "next/font/google";
import Script from "next/script";
import { env } from "next-runtime-env";
import { ThemeProvider } from "next-themes";
import posthog from "posthog-js";
import { PostHogProvider } from "posthog-js/react";
import { useEffect } from "react";
@@ -14,7 +15,6 @@ import { useEffect } from "react";
import { LinguiProviderWrapper } from "~/providers/lingui";
import { ModalProvider } from "~/providers/modal";
import { PopupProvider } from "~/providers/popup";
import { ThemeProvider } from "next-themes";
import { api } from "~/utils/api";
const jakarta = Plus_Jakarta_Sans({
@@ -82,8 +82,8 @@ const MyApp: AppType = ({ Component, pageProps }: AppPropsWithLayout) => {
<main className="font-sans">
<LinguiProviderWrapper>
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
<ModalProvider>
<PopupProvider>
<ModalProvider>
<PopupProvider>
{posthogKey ? (
<PostHogProvider client={posthog}>
{getLayout(<Component {...pageProps} />)}
@@ -91,8 +91,8 @@ const MyApp: AppType = ({ Component, pageProps }: AppPropsWithLayout) => {
) : (
getLayout(<Component {...pageProps} />)
)}
</PopupProvider>
</ModalProvider>
</PopupProvider>
</ModalProvider>
</ThemeProvider>
</LinguiProviderWrapper>
</main>

View File

@@ -1,14 +1,45 @@
import Image from "next/image";
import { t } from "@lingui/core/macro";
import { env } from "next-runtime-env";
import { useState } from "react";
import { useCallback, useRef, useState } from "react";
import ReactCrop from "react-image-crop";
import "react-image-crop/dist/ReactCrop.css";
import { generateUID } from "@kan/shared/utils";
import Button from "~/components/Button";
import Modal from "~/components/modal";
import { usePopup } from "~/providers/popup";
import { api } from "~/utils/api";
import { getAvatarUrl } from "~/utils/helpers";
interface PercentCrop {
unit: "%";
x: number;
y: number;
width: number;
height: number;
}
interface LocalPixelCrop {
x: number;
y: number;
width: number;
height: number;
}
interface ReactCropProps {
crop: PercentCrop | undefined;
onChange: (crop: LocalPixelCrop, percentCrop: PercentCrop) => void;
aspect?: number;
className?: string;
circularCrop?: boolean;
children: React.ReactNode;
}
const AnyReactCrop = ReactCrop as unknown as React.FC<ReactCropProps>;
export default function Avatar({
userId,
userImage,
@@ -19,6 +50,13 @@ export default function Avatar({
const utils = api.useUtils();
const { showPopup } = usePopup();
const [uploading, setUploading] = useState(false);
const [cropDialogOpen, setCropDialogOpen] = useState(false);
const [selectedFile, setSelectedFile] = useState<File | null>(null);
const [selectedPreviewUrl, setSelectedPreviewUrl] = useState<string | null>(
null,
);
const [crop, setCrop] = useState<PercentCrop>();
const imgRef = useRef<HTMLImageElement | null>(null);
const updateUser = api.user.update.useMutation({
onSuccess: async () => {
@@ -45,24 +83,109 @@ export default function Avatar({
const avatarUrl = userImage ? getAvatarUrl(userImage) : undefined;
const uploadAvatar = async (event: React.ChangeEvent<HTMLInputElement>) => {
try {
event.preventDefault();
const onFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
event.preventDefault();
const file = event.target.files?.[0] ?? null;
if (!file || !userId) {
return showPopup({
header: t`Error uploading profile image`,
message: t`Please select a file to upload.`,
icon: "error",
});
}
// Open crop dialog with preview
setSelectedFile(file);
const objUrl = URL.createObjectURL(file);
setSelectedPreviewUrl(objUrl);
setCropDialogOpen(true);
};
const file = event.target.files?.[0];
if (!file || !userId) {
return showPopup({
header: t`Error uploading profile image`,
message: t`Please select a file to upload.`,
icon: "error",
});
const onImageLoad = useCallback(
(e: React.SyntheticEvent<HTMLImageElement>) => {
const { naturalWidth, naturalHeight } = e.currentTarget;
// Create a centered square crop at ~90% of the smaller dimension
// Compute width% so that the square fits within the image
let widthPercent: number;
let heightPercent: number;
if (naturalWidth >= naturalHeight) {
// landscape: height is limiting
heightPercent = 90;
widthPercent = (naturalHeight / naturalWidth) * heightPercent;
} else {
// portrait: width is limiting
widthPercent = 90;
heightPercent = (naturalWidth / naturalHeight) * widthPercent;
}
const x = (100 - widthPercent) / 2;
const y = (100 - heightPercent) / 2;
setCrop({ unit: "%", x, y, width: widthPercent, height: heightPercent });
},
[],
);
const fileExt = file.name.split(".").pop();
const fileName = `${userId}/avatar-${generateUID()}.${fileExt}`;
const getCroppedBlob = useCallback(async (): Promise<Blob> => {
if (!imgRef.current || !crop) throw new Error("No crop to save");
const image = imgRef.current;
const canvas = document.createElement("canvas");
const cropXpx = (crop.x / 100) * image.naturalWidth;
const cropYpx = (crop.y / 100) * image.naturalHeight;
const cropWpx = (crop.width / 100) * image.naturalWidth;
const cropHpx = (crop.height / 100) * image.naturalHeight;
canvas.width = Math.max(1, Math.floor(cropWpx));
canvas.height = Math.max(1, Math.floor(cropHpx));
const ctx = canvas.getContext("2d");
if (!ctx) throw new Error("Canvas not supported");
// For better quality on HiDPI screens
const pixelRatio = window.devicePixelRatio || 1;
canvas.width = canvas.width * pixelRatio;
canvas.height = canvas.height * pixelRatio;
ctx.setTransform(pixelRatio, 0, 0, pixelRatio, 0, 0);
ctx.imageSmoothingQuality = "high";
ctx.drawImage(
image,
cropXpx,
cropYpx,
cropWpx,
cropHpx,
0,
0,
canvas.width / pixelRatio,
canvas.height / pixelRatio,
);
const mime = selectedFile?.type ?? "image/jpeg";
const blob: Blob = await new Promise((resolve, reject) => {
canvas.toBlob(
(b) => (b ? resolve(b) : reject(new Error("toBlob failed"))),
mime,
);
});
return blob;
}, [crop, selectedFile]);
const resetCropState = useCallback(() => {
setCrop(undefined);
setSelectedFile(null);
if (selectedPreviewUrl) URL.revokeObjectURL(selectedPreviewUrl);
setSelectedPreviewUrl(null);
}, [selectedPreviewUrl]);
const handleCancelCrop = useCallback(() => {
setCropDialogOpen(false);
resetCropState();
}, [resetCropState]);
const handleSaveCrop = useCallback(async () => {
try {
if (!userId || !selectedFile) return;
setUploading(true);
const blob = await getCroppedBlob();
const originalExt = selectedFile.name.split(".").pop() ?? "jpg";
const fileName = `${userId}/avatar-${generateUID()}.${originalExt}`;
const response = await fetch(
env("NEXT_PUBLIC_BASE_URL") + "/api/upload/image",
@@ -71,26 +194,24 @@ export default function Avatar({
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ filename: fileName, contentType: file.type }),
body: JSON.stringify({ filename: fileName, contentType: blob.type }),
},
);
if (!response.ok) throw new Error("Failed to get pre-signed URL");
const { url } = (await response.json()) as {
url: string;
};
const { url } = (await response.json()) as { url: string };
const uploadResponse = await fetch(url, {
method: "PUT",
body: file,
body: blob,
});
if (!uploadResponse.ok) throw new Error("Failed to upload profile image");
updateUser.mutate({
image: fileName,
});
updateUser.mutate({ image: fileName });
setCropDialogOpen(false);
resetCropState();
} catch (error) {
console.error(error);
showPopup({
@@ -101,7 +222,14 @@ export default function Avatar({
} finally {
setUploading(false);
}
};
}, [
getCroppedBlob,
resetCropState,
selectedFile,
showPopup,
updateUser,
userId,
]);
return (
<div>
@@ -111,7 +239,7 @@ export default function Avatar({
type="file"
id="single"
accept="image/*"
onChange={uploadAvatar}
onChange={onFileChange}
disabled={uploading}
/>
{avatarUrl ? (
@@ -134,6 +262,56 @@ export default function Avatar({
</span>
)}
</div>
{/* Crop Dialog */}
{cropDialogOpen && (
<Modal modalSize="md" positionFromTop="sm" isVisible>
<div className="p-4 sm:p-6">
<div className="mb-4">
<h3 className="text-base font-semibold text-light-1000 dark:text-dark-1000">
{t`Crop your avatar`}
</h3>
<p className="mt-1 text-sm text-light-800 dark:text-dark-800">
{t`Adjust the square crop to fit your avatar.`}
</p>
</div>
<div className="max-h-[80vh]">
<div className="rounded-md border border-light-600 p-2 dark:border-dark-600">
<AnyReactCrop
crop={crop}
onChange={(_crop: LocalPixelCrop, percentCrop: PercentCrop) =>
setCrop(percentCrop)
}
aspect={1}
circularCrop
className="w-full"
>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
ref={imgRef}
src={selectedPreviewUrl ?? undefined}
alt="Avatar to crop"
onLoad={onImageLoad}
className="h-auto max-h-[50vh] w-full object-contain"
/>
</AnyReactCrop>
</div>
</div>
<div className="mt-6 flex items-center justify-end gap-2">
<Button
variant="ghost"
onClick={handleCancelCrop}
disabled={uploading}
>
{t`Cancel`}
</Button>
<Button onClick={handleSaveCrop} isLoading={uploading}>
{t`Save`}
</Button>
</div>
</div>
</Modal>
)}
</div>
);
}

12
pnpm-lock.yaml generated
View File

@@ -205,6 +205,9 @@ importers:
react-icons:
specifier: ^5.5.0
version: 5.5.0(react@18.3.1)
react-image-crop:
specifier: ^11.0.10
version: 11.0.10(react@18.3.1)
react-lottie-player:
specifier: ^1.5.5
version: 1.5.6(react@18.3.1)
@@ -6116,6 +6119,11 @@ packages:
peerDependencies:
react: '*'
react-image-crop@11.0.10:
resolution: {integrity: sha512-+5FfDXUgYLLqBh1Y/uQhIycpHCbXkI50a+nbfkB1C0xXXUTwkisHDo2QCB1SQJyHCqIuia4FeyReqXuMDKWQTQ==}
peerDependencies:
react: '>=16.13.1'
react-is@16.13.1:
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
@@ -13696,6 +13704,10 @@ snapshots:
dependencies:
react: 18.3.1
react-image-crop@11.0.10(react@18.3.1):
dependencies:
react: 18.3.1
react-is@16.13.1: {}
react-is@17.0.2: {}