Skip to content

fix(profile): add about character counter#431

Open
JhonataSnts wants to merge 4 commits into
he4rt:4.xfrom
JhonataSnts:fix/426-profile-about-character-counter
Open

fix(profile): add about character counter#431
JhonataSnts wants to merge 4 commits into
he4rt:4.xfrom
JhonataSnts:fix/426-profile-about-character-counter

Conversation

@JhonataSnts

@JhonataSnts JhonataSnts commented Jul 15, 2026

Copy link
Copy Markdown

O que foi feito

Adiciona um contador de caracteres no campo Sobre da página de perfil, exibindo o progresso no formato atual/500.

A mudança mantém o limite já existente de 500 caracteres e apenas melhora o feedback visual para o usuário durante o preenchimento do perfil.

Detalhes técnicos

  • Adicionado contador no Textarea::make('about').
  • Mantido maxLength(500).
  • Trocado live(onBlur: true) por live() para atualizar o campo durante a digitação.
  • Usado mb_strlen() para respeitar caracteres multibyte.
  • O contador visual é atualizado no cliente para evitar atraso/perda de sincronia durante re-renders do Livewire.

Evidência visual

image

Testes

vendor\bin\pest app-modules\panel-app\tests\Feature\ProfilePageTest.php --compact

Teste manual

  1. Acesse a página de perfil.
  2. Digite no campo Sobre.
  3. Verifique se o contador acompanha a digitação no formato atual/500.

Closes #426

@JhonataSnts
JhonataSnts requested a review from a team July 15, 2026 23:57
@coderabbitai

coderabbitai Bot commented Jul 15, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The profile about textarea now uses live binding and renders a JsContent character counter based on the current state, capped at 500 characters. Feature coverage verifies the textarea type, hint content, maximum length marker, and live binding.

Suggested reviewers: danielhe4rt

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed It clearly summarizes the main change: adding a character counter to the profile About field.
Description check ✅ Passed It describes the same character-counter change and stays on-topic.
Linked Issues check ✅ Passed The change adds a visible real-time 500-character counter for the profile About field and preserves the limit.
Out of Scope Changes check ✅ Passed No unrelated code changes are evident beyond the counter and its test coverage.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app-modules/panel-app/src/Pages/ProfilePage.php`:
- Around line 135-139: Update the character-counting field configuration using
the visible hint callback and field chain to replace immediate live updates with
blur-triggered updates by setting live to onBlur mode. Preserve the existing
hint, length, row, and column settings.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Central YAML (inherited)

Review profile: CHILL

Plan: Pro

Run ID: e77e7255-6e0a-444e-a8b3-f599b58ccab0

📥 Commits

Reviewing files that changed from the base of the PR and between 1fb560d and 28887d6.

📒 Files selected for processing (2)
  • app-modules/panel-app/src/Pages/ProfilePage.php
  • app-modules/panel-app/tests/Feature/ProfilePageTest.php

Comment thread app-modules/panel-app/src/Pages/ProfilePage.php Outdated
->maxLength(500)
->rows(4)
->live(onBlur: true)
->live()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Contador ficou bom. Uma observação (olhando pelo celular, não testei, então é mais hipótese): o número já é atualizado no cliente pelo Alpine, então o ->live() acaba não servindo pra contagem e dispara uma request Livewire a cada tecla à toa.
Acho que dá pra simplificar deixando o Livewire cuidar disso, sem Alpine.

Suggested change
->live()
->hint(fn (?string $state): string => mb_strlen($state ?? '') . '/500')
->live()

Assim o ->live() passa a fazer sentido (é ele que re-renderiza o hint) e some o HtmlString, o aboutCharacterCounterHint(), o wire:ignore e o bloco de Alpine.
Trade-off: com ->live() cada tecla vira uma request; se pesar, dá pra usar ->live(onBlur: true). Como não testei, pode ser que tenha algum motivo pro Alpine que eu não vi. Se for, me corrige.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Eu cheguei a testar uma abordagem nessa linha, deixando o hint(fn...) ser recalculado pelo Livewire com ->live(). Funciona, mas no teste manual o contador ficou dependente do round-trip do Livewire e apresentou atraso/inconsistência durante a digitação. Seguindo a sugestão do @gvieira18, vou ajustar para JsContent, que mantém o contador calculado no cliente, mas remove o HtmlString, o método auxiliar, wire:ignore e o bloco manual de Alpine. Assim o código fica mais simples e mais alinhado com o Filament.

@gvieira18 gvieira18 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Eu apresentei uma opção, o @Clintonrocha98 apresentou outra. Aguardamos mais sugestões

Textarea::make('about')
->label(__('panel-app::profile.fields.about'))
->placeholder(__('panel-app::profile.placeholders.about'))
->hint(fn (?string $state): HtmlString => $this->aboutCharacterCounterHint($state))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Acho que um JsContent::make resolveria o problema e ficando somente do lado do AlpineJS, o que poderia remover* a dependência do ->live para o servidor contar e renderizar a cada alteração.

Algo assim:

Suggested change
->hint(fn (?string $state): HtmlString => $this->aboutCharacterCounterHint($state))
->hint(JsContent::make('`${$state?.length ?? 0}/500`'))

Note

Porém eu não sobre os casos extras tipo por algum motivo passou de 500 chars, qual vai ser o comportamento dele

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Se não me engano o usuário nao consegue passar dos 500

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Usar um array from resolve o problema de passar dos 500 caracteres e também de emojis que podem ter um tamanho maior

->hint(JsContent::make('${Array.from($state ?? "").length}/500'))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Usar um array from resolve o problema de passar dos 500 caracteres e também de emojis que podem ter um tamanho maior

->hint(JsContent::make('${Array.from($state ?? "").length}/500'))

Bem lembrado, emojis podem ter de 2 a 4 chars

@YuriSouzaDev

Copy link
Copy Markdown
Contributor

Sugiro criar um teste com inserção de emojis para ver o comportamento do input.

@ewbenigno

Copy link
Copy Markdown

Revisei e testei localmente esse PR. Registrando os testes que fiz:

  1. Rodei o teste automatizado (ProfilePageTest.php): 14 passed, 1 failed. A falha foi só por falta de build do Vite no meu ambiente local (manifest.json ausente), não relacionada à lógica do PR. Confirmei isso rodando o teste manual via npm run dev, que renderizou a página normalmente.
  2. Teste manual: acessei a página de perfil e digitei no campo Sobre, o contador atualiza em tempo real, sem precisar sair do campo.
  3. Colei um texto de 610 caracteres de uma vez: o campo travou em 500/500.
  4. Testei com emojis: contador mostrou 97/500, contagem bateu com o que eu contaria visualmente.

@YuriSouzaDev

Copy link
Copy Markdown
Contributor

@ewbenigno Mas se criar um teste automatizado é bom que cobre um caso de uso além do seu teste de usuário.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app-modules/panel-app/src/Pages/ProfilePage.php`:
- Line 138: Update the form field configuration containing ->live() to use
live(onBlur: true), so the client-side JsContent counter updates without sending
a network request on every keystroke while preserving live synchronization when
the field loses focus.

