Skip to content

feat: Public Profile Page (#257)#305

Open
GustavoSimao wants to merge 2 commits into
4.xfrom
feat/public-profile-page
Open

feat: Public Profile Page (#257)#305
GustavoSimao wants to merge 2 commits into
4.xfrom
feat/public-profile-page

Conversation

@GustavoSimao

@GustavoSimao GustavoSimao commented Jun 3, 2026

Copy link
Copy Markdown
Member

Description

Adds a public profile page accessible at /@{username}. Implements ProfileController to load tenant-scoped user/profile data, builds connected accounts, registers a public route, and introduces a responsive Blade view rendering avatar, headline, social links, badges and recruiter summary. Feature tests cover successful displays and error cases.

References

Dependencies & Requirements

  • No new composer/npm dependencies added.
  • No environment variables or configuration changes required.
  • Compatible with existing tenant-scoped profile structures and provider enum mappings.

Contributor Summary

Contributor Lines Added Lines Removed Files Changed
GustavoSimao 529 1 5

Changes Summary

File Path Change Description
app-modules/portal/resources/views/components/layouts/app.blade.php Removed default class="dark" from <html> root.
app-modules/profile/resources/views/public-profile.blade.php New public profile Blade view: avatar, headline, about, social links, badges, connected accounts, responsive layout.
app-modules/profile/src/Http/ProfileController.php New controller: tenant/user/profile lookup, buildConnectedAccounts logic, renders public profile.
app-modules/profile/src/ProfileServiceProvider.php Registered profile views namespace and public route GET /@{username}.
app-modules/profile/tests/Feature/PublicProfileTest.php New feature tests for public profile display, minimal profiles, and 404 scenarios.

@GustavoSimao GustavoSimao requested a review from a team June 3, 2026 18:58
@coderabbitai

coderabbitai Bot commented Jun 3, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Refactors SeniorityLevel enum (mid→pleno), introduces SkillCategory and Skills utility classes for categorized skill validation with icon mapping, extends SocialPlatform with four new platforms, and adds English/Portuguese translations. Creates user_projects and user_pull_requests database tables with tenant scoping and metadata fields. Extends Profile model with HasMedia, projects/pullRequests relationships, media collection registration, and skillsByCategory helper with attribute mutation. Adds Project and PullRequest models with relationships and attribute casting. Updates UpsertProfileDTO and UpsertProfile to persist skills, work_types, and languages. Implements ProfileController::show to resolve tenant by host, load user and profile with relations, build normalized connected accounts, and render public-profile Blade template. Creates responsive public profile view with avatar, headline, social links, about section, skill categories, projects, pull requests, and character badges. Adds profile.css with design tokens, dark-mode support, avatar animation ring, and component styling. Registers public route GET /@{username}. Synchronizes seniority level references in panel-app preview component and tests. Adds comprehensive feature and unit test coverage.

Possibly related PRs

  • he4rt/heartdevs.com#274: Modifies core profile module files (Profile model, ProfileFactory, SeniorityLevel enum, translations, ProfileServiceProvider) to add the public profile UI feature with skills, work types, languages, projects, PRs, and media handling.
  • he4rt/heartdevs.com#282: Extends the profile DTO/action flow (UpsertProfileDTO and UpsertProfile) introduced in that PR to persist additional fields.

Suggested reviewers

  • danielhe4rt
  • gvieira18
  • Clintonrocha98
🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning No description was provided by the author, making it impossible to assess relevance to the changeset. Add a description explaining the public profile page implementation, related features, and any important context.
Docstring Coverage ⚠️ Warning Docstring coverage is 46.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main change: implementing a public profile page feature.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
⚔️ Resolve merge conflicts
  • Resolve merge conflict in branch feat/public-profile-page

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 and usage tips.

@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: 6

🧹 Nitpick comments (2)
app-modules/profile/tests/Feature/PublicProfileTest.php (1)

26-42: ⚡ Quick win

"Minimal profile" test doesn't actually create a minimal profile.

Profile::factory()->create([...]) populates all factory defaults (headline, about, etc.), so this doesn't exercise the null-field rendering path the test name implies. To truly validate the "render only filled fields" requirement, null out the optional attributes explicitly.

🤖 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/profile/tests/Feature/PublicProfileTest.php` around lines 26 -
42, The test PublicProfileTest::it('renders minimal profile without crashing')
currently uses Profile::factory()->create(...) which fills optional fields with
defaults; update the Profile creation in this test to explicitly set optional
attributes to null (e.g., headline, about, location, social links) when calling
Profile::factory()->create so the profile truly has minimal/null fields and the
assertions for not rendering 'null'/'undefined' exercise the null-field
rendering path.
app-modules/profile/resources/views/public-profile.blade.php (1)

18-52: 💤 Low value

Header markup is largely duplicated across the availability branches.

The @if ($profile->available_for_proposals) ... @else ... blocks repeat the name/headline/seniority markup with only the availability badge and a few margin classes differing. Extracting the shared name/headline portion and conditionally rendering just the badge would reduce drift risk.

🤖 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/profile/resources/views/public-profile.blade.php` around lines 18
- 52, The header markup is duplicated between the `@if`
($profile->available_for_proposals) branches; refactor by rendering the shared
elements (the <h1> that uses $profile->nickname ?? $user->name and the <p> that
outputs $profile->headline / seniority_level / $profile->years_experience) once,
apply the differing margin classes conditionally (e.g., compute class string
based on $profile->available_for_proposals) and move the availability badge
(<span> with the "Disponível para propostas" text and <x-filament::icon
icon="heroicon-s-check-circle" />) into a small conditional that only renders
when $profile->available_for_proposals is true; remove the duplicated blocks so
only one header block exists and the badge + margin adjustments are
conditionally applied.
🤖 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/profile/resources/views/public-profile.blade.php`:
- Line 29: The experience label lacks spacing; update the Blade template
occurrences that render the years (referenced by the $profile->years_experience
usage) to include spaces around the separator and between the number and the
unit so it reads "· {{ $profile->years_experience }} anos de experiência" (fix
both occurrences, including the one around line 49).

In `@app-modules/profile/src/Http/ProfileController.php`:
- Around line 60-84: The current logic can emit duplicate accounts and produce
malformed URLs when external_account_id is null; update the handling in
ProfileController so that when building $accounts you (1) track seen provider
keys (e.g. use a local array or collection) and skip adding a provider if its
key already came from $profile->social_links or a previous provider entry, and
(2) when iterating $user->providers skip any provider whose
$provider->external_account_id is null before calling sprintf with
self::PROVIDER_URLS[$name]; ensure you still normalize the provider name
resolution (the $name computed from IdentityProvider) and mark that provider as
seen after successfully adding it.
- Around line 27-29: In ProfileController, the Tenant lookup uses
Tenant::query()->where('domain', $request->getHost())->firstOrFail() but does
not check the Tenant.active flag; update the query to also filter for active
tenants (e.g. add ->where('active', true)) before calling firstOrFail() so
deactivated tenants are not resolved; make this change where the
Tenant::query(...) call appears to ensure inactive tenants return a 404.
- Around line 44-46: ProfileController@show is eager-loading $user->providers
which can return ExternalIdentity rows from other tenants; update the
$user->load call (or the relationship query) so that 'providers' is constrained
to the resolved $tenant->id (e.g., eager-load via a closure or use a scoped
relationship) so buildConnectedAccounts only iterates provider records for that
tenant; additionally, change the Tenant::query()->where('domain',
...)->firstOrFail() lookup to include an active filter (e.g., where('active',
true)) to avoid resolving inactive tenants; finally, replace the PHP 8.3-only
private const array PROVIDER_URLS with an alternative compatible declaration
(e.g., a private static array or a protected constant compatible with your PHP
target) so the code remains portable.

In `@app-modules/profile/src/ProfileServiceProvider.php`:
- Around line 25-26: The public profile route declaration
Route::get('/@{username}', [ProfileController::class,
'show'])->name('profile.public') lacks the web middleware; update that route to
attach ->middleware('web') (e.g., chain ->middleware('web') on the Route::get
call for that same route) so it receives the session/cookie/locale stack like
other routes.

In `@app-modules/profile/tests/Feature/PublicProfileTest.php`:
- Around line 63-79: The test in PublicProfileTest contains a tautological
assertion assertDontSee('Indisponível') which should be removed; update the test
(the it(...) block in PublicProfileTest) to delete the redundant
assertDontSee('Indisponível') so only assertDontSee('Disponível') remains, then
add two new focused tests: one that verifies tenant isolation by creating the
same username on a different Tenant and asserting a 404 when requesting the
profile on the original domain (use the same get(...) pattern and
assertStatus(404)), and another that seeds a Profile with connected accounts and
asserts the public profile page contains the expected connected-account markers
(using get(...) and assertSee for the connected-account labels).

---

Nitpick comments:
In `@app-modules/profile/resources/views/public-profile.blade.php`:
- Around line 18-52: The header markup is duplicated between the `@if`
($profile->available_for_proposals) branches; refactor by rendering the shared
elements (the <h1> that uses $profile->nickname ?? $user->name and the <p> that
outputs $profile->headline / seniority_level / $profile->years_experience) once,
apply the differing margin classes conditionally (e.g., compute class string
based on $profile->available_for_proposals) and move the availability badge
(<span> with the "Disponível para propostas" text and <x-filament::icon
icon="heroicon-s-check-circle" />) into a small conditional that only renders
when $profile->available_for_proposals is true; remove the duplicated blocks so
only one header block exists and the badge + margin adjustments are
conditionally applied.

In `@app-modules/profile/tests/Feature/PublicProfileTest.php`:
- Around line 26-42: The test PublicProfileTest::it('renders minimal profile
without crashing') currently uses Profile::factory()->create(...) which fills
optional fields with defaults; update the Profile creation in this test to
explicitly set optional attributes to null (e.g., headline, about, location,
social links) when calling Profile::factory()->create so the profile truly has
minimal/null fields and the assertions for not rendering 'null'/'undefined'
exercise the null-field rendering path.
🪄 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: e3fb3dd0-58e4-416e-b50a-5d6ee8f6c9fa

📥 Commits

Reviewing files that changed from the base of the PR and between 01f60a2 and 00fae1b.

📒 Files selected for processing (5)
  • app-modules/portal/resources/views/components/layouts/app.blade.php
  • app-modules/profile/resources/views/public-profile.blade.php
  • app-modules/profile/src/Http/ProfileController.php
  • app-modules/profile/src/ProfileServiceProvider.php
  • app-modules/profile/tests/Feature/PublicProfileTest.php

'profile::enums.seniority_level.' . $profile->seniority_level->value,
)
}}
@if ($profile->years_experience) ·{{ $profile->years_experience }}anos de experiência @endif

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Spacing in experience label.

·{{ $profile->years_experience }}anos de experiência renders without a space after the separator and before "anos" (e.g. "·5anos de experiência"). Same issue at Line 49.

✏️ Suggested fix
-                                `@if` ($profile->years_experience) ·{{ $profile->years_experience }}anos de experiência `@endif`
+                                `@if` ($profile->years_experience) · {{ $profile->years_experience }} anos de experiência `@endif`
📝 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
@if ($profile->years_experience) ·{{ $profile->years_experience }}anos de experiência @endif
`@if` ($profile->years_experience) · {{ $profile->years_experience }} anos de experiência `@endif`
🤖 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/profile/resources/views/public-profile.blade.php` at line 29, The
experience label lacks spacing; update the Blade template occurrences that
render the years (referenced by the $profile->years_experience usage) to include
spaces around the separator and between the number and the unit so it reads "·
{{ $profile->years_experience }} anos de experiência" (fix both occurrences,
including the one around line 49).

Comment thread app-modules/profile/src/Http/ProfileController.php
Comment thread app-modules/profile/src/Http/ProfileController.php Outdated
Comment thread app-modules/profile/src/Http/ProfileController.php
Comment thread app-modules/profile/src/ProfileServiceProvider.php Outdated
Comment thread app-modules/profile/tests/Feature/PublicProfileTest.php Outdated
…nserção de novas regras de negócio (#257)

Novos arquivos:

- Models: Project.php, PullRequest.php — seções de projetos e PRs manuais
- Migrations (5): add_skills, add_work_types, add_languages, create_user_projects, create_user_pull_requests
- Enums (2): SkillCategory.php, Skills.php — skills free-text com ícones
- Actions: UpsertProfile.php + UpsertProfileDTO.php — ação de criar/atualizar profile
- CSS: profile.css — tema dark/light com dupla detecção (prefers-color-scheme + .dark class)
- Testes: UpsertProfileTest.php, ProfileEnumTest.php + PublicProfileTest.php expandido para 9 testes

Funcionalidades extras (fora da issue original):

- Projetos — registro manual, max 3 com "Ver todos"
- Pull Requests — registro manual, max 3 com "Ver todos"
- Skills — free-text com chips coloridos por categoria (Frontend, Backend, Infra, Soft Skills)
- Work Types + Idiomas — chips visuais
- XP Bar — progresso xp / nextThreshold * 100
- H4 Verified Badge — badge especial (redeem_code H4_VERIFIED) só pra quem for verificado pela he4rt
- Badge expand — max 4, "Ver todas"
- Resume upload/download
- Tema completo dark/light — fundo preto/off-white, cards, gradientes, chips vibrantes

O que a Issue #257 pede e que foi feito:

- Rota /{tenant:domain}/@username ✅
- Controller com eager-load (character, badges, providers, address) ✅
- 404 se user não existe ou sem profile no tenant ✅
- Header: avatar, nome, username, headline, badge disponível, localização ✅
- Sobre: bio, senioridade, anos de experiência ✅
- Links: social_links manuais + OAuth providers (GitHub/Discord) ✅
- Gamificação: level + badges ✅
- Disponibilidade: badge + prazo ✅
- Esconder seções vazias (sem null/placeholders) ✅
- Sem autenticação ✅
- Blade + Tailwind (não Filament) ✅
- Responsivo ✅
- Dark mode ✅
- Testes de feature (completo, vazio, 404, domain routing) ✅
- Pint passando ✅
@GustavoSimao GustavoSimao force-pushed the feat/public-profile-page branch from caa81ee to 21e5eb5 Compare June 15, 2026 22:03

@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: 9

🧹 Nitpick comments (2)
app-modules/profile/resources/css/profile.css (1)

34-60: 💤 Low value

Dark mode variables are duplicated.

:root.dark (lines 34-60) and @media (prefers-color-scheme: dark) :root (lines 78-104) define identical values. Extract to a mixin, CSS nesting, or a single source to avoid drift.

Also applies to: 77-112

🤖 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/profile/resources/css/profile.css` around lines 34 - 60, The dark
mode CSS variables are defined in two separate locations (the :root.dark
selector and the media query for prefers-color-scheme: dark :root) with
identical values, creating duplication that can lead to maintenance issues.
Consolidate these duplicate variable definitions into a single source using one
of these approaches: create a reusable mixin that both selectors can reference,
use CSS nesting to apply the variables once, or modify the selectors to share a
single declaration block. Ensure both the :root.dark selector and the `@media`
(prefers-color-scheme: dark) :root selector apply the same set of color
variables without duplication.
app-modules/profile/resources/views/public-profile.blade.php (1)

261-307: ⚖️ Poor tradeoff

Duplicated project card markup.

The project card structure is repeated for take(3) and skip(3). Extract to a Blade component or @include partial to reduce maintenance burden.

Also applies to: 310-356

🤖 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/profile/resources/views/public-profile.blade.php` around lines
261 - 307, The project card markup displaying project name, description, URL
link, tags, stars, and forks is duplicated in two locations within the
public-profile.blade.php file: once in the take(3) section (lines 261-307) and
again in the skip(3) section (lines 310-356). Extract the entire project card
div structure (the rounded-lg border div containing all project details) into a
new Blade partial file named something like _project-card.blade.php in the
resources/views directory. Then replace both duplicated card structures with
`@include`('public-profile._project-card', ['project' => $project]) calls to
reference the shared partial, ensuring the $project variable is passed to
maintain access to all project properties like name, description, url, tags,
stars, and forks.
🤖 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/profile/resources/views/public-profile.blade.php`:
- Around line 148-156: The XP bar percentage calculation can exceed 100% when
the player is at max level because LEVEL_THRESHOLDS[$xpBarLevel + 1] defaults to
1 instead of a proper threshold value, causing the experience to be multiplied
by 100. Fix this by capping the calculated $xpBarPercent value to a maximum of
100 using the min() function, so the progress bar never exceeds its container
width regardless of the player's current experience amount at max level.

In `@app-modules/profile/src/Actions/UpsertProfile.php`:
- Around line 50-60: The UpsertProfile.php file sets work_types and languages
attributes directly without validation in the conditional blocks (around lines
50-60), while the skills attribute uses a mutator (Skills::validateAndLimit())
for validation. Add mutators or validation rules for work_types and languages to
validate their content: work_types should validate allowed values such as
'immediate' and 'remote', and languages should validate that each element
contains the required keys 'name' and 'level'. Implement these validations to
match the existing skills validation pattern.

In `@app-modules/profile/src/Enums/SeniorityLevel.php`:
- Around line 16-17: The SeniorityLevel enum is being contracted to only include
Pleno and Senior cases, but existing database records contain legacy values
(mid, specialist, lead) that will fail during enum hydration and casting. Before
deploying this enum change, create a database migration that maps all existing
legacy seniority values in persisted records to their appropriate new enum
values (either Pleno or Senior). Execute this migration to backfill the data in
the database prior to rolling out the updated SeniorityLevel enum definition to
ensure no enum hydration failures occur.

In `@app-modules/profile/src/Enums/SkillCategory.php`:
- Around line 17-20: The SkillCategory enum has hardcoded Portuguese labels in
the label() method for the LanguagesFrameworks, InfraDatabases, SoftSkillsTools,
and Idiomas cases. Replace each hardcoded Portuguese string literal with a
translation key (such as using trans() helper or trans_choice() depending on
your translation system) to make the labels locale-aware and respect the
application's current language setting. This way the label() method will return
the appropriate translated label based on the active locale instead of always
displaying Portuguese text.

In `@app-modules/profile/src/Enums/Skills.php`:
- Line 93: The icon matching condition in Skills.php currently includes a
reverse containment check with str_contains($keyword, $normalized) that causes
false positives for short input tokens like single letters. Remove the
str_contains($keyword, $normalized) clause from the matching condition on line
93, or alternatively add a minimum length requirement (e.g., check that both
$normalized and $keyword exceed a minimum length threshold) before the fuzzy
matching logic is applied to prevent unrelated icons from being matched.

In `@app-modules/profile/src/Models/Project.php`:
- Around line 67-69: The profile() method in the Project model uses
belongsTo(Profile::class) but the user_projects table does not contain a
profile_id column — it only has user_id and tenant_id. Correct the relationship
definition in the profile() method by either changing it to reference the User
model instead of Profile (since user_id exists in the table), or if a
relationship to Profile is truly intended, specify the correct foreign key and
owner key parameters in the belongsTo call to match the actual table structure.
Verify the relationship matches the table schema in the migration.

In `@app-modules/profile/src/Models/PullRequest.php`:
- Around line 67-69: The profile() method in the PullRequest model incorrectly
assumes a profile_id foreign key when using belongsTo(Profile::class), but the
users_pull_requests table actually uses user_id as the foreign key column.
Modify the belongsTo() call in the profile() method to explicitly specify
user_id as the foreign key parameter instead of relying on the default
convention.

In `@app-modules/profile/tests/Feature/PublicProfileTest.php`:
- Around line 133-135: The assertion on line 134 uses assertDontSee('Stack')
which is overly broad and could match unintended content containing the word
"Stack". Replace this with an assertion that checks for the exact section
heading used in the view for the Stack section, such as assertDontSee with the
full heading text (e.g., "My Stack" or similar exact phrase that appears in the
view). This makes the test more precise and less brittle to unrelated content
changes.

In `@app-modules/profile/tests/Unit/ProfileEnumTest.php`:
- Around line 50-51: The test assertions in ProfileEnumTest.php at lines 50-51
only validate the count of colors and icons arrays, but do not actually verify
that the values are unique as the test name suggests. Modify the expect
statements to verify distinctness by confirming that the count of each array
equals the count of its unique values, ensuring duplicates would cause the test
to fail. Consider using array comparison or filtering logic to confirm each
element in $colors and $icons appears only once.

---

Nitpick comments:
In `@app-modules/profile/resources/css/profile.css`:
- Around line 34-60: The dark mode CSS variables are defined in two separate
locations (the :root.dark selector and the media query for prefers-color-scheme:
dark :root) with identical values, creating duplication that can lead to
maintenance issues. Consolidate these duplicate variable definitions into a
single source using one of these approaches: create a reusable mixin that both
selectors can reference, use CSS nesting to apply the variables once, or modify
the selectors to share a single declaration block. Ensure both the :root.dark
selector and the `@media` (prefers-color-scheme: dark) :root selector apply the
same set of color variables without duplication.

In `@app-modules/profile/resources/views/public-profile.blade.php`:
- Around line 261-307: The project card markup displaying project name,
description, URL link, tags, stars, and forks is duplicated in two locations
within the public-profile.blade.php file: once in the take(3) section (lines
261-307) and again in the skip(3) section (lines 310-356). Extract the entire
project card div structure (the rounded-lg border div containing all project
details) into a new Blade partial file named something like
_project-card.blade.php in the resources/views directory. Then replace both
duplicated card structures with `@include`('public-profile._project-card',
['project' => $project]) calls to reference the shared partial, ensuring the
$project variable is passed to maintain access to all project properties like
name, description, url, tags, stars, and forks.
🪄 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: 0460d398-16e3-4624-b9d7-6cbb56599266

📥 Commits

Reviewing files that changed from the base of the PR and between caa81ee and 21e5eb5.

📒 Files selected for processing (28)
  • app-modules/panel-app/resources/views/components/profile-preview-card.blade.php
  • app-modules/panel-app/tests/Feature/ProfilePageTest.php
  • app-modules/portal/resources/views/components/layouts/app.blade.php
  • app-modules/profile/database/factories/ProfileFactory.php
  • app-modules/profile/database/migrations/2026_06_11_000000_add_skills_to_user_profiles_table.php
  • app-modules/profile/database/migrations/2026_06_14_000001_add_work_types_to_user_profiles_table.php
  • app-modules/profile/database/migrations/2026_06_14_000002_add_languages_to_user_profiles_table.php
  • app-modules/profile/database/migrations/2026_06_14_000003_create_user_projects_table.php
  • app-modules/profile/database/migrations/2026_06_14_000004_create_user_pull_requests_table.php
  • app-modules/profile/lang/en/enums.php
  • app-modules/profile/lang/pt_BR/enums.php
  • app-modules/profile/resources/css/profile.css
  • app-modules/profile/resources/views/public-profile.blade.php
  • app-modules/profile/src/Actions/UpsertProfile.php
  • app-modules/profile/src/DTOs/UpsertProfileDTO.php
  • app-modules/profile/src/Enums/SeniorityLevel.php
  • app-modules/profile/src/Enums/SkillCategory.php
  • app-modules/profile/src/Enums/Skills.php
  • app-modules/profile/src/Enums/SocialPlatform.php
  • app-modules/profile/src/Http/ProfileController.php
  • app-modules/profile/src/Models/Profile.php
  • app-modules/profile/src/Models/Project.php
  • app-modules/profile/src/Models/PullRequest.php
  • app-modules/profile/src/ProfileServiceProvider.php
  • app-modules/profile/tests/Feature/PublicProfileTest.php
  • app-modules/profile/tests/Feature/UpsertProfileTest.php
  • app-modules/profile/tests/Unit/ProfileEnumTest.php
  • vite.config.js
✅ Files skipped from review due to trivial changes (5)
  • vite.config.js
  • app-modules/profile/database/migrations/2026_06_14_000002_add_languages_to_user_profiles_table.php
  • app-modules/profile/database/migrations/2026_06_14_000001_add_work_types_to_user_profiles_table.php
  • app-modules/profile/database/migrations/2026_06_14_000003_create_user_projects_table.php
  • app-modules/profile/database/migrations/2026_06_11_000000_add_skills_to_user_profiles_table.php
🚧 Files skipped from review as they are similar to previous changes (2)
  • app-modules/profile/src/ProfileServiceProvider.php
  • app-modules/profile/src/Http/ProfileController.php

Comment on lines +148 to +156
@php
$xpBarLevel = $user->character->level;
$xpBarNext = \He4rt\Gamification\Character\Models\Character::LEVEL_THRESHOLDS[$xpBarLevel + 1] ?? 1;
$xpBarPercent = $xpBarNext > 0 ? round(($user->character->experience / $xpBarNext) * 100, 1) : 100;
@endphp
<div class="xp-track h-2 w-full overflow-hidden rounded-full">
<div
class="xp-bar h-full rounded-full"
style="width: {{ max($xpBarPercent, 1) }}%;"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

XP bar can exceed 100% at max level.

When $xpBarLevel is 50, LEVEL_THRESHOLDS[51] is null, defaulting to 1. This makes $xpBarPercent = experience * 100, far exceeding 100%. Cap the percentage.

🐛 Proposed fix
 `@php`
     $xpBarLevel = $user->character->level;
     $xpBarNext = \He4rt\Gamification\Character\Models\Character::LEVEL_THRESHOLDS[$xpBarLevel + 1] ?? 1;
-    $xpBarPercent = $xpBarNext > 0 ? round(($user->character->experience / $xpBarNext) * 100, 1) : 100;
+    $xpBarPercent = $xpBarNext > 1
+        ? min(round(($user->character->experience / $xpBarNext) * 100, 1), 100)
+        : 100;
 `@endphp`
📝 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
@php
$xpBarLevel = $user->character->level;
$xpBarNext = \He4rt\Gamification\Character\Models\Character::LEVEL_THRESHOLDS[$xpBarLevel + 1] ?? 1;
$xpBarPercent = $xpBarNext > 0 ? round(($user->character->experience / $xpBarNext) * 100, 1) : 100;
@endphp
<div class="xp-track h-2 w-full overflow-hidden rounded-full">
<div
class="xp-bar h-full rounded-full"
style="width: {{ max($xpBarPercent, 1) }}%;"
`@php`
$xpBarLevel = $user->character->level;
$xpBarNext = \He4rt\Gamification\Character\Models\Character::LEVEL_THRESHOLDS[$xpBarLevel + 1] ?? 1;
$xpBarPercent = $xpBarNext > 1
? min(round(($user->character->experience / $xpBarNext) * 100, 1), 100)
: 100;
`@endphp`
<div class="xp-track h-2 w-full overflow-hidden rounded-full">
<div
class="xp-bar h-full rounded-full"
style="width: {{ max($xpBarPercent, 1) }}%;"
🤖 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/profile/resources/views/public-profile.blade.php` around lines
148 - 156, The XP bar percentage calculation can exceed 100% when the player is
at max level because LEVEL_THRESHOLDS[$xpBarLevel + 1] defaults to 1 instead of
a proper threshold value, causing the experience to be multiplied by 100. Fix
this by capping the calculated $xpBarPercent value to a maximum of 100 using the
min() function, so the progress bar never exceeds its container width regardless
of the player's current experience amount at max level.

Comment on lines +50 to +60
if ($dto->workTypes !== null) {
$attributes['work_types'] = $dto->workTypes;
}

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

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -n -C3 "work_types|languages|skills|function rules\\(|'rules'\\s*=>" app-modules/profile app-modules/panel-app

Repository: he4rt/heartdevs.com

Length of output: 30008


🏁 Script executed:

cat -n app-modules/profile/src/Actions/UpsertProfile.php

Repository: he4rt/heartdevs.com

Length of output: 3886


🏁 Script executed:

# Search for validation rules in profile module
rg -n "rules\(\)|validate|work_types|languages" app-modules/profile/src --type php -A 3 -B 1 | head -100

Repository: he4rt/heartdevs.com

Length of output: 5673


🏁 Script executed:

# Check for mutators/validation for work_types and languages in Profile model
cat -n app-modules/profile/src/Models/Profile.php | grep -A 20 "protected function"

Repository: he4rt/heartdevs.com

Length of output: 1860


🏁 Script executed:

# Check if work_types has allowed values enum like skills does
rg -n "work.?type|WorkType" app-modules/profile/src --type php -B 2 -A 5

Repository: he4rt/heartdevs.com

Length of output: 3434


Add validation for work_types and languages arrays.

skills validates content via mutator (Skills::validateAndLimit()), but work_types and languages persist without validation. Missing validation for:

  • work_types: Should validate allowed values (e.g., 'immediate', 'remote')
  • languages: Should validate required keys (name, level) in each element

Add mutators or validation rules to match the skills implementation.

🤖 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/profile/src/Actions/UpsertProfile.php` around lines 50 - 60, The
UpsertProfile.php file sets work_types and languages attributes directly without
validation in the conditional blocks (around lines 50-60), while the skills
attribute uses a mutator (Skills::validateAndLimit()) for validation. Add
mutators or validation rules for work_types and languages to validate their
content: work_types should validate allowed values such as 'immediate' and
'remote', and languages should validate that each element contains the required
keys 'name' and 'level'. Implement these validations to match the existing
skills validation pattern.

Comment on lines +16 to 17
case Pleno = 'pleno';
case Senior = 'senior';

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Backfill legacy seniority values before this enum contraction.

Line 16 introduces pleno while legacy values were removed; existing persisted mid/specialist/lead values will break enum hydration/casts unless mapped in a data migration before rollout.

🤖 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/profile/src/Enums/SeniorityLevel.php` around lines 16 - 17, The
SeniorityLevel enum is being contracted to only include Pleno and Senior cases,
but existing database records contain legacy values (mid, specialist, lead) that
will fail during enum hydration and casting. Before deploying this enum change,
create a database migration that maps all existing legacy seniority values in
persisted records to their appropriate new enum values (either Pleno or Senior).
Execute this migration to backfill the data in the database prior to rolling out
the updated SeniorityLevel enum definition to ensure no enum hydration failures
occur.

Comment on lines +17 to +20
self::LanguagesFrameworks => 'Linguagens & Frameworks',
self::InfraDatabases => 'Infraestrutura & Databases',
self::SoftSkillsTools => 'Soft Skills & Ferramentas',
self::Idiomas => 'Idiomas',

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Remove hardcoded PT labels from enum output.

Lines 17-20 force Portuguese labels in every locale. Replace literals with translation keys to keep label() locale-aware.

🤖 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/profile/src/Enums/SkillCategory.php` around lines 17 - 20, The
SkillCategory enum has hardcoded Portuguese labels in the label() method for the
LanguagesFrameworks, InfraDatabases, SoftSkillsTools, and Idiomas cases. Replace
each hardcoded Portuguese string literal with a translation key (such as using
trans() helper or trans_choice() depending on your translation system) to make
the labels locale-aware and respect the application's current language setting.
This way the label() method will return the appropriate translated label based
on the active locale instead of always displaying Portuguese text.


foreach (self::ICON_MAP as $entry) {
foreach ($entry['keywords'] as $keyword) {
if ($normalized === $keyword || str_contains($normalized, $keyword) || str_contains($keyword, $normalized)) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Tighten icon matching to avoid false positives.

Line 93 matches when $keyword contains $normalized; short inputs (e.g., single letters) can resolve to unrelated icons. Remove reverse containment or require a minimum token length before fuzzy matching.

🤖 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/profile/src/Enums/Skills.php` at line 93, The icon matching
condition in Skills.php currently includes a reverse containment check with
str_contains($keyword, $normalized) that causes false positives for short input
tokens like single letters. Remove the str_contains($keyword, $normalized)
clause from the matching condition on line 93, or alternatively add a minimum
length requirement (e.g., check that both $normalized and $keyword exceed a
minimum length threshold) before the fuzzy matching logic is applied to prevent
unrelated icons from being matched.

Comment on lines +67 to +69
public function profile(): BelongsTo
{
return $this->belongsTo(Profile::class);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
fd -i '2026_06_14_000003_create_user_projects_table.php' -x sed -n '1,220p' {}
rg -n -C2 "function profile\\(|belongsTo\\(Profile::class" app-modules/profile/src/Models/Project.php app-modules/profile/src/Models/PullRequest.php

Repository: he4rt/heartdevs.com

Length of output: 2134


profile() relationship is broken — user_projects table has no profile_id column.

The migration shows user_projects contains user_id and tenant_id, but no profile_id. The belongsTo(Profile::class) call will fail at runtime. Correct the foreign key reference or verify the intended relationship.

🤖 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/profile/src/Models/Project.php` around lines 67 - 69, The
profile() method in the Project model uses belongsTo(Profile::class) but the
user_projects table does not contain a profile_id column — it only has user_id
and tenant_id. Correct the relationship definition in the profile() method by
either changing it to reference the User model instead of Profile (since user_id
exists in the table), or if a relationship to Profile is truly intended, specify
the correct foreign key and owner key parameters in the belongsTo call to match
the actual table structure. Verify the relationship matches the table schema in
the migration.

Comment on lines +67 to +69
public function profile(): BelongsTo
{
return $this->belongsTo(Profile::class);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

find . -type f -name "*.php" | grep -i "pullrequest\|migration" | head -20

Repository: he4rt/heartdevs.com

Length of output: 1998


🏁 Script executed:

find . -type f -name "*migration*" -o -name "*schema*" | grep -E "(user_pull|pull_request)" | head -20

Repository: he4rt/heartdevs.com

Length of output: 45


🏁 Script executed:

rg "user_pull_requests" --type php -A 5 -B 5

Repository: he4rt/heartdevs.com

Length of output: 3190


🏁 Script executed:

cat -n app-modules/profile/src/Models/PullRequest.php | head -100

Repository: he4rt/heartdevs.com

Length of output: 3128


🏁 Script executed:

find . -type f -name "Profile.php" | grep -i models

Repository: he4rt/heartdevs.com

Length of output: 108


🏁 Script executed:

cat -n app-modules/profile/src/Models/Profile.php | head -80

Repository: he4rt/heartdevs.com

Length of output: 2988


profile() uses non-existent foreign key.

The user_pull_requests table has no profile_id column. The belongsTo(Profile::class) call defaults to profile_id, which doesn't exist. Use user_id as the foreign key instead:

Fix
     public function profile(): BelongsTo
     {
-        return $this->belongsTo(Profile::class);
+        return $this->belongsTo(Profile::class, 'user_id', 'user_id')
+            ->where('tenant_id', $this->tenant_id);
     }
📝 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
public function profile(): BelongsTo
{
return $this->belongsTo(Profile::class);
public function profile(): BelongsTo
{
return $this->belongsTo(Profile::class, 'user_id', 'user_id')
->where('tenant_id', $this->tenant_id);
}
🤖 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/profile/src/Models/PullRequest.php` around lines 67 - 69, The
profile() method in the PullRequest model incorrectly assumes a profile_id
foreign key when using belongsTo(Profile::class), but the users_pull_requests
table actually uses user_id as the foreign key column. Modify the belongsTo()
call in the profile() method to explicitly specify user_id as the foreign key
parameter instead of relying on the default convention.

Comment on lines +133 to +135
$response->assertOk();
$response->assertDontSee('Stack');
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Use exact heading in the negative assertion.

Line 134 asserts assertDontSee('Stack'), which is overly broad and brittle. Assert against the exact section heading used by the view.

Diff
-    $response->assertDontSee('Stack');
+    $response->assertDontSee('Stack & Skills', false);
📝 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
$response->assertOk();
$response->assertDontSee('Stack');
});
$response->assertOk();
$response->assertDontSee('Stack & Skills', false);
});
🤖 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/profile/tests/Feature/PublicProfileTest.php` around lines 133 -
135, The assertion on line 134 uses assertDontSee('Stack') which is overly broad
and could match unintended content containing the word "Stack". Replace this
with an assertion that checks for the exact section heading used in the view for
the Stack section, such as assertDontSee with the full heading text (e.g., "My
Stack" or similar exact phrase that appears in the view). This makes the test
more precise and less brittle to unrelated content changes.

Comment on lines +50 to +51
expect($colors)->toHaveCount(3)
->and($icons)->toHaveCount(3);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Assert uniqueness, not just array length.

Lines 50-51 validate only case count, not distinct colors/icons as the test name claims.

Diff
-    expect($colors)->toHaveCount(3)
-        ->and($icons)->toHaveCount(3);
+    expect(array_unique($colors))->toHaveCount(3)
+        ->and(array_unique($icons))->toHaveCount(3);
🤖 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/profile/tests/Unit/ProfileEnumTest.php` around lines 50 - 51, The
test assertions in ProfileEnumTest.php at lines 50-51 only validate the count of
colors and icons arrays, but do not actually verify that the values are unique
as the test name suggests. Modify the expect statements to verify distinctness
by confirming that the count of each array equals the count of its unique
values, ensuring duplicates would cause the test to fail. Consider using array
comparison or filtering logic to confirm each element in $colors and $icons
appears only once.

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.

2 participants