mirror of
https://gitlab.unicamp.br/infimecc_drupal11_modules/site_users.git
synced 2026-05-05 21:45:29 -03:00
Compare commits
9 Commits
d3c1282e47
...
0ce327026d
| Author | SHA1 | Date | |
|---|---|---|---|
| 0ce327026d | |||
| 03857ee1f2 | |||
| 3257c89ff7 | |||
| 9ec7f951bf | |||
| 84f4661798 | |||
| 505c9fb64a | |||
| 0c7b346530 | |||
| 0b137e8d12 | |||
| d169052065 |
123
README.md
123
README.md
@@ -108,6 +108,109 @@ if ($photo instanceof \Drupal\media\MediaInterface) {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Sub-módulo: Site Users Microsite
|
||||||
|
|
||||||
|
O sub-módulo `site_users_microsite` provê micro-sites pessoais para cada usuário,
|
||||||
|
acessíveis em `/user/{id}`, com tema próprio e sem a navegação principal do site.
|
||||||
|
|
||||||
|
### Funcionalidades
|
||||||
|
|
||||||
|
- Tema alternativo aplicado automaticamente em todas as rotas `/user/{id}` e `/user/{id}/content`
|
||||||
|
- Página `/user/{id}/content` listando o conteúdo publicado pelo usuário
|
||||||
|
- Configuração de quais tipos de conteúdo cada papel pode publicar no micro-site
|
||||||
|
- Aba "Content" adicionada às páginas de perfil de usuário
|
||||||
|
|
||||||
|
### Instalação
|
||||||
|
|
||||||
|
O sub-módulo está neste repositório mas é ativado independentemente:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
drush en site_users_microsite
|
||||||
|
```
|
||||||
|
|
||||||
|
O tema `site_users_microsite_theme` também precisa ser implantado e ativado
|
||||||
|
(ver seção [Deploy do tema](#deploy-do-tema) abaixo).
|
||||||
|
|
||||||
|
### Configuração
|
||||||
|
|
||||||
|
Acesse as configurações em:
|
||||||
|
**Administração > Configuração > Módulos Locais > Site Users > User Microsite**
|
||||||
|
|
||||||
|
`/admin/config/local-modules/site-users/microsite`
|
||||||
|
|
||||||
|
Configure quais tipos de conteúdo cada papel pode publicar no micro-site.
|
||||||
|
|
||||||
|
### Layout de blocos
|
||||||
|
|
||||||
|
No tema `site_users_microsite_theme`, configure os blocos em
|
||||||
|
**Estrutura > Layout de blocos**:
|
||||||
|
|
||||||
|
| Bloco | Região sugerida |
|
||||||
|
|---|---|
|
||||||
|
| Informações do Usuário (ou bloco customizado) | Microsite Header |
|
||||||
|
| Primary tabs | Tabs |
|
||||||
|
| Status messages | Messages |
|
||||||
|
| Main page content | Content |
|
||||||
|
|
||||||
|
### Permissões
|
||||||
|
|
||||||
|
| Permissão | Descrição |
|
||||||
|
|---|---|
|
||||||
|
| `administer site_users_microsite settings` | Configurar tipos de conteúdo por papel |
|
||||||
|
|
||||||
|
### Variáveis disponíveis no tema
|
||||||
|
|
||||||
|
O módulo injeta as seguintes variáveis em `page.html.twig` quando em rotas do micro-site:
|
||||||
|
|
||||||
|
```twig
|
||||||
|
{{ microsite_user }} {# entidade UserInterface do dono do micro-site #}
|
||||||
|
{{ microsite_user_name }} {# nome de exibição (string) #}
|
||||||
|
{{ microsite_user_roles }} {# array de roles (exceto 'authenticated') #}
|
||||||
|
{{ microsite_user_photo }} {# render array da foto padrão (view mode 'thumbnail') #}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Deploy do tema
|
||||||
|
|
||||||
|
O tema `site_users_microsite_theme` está versionado neste repositório em
|
||||||
|
`themes/site_users_microsite_theme/`, mas o Drupal só descobre temas em
|
||||||
|
`themes/custom/`. É necessário implantá-lo manualmente no servidor.
|
||||||
|
|
||||||
|
### Opção 1 — Symlink (recomendado em desenvolvimento)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ln -s /caminho/para/site_users/themes/site_users_microsite_theme \
|
||||||
|
/caminho/para/drupal/themes/custom/site_users_microsite_theme
|
||||||
|
```
|
||||||
|
|
||||||
|
### Opção 2 — Cópia (recomendado em produção)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp -r themes/site_users_microsite_theme \
|
||||||
|
/caminho/para/drupal/themes/custom/site_users_microsite_theme
|
||||||
|
```
|
||||||
|
|
||||||
|
Após implantar, ative o tema:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
drush theme:enable site_users_microsite_theme
|
||||||
|
```
|
||||||
|
|
||||||
|
O tema não precisa ser definido como tema padrão do site — o módulo
|
||||||
|
`site_users_microsite` o aplica automaticamente via ThemeNegotiator apenas
|
||||||
|
nas rotas de micro-site.
|
||||||
|
|
||||||
|
### Personalização do tema
|
||||||
|
|
||||||
|
- `base theme` em `site_users_microsite_theme.info.yml` está definido como `stable9`
|
||||||
|
(tema base mínimo do core, sem estilos próprios). Altere para o tema principal
|
||||||
|
do site se quiser herdar fontes e variáveis CSS.
|
||||||
|
- O template `templates/layout/page.html.twig` define o layout completo da página.
|
||||||
|
- O CSS base está em `css/microsite.css`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Estrutura do Módulo
|
## Estrutura do Módulo
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -127,6 +230,26 @@ site_users/
|
|||||||
├── templates/
|
├── templates/
|
||||||
│ ├── site-user-info-block.html.twig
|
│ ├── site-user-info-block.html.twig
|
||||||
│ └── user--full.html.twig
|
│ └── user--full.html.twig
|
||||||
|
├── site_users_microsite/ ← sub-módulo
|
||||||
|
│ ├── config/install/
|
||||||
|
│ │ └── site_users_microsite.settings.yml
|
||||||
|
│ ├── src/
|
||||||
|
│ │ ├── Controller/MicrositeContentController.php
|
||||||
|
│ │ ├── Form/MicrositeSettingsForm.php
|
||||||
|
│ │ └── Theme/MicrositeThemeNegotiator.php
|
||||||
|
│ ├── site_users_microsite.info.yml
|
||||||
|
│ ├── site_users_microsite.links.menu.yml
|
||||||
|
│ ├── site_users_microsite.links.task.yml
|
||||||
|
│ ├── site_users_microsite.module
|
||||||
|
│ ├── site_users_microsite.permissions.yml
|
||||||
|
│ ├── site_users_microsite.routing.yml
|
||||||
|
│ └── site_users_microsite.services.yml
|
||||||
|
├── themes/
|
||||||
|
│ └── site_users_microsite_theme/ ← tema (deploy em themes/custom/)
|
||||||
|
│ ├── css/microsite.css
|
||||||
|
│ ├── templates/layout/page.html.twig
|
||||||
|
│ ├── site_users_microsite_theme.info.yml
|
||||||
|
│ └── site_users_microsite_theme.libraries.yml
|
||||||
├── site_users.info.yml
|
├── site_users.info.yml
|
||||||
├── site_users.install
|
├── site_users.install
|
||||||
├── site_users.libraries.yml
|
├── site_users.libraries.yml
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
langcode: en
|
||||||
|
status: true
|
||||||
|
dependencies:
|
||||||
|
config:
|
||||||
|
- field.storage.user.field_user_homepage
|
||||||
|
module:
|
||||||
|
- link
|
||||||
|
- user
|
||||||
|
id: user.user.field_user_homepage
|
||||||
|
field_name: field_user_homepage
|
||||||
|
entity_type: user
|
||||||
|
bundle: user
|
||||||
|
label: 'Personal homepage'
|
||||||
|
description: 'URL of the user personal homepage.'
|
||||||
|
required: false
|
||||||
|
translatable: false
|
||||||
|
default_value: { }
|
||||||
|
default_value_callback: ''
|
||||||
|
settings:
|
||||||
|
title: 0
|
||||||
|
link_type: 16
|
||||||
|
field_type: link
|
||||||
20
config/optional/field.storage.user.field_user_homepage.yml
Normal file
20
config/optional/field.storage.user.field_user_homepage.yml
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
langcode: en
|
||||||
|
status: true
|
||||||
|
dependencies:
|
||||||
|
module:
|
||||||
|
- link
|
||||||
|
- user
|
||||||
|
id: user.field_user_homepage
|
||||||
|
field_name: field_user_homepage
|
||||||
|
entity_type: user
|
||||||
|
type: link
|
||||||
|
settings:
|
||||||
|
title: 0
|
||||||
|
link_type: 16
|
||||||
|
module: link
|
||||||
|
locked: false
|
||||||
|
cardinality: 1
|
||||||
|
translatable: true
|
||||||
|
indexes: { }
|
||||||
|
persist_with_no_fields: false
|
||||||
|
custom_storage: false
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
label: 'Página pessoal'
|
||||||
|
description: 'Endereço web da página pessoal do usuário.'
|
||||||
92
css/user-photos-widget.css
Normal file
92
css/user-photos-widget.css
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
/**
|
||||||
|
* @file
|
||||||
|
* Widget de tira de thumbnails com seletor de foto padrão.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* ---- Título da seção ----------------------------------------------------- */
|
||||||
|
|
||||||
|
.user-photos-strip-label {
|
||||||
|
margin: 0 0 0.5rem;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: #444;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- Tira de thumbnails -------------------------------------------------- */
|
||||||
|
|
||||||
|
.user-photos-strip {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 1rem;
|
||||||
|
margin-bottom: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-photos-empty {
|
||||||
|
color: #666;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- Card de foto -------------------------------------------------------- */
|
||||||
|
|
||||||
|
.user-photos-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* O .form-type--radio (ou .form-type-radio) envolve input + label.
|
||||||
|
Tratamos o bloco inteiro como o card clicável. */
|
||||||
|
.user-photos-item .form-type--radio,
|
||||||
|
.user-photos-item .form-type-radio {
|
||||||
|
position: relative;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Oculta o círculo do radio sem tirá-lo do fluxo de acessibilidade */
|
||||||
|
.user-photos-item .form-radio {
|
||||||
|
position: absolute;
|
||||||
|
opacity: 0;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Label = thumbnail — recebe borda ao ser selecionado */
|
||||||
|
.user-photos-item .form-type--radio label,
|
||||||
|
.user-photos-item .form-type-radio label {
|
||||||
|
display: block;
|
||||||
|
cursor: pointer;
|
||||||
|
border: 3px solid transparent;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 2px;
|
||||||
|
transition: border-color 0.15s, background-color 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Destaque na foto padrão selecionada */
|
||||||
|
.user-photos-item .form-type--radio:has(input[type="radio"]:checked) label,
|
||||||
|
.user-photos-item .form-type-radio:has(input[type="radio"]:checked) label {
|
||||||
|
border-color: #0678be;
|
||||||
|
background-color: #e8f4fd;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Thumbnail contido dentro do label */
|
||||||
|
.user-photos-item .form-type--radio label .media,
|
||||||
|
.user-photos-item .form-type-radio label .media,
|
||||||
|
.user-photos-item .form-type--radio label img,
|
||||||
|
.user-photos-item .form-type-radio label img {
|
||||||
|
display: block;
|
||||||
|
width: 120px;
|
||||||
|
height: 100px;
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- Área de upload ------------------------------------------------------ */
|
||||||
|
|
||||||
|
.user-photos-widget .js-form-item-new-photo,
|
||||||
|
.user-photos-widget .form-item-new-photo {
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
padding-top: 1rem;
|
||||||
|
border-top: 1px solid #ddd;
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
role_content_types: {}
|
||||||
10
modules/site_users_microsite/site_users_microsite.info.yml
Normal file
10
modules/site_users_microsite/site_users_microsite.info.yml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
name: 'Site Users Microsite'
|
||||||
|
type: module
|
||||||
|
description: 'Micro-site pessoal para usuários do site, com tema próprio e conteúdo por papel.'
|
||||||
|
core_version_requirement: ^10 || ^11
|
||||||
|
package: Custom
|
||||||
|
dependencies:
|
||||||
|
- drupal:user
|
||||||
|
- drupal:node
|
||||||
|
- site_users:site_users
|
||||||
|
- structural_pages:structural_pages
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @file
|
||||||
|
* Install, update and uninstall functions for the Site Users Microsite module.
|
||||||
|
*/
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
site_users_microsite.settings:
|
||||||
|
title: 'User Microsite'
|
||||||
|
description: 'Configure content types available per role in user micro-sites.'
|
||||||
|
route_name: site_users_microsite.settings
|
||||||
|
parent: site_users.settings
|
||||||
|
weight: 10
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# Aba de configurações do submódulo na página de settings do módulo pai.
|
||||||
|
site_users_microsite.settings_tab:
|
||||||
|
route_name: site_users_microsite.settings
|
||||||
|
title: 'Microsite'
|
||||||
|
base_route: site_users.settings
|
||||||
|
weight: 10
|
||||||
|
|
||||||
|
# Aba "Perfil" nas páginas do micro-site do usuário.
|
||||||
|
site_users_microsite.profile_tab:
|
||||||
|
route_name: site_users_microsite.profile
|
||||||
|
title: 'Profile'
|
||||||
|
base_route: entity.user.canonical
|
||||||
|
weight: 5
|
||||||
|
|
||||||
|
# Aba "Conteúdo" nas páginas do micro-site do usuário.
|
||||||
|
site_users_microsite.content_tab:
|
||||||
|
route_name: site_users_microsite.content
|
||||||
|
title: 'Content'
|
||||||
|
base_route: entity.user.canonical
|
||||||
|
weight: 10
|
||||||
65
modules/site_users_microsite/site_users_microsite.module
Normal file
65
modules/site_users_microsite/site_users_microsite.module
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @file
|
||||||
|
* Sub-módulo de micro-site pessoal de usuário.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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,
|
||||||
|
'homepage' => NULL,
|
||||||
|
'lattes_id' => NULL,
|
||||||
|
'orcid_id' => NULL,
|
||||||
|
'mathscinet_id' => NULL,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implements hook_preprocess_page().
|
||||||
|
*
|
||||||
|
* Disponibiliza variáveis do usuário do micro-site para o tema.
|
||||||
|
*/
|
||||||
|
function site_users_microsite_preprocess_page(array &$variables): void {
|
||||||
|
$route_match = \Drupal::routeMatch();
|
||||||
|
$route_name = $route_match->getRouteName();
|
||||||
|
|
||||||
|
$is_microsite = $route_name === 'entity.user.canonical'
|
||||||
|
|| str_starts_with($route_name, 'site_users_microsite.');
|
||||||
|
|
||||||
|
if (!$is_microsite) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$user = $route_match->getParameter('user');
|
||||||
|
if (!($user instanceof UserInterface)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$variables['microsite_user'] = $user;
|
||||||
|
$variables['microsite_user_name'] = $user->getDisplayName();
|
||||||
|
$variables['microsite_user_roles'] = $user->getRoles(TRUE);
|
||||||
|
|
||||||
|
$photo = site_users_get_default_photo($user);
|
||||||
|
if ($photo) {
|
||||||
|
$render = \Drupal::entityTypeManager()
|
||||||
|
->getViewBuilder('media')
|
||||||
|
->view($photo, 'thumbnail');
|
||||||
|
$variables['microsite_user_photo'] = \Drupal::service('renderer')
|
||||||
|
->renderInIsolation($render);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
administer site_users_microsite settings:
|
||||||
|
title: 'Administer User Microsite settings'
|
||||||
|
description: 'Configure which content types are available per role in micro-sites.'
|
||||||
|
restrict access: true
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
site_users_microsite.profile:
|
||||||
|
path: '/user/{user}/profile'
|
||||||
|
defaults:
|
||||||
|
_controller: '\Drupal\site_users_microsite\Controller\MicrositeHomeController::profile'
|
||||||
|
_title_callback: '\Drupal\site_users_microsite\Controller\MicrositeHomeController::title'
|
||||||
|
requirements:
|
||||||
|
_entity_access: 'user.view'
|
||||||
|
user: \d+
|
||||||
|
options:
|
||||||
|
parameters:
|
||||||
|
user:
|
||||||
|
type: entity:user
|
||||||
|
|
||||||
|
site_users_microsite.content:
|
||||||
|
path: '/user/{user}/content'
|
||||||
|
defaults:
|
||||||
|
_controller: '\Drupal\site_users_microsite\Controller\MicrositeContentController::content'
|
||||||
|
_title_callback: '\Drupal\site_users_microsite\Controller\MicrositeContentController::title'
|
||||||
|
requirements:
|
||||||
|
_permission: 'access content'
|
||||||
|
user: \d+
|
||||||
|
options:
|
||||||
|
parameters:
|
||||||
|
user:
|
||||||
|
type: entity:user
|
||||||
|
|
||||||
|
site_users_microsite.settings:
|
||||||
|
path: '/admin/config/local-modules/site-users/microsite'
|
||||||
|
defaults:
|
||||||
|
_form: '\Drupal\site_users_microsite\Form\MicrositeSettingsForm'
|
||||||
|
_title: 'User Microsite Settings'
|
||||||
|
requirements:
|
||||||
|
_permission: 'administer site_users_microsite settings'
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
services:
|
||||||
|
site_users_microsite.theme_negotiator:
|
||||||
|
class: Drupal\site_users_microsite\Theme\MicrositeThemeNegotiator
|
||||||
|
tags:
|
||||||
|
- { name: theme_negotiator, priority: 100 }
|
||||||
|
|
||||||
|
site_users_microsite.route_subscriber:
|
||||||
|
class: Drupal\site_users_microsite\Routing\RouteSubscriber
|
||||||
|
tags:
|
||||||
|
- { name: event_subscriber }
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Drupal\site_users_microsite\Controller;
|
||||||
|
|
||||||
|
use Drupal\Core\Controller\ControllerBase;
|
||||||
|
use Drupal\Core\StringTranslation\TranslatableMarkup;
|
||||||
|
use Drupal\user\UserInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Controller para listagem de conteúdo do micro-site do usuário.
|
||||||
|
*/
|
||||||
|
class MicrositeContentController extends ControllerBase {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Página de listagem de conteúdo do micro-site.
|
||||||
|
*/
|
||||||
|
public function content(UserInterface $user): array {
|
||||||
|
$allowed_types = $this->getAllowedContentTypes($user);
|
||||||
|
|
||||||
|
$cache = [
|
||||||
|
'contexts' => ['user'],
|
||||||
|
'tags' => [
|
||||||
|
'node_list',
|
||||||
|
'user:' . $user->id(),
|
||||||
|
'config:site_users_microsite.settings',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
if (empty($allowed_types)) {
|
||||||
|
return [
|
||||||
|
'#markup' => $this->t('No content types are configured for this profile type.'),
|
||||||
|
'#cache' => $cache,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$nids = $this->entityTypeManager()->getStorage('node')
|
||||||
|
->getQuery()
|
||||||
|
->condition('uid', $user->id())
|
||||||
|
->condition('type', $allowed_types, 'IN')
|
||||||
|
->condition('status', 1)
|
||||||
|
->accessCheck(TRUE)
|
||||||
|
->sort('created', 'DESC')
|
||||||
|
->execute();
|
||||||
|
|
||||||
|
if (empty($nids)) {
|
||||||
|
return [
|
||||||
|
'#markup' => $this->t('No content published yet.'),
|
||||||
|
'#cache' => $cache,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$nodes = $this->entityTypeManager()->getStorage('node')->loadMultiple($nids);
|
||||||
|
$build = $this->entityTypeManager()->getViewBuilder('node')->viewMultiple($nodes, 'teaser');
|
||||||
|
$build['#cache'] = $cache;
|
||||||
|
|
||||||
|
return $build;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback de título para a página de conteúdo.
|
||||||
|
*/
|
||||||
|
public function title(UserInterface $user): TranslatableMarkup {
|
||||||
|
return $this->t("@name's content", ['@name' => $user->getDisplayName()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retorna os tipos de conteúdo permitidos para os papéis do usuário.
|
||||||
|
*/
|
||||||
|
protected function getAllowedContentTypes(UserInterface $user): array {
|
||||||
|
$role_content_types = $this->config('site_users_microsite.settings')
|
||||||
|
->get('role_content_types') ?? [];
|
||||||
|
|
||||||
|
$allowed = [];
|
||||||
|
foreach ($user->getRoles() as $role_id) {
|
||||||
|
foreach ($role_content_types[$role_id] ?? [] as $type) {
|
||||||
|
$allowed[$type] = $type;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return array_keys($allowed);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Drupal\site_users_microsite\Controller;
|
||||||
|
|
||||||
|
use Drupal\Core\Controller\ControllerBase;
|
||||||
|
use Drupal\Core\StringTranslation\TranslatableMarkup;
|
||||||
|
use Drupal\user\UserInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Controller para a página inicial do micro-site do usuário.
|
||||||
|
*
|
||||||
|
* Carrega e exibe o nó do tipo content_page cujo autor é o usuário
|
||||||
|
* da rota /user/{user}/home.
|
||||||
|
*/
|
||||||
|
class MicrositeHomeController extends ControllerBase {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Página inicial do micro-site.
|
||||||
|
*/
|
||||||
|
public function home(UserInterface $user): array {
|
||||||
|
$cache = [
|
||||||
|
'tags' => ['node_list:content_page', 'user:' . $user->id()],
|
||||||
|
'contexts' => ['route'],
|
||||||
|
];
|
||||||
|
|
||||||
|
$nids = $this->entityTypeManager()->getStorage('node')
|
||||||
|
->getQuery()
|
||||||
|
->condition('uid', $user->id())
|
||||||
|
->condition('type', 'content_page')
|
||||||
|
->condition('status', 1)
|
||||||
|
->accessCheck(TRUE)
|
||||||
|
->range(0, 1)
|
||||||
|
->execute();
|
||||||
|
|
||||||
|
if (empty($nids)) {
|
||||||
|
return [
|
||||||
|
'#markup' => $this->t('No content published yet.'),
|
||||||
|
'#cache' => $cache,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$node = $this->entityTypeManager()->getStorage('node')->load(reset($nids));
|
||||||
|
$build = $this->entityTypeManager()->getViewBuilder('node')->view($node, 'full');
|
||||||
|
$build['#cache'] = $cache;
|
||||||
|
|
||||||
|
return $build;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Página de perfil do usuário em /user/{user}/profile.
|
||||||
|
*/
|
||||||
|
public function profile(UserInterface $user): array {
|
||||||
|
return $this->entityTypeManager()->getViewBuilder('user')->view($user, 'full');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback de título para a página inicial.
|
||||||
|
*/
|
||||||
|
public function title(UserInterface $user): TranslatableMarkup {
|
||||||
|
return $this->t("@name", ['@name' => $user->getDisplayName()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Drupal\site_users_microsite\Form;
|
||||||
|
|
||||||
|
use Drupal\Core\Form\ConfigFormBase;
|
||||||
|
use Drupal\Core\Form\FormStateInterface;
|
||||||
|
use Drupal\user\Entity\Role;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formulário de configuração do módulo User Microsite.
|
||||||
|
*/
|
||||||
|
class MicrositeSettingsForm extends ConfigFormBase {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function getFormId(): string {
|
||||||
|
return 'site_users_microsite_settings_form';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
protected function getEditableConfigNames(): array {
|
||||||
|
return ['site_users_microsite.settings'];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function buildForm(array $form, FormStateInterface $form_state): array {
|
||||||
|
$config = $this->config('site_users_microsite.settings');
|
||||||
|
$content_types = $this->getContentTypes();
|
||||||
|
|
||||||
|
$form['role_content_types'] = [
|
||||||
|
'#type' => 'fieldset',
|
||||||
|
'#title' => $this->t('Content types per role'),
|
||||||
|
'#description' => $this->t('Select which content types each role can publish in their micro-site.'),
|
||||||
|
'#tree' => TRUE,
|
||||||
|
];
|
||||||
|
|
||||||
|
if (empty($content_types)) {
|
||||||
|
$form['role_content_types']['_empty'] = [
|
||||||
|
'#markup' => '<p>' . $this->t('No content types found.') . '</p>',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
foreach (Role::loadMultiple() as $role_id => $role) {
|
||||||
|
if (in_array($role_id, ['anonymous', 'authenticated'])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$form['role_content_types'][$role_id] = [
|
||||||
|
'#type' => 'checkboxes',
|
||||||
|
'#title' => $role->label(),
|
||||||
|
'#options' => $content_types,
|
||||||
|
'#default_value' => $config->get('role_content_types.' . $role_id) ?? [],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return parent::buildForm($form, $form_state);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function submitForm(array &$form, FormStateInterface $form_state): void {
|
||||||
|
$config = $this->config('site_users_microsite.settings');
|
||||||
|
$raw = $form_state->getValue('role_content_types') ?? [];
|
||||||
|
|
||||||
|
foreach (Role::loadMultiple() as $role_id => $role) {
|
||||||
|
if (in_array($role_id, ['anonymous', 'authenticated']) || !isset($raw[$role_id])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$config->set('role_content_types.' . $role_id, array_values(array_filter($raw[$role_id])));
|
||||||
|
}
|
||||||
|
|
||||||
|
$config->save();
|
||||||
|
parent::submitForm($form, $form_state);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retorna todos os tipos de conteúdo disponíveis.
|
||||||
|
*/
|
||||||
|
protected function getContentTypes(): array {
|
||||||
|
$options = [];
|
||||||
|
foreach (\Drupal::entityTypeManager()->getStorage('node_type')->loadMultiple() as $type) {
|
||||||
|
$options[$type->id()] = $type->label();
|
||||||
|
}
|
||||||
|
ksort($options);
|
||||||
|
return $options;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,180 @@
|
|||||||
|
<?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(),
|
||||||
|
'#homepage' => $this->getFieldUri($user, 'field_user_homepage'),
|
||||||
|
'#lattes_id' => $this->getFieldValue($user, 'field_user_id_lattes'),
|
||||||
|
'#orcid_id' => $this->getFieldValue($user, 'field_user_orcid'),
|
||||||
|
'#mathscinet_id' => $this->getFieldValue($user, 'field_user_mathscinetid'),
|
||||||
|
'#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 a URI de um campo link do usuário.
|
||||||
|
*/
|
||||||
|
protected function getFieldUri(UserInterface $user, string $field_name): ?string {
|
||||||
|
if ($user->hasField($field_name) && !$user->get($field_name)->isEmpty()) {
|
||||||
|
return $user->get($field_name)->uri;
|
||||||
|
}
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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']);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
23
modules/site_users_microsite/src/Routing/RouteSubscriber.php
Normal file
23
modules/site_users_microsite/src/Routing/RouteSubscriber.php
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Drupal\site_users_microsite\Routing;
|
||||||
|
|
||||||
|
use Drupal\Core\Routing\RouteSubscriberBase;
|
||||||
|
use Symfony\Component\Routing\RouteCollection;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sobrescreve a rota canônica do usuário para exibir o micro-site.
|
||||||
|
*/
|
||||||
|
class RouteSubscriber extends RouteSubscriberBase {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
protected function alterRoutes(RouteCollection $collection): void {
|
||||||
|
if ($route = $collection->get('entity.user.canonical')) {
|
||||||
|
$route->setDefault('_controller', '\Drupal\site_users_microsite\Controller\MicrositeHomeController::home');
|
||||||
|
$route->setDefault('_title_callback', '\Drupal\site_users_microsite\Controller\MicrositeHomeController::title');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Drupal\site_users_microsite\Theme;
|
||||||
|
|
||||||
|
use Drupal\Core\Routing\RouteMatchInterface;
|
||||||
|
use Drupal\Core\Theme\ThemeNegotiatorInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Aplica o tema microsite nas rotas de perfil de usuário e do micro-site.
|
||||||
|
*/
|
||||||
|
class MicrositeThemeNegotiator implements ThemeNegotiatorInterface {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function applies(RouteMatchInterface $route_match): bool {
|
||||||
|
$route_name = $route_match->getRouteName();
|
||||||
|
$excluded = ['site_users_microsite.settings'];
|
||||||
|
return ($route_name === 'entity.user.canonical'
|
||||||
|
|| str_starts_with($route_name, 'site_users_microsite.'))
|
||||||
|
&& !in_array($route_name, $excluded);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function determineActiveTheme(RouteMatchInterface $route_match): ?string {
|
||||||
|
return 'site_users_microsite_theme';
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
{#
|
||||||
|
/**
|
||||||
|
* @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).
|
||||||
|
* - homepage: URL da página pessoal (string|null).
|
||||||
|
* - lattes_id: ID do Currículo Lattes (string|null).
|
||||||
|
* - orcid_id: ORCID iD (string|null).
|
||||||
|
* - mathscinet_id: MathSciNet Author ID (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 %}
|
||||||
|
|
||||||
|
{% if homepage or lattes_id or orcid_id or mathscinet_id %}
|
||||||
|
<div class="msite-header-block__academic-links">
|
||||||
|
|
||||||
|
{% if homepage %}
|
||||||
|
<a href="{{ homepage }}"
|
||||||
|
class="msite-header-block__academic-link msite-header-block__academic-link--homepage"
|
||||||
|
target="_blank" rel="noopener noreferrer"
|
||||||
|
title="{{ 'Página pessoal'|t }}"
|
||||||
|
aria-label="{{ 'Página pessoal'|t }}">
|
||||||
|
{{ site_tools_academic_icon('home') }}
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if lattes_id %}
|
||||||
|
<a href="https://lattes.cnpq.br/{{ lattes_id }}"
|
||||||
|
class="msite-header-block__academic-link msite-header-block__academic-link--lattes"
|
||||||
|
target="_blank" rel="noopener noreferrer"
|
||||||
|
title="{{ 'Currículo Lattes'|t }}"
|
||||||
|
aria-label="{{ 'Currículo Lattes'|t }}">
|
||||||
|
{{ site_tools_academic_icon('lattes') }}
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if orcid_id %}
|
||||||
|
<a href="https://orcid.org/{{ orcid_id }}"
|
||||||
|
class="msite-header-block__academic-link msite-header-block__academic-link--orcid"
|
||||||
|
target="_blank" rel="noopener noreferrer"
|
||||||
|
title="{{ 'Perfil ORCID'|t }}"
|
||||||
|
aria-label="{{ 'Perfil ORCID'|t }}">
|
||||||
|
{{ site_tools_academic_icon('orcid') }}
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if mathscinet_id %}
|
||||||
|
<a href="https://mathscinet.ams.org/mathscinet/author?AuthorID={{ mathscinet_id }}"
|
||||||
|
class="msite-header-block__academic-link msite-header-block__academic-link--mathscinet"
|
||||||
|
target="_blank" rel="noopener noreferrer"
|
||||||
|
title="{{ 'Perfil MathSciNet'|t }}"
|
||||||
|
aria-label="{{ 'Perfil MathSciNet'|t }}">
|
||||||
|
{{ site_tools_academic_icon('mathscinet') }}
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
@@ -548,6 +548,72 @@ function site_users_update_10008() {
|
|||||||
return t('MathSciNet ID field created successfully.');
|
return t('MathSciNet ID field created successfully.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cria o campo field_user_homepage para página pessoal do usuário.
|
||||||
|
*/
|
||||||
|
function site_users_update_10009() {
|
||||||
|
if (!FieldStorageConfig::loadByName('user', 'field_user_homepage')) {
|
||||||
|
FieldStorageConfig::create([
|
||||||
|
'field_name' => 'field_user_homepage',
|
||||||
|
'entity_type' => 'user',
|
||||||
|
'type' => 'link',
|
||||||
|
'settings' => [
|
||||||
|
'title' => 0,
|
||||||
|
'link_type' => 16,
|
||||||
|
],
|
||||||
|
'cardinality' => 1,
|
||||||
|
'translatable' => TRUE,
|
||||||
|
])->save();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!FieldConfig::loadByName('user', 'user', 'field_user_homepage')) {
|
||||||
|
FieldConfig::create([
|
||||||
|
'field_name' => 'field_user_homepage',
|
||||||
|
'entity_type' => 'user',
|
||||||
|
'bundle' => 'user',
|
||||||
|
'label' => 'Personal homepage',
|
||||||
|
'description' => 'URL of the user personal homepage.',
|
||||||
|
'required' => FALSE,
|
||||||
|
'settings' => [
|
||||||
|
'title' => 0,
|
||||||
|
'link_type' => 16,
|
||||||
|
],
|
||||||
|
])->save();
|
||||||
|
}
|
||||||
|
|
||||||
|
$form_display = EntityFormDisplay::load('user.user.default');
|
||||||
|
if ($form_display && !$form_display->getComponent('field_user_homepage')) {
|
||||||
|
$form_display->setComponent('field_user_homepage', [
|
||||||
|
'type' => 'link_default',
|
||||||
|
'weight' => 12,
|
||||||
|
'settings' => [
|
||||||
|
'placeholder_url' => 'https://',
|
||||||
|
'placeholder_title' => '',
|
||||||
|
],
|
||||||
|
'region' => 'content',
|
||||||
|
])->save();
|
||||||
|
}
|
||||||
|
|
||||||
|
$view_display = EntityViewDisplay::load('user.user.default');
|
||||||
|
if ($view_display && !$view_display->getComponent('field_user_homepage')) {
|
||||||
|
$view_display->setComponent('field_user_homepage', [
|
||||||
|
'type' => 'link',
|
||||||
|
'weight' => 12,
|
||||||
|
'label' => 'above',
|
||||||
|
'settings' => [
|
||||||
|
'trim_length' => 80,
|
||||||
|
'url_only' => FALSE,
|
||||||
|
'url_plain' => FALSE,
|
||||||
|
'rel' => 'noopener noreferrer',
|
||||||
|
'target' => '_blank',
|
||||||
|
],
|
||||||
|
'region' => 'content',
|
||||||
|
])->save();
|
||||||
|
}
|
||||||
|
|
||||||
|
return t('Campo field_user_homepage criado com sucesso.');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Corrige mapeamentos LDAP com campos de string nulos na config ativa.
|
* Corrige mapeamentos LDAP com campos de string nulos na config ativa.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -3,3 +3,9 @@ user-info-block:
|
|||||||
css:
|
css:
|
||||||
component:
|
component:
|
||||||
css/site-user-info-block.css: {}
|
css/site-user-info-block.css: {}
|
||||||
|
|
||||||
|
user_photos_widget:
|
||||||
|
version: 1.x
|
||||||
|
css:
|
||||||
|
component:
|
||||||
|
css/user-photos-widget.css: {}
|
||||||
|
|||||||
5
site_users.links.task.yml
Normal file
5
site_users.links.task.yml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
site_users.settings_tab:
|
||||||
|
route_name: site_users.settings
|
||||||
|
title: 'General'
|
||||||
|
base_route: site_users.settings
|
||||||
|
weight: 0
|
||||||
@@ -32,13 +32,7 @@ function site_users_theme($existing, $type, $theme, $path) {
|
|||||||
'template' => 'user--restricted',
|
'template' => 'user--restricted',
|
||||||
'base hook' => 'user',
|
'base hook' => 'user',
|
||||||
],
|
],
|
||||||
'site_users_info_block' => [
|
|
||||||
'template' => 'site-user-info-block',
|
|
||||||
'variables' => [
|
|
||||||
'user_info' => [],
|
|
||||||
'user' => NULL,
|
|
||||||
],
|
|
||||||
],
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -327,8 +321,18 @@ function site_users_form_user_register_form_alter(&$form, FormStateInterface $fo
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Adiciona o seletor de foto padrão ao formulário.
|
* Adiciona o seletor de foto padrão ao formulário.
|
||||||
|
*
|
||||||
|
* Ignorado quando o widget user_photos_widget está ativo para
|
||||||
|
* field_user_photos, pois ele já gerencia a foto padrão.
|
||||||
*/
|
*/
|
||||||
function _site_users_add_default_photo_selector(&$form, FormStateInterface $form_state) {
|
function _site_users_add_default_photo_selector(&$form, FormStateInterface $form_state) {
|
||||||
|
// Verificar se o widget unificado está sendo usado.
|
||||||
|
$form_display = $form_state->getFormObject()->getFormDisplay($form_state);
|
||||||
|
$component = $form_display->getComponent('field_user_photos');
|
||||||
|
if ($component && ($component['type'] ?? '') === 'user_photos_widget') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
/** @var \Drupal\user\UserInterface $user */
|
/** @var \Drupal\user\UserInterface $user */
|
||||||
$user = $form_state->getFormObject()->getEntity();
|
$user = $form_state->getFormObject()->getEntity();
|
||||||
|
|
||||||
|
|||||||
@@ -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']);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
48
src/Plugin/Field/FieldFormatter/LattesLinkFormatter.php
Normal file
48
src/Plugin/Field/FieldFormatter/LattesLinkFormatter.php
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Drupal\site_users\Plugin\Field\FieldFormatter;
|
||||||
|
|
||||||
|
use Drupal\Core\Field\Attribute\FieldFormatter;
|
||||||
|
use Drupal\Core\Field\FieldDefinitionInterface;
|
||||||
|
use Drupal\Core\Field\FieldItemListInterface;
|
||||||
|
use Drupal\Core\Field\FormatterBase;
|
||||||
|
use Drupal\Core\StringTranslation\TranslatableMarkup;
|
||||||
|
use Drupal\Core\Url;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formata o ID Lattes como link para o Currículo Lattes.
|
||||||
|
*
|
||||||
|
* URL gerada: https://lattes.cnpq.br/{id}
|
||||||
|
*
|
||||||
|
* @FieldFormatter(
|
||||||
|
* id = "site_users_lattes_link",
|
||||||
|
* label = @Translation("Link para o Currículo Lattes"),
|
||||||
|
* field_types = {"integer"}
|
||||||
|
* )
|
||||||
|
*/
|
||||||
|
#[FieldFormatter(
|
||||||
|
id: 'site_users_lattes_link',
|
||||||
|
label: new TranslatableMarkup('Link para o Currículo Lattes'),
|
||||||
|
field_types: ['integer'],
|
||||||
|
)]
|
||||||
|
class LattesLinkFormatter extends FormatterBase {
|
||||||
|
|
||||||
|
public static function isApplicable(FieldDefinitionInterface $field_definition): bool {
|
||||||
|
return $field_definition->getName() === 'field_user_id_lattes';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function viewElements(FieldItemListInterface $items, $langcode): array {
|
||||||
|
$elements = [];
|
||||||
|
foreach ($items as $delta => $item) {
|
||||||
|
$id = (string) $item->value;
|
||||||
|
$elements[$delta] = [
|
||||||
|
'#type' => 'link',
|
||||||
|
'#title' => $id,
|
||||||
|
'#url' => Url::fromUri('https://lattes.cnpq.br/' . $id),
|
||||||
|
'#attributes' => ['target' => '_blank', 'rel' => 'noopener noreferrer'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
return $elements;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
48
src/Plugin/Field/FieldFormatter/MathSciNetLinkFormatter.php
Normal file
48
src/Plugin/Field/FieldFormatter/MathSciNetLinkFormatter.php
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Drupal\site_users\Plugin\Field\FieldFormatter;
|
||||||
|
|
||||||
|
use Drupal\Core\Field\Attribute\FieldFormatter;
|
||||||
|
use Drupal\Core\Field\FieldDefinitionInterface;
|
||||||
|
use Drupal\Core\Field\FieldItemListInterface;
|
||||||
|
use Drupal\Core\Field\FormatterBase;
|
||||||
|
use Drupal\Core\StringTranslation\TranslatableMarkup;
|
||||||
|
use Drupal\Core\Url;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formata o MathSciNet ID como link para o perfil de autor no MathSciNet.
|
||||||
|
*
|
||||||
|
* URL gerada: https://mathscinet.ams.org/mathscinet/author?AuthorID={id}
|
||||||
|
*
|
||||||
|
* @FieldFormatter(
|
||||||
|
* id = "site_users_mathscinet_link",
|
||||||
|
* label = @Translation("Link para o perfil MathSciNet"),
|
||||||
|
* field_types = {"integer"}
|
||||||
|
* )
|
||||||
|
*/
|
||||||
|
#[FieldFormatter(
|
||||||
|
id: 'site_users_mathscinet_link',
|
||||||
|
label: new TranslatableMarkup('Link para o perfil MathSciNet'),
|
||||||
|
field_types: ['integer'],
|
||||||
|
)]
|
||||||
|
class MathSciNetLinkFormatter extends FormatterBase {
|
||||||
|
|
||||||
|
public static function isApplicable(FieldDefinitionInterface $field_definition): bool {
|
||||||
|
return $field_definition->getName() === 'field_user_mathscinetid';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function viewElements(FieldItemListInterface $items, $langcode): array {
|
||||||
|
$elements = [];
|
||||||
|
foreach ($items as $delta => $item) {
|
||||||
|
$id = (string) $item->value;
|
||||||
|
$elements[$delta] = [
|
||||||
|
'#type' => 'link',
|
||||||
|
'#title' => $id,
|
||||||
|
'#url' => Url::fromUri('https://mathscinet.ams.org/mathscinet/author?AuthorID=' . $id),
|
||||||
|
'#attributes' => ['target' => '_blank', 'rel' => 'noopener noreferrer'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
return $elements;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
48
src/Plugin/Field/FieldFormatter/OrcidLinkFormatter.php
Normal file
48
src/Plugin/Field/FieldFormatter/OrcidLinkFormatter.php
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Drupal\site_users\Plugin\Field\FieldFormatter;
|
||||||
|
|
||||||
|
use Drupal\Core\Field\Attribute\FieldFormatter;
|
||||||
|
use Drupal\Core\Field\FieldDefinitionInterface;
|
||||||
|
use Drupal\Core\Field\FieldItemListInterface;
|
||||||
|
use Drupal\Core\Field\FormatterBase;
|
||||||
|
use Drupal\Core\StringTranslation\TranslatableMarkup;
|
||||||
|
use Drupal\Core\Url;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formata o ORCID como link para o perfil em orcid.org.
|
||||||
|
*
|
||||||
|
* URL gerada: https://orcid.org/{orcid}
|
||||||
|
*
|
||||||
|
* @FieldFormatter(
|
||||||
|
* id = "site_users_orcid_link",
|
||||||
|
* label = @Translation("Link para o perfil ORCID"),
|
||||||
|
* field_types = {"string"}
|
||||||
|
* )
|
||||||
|
*/
|
||||||
|
#[FieldFormatter(
|
||||||
|
id: 'site_users_orcid_link',
|
||||||
|
label: new TranslatableMarkup('Link para o perfil ORCID'),
|
||||||
|
field_types: ['string'],
|
||||||
|
)]
|
||||||
|
class OrcidLinkFormatter extends FormatterBase {
|
||||||
|
|
||||||
|
public static function isApplicable(FieldDefinitionInterface $field_definition): bool {
|
||||||
|
return $field_definition->getName() === 'field_user_orcid';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function viewElements(FieldItemListInterface $items, $langcode): array {
|
||||||
|
$elements = [];
|
||||||
|
foreach ($items as $delta => $item) {
|
||||||
|
$id = $item->value;
|
||||||
|
$elements[$delta] = [
|
||||||
|
'#type' => 'link',
|
||||||
|
'#title' => $id,
|
||||||
|
'#url' => Url::fromUri('https://orcid.org/' . $id),
|
||||||
|
'#attributes' => ['target' => '_blank', 'rel' => 'noopener noreferrer'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
return $elements;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
262
src/Plugin/Field/FieldWidget/UserPhotosWidget.php
Normal file
262
src/Plugin/Field/FieldWidget/UserPhotosWidget.php
Normal file
@@ -0,0 +1,262 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Drupal\site_users\Plugin\Field\FieldWidget;
|
||||||
|
|
||||||
|
use Drupal\Component\Utility\NestedArray;
|
||||||
|
use Drupal\Core\Entity\EntityInterface;
|
||||||
|
use Drupal\Core\Entity\EntityTypeManagerInterface;
|
||||||
|
use Drupal\Core\Field\FieldDefinitionInterface;
|
||||||
|
use Drupal\Core\Field\FieldItemListInterface;
|
||||||
|
use Drupal\Core\Field\WidgetBase;
|
||||||
|
use Drupal\Core\Form\FormStateInterface;
|
||||||
|
use Drupal\file\FileInterface;
|
||||||
|
use Drupal\media\MediaInterface;
|
||||||
|
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Widget de tira de thumbnails para gerenciar fotos de usuário.
|
||||||
|
*
|
||||||
|
* Exibe as fotos em linha; a foto padrão é destacada com borda colorida.
|
||||||
|
* Clicar em um thumbnail muda a foto padrão. Ao salvar, atualiza
|
||||||
|
* field_user_default_photo na entidade.
|
||||||
|
*
|
||||||
|
* @FieldWidget(
|
||||||
|
* id = "user_photos_widget",
|
||||||
|
* label = @Translation("Galeria de fotos com foto padrão"),
|
||||||
|
* field_types = {
|
||||||
|
* "entity_reference"
|
||||||
|
* }
|
||||||
|
* )
|
||||||
|
*/
|
||||||
|
class UserPhotosWidget extends WidgetBase {
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
$plugin_id,
|
||||||
|
$plugin_definition,
|
||||||
|
FieldDefinitionInterface $field_definition,
|
||||||
|
array $settings,
|
||||||
|
array $third_party_settings,
|
||||||
|
protected EntityTypeManagerInterface $entityTypeManager,
|
||||||
|
) {
|
||||||
|
parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $third_party_settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition): static {
|
||||||
|
return new static(
|
||||||
|
$plugin_id,
|
||||||
|
$plugin_definition,
|
||||||
|
$configuration['field_definition'],
|
||||||
|
$configuration['settings'],
|
||||||
|
$configuration['third_party_settings'],
|
||||||
|
$container->get('entity_type.manager'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function isApplicable(FieldDefinitionInterface $field_definition): bool {
|
||||||
|
$storage = $field_definition->getFieldStorageDefinition();
|
||||||
|
if ($storage->getSetting('target_type') !== 'media') {
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
$handler_settings = $field_definition->getSetting('handler_settings');
|
||||||
|
return !empty($handler_settings['target_bundles']['image']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function form(FieldItemListInterface $items, array &$form, FormStateInterface $form_state, $get_delta = NULL): array {
|
||||||
|
$field_name = $this->fieldDefinition->getName();
|
||||||
|
$parents = $form['#parents'] ?? [];
|
||||||
|
|
||||||
|
$element = [
|
||||||
|
'#field_name' => $field_name,
|
||||||
|
'#field_parents' => $parents,
|
||||||
|
'#parents' => array_merge($parents, [$field_name]),
|
||||||
|
'#array_parents' => array_merge($form['#array_parents'] ?? [], [$field_name]),
|
||||||
|
'#tree' => TRUE,
|
||||||
|
'#attached' => ['library' => ['site_users/user_photos_widget']],
|
||||||
|
'#attributes' => ['class' => ['user-photos-widget']],
|
||||||
|
];
|
||||||
|
|
||||||
|
$entity = $items->getEntity();
|
||||||
|
|
||||||
|
// Foto padrão atual: na entidade salva, ou primeira disponível.
|
||||||
|
$current_default = '';
|
||||||
|
if ($entity->hasField('field_user_default_photo') && !$entity->get('field_user_default_photo')->isEmpty()) {
|
||||||
|
$current_default = (string) $entity->get('field_user_default_photo')->target_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Coleta mídias existentes na ordem atual do campo.
|
||||||
|
$medias = [];
|
||||||
|
foreach ($items as $item) {
|
||||||
|
if (!$item->isEmpty() && $item->entity instanceof MediaInterface) {
|
||||||
|
$medias[(string) $item->entity->id()] = $item->entity;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($current_default === '' || !isset($medias[$current_default])) {
|
||||||
|
$current_default = (string) (array_key_first($medias) ?? '');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Caminho compartilhado por todos os radios do grupo "foto padrão".
|
||||||
|
// Todos os radios com o mesmo #parents geram o mesmo HTML name, formando
|
||||||
|
// um grupo. O Drupal calcula #checked automaticamente comparando
|
||||||
|
// #return_value com o valor submetido / #default_value.
|
||||||
|
$radio_parents = array_merge($element['#parents'], ['default_photo']);
|
||||||
|
|
||||||
|
// --- Tira de thumbnails ---
|
||||||
|
if (!empty($medias)) {
|
||||||
|
$element['photos_label'] = [
|
||||||
|
'#type' => 'html_tag',
|
||||||
|
'#tag' => 'p',
|
||||||
|
'#value' => $this->t('Selecionar foto padrão'),
|
||||||
|
'#attributes' => ['class' => ['user-photos-strip-label']],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$element['photos'] = [
|
||||||
|
'#type' => 'container',
|
||||||
|
'#attributes' => ['class' => ['user-photos-strip']],
|
||||||
|
];
|
||||||
|
|
||||||
|
if (empty($medias)) {
|
||||||
|
$element['photos']['empty'] = [
|
||||||
|
'#type' => 'html_tag',
|
||||||
|
'#tag' => 'p',
|
||||||
|
'#value' => $this->t('Nenhuma foto adicionada ainda.'),
|
||||||
|
'#attributes' => ['class' => ['user-photos-empty']],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$view_builder = $this->entityTypeManager->getViewBuilder('media');
|
||||||
|
|
||||||
|
foreach ($medias as $mid_str => $media) {
|
||||||
|
// Cada foto é um card .user-photos-item.
|
||||||
|
$element['photos'][$mid_str] = [
|
||||||
|
'#type' => 'container',
|
||||||
|
'#attributes' => ['class' => ['user-photos-item']],
|
||||||
|
];
|
||||||
|
|
||||||
|
// Radio oculto via CSS: o título (thumbnail) torna-se o <label> clicável.
|
||||||
|
// Todos os radios compartilham #parents → mesmo HTML name → grupo rádio.
|
||||||
|
// Drupal calcula #checked comparando #return_value com o valor do grupo.
|
||||||
|
$element['photos'][$mid_str]['default'] = [
|
||||||
|
'#type' => 'radio',
|
||||||
|
'#title' => $view_builder->view($media, 'thumbnail'),
|
||||||
|
'#title_display' => 'after',
|
||||||
|
'#return_value' => $mid_str,
|
||||||
|
'#default_value' => $current_default,
|
||||||
|
'#parents' => $radio_parents,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Upload de nova foto ---
|
||||||
|
$uid = $entity->id() ?: 'new';
|
||||||
|
$max_count = \Drupal::config('site_users.settings')->get('photos.max_count') ?? 5;
|
||||||
|
|
||||||
|
$element['new_photo'] = [
|
||||||
|
'#type' => 'managed_file',
|
||||||
|
'#title' => $this->t('Adicionar foto'),
|
||||||
|
'#description' => $this->t(
|
||||||
|
'Formatos aceitos: JPEG, PNG, GIF, WebP. Máximo @max fotos no total.',
|
||||||
|
['@max' => $max_count],
|
||||||
|
),
|
||||||
|
'#upload_location' => 'public://user-photos/' . $uid . '/',
|
||||||
|
'#upload_validators' => [
|
||||||
|
'FileExtension' => ['extensions' => 'jpg jpeg png gif webp'],
|
||||||
|
'FileSizeLimit' => ['fileLimit' => 10 * 1024 * 1024],
|
||||||
|
],
|
||||||
|
'#multiple' => FALSE,
|
||||||
|
];
|
||||||
|
|
||||||
|
return $element;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function extractFormValues(FieldItemListInterface $items, array $form, FormStateInterface $form_state): void {
|
||||||
|
$field_name = $this->fieldDefinition->getName();
|
||||||
|
$parents = $form['#parents'] ?? [];
|
||||||
|
|
||||||
|
$widget_values = NestedArray::getValue(
|
||||||
|
$form_state->getValues(),
|
||||||
|
array_merge($parents, [$field_name]),
|
||||||
|
) ?? [];
|
||||||
|
|
||||||
|
// IDs atuais na ordem do campo.
|
||||||
|
$current_ids = [];
|
||||||
|
foreach ($items as $item) {
|
||||||
|
if (!$item->isEmpty()) {
|
||||||
|
$current_ids[] = (string) $item->target_id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$remaining_ids = $current_ids;
|
||||||
|
|
||||||
|
// Processa upload de nova foto.
|
||||||
|
$new_fids = $widget_values['new_photo'] ?? [];
|
||||||
|
if (!empty($new_fids)) {
|
||||||
|
$fid = is_array($new_fids) ? (int) reset($new_fids) : (int) $new_fids;
|
||||||
|
if ($fid) {
|
||||||
|
$new_mid = $this->createMediaFromFile($fid, $items->getEntity());
|
||||||
|
if ($new_mid) {
|
||||||
|
$remaining_ids[] = (string) $new_mid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Atualiza field_user_photos.
|
||||||
|
$values = array_map(fn($id) => ['target_id' => (int) $id], $remaining_ids);
|
||||||
|
$items->setValue($values);
|
||||||
|
$items->filterEmptyItems();
|
||||||
|
|
||||||
|
// Atualiza field_user_default_photo.
|
||||||
|
// O valor do grupo de radios está em default_photo (caminho compartilhado).
|
||||||
|
$default_photo_id = (string) ($widget_values['default_photo'] ?? '');
|
||||||
|
$entity = $items->getEntity();
|
||||||
|
if ($entity->hasField('field_user_default_photo')) {
|
||||||
|
$entity->set('field_user_default_photo', $default_photo_id ?: NULL);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cria uma entidade de mídia do tipo 'image' a partir de um FID.
|
||||||
|
*/
|
||||||
|
protected function createMediaFromFile(int $fid, EntityInterface $owner): ?int {
|
||||||
|
$file = $this->entityTypeManager->getStorage('file')->load($fid);
|
||||||
|
if (!$file instanceof FileInterface) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
$file->setPermanent();
|
||||||
|
$file->save();
|
||||||
|
|
||||||
|
$media_type = $this->entityTypeManager->getStorage('media_type')->load('image');
|
||||||
|
if (!$media_type) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
$source_field = $media_type->getSource()->getConfiguration()['source_field'];
|
||||||
|
|
||||||
|
$media = $this->entityTypeManager->getStorage('media')->create([
|
||||||
|
'bundle' => 'image',
|
||||||
|
'uid' => $owner->id() ?: \Drupal::currentUser()->id(),
|
||||||
|
'status' => 1,
|
||||||
|
$source_field => [
|
||||||
|
'target_id' => $fid,
|
||||||
|
'alt' => $file->getFilename(),
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
$media->save();
|
||||||
|
|
||||||
|
return (int) $media->id();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state): array {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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>
|
|
||||||
522
themes/site_users_microsite_theme/css/microsite.css
Normal file
522
themes/site_users_microsite_theme/css/microsite.css
Normal file
@@ -0,0 +1,522 @@
|
|||||||
|
/**
|
||||||
|
* Microsite theme — estilos base.
|
||||||
|
* Ponto de partida minimalista. Personalize conforme o design do site.
|
||||||
|
*/
|
||||||
|
|
||||||
|
*,
|
||||||
|
*::before,
|
||||||
|
*::after {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.microsite {
|
||||||
|
margin: 0;
|
||||||
|
font-family: system-ui, sans-serif;
|
||||||
|
background: #f5f5f5;
|
||||||
|
color: #222;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Skip link */
|
||||||
|
.skip-link {
|
||||||
|
position: absolute;
|
||||||
|
top: -100%;
|
||||||
|
left: 0;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
background: #000;
|
||||||
|
color: #fff;
|
||||||
|
z-index: 9999;
|
||||||
|
}
|
||||||
|
.skip-link:focus {
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Header ------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
.microsite-header {
|
||||||
|
background: #1a1a2e;
|
||||||
|
color: #fff;
|
||||||
|
padding: 2rem 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.microsite-header__inner {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1.5rem;
|
||||||
|
max-width: 900px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.microsite-header__photo img {
|
||||||
|
width: 80px;
|
||||||
|
height: 80px;
|
||||||
|
border-radius: 50%;
|
||||||
|
object-fit: cover;
|
||||||
|
border: 3px solid rgba(255, 255, 255, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.microsite-header__name {
|
||||||
|
margin: 0 0 0.25rem;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.microsite-header__roles {
|
||||||
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.microsite-header__role {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
background: rgba(255, 255, 255, 0.15);
|
||||||
|
padding: 0.2rem 0.6rem;
|
||||||
|
border-radius: 999px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Navigation --------------------------------------------------------------- */
|
||||||
|
|
||||||
|
.microsite-nav {
|
||||||
|
background: #16213e;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.microsite-nav ul.tabs {
|
||||||
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0 1.5rem;
|
||||||
|
display: flex;
|
||||||
|
max-width: 900px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.microsite-nav ul.tabs li {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.microsite-nav ul.tabs a {
|
||||||
|
display: block;
|
||||||
|
padding: 0.75rem 1.25rem;
|
||||||
|
color: rgba(255, 255, 255, 0.7);
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
border-bottom: 3px solid transparent;
|
||||||
|
transition: color 0.2s, border-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.microsite-nav ul.tabs a:hover,
|
||||||
|
.microsite-nav ul.tabs a:focus {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.microsite-nav ul.tabs li.is-active a,
|
||||||
|
.microsite-nav ul.tabs a.is-active {
|
||||||
|
color: #fff;
|
||||||
|
border-bottom-color: #e94560;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Highlighted -------------------------------------------------------------- */
|
||||||
|
|
||||||
|
.microsite-highlighted {
|
||||||
|
max-width: 900px;
|
||||||
|
margin: 1.5rem auto 0;
|
||||||
|
padding: 0 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Main + Sidebar ----------------------------------------------------------- */
|
||||||
|
|
||||||
|
.microsite-main-wrapper {
|
||||||
|
max-width: 900px;
|
||||||
|
margin: 2rem auto;
|
||||||
|
padding: 0 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.microsite-main-wrapper--has-sidebar {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 280px;
|
||||||
|
gap: 2rem;
|
||||||
|
max-width: 1200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.microsite-main {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.microsite-sidebar {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.microsite-messages {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- Links acadêmicos (Lattes, ORCID, MathSciNet) ------------------------- */
|
||||||
|
|
||||||
|
.msite-header-block__academic-links {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.6rem;
|
||||||
|
margin-top: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.msite-header-block__academic-link svg {
|
||||||
|
display: block;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
transition: opacity 0.15s, transform 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.msite-header-block__academic-link--homepage svg {
|
||||||
|
fill: rgba(255, 255, 255, 0.85);
|
||||||
|
}
|
||||||
|
|
||||||
|
.msite-header-block__academic-link:hover svg {
|
||||||
|
opacity: 0.85;
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Top Bar ------------------------------------------------------------------ */
|
||||||
|
/*
|
||||||
|
* Barra estreita no topo da página, antes do header.
|
||||||
|
* Destinada ao menu do usuário (login, conta, logout).
|
||||||
|
* Alinhada à direita, fundo escuro, texto claro.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.microsite-top-bar {
|
||||||
|
background-color: hsl(201, 15%, 20%);
|
||||||
|
padding-inline: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.microsite-top-bar .block-menu {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.microsite-top-bar ul.menu {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.microsite-top-bar li.menu__item:not(:last-child) {
|
||||||
|
margin-inline-end: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.microsite-top-bar ul.menu ul.menu {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.microsite-top-bar a.menu__link {
|
||||||
|
position: relative;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
padding-block: 7px;
|
||||||
|
text-decoration: none;
|
||||||
|
color: hsl(201, 15%, 88%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.microsite-top-bar a.menu__link:hover {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.microsite-top-bar a.menu__link.is-active {
|
||||||
|
color: hsl(202, 79%, 70%);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Navigation --------------------------------------------------------------- */
|
||||||
|
/*
|
||||||
|
* Estilo baseado no nav-secondary do Olivero.
|
||||||
|
* Variáveis Olivero traduzidas para valores fixos (o tema base é stable9):
|
||||||
|
* --color--primary-50 → hsl(202, 79%, 50%) ≈ #1a9fd4
|
||||||
|
* --color--gray-5 → hsl(201, 15%, 5%) ≈ #0b0e0f (texto)
|
||||||
|
* --sp → 18px
|
||||||
|
* --sp0-5 → 9px
|
||||||
|
* --sp2 → 36px
|
||||||
|
* --font-size-s → 14px
|
||||||
|
*/
|
||||||
|
|
||||||
|
.microsite-nav {
|
||||||
|
background-color: #fff;
|
||||||
|
border-bottom: 3px solid hsl(202, 79%, 50%);
|
||||||
|
padding-inline: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Bloco de menu padrão dentro da região nav */
|
||||||
|
.microsite-nav .block-menu {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Lista principal: horizontal, sem marcadores */
|
||||||
|
.microsite-nav ul.menu {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Itens: espaço entre eles */
|
||||||
|
.microsite-nav li.menu__item:not(:last-child) {
|
||||||
|
margin-inline-end: 36px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Subitens: não exibidos (menu plano) */
|
||||||
|
.microsite-nav ul.menu ul.menu {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Links */
|
||||||
|
.microsite-nav a.menu__link {
|
||||||
|
position: relative;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
padding-block: 12px;
|
||||||
|
text-decoration: none;
|
||||||
|
color: hsl(201, 15%, 5%);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Underline animado no hover (padrão Olivero) */
|
||||||
|
.microsite-nav a.menu__link::after {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 0;
|
||||||
|
content: "";
|
||||||
|
transition: opacity 0.2s, transform 0.2s;
|
||||||
|
transform: translateY(5px);
|
||||||
|
opacity: 0;
|
||||||
|
border-top: solid 2px currentColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
.microsite-nav a.menu__link:hover::after,
|
||||||
|
.microsite-nav a.menu__link.is-active::after {
|
||||||
|
transform: translateY(0);
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Link ativo: cor primária */
|
||||||
|
.microsite-nav a.menu__link.is-active {
|
||||||
|
color: hsl(202, 79%, 50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Social Bar --------------------------------------------------------------- */
|
||||||
|
/*
|
||||||
|
* Mobile: barra horizontal compacta.
|
||||||
|
* Desktop (≥ 1200px): coluna lateral fixa à esquerda, 90px de largura,
|
||||||
|
* conteúdo girado −90° (estilo Olivero Social Bar).
|
||||||
|
*/
|
||||||
|
|
||||||
|
.social-bar__inner {
|
||||||
|
position: relative;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.social-bar__inner .rotate > * {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1200px) {
|
||||||
|
/* Layout flex: barra social à esquerda, conteúdo principal à direita */
|
||||||
|
.microsite-layout--has-social {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.microsite-content-area {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Coluna .social-bar no fluxo (placeholder de 90px) */
|
||||||
|
.social-bar {
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 90px;
|
||||||
|
background-color: #f3f3f3;
|
||||||
|
align-self: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Wrapper interno: posicionamento relativo normal; .is-fixed o fixa */
|
||||||
|
.social-bar__inner {
|
||||||
|
position: relative;
|
||||||
|
width: 90px;
|
||||||
|
padding: 2.5rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.social-bar__inner.is-fixed {
|
||||||
|
position: fixed;
|
||||||
|
top: 3rem;
|
||||||
|
left: 0;
|
||||||
|
height: calc(100vh - 3rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Conteúdo girado em −90° */
|
||||||
|
.social-bar__inner .rotate {
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row-reverse;
|
||||||
|
width: 100vh;
|
||||||
|
transform: rotate(-90deg) translateX(-100%);
|
||||||
|
transform-origin: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
@supports (width: max-content) {
|
||||||
|
.social-bar__inner .rotate {
|
||||||
|
width: max-content;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.social-bar__inner .rotate > * {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.social-bar__inner .rotate > *:not(:first-child) {
|
||||||
|
margin-right: 2rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Footer ------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
.microsite-footer {
|
||||||
|
background: #16213e;
|
||||||
|
color: rgba(255, 255, 255, 0.7);
|
||||||
|
padding: 2rem 1.5rem;
|
||||||
|
margin-top: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.microsite-footer a {
|
||||||
|
color: rgba(255, 255, 255, 0.85);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsividade básica ---------------------------------------------------- */
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.microsite-main-wrapper--has-sidebar {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.microsite-header__inner {
|
||||||
|
flex-direction: column;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.microsite-header__roles {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
35
themes/site_users_microsite_theme/js/social-bar.js
Normal file
35
themes/site_users_microsite_theme/js/social-bar.js
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
/**
|
||||||
|
* @file
|
||||||
|
* Barra social — comportamento de fixação ao scroll (estilo Olivero).
|
||||||
|
*
|
||||||
|
* Aplica a classe .is-fixed ao elemento .social-bar__inner quando o cabeçalho
|
||||||
|
* do micro-site sai do viewport, no breakpoint desktop (≥ 1200px).
|
||||||
|
*/
|
||||||
|
((Drupal) => {
|
||||||
|
const socialBarInner = document.querySelector(
|
||||||
|
'[data-drupal-selector="social-bar-inner"]',
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!socialBarInner) return;
|
||||||
|
|
||||||
|
const navBreakpoint = window.matchMedia('(min-width: 1200px)');
|
||||||
|
|
||||||
|
function updateFixed(entries) {
|
||||||
|
if (!navBreakpoint.matches) {
|
||||||
|
socialBarInner.classList.remove('is-fixed');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
entries.forEach((entry) => {
|
||||||
|
socialBarInner.classList.toggle('is-fixed', entry.intersectionRatio < 1);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const header = document.querySelector('.microsite-header');
|
||||||
|
|
||||||
|
if (header) {
|
||||||
|
const observer = new IntersectionObserver(updateFixed, {
|
||||||
|
threshold: [0.999, 1],
|
||||||
|
});
|
||||||
|
observer.observe(header);
|
||||||
|
}
|
||||||
|
})(Drupal);
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
name: 'Site Users Microsite Theme'
|
||||||
|
type: theme
|
||||||
|
description: 'Tema para micro-sites pessoais de usuários. Sem navegação principal do site.'
|
||||||
|
core_version_requirement: ^10 || ^11
|
||||||
|
# Use o tema principal do site aqui se quiser herdar fontes e variáveis CSS.
|
||||||
|
# stable9 é o tema base mínimo do core (sem estilos próprios).
|
||||||
|
base theme: stable9
|
||||||
|
libraries:
|
||||||
|
- site_users_microsite_theme/global
|
||||||
|
|
||||||
|
regions:
|
||||||
|
top_bar: Top Bar
|
||||||
|
header: Header
|
||||||
|
nav: Navigation
|
||||||
|
highlighted: Highlighted
|
||||||
|
tabs: Tabs
|
||||||
|
messages: Messages
|
||||||
|
content: Content
|
||||||
|
sidebar: Sidebar
|
||||||
|
social: Social
|
||||||
|
footer: Footer
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
global:
|
||||||
|
css:
|
||||||
|
theme:
|
||||||
|
css/microsite.css: {}
|
||||||
|
js:
|
||||||
|
js/social-bar.js: {}
|
||||||
|
dependencies:
|
||||||
|
- core/drupal
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
{#
|
||||||
|
/**
|
||||||
|
* @file
|
||||||
|
* Template de página do micro-site pessoal.
|
||||||
|
*
|
||||||
|
* Renderiza apenas as regiões — a estrutura HTML completa (html, head, body)
|
||||||
|
* é responsabilidade do html.html.twig herdado do tema base (stable9).
|
||||||
|
*
|
||||||
|
* Variáveis adicionadas pelo site_users_microsite.module:
|
||||||
|
* - microsite_user: entidade UserInterface do usuário dono do micro-site.
|
||||||
|
* - microsite_user_name: nome de exibição do usuário (string).
|
||||||
|
* - microsite_user_roles: array de roles do usuário (exceto 'authenticated').
|
||||||
|
* - microsite_user_photo: render array da foto padrão (view mode 'thumbnail').
|
||||||
|
*/
|
||||||
|
#}
|
||||||
|
<div class="microsite-layout{% if page.social %} microsite-layout--has-social{% endif %}">
|
||||||
|
|
||||||
|
{% if page.social %}
|
||||||
|
<div class="social-bar">
|
||||||
|
{{ page.social }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="microsite-content-area">
|
||||||
|
|
||||||
|
{% if page.top_bar %}
|
||||||
|
<div class="microsite-top-bar" role="navigation" aria-label="{{ 'User account menu'|t }}">
|
||||||
|
{{ page.top_bar }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{# Header: blocos configurados no admin OU header padrão gerado com dados do usuário. #}
|
||||||
|
{% if page.header %}
|
||||||
|
<header class="microsite-header" role="banner">
|
||||||
|
{{ page.header }}
|
||||||
|
</header>
|
||||||
|
{% elseif microsite_user is defined %}
|
||||||
|
<header class="microsite-header microsite-header--default" role="banner">
|
||||||
|
<div class="microsite-header__inner">
|
||||||
|
{% if microsite_user_photo is defined %}
|
||||||
|
<div class="microsite-header__photo">
|
||||||
|
{{ microsite_user_photo }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<div class="microsite-header__info">
|
||||||
|
<h1 class="microsite-header__name">{{ microsite_user_name }}</h1>
|
||||||
|
{% if microsite_user_roles %}
|
||||||
|
<ul class="microsite-header__roles">
|
||||||
|
{% for role in microsite_user_roles %}
|
||||||
|
<li class="microsite-header__role">{{ role }}</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if page.nav %}
|
||||||
|
<nav class="microsite-nav" role="navigation" aria-label="{{ 'User page menu'|t }}">
|
||||||
|
{{ page.nav }}
|
||||||
|
</nav>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if page.highlighted %}
|
||||||
|
<div class="microsite-highlighted">
|
||||||
|
{{ page.highlighted }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<nav class="microsite-nav" aria-label="{{ 'User site navigation'|t }}">
|
||||||
|
{{ page.tabs }}
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
{% if page.messages %}
|
||||||
|
<div class="microsite-messages">
|
||||||
|
{{ page.messages }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="microsite-main-wrapper{% if page.sidebar %} microsite-main-wrapper--has-sidebar{% endif %}">
|
||||||
|
<main id="main-content" class="microsite-main" role="main">
|
||||||
|
{{ page.content }}
|
||||||
|
</main>
|
||||||
|
|
||||||
|
{% if page.sidebar %}
|
||||||
|
<aside class="microsite-sidebar">
|
||||||
|
{{ page.sidebar }}
|
||||||
|
</aside>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if page.footer %}
|
||||||
|
<footer class="microsite-footer">
|
||||||
|
{{ page.footer }}
|
||||||
|
</footer>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
</div>{# fim .microsite-content-area #}
|
||||||
|
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
{#
|
||||||
|
/**
|
||||||
|
* @file
|
||||||
|
* Template para a região Social — barra lateral vertical (estilo Olivero).
|
||||||
|
*
|
||||||
|
* Available variables:
|
||||||
|
* - content: The content for this region, typically blocks.
|
||||||
|
* - attributes: HTML attributes for the region <div>.
|
||||||
|
* - region: The name of the region variable as defined in the theme's
|
||||||
|
* .info.yml file.
|
||||||
|
*
|
||||||
|
* @see \Drupal\Core\Theme\ThemePreprocess::preprocessRegion()
|
||||||
|
*/
|
||||||
|
#}
|
||||||
|
|
||||||
|
<div class="social-bar__inner" data-drupal-selector="social-bar-inner">
|
||||||
|
<div class="rotate">
|
||||||
|
{{ content }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
{#
|
||||||
|
/**
|
||||||
|
* @file
|
||||||
|
* Template de menu do microsite.
|
||||||
|
*
|
||||||
|
* Gera as classes usadas pelo CSS da região nav (.menu, .menu__item,
|
||||||
|
* .menu__link), seguindo o mesmo padrão de classes do Olivero.
|
||||||
|
*/
|
||||||
|
#}
|
||||||
|
{% import _self as menus %}
|
||||||
|
{% set attributes = attributes.addClass('menu') %}
|
||||||
|
{{ menus.menu_links(items, attributes, 0) }}
|
||||||
|
|
||||||
|
{% macro menu_links(items, attributes, menu_level) %}
|
||||||
|
{% import _self as menus %}
|
||||||
|
{% if items %}
|
||||||
|
<ul{{ attributes.addClass('menu--level-' ~ (menu_level + 1)) }}>
|
||||||
|
{% set attributes = attributes.removeClass('menu--level-' ~ (menu_level + 1)) %}
|
||||||
|
{% for item in items %}
|
||||||
|
<li{{ item.attributes.addClass([
|
||||||
|
'menu__item',
|
||||||
|
'menu__item--level-' ~ (menu_level + 1),
|
||||||
|
item.in_active_trail ? 'menu__item--active-trail',
|
||||||
|
]) }}>
|
||||||
|
{{ link(item.title, item.url, {
|
||||||
|
'class': [
|
||||||
|
'menu__link',
|
||||||
|
'menu__link--level-' ~ (menu_level + 1),
|
||||||
|
item.in_active_trail ? 'menu__link--active-trail',
|
||||||
|
]
|
||||||
|
}) }}
|
||||||
|
{% if item.below %}
|
||||||
|
{{ menus.menu_links(item.below, attributes, menu_level + 1) }}
|
||||||
|
{% endif %}
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% endif %}
|
||||||
|
{% endmacro %}
|
||||||
Reference in New Issue
Block a user