Skip to content

feat(profile): public profile page with domain routing (/@username) #257

@danielhe4rt

Description

@danielhe4rt

Parent

Parte da PRD #250 — Módulo Profile Fase 1.

Contexto

O perfil de um membro é público — qualquer pessoa (incluindo recrutadores sem conta) pode visualizar acessando a URL do domínio do tenant. É a vitrine do desenvolvedor pra comunidade e pro mercado.

O tenant é resolvido via domain routing do Laravel. Cada tenant tem uma coluna domain na tabela tenants. Quando alguém acessa he4rtdevs.com/@danielhe4rt, o Laravel resolve o tenant pelo domínio e busca o profile daquele user naquele tenant.

A página agrega dados de múltiplos módulos

A view pública não mostra só dados do user_profiles. Ela compõe informações de:

  • Profile (módulo profile): headline, bio, senioridade, anos de experiência, disponibilidade, social links, nickname
  • Identity (módulo identity): nome do User, username, avatar (media do User)
  • Gamification (módulo gamification): Character (level, XP), Badges conquistados
  • Identity (ExternalIdentity): provedores OAuth conectados (GitHub, Discord) — derivar URLs automaticamente
  • Shared (Address polimórfico): localização (país, estado, cidade)

Não é Filament

Esta página é uma rota web pública, fora do Filament. Similar ao módulo landing que serve o GET /. Usa Blade + Tailwind (+ Livewire se necessário pra interatividade).

Sem autenticação

A rota não requer login. Todos os dados exibidos são públicos. A disponibilidade (available_for_proposals) também é pública — decisão da PRD.

O que construir

1. Rota com domain routing

Configurar rota que usa o domínio do tenant:

Route::domain('{tenant:domain}')
    ->get('/@{username}', ProfileController@show)
    ->name('profile.public');

(A implementação exata depende de como o Laravel resolve tenants por domínio — consultar docs do Laravel sobre domain routing.)

A rota recebe o username do User e o tenant é resolvido pelo domínio.

2. Controller / Livewire component

Um controller (ou Livewire full-page component) que:

  1. Resolve o tenant pelo domínio
  2. Busca o User pelo username
  3. Busca o Profile daquele User naquele Tenant
  4. Eager-load: character, character.badges, providers (ExternalIdentity), address (polimórfico)
  5. Retorna 404 se User não existe ou não tem Profile naquele tenant

3. View Blade

Página com layout público (sem sidebar de Filament). Seções:

Header:

  • Cover image (se existir, via media do User) ou gradient default
  • Avatar do User (media ou GitHub fallback)
  • Nome, username (@handle), headline
  • Badge de "Disponível" (se available_for_proposals = true)
  • Localização (cidade, estado, país — se preenchido)

Sobre:

  • Bio (about) — texto completo
  • Senioridade + anos de experiência

Links:

  • Social links manuais (do JSONB social_links) — com ícones por plataforma
  • Provedores OAuth conectados (GitHub, Discord, etc.) — URLs derivadas do ExternalIdentity

Gamificação:

  • Level do Character
  • Badges conquistados (com imagem, se tiver media)

Disponibilidade:

  • Se ativo: badge "Disponível" + prazo de início
  • Se inativo: nada (não mostrar "Indisponível")

4. Tratamento de casos especiais

  • User não existe: 404
  • User existe mas não tem Profile no tenant: 404
  • Profile existe mas campos vazios: renderizar só o que foi preenchido, sem mostrar "null" ou placeholders quebrados
  • User sem Character: seção de gamificação não aparece
  • User sem Address: seção de localização não aparece

Cenários BDD

Funcionalidade: Página pública de perfil

  Cenário: Visitante acessa perfil completo
    Dado que "danielhe4rt" tem Profile no tenant com domínio "he4rtdevs.com"
    E headline é "Backend Developer", seniority "senior", about "Dev PHP com 8 anos"
    E available_for_proposals é true, start_availability "immediate"
    E Character com level 15 e 3 badges
    E Address com country "BR", state "SP", city "São Paulo"
    E ExternalIdentity conectado: GitHub "danielhe4rt"
    E social_links: {"instagram": "@danielhe4rt"}
    Quando um visitante anônimo acessa "he4rtdevs.com/@danielhe4rt"
    Então a página exibe nome, username, headline "Backend Developer"
    E exibe badge "Disponível"
    E exibe localização "São Paulo, SP, BR"
    E exibe bio completa
    E exibe senioridade "Sênior" e "8 anos de experiência"
    E exibe link do Instagram
    E exibe link do GitHub (derivado do ExternalIdentity)
    E exibe level 15 e 3 badges
    E não requer autenticação (status 200)

  Cenário: Visitante acessa perfil mínimo (quase vazio)
    Dado que "novato" tem Profile no tenant mas nunca preencheu nada
    E não tem Character nem Address
    Quando um visitante acessa o perfil
    Então exibe o nome do User (da tabela users)
    E não exibe seção de bio, senioridade, gamificação, localização ou links
    E não exibe "null" ou placeholders quebrados

  Cenário: Username inexistente retorna 404
    Quando um visitante acessa "he4rtdevs.com/@fantasma"
    Então recebe status 404

  Cenário: User existe mas não tem Profile no tenant
    Dado que "danielhe4rt" existe como User
    Mas não é membro do tenant com domínio "outro-dominio.com"
    Quando um visitante acessa "outro-dominio.com/@danielhe4rt"
    Então recebe status 404

  Cenário: Disponibilidade inativa não mostra badge
    Dado que "danielhe4rt" tem available_for_proposals false
    Quando um visitante acessa o perfil
    Então não exibe badge "Disponível"
    E não exibe "Indisponível"

  Cenário: Domínio resolve o tenant correto
    Dado que "danielhe4rt" tem Profile em dois tenants:
      - tenant A (domínio "he4rtdevs.com") com headline "PHP Dev"
      - tenant B (domínio "outra.com") com headline "Rust Dev"
    Quando visitante acessa "he4rtdevs.com/@danielhe4rt"
    Então headline exibido é "PHP Dev"
    Quando visitante acessa "outra.com/@danielhe4rt"
    Então headline exibido é "Rust Dev"

Acceptance Criteria

  • Rota /@{username} funciona com domain routing (tenant resolvido pelo domínio)
  • Página não requer autenticação
  • Exibe dados de Profile, User, Character, Badges, ExternalIdentity e Address
  • Campos vazios são omitidos (sem null/placeholders)
  • 404 pra username inexistente ou sem profile no tenant
  • Badge "Disponível" exibido quando available_for_proposals = true
  • Social links manuais exibidos com ícones por plataforma
  • Links OAuth derivados automaticamente do ExternalIdentity
  • Layout usa Blade + Tailwind (não Filament)
  • Responsivo (mobile + desktop)
  • Dark mode funcional (sem bg-white hardcoded)
  • Testes de feature cobrem: perfil completo, perfil vazio, 404, domain routing
  • vendor/bin/pint --dirty --format agent passa sem erros

Blocked by

Metadata

Metadata

Assignees

No one assigned

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions