Adiciona bloco MicrositeHeaderBlock e regiões ao tema do microsite

- Novo bloco MicrositeHeaderBlock (site_users_microsite): exibe foto
  circular (220×280px), nome (h1), biografia e contatos (telefone,
  e-mail). Título oculto por padrão; biografia renderizada via
  ->processed com |raw no template.
- Remove UserInfoBlock e seu template (não estava em uso).
- Adiciona regiões Top Bar e Navigation ao tema; menu.html.twig para
  gerar classes .menu__item/.menu__link compatíveis com o CSS.
- CSS: estilos das novas regiões e do bloco de cabeçalho.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-17 07:12:55 -03:00
parent 505c9fb64a
commit 84f4661798
7 changed files with 343 additions and 289 deletions

View File

@@ -7,6 +7,24 @@
use Drupal\user\UserInterface;
/**
* Implements hook_theme().
*/
function site_users_microsite_theme(): array {
return [
'microsite_header_block' => [
'variables' => [
'photo_url' => NULL,
'photo_alt' => '',
'name' => NULL,
'bio' => NULL,
'phone' => NULL,
'email' => NULL,
],
],
];
}
/**
* Implements hook_preprocess_page().
*

View File

@@ -0,0 +1,166 @@
<?php
namespace Drupal\site_users_microsite\Plugin\Block;
use Drupal\Core\Block\BlockBase;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\user\UserInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Bloco de cabeçalho do microsite com informações do usuário.
*
* Exibe foto (circular), nome, biografia e
* informações de contato (telefone, e-mail).
*
* @Block(
* id = "site_users_microsite_header",
* admin_label = @Translation("Microsite — cabeçalho do usuário"),
* category = @Translation("Site Users")
* )
*/
class MicrositeHeaderBlock extends BlockBase implements ContainerFactoryPluginInterface {
public function __construct(
array $configuration,
$plugin_id,
$plugin_definition,
protected RouteMatchInterface $routeMatch,
protected EntityTypeManagerInterface $entityTypeManager,
) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
}
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition): static {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('current_route_match'),
$container->get('entity_type.manager'),
);
}
/**
* {@inheritdoc}
*/
public function defaultConfiguration(): array {
return ['label_display' => '0'] + parent::defaultConfiguration();
}
/**
* {@inheritdoc}
*/
public function build(): array {
$user = $this->getUser();
if (!$user instanceof UserInterface) {
return [];
}
return [
'#theme' => 'microsite_header_block',
'#photo_url' => $this->getPhotoUrl($user),
'#photo_alt' => $this->getPhotoAlt($user),
'#name' => $this->getFieldValue($user, 'field_user_name') ?: $user->getDisplayName(),
'#bio' => $this->getProcessedValue($user, 'field_user_bio'),
'#phone' => $this->getFieldValue($user, 'field_user_phone'),
'#email' => $user->getEmail(),
'#cache' => [
'tags' => $user->getCacheTags(),
'contexts' => ['route'],
],
];
}
/**
* Retorna o usuário da rota atual.
*/
protected function getUser(): ?UserInterface {
$user = $this->routeMatch->getParameter('user');
if ($user instanceof UserInterface) {
return $user;
}
if (is_numeric($user)) {
return $this->entityTypeManager->getStorage('user')->load($user);
}
return NULL;
}
/**
* Retorna a URL absoluta da foto padrão do usuário.
*/
protected function getPhotoUrl(UserInterface $user): ?string {
if (!function_exists('site_users_get_default_photo')) {
return NULL;
}
$media = site_users_get_default_photo($user);
if (!$media) {
return NULL;
}
$source_field = $media->getSource()->getConfiguration()['source_field'];
if (!$media->hasField($source_field) || $media->get($source_field)->isEmpty()) {
return NULL;
}
$file = $media->get($source_field)->entity;
if (!$file) {
return NULL;
}
return \Drupal::service('file_url_generator')->generateAbsoluteString($file->getFileUri());
}
/**
* Retorna o texto alternativo da foto padrão.
*/
protected function getPhotoAlt(UserInterface $user): string {
if (!function_exists('site_users_get_default_photo')) {
return '';
}
$media = site_users_get_default_photo($user);
return $media ? $media->label() : '';
}
/**
* Retorna o valor de texto de um campo do usuário.
*/
protected function getFieldValue(UserInterface $user, string $field_name): ?string {
if ($user->hasField($field_name) && !$user->get($field_name)->isEmpty()) {
return $user->get($field_name)->value;
}
return NULL;
}
/**
* Retorna o valor processado (HTML filtrado) de um campo text_long.
*
* Usa ->processed, que aplica o formato de texto configurado e retorna um
* objeto Markup — o Twig renderiza como HTML sem escape adicional.
*/
protected function getProcessedValue(UserInterface $user, string $field_name): ?string {
if ($user->hasField($field_name) && !$user->get($field_name)->isEmpty()) {
return $user->get($field_name)->processed;
}
return NULL;
}
/**
* {@inheritdoc}
*/
public function getCacheTags(): array {
$user = $this->getUser();
if ($user instanceof UserInterface) {
return Cache::mergeTags(parent::getCacheTags(), $user->getCacheTags());
}
return parent::getCacheTags();
}
/**
* {@inheritdoc}
*/
public function getCacheContexts(): array {
return Cache::mergeContexts(parent::getCacheContexts(), ['route']);
}
}

