From cde25c37dc53ee1f1ad0338570600b12ff4d410e Mon Sep 17 00:00:00 2001 From: fernanduandrade Date: Tue, 21 Jul 2026 21:28:58 -0300 Subject: [PATCH 1/2] fix: corrigir novo campo de skills, adicionado linkedin e links redirect no profile preview --- app-modules/panel-app/lang/en/profile.php | 9 ++ app-modules/panel-app/lang/pt_BR/profile.php | 9 ++ .../components/profile-preview-card.blade.php | 27 +++-- .../panel-app/src/Pages/ProfilePage.php | 107 ++++++++++++++++-- app-modules/profile/lang/en/enums.php | 1 + app-modules/profile/lang/pt_BR/enums.php | 1 + .../profile/src/Enums/SocialPlatform.php | 20 ++++ .../profile/tests/Unit/SocialPlatformTest.php | 59 ++++++++++ 8 files changed, 213 insertions(+), 20 deletions(-) create mode 100644 app-modules/profile/tests/Unit/SocialPlatformTest.php diff --git a/app-modules/panel-app/lang/en/profile.php b/app-modules/panel-app/lang/en/profile.php index 39a63f386..c8461e68e 100644 --- a/app-modules/panel-app/lang/en/profile.php +++ b/app-modules/panel-app/lang/en/profile.php @@ -87,4 +87,13 @@ 'page' => [ 'subtitle' => 'Fill in the fields and watch your card being built in real time', ], + + 'preview' => [ + 'available' => 'Available', + 'years_experience' => ':count year of exp.|:count years of exp.', + 'skills' => 'Skills', + 'experience' => 'Experience', + 'can_start' => 'Can start:', + 'footer' => 'This card appears in the member listing and on your public profile.', + ], ]; diff --git a/app-modules/panel-app/lang/pt_BR/profile.php b/app-modules/panel-app/lang/pt_BR/profile.php index 413407ba6..57671c1e8 100644 --- a/app-modules/panel-app/lang/pt_BR/profile.php +++ b/app-modules/panel-app/lang/pt_BR/profile.php @@ -87,4 +87,13 @@ 'page' => [ 'subtitle' => 'Preencha os campos e veja seu card sendo montado em tempo real', ], + + 'preview' => [ + 'available' => 'Disponível', + 'years_experience' => ':count ano de exp.|:count anos de exp.', + 'skills' => 'Skills', + 'experience' => 'Experiência', + 'can_start' => 'Pode iniciar:', + 'footer' => 'Esse card aparece na listagem de membros e no seu perfil público.', + ], ]; diff --git a/app-modules/panel-app/resources/views/components/profile-preview-card.blade.php b/app-modules/panel-app/resources/views/components/profile-preview-card.blade.php index 5386dac5f..f10c5700d 100644 --- a/app-modules/panel-app/resources/views/components/profile-preview-card.blade.php +++ b/app-modules/panel-app/resources/views/components/profile-preview-card.blade.php @@ -123,7 +123,7 @@ class="flex h-16 w-16 items-center justify-center rounded-full border-4 border-w class="inline-flex items-center gap-1 rounded-full bg-green-50 px-2 py-0.5 text-xs font-medium text-green-700 dark:bg-green-500/10 dark:text-green-400" > - Disponível + {{ __('panel-app::profile.preview.available') }} @endif @@ -165,7 +165,7 @@ class="inline-flex items-center gap-1 rounded-full bg-green-50 px-2 py-0.5 text- @endif @if ($yearsExperience) - {{ $yearsExperience }} {{ $yearsExperience == 1 ? 'ano' : 'anos' }} de exp. + {{ trans_choice('panel-app::profile.preview.years_experience', $yearsExperience, ['count' => $yearsExperience]) }} @endif @@ -180,7 +180,7 @@ class="inline-flex items-center gap-1 rounded-full bg-green-50 px-2 py-0.5 text- @if ($skills->isNotEmpty())
- Skills + {{ __('panel-app::profile.preview.skills') }}
@foreach ($skills as $skill) @@ -203,7 +203,7 @@ class="inline-flex items-center gap-1 rounded-md bg-purple-50 px-2 py-0.5 text-x
Experiência{{ __('panel-app::profile.preview.experience') }} - {{ $platform->getLabel() }} {{ $link['handle'] }} - + + + {{ $platform->getLabel() }} + + + + @endif @endforeach
@@ -257,13 +266,13 @@ class="inline-block rounded-full border border-gray-200 bg-gray-50 px-2.5 py-0.5 {{-- Start availability --}} @if ($available && $startAvailability)

