Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -127,11 +127,15 @@ class="absolute inset-0 z-30 flex items-center justify-center rounded-full bg-bl
<input
id="nickname"
type="text"
wire:model.blur="data.nickname"
wire:model.blur="nicknameInput"
x-on:scroll-to-nickname.window="$el.scrollIntoView({ behavior: 'smooth', block: 'center' }); $el.focus()"
placeholder="{{ __('panel-app::profile.placeholders.nickname') }}"
maxlength="100"
class="fi-input block w-full rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm text-gray-900 shadow-sm transition-colors focus:border-purple-500 focus:ring-1 focus:ring-purple-500 dark:border-white/10 dark:bg-white/5 dark:text-white dark:focus:border-purple-500"
class="fi-input block w-full rounded-lg border {{ $errors->has('nickname') ? 'border-red-500 focus:border-red-500 focus:ring-red-500' : 'border-gray-300 focus:border-purple-500 focus:ring-purple-500' }} bg-white px-3 py-2 text-sm text-gray-900 shadow-sm transition-colors focus:ring-1 dark:border-white/10 dark:bg-white/5 dark:text-white"
/>
@error('nickname')
<p class="mt-1 text-xs text-red-600 dark:text-red-400">{{ $message }}</p>
@enderror
</div>
<div>
<label for="birthdate" class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300">
Expand All @@ -146,4 +150,4 @@ class="fi-input block w-full rounded-lg border border-gray-300 bg-white px-3 py-
</div>
</div>
</div>
</div>
</div>
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@
])

@php
$name = $user?->name ?? '';
$username = $user?->username ?? '';
$nickname = $data['nickname'] ?? null;
$name = ($nickname ?: null) ?? $user?->name ?? $username;
$headline = $data['headline'] ?? null;
$about = $data['about'] ?? null;
$yearsExperience = $data['years_experience'] ?? null;
Expand Down Expand Up @@ -266,4 +266,4 @@ class="inline-block rounded-full border border-gray-200 bg-gray-50 px-2.5 py-0.5
<div class="border-t border-gray-100 px-6 py-3 dark:border-white/5">
<p class="text-center text-xs text-gray-400 dark:text-gray-500">Esse card aparece na listagem de membros e no seu perfil público.</p>
</div>
</div>
</div>
20 changes: 18 additions & 2 deletions app-modules/panel-app/src/Pages/ProfilePage.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
use He4rt\Profile\Models\Profile;
use He4rt\Profile\Models\Skill;
use Illuminate\Support\Str;
use Illuminate\Validation\ValidationException;
use Livewire\Attributes\Computed;
use Livewire\Attributes\Validate;
use Livewire\Features\SupportFileUploads\TemporaryUploadedFile;
Expand All @@ -61,6 +62,8 @@ class ProfilePage extends Page
#[Validate(rule: 'nullable|image|mimes:jpg,jpeg,png,webp|max:4096')]
public $coverUpload;

public ?string $nicknameInput = null;

protected static string|null|BackedEnum $navigationIcon = 'heroicon-o-user-circle';

protected static ?string $title = 'Profile';
Expand Down Expand Up @@ -98,6 +101,8 @@ public function mount(): void
$profile->preferences->employmentTypes,
),
]);

$this->nicknameInput = $profile->nickname;
}

public function form(Schema $schema): Schema
Expand Down Expand Up @@ -374,13 +379,15 @@ public function form(Schema $schema): Schema

public function save(): void
{
$this->resetErrorBag('nickname');

$formData = $this->form->getState();
$profile = $this->getRecord();

$socialLinks = $this->repeaterToSocialLinks($formData['social_links'] ?? []);

$dto = UpsertProfileDTO::fromArray([
'nickname' => $this->data['nickname'] ?? null,
'nickname' => mb_trim($this->nicknameInput ?? ''),
'birthdate' => $this->data['birthdate'] ?? null,
'about' => $formData['about'] ?? null,
'headline' => $formData['headline'] ?? null,
Expand All @@ -397,7 +404,16 @@ public function save(): void
],
]);

resolve(UpsertProfile::class)->handle($profile, $dto);
try {
resolve(UpsertProfile::class)->handle($profile, $dto);
} catch (ValidationException $validationException) {
$this->addError('nickname', $validationException->validator->errors()->first('nickname'));
$this->dispatch('scroll-to-nickname');

return;
}

$this->data['nickname'] = mb_trim($this->nicknameInput ?? '') ?: null;
Comment on lines +407 to +416

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 | 🟠 Major | ⚡ Quick win