View File

@@ -0,0 +1,59 @@
{#
/**
* @file
* Template do bloco de cabeçalho do microsite.
*
* Variáveis:
* - photo_url: URL absoluta da foto padrão (string|null).
* - photo_alt: Texto alternativo da foto (string).
* - name: Nome completo do usuário (string).
* - roles: Array de rótulos das roles do usuário (string[]).
* - bio: Biografia (string|null).
* - phone: Telefone (string|null).
* - email: E-mail (string|null).
*/
#}
<div class="msite-header-block">
<div class="msite-header-block__photo-wrap">
{% if photo_url %}
<img
class="msite-header-block__photo"
src="{{ photo_url }}"
alt="{{ photo_alt }}"
/>
{% else %}
<div class="msite-header-block__photo msite-header-block__photo--initials" aria-hidden="true">
{{ name|first|upper }}
</div>
{% endif %}
</div>
<div class="msite-header-block__info">
<h1 class="msite-header-block__name">{{ name }}</h1>
{% if bio %}
<div class="msite-header-block__bio">{{ bio|raw }}</div>
{% endif %}
{% if phone or email %}
<ul class="msite-header-block__contact">
{% if phone %}
<li class="msite-header-block__contact-item">
<span class="msite-header-block__contact-label">{{ 'Telefone'|t }}:</span>
<a href="tel:{{ phone }}">{{ phone }}</a>
</li>
{% endif %}
{% if email %}
<li class="msite-header-block__contact-item">
<span class="msite-header-block__contact-label">{{ 'E-mail'|t }}:</span>
<a href="mailto:{{ email }}">{{ email }}</a>
</li>
{% endif %}
</ul>
{% endif %}
</div>
</div>

View File

@@ -32,13 +32,7 @@ function site_users_theme($existing, $type, $theme, $path) {
'template' => 'user--restricted',
'base hook' => 'user',
],
'site_users_info_block' => [
'template' => 'site-user-info-block',
'variables' => [
'user_info' => [],
'user' => NULL,
],
],
];
}

View File