- Pode iniciar: {{ $startAvailability->getLabel() }} + {{ __('panel-app::profile.preview.can_start') }} {{ $startAvailability->getLabel() }}

@endif
{{-- Footer --}}
-

Esse card aparece na listagem de membros e no seu perfil público.

+

{{ __('panel-app::profile.preview.footer') }}

diff --git a/app-modules/panel-app/src/Pages/ProfilePage.php b/app-modules/panel-app/src/Pages/ProfilePage.php index 350b4d02d..32a6f8a6c 100644 --- a/app-modules/panel-app/src/Pages/ProfilePage.php +++ b/app-modules/panel-app/src/Pages/ProfilePage.php @@ -147,13 +147,52 @@ public function form(Schema $schema): Schema ->getOptionLabelUsing(fn (?string $value): ?string => $value === null ? null : (Skill::labelsById()[$value] ?? null)) ->optionsLimit(50) ->distinct() - ->required() + ->required(fn (Get $get): bool => filled($get('proficiency'))) ->columnSpan(1), Select::make('proficiency') ->label(__('panel-app::profile.fields.proficiency')) ->options(SkillProficiency::class) - ->required() + ->required(fn (Get $get): bool => filled($get('skill_id'))) + ->live() + ->afterStateUpdated(function (Get $get, $component): void { + if (blank($get('skill_id')) || blank($get('proficiency'))) { + return; + } + + $repeater = $component->getParentRepeater(); + + if (!$repeater) { + return; + } + + $items = $repeater->getRawState(); + + if (blank($items)) { + return; + } + + // Only auto-add when filling the last row + $repeaterPath = $repeater->getStatePath(); + $currentKey = explode('.', mb_substr((string) $component->getStatePath(), mb_strlen($repeaterPath) + 1))[0]; + + if ($currentKey !== array_key_last($items)) { + return; + } + + $newUuid = $repeater->generateUuid(); + + if ($newUuid) { + $items[$newUuid] = []; + } else { + $items[] = []; + } + + $repeater->rawState($items); + $repeater->getChildSchema($newUuid ?? array_key_last($items))->fill(); + $repeater->collapsed(false, shouldMakeComponentCollapsible: false); + $repeater->callAfterStateUpdated(); + }) ->columnSpan(1), TextInput::make('years_experience') @@ -221,13 +260,51 @@ public function form(Schema $schema): Schema Select::make('platform') ->label(__('panel-app::profile.fields.platform')) ->options(SocialPlatform::class) - ->required() + ->required(fn (Get $get): bool => filled($get('handle'))) ->columnSpan(1), TextInput::make('handle') ->label(__('panel-app::profile.fields.handle')) ->placeholder(__('panel-app::profile.placeholders.handle')) - ->required() + ->required(fn (Get $get): bool => filled($get('platform'))) + ->live(onBlur: true) + ->afterStateUpdated(function (Get $get, $component): void { + if (blank($get('platform')) || blank($get('handle'))) { + return; + } + + $repeater = $component->getParentRepeater(); + + if (!$repeater) { + return; + } + + $items = $repeater->getRawState(); + + if (blank($items)) { + return; + } + + $repeaterPath = $repeater->getStatePath(); + $currentKey = explode('.', mb_substr((string) $component->getStatePath(), mb_strlen($repeaterPath) + 1))[0]; + + if ($currentKey !== array_key_last($items)) { + return; + } + + $newUuid = $repeater->generateUuid(); + + if ($newUuid) { + $items[$newUuid] = []; + } else { + $items[] = []; + } + + $repeater->rawState($items); + $repeater->getChildSchema($newUuid ?? array_key_last($items))->fill(); + $repeater->collapsed(false, shouldMakeComponentCollapsible: false); + $repeater->callAfterStateUpdated(); + }) ->columnSpan(1), ]), ]) @@ -301,18 +378,18 @@ public function form(Schema $schema): Schema Grid::make(2)->schema([ TextInput::make('company_name') ->label(__('panel-app::profile.fields.company_name')) - ->required() + ->required(fn (Get $get): bool => filled($get('position')) || filled($get('description')) || filled($get('start_date'))) ->maxLength(255) ->columnSpan(1), TextInput::make('position') ->label(__('panel-app::profile.fields.position')) - ->required() + ->required(fn (Get $get): bool => filled($get('company_name')) || filled($get('description')) || filled($get('start_date'))) ->maxLength(255) ->columnSpan(1), ]), Textarea::make('description') ->label(__('panel-app::profile.fields.experience_description')) - ->required() + ->required(fn (Get $get): bool => filled($get('company_name')) || filled($get('position')) || filled($get('start_date'))) ->rows(3) ->maxLength(2_000) ->columnSpanFull(), @@ -323,7 +400,7 @@ public function form(Schema $schema): Schema ->displayFormat('M Y') ->format('Y-m-d') ->maxDate(now()) - ->required() + ->required(fn (Get $get): bool => filled($get('company_name')) || filled($get('position')) || filled($get('description'))) ->columnSpan(1), DatePicker::make('end_date') ->label(__('panel-app::profile.fields.end_date')) @@ -332,7 +409,7 @@ public function form(Schema $schema): Schema ->format('Y-m-d') ->maxDate(now()) ->afterOrEqual('start_date') - ->required(fn (Get $get): bool => !$get('is_currently_working_here')) + ->required(fn (Get $get): bool => !$get('is_currently_working_here') && (filled($get('company_name')) || filled($get('position')) || filled($get('description')) || filled($get('start_date')))) ->hidden(fn (Get $get): bool => (bool) $get('is_currently_working_here')) ->columnSpan(1), ]), @@ -606,10 +683,18 @@ private function repeaterToSocialLinks(array $repeaterData): array /** * @param array $data - * @return array + * @return array|null null tells Filament to skip creating/saving this row */ - private function normalizeWorkExperienceData(array $data): array + private function normalizeWorkExperienceData(array $data): ?array { + $keyFields = ['company_name', 'position', 'description', 'start_date']; + + $hasAnyData = collect($keyFields)->contains(fn (string $field): bool => filled($data[$field] ?? null)); + + if (!$hasAnyData) { + return null; + } + if ($data['is_currently_working_here'] ?? false) { $data['end_date'] = null; } diff --git a/app-modules/profile/lang/en/enums.php b/app-modules/profile/lang/en/enums.php index 685489637..3ffd48b77 100644 --- a/app-modules/profile/lang/en/enums.php +++ b/app-modules/profile/lang/en/enums.php @@ -17,6 +17,7 @@ 'website' => 'Website', 'youtube' => 'YouTube', 'bluesky' => 'Bluesky', + 'linkedin' => 'LinkedIn', ], 'skill_category' => [ diff --git a/app-modules/profile/lang/pt_BR/enums.php b/app-modules/profile/lang/pt_BR/enums.php index e01c86c7d..1b1fa3b9b 100644 --- a/app-modules/profile/lang/pt_BR/enums.php +++ b/app-modules/profile/lang/pt_BR/enums.php @@ -17,6 +17,7 @@ 'website' => 'Website', 'youtube' => 'YouTube', 'bluesky' => 'Bluesky', + 'linkedin' => 'LinkedIn', ], 'skill_category' => [ diff --git a/app-modules/profile/src/Enums/SocialPlatform.php b/app-modules/profile/src/Enums/SocialPlatform.php index 8dccf0275..db6eaf65d 100644 --- a/app-modules/profile/src/Enums/SocialPlatform.php +++ b/app-modules/profile/src/Enums/SocialPlatform.php @@ -15,6 +15,7 @@ enum SocialPlatform: string implements HasIcon, HasLabel case Website = 'website'; case YouTube = 'youtube'; case Bluesky = 'bluesky'; + case LinkedIn = 'linkedin'; /** * @return array @@ -40,6 +41,25 @@ public function getIcon(): Heroicon self::Website => Heroicon::GlobeAlt, self::YouTube => Heroicon::PlayCircle, self::Bluesky => Heroicon::Cloud, + self::LinkedIn => Heroicon::Link + }; + } + + public function getUrl(string $handle): string + { + if (str_starts_with($handle, 'http://') || str_starts_with($handle, 'https://')) { + return $handle; + } + + $slug = mb_ltrim($handle, '@'); + + return match ($this) { + self::Instagram => 'https://instagram.com/'.$slug, + self::Twitter => 'https://x.com/'.$slug, + self::LinkedIn => 'https://linkedin.com/in/'.$slug, + self::YouTube => 'https://youtube.com/@'.$slug, + self::Bluesky => 'https://bsky.app/profile/'.$slug, + self::Website => 'https://'.$slug, }; } } diff --git a/app-modules/profile/tests/Unit/SocialPlatformTest.php b/app-modules/profile/tests/Unit/SocialPlatformTest.php new file mode 100644 index 000000000..d7da83f3c --- /dev/null +++ b/app-modules/profile/tests/Unit/SocialPlatformTest.php @@ -0,0 +1,59 @@ + ['johndoe'], + 'username with @' => ['@johndoe'], +]); + +test('instagram builds correct url from handle', function (string $handle): void { + expect(SocialPlatform::Instagram->getUrl($handle)) + ->toBe('https://instagram.com/johndoe'); +})->with('username handles'); + +test('twitter builds correct url from handle', function (string $handle): void { + expect(SocialPlatform::Twitter->getUrl($handle)) + ->toBe('https://x.com/johndoe'); +})->with('username handles'); + +test('linkedin builds correct url from handle', function (string $handle): void { + expect(SocialPlatform::LinkedIn->getUrl($handle)) + ->toBe('https://linkedin.com/in/johndoe'); +})->with('username handles'); + +test('youtube builds correct url from handle', function (string $handle): void { + expect(SocialPlatform::YouTube->getUrl($handle)) + ->toBe('https://youtube.com/@johndoe'); +})->with('username handles'); + +test('bluesky builds correct url from handle', function (string $handle): void { + expect(SocialPlatform::Bluesky->getUrl($handle)) + ->toBe('https://bsky.app/profile/johndoe'); +})->with('username handles'); + +test('website prepends https when no scheme is provided', function (): void { + expect(SocialPlatform::Website->getUrl('example.dev')) + ->toBe('https://example.dev'); +}); + +test('full https url is returned as-is for any platform', function (): void { + $url = 'https://www.linkedin.com/in/johndoe/'; + + expect(SocialPlatform::LinkedIn->getUrl($url))->toBe($url); +}); + +test('full http url is returned as-is for any platform', function (): void { + $url = 'http://example.dev'; + + expect(SocialPlatform::Website->getUrl($url))->toBe($url); +}); + +test('full url is not double-prefixed for all platforms', function (): void { + foreach (SocialPlatform::cases() as $platform) { + $url = 'https://example.com/foo'; + expect($platform->getUrl($url))->toBe($url); + } +}); From 9959ce74b704ca49656ab19287853f9ed7571ff1 Mon Sep 17 00:00:00 2001 From: fernanduandrade Date: Wed, 22 Jul 2026 21:42:47 -0300 Subject: [PATCH 2/2] =?UTF-8?q?fix(Profile):=20ajustar=20de=20acordo=20com?= =?UTF-8?q?=20a=20an=C3=A1lise=20do=20phpstan?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../panel-app/src/Pages/ProfilePage.php | 124 ++++++++---------- 1 file changed, 57 insertions(+), 67 deletions(-) diff --git a/app-modules/panel-app/src/Pages/ProfilePage.php b/app-modules/panel-app/src/Pages/ProfilePage.php index 32a6f8a6c..5c7c6a93a 100644 --- a/app-modules/panel-app/src/Pages/ProfilePage.php +++ b/app-modules/panel-app/src/Pages/ProfilePage.php @@ -155,43 +155,12 @@ public function form(Schema $schema): Schema ->options(SkillProficiency::class) ->required(fn (Get $get): bool => filled($get('skill_id'))) ->live() - ->afterStateUpdated(function (Get $get, $component): void { + ->afterStateUpdated(function (Get $get, Select $component): void { if (blank($get('skill_id')) || blank($get('proficiency'))) { return; } - $repeater = $component->getParentRepeater(); - - if (!$repeater) { - return; - } - - $items = $repeater->getRawState(); - - if (blank($items)) { - return; - } - - // Only auto-add when filling the last row - $repeaterPath = $repeater->getStatePath(); - $currentKey = explode('.', mb_substr((string) $component->getStatePath(), mb_strlen($repeaterPath) + 1))[0]; - - if ($currentKey !== array_key_last($items)) { - return; - } - - $newUuid = $repeater->generateUuid(); - - if ($newUuid) { - $items[$newUuid] = []; - } else { - $items[] = []; - } - - $repeater->rawState($items); - $repeater->getChildSchema($newUuid ?? array_key_last($items))->fill(); - $repeater->collapsed(false, shouldMakeComponentCollapsible: false); - $repeater->callAfterStateUpdated(); + $this->appendEmptyRepeaterItemIfLastRow($component); }) ->columnSpan(1), @@ -268,42 +237,12 @@ public function form(Schema $schema): Schema ->placeholder(__('panel-app::profile.placeholders.handle')) ->required(fn (Get $get): bool => filled($get('platform'))) ->live(onBlur: true) - ->afterStateUpdated(function (Get $get, $component): void { + ->afterStateUpdated(function (Get $get, TextInput $component): void { if (blank($get('platform')) || blank($get('handle'))) { return; } - $repeater = $component->getParentRepeater(); - - if (!$repeater) { - return; - } - - $items = $repeater->getRawState(); - - if (blank($items)) { - return; - } - - $repeaterPath = $repeater->getStatePath(); - $currentKey = explode('.', mb_substr((string) $component->getStatePath(), mb_strlen($repeaterPath) + 1))[0]; - - if ($currentKey !== array_key_last($items)) { - return; - } - - $newUuid = $repeater->generateUuid(); - - if ($newUuid) { - $items[$newUuid] = []; - } else { - $items[] = []; - } - - $repeater->rawState($items); - $repeater->getChildSchema($newUuid ?? array_key_last($items))->fill(); - $repeater->collapsed(false, shouldMakeComponentCollapsible: false); - $repeater->callAfterStateUpdated(); + $this->appendEmptyRepeaterItemIfLastRow($component); }) ->columnSpan(1), ]), @@ -720,6 +659,53 @@ private function skillsToRepeater(Profile $profile): array return $skills; } + private function appendEmptyRepeaterItemIfLastRow(Select|TextInput $component): void + { + $repeater = $component->getParentRepeater(); + + if (!$repeater instanceof Repeater) { + return; + } + + $items = $repeater->getRawState(); + + if (!is_array($items) || blank($items)) { + return; + } + + $repeaterPath = $repeater->getStatePath(); + $componentPath = $component->getStatePath(); + + if ($repeaterPath === null || $componentPath === null) { + return; + } + + $currentKey = explode('.', mb_substr($componentPath, mb_strlen($repeaterPath) + 1))[0]; + + if ($currentKey !== array_key_last($items)) { + return; + } + + $newUuid = $repeater->generateUuid(); + + if ($newUuid) { + $items[$newUuid] = []; + } else { + $items[] = []; + } + + $repeater->rawState($items); + + $childSchema = $repeater->getChildSchema($newUuid ?? array_key_last($items)); + + if ($childSchema instanceof Schema) { + $childSchema->fill(); + } + + $repeater->collapsed(condition: false, shouldMakeComponentCollapsible: false); + $repeater->callAfterStateUpdated(); + } + /** * Skill ids already chosen in the other rows of the skills repeater, so the * search can omit them and each skill is only pickable once. @@ -729,13 +715,17 @@ private function skillsToRepeater(Profile $profile): array private function skillIdsInSiblingRows(Select $component): array { $repeater = $component->getParentRepeater(); - if ($repeater === null) { + + if (!$repeater instanceof Repeater) { return []; } - /** @var array> $rows */ $rows = $repeater->getRawState(); + if (!is_array($rows)) { + return []; + } + return array_values( collect($rows) ->pluck('skill_id')