Catch block silently swallows non-nickname validation errors.

UpsertProfile::validate() throws ValidationException for fields beyond nickname (about, headline, years_experience, expected_salary_min, expected_salary_max, social_links). The catch only extracts first('nickname'), which returns '' when no nickname key exists — resulting in an empty error message on the nickname field, a scroll to the wrong input, and the actual error being lost.

This is reachable today: the form has no gte:expected_salary_min rule on expected_salary_max, so min > max passes form validation but fails in UpsertProfile at lines 100–109, and that error is never surfaced.

🐛 Proposed fix
         try {
             resolve(UpsertProfile::class)->handle($profile, $dto);
         } catch (ValidationException $validationException) {
-            $this->addError('nickname', $validationException->validator->errors()->first('nickname'));
-            $this->dispatch('scroll-to-nickname');
+            $errors = $validationException->validator->errors();
+
+            foreach ($errors->keys() as $key) {
+                $this->addError($key, $errors->first($key));
+            }
+
+            if ($errors->has('nickname')) {
+                $this->dispatch('scroll-to-nickname');
+            }

             return;
         }
📝 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
try {
resolve(UpsertProfile::class)->handle($profile, $dto);
} catch (ValidationException $validationException) {
$this->addError('nickname', $validationException->validator->errors()->first('nickname'));
$this->dispatch('scroll-to-nickname');
return;
}
$this->data['nickname'] = mb_trim($this->nicknameInput ?? '') ?: null;
try {
resolve(UpsertProfile::class)->handle($profile, $dto);
} catch (ValidationException $validationException) {
$errors = $validationException->validator->errors();
foreach ($errors->keys() as $key) {
$this->addError($key, $errors->first($key));
}
if ($errors->has('nickname')) {
$this->dispatch('scroll-to-nickname');
}
return;
}
$this->data['nickname'] = mb_trim($this->nicknameInput ?? '') ?: null;
🤖 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 407 - 416, The
catch in ProfilePage::save/update around resolve(UpsertProfile::class)->handle()
is too narrow and only surfaces nickname errors, which can hide validation
failures from UpsertProfile::validate() for other fields. Update the
ValidationException handling to read the actual field-specific error from the
exception/validator for whichever key failed, and only call addError('nickname')
and dispatch('scroll-to-nickname') when nickname is the failing field; otherwise
route the message to the correct input or rethrow/handle generically so
non-nickname validation errors are not lost.


$available = (bool) ($formData['available_for_proposals'] ?? false);
$rawStartAvailability = $formData['start_availability'] ?? null;
Expand Down
2 changes: 1 addition & 1 deletion app-modules/panel-app/tests/Feature/ProfilePageTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@

test('profile page saves all fields', function (): void {
livewire(ProfilePage::class)
->set('data.nickname', 'Dan')
->set('nicknameInput', 'Dan')
->fillForm([
'headline' => 'Backend Developer',
'seniority_level' => 'mid',
Expand Down
12 changes: 9 additions & 3 deletions app-modules/profile/src/Actions/UpsertProfile.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ public function handle(Profile $profile, UpsertProfileDTO $dto): Profile
$attributes = [];

if ($dto->nickname !== null) {
$attributes['nickname'] = $dto->nickname;
$attributes['nickname'] = $dto->nickname === '' ? null : $dto->nickname;
}

if ($dto->birthdate instanceof CarbonInterface) {
Expand Down Expand Up @@ -79,8 +79,14 @@ private function validate(UpsertProfileDTO $dto): void
$errors['headline'] = [__('validation.max.string', ['attribute' => 'headline', 'max' => 100])];
}

if ($dto->nickname !== null && mb_strlen($dto->nickname) > 100) {
$errors['nickname'] = [__('validation.max.string', ['attribute' => 'nickname', 'max' => 100])];
if ($dto->nickname !== null && $dto->nickname !== '') {
if (mb_strlen($dto->nickname) > 100) {
$errors['nickname'] = ['O apelido deve ter no máximo 100 caracteres.'];
} elseif (!preg_match("/^[\p{L}\p{M}\p{N}\s\-']+$/u", $dto->nickname)) {
$errors['nickname'] = ['Apenas letras, números, espaços, hífens e apóstrofos são permitidos.'];
} elseif (!preg_match('/[\p{L}]{2,}/u', $dto->nickname)) {
$errors['nickname'] = ['O apelido deve conter pelo menos 2 letras juntas.'];
}
}

if ($dto->yearsExperience !== null && ($dto->yearsExperience < 0 || $dto->yearsExperience > 50)) {
Expand Down