@@ -1,219 +0,0 @@
<?php
namespace Drupal\site_users\Plugin\Block;
use Drupal\Core\Block\BlockBase;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\user\UserInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Bloco com informações básicas do usuário.
*
* Exibe automaticamente as informações do usuário da rota /user/{id}.
*
* @Block(
* id = "site_users_info_block",
* admin_label = @Translation("User Information"),
* category = @Translation("Site Users")
* )
*/
class UserInfoBlock extends BlockBase implements ContainerFactoryPluginInterface {
/**
* The current route match.
*
* @var \Drupal\Core\Routing\RouteMatchInterface
*/
protected $routeMatch;
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* Constructs a new UserInfoBlock instance.
*
* @param array $configuration
* The plugin configuration.
* @param string $plugin_id
* The plugin ID.
* @param mixed $plugin_definition
* The plugin definition.
* @param \Drupal\Core\Routing\RouteMatchInterface $route_match
* The current route match.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
*/
public function __construct(
array $configuration,
$plugin_id,
$plugin_definition,
RouteMatchInterface $route_match,
EntityTypeManagerInterface $entity_type_manager
) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->routeMatch = $route_match;
$this->entityTypeManager = $entity_type_manager;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('current_route_match'),
$container->get('entity_type.manager')
);
}
/**
* {@inheritdoc}
*/
public function build() {
$user = $this->getUserFromContext();
if (!$user instanceof UserInterface) {
return [];
}
// Obter foto padrão.
$default_photo = NULL;
$default_photo_url = NULL;
if (function_exists('site_users_get_default_photo')) {
$default_photo = site_users_get_default_photo($user);
if ($default_photo) {
// Usar o source field do media (forma padrão do Drupal).
$source_field = $default_photo->getSource()->getConfiguration()['source_field'];
if ($default_photo->hasField($source_field) && !$default_photo->get($source_field)->isEmpty()) {
$file = $default_photo->get($source_field)->entity;
if ($file) {
$default_photo_url = \Drupal::service('file_url_generator')->generateAbsoluteString($file->getFileUri());
}
}
}
}
// Coletar informações do usuário.
$user_info = [
'uid' => $user->id(),
'username' => $user->getDisplayName(),
'name' => $this->getFieldValue($user, 'field_user_name'),
'phone' => $this->getFieldValue($user, 'field_user_phone'),
'bio' => $this->getFieldValue($user, 'field_user_bio'),
'social_links' => $this->getSocialLinks($user),
'photo_url' => $default_photo_url,
'photo_alt' => $default_photo ? $default_photo->label() : '',
];
return [
'#theme' => 'site_users_info_block',
'#user_info' => $user_info,
'#user' => $user,
'#attached' => [
'library' => [
'site_users/user-info-block',
],
],
];
}
/**
* Obtém o usuário da rota atual.
*
* @return \Drupal\user\UserInterface|null
* O usuário ou NULL se não estiver em uma página de usuário.
*/
protected function getUserFromContext(): ?UserInterface {
// Obter da rota /user/{user}.
$user = $this->routeMatch->getParameter('user');
if ($user instanceof UserInterface) {
return $user;
}
// Se for apenas o ID, carregar o usuário.
if (is_numeric($user)) {
return $this->entityTypeManager->getStorage('user')->load($user);
}
return NULL;
}
/**
* Returns the social network links of a user.
*
* @param \Drupal\user\UserInterface $user
* The user entity.
*
* @return array
* Array of items with 'network' and 'url' keys.
*/
protected function getSocialLinks(UserInterface $user): array {
$links = [];
if (!$user->hasField('field_user_social_links') || $user->get('field_user_social_links')->isEmpty()) {
return $links;
}
foreach ($user->get('field_user_social_links') as $item) {
if (!$item->isEmpty()) {
$links[] = [
'network' => $item->network,
'url' => $item->url,
];
}
}
return $links;
}
/**
* Obtém o valor de um campo do usuário.
*
* @param \Drupal\user\UserInterface $user
* O usuário.
* @param string $field_name
* O nome do campo.
*
* @return string|null
* O valor do campo ou NULL.
*/
protected function getFieldValue(UserInterface $user, string $field_name): ?string {
if ($user->hasField($field_name) && !$user->get($field_name)->isEmpty()) {
return $user->get($field_name)->value;
}
return NULL;
}
/**
* {@inheritdoc}
*/
public function getCacheTags() {
$tags = parent::getCacheTags();
$user = $this->getUserFromContext();
if ($user instanceof UserInterface) {
$tags = Cache::mergeTags($tags, $user->getCacheTags());
}
return $tags;
}
/**
* {@inheritdoc}
*/
public function getCacheContexts() {
return Cache::mergeContexts(parent::getCacheContexts(), ['route', 'user']);
}
}

View File