In `@app-modules/panel-app/tests/Feature/ProfilePageTest.php`:
- Around line 193-199: Update the form field setup exercised by ProfilePageTest
so the about Textarea uses live(onBlur: true), and change the assertion in the
assertFormFieldExists callback to expect the ['blur'] state binding modifier.
Add automated coverage in ProfilePageTest verifying the field’s emoji behavior
while preserving the existing hint assertions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Central YAML (inherited)

Review profile: CHILL

Plan: Pro

Run ID: 852504c4-2461-4354-a96e-d70682907f38

📥 Commits

Reviewing files that changed from the base of the PR and between 28887d6 and c08f6d9.

📒 Files selected for processing (2)
  • app-modules/panel-app/src/Pages/ProfilePage.php
  • app-modules/panel-app/tests/Feature/ProfilePageTest.php

Comment thread app-modules/panel-app/src/Pages/ProfilePage.php Outdated
Comment on lines +193 to +199
'about' => 'abc',
])
->assertFormFieldExists('about', function (Textarea $field): bool {
expect($field->getHint())->toBeInstanceOf(JsContent::class)
->and($field->getHint()->toHtml())->toContain('Array.from($state')
->toContain('500')
->and($field->getStateBindingModifiers())->toBe(['live']);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Add emoji test coverage and update state binding modifier.

If ->live(onBlur: true) is applied to the field, the expected binding modifier must be updated to ['blur']. As per PR objectives, the emoji behavior should also be covered by an automated test.

♻️ Proposed fix
-            'about' => 'abc',
+            'about' => 'abc 🚀👨‍💻',
         ])
         ->assertFormFieldExists('about', function (Textarea $field): bool {
             expect($field->getHint())->toBeInstanceOf(JsContent::class)
                 ->and($field->getHint()->toHtml())->toContain('Array.from($state')
                 ->toContain('500')
-                ->and($field->getStateBindingModifiers())->toBe(['live']);
+                ->and($field->getStateBindingModifiers())->toBe(['blur']);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
'about' => 'abc',
])
->assertFormFieldExists('about', function (Textarea $field): bool {
expect($field->getHint())->toBeInstanceOf(JsContent::class)
->and($field->getHint()->toHtml())->toContain('Array.from($state')
->toContain('500')
->and($field->getStateBindingModifiers())->toBe(['live']);
'about' => 'abc 🚀👨‍💻',
])
->assertFormFieldExists('about', function (Textarea $field): bool {
expect($field->getHint())->toBeInstanceOf(JsContent::class)
->and($field->getHint()->toHtml())->toContain('Array.from($state')
->toContain('500')
->and($field->getStateBindingModifiers())->toBe(['blur']);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app-modules/panel-app/tests/Feature/ProfilePageTest.php` around lines 193 -
199, Update the form field setup exercised by ProfilePageTest so the about
Textarea uses live(onBlur: true), and change the assertion in the
assertFormFieldExists callback to expect the ['blur'] state binding modifier.
Add automated coverage in ProfilePageTest verifying the field’s emoji behavior
while preserving the existing hint assertions.

@JhonataSnts

Copy link
Copy Markdown
Author

Sugiro criar um teste com inserção de emojis para ver o comportamento do input.

Ajustei a expressão para usar Array.from($state ?? "").length em vez de .length, o que lida melhor com emojis e caracteres especiais nesse contador.

@JhonataSnts

Copy link
Copy Markdown
Author

Atualizei novamente seguindo os últimos comentários:

  • mantive o contador com JsContent, calculado no cliente;
  • troquei o campo para live(onBlur: true) para evitar requests Livewire a cada tecla;
  • ajustei o teste para esperar ['live', 'blur'];
  • incluí um valor com emoji no teste e mantive Array.from($state ?? "").length para lidar melhor com esse caso.

Rodei:

vendor\bin\pest app-modules\panel-app\tests\Feature\ProfilePageTest.php --compact

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app-modules/panel-app/src/Pages/ProfilePage.php`:
- Around line 135-138: Update the counter expression in the profile text field
to measure UTF-16 code units, matching the 500-character maxLength enforcement,
instead of using Array.from(...).length. Preserve the existing null-safe state
handling and 500-character display limit.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Central YAML (inherited)

Review profile: CHILL

Plan: Pro

Run ID: 85b9d964-dbd8-4878-b992-1efc93e62d24

📥 Commits

Reviewing files that changed from the base of the PR and between c08f6d9 and 5833cf9.

📒 Files selected for processing (2)
  • app-modules/panel-app/src/Pages/ProfilePage.php
  • app-modules/panel-app/tests/Feature/ProfilePageTest.php

Comment on lines +135 to 138
->hint(JsContent::make('`${Array.from($state ?? "").length}/500`'))
->maxLength(500)
->rows(4)
->live(onBlur: true)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="app-modules/panel-app/src/Pages/ProfilePage.php"

echo "== file size =="
wc -l "$FILE"

echo
echo "== relevant lines =="
sed -n '120,150p' "$FILE" | cat -n

Repository: he4rt/heartdevs.com

Length of output: 2210


🌐 Web query:

HTML maxlength UTF-16 code units textarea maxlength semantics MDN

💡 Result:

The HTML maxlength attribute for textarea elements defines the maximum length of the element's value, measured in UTF-16 code units [1][2]. Key semantic details include: Measurement Unit: The limit is specifically defined by the number of UTF-16 code units, not necessarily the number of user-perceived characters (grapheme clusters) [1]. Since many common characters fit into a single 16-bit code unit, this often matches the character count, but characters outside the Basic Multilingual Plane (such as certain emojis or complex symbols) require two UTF-16 code units (a surrogate pair), meaning they count as two against the maxlength limit [1][3]. Newline Normalization: For textarea elements, the length calculation includes a normalization step where newline characters are counted as a single character, regardless of whether the platform uses CR, LF, or CRLF line endings [4][5]. Constraint Validation: The maxlength constraint is evaluated only when the value is modified by the user [1][6]. If the text length exceeds the specified limit, the element is considered invalid, and the ValidityState object's tooLong property will be true [1][7]. Browsers typically prevent users from entering text beyond the specified limit, but this constraint is not applied when values are set programmatically (e.g., via JavaScript) [1][5]. API Properties: - The HTMLTextAreaElement.maxLength property reflects the maxlength attribute, returning the limit or -1 if no limit exists [7]. - The HTMLTextAreaElement.textLength property provides the current length of the textarea's value in UTF-16 code units [8].

Citations:


Keep the counter on UTF-16 units. Array.from($state ?? "").length counts code points, but maxLength(500) is enforced in UTF-16 units, so emoji/astral characters will show a smaller count than the actual limit.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app-modules/panel-app/src/Pages/ProfilePage.php` around lines 135 - 138,
Update the counter expression in the profile text field to measure UTF-16 code
units, matching the 500-character maxLength enforcement, instead of using
Array.from(...).length. Preserve the existing null-safe state handling and
500-character display limit.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Campo "Sobre" no perfil não exibe contagem/limite de caracteres (500)

6 participants