diff --git a/components/BioEditor.tsx b/components/BioEditor.tsx index dca5287..c9800d3 100644 --- a/components/BioEditor.tsx +++ b/components/BioEditor.tsx @@ -10,6 +10,27 @@ import clsx from 'clsx' import type { BioLanguage, BioPostContent } from '@/lib/bio-api' import BioEditorToolbar from '@/components/BioEditorToolbar' +// Bio length targets. The team agreed bios should land around 80 words; the +// zones below give the author live feedback as they type. Boundaries are +// inclusive on the green side so 70 and 90 read as "good", not "borderline". +const BIO_TARGET_MIN = 70 +const BIO_TARGET_MAX = 90 +const BIO_WARNING_MIN = 60 +const BIO_WARNING_MAX = 100 + +function countWords(text: string): number { + // Plain whitespace split is fine for the 6 European languages we support. + // CJK / Arabic would need Intl.Segmenter with granularity: 'word'. + const trimmed = text.trim() + return trimmed === '' ? 0 : trimmed.split(/\s+/).length +} + +function wordCountZone(count: number): 'green' | 'orange' | 'red' { + if (count >= BIO_TARGET_MIN && count <= BIO_TARGET_MAX) return 'green' + if (count >= BIO_WARNING_MIN && count <= BIO_WARNING_MAX) return 'orange' + return 'red' +} + type EditableFields = { member_title: string member_linkedin_url: string @@ -41,6 +62,7 @@ export default function BioEditor({ const [isDirty, setIsDirty] = useState(false) const [isSaving, setIsSaving] = useState(false) const [isLoadingLang, setIsLoadingLang] = useState(false) + const [wordCount, setWordCount] = useState(0) const [status, setStatus] = useState< | { kind: 'idle' } | { kind: 'success'; queued: string[] } @@ -81,7 +103,13 @@ export default function BioEditor({ ], content: initialPost.content, immediatelyRender: false, - onUpdate: markDirty, + onCreate({ editor }) { + setWordCount(countWords(editor.getText())) + }, + onUpdate({ editor }) { + markDirty() + setWordCount(countWords(editor.getText())) + }, editorProps: { attributes: { // rounded-b-md (not rounded-md) + border-t-0 because the @@ -135,6 +163,12 @@ export default function BioEditor({ setCurrentTitle(post.title) setFields(postToEditable(post)) editor?.commands.setContent(post.content, { emitUpdate: false }) + // setContent with emitUpdate: false suppresses onUpdate by design + // (we don't want the language switch to flip isDirty), so sync the + // word count manually here. + if (editor) { + setWordCount(countWords(editor.getText())) + } setIsDirty(false) } catch (err) { setStatus({ kind: 'error', message: (err as Error).message }) @@ -226,6 +260,22 @@ export default function BioEditor({
+
+ + {t('wordCount', { count: wordCount })} + + + {t('wordCountTarget', { min: BIO_TARGET_MIN, max: BIO_TARGET_MAX })} + +
diff --git a/messages/de.json b/messages/de.json index 002e961..6bbb4e8 100644 --- a/messages/de.json +++ b/messages/de.json @@ -305,6 +305,8 @@ "savedNoChange": "Gespeichert.", "saveError": "Speichern fehlgeschlagen: {error}", "unsavedSwitch": "Du hast nicht gespeicherte \u00c4nderungen. Verwerfen und Sprache wechseln?", + "wordCount": "{count, plural, =0 {Keine W\u00f6rter} one {# Wort} other {# W\u00f6rter}}", + "wordCountTarget": "Ziel {min}-{max} W\u00f6rter", "notLinked": "Dein Konto ist mit keinem team_member-Profil verkn\u00fcpft. Wende dich an einen Administrator.", "noLanguages": "Es sind noch keine Sprachversionen zur Bearbeitung verf\u00fcgbar.", "loadError": "Diese Version deiner Bio konnte nicht geladen werden.", diff --git a/messages/en.json b/messages/en.json index 3072bf4..daf2dfe 100644 --- a/messages/en.json +++ b/messages/en.json @@ -305,6 +305,8 @@ "savedNoChange": "Saved.", "saveError": "Could not save: {error}", "unsavedSwitch": "You have unsaved changes. Discard them and switch language?", + "wordCount": "{count, plural, =0 {No words yet} one {# word} other {# words}}", + "wordCountTarget": "target {min}–{max} words", "notLinked": "Your account isn't linked to a team_member profile. Contact an administrator.", "noLanguages": "No language versions are available to edit yet.", "loadError": "Could not load this version of your bio.", diff --git a/messages/es.json b/messages/es.json index 434b858..7129ed6 100644 --- a/messages/es.json +++ b/messages/es.json @@ -305,6 +305,8 @@ "savedNoChange": "Guardado.", "saveError": "No se pudo guardar: {error}", "unsavedSwitch": "Tienes cambios sin guardar. \u00bfDescartarlos y cambiar de idioma?", + "wordCount": "{count, plural, =0 {Sin palabras} one {# palabra} other {# palabras}}", + "wordCountTarget": "objetivo {min}-{max} palabras", "notLinked": "Tu cuenta no est\u00e1 vinculada a un perfil de team_member. Contacta a un administrador.", "noLanguages": "A\u00fan no hay versiones en otros idiomas para editar.", "loadError": "No se pudo cargar esta versi\u00f3n de tu biograf\u00eda.", diff --git a/messages/fr.json b/messages/fr.json index 7726cf9..f7f95be 100644 --- a/messages/fr.json +++ b/messages/fr.json @@ -305,6 +305,8 @@ "savedNoChange": "Enregistr\u00e9.", "saveError": "\u00c9chec de l'enregistrement : {error}", "unsavedSwitch": "Vous avez des modifications non enregistr\u00e9es. Les abandonner et changer de langue ?", + "wordCount": "{count, plural, =0 {Aucun mot} one {# mot} other {# mots}}", + "wordCountTarget": "cible {min}-{max} mots", "notLinked": "Votre compte n'est pas li\u00e9 \u00e0 un profil team_member. Contactez un administrateur.", "noLanguages": "Aucune version linguistique n'est disponible \u00e0 la modification.", "loadError": "Impossible de charger cette version de votre bio.", diff --git a/messages/it.json b/messages/it.json index e040ae6..2f88810 100644 --- a/messages/it.json +++ b/messages/it.json @@ -305,6 +305,8 @@ "savedNoChange": "Salvato.", "saveError": "Impossibile salvare: {error}", "unsavedSwitch": "Hai modifiche non salvate. Scartarle e cambiare lingua?", + "wordCount": "{count, plural, =0 {Nessuna parola} one {# parola} other {# parole}}", + "wordCountTarget": "obiettivo {min}-{max} parole", "notLinked": "Il tuo account non \u00e8 collegato a un profilo team_member. Contatta un amministratore.", "noLanguages": "Nessuna versione linguistica disponibile per la modifica.", "loadError": "Impossibile caricare questa versione della tua bio.", diff --git a/messages/pt.json b/messages/pt.json index c9a1c2b..b68540e 100644 --- a/messages/pt.json +++ b/messages/pt.json @@ -305,6 +305,8 @@ "savedNoChange": "Guardado.", "saveError": "N\u00e3o foi poss\u00edvel guardar: {error}", "unsavedSwitch": "Tem altera\u00e7\u00f5es por guardar. Descart\u00e1-las e mudar de idioma?", + "wordCount": "{count, plural, =0 {Sem palavras} one {# palavra} other {# palavras}}", + "wordCountTarget": "alvo {min}-{max} palavras", "notLinked": "A sua conta n\u00e3o est\u00e1 ligada a um perfil team_member. Contacte um administrador.", "noLanguages": "Ainda n\u00e3o h\u00e1 vers\u00f5es lingu\u00edsticas dispon\u00edveis para edi\u00e7\u00e3o.", "loadError": "N\u00e3o foi poss\u00edvel carregar esta vers\u00e3o da sua bio.",