@@ -1,63 +0,0 @@
{#
/**
* @file
* Template for the user information block.
*
* Available variables:
* - user_info: Array with user information:
* - uid: User ID
* - username: Display name
* - name: Full name
* - phone: Phone number
* - bio: Biography
* - social_links: Array of social links, each with 'network' and 'url' keys
* - photo_url: Default photo URL
* - photo_alt: Photo alternative text
* - user: User entity.
*/
#}
<div class="site-user-info-block">
<div class="site-user-info-block__photo">
{% if user_info.photo_url %}
<img src="{{ user_info.photo_url }}" alt="{{ user_info.photo_alt }}" class="site-user-info-block__image" />
{% else %}
<div class="site-user-info-block__no-photo">
<span class="site-user-info-block__initials">
{{ user_info.name ? user_info.name|first|upper : user_info.username|first|upper }}
</span>
</div>
{% endif %}
</div>
<div class="site-user-info-block__details">
<h2 class="site-user-info-block__name">
{{ user_info.name ?: user_info.username }}
</h2>
{% if user_info.phone %}
<div class="site-user-info-block__phone">
<span class="site-user-info-block__label">{{ 'Phone:'|t }}</span>
<a href="tel:{{ user_info.phone }}">{{ user_info.phone }}</a>
</div>
{% endif %}
{% if user_info.bio %}
<div class="site-user-info-block__bio">
{{ user_info.bio }}
</div>
{% endif %}
{% if user_info.social_links %}
<div class="site-user-info-block__social-links">
{% for link in user_info.social_links %}
<a href="{{ link.url }}"
class="social-link social-link--{{ link.network }}"
target="_blank"
rel="noopener noreferrer">
{{ link.network }}
</a>
{% endfor %}
</div>
{% endif %}
</div>
</div>

View File

@@ -154,6 +154,105 @@ body.microsite {
margin-bottom: 1.5rem;
}
/* Microsite Header Block --------------------------------------------------- */
/*
* Bloco .msite-header-block: foto circular, nome (h1), meta (roles +
* departamento), biografia e lista de contatos.
* Assume fundo escuro herdado do .microsite-header.
*/
.msite-header-block {
display: flex;
align-items: flex-start;
gap: 1.75rem;
max-width: 1500px;
margin: 0 auto;
padding: 2rem 1.5rem;
color: #fff;
}
/* --- Foto ----------------------------------------------------------------- */
.msite-header-block__photo-wrap {
flex-shrink: 0;
}
.msite-header-block__photo {
display: block;
width: 220px;
height: 280px;
border-radius: 50%;
object-fit: cover;
border: 3px solid rgba(255, 255, 255, 0.35);
}
/* Fallback de iniciais quando não há foto */
.msite-header-block__photo--initials {
display: flex;
align-items: center;
justify-content: center;
background: rgba(255, 255, 255, 0.15);
font-size: 4rem;
font-weight: 700;
color: #fff;
user-select: none;
}
/* --- Informações ---------------------------------------------------------- */
.msite-header-block__info {
flex: 1;
min-width: 0;
}
.msite-header-block__name {
margin: 0 0 0.3rem;
font-size: 1.8rem;
font-weight: 700;
line-height: 1.2;
color: #fff;
}
.msite-header-block__meta {
margin: 0 0 0.75rem;
font-size: 0.95rem;
font-style: italic;
color: rgba(255, 255, 255, 0.75);
}
.msite-header-block__bio {
margin-bottom: 0.75rem;
font-size: 0.9rem;
line-height: 1.5;
color: rgba(255, 255, 255, 0.85);
}
/* --- Contatos ------------------------------------------------------------- */
.msite-header-block__contact {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-wrap: wrap;
gap: 0.3rem 1.5rem;
font-size: 0.85rem;
color: rgba(255, 255, 255, 0.8);
}
.msite-header-block__contact-label {
font-weight: 600;
}
.msite-header-block__contact a {
color: hsl(202, 79%, 70%);
text-decoration: none;
}
.msite-header-block__contact a:hover {
text-decoration: underline;
}
/* Top Bar ------------------------------------------------------------------ */
/*
* Barra estreita no topo da página, antes do header.