feat(theme): add [roi_post_grid] shortcode for static pages

- Create PostGridShortcodeRegistrar for WordPress shortcode registration
- Implement RenderPostGridUseCase following Clean Architecture
- Add PostGridQueryBuilder for custom WP_Query construction
- Add PostGridShortcodeRenderer for HTML/CSS generation
- Register shortcode in DIContainer with proper DI
- Add shortcode usage guide in post-grid admin panel
- Fix sidebar layout: add hide_for_logged_in check to wrapper visibility

Shortcode attributes: category, tag, author, posts_per_page, columns,
show_pagination, show_thumbnail, show_excerpt, show_meta, etc.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
FrankZamora
2025-12-06 21:33:20 -06:00
parent c23dc22d76
commit 79e91f59ee
13 changed files with 1532 additions and 2 deletions

View File

@@ -0,0 +1,109 @@
<?php
declare(strict_types=1);
namespace ROITheme\Shared\Infrastructure\Query;
use ROITheme\Shared\Domain\Contracts\PostGridQueryBuilderInterface;
/**
* Implementacion de PostGridQueryBuilderInterface
*
* RESPONSABILIDAD: Construir WP_Query a partir de parametros de filtro.
* No genera HTML ni obtiene settings.
*
* @package ROITheme\Shared\Infrastructure\Query
*/
final class PostGridQueryBuilder implements PostGridQueryBuilderInterface
{
/**
* {@inheritdoc}
*/
public function build(array $params): \WP_Query
{
$args = [
'post_type' => 'post',
'post_status' => 'publish',
'posts_per_page' => (int) ($params['posts_per_page'] ?? 9),
'orderby' => $params['orderby'] ?? 'date',
'order' => $params['order'] ?? 'DESC',
'paged' => (int) ($params['paged'] ?? 1),
];
// Offset
if (!empty($params['offset'])) {
$args['offset'] = (int) $params['offset'];
}
// Filtro por categoria(s)
if (!empty($params['category'])) {
$args['category_name'] = $this->sanitizeSlugs($params['category']);
}
// Excluir categoria(s)
if (!empty($params['exclude_category'])) {
$excludeIds = $this->getCategoryIds($params['exclude_category']);
if (!empty($excludeIds)) {
$args['category__not_in'] = $excludeIds;
}
}
// Filtro por tag(s)
if (!empty($params['tag'])) {
$args['tag'] = $this->sanitizeSlugs($params['tag']);
}
// Filtro por autor
if (!empty($params['author'])) {
$author = $params['author'];
if (is_numeric($author)) {
$args['author'] = (int) $author;
} else {
$user = get_user_by('login', $author);
if ($user) {
$args['author'] = $user->ID;
}
}
}
// Excluir posts por ID
if (!empty($params['exclude_posts'])) {
$excludeIds = array_map('intval', explode(',', $params['exclude_posts']));
$excludeIds = array_filter($excludeIds, fn($id) => $id > 0);
if (!empty($excludeIds)) {
$args['post__not_in'] = $excludeIds;
}
}
return new \WP_Query($args);
}
/**
* Sanitiza slugs separados por coma
*/
private function sanitizeSlugs(string $slugs): string
{
$parts = explode(',', $slugs);
$sanitized = array_map('sanitize_title', $parts);
return implode(',', $sanitized);
}
/**
* Obtiene IDs de categorias desde slugs
*
* @return int[]
*/
private function getCategoryIds(string $slugs): array
{
$parts = explode(',', $slugs);
$ids = [];
foreach ($parts as $slug) {
$term = get_term_by('slug', trim($slug), 'category');
if ($term instanceof \WP_Term) {
$ids[] = $term->term_id;
}
}
return $ids;
